diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..48ed328 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +usb1/_version.py export-subst diff --git a/.gitignore b/.gitignore index d915906..34330f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -test-libusb/ -MANIFEST -dist/ -build/ +/test-libusb/ +/MANIFEST +/dist/ +/build/ *.pyc +/.eggs/ diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..1eff7f6 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,636 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS,_version.py + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules=usb1._libusb1 + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.11 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are: text, parseable, colorized, +# json2 (improved json format), json (old json format) and msvs (visual +# studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: en (aspell), en_AU +# (aspell), en_CA (aspell), en_GB (aspell), en_US (aspell), fr (hunspell), +# fr_BE (hunspell), fr_CA (hunspell), fr_CH (hunspell), fr_FR (hunspell), fr_LU +# (hunspell), fr_MC (hunspell). +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 318e384..0000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -sudo: required -dist: trusty -language: python -python: -- "2.7" -- "3.4" -- "pypy" -addons: - apt: - packages: - - libusb-1.0-0 - - libudev-dev - - doxygen - sources: - - ubuntu-toolchain-r-test -# Note: "doxygen" actually comes from "apt-get build-deps libusb-1.0-0", but -# there is no direct travis equivalent. -before_script: -- ./buildTestLibusb.sh https://github.com/libusb/libusb.git libusb.git master v1.0.6 v1.0.20 -install: pip install . -script: python -m usb1.testUSB1 -env: -- LD_LIBRARY_PATH= -- LD_LIBRARY_PATH=test-libusb/build/libusb.git/master/lib -- LD_LIBRARY_PATH=test-libusb/build/libusb.git/v1.0.6/lib -- LD_LIBRARY_PATH=test-libusb/build/libusb.git/v1.0.19/lib diff --git a/KEYS b/KEYS new file mode 100644 index 0000000..2bb4aa5 --- /dev/null +++ b/KEYS @@ -0,0 +1,160 @@ +This file contains the PGP keys authorised to sign releases. + +NOTE: Releases from version 1.8.1 onward must be expected to be signed. +If one is not signed, please do not use it, and report it as a bug here: + https://github.com/vpelletier/python-libusb1/issues/ + +Users: + Adding/updating trusted keys: + gpg --homedir + --import KEYS + Fetching and checking a release (see https://stackoverflow.com/a/62687869): + wget + wget .asc + gpg --homedir + --verify .asc +Release maintainers: + Adding your key to this file: + LC_ALL=C; (gpg --list-sigs + && gpg --armor --export ) >> KEYS + Signing and publishing a release: + git clean -idx + python setup.py sdist check + # Check dist content before signing as desired + gpg --armor --detach-sign dist/ + twine upload dist/ dist/.asc + +pub rsa4096 2014-08-21 [SC] + 983AE8B73B9115987A923845CAC936914257B0C1 +uid [ unknown] Vincent Pelletier +sig 3 CAC936914257B0C1 2014-08-21 Vincent Pelletier +uid [ unknown] Vincent Pelletier +sig 3 CAC936914257B0C1 2014-08-21 Vincent Pelletier +sub rsa4096 2014-08-21 [E] +sig CAC936914257B0C1 2014-08-21 Vincent Pelletier +sub rsa4096 2020-07-05 [S] [expires: 2022-07-05] +sig CAC936914257B0C1 2020-07-05 Vincent Pelletier + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBFP2ZD4BEAC3jysokNBSK8cKSO3AKg8a5noHsGB8G1yMzTURUlsoYGTCVFbx +1S35xww0w2sgbJvMe8TG1hEReOVyf7GNIlDYtsEznWkib4MQQwLYVOgZE2ULxQOI +gYmULaFa97CICdzt3VvmmcbWyxyNe4JaPKinAXmS46B70ep33wVDYih/ptPvAJxO +Q+OU2AiPHcn2iEDuzP/1ey2YFnC4gLe2dG8Ri9JMf3Gxb4rOWJEf4mxHKWU9gC3i +0Ne++z5cIXRY4Hiz37t0QKVEd6nOn/niR8KyjOoIHOu2n6cv05x+5Clq+5dfltU4 +/5Lg7UZF43aLr4W/udJ6PddAQnpHjIb7poFDTZqGGRvI59vsWCcNuzI1ENX+clBq +fnbdGgfejY+cpjs1hyQGF5NduefG2A6utZXGsKqh6p8vslO2+3dDu6rlCUlhNoKH +naXEPGjhyVZyeztm5VO4qTlvw71a4+BQZ0LV/3r3tVuD3RDEIOExn0w5QNoZ9s6M +KlTWLNxLBmiH6qHKJUdaNtpZqkijEezeMh6pu6VVjzxEAWvejdvU/zMr81cVRFxt +Y3mvtBFtNphFTHj3hYdY2U9WPRgpW7rmrvRRJdoV9MZxdiVZcD+qfrBua1Qxgd4X +6xW+IOURd3o7oHX9Zlk0BUH671GQPUNeLEnAmd4gu2z2YairXW1ErK7YowARAQAB +tCpWaW5jZW50IFBlbGxldGllciA8dmluY2VudC5wbHJAd2FuYWRvby5mcj6JAjkE +EwECACMFAlP2Ze8CGwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRDKyTaR +QlewwSk9EACf53GA86mlTl7Y5iHPm0/LbieldvBT2TcqElPTj1EGWXaYqfWr05lr +1wSYCx0WWtcFP4zO7eExMsMKiRmdsxtkatrqlENb7SiRQ3kEFkiUWP9a1dFiGdlp +X/5w4yIbWtd35y8Y6GR3e0YWRQ6FFTAKFTYQR5IfyGmWE8BfLo3RDH6CbHB4o7xS +MpTYmmP4XqZgLMX8RpD9ikrvP0hB6rEUJ+chJWcNzqqAoLim91ViwOy5Fybwlp32 +jlPw722fOooTnAHDlbGffviBQQ1wcLOFtL3ODPOzAnL0Sm4rFmMghVG7qZgA+TWa +/Fo21php2FCoVrx7ONlktkEm8niO3v/k6FMxi56yaFrk33O34fxbwVc3O+5xAKFt +NfN38+DL3VuW2Fix1oA31xG+7HKVtLE03f1njjRAH7Y7oEiP77oy26IsPqmJfZfw +kMweXV2haaLDKdGrF+UhYt4NFYeCcPDsl6FxGuVy9G61l8ko2xYG5pdbsmL4k2p8 +sHChuj49lbckhSPyhnnp/+2Bcwo/cWUK1Xh0S4u+ARlKKp/nJGKsQ3GpFlATMnhX +4Mvwj/X24Its/VQi0mofXLeqonUSesj4duB7XDHLGy9cmRb4NSv6HEW9mrPaB8Xh +b9Ws7tIEFC7xcx9z4qUTb8Czjpk5riJ9Jd/ye/hoa2ttqNS+fenlM7QwVmluY2Vu +dCBQZWxsZXRpZXIgPHBlbGxldGllci52aW5jZW50QHdhbmFkb28uZnI+iQJEBDAB +CAAuBQJXDYjJJx0gTWFpbCBhY2NvdW50IHRlcm1pbmF0ZWQgb24gMjAxNi8wMy8y +MAAKCRDKyTaRQlewwaNXD/4ivgizOg2aslmTQXDze8nvqY07EuCh1kLHv4w/R67F +EkxRlBnxgW47elyYiLxAnDtZ/rR9oaNHWs/MR2bHrxfT7sG1+oYds92/hqEFrU71 +VfLjGKcqMUv7HBVm1NlYphhl2pBMiTYihWMfVGDsMth8lbdjaV0lEksbL5WiHWlf +sab6QetmK3voK6QnP3xqKhyJBNTjGKb3qThU+c4EI57QUqvLm4bGrYNp6m+/6VIv +MbqUAT1nScWfDYGwTHmEr3caGH0ACJ/tMoCeqgFRVx0/llbnLHlCoAOs/k3UtTDk +ltWPVr+QWFDNDIrCeZu/PQbW4HbwEwEdZmJvuxE0waMCr+gdSTXMe05ELen75EGu +u2CaG3weHhIUVmbp4IHc+dP6kin2GxoxThZk4QgjAvhtxZFJhpd4H36E8UsxMLvH +FNgIdMI6RHj3OPoQM21TmTZYrWjVSojmAaiPn64ktzh6CeyNZwu73ilFRJrqwInL +cnm36BZtfNwXAJmDDah+e3wBr1SL+pF8nEHX/nZTtnHFl5KFY8VD8oDqu1HmzNak +2Yt9ZC8yJmuq4vXKNzbmHqiWGsgtRPUGBN1/NaPT+TXkTBPGTnYUdBdX/8PP6wGI +ssdg20Ndp57jRcP/InjvDElTjpzxfaBHvwPkWi3E2fE9AP4m19D+ONzJxjAYM0fT +1IkCOQQTAQIAIwUCU/ZmCwIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAAAoJ +EMrJNpFCV7DBLWsP/0sjHMiNR+Si4b9p3svyP14jpvWqqLm9K/k2F/E8L4p1lI4T +XuvQ6yGxfj3nt4RePFbzO2QYfH/hK7I1Pv3IJTlzfKDLA3X7SQW19x6LIr1l8kv/ +/xPLOF+Ti3JPN8IHHdGC4limDnGM7Ye+568ZPu3MHg1Wm6qCaF/mQANAmEfIrV7T +eEEudkBkh6gyIk0R1+Nfk6uqbz4WarPEb6BZkEKOQYuXnnF7VJjhxy9tu7dtyyCr +X+eN6A57i4cUxsoqVg4gHThTEkC2zAemfjcpbVFDICTYN136HM89jjljueWBJZZf +19ZC9MW6iWqiTC7RBRMBYE9FasjM1xDHt2wnIrFNtYadYbke7pCB3v5KdA8U5bIC +3PTtUS4rvjktpg4bXTBaKRyEwzrfofSJubZq23RbMDG7lB6UKxSC/5S1V3vVbBLp +6kHONSDfuwIzy6uAc7TM6bUNHEatKJ36/6Nhtn7D2mfVYIUDQStkJsZbqTTSnilh +QJcUjuvyD5o8robeDbM+D5CIJVsWUGLj7E9bDwVQpmp65KrwOLVOsyAhtcmzGqtJ +PloqHGdDCPVDSu8St3skZmL7jlEuGiNjJ825Q6gPLbZjjJVSBGQGp+49HTcTrv5A +P6PbXNoILAEGfq0l/hdcD71LubHHzB0IXfRIc1CZ5zQ+eR3gIa9ZhS4wBolptClW +aW5jZW50IFBlbGxldGllciA8cGxyLnZpbmNlbnRAZ21haWwuY29tPokCOAQTAQIA +IgUCU/ZkPgIbAwYLCQgHAwIGFQgCCQoLBBYCAwECHgECF4AACgkQysk2kUJXsMG/ +eA//f0ErMyZE5wQotWBbj+SpHUudlbwcU2oPOduSFnYGptM42DspLlkdUz6df2Yj +j3DUnfz7DNp9Y18W5N2VCI1x87O9iJTPtHTVHdkBflZ1MnEUxcu6+0UmjnSrfrO7 +G0h29I7tdxnDWFHU7yQ539jjH9VQ8fy9rm1YBMxxYLYgkht4iho1J6AjzNf5bT48 +vdJWOA1KuA0I18IllvujjR4r02iuFHNRIJaeCG8Rt++l6ZLc7rWiyeIBaPEenKU3 +L4QYRwaiQIzKFRi3Uc7Sf01as0F9oXbIJX3w21J1NF1LHS5kRw8J8Lp+6Ac1fXoq +fVo6HGR/JSlXaJaP+1Srq+vbcj49GXLRyBrClPdAAIiw8MnaCtSW5xulpqWVssd1 +BTmr3+1/sP7Lkzd4K5uxdD6Q7xuVDCut5ofNK8EPXmnT7O/UtDITPpyvvN91DIP0 +8b8O3RavzdBXB4dw0BtDMevOEAnNXEWqIOMJqv/tMatLbwRWiGP9tnwDUtTA4phU +/vBvI4+VjI7NXcP+zGTPEUABqjiNOgfBEJ8pqjH50C0bT87v5Kje2s4++Phlvf1v +9kN9CSC4nnxCilBiCVzt5O+2HRDYECylDVjKfFVEBQLCntxT6CiNDeQ3j8vnX6z6 +KqtdGhnvBEp+eg/yQWXmAoz3u7yaw0wFWPSUQBYfrQufdxu5Ag0EU/ZkPgEQANnn +M4q1wg4jQkpYFWU/1olm1vtvOabX/EBSM6KTRbAYDYpxyrqks0tnvT7ppFL6i5dK +hBGyWXJJ+Yc66SPkdGtVfkpFQbXY3h1DN9W1A7I868M97KsA0GjEe8aC0qqQ/+WL +c3JrD9rezn5K4E4RRR1Si1KBW9hFi8m2/5hIvOOC0MvpumJIOdhrq0dhwYoytTmJ +2ah8QJzlPf/4gdVSgHRIfOF/8KJkDQni+hWnBgdCA/RDzyrPZbj2owhBim/V68D1 +giPnnyWvmDPkBwHxMcVWCpWHwNTyuLPQL8jFTYlMewbM/Uv4I8+BwrpB4LC/kXAs +Ib3eY6uPHK6EcsMi4f4h7nR55Tcl4FBtWytN2E+/7c03ZoOI8l7ihwX6ZyY0H71U +KQcg/WMAtoNZAe2XfIC2Wu60JJ4F4n9UmN7m48geZq7lXU3uHd+tmCm3Mc62ifIf +uYzke0kxEEClA3I5t/PeSPWbE2+YwinsTSQemowWV63gNPbvyXMkAFf6GIYMnPRG +Z4S2BgjOKdvPXuAipAkaknmBHu9KQYd96UP5flyhSm0OsdurlotlV8sFnQ/RQ+sz +bOVo8EBSZXkCDsb44i+evHAG7a7Z6Uua3APh58PfskrvvgEFvUw69ZGnNMGXicA6 +3OuujsjUiFof6UIB8PHH5N1rDPA3E0U0FQpl1KjzABEBAAGJAh8EGAECAAkFAlP2 +ZD4CGwwACgkQysk2kUJXsMGJrw/8DIWj4BUMjqYSaS71O5k9NWp/Wkh/RNuugLOk +pZID4nCgK2j/MThzZsuVFDA5vLFVAcqvnJLsDdmjV5ZyddRsDLzbJEGc2M+0sKjU +OWzJNE3BitkD9iuisx6uZdF2s8ZqWE0fSjLewLeu72O79kMRgyLeLHBhoL80K9Rf +g71DuVSKIxjOfAcmRgKJ8qYK6xLIa12NpGuNsJnjuo38mi82Yf7c3ZViXllkaide +k0Wv7OEYAaMR+fjy7Xcwj5tD+F1UowTw3rcRt+LiGks3UHfLoEI+xCw872ny2qHE +linjpPn00WxKfG5HvjLDtDNJFK5n2MDXqLNEIAfPSRr8RwS/8NwVPguCxKL0ZmVM +cwG4xHBVpX52mxMts9r8929wH7MUy75RnuAEM7XDGctxi8xcbml5K7+sGpu6HjUK +pcM18xZIyMMPtc31X4e62iCZJW+bwPMdPD4gN4Mf1f1RVvpk8QCNXNZAFp3vVLRx +Wigs0eQoRoupHnZNTUKasgVb9ysLMhYPgp2P1FjB8poSVfKpI0YI6q6lrXvWpSU8 +GzDJpLUnJ+nIlIM1FTslGj43BMl/YIXT/NqWj3hrfHBLOb1Rj4avZsxC+aktiSRo +qNoVeNgzr51TdJ652lOp9CJrH1Tpk53W0lomr3X92qLU4RVkKIj9AxewGSmoNvAA +mkkHHaa5Ag0EXwGWywEQALkAlfB2IP00GpuhZC9bblcjY/UK37YowYWYF0AV3uMA +eG1RBQFUB05+b5BX1HmTn7NQwujHJYNvc5y71lijee+u3VizDOx3T/H+iXV9PqEb +BJl5xJ2Mc87QTSA/yHGzZblRz2rHUeCB2WdjC9hezLyrGa/5SiLfBF2sTopfN/p4 +PYvYz9lCJuz5u+HwAZ4mRE6EPfgH74SJWY+qjVYD53z50weMo8zsxoko8mGxKIfx +GTtmBdHqIU5s16QzlsUX5HUkN4AwuleN0DWGY8EzdOdmEsRkRNXZ7Mb5nCXhfShE +Ehbz0dSU1VFzyFMk8AdCfk9cwpzwyEDw+A4HcMsWdle8Rm2aPslGsPhryiTm7MR4 +uHIY2WuJUpPVeqLezX7Jb6snY97RDBisUIB0srZ9hxnjfNJIZLb4ewmG5k2aLqZs +FdIiMj5fh+ibEFTo2qtf10KwXhZ1MVMeOZql95svUzv75uktkeQZCBs98H5sT+dX +qT34jqyxBA9kkGlFaSu+B02AmBQ9LHmfWa1nbLB1A4h2rMxaL4rB1/FDKDz3oO8C +t1KKRL2LrxYlOBiCCUsO4Tmqf+9Botq/YVphYUFSaaodxuKJCRL57X/RTTigqzw3 +xqZaYtZykYDFrPfEwGzoDgBwk0xr1wpfkXK7fonilwP0enI+a/YVuJlpFePM97rH +ABEBAAGJBHIEGAEKACYWIQSYOui3O5EVmHqSOEXKyTaRQlewwQUCXwGWywIbAgUJ +A8JnAAJACRDKyTaRQlewwcF0IAQZAQoAHRYhBPGudqxMr0EJfTJqfdbSuAkYG8jA +BQJfAZbLAAoJENbSuAkYG8jA5ecP/ivaQaN4bC+fMMa0T/0RKiXF+ErP4cgHV/l5 +3PG36aluFuQVhx1td6Zbz77ivXDqVLMxxld/Jl+odzsAXc9UI3QBAAeiarnGVtYz +Ig7SFgBc51Jd1qtoWX9XtN+mjH845Y+NHfQFlAfHt6264zmg7350ZVAWl6E4Q2vv +cbtHBgfsXfqCHeuTT6uQ8RMQPClwkOUbUrYo6RnIf9BwMHjfXisyfAVPrvBGpTCS +nw1d0gzPp+7S0ByKOLvnBUnYy9t/M11Ebswmo4VbKSeC4zu6o77bdsFKBAofn23+ +lYTOYGHF+qtw+TIWsAr+VuT5jZdudU2yBA/UTUKsXAemAzx5gDbiV0mEVMCJmIKB +7RU1sRqZ5LtjY3vMVemwv+D6Y7XrkcLMw5nz6hxB4I2Hhv3XtPy0fHi7X6xjPnQp +fKyRCNFtIIUfDw2TYurTrcsNw7xDHJpGgslXCU3wPWyDMKktWAQoiyMXpQRCOvxm +HtkwfHAjcBivH8KyOCJhZSG8xgtZhvB//GQX7SapqOUWiDFGtGaot02WZ689NI9s +c1NASLCNRLcBt13hoAKKIvqcxAsP71XYAptMqCXBhYR05kJfeW/SBdNN6iWgTH4D +CyLxsEoaj0Vm7tFjtj+PhkrbXxh+Qrvmr22UmG61IzoVrrWHEfoGNBC6bSsYUHsI +EJZ+9CjqJiwP/23IMwmYHl2uBvM44WaseO3nzNblMW7iO6i43HDwnAUT916BUt17 +Xgt4MjE0XvgcXvAzNNz8+NBPTHPYDVXf8zxzTI1oIsjjeqj/wgGVG52T7KyDgFg1 +6ZkqsDDSoxCko9O9pMePIdS8PgSi68B9uUjVN3RdRrMcm1H7ltY98tmfOe6BxIy0 +0ru9PvjnSqgQe7FN3vGzWou/xO8CeGQb+dsP2H3fOdWXdmNlXu5wxkI2KTgB8zMs +ibx0pU8xGNkvyJMvHxLUnW2BZaZizwXaQRD2e95LQ2AR3mleYS7qRgtrySA7lQih +52KH8s9OzZa6/EPq0oLQwK7qGxyuLfd4nsCFPt99TOFwRGMsosMmf1Uzvf4Gcmk6 +wkxvdWWF43VC7BqV0wvsUpahmtiwzeSNJEaSgj0ytSDdrIl4FVVisVdKCv89/khV +C44FzlML1ZX+cyVzT9UTH7C05lmOVBrCJS8ANR8hchQVCPQ1NspsvXabDr/kB84S +lxr7zkFA0QPH7dNHEaeVPU3m2fmlSqVN2L0rsBkqrzNU0HKFSTiro07Z43suEYxp +1EujEMTYRidAvym1qJ0d4lFXd3x6SU/OI0HscaeT+VYtDScV5cgpYNwP3zr7Or1R +ck5wEliBlN6LBIq1AhYn8fQdM2jQIVWF4xqF6FNKF6/exyAg9nsXhv4b +=52Ev +-----END PGP PUBLIC KEY BLOCK----- diff --git a/MANIFEST.in b/MANIFEST.in index 93d005e..1cf7a3b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,8 @@ include COPYING include COPYING.LESSER include examples/README include examples/*.py -include testUSB1.py +include usb1/testUSB1.py include stdeb.cfg +include versioneer.py +include usb1/_version.py +include usb1/*.dll diff --git a/README.rst b/README.rst index 60bdb86..39aea49 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -Pure-python wrapper for libusb-1.0 +.. contents:: Supports all transfer types, both in synchronous and asynchronous mode. @@ -15,9 +15,8 @@ PyPI: http://pypi.python.org/pypi/libusb1 Dependencies ============ -- CPython_ 2.7+ or 3.4+, pypy_ 2.0+. Older versions may work, but are not +- CPython_ 3.6+, pypy_ 2.0+. Older versions may work, but are not recommended as there is no automated regression testing set up for them. - - libusb-1.0_ Supported OSes @@ -26,13 +25,9 @@ Supported OSes python-libusb1 can be expected to work on: - GNU/Linux - - Windows [#]_ native dll or via Cygwin_ - - OSX [#]_ via MacPorts_, Fink_ or Homebrew_ - - FreeBSD (including Debian GNU/kFreeBSD) - - OpenBSD .. [#] Beware of libusb-win32, which implements 0.1 API, not 1.0 . @@ -47,15 +42,35 @@ Releases from PyPI, with name *libusb1*. Installing from command line:: $ pip install libusb1 -or:: - - $ easy_install libusb1 - Latest version from source tree:: $ git clone https://github.com/vpelletier/python-libusb1.git $ cd python-libusb1 - $ python setup.py install + $ pip install . + +Windows installation notes +-------------------------- + +On Windows, installing wheels from pypi also installs the libusb dll within the +usb1 python module. It does not install any driver, so you still need to decide +which of libusbk or WinUSB to use for each device and install it appropriately +(possibly using Zadig_, or by providing a driver for your users to install). + +Installing from source tree does not install the dll, so you need to install the +library where ctypes can find it - and of course the driver as well. + +Checking release file signature +------------------------------- + +pipy releases are signed. To verify the signature: + +- download the release file, note its URL +- download its detached signature by adding `.asc` at the end of the release + file URL +- add the release key(s) to a gnupg keyring (`KEYS` file in the home + repository), and use gnupg to validate the signature both corresponds to the + distribution file and is trusted by your keyring +- install the already-fetched release file Usage ===== @@ -94,14 +109,14 @@ Asynchronous I/O, with more error handling: # This example does not resubmit transfers on errors. You may want # to resubmit in some cases (timeout, ...). return - data = handle.getBuffer()[:transfer.getActualLength()] + data = transfer.getBuffer()[:transfer.getActualLength()] # Process data... # Resubmit transfer once data is processed. transfer.submit() # Build a list of transfer objects and submit them to prime the pump. transfer_list = [] - for _ in xrange(TRANSFER_COUNT): + for _ in range(TRANSFER_COUNT): transfer = handle.getTransfer() transfer.setBulk( usb1.ENDPOINT_IN | ENDPOINT, @@ -111,7 +126,7 @@ Asynchronous I/O, with more error handling: transfer.submit() transfer_list.append(transfer) # Loop as long as there is at least one submitted transfer. - while any(x.isSubmitted() for x in reader_list): + while any(x.isSubmitted() for x in transfer_list): try: context.handleEvents() except usb1.USBErrorInterrupted: @@ -133,7 +148,7 @@ Some description is needed though on how to jump from libusb-1.0 documentation to python-libusb1, and vice-versa: ``usb1`` module groups libusb-1.0 functions as class methods. The first -parameter (when it's a ``libusb_...`` pointer) defined the class the fonction +parameter (when it's a ``libusb_...`` pointer) defined the class the function belongs to. For example: - :c_code:`int libusb_init (libusb_context **context)` becomes USBContext class @@ -422,6 +437,7 @@ Expose bare string descriptors (aka string indexes) on USBDevice. get{,ASCII}StringDescriptor now return None for descriptor 0 instead of raising UnicodeDecodeError. Use getSupportedLanguageList to access it. + Moved getManufacturer, getProduct and getSerialNumber to USBDeviceHandle. Kept shortcuts for these on USBDevice. @@ -430,11 +446,154 @@ shortcuts for these on USBDevice. get{,ASCII}StringDescriptor now return None for descriptor 0, use getSupportedLanguageList to get its content. + getManufacturer, getProduct and getSerialNumber are now on USBDeviceHandle, with backward-compatibility aliases on their original location. + Synchronous bulk and interrupt API exposes number of bytes sent and received bytes even when a timeout occurs. +1.7.1 +----- + +usb1.__version__ is now present, managed by versioneer. + +Fix an occasional segfault when closing a transfer from inside its callback +function. + +1.8 +--- + +Fix getExtra and libusb1.libusb_control_transfer_get_data . + +Fix getMaxPower unit on SuperSpeed devices. + +1.8.1 +----- + +Release process rework: + +- embed libusb1 dll for easier deployment on Windows +- cryptographically signed releases + +Use libusb_free_pollfds whenever available (libusb1>=1.0.20). + +Fix hotplug callback destruction at context teardown. + +Drop remnants of python 2.6 support code. + +1.9 +--- + +Drop USBPollerThread and deprecate libusb-lock-related USBContext API. + +1.9.1 +----- + +Fix installation from pypi source tarball, broken in 1.8.1 . + +1.9.2 +----- + +Windows wheels: Update bundled libusb to 1.0.24 . + +Fix soure-only build when wheel is not available. + +1.9.3 +----- + +Add support for pyinstaller. + +Improve the way the windows dlls are embedded in wheels. + +Fix support for python 3.10 . + +Add support for homebrew on Apple M1. + +1.10.1 (yanked) +--------------- + +NOTE: Release yanked_ from pypi and re-released as 2.0.0. + +2.0.0 +----- + +Drop python <3.4 support. + +Do not load the C library on import. Allows applications to customise the +lookup logic (see `usb1.loadLibrary`). + +Add LIBUSB_SPEED_SUPER_PLUS. + +Better control device iterator end of life. + +Fix objects escaping control from their parent. + +2.0.1 +----- + +Fix a TypeError exception in USBContext.handleEvents . + +Fix an AttributeError exception in USBContext.hotplugRegisterCallback . + +Fix segfault in pypy3 when finalizing USBDevice objects . + +Source only: convert examples to python3. + +Release process: also run some examples scripts. + +3.0.0 +----- + +Update versioneer to be compatible with 3.11 . + +Drop python <3.6 support (consequence of versioneer update), hence the major +version change. + +3.1.0 +----- + +Fix bug preventing use of setPollFDNotifiers. + +Wrap libusb_interrupt_event_handler, available since libusb 1.0.21, to help +applications wake an event handling thread (ex: durring exit). + +Windows wheels: Update bundled libusb dll to 1.0.26 (warning: libusb1 7z archive changed since this release, and reports mention the DLLs were of 1.0.25) . + +3.2.0 +----- + +Fix callbacks on 32bits Windows. + +Try loading libusb1.so.0 in addition to libusb1.so on \*nix. + +Bundle libusb1 dll 1.0.27 in Windows wheels. + +Expose more modern libusb1 API: +- libusb_init_context +- libusb_set_log_cb +- libusb_setlocale + libusb_strerror +- libusb_wrap_sys_device +- transfer flags LIBUSB_TRANSFER_SHORT_NOT_OK and LIBUSB_TRANSFER_ADD_ZERO_PACKET + +As a result, python-libusb1 should now be usable on Android. + +Fix tests on python 3.13 . + +3.3.0 +----- + +Improve deprecation warning, by showing the correct caller location. + +Fix finalizer registration errors on pypi by using a thread-safe sequential number generator instead of relying on object id unicity and timely finalizer triggering. + +Check in-dll version when creating Windows wheels. Should hopefully version mixups like what happened in 3.1.0 . + +3.3.1 +----- + +Fix ``AttributeError`` in ``USBContext.setPollFDNotifiers`` introduced in 3.3.0 . + .. _CPython: http://www.python.org/ .. _pypy: http://pypy.org/ @@ -450,3 +609,7 @@ bytes even when a timeout occurs. .. _libusb-1.0: https://github.com/libusb/libusb/wiki/ .. _libusb1.0 documentation: http://libusb.sourceforge.net/api-1.0/ + +.. _Zadig: https://zadig.akeo.ie/ + +.. _yanked: https://www.python.org/dev/peps/pep-0592/ diff --git a/buildTestLibusb.sh b/buildTestLibusb.sh deleted file mode 100755 index b2aec5c..0000000 --- a/buildTestLibusb.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh -# Build several libusb versions to run tests against. -# Useful to check backward-compatibility with libusb versions which lack some -# exports. -if [ $# -lt 3 ]; then - echo "Usage: $0 remote remte_name changeset [changeset [...]]" - exit 1 -fi - -set -e - -BASE="$PWD/test-libusb" -BUILD_BASE="$BASE/build" - -REMOTE="$1" -REMOTE_NAME="$2" -shift 2 -REPO_DIR="$BASE/repo/$REMOTE_NAME" -mkdir -p "$REPO_DIR" -git clone -n "$REMOTE" "$REPO_DIR" -while [ $# -ne 0 ]; do - CHANGESET="$1" - shift - BUILD_DIR="$BUILD_BASE/$REMOTE_NAME/$CHANGESET" - mkdir -p "$BUILD_DIR" - cd "$REPO_DIR" - git checkout --force "$CHANGESET" - git clean --force -dx - ./autogen.sh --prefix="$BUILD_DIR" - make - make install -done diff --git a/examples/hotplug.py b/examples/hotplug.py index fc14ba2..6dbc1ed 100755 --- a/examples/hotplug.py +++ b/examples/hotplug.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2013-2018 Vincent Pelletier +# Copyright (C) 2013-2021 Vincent Pelletier # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -from __future__ import print_function + import usb1 def hotplug_callback(context, device, event): diff --git a/examples/hotplug_advanced.py b/examples/hotplug_advanced.py index 20fe223..6f57ea1 100755 --- a/examples/hotplug_advanced.py +++ b/examples/hotplug_advanced.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2013-2018 Vincent Pelletier +# Copyright (C) 2013-2021 Vincent Pelletier # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -18,7 +18,7 @@ Advanced hotplug examples. Presents ways of integrating hotplug into your userland USB driver. """ -from __future__ import print_function + import select import sys import usb1 @@ -58,12 +58,12 @@ def poll(self, timeout=None): for fd_list, happened_flag in zip( select.select(*([[ fd - for fd, events in self._fd_dict.iteritems() if events & flag + for fd, events in self._fd_dict.items() if events & flag ] for flag in flag_list] + [timeout])), flag_list, ): result[fd] = result.get(fd, 0) | happened_flag - return result.items() + return list(result.items()) # (end of demonstration helpers) class AwesomeDevice(object): @@ -227,7 +227,7 @@ def main(): ) try: mode() - except NoHotplugSupport, exc: + except NoHotplugSupport as exc: print(exc.value) sys.exit(1) except (KeyboardInterrupt, SystemExit): diff --git a/examples/listdevs.py b/examples/listdevs.py index 48f6c57..ae2a218 100755 --- a/examples/listdevs.py +++ b/examples/listdevs.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -# Copyright (C) 2013-2018 Vincent Pelletier +# Copyright (C) 2013-2021 Vincent Pelletier # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -from __future__ import print_function + import usb1 def main(): diff --git a/examples/scan_device_tree.py b/examples/scan_device_tree.py new file mode 100644 index 0000000..bed7bfa --- /dev/null +++ b/examples/scan_device_tree.py @@ -0,0 +1,75 @@ +""" +python-libusb1 device tree scan example. +This program shows the entire tree of devices/configurations/interfaces/endpoints +that are available on your computer. It should be useful when interfacing with a +new USB device (since you can easily examine what features are available on the device). +""" + +import usb1 + +def scan_device_tree(): + with usb1.USBContext() as context: + for dev in context.getDeviceIterator(skip_on_error=True): + dev: usb1.USBDevice + + try: + print( + f"- Device: {dev.getVendorID():04x}:{dev.getProductID():04x}, {dev.getManufacturer()} {dev.getProduct()}, SerNo: {dev.getSerialNumber()}" + ) + except usb1.USBError: + print( + f"- Device: {dev.getVendorID():04x}:{dev.getProductID():04x} " + ) + + for cfg in dev: + # Note: for docs on USBConfiguration, see here: https://libusb.sourceforge.io/api-1.0/structlibusb__config__descriptor.html + # Also see https://www.beyondlogic.org/usbnutshell/usb5.shtml#ConfigurationDescriptors + cfg: usb1.USBConfiguration + print( + f"---> Cfg: Num Interfaces: {cfg.getNumInterfaces()}, Identifier: {cfg.getConfigurationValue()}, " + f"Attributes: 0x{cfg.getAttributes():02x}" + ) + + for iface_idx, iface in enumerate(cfg): + iface: usb1.USBInterface + + print(f" ---> Interface {iface_idx}") + + for altsetting_idx, altsetting in enumerate(iface): + altsetting: usb1.USBInterfaceSetting + + # The docs for USBInterfaceSetting can be seen here: + # https://libusb.sourceforge.io/api-1.0/structlibusb__interface__descriptor.html + # For an enumeration of defined class, subclass, and protocol values, see here: + # https://www.usb.org/defined-class-codes + + print( + f" ---> Alternate settings {altsetting_idx}: Num Endpoints: {altsetting.getNumEndpoints()}, " + f"Class and SubClass: (0x{altsetting.getClass():02x}, 0x{altsetting.getSubClass():02x}), " + f"Protocol: {altsetting.getProtocol()}" + ) + + for endpoint in altsetting: + endpoint: usb1.USBEndpoint + + # The docs for USBEndpoint can be seen here: + # https://libusb.sourceforge.io/api-1.0/structlibusb__endpoint__descriptor.html#a111d087a09cbeded8e15eda9127e23d2 + + # Process attributes field + if endpoint.getAttributes() & 3 == 0: + ep_type = "Control" + elif endpoint.getAttributes() & 3 == 1: + ep_type = "Isochronous" + elif endpoint.getAttributes() & 3 == 2: + ep_type = "Bulk" + else: + ep_type = "Interrupt" + + print( + f" ---> Endpoint 0x{endpoint.getAddress():02x}: Direction: " + f"{'Dev-To-Host' if endpoint.getAddress() & 0x80 != 0 else 'Host-To-Dev'}, Type: {ep_type}" + ) + + +if __name__ == '__main__': + scan_device_tree() diff --git a/pylint.sh b/pylint.sh deleted file mode 100755 index 8d01ae7..0000000 --- a/pylint.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -pylint --disable=locally-disabled,locally-enabled,too-many-lines "$@" diff --git a/runTestLibusb.sh b/runTestLibusb.sh new file mode 100755 index 0000000..190820c --- /dev/null +++ b/runTestLibusb.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# Run tests against multiple libusb versions. +# Useful to check backward-compatibility with libusb versions which lack some +# exports. +set -eu + +if [ $# -lt 4 ]; then + echo "Usage: $0 python remote remote_name changeset [changeset [...]]" + exit 1 +fi +python="$1" +remote="$2" +remote_name="$3" +shift 3 + +if [ "x$python" = "x" ]; then + echo " argument must not be empty" + exit 1 +fi + +python_libusb1="$(dirname "$(realpath "$0")")" +base="${python_libusb1}/test-libusb" +venv_dir="${base}/$(basename "$python")" +build_base="${base}/build" +repo_dir="${base}/repo/${remote_name}" + +test -e "$venv_dir" && rm -r "$venv_dir" +virtualenv --python "$python" "$venv_dir" +"${venv_dir}/bin/pip" install "$python_libusb1" + +if [ -e "$repo_dir" ]; then + git -C "$repo_dir" fetch +else + git clone --no-checkout "$remote" "$repo_dir" +fi +cd "$repo_dir" +# Also test against system-installed libusb +lib_dir_list=("") +# Build all first, test later, so errors are all visible at the end +while [ $# -ne 0 ]; do + changeset="$1" + shift + build_dir="${build_base}/${remote_name}/${changeset}" + if [ ! -e "$build_dir" ]; then + mkdir -p "$build_dir" + git checkout --force "$changeset" + git clean --force -dx + ./autogen.sh --prefix="$build_dir" + make + make install + fi + lib_dir_list+=("${build_dir}/lib") +done + +result=0 +venv_python="${venv_dir}/bin/python" +for lib_dir in "${lib_dir_list[@]}"; do + export LD_LIBRARY_PATH="${lib_dir}" + if "$venv_python" -m usb1.testUSB1; then + : + else + echo "usb1.testUSB1 failed with ${lib_dir}: status=$?" + result=1 + fi + if "$venv_python" "${python_libusb1}/examples/listdevs.py"; then + : + else + echo "examples/listdevs.py failed with ${lib_dir}: status=$?" + result=1 + fi + if timeout --preserve-status --signal INT 1 "$venv_python" "${python_libusb1}/examples/hotplug.py"; then + : + else + echo "examples/hotplug.py failed with ${lib_dir}: status=$?" + result=1 + fi +done +exit $result diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..a1d83b6 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,41 @@ +[metadata] +name = libusb1 +url = https://github.com/vpelletier/python-libusb1 +author = Vincent Pelletier +author_email = plr.vincent@gmail.com +classifiers = + Intended Audience :: Developers + License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) + Operating System :: OS Independent + Programming Language :: Python :: 3 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy + Topic :: Software Development :: Libraries + Topic :: System :: Hardware :: Hardware Drivers +license = LGPLv2.1+ +description = Pure-python wrapper for libusb-1.0 +long_description = file: README.rst +long_description_content_type = text/x-rst +keywords = usb, libusb +platforms = any + +[options] +packages = find: +# For backward compatibility with python-libusb1 < 1.6 +py_modules = libusb1 + +[options.package_data] +usb1 = libusb-1.0.dll + +[options.entry_points] +pyinstaller40 = + hook-dirs = usb1.__pyinstaller:get_hook_dirs + tests = usb1.__pyinstaller:get_PyInstaller_tests + +[versioneer] +VCS = git +style = pep440 +versionfile_source = usb1/_version.py +versionfile_build = usb1/_version.py +tag_prefix = +parentdir_prefix = usb1- diff --git a/setup.py b/setup.py index 1d902db..737b73e 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2018 Vincent Pelletier +# Copyright (C) 2010-2021 Vincent Pelletier # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -13,52 +13,171 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + from setuptools import setup +from setuptools import Command from codecs import open +import csv +import hashlib +from html.parser import HTMLParser import os +import subprocess import sys +from urllib.parse import urlsplit +from urllib.request import urlopen +import versioneer + +if os.getenv('I_KNOW_HOW_TO_RELEASE_PYTHON_LIBUSB1') != '1' and any( + x in sys.argv for x in ('sdist', 'upload') +): + print('Use setup.sh to build') + sys.exit(1) + +CURRENT_WINDOWS_7Z_SHA256 = ( + 'ce633f98c3ce6aec82be3c0d273ab8bdd160642174faba4eb47d8dcd92af7f05' +) +CURRENT_DLL_VERSION = '1.0.28.11946' + +cmdclass = versioneer.get_cmdclass() +class upload(Command): + """ + Declaw "setup.py upload". + """ + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass -long_description = open( - os.path.join(os.path.dirname(__file__), 'README.rst'), - encoding='utf8', -).read() + def run(self): + print('This project uses signed releases. See KEYS for instructions.') + print('Hint:') + print(' twine upload dist/ dist/.asc') + sys.exit(1) +cmdclass['upload'] = upload -try: - next -except NameError: - # "next" builtin missing < 2.6 - next = lambda x: x.next() +class update_libusb(Command): + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + class WindowsBinariesArchiveLinkFinder(HTMLParser): + found = None + __a_href = None + def handle_starttag(self, tag, attrs): + if tag == 'a': + assert self.__a_href is None, repr(self.__a_href) + self.__a_href = dict(attrs).get('href') + + def handle_endtag(self, tag): + if tag == 'a': + self.__a_href = None + + def handle_data(self, data): + if self.__a_href is not None and data == 'Latest Windows Binaries': + assert self.found is None, repr(self.found) + self.found = self.__a_href + + def run(self): + finder = self.WindowsBinariesArchiveLinkFinder() + finder.feed(urlopen('https://libusb.info/').read().decode('utf-8')) + finder.close() + url = finder.found + if url is None: + raise ValueError('Failed to locate current windows binary release') + if not url.endswith('.7z'): + raise ValueError('unexpected extension: %r' % (url, )) + build_dir = os.path.join(os.path.dirname(__file__), 'build') + download_cache_path = os.path.join(build_dir, 'download-cache') + if not os.path.exists(download_cache_path): + os.makedirs(download_cache_path) + url_basename = urlsplit(url).path.rsplit('/', 1)[-1] + archive_path = os.path.join(download_cache_path, url_basename) + if not os.path.exists(archive_path): + for suffix in ('', '.asc'): + with open(archive_path + suffix, 'wb') as archive_file: + archive_file.write(urlopen(url + suffix).read()) + # to build/update trustedkeys-libusb.kbx: + # gpg --no-default-keyring --keyring trustedkeys-libusb.kbx --receive-keys ... + subprocess.check_call( + [ + 'gpgv', + '--keyring', 'trustedkeys-libusb.kbx', + archive_path + '.asc', archive_path, + ], + # gnupg will not shut its pie hole. + stderr=subprocess.DEVNULL, + close_fds=True, + ) + # This check is for the maintainer to notice a new release, and + # to retrospectively confirm that a release was done with files + # from a certain archive (and not just any signed release). + # It is *not* to check file authenticity (for this, we have gpg). + with open(archive_path, 'rb') as archive_file: + archive_sha256 = hashlib.sha256(archive_file.read()).hexdigest() + if archive_sha256 != CURRENT_WINDOWS_7Z_SHA256: + raise ValueError( + 'Windows release sha56 mismatch: %r fetched with a sha256 of %r' % ( + url, + archive_sha256, + ) + ) + for arch_path, out_dir in ( + ('VS2019/MS32/dll/libusb-1.0.dll', os.path.join(build_dir, 'win32')), + ('VS2019/MS64/dll/libusb-1.0.dll', os.path.join(build_dir, 'win_amd64')), + ): + subprocess.check_call( + [ + '7z', 'e', '-aoa', + '-o' + out_dir, + archive_path, + arch_path, + ], + # 7z will not shut its pie hole. + stdout=subprocess.DEVNULL, + close_fds=True, + ) + out_dll = os.path.join(out_dir, 'libusb-1.0.dll') + assert os.path.exists(out_dll) + peres_stdout = subprocess.run( + [ + 'peres', + '--format', 'csv', + '-v', + out_dll, + ], + check=True, + stdout=subprocess.PIPE, + encoding='ascii', + close_fds=True, + ).stdout + try: + for name, value in csv.reader(peres_stdout.splitlines()): + if name == 'Product Version': + if value != CURRENT_DLL_VERSION: + raise ValueError( + f'{out_dll} unexpected DLL version: {value}', + ) + break + else: + raise ValueError('No "Product Version" in peres output') + except Exception as exc: + raise ValueError(f'Peres stdout: {peres_stdout!r}') from exc +cmdclass['update_libusb'] = update_libusb setup( - name='libusb1', - description=next(x for x in long_description.splitlines() if x.strip()), - long_description='.. contents::\n\n' + long_description, - keywords='usb libusb', - version='1.7', - author='Vincent Pelletier', - author_email='plr.vincent@gmail.com', - url='http://github.com/vpelletier/python-libusb1', - license='LGPLv2.1+', - platforms=['any'], - py_modules=['libusb1'], - packages=['usb1'], - classifiers=[ - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.4', - 'Programming Language :: Python :: 2.5', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Software Development :: Libraries', - 'Topic :: System :: Hardware :: Hardware Drivers', - ], - use_2to3=sys.version_info >= (3, ), + version=versioneer.get_version(), + cmdclass=cmdclass, + + setup_requires=( + ['wheel'] + if 'bdist_wheel' in sys.argv else + [] + ), ) diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..8d9cc50 --- /dev/null +++ b/setup.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -eu +cd "$(dirname "$(realpath "$0")")" +for python_v in python3 pypy3; do + if ./runTestLibusb.sh "$python_v" https://github.com/libusb/libusb.git libusb.git master v1.0.19 v1.0.22 v1.0.24 v1.0.25 v1.0.26 v1.0.27; then + : + else + echo "runTestLibusb.sh failed with ${python_v} ($("$python_v" --version))" + exit 1 + fi +done +export I_KNOW_HOW_TO_RELEASE_PYTHON_LIBUSB1=1 +echo "Fetching libusb1 windows binary distribution..." +python3 setup.py --quiet update_libusb +echo "Building distributions..." +embedded_dll_path="usb1/libusb-1.0.dll" +for python_v in python3; do + echo "$python_v bdist_wheel win32" + cp "build/win32/libusb-1.0.dll" "$embedded_dll_path" + "${python_v}" setup.py --quiet bdist_wheel --plat-name win32 clean --all + cp "build/win_amd64/libusb-1.0.dll" "$embedded_dll_path" + "${python_v}" setup.py --quiet bdist_wheel --plat-name win_amd64 clean --all + rm "$embedded_dll_path" + "${python_v}" setup.py --quiet bdist_wheel --plat-name any clean --all +done +python3 setup.py --quiet sdist clean --all +release_prefix="dist/libusb1-$(python3 -c 'import versioneer; print(versioneer.get_version())')" +twine check --strict "${release_prefix}"*.{whl,tar.gz} +echo "Done. Next, check their content, sign each:" +echo " for release in ${release_prefix}-*.whl ${release_prefix}.tar.gz; do gpg --armor --detach-sign \"\$release\"; done" +echo "upload them to pypi:" +echo " twine upload ${release_prefix}-*.whl ${release_prefix}.tar.gz" +echo "and create a new release on github" diff --git a/stdeb.cfg b/stdeb.cfg index c50a240..f5f8e4c 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -1,4 +1,4 @@ [DEFAULT] -Source=python-libusb1 -Package=python-libusb1 +Source=python-usb1 +Package=python-usb1 Depends=libusb-1.0-0 | libusb2debian diff --git a/usb1/__init__.py b/usb1/__init__.py index 0459113..f876808 100644 --- a/usb1/__init__.py +++ b/usb1/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2018 Vincent Pelletier +# Copyright (C) 2010-2021 Vincent Pelletier # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -16,7 +16,7 @@ # pylint: disable=invalid-name, too-many-locals, too-many-arguments # pylint: disable=too-many-public-methods, too-many-instance-attributes -# pylint: disable=missing-docstring +# pylint: disable=missing-docstring, too-many-lines """ Pythonic wrapper for libusb-1.0. @@ -45,38 +45,36 @@ subclassing USBError. """ -from __future__ import division, absolute_import +import collections +import contextlib from ctypes import byref, c_int, sizeof, POINTER, \ cast, c_uint8, c_uint16, c_ubyte, c_void_p, cdll, addressof, \ c_char from ctypes.util import find_library +import functools +import inspect +import itertools import sys import threading import warnings import weakref -import collections -import functools -import contextlib -import inspect -from . import libusb1 -if sys.version_info[:2] >= (2, 6): +from . import _libusb1 as libusb1 +from . import _version +__version__ = _version.get_versions()['version'] # pylint: disable=wrong-import-order,ungrouped-imports - if sys.platform == 'win32': - from ctypes import get_last_error as get_errno - else: - from ctypes import get_errno -# pylint: enable=wrong-import-order,ungrouped-imports +if sys.platform == 'win32': + from ctypes import get_last_error as get_errno else: - def get_errno(): - raise NotImplementedError( - 'Your python version does not support errno/last_error' - ) + from ctypes import get_errno +# pylint: enable=wrong-import-order,ungrouped-imports __all__ = [ 'USBContext', 'USBDeviceHandle', 'USBDevice', 'hasCapability', 'USBPoller', 'USBTransfer', 'USBTransferHelper', 'EVENT_CALLBACK_SET', 'USBEndpoint', 'USBInterfaceSetting', 'USBInterface', 'USBConfiguration', 'DoomedTransferError', 'getVersion', 'USBError', + 'setLogCallback', 'setLocale', + 'loadLibrary', ] # Bind libusb1 constants and libusb1.USBError to this module, so user does not # have to import two modules. @@ -117,11 +115,9 @@ def __bindConstants(): def raiseUSBError( value, # Avoid globals lookup on call to work during interpreter shutdown. - # pylint: disable=dangerous-default-value __STATUS_TO_EXCEPTION_DICT=STATUS_TO_EXCEPTION_DICT, - # pylint: enable=dangerous-default-value __USBError=USBError, - ): + ): # pylint: disable=dangerous-default-value raise __STATUS_TO_EXCEPTION_DICT.get(value, __USBError)(value) def mayRaiseUSBError( @@ -133,112 +129,15 @@ def mayRaiseUSBError( __raiseUSBError(value) return value -try: - namedtuple = collections.namedtuple -except AttributeError: - Version = tuple -else: - Version = namedtuple( - 'Version', - ['major', 'minor', 'micro', 'nano', 'rc', 'describe'], - ) - -if sys.version_info[0] == 3: - BYTE = bytes([0]) - # pylint: disable=redefined-builtin - xrange = range - long = int - # pylint: enable=redefined-builtin - integer_memoryview = memoryview -else: - BYTE = '\x00' - # Work around python2's memoryview, which only accepts & generates strings. - # For consistency between async control and other async transfers. - # Python 2.7 will not be fixed, so wrap its memoryview. - # Breaks the no-copy promise, but control transfer performance should - # matter less than other types. - class integer_memoryview(object): - def __init__(self, view): - if not isinstance(view, memoryview): - view = memoryview(view) - self.__view = view - - # Many boring magic methods, just to mimic memoryview - def __eq__(self, other): - return self.__view == other - - def __ge__(self, other): - return self.__view >= other - - def __gt__(self, other): - return self.__view > other - - def __le__(self, other): - return self.__view <= other - - def __lt__(self, other): - return self.__view < other - - def __ne__(self, other): - return self.__view != other - - def __hash__(self): - # raises - return hash(self.__view) - - def __delitem__(self, key): - # raises - del self.__view[key] - - def __len__(self): - return len(self.__view) - - # To access format, itemsize, ndim, readonly, shape, strides, - # suboffsets, tobytes, tolist. - def __getattr__(self, name): - return getattr(self.__view, name) - - # Actual payload of this class. - def __getitem__(self, key): - value = self.__view[key] - if isinstance(value, memoryview): - return self.__class__(value) - return ord(value) - - def __setitem__(self, key, value): - if isinstance(value, (int, long)): - value = chr(value) - else: - value = ''.join(chr(x) for x in value) - self.__view[key] = value +Version = collections.namedtuple( + 'Version', + ['major', 'minor', 'micro', 'nano', 'rc', 'describe'], +) # pylint: disable=undefined-variable -CONTROL_SETUP = BYTE * CONTROL_SETUP_SIZE +CONTROL_SETUP = b'\x00' * CONTROL_SETUP_SIZE # pylint: enable=undefined-variable -__libc_name = find_library('c') -if __libc_name is None: - # Of course, will leak memory. - # Should we warn user ? How ? - _free = lambda x: None -else: - _free = getattr(cdll, __libc_name).free -del __libc_name - -try: - WeakSet = weakref.WeakSet -except AttributeError: - # Python < 2.7: tiny wrapper around WeakKeyDictionary - class WeakSet(object): - def __init__(self): - self.__dict = weakref.WeakKeyDictionary() - - def add(self, item): - self.__dict[item] = None - - def pop(self): - return self.__dict.popitem()[0] - # Default string length # From a comment in libusb-1.0: "Some devices choke on size > 255" STRING_LENGTH = 255 @@ -259,28 +158,72 @@ def pop(self): # pylint: enable=undefined-variable )) -DEFAULT_ASYNC_TRANSFER_ERROR_CALLBACK = lambda x: False +def DEFAULT_ASYNC_TRANSFER_ERROR_CALLBACK(_): + return False def create_binary_buffer(init_or_size): """ ctypes.create_string_buffer variant which does not add a trailing null when init_or_size is not a size. """ - # As per ctypes.create_string_buffer, as of python 2.7.10 at least: - # - int or long is a length - # - str or unicode is an initialiser - # Testing the latter confuses 2to3, so test the former. - if isinstance(init_or_size, (int, long)): + # As per ctypes.create_string_buffer: + # - int is a length + # - bytes is an initialiser + if isinstance(init_or_size, int): init_or_size = bytearray(init_or_size) return create_initialised_buffer(init_or_size) +class _LibUSB1Finalizer: # pylint: disable=too-few-public-methods + """ + Create, and keep track of, finalizer objects. + Allows outstanding finalizers to be triggered, typically so C resources + owned by the objects associated with those finalizers are freed before + another one they depend (and owned by the instance of this class) on may + be freed. + """ + def __init__(self): + self._finalizer_dict = {} + self.__finalizer_id_generator = itertools.count() + self.__finalizer_id_generator_lock = threading.Lock() + + @staticmethod + def __finalize(handle, pop, func, kw): + try: + func(**kw) + finally: + pop(handle) + + def _getFinalizer(self, obj, func, **kw): + """ + Creates and adds to _finalizer_dict a finalizer which will trigger + after obj becomes unreachable and before it is garbage-collected. + When it triggers, func(**kw) is called and the finalizer is removed + from _finalizer_dict. + + Returns the created finalizer object. + """ + with self.__finalizer_id_generator_lock: + handle = next(self.__finalizer_id_generator) + finalizer_dict = self._finalizer_dict + finalizer_dict[handle] = finalizer = weakref.finalize( + obj, + functools.partial( + self.__finalize, # Note: static method + handle=handle, + pop=finalizer_dict.pop, + func=func, + kw=kw, + ), + ) + return finalizer + def create_initialised_buffer(init): # raises if init is an integer - this is intentional string_type = c_char * len(init) try: # zero-copy if init is a writable buffer return string_type.from_buffer(init), init - # cpython (2.7 and 3.5) raises TypeError, pypy 5.4.1 raises ValueError + # cpython (3.5, 3.9) raises TypeError, pypy 5.4.1 raises ValueError except (TypeError, ValueError): # create our own writable buffer init = bytearray(init) @@ -288,9 +231,8 @@ def create_initialised_buffer(init): class DoomedTransferError(Exception): """Exception raised when altering/submitting a doomed transfer.""" - pass -class USBTransfer(object): +class USBTransfer: """ USB asynchronous transfer control & data. @@ -303,26 +245,29 @@ class USBTransfer(object): change nothing for you, unless you are looking at underlying C transfer structure - which you should never have to. """ - # Prevent garbage collector from freeing the free function before our - # instances, as we need it to property destruct them. - __libusb_free_transfer = libusb1.libusb_free_transfer - __libusb_cancel_transfer = libusb1.libusb_cancel_transfer - __USBError = USBError - # pylint: disable=undefined-variable - __USBErrorNotFound = USBErrorNotFound - __mayRaiseUSBError = staticmethod(mayRaiseUSBError) - # pylint: enable=undefined-variable __transfer = None __initialized = False - __submitted = False + __submitted_dict = {} __callback = None + # Just to silence pylint watnings, this attribute gets overridden after + # class definition. __ctypesCallbackWrapper = None __doomed = False __user_data = None __transfer_buffer = None __transfer_py_buffer = None - def __init__(self, handle, iso_packets, before_submit, after_completion): + def __init__( + self, + context, + handle, + iso_packets, + before_submit, + after_completion, + getFinalizer, + short_is_error, + add_zero_packet, + ): """ You should not instanciate this class directly. Call "getTransfer" method on an USBDeviceHandle instance to get @@ -336,79 +281,99 @@ def __init__(self, handle, iso_packets, before_submit, after_completion): self.__before_submit = before_submit self.__after_completion = after_completion self.__num_iso_packets = iso_packets - result = libusb1.libusb_alloc_transfer(iso_packets) - if not result: + transfer = libusb1.libusb_alloc_transfer(iso_packets) + if not transfer: # pylint: disable=undefined-variable raise USBErrorNoMem # pylint: enable=undefined-variable - self.__transfer = result - self.__ctypesCallbackWrapper = libusb1.libusb_transfer_cb_fn_p( - self.__callbackWrapper) + self.__transfer = transfer + self.setShortIsError(short_is_error) + self.setAddZeroPacket(add_zero_packet) + self.__close = getFinalizer( + self, + self.__close, # Note: class method + transfer=transfer, + context=context, + libusb_free_transfer=libusb1.libusb_free_transfer, + libusb_cancel_transfer=libusb1.libusb_cancel_transfer, + ) def close(self): """ Break reference cycles to allow instance to be garbage-collected. Raises if called on a submitted transfer. """ - if self.__submitted: + if self.isSubmitted(): raise ValueError('Cannot close a submitted transfer') self.doom() self.__initialized = False # Break possible external reference cycles self.__callback = None self.__user_data = None - # Break libusb_transfer reference cycles - self.__ctypesCallbackWrapper = None # For some reason, overwriting callback is not enough to remove this # reference cycle - though sometimes it works: # self -> self.__dict__ -> libusb_transfer -> dict[x] -> dict[x] -> # CThunkObject -> __callbackWrapper -> self # So free transfer altogether. - if self.__transfer is not None: - self.__libusb_free_transfer(self.__transfer) - self.__transfer = None + self.__close() + self.__transfer = None + # pylint: disable=unused-private-member self.__transfer_buffer = None + # pylint: enable=unused-private-member # Break USBDeviceHandle reference cycle self.__before_submit = None self.__after_completion = None + @classmethod + def __close( # pylint: disable=method-hidden + cls, + transfer, + context, + libusb_free_transfer, + libusb_cancel_transfer, + + # pylint: disable=undefined-variable + USBErrorInterrupted_=USBErrorInterrupted, + # pylint: enable=undefined-variable + ): + isSubmitted = cls.__isSubmitted + # Unlikely to be true if we are triggered by object destruction + # (cls.__submitted_dict is precisely here to prevent collection of + # submitted transfers). + # And if we are called by the user, then they should have the ability + # to cancel the transfer cleanly before closing it. + if isSubmitted(transfer): + libusb_cancel_transfer(transfer) + while isSubmitted(transfer): + try: + context.handleEvents() + except USBErrorInterrupted_: + pass + libusb_free_transfer(transfer) + def doom(self): """ Prevent transfer from being submitted again. """ self.__doomed = True - def __del__(self): - if self.__transfer is not None: - try: - # If this doesn't raise, we're doomed; transfer was submitted, - # still python decided to garbage-collect this instance. - # Stick to libusb's documentation, and don't free the - # transfer. If interpreter is shutting down, kernel will - # reclaim memory anyway. - # Note: we can't prevent transfer's buffer from being - # garbage-collected as soon as there will be no remaining - # reference to transfer, so a segfault might happen anyway. - # Should we warn user ? How ? - self.cancel() - except self.__USBErrorNotFound: - # Transfer was not submitted, we can free it. - self.__libusb_free_transfer(self.__transfer) - - # pylint: disable=unused-argument - def __callbackWrapper(self, transfer_p): + @classmethod + # pylint: disable=unused-private-member + def __callbackWrapper(cls, transfer_p): + # pylint: enable=unused-private-member """ Makes it possible for user-provided callback to alter transfer when fired (ie, mark transfer as not submitted upon call). """ - self.__submitted = False + # pylint: disable=protected-access + self = cls.__submitted_dict.pop(addressof(transfer_p.contents)) self.__after_completion(self) callback = self.__callback if callback is not None: callback(self) if self.__doomed: self.close() - # pylint: enable=unused-argument + # pylint: enable=protected-access def setCallback(self, callback): """ @@ -433,7 +398,7 @@ def setControl( request_type defines transfer direction (see ENDPOINT_OUT and ENDPOINT_IN)). buffer_or_len - Either a string (when sending data), or expected data length (when + Either bytes (when sending data), or expected data length (when receiving data). callback Callback function to be invoked on transfer completion. @@ -443,11 +408,11 @@ def setControl( timeout Transfer timeout in milliseconds. 0 to disable. """ - if self.__submitted: + if self.isSubmitted(): raise ValueError('Cannot alter a submitted transfer') if self.__doomed: raise DoomedTransferError('Cannot reuse a doomed transfer') - if isinstance(buffer_or_len, (int, long)): + if isinstance(buffer_or_len, int): length = buffer_or_len # pylint: disable=undefined-variable string_buffer, transfer_py_buffer = create_binary_buffer( @@ -460,9 +425,11 @@ def setControl( CONTROL_SETUP + buffer_or_len, ) self.__initialized = False + # pylint: disable=unused-private-member self.__transfer_buffer = string_buffer + # pylint: enable=unused-private-member # pylint: disable=undefined-variable - self.__transfer_py_buffer = integer_memoryview( + self.__transfer_py_buffer = memoryview( transfer_py_buffer, )[CONTROL_SETUP_SIZE:] # pylint: enable=undefined-variable @@ -485,7 +452,7 @@ def setBulk( Endpoint to submit transfer to. Defines transfer direction (see ENDPOINT_OUT and ENDPOINT_IN)). buffer_or_len - Either a string (when sending data), or expected data length (when + Either bytes (when sending data), or expected data length (when receiving data) To avoid memory copies, use an object implementing the writeable buffer interface (ex: bytearray). @@ -497,7 +464,7 @@ def setBulk( timeout Transfer timeout in milliseconds. 0 to disable. """ - if self.__submitted: + if self.isSubmitted(): raise ValueError('Cannot alter a submitted transfer') if self.__doomed: raise DoomedTransferError('Cannot reuse a doomed transfer') @@ -505,7 +472,9 @@ def setBulk( buffer_or_len ) self.__initialized = False + # pylint: disable=unused-private-member self.__transfer_buffer = string_buffer + # pylint: enable=unused-private-member self.__user_data = user_data libusb1.libusb_fill_bulk_transfer( self.__transfer, self.__handle, endpoint, string_buffer, @@ -523,7 +492,7 @@ def setInterrupt( Endpoint to submit transfer to. Defines transfer direction (see ENDPOINT_OUT and ENDPOINT_IN)). buffer_or_len - Either a string (when sending data), or expected data length (when + Either bytes (when sending data), or expected data length (when receiving data) To avoid memory copies, use an object implementing the writeable buffer interface (ex: bytearray). @@ -535,7 +504,7 @@ def setInterrupt( timeout Transfer timeout in milliseconds. 0 to disable. """ - if self.__submitted: + if self.isSubmitted(): raise ValueError('Cannot alter a submitted transfer') if self.__doomed: raise DoomedTransferError('Cannot reuse a doomed transfer') @@ -543,7 +512,9 @@ def setInterrupt( buffer_or_len ) self.__initialized = False + # pylint: disable=unused-private-member self.__transfer_buffer = string_buffer + # pylint: enable=unused-private-member self.__user_data = user_data libusb1.libusb_fill_interrupt_transfer( self.__transfer, self.__handle, endpoint, string_buffer, @@ -561,7 +532,7 @@ def setIsochronous( Endpoint to submit transfer to. Defines transfer direction (see ENDPOINT_OUT and ENDPOINT_IN)). buffer_or_len - Either a string (when sending data), or expected data length (when + Either bytes (when sending data), or expected data length (when receiving data) To avoid memory copies, use an object implementing the writeable buffer interface (ex: bytearray). @@ -577,7 +548,7 @@ def setIsochronous( will be divided evenly among available transfers if possible, and raise ValueError otherwise. """ - if self.__submitted: + if self.isSubmitted(): raise ValueError('Cannot alter a submitted transfer') num_iso_packets = self.__num_iso_packets if num_iso_packets == 0: @@ -594,33 +565,26 @@ def setIsochronous( iso_length, remainder = divmod(buffer_length, num_iso_packets) if remainder: raise ValueError( - 'Buffer size %i cannot be evenly distributed among %i ' - 'transfers' % ( - buffer_length, - num_iso_packets, - ) + f'Buffer size {buffer_length} cannot be evenly distributed ' + f'among {num_iso_packets} transfers', ) iso_transfer_length_list = [iso_length] * num_iso_packets configured_iso_packets = len(iso_transfer_length_list) if configured_iso_packets > num_iso_packets: raise ValueError( - 'Too many ISO transfer lengths (%i), there are ' - 'only %i ISO transfers available' % ( - configured_iso_packets, - num_iso_packets, - ) + f'Too many ISO transfer lengths ({configured_iso_packets}), ' + f'there are only {num_iso_packets} ISO transfers available', ) if sum(iso_transfer_length_list) > buffer_length: raise ValueError( - 'ISO transfers too long (%i), there are only ' - '%i bytes available' % ( - sum(iso_transfer_length_list), - buffer_length, - ) + f'ISO transfers too long ({sum(iso_transfer_length_list)}), ' + f'there are only {buffer_length} bytes available', ) transfer_p = self.__transfer self.__initialized = False + # pylint: disable=unused-private-member self.__transfer_buffer = string_buffer + # pylint: enable=unused-private-member self.__transfer_py_buffer = transfer_py_buffer self.__user_data = user_data libusb1.libusb_fill_iso_transfer( @@ -771,7 +735,7 @@ def setBuffer(self, buffer_or_len): setIsochronous). Note: disallowed on control transfers (use setControl). """ - if self.__submitted: + if self.isSubmitted(): raise ValueError('Cannot alter a submitted transfer') transfer = self.__transfer.contents # pylint: disable=undefined-variable @@ -789,22 +753,67 @@ def setBuffer(self, buffer_or_len): 'To alter isochronous transfer buffer length, use ' 'setIsochronous' ) + # pylint: disable=unused-private-member self.__transfer_buffer = buff + # pylint: enable=unused-private-member self.__transfer_py_buffer = transfer_py_buffer transfer.buffer = cast(buff, c_void_p) transfer.length = sizeof(buff) + def isShortAnError(self): + """ + Returns whether the LIBUSB_TRANSFER_SHORT_NOT_OK flag is set on this + transfer. + """ + return bool(self.__transfer.contents.flags & libusb1.LIBUSB_TRANSFER_SHORT_NOT_OK) + + def setShortIsError(self, state): + """ + state (bool) + When true, LIBUSB_TRANSFER_SHORT_NOT_OK flag is set on this + transfer. + Otherwise, it is cleared. + """ + if state: + self.__transfer.contents.flags |= libusb1.LIBUSB_TRANSFER_SHORT_NOT_OK + else: + self.__transfer.contents.flags &= ~libusb1.LIBUSB_TRANSFER_SHORT_NOT_OK + + def isZeroPacketAdded(self): + """ + Returns whether the LIBUSB_TRANSFER_ADD_ZERO_PACKET flag is set on this + transfer. + """ + return bool(self.__transfer.contents.flags & libusb1.LIBUSB_TRANSFER_ADD_ZERO_PACKET) + + def setAddZeroPacket(self, state): + """ + state (bool) + When true, LIBUSB_TRANSFER_ADD_ZERO_PACKET flag is set on this + transfer. + Otherwise, it is cleared. + """ + if state: + self.__transfer.contents.flags |= libusb1.LIBUSB_TRANSFER_ADD_ZERO_PACKET + else: + self.__transfer.contents.flags &= ~libusb1.LIBUSB_TRANSFER_ADD_ZERO_PACKET + def isSubmitted(self): """ Tells if this transfer is submitted and still pending. """ - return self.__submitted + transfer = self.__transfer + return transfer is not None and self.__isSubmitted(transfer) + + @classmethod + def __isSubmitted(cls, transfer): + return addressof(transfer.contents) in cls.__submitted_dict def submit(self): """ Submit transfer for asynchronous handling. """ - if self.__submitted: + if self.isSubmitted(): raise ValueError('Cannot submit a submitted transfer') if not self.__initialized: raise ValueError( @@ -813,11 +822,13 @@ def submit(self): if self.__doomed: raise DoomedTransferError('Cannot submit doomed transfer') self.__before_submit(self) - self.__submitted = True - result = libusb1.libusb_submit_transfer(self.__transfer) + transfer = self.__transfer + assert transfer is not None + self.__submitted_dict[addressof(transfer.contents)] = self + result = libusb1.libusb_submit_transfer(transfer) if result: self.__after_completion(self) - self.__submitted = False + self.__submitted_dict.pop(addressof(transfer.contents)) raiseUSBError(result) def cancel(self): @@ -826,14 +837,22 @@ def cancel(self): Note: cancellation happens asynchronously, so you must wait for TRANSFER_CANCELLED. """ - if not self.__submitted: + if not self.isSubmitted(): # XXX: Workaround for a bug reported on libusb 1.0.8: calling # libusb_cancel_transfer on a non-submitted transfer might # trigger a segfault. - raise self.__USBErrorNotFound - self.__mayRaiseUSBError(self.__libusb_cancel_transfer(self.__transfer)) - -class USBTransferHelper(object): + raise USBErrorNotFound # pylint: disable=undefined-variable + mayRaiseUSBError(libusb1.libusb_cancel_transfer(self.__transfer)) + +# XXX: This is very unsightly, but I do not see another way of declaring within +# class body both the class method and its ctypes function pointer. +# pylint: disable=protected-access,no-member +USBTransfer._USBTransfer__ctypesCallbackWrapper = libusb1.libusb_transfer_cb_fn_p( + USBTransfer._USBTransfer__callbackWrapper, +) +# pylint: enable=protected-access,no-member + +class USBTransferHelper: """ Simplifies subscribing to the same transfer over and over, and callback handling: @@ -900,7 +919,7 @@ def setEventCallback(self, event, callback): TRANSFER_OVERFLOW """ if event not in EVENT_CALLBACK_SET: - raise ValueError('Unknown event %r.' % (event, )) + raise ValueError(f'Unknown event {event!r}.') self.__event_callback_dict[event] = callback def setDefaultCallback(self, callback): @@ -936,112 +955,7 @@ def isSubmited(self): # Deprecated: to drop return self.__transfer.isSubmitted() -# BBB -class USBPollerThread(threading.Thread): - """ - Implements libusb1 documentation about threaded, asynchronous - applications. - In short, instanciate this class once (...per USBContext instance), call - start() on the instance, and do whatever you need. - This thread will be used to execute transfer completion callbacks, and you - are free to use libusb1's synchronous API in another thread, and can forget - about libusb1 file descriptors. - - See http://libusb.sourceforge.net/api-1.0/mtasync.html . - """ - _can_run = True - - def __init__(self, context, poller, exc_callback=None): - """ - Create a poller thread for given context. - Warning: it will not check if another poller instance was already - present for that context, and will replace it. - - poller - (same as USBPoller.__init__ "poller" parameter) - - exc_callback (callable) - Called with a libusb_error value as single parameter when event - handling fails. - If not given, an USBError will be raised, interrupting the thread. - """ - super(USBPollerThread, self).__init__() - warnings.warn( - 'USBPollerThread causes long stalls when used with poll (it was ' - 'intended for epoll), and is generally misleading. Consider ' - 'calling looping over context.handleEvents() in a thread instead.', - DeprecationWarning, - ) - self.daemon = True - self.__context = context - self.__poller = poller - self.__fd_set = set() - if exc_callback is not None: - self.exceptionHandler = exc_callback - - def stop(self): - """ - Stop & join thread. - - Allows stopping event thread before context gets closed. - """ - self._can_run = False - self.join() - - # pylint: disable=method-hidden - @staticmethod - def exceptionHandler(exc): - raise exc - # pylint: enable=method-hidden - - def run(self): - # We expect quite some spinning in below loop, so move any unneeded - # operation out of it. - context = self.__context - poll = self.__poller.poll - try_lock_events = context.tryLockEvents - lock_event_waiters = context.lockEventWaiters - wait_for_event = context.waitForEvent - unlock_event_waiters = context.unlockEventWaiters - event_handling_ok = context.eventHandlingOK - unlock_events = context.unlockEvents - handle_events_locked = context.handleEventsLocked - event_handler_active = context.eventHandlerActive - getNextTimeout = context.getNextTimeout - exceptionHandler = self.exceptionHandler - fd_set = self.__fd_set - context.setPollFDNotifiers(self._registerFD, self._unregisterFD) - for fd, events in context.getPollFDList(): - self._registerFD(fd, events, None) - try: - while fd_set and self._can_run: - if try_lock_events(): - lock_event_waiters() - while event_handler_active(): - wait_for_event() - unlock_event_waiters() - else: - try: - while event_handling_ok(): - if poll(getNextTimeout()): - try: - handle_events_locked() - except USBError: - exceptionHandler(sys.exc_info()[1]) - finally: - unlock_events() - finally: - context.setPollFDNotifiers(None, None) - - def _registerFD(self, fd, events, _): - self.__poller.register(fd, events) - self.__fd_set.add(fd) - - def _unregisterFD(self, fd, _): - self.__fd_set.discard(fd) - self.__poller.unregister(fd) - -class USBPoller(object): +class USBPoller: """ Class allowing integration of USB event polling in a file-descriptor monitoring event loop. @@ -1138,7 +1052,7 @@ def _unregisterFD(self, fd, user_data=None): self.unregister(fd) # pylint: enable=unused-argument -class _ReleaseInterface(object): +class _ReleaseInterface: def __init__(self, handle, interface): self._handle = handle self._interface = interface @@ -1150,35 +1064,31 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self._handle.releaseInterface(self._interface) -class USBDeviceHandle(object): +class USBDeviceHandle(_LibUSB1Finalizer): """ Represents an opened USB device. """ __handle = None - __libusb_close = libusb1.libusb_close - # pylint: disable=undefined-variable - __USBErrorNoDevice = USBErrorNoDevice - __USBErrorNotFound = USBErrorNotFound - __USBErrorInterrupted = USBErrorInterrupted - # pylint: enable=undefined-variable - __set = set - __KeyError = KeyError - __sys = sys - def __init__(self, context, handle, device): + def __init__( + self, + context, + handle, + device, + getFinalizer, + can_close_device, + ): """ You should not instanciate this class directly. Call "open" method on an USBDevice instance to get an USBDeviceHandle instance. """ + super().__init__() self.__context = context - # Weak reference to transfers about this device so we can clean up - # before closing device. - self.__transfer_set = WeakSet() # Strong references to inflight transfers so they do not get freed # even if user drops all strong references to them. If this instance # is garbage-collected, we close all transfers, so it's fine. - self.__inflight = inflight = set() + inflight = set() # XXX: For some reason, doing self.__inflight.{add|remove} inside # getTransfer causes extra intermediate python objects for each # allocated transfer. Storing them as properties solves this. Found @@ -1187,11 +1097,37 @@ def __init__(self, context, handle, device): self.__inflight_remove = inflight.remove self.__handle = handle self.__device = device + self.close = getFinalizer( + self, + self.close, # Note: static method + context=context, + handle=handle, + device=( + device + if can_close_device else + None + ), + inflight=inflight, + finalizer_dict=self._finalizer_dict, + libusb_close=libusb1.libusb_close, + ) - def __del__(self): - self.close() - - def close(self): + @staticmethod + def close( # pylint: disable=method-hidden + context, + handle, + device, + inflight, + finalizer_dict, + libusb_close, + + set_=set, + # pylint: disable=undefined-variable + USBErrorNotFound_=USBErrorNotFound, + USBErrorNoDevice_=USBErrorNoDevice, + USBErrorInterrupted_=USBErrorInterrupted, + # pylint: enable=undefined-variable + ): """ Close this handle. If not called explicitely, will be called by destructor. @@ -1203,39 +1139,26 @@ def close(self): do not close nor let GC collect a USBDeviceHandle which has in-flight transfers. """ - handle = self.__handle - if handle is None: - return - # Build a strong set from weak self.__transfer_set so we can doom - # and close all contained transfers. - # Because of backward compatibility, self.__transfer_set might be a - # wrapper around WeakKeyDictionary. As it might be modified by gc, - # we must pop until there is not key left instead of iterating over - # it. - weak_transfer_set = self.__transfer_set - transfer_set = self.__set() - while True: - try: - transfer = weak_transfer_set.pop() - except self.__KeyError: - break - transfer_set.add(transfer) - transfer.doom() - inflight = self.__inflight - for transfer in inflight: - try: - transfer.cancel() - except (self.__USBErrorNotFound, self.__USBErrorNoDevice): - pass + cancelled_set = set_() while inflight: + for transfer in tuple(inflight): + if transfer not in cancelled_set: + try: + transfer.cancel() + except (USBErrorNotFound_, USBErrorNoDevice_): + pass + cancelled_set.add(transfer) try: - self.__context.handleEvents() - except self.__USBErrorInterrupted: + context.handleEvents() + except USBErrorInterrupted_: pass - for transfer in transfer_set: - transfer.close() - self.__libusb_close(handle) - self.__handle = None + while finalizer_dict: + for finalizer_handle, finalizer in list(finalizer_dict.items()): + finalizer() + assert finalizer_handle not in finalizer_dict + if device is not None: + device.close() + libusb_close(handle) def getDevice(self): """ @@ -1328,8 +1251,8 @@ def resetDevice(self): Reinitialise current device. Attempts to restore current configuration & alt settings. If this fails, will result in a device disconnect & reconnect, so you - have to close current device and rediscover it (notified by a - ERROR_NOT_FOUND error code). + have to close current device and rediscover it (notified by an + USBErrorNotFound exception). """ mayRaiseUSBError(libusb1.libusb_reset_device(self.__handle)) @@ -1340,9 +1263,10 @@ def kernelDriverActive(self, interface): result = libusb1.libusb_kernel_driver_active(self.__handle, interface) if result == 0: return False - elif result == 1: + if result == 1: return True raiseUSBError(result) + return None # unreachable, to make pylint happy def detachKernelDriver(self, interface): """ @@ -1394,7 +1318,7 @@ def getSupportedLanguageList(self): langid_list = cast(descriptor_string, POINTER(c_uint16)) return [ libusb1.libusb_le16_to_cpu(langid_list[offset]) - for offset in xrange(1, cast(descriptor_string, POINTER(c_ubyte))[0] // 2) + for offset in range(1, cast(descriptor_string, POINTER(c_ubyte))[0] // 2) ] def getStringDescriptor(self, descriptor, lang_id, errors='strict'): @@ -1417,7 +1341,9 @@ def getStringDescriptor(self, descriptor, lang_id, errors='strict'): except USBErrorNotFound: # pylint: enable=undefined-variable return None + # pylint: disable=undefined-variable if received < 2 or descriptor_string[1] != DT_STRING: + # pylint: enable=undefined-variable raise ValueError('Invalid string descriptor') return descriptor_string[2:min( received, @@ -1508,7 +1434,9 @@ def _bulkTransfer(self, endpoint, data, length, timeout): mayRaiseUSBError(libusb1.libusb_bulk_transfer( self.__handle, endpoint, data, length, byref(transferred), timeout, )) + # pylint: disable=undefined-variable except USBErrorTimeout as exception: + # pylint: enable=undefined-variable exception.transferred = transferred.value raise return transferred.value @@ -1558,7 +1486,9 @@ def bulkRead(self, endpoint, length, timeout=0): data, data_buffer = create_binary_buffer(length) try: transferred = self._bulkTransfer(endpoint, data, length, timeout) + # pylint: disable=undefined-variable except USBErrorTimeout as exception: + # pylint: enable=undefined-variable exception.received = data_buffer[:exception.transferred] raise return data_buffer[:transferred] @@ -1574,7 +1504,9 @@ def _interruptTransfer(self, endpoint, data, length, timeout): byref(transferred), timeout, )) + # pylint: disable=undefined-variable except USBErrorTimeout as exception: + # pylint: enable=undefined-variable exception.transferred = transferred.value raise return transferred.value @@ -1629,26 +1561,35 @@ def interruptRead(self, endpoint, length, timeout=0): length, timeout, ) + # pylint: disable=undefined-variable except USBErrorTimeout as exception: + # pylint: enable=undefined-variable exception.received = data_buffer[:exception.transferred] raise return data_buffer[:transferred] - def getTransfer(self, iso_packets=0): + def getTransfer(self, iso_packets=0, short_is_error=False, add_zero_packet=False): """ Get an USBTransfer instance for asynchronous use. iso_packets: the number of isochronous transfer descriptors to allocate. - """ - result = USBTransfer( - self.__handle, iso_packets, - self.__inflight_add, self.__inflight_remove, + short_is_error: When true, short frames are reported as errors. + add_zero_packet: When true, transfers of a multiple of the endpoint + size are followed by a zero-length packet. + """ + return USBTransfer( + context=self.__context, + handle=self.__handle, + iso_packets=iso_packets, + before_submit=self.__inflight_add, + after_completion=self.__inflight_remove, + getFinalizer=self._getFinalizer, + short_is_error=short_is_error, + add_zero_packet=add_zero_packet, ) - self.__transfer_set.add(result) - return result -class USBConfiguration(object): - def __init__(self, context, config): +class USBConfiguration: + def __init__(self, context, config, device_speed): """ You should not instanciate this class directly. Call USBDevice methods to get instances of this class. @@ -1657,6 +1598,7 @@ def __init__(self, context, config): raise TypeError('Unexpected descriptor type.') self.__config = config self.__context = context + self.__device_speed = device_speed def getNumInterfaces(self): return self.__config.bNumInterfaces @@ -1674,11 +1616,15 @@ def getAttributes(self): def getMaxPower(self): """ - Returns device's power consumption in mW. - Beware of unit: USB descriptor uses 2mW increments, this method - converts it to mW units. + Returns device's power consumption in mA. + + USB descriptor is expressed in units of 2 mA when the device is operating in high-speed mode + and in units of 8 mA when the device is operating in super-speed mode. This function scales + the descriptor value appropriately. """ - return self.__config.MaxPower * 2 + # pylint: disable=undefined-variable + return self.__config.MaxPower * (8 if self.__device_speed == SPEED_SUPER else 2) + # pylint: enable=undefined-variable def getExtra(self): """ @@ -1693,7 +1639,7 @@ def __iter__(self): """ context = self.__context interface_list = self.__config.interface - for interface_num in xrange(self.getNumInterfaces()): + for interface_num in range(self.getNumInterfaces()): yield USBInterface(context, interface_list[interface_num]) # BBB @@ -1706,10 +1652,10 @@ def __getitem__(self, interface): if not isinstance(interface, int): raise TypeError('interface parameter must be an integer') if not 0 <= interface < self.getNumInterfaces(): - raise IndexError('No such interface: %r' % (interface, )) + raise IndexError(f'No such interface: {interface!r}') return USBInterface(self.__context, self.__config.interface[interface]) -class USBInterface(object): +class USBInterface: def __init__(self, context, interface): """ You should not instanciate this class directly. @@ -1732,7 +1678,7 @@ def __iter__(self): """ context = self.__context alt_setting_list = self.__interface.altsetting - for alt_setting_num in xrange(self.getNumSettings()): + for alt_setting_num in range(self.getNumSettings()): yield USBInterfaceSetting( context, alt_setting_list[alt_setting_num]) @@ -1746,11 +1692,11 @@ def __getitem__(self, alt_setting): if not isinstance(alt_setting, int): raise TypeError('alt_setting parameter must be an integer') if not 0 <= alt_setting < self.getNumSettings(): - raise IndexError('No such setting: %r' % (alt_setting, )) + raise IndexError(f'No such setting: {alt_setting!r}') return USBInterfaceSetting( self.__context, self.__interface.altsetting[alt_setting]) -class USBInterfaceSetting(object): +class USBInterfaceSetting: def __init__(self, context, alt_setting): """ You should not instanciate this class directly. @@ -1805,7 +1751,7 @@ def __iter__(self): """ context = self.__context endpoint_list = self.__alt_setting.endpoint - for endpoint_num in xrange(self.getNumEndpoints()): + for endpoint_num in range(self.getNumEndpoints()): yield USBEndpoint(context, endpoint_list[endpoint_num]) # BBB @@ -1818,16 +1764,18 @@ def __getitem__(self, endpoint): if not isinstance(endpoint, int): raise TypeError('endpoint parameter must be an integer') if not 0 <= endpoint < self.getNumEndpoints(): - raise ValueError('No such endpoint: %r' % (endpoint, )) + raise ValueError(f'No such endpoint: {endpoint}') return USBEndpoint( self.__context, self.__alt_setting.endpoint[endpoint]) -class USBEndpoint(object): +class USBEndpoint: def __init__(self, context, endpoint): if not isinstance(endpoint, libusb1.libusb_endpoint_descriptor): raise TypeError('Unexpected descriptor type.') self.__endpoint = endpoint + # pylint: disable=unused-private-member self.__context = context + # pylint: enable=unused-private-member def getAddress(self): return self.__endpoint.bEndpointAddress @@ -1850,7 +1798,7 @@ def getSyncAddress(self): def getExtra(self): return libusb1.get_extra(self.__endpoint) -class USBDevice(object): +class USBDevice(_LibUSB1Finalizer): """ Represents a USB device. @@ -1860,31 +1808,51 @@ class USBDevice(object): """ __configuration_descriptor_list = () - __libusb_unref_device = libusb1.libusb_unref_device - __libusb_free_config_descriptor = libusb1.libusb_free_config_descriptor - __byref = byref - __KeyError = KeyError - - def __init__(self, context, device_p, can_load_configuration=True): + __device_handle = None + + def __init__( + self, + context, + device_p, + getFinalizer, + can_load_configuration, + can_change_refcount, + handle_p, + ): """ You should not instanciate this class directly. Call USBContext methods to receive instances of this class. """ + super().__init__() self.__context = context - self.__close_set = WeakSet() - libusb1.libusb_ref_device(device_p) + self.__configuration_descriptor_list = descriptor_list = [] + if can_change_refcount: + libusb1.libusb_ref_device(device_p) + self.close = getFinalizer( + self, + self.close, # Note: static method + device_p=( + device_p + if can_change_refcount else + None + ), + finalizer_dict=self._finalizer_dict, + descriptor_list=descriptor_list, + libusb_unref_device=libusb1.libusb_unref_device, + libusb_free_config_descriptor=libusb1.libusb_free_config_descriptor, + ) self.device_p = device_p # Fetch device descriptor + # Note: if this is made lazy, access errors will happen later, breaking + # getDeviceIterator exception handling. device_descriptor = libusb1.libusb_device_descriptor() result = libusb1.libusb_get_device_descriptor( device_p, byref(device_descriptor)) mayRaiseUSBError(result) self.device_descriptor = device_descriptor if can_load_configuration: - self.__configuration_descriptor_list = descriptor_list = [] append = descriptor_list.append - device_p = self.device_p - for configuration_id in xrange( + for configuration_id in range( self.device_descriptor.bNumConfigurations): config = libusb1.libusb_config_descriptor_p() result = libusb1.libusb_get_config_descriptor( @@ -1898,37 +1866,44 @@ def __init__(self, context, device_p, can_load_configuration=True): continue mayRaiseUSBError(result) append(config.contents) + self.__bus_number = libusb1.libusb_get_bus_number(device_p) + self.__port_number = libusb1.libusb_get_port_number(device_p) + self.__device_address = libusb1.libusb_get_device_address(device_p) + if handle_p is not None: + self.__device_handle = USBDeviceHandle( + context=context, + handle=handle_p, + device=self, + getFinalizer=self._getFinalizer, + can_close_device=True, + ) - def __del__(self): - self.close() - - def close(self): - pop = self.__close_set.pop - while True: - try: - closable = pop() - except self.__KeyError: - break - closable.close() - if not self.device_p: - return - self.__libusb_unref_device(self.device_p) - # pylint: disable=redefined-outer-name - byref = self.__byref - # pylint: enable=redefined-outer-name - descriptor_list = self.__configuration_descriptor_list + @staticmethod + def close( # pylint: disable=method-hidden + device_p, + finalizer_dict, + descriptor_list, + libusb_unref_device, + libusb_free_config_descriptor, + + byref_=byref, + ): + while finalizer_dict: + for handle, finalizer in list(finalizer_dict.items()): + finalizer() + assert handle not in finalizer_dict + if device_p is not None: + libusb_unref_device(device_p) while descriptor_list: - self.__libusb_free_config_descriptor( - byref(descriptor_list.pop()), + libusb_free_config_descriptor( + byref_(descriptor_list.pop()), ) - self.device_p = None def __str__(self): - return 'Bus %03i Device %03i: ID %04x:%04x' % ( - self.getBusNumber(), - self.getDeviceAddress(), - self.getVendorID(), - self.getProductID(), + return ( + f'Bus {self.getBusNumber():03} ' + f'Device {self.getDeviceAddress():03}: ' + f'ID {self.getVendorID():04x}:{self.getProductID():04x}' ) def __len__(self): @@ -1936,13 +1911,13 @@ def __len__(self): def __getitem__(self, index): return USBConfiguration( - self.__context, self.__configuration_descriptor_list[index]) + self.__context, self.__configuration_descriptor_list[index], self.getDeviceSpeed()) def __key(self): return ( - id(self.__context), self.getBusNumber(), - self.getDeviceAddress(), self.getVendorID(), - self.getProductID(), + id(self.__context), self.__bus_number, + self.__device_address, self.device_descriptor.idVendor, + self.device_descriptor.idProduct, ) def __hash__(self): @@ -1961,7 +1936,7 @@ def __eq__(self, other): def iterConfigurations(self): context = self.__context for config in self.__configuration_descriptor_list: - yield USBConfiguration(context, config) + yield USBConfiguration(context, config, self.getDeviceSpeed()) # BBB iterConfiguations = iterConfigurations @@ -1969,20 +1944,19 @@ def iterConfigurations(self): def iterSettings(self): for config in self.iterConfigurations(): for interface in config: - for setting in interface: - yield setting + yield from interface def getBusNumber(self): """ Get device's bus number. """ - return libusb1.libusb_get_bus_number(self.device_p) + return self.__bus_number def getPortNumber(self): """ Get device's port number. """ - return libusb1.libusb_get_port_number(self.device_p) + return self.__port_number def getPortNumberList(self): """ @@ -2001,7 +1975,7 @@ def getDeviceAddress(self): """ Get device's address on its bus. """ - return libusb1.libusb_get_device_address(self.device_p) + return self.__device_address def getbcdUSB(self): """ @@ -2147,6 +2121,7 @@ def getDeviceSpeed(self): SPEED_FULL SPEED_HIGH SPEED_SUPER + SPEED_SUPER_PLUS """ return libusb1.libusb_get_device_speed(self.device_p) @@ -2155,32 +2130,38 @@ def open(self): Open device. Returns an USBDeviceHandle instance. """ + if self.__device_handle is not None: + return self.__device_handle handle = libusb1.libusb_device_handle_p() mayRaiseUSBError(libusb1.libusb_open(self.device_p, byref(handle))) - result = USBDeviceHandle(self.__context, handle, self) - self.__close_set.add(result) - return result + return USBDeviceHandle( + context=self.__context, + handle=handle, + device=self, + getFinalizer=self._getFinalizer, + can_close_device=False, + ) _zero_tv = libusb1.timeval(0, 0) _zero_tv_p = byref(_zero_tv) +_null_pointer = c_void_p() +_NULL_LOG_CALLBACK = libusb1.libusb_log_cb_p(0) -class USBContext(object): +class USBContext(_LibUSB1Finalizer): """ libusb1 USB context. Provides methods to enumerate & look up USB devices. Also provides access to global (device-independent) libusb1 functions. """ - __libusb_exit = libusb1.libusb_exit __context_p = None __added_cb = None __removed_cb = None __poll_cb_user_data = None - __libusb_set_pollfd_notifiers = libusb1.libusb_set_pollfd_notifiers - __null_pointer = c_void_p() - __KeyError = KeyError - __cast = staticmethod(cast) __auto_open = True + __has_pollfd_finalizer = False + __mayRaiseUSBError = staticmethod(mayRaiseUSBError) + __libusb_handle_events = None # pylint: disable=no-self-argument,protected-access def _validContext(func): @@ -2194,6 +2175,7 @@ def refcount(self): 'Use "with USBContext() as context:" for safer cleanup' ' on interpreter shutdown. See also USBContext.open().', DeprecationWarning, + stacklevel=4 ) self.open() self.__context_refcount += 1 @@ -2203,15 +2185,18 @@ def refcount(self): with self.__context_cond: self.__context_refcount -= 1 if not self.__context_refcount: - self.__context_cond.notifyAll() + self.__context_cond.notify_all() if inspect.isgeneratorfunction(func): def wrapper(self, *args, **kw): with refcount(self): if self.__context_p: # pylint: disable=not-callable - for value in func(self, *args, **kw): - # pylint: enable=not-callable - yield value + generator = func(self, *args, **kw) + # pylint: enable=not-callable + try: + yield from generator + finally: + generator.close() else: def wrapper(self, *args, **kw): with refcount(self): @@ -2219,28 +2204,53 @@ def wrapper(self, *args, **kw): # pylint: disable=not-callable return func(self, *args, **kw) # pylint: enable=not-callable + return None functools.update_wrapper(wrapper, func) return wrapper # pylint: enable=no-self-argument,protected-access - def __init__(self): + def __init__( + self, + log_level=None, + use_usbdk=False, + with_device_discovery=True, + log_callback=None, + ): """ Create a new USB context. - """ + + log_level (LOG_LEVEL_*) + Sets the context's log level as soon as it is created. + Maybe have no effect depending on libusb's build options. + use_usbdk (bool) + Windows only. + Whether to use the UsbDk backend if available. + with_device_discovery (bool) + Linux only. + Disables device scan while initialising the library. + This has knowck-on effects on how devices may be opened and how + descriptors are accessed. For more details, see libusb1's + documentation. + log_callback ((int, bytes): None) + Context's log callback function. + + Note: providing non-default values may cause context creation (during + __enter__, open, or the first context-dependent call, whichever happens + first) to fail if libusb is older than v1.0.27 . + """ + super().__init__() # Used to prevent an exit to cause a segfault if a concurrent thread # is still in libusb. self.__context_refcount = 0 self.__context_cond = threading.Condition() self.__context_p = libusb1.libusb_context_p() + assert not self.__context_p self.__hotplug_callback_dict = {} - self.__close_set = WeakSet() - - def __del__(self): - # Avoid locking. - # XXX: Assumes __del__ should not normally be called while any - # instance's method is being executed. It seems unlikely (they hold a - # reference to their instance). - self._exit() + self.__log_level = log_level + self.__use_usbdk = use_usbdk + self.__with_device_discovery = with_device_discovery + self.__user_log_callback = log_callback + self.__log_callback_p = libusb1.libusb_log_cb_p(self.__log_callback) def __enter__(self): return self.open() @@ -2255,13 +2265,55 @@ def open(self): This happens automatically on the first method call needing access to the uninitialised properties, but with a warning. Call this method ONLY if your usage pattern prevents you from using the - with USBContext() as contewt: + with USBContext() as context: form: this means there are ways to avoid calling close(), which can cause issues particularly hard to debug (ex: interpreter hangs on exit). + + Calls loadLibrary. """ assert self.__context_refcount == 0 - mayRaiseUSBError(libusb1.libusb_init(byref(self.__context_p))) + assert not self.__context_p + loadLibrary() + self.__libusb_handle_events = libusb1.libusb_handle_events + option_array = ( + libusb1.libusb_init_option * libusb1.LIBUSB_OPTION_MAX + )() + option_count = 0 + if self.__log_level is not None: + option = option_array[option_count] + option_count += 1 + option.option = libusb1.LIBUSB_OPTION_LOG_LEVEL + option.value.ival = self.__log_level + if self.__use_usbdk: + option = option_array[option_count] + option_count += 1 + option.option = libusb1.LIBUSB_OPTION_USE_USBDK + option.value.ival = 1 + if not self.__with_device_discovery: + option = option_array[option_count] + option_count += 1 + option.option = libusb1.LIBUSB_OPTION_NO_DEVICE_DISCOVERY + option.value.ival = 1 + if self.__user_log_callback is not None: + option = option_array[option_count] + option_count += 1 + option.option = libusb1.LIBUSB_OPTION_LOG_CB + option.value.log_cbval = self.__log_callback_p + mayRaiseUSBError(libusb1.libusb_init_context( + byref(self.__context_p), + option_array, + option_count, + )) + self.__close = weakref.finalize( + self, + self.___close, # Note: static method + context_p=self.__context_p, + hotplug_callback_dict=self.__hotplug_callback_dict, + finalizer_dict=self._finalizer_dict, + libusb_exit=libusb1.libusb_exit, + libusb_hotplug_deregister_callback=libusb1.libusb_hotplug_deregister_callback, + ) return self def close(self): @@ -2279,31 +2331,44 @@ def close(self): try: while self.__context_refcount and self.__context_p: self.__context_cond.wait() - self._exit() + self.__close() + # pylint: disable=unused-private-member + self.__added_cb = None + self.__removed_cb = None + self.__poll_cb_user_data = None + # pylint: enable=unused-private-member finally: - self.__context_cond.notifyAll() + self.__context_cond.notify_all() self.__context_cond.release() - def _exit(self): - context_p = self.__context_p - if context_p: - for handle in self.__hotplug_callback_dict.keys(): - self.hotplugDeregisterCallback(handle) - pop = self.__close_set.pop - while True: - try: - closable = pop() - except self.__KeyError: - break - closable.close() - self.__libusb_exit(context_p) - self.__context_p = libusb1.libusb_context_p() - self.__added_cb = self.__null_pointer - self.__removed_cb = self.__null_pointer - # BBB exit = close + @staticmethod + def __close(): # pylint: disable=method-hidden + # Placeholder, masked on open() + pass + + @staticmethod + def ___close( # pylint: disable=method-hidden + context_p, + hotplug_callback_dict, + finalizer_dict, + libusb_exit, + libusb_hotplug_deregister_callback, + ): + while hotplug_callback_dict: + # Duplicates hotplugDeregisterCallback logic, to avoid finalizer + # referencing its own instance. + handle, _ = hotplug_callback_dict.popitem() + libusb_hotplug_deregister_callback(context_p, handle) + while finalizer_dict: + for handle, finalizer in list(finalizer_dict.items()): + finalizer() + assert handle not in finalizer_dict + libusb_exit(context_p) + context_p.value = None + @_validContext def getDeviceIterator(self, skip_on_error=False): """ @@ -2313,28 +2378,29 @@ def getDeviceIterator(self, skip_on_error=False): skip_on_error (bool) If True, ignore devices which raise USBError. """ + libusb_free_device_list = libusb1.libusb_free_device_list device_p_p = libusb1.libusb_device_p_p() - libusb_device_p = libusb1.libusb_device_p device_list_len = libusb1.libusb_get_device_list(self.__context_p, byref(device_p_p)) mayRaiseUSBError(device_list_len) try: for device_p in device_p_p[:device_list_len]: try: - # Instanciate our own libusb_device_p object so we can free - # libusb-provided device list. Is this a bug in ctypes that - # it doesn't copy pointer value (=pointed memory address) ? - # At least, it's not so convenient and forces using such - # weird code. - device = USBDevice(self, libusb_device_p(device_p.contents)) + device = USBDevice( + context=self, + device_p=device_p, + getFinalizer=self._getFinalizer, + can_load_configuration=True, + can_change_refcount=True, + handle_p=None, + ) except USBError: if not skip_on_error: raise else: - self.__close_set.add(device) yield device finally: - libusb1.libusb_free_device_list(device_p_p, 1) + libusb_free_device_list(device_p_p, 1) def getDeviceList(self, skip_on_access_error=False, skip_on_error=False): """ @@ -2364,12 +2430,18 @@ def getByVendorIDAndProductID( skip_on_access_error (bool) (see getDeviceList) """ - for device in self.getDeviceIterator( - skip_on_error=skip_on_access_error or skip_on_error, - ): - if device.getVendorID() == vendor_id and \ - device.getProductID() == product_id: - return device + device_iterator = self.getDeviceIterator( + skip_on_error=skip_on_access_error or skip_on_error, + ) + try: + for device in device_iterator: + if device.getVendorID() == vendor_id and \ + device.getProductID() == product_id: + return device + device.close() + finally: + device_iterator.close() + return None def openByVendorIDAndProductID( self, vendor_id, product_id, @@ -2389,6 +2461,39 @@ def openByVendorIDAndProductID( skip_on_error=skip_on_error) if result is not None: return result.open() + return None + + @_validContext + def wrapSysDevice(self, sys_device): + """ + Wrap sys_device to obtain a USBDeviceHandle instance. + + sys_device (file, int): + File or file descriptor of the sys device node to wrap. + You must keep this file open while the device is, + and are expected to close it any time after it is closed. + + You may get a USBDevice instance by calling getDevice on the returned + value, but note that this device will be closed once the handle is. + """ + if not isinstance(sys_device, int): + sys_device = sys_device.fileno() + handle_p = libusb1.libusb_device_handle_p() + mayRaiseUSBError( + libusb1.libusb_wrap_sys_device( + self.__context_p, + sys_device, + byref(handle_p), + ) + ) + return USBDevice( + context=self, + device_p=libusb1.libusb_get_device(handle_p), + getFinalizer=self._getFinalizer, + can_load_configuration=True, # XXX: give the caller control ? + can_change_refcount=False, + handle_p=handle_p, + ).open() @_validContext def getPollFDList(self): @@ -2402,10 +2507,9 @@ def getPollFDList(self): errno = get_errno() if errno: raise OSError(errno) - else: - # Assume not implemented - raise NotImplementedError( - 'Your libusb does not seem to implement pollable FDs') + # Assume not implemented + raise NotImplementedError( + 'Your libusb does not seem to implement pollable FDs') try: result = [] append = result.append @@ -2417,7 +2521,7 @@ def getPollFDList(self): )) fd_index += 1 finally: - _free(pollfd_p_p) + libusb1.libusb_free_pollfds(pollfd_p_p) return result @_validContext @@ -2427,8 +2531,8 @@ def handleEvents(self): See libusb1 documentation for details (there is a timeout, so it's not "really" blocking). """ - mayRaiseUSBError( - libusb1.libusb_handle_events(self.__context_p), + self.__mayRaiseUSBError( + self.__libusb_handle_events(self.__context_p), ) # TODO: handleEventsCompleted @@ -2454,6 +2558,15 @@ def handleEventsTimeout(self, tv=0): # TODO: handleEventsTimeoutCompleted + @_validContext + def interruptEventHandler(self): + """ + Interrupt any active thread that is handling events. + This is mainly useful for interrupting a dedicated event handling thread + when the application wishes to exit. + """ + libusb1.libusb_interrupt_event_handler(self.__context_p) + @_validContext def setPollFDNotifiers( self, added_cb=None, removed_cb=None, user_data=None): @@ -2464,24 +2577,54 @@ def setPollFDNotifiers( this class with a polling mechanism. """ if added_cb is None: - added_cb = self.__null_pointer + added_cb = _null_pointer else: added_cb = libusb1.libusb_pollfd_added_cb_p(added_cb) if removed_cb is None: - removed_cb = self.__null_pointer + removed_cb = _null_pointer else: removed_cb = libusb1.libusb_pollfd_removed_cb_p(removed_cb) if user_data is None: - user_data = self.__null_pointer + user_data = _null_pointer + # pylint: disable=unused-private-member self.__added_cb = added_cb self.__removed_cb = removed_cb self.__poll_cb_user_data = user_data - self.__libusb_set_pollfd_notifiers( + # pylint: enable=unused-private-member + libusb1.libusb_set_pollfd_notifiers( self.__context_p, - self.__cast(added_cb, libusb1.libusb_pollfd_added_cb_p), - self.__cast(removed_cb, libusb1.libusb_pollfd_removed_cb_p), + cast(added_cb, libusb1.libusb_pollfd_added_cb_p), + cast(removed_cb, libusb1.libusb_pollfd_removed_cb_p), user_data, ) + if not self.__has_pollfd_finalizer: + # Note: the above condition is just to avoid creating finalizers on + # every call. If more than one is created (because of a + # race-condition) it is not a big deal, as __finalizePollFDNotifiers + # will do the right thing even if called multiple times in a row. + self.__has_pollfd_finalizer = True + self._getFinalizer( + self, + self.__finalizePollFDNotifiers, # Note: staticmethod + context_p=self.__context_p, + libusb_set_pollfd_notifiers=libusb1.libusb_set_pollfd_notifiers, + ) + + @staticmethod + def __finalizePollFDNotifiers( + context_p, + libusb_set_pollfd_notifiers, + + null_pointer=_null_pointer, + added_cb_p=cast(_null_pointer, libusb1.libusb_pollfd_added_cb_p), + removed_cb_p=cast(_null_pointer, libusb1.libusb_pollfd_removed_cb_p), + ): + libusb_set_pollfd_notifiers( + context_p, + added_cb_p, + removed_cb_p, + null_pointer, + ) @_validContext def getNextTimeout(self): @@ -2496,9 +2639,10 @@ def getNextTimeout(self): self.__context_p, byref(timeval)) if result == 0: return None - elif result == 1: + if result == 1: return timeval.tv_sec + (timeval.tv_usec * 0.000001) raiseUSBError(result) + return None # unreachable, to make pylint happy @_validContext def setDebug(self, level): @@ -2511,30 +2655,37 @@ def setDebug(self, level): @_validContext def tryLockEvents(self): - """ - See libusb_try_lock_events doc. - """ + warnings.warn( + 'You may not be able to unlock in the event of USBContext exit. ' + 'Consider looping over handleEvents() in a thread.', + DeprecationWarning, + ) return libusb1.libusb_try_lock_events(self.__context_p) @_validContext def lockEvents(self): - """ - See libusb_lock_events doc. - """ + warnings.warn( + 'You may not be able to unlock in the event of USBContext exit. ' + 'Consider looping over handleEvents() in a thread.', + DeprecationWarning, + ) libusb1.libusb_lock_events(self.__context_p) @_validContext def lockEventWaiters(self): - """ - See libusb_lock_event_waiters doc. - """ + warnings.warn( + 'You may not be able to unlock in the event of USBContext exit. ' + 'Consider looping over handleEvents() in a thread.', + DeprecationWarning, + ) libusb1.libusb_lock_event_waiters(self.__context_p) @_validContext def waitForEvent(self, tv=0): - """ - See libusb_wait_for_event doc. - """ + warnings.warn( + 'Consider looping over handleEvents() in a thread.', + DeprecationWarning, + ) if tv is None: tv = 0 tv_s = int(tv) @@ -2543,30 +2694,38 @@ def waitForEvent(self, tv=0): @_validContext def unlockEventWaiters(self): - """ - See libusb_unlock_event_waiters doc. - """ + warnings.warn( + 'This method will lock in the event of USBContext exit, ' + 'preventing libusb lock release. ' + 'Consider looping over handleEvents() in a thread.', + DeprecationWarning, + ) libusb1.libusb_unlock_event_waiters(self.__context_p) @_validContext def eventHandlingOK(self): - """ - See libusb_event_handling_ok doc. - """ + warnings.warn( + 'Consider looping over handleEvents() in a thread.', + DeprecationWarning, + ) return libusb1.libusb_event_handling_ok(self.__context_p) @_validContext def unlockEvents(self): - """ - See libusb_unlock_events doc. - """ + warnings.warn( + 'This method will lock in the event of USBContext exit, ' + 'preventing libusb lock release. ' + 'Consider looping over handleEvents() in a thread.', + DeprecationWarning, + ) libusb1.libusb_unlock_events(self.__context_p) @_validContext def handleEventsLocked(self): - """ - See libusb_handle_events_locked doc. - """ + warnings.warn( + 'Consider looping over handleEvents() in a thread.', + DeprecationWarning, + ) # XXX: does tv parameter need to be exposed ? mayRaiseUSBError(libusb1.libusb_handle_events_locked( self.__context_p, _zero_tv_p, @@ -2574,9 +2733,10 @@ def handleEventsLocked(self): @_validContext def eventHandlerActive(self): - """ - See libusb_event_handler_active doc. - """ + warnings.warn( + 'Consider looping over handleEvents() in a thread.', + DeprecationWarning, + ) return libusb1.libusb_event_handler_active(self.__context_p) @staticmethod @@ -2616,16 +2776,19 @@ def hotplugRegisterCallback( it cannot call any synchronous libusb function. """ def wrapped_callback(context_p, device_p, event, _): - assert addressof(context_p.contents) == addressof( - self.__context_p.contents), (context_p, self.__context_p) + assert context_p == self.__context_p.value, ( + context_p, self.__context_p, + ) device = USBDevice( - self, - device_p, + context=self, + device_p=device_p, + getFinalizer=self._getFinalizer, # pylint: disable=undefined-variable - event != HOTPLUG_EVENT_DEVICE_LEFT, + can_load_configuration=event != HOTPLUG_EVENT_DEVICE_LEFT, # pylint: enable=undefined-variable + can_change_refcount=True, + handle_p=None, ) - self.__close_set.add(device) unregister = bool(callback( self, device, @@ -2659,6 +2822,41 @@ def hotplugDeregisterCallback(self, handle): del self.__hotplug_callback_dict[handle] libusb1.libusb_hotplug_deregister_callback(self.__context_p, handle) + def __log_callback(self, _unused_context_p, level, value): + """ + Internal log callback function, calls into the user-provided function + if any. + """ + # Note: as of libusb1, context_p is de facto guaranteed to be us: + # ctx->log_handler(ctx, level, buf); + # so do not bother checking. + user_log_callback = self.__user_log_callback + if user_log_callback is not None: + user_log_callback(self, level, value) + + @_validContext + def setLogCallback(self, log_callback): + """ + Change the active log callback for this context. + + log_callback (None, (USBContext, int, bytes): None) + The function called when libusb emits log messages for the current + context. + None to disable the callback. + """ + user_log_callback_was_unset = self.__user_log_callback is None + self.__user_log_callback = log_callback + if user_log_callback_was_unset != (log_callback is None): + libusb1.libusb_set_log_cb( + self.__context_p, + ( + _NULL_LOG_CALLBACK + if log_callback is None else + self.__log_callback_p + ), + libusb1.LIBUSB_LOG_CB_CONTEXT, + ) + del USBContext._validContext def getVersion(): @@ -2672,7 +2870,10 @@ def getVersion(): - rc - describe Returns (0, 0, 0, 0, '', '') if libusb doesn't have required entry point. + + Calls loadLibrary. """ + loadLibrary() version = libusb1.libusb_get_version().contents return Version( version.major, @@ -2692,9 +2893,93 @@ def hasCapability(capability): CAP_HAS_HOTPLUG CAP_HAS_HID_ACCESS CAP_SUPPORTS_DETACH_KERNEL_DRIVER + + Calls loadLibrary. """ + loadLibrary() return libusb1.libusb_has_capability(capability) +class __GlobalLogCallback: # pylint: disable=too-few-public-methods + """ + Singleton class keeping a reference to the global log callback function + so it is unregistered from libusb before the module gets garbage-collected. + """ + __user_log_callback = None + __finalizer = None + + def __init__(self): + self.__log_callback_p = libusb1.libusb_log_cb_p(self.__log_callback) + + @staticmethod + def __close( + libusb_set_log_cb, + LIBUSB_LOG_CB_GLOBAL, + ): + libusb_set_log_cb(None, _NULL_LOG_CALLBACK, LIBUSB_LOG_CB_GLOBAL) + + def __log_callback(self, context_p, level, message): + # As of at least libusb1, context_p of the global log callback + # is always NULL, even when the event originated from a context. + # So just ignore it here, forcing it to None. + # Should it change later, then some USBContext lookup will be + # needed. + _ = context_p + user_log_callback = self.__user_log_callback + if user_log_callback is not None: + user_log_callback(None, level, message) + + def __call__(self, log_callback): + """ + Set the global log callback. + + log_callback (None, (None, int, bytes): None) + A callable to set as libusb global log callback, or None to disable + this feature. + The first argument should be ignored. + The second argument is the message level. + The third argument is the message itself, as bytes. + + Calls loadLibrary. + libusb_set_log_cb will be called once more during module teardown. + """ + if self.__finalizer is None: + # Lazy initialisation, so loadLibrary is not called at + # module load time. + loadLibrary() + self.__finalizer = weakref.finalize( + self, + self.__close, # Note: static method + libusb_set_log_cb=libusb1.libusb_set_log_cb, + LIBUSB_LOG_CB_GLOBAL=libusb1.LIBUSB_LOG_CB_GLOBAL, + ) + user_log_callback_was_unset = self.__user_log_callback is None + self.__user_log_callback = log_callback + if user_log_callback_was_unset != (log_callback is None): + libusb1.libusb_set_log_cb( + None, + ( + _NULL_LOG_CALLBACK + if log_callback is None else + self.__log_callback_p + ), + libusb1.LIBUSB_LOG_CB_GLOBAL, + ) +setLogCallback = __GlobalLogCallback().__call__ + +def setLocale(locale): + """ + Set locale used for translatable libusb1 messages. + + locale (str) + 2 letter ISO 639-1 code, optionally followed by region and codeset. + + Calls loadLibrary. + """ + loadLibrary() + mayRaiseUSBError( + libusb1.libusb_setlocale(locale.encode('ascii')) + ) + class LibUSBContext(USBContext): """ Backward-compatibility alias for USBContext. @@ -2704,4 +2989,6 @@ def __init__(self): 'LibUSBContext is being renamed to USBContext', DeprecationWarning, ) - super(LibUSBContext, self).__init__() + super().__init__() + +loadLibrary = libusb1.loadLibrary diff --git a/usb1/__pyinstaller/__init__.py b/usb1/__pyinstaller/__init__.py new file mode 100644 index 0000000..1cf833b --- /dev/null +++ b/usb1/__pyinstaller/__init__.py @@ -0,0 +1,10 @@ +# pylint: disable=missing-module-docstring +# pylint: disable=missing-function-docstring +# pylint: disable=invalid-name +import os + +def get_hook_dirs(): + return [os.path.dirname(__file__)] + +def get_PyInstaller_tests(): + return [os.path.dirname(__file__)] diff --git a/usb1/__pyinstaller/hook-usb1.py b/usb1/__pyinstaller/hook-usb1.py new file mode 100644 index 0000000..d9d3cc8 --- /dev/null +++ b/usb1/__pyinstaller/hook-usb1.py @@ -0,0 +1,12 @@ +# pylint: disable=missing-module-docstring +# pylint: disable=invalid-name +import logging +# pylint: disable=import-error +from PyInstaller.utils.hooks import collect_dynamic_libs +# pylint: enable=import-error + +logger = logging.getLogger(__name__) +logger.info("--- libusb1 pyinstaller hook ---") +binaries = collect_dynamic_libs('usb1') + +logger.info("Added libusb binaries: %s", binaries) diff --git a/usb1/__pyinstaller/test_libusb1_packaging.py b/usb1/__pyinstaller/test_libusb1_packaging.py new file mode 100644 index 0000000..9799995 --- /dev/null +++ b/usb1/__pyinstaller/test_libusb1_packaging.py @@ -0,0 +1,37 @@ +""" +PyTest testcases for the pyinstaller hook + +Instructions to run these tests: + +- Install PyInstaller with the hook_testing extra requirements: + > pip install pyinstaller[hook_testing] + +- Run the tests using the provided utility by pyinstaller: + > python -m PyInstaller.utils.run_tests --include_only usb1 + +""" +# pylint: disable=missing-function-docstring +import subprocess +# pylint: disable=import-error +from PyInstaller import __main__ as pyi_main +# pylint: enable=import-error + + +def test_pyi_hooksample(tmp_path): + app_name = "userapp" + workpath = tmp_path / "build" + distpath = tmp_path / "dist" + app = tmp_path / (app_name + ".py") + app.write_text("\n".join([ + "import usb1", + "print(usb1.getVersion())" + ])) + args = [ + # Place all generated files in ``tmp_path``. + '--workpath', str(workpath), + '--distpath', str(distpath), + '--specpath', str(tmp_path), + str(app), + ] + pyi_main.run(args) + subprocess.run([str(distpath / app_name / app_name)], check=True) diff --git a/usb1/_libusb1.py b/usb1/_libusb1.py new file mode 100644 index 0000000..008966c --- /dev/null +++ b/usb1/_libusb1.py @@ -0,0 +1,1527 @@ +# Copyright (C) 2010-2021 Vincent Pelletier +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +# pylint: disable=invalid-name, too-few-public-methods, too-many-arguments +# pylint: disable=missing-docstring, too-many-lines +""" +Python ctypes bindings for libusb-1.0. + +You should not need to import this if you use usb1 module. + +Declares all constants, data structures and exported symbols. +Some are only available after calling loadLibrary. +""" +from ctypes import ( + Structure, LittleEndianStructure, + Union, + CFUNCTYPE, POINTER, addressof, sizeof, cast, + c_short, c_int, c_uint, c_long, c_longlong, + c_uint8, c_uint16, c_uint32, + c_void_p, c_char_p, py_object, pointer, c_char, + c_ssize_t, CDLL +) +import ctypes.util +import errno +import os.path +import platform +import sys +from threading import Lock + +class Enum: + def __init__(self, member_dict, scope_dict=None): + if scope_dict is None: + # Affect caller's locals, not this module's. + # pylint: disable=protected-access + scope_dict = sys._getframe(1).f_locals + # pylint: enable=protected-access + forward_dict = {} + reverse_dict = {} + next_value = 0 + for name, value in member_dict.items(): + if value is None: + value = next_value + next_value += 1 + forward_dict[name] = value + if value in reverse_dict: + raise ValueError( + 'Multiple names for value ' + f'{value!r}: {reverse_dict[value]!r}, {name!r}' + ) + reverse_dict[value] = name + scope_dict[name] = value + self.forward_dict = forward_dict + self.reverse_dict = reverse_dict + + def __call__(self, value): + return self.reverse_dict[value] + + def get(self, value, default=None): + return self.reverse_dict.get(value, default) + +def buffer_at(address, length): + """ + Simular to ctypes.string_at, but zero-copy and requires an integer address. + """ + return bytearray((c_char * length).from_address(address)) + +_desc_type_dict = { + 'b': c_uint8, + 'bcd': c_uint16, + 'bm': c_uint8, + 'dw': c_uint32, + 'i': c_uint8, + 'id': c_uint16, + 'w': c_uint16, +} + +def newStruct(field_name_list): + """ + Create a ctype structure class based on USB standard field naming + (type-prefixed). + """ + field_list = [] + append = field_list.append + for field in field_name_list: + type_prefix = '' + for char in field: + if not char.islower(): + break + type_prefix += char + append((field, _desc_type_dict[type_prefix])) + result = type('some_descriptor', (LittleEndianStructure, ), {}) + # Not using type()'s 3rd param to initialise class, as per ctypes + # documentation: + # _pack_ must already be defined when _fields_ is assigned, otherwise it + # will have no effect. + # pylint: disable=protected-access + result._pack_ = 1 + result._fields_ = field_list + # pylint: enable=protected-access + return result + +def newDescriptor(field_name_list): + """ + Create a USB descriptor ctype structure, ie starting with bLength and + bDescriptorType fields. + + See newStruct(). + """ + return newStruct(['bLength', 'bDescriptorType'] + list(field_name_list)) + +# Stand-in for the function until libusb is loaded - if the current libusb +# version has this export. +def libusb_strerror(_unused_errcode): + return None + +class USBError(Exception): + value = None + + def __init__(self, value=None): + Exception.__init__(self) + if value is not None: + self.value = value + + def __str__(self): + return f'{libusb_error.get(self.value, "Unknown error")} [{self.value}]' + + def getMessage(self): + """ + Get user-friendly message representing the current error, and based on + the current locale. + + Returns str, or None. + If libusb has not been loaded yet, returns None. + """ + # pylint: disable=assignment-from-none + message = libusb_strerror(self.value) + # pylint: enable=assignment-from-none + if message is not None: + message = message.decode('utf-8') + return message + +c_uchar = c_uint8 +c_int_p = POINTER(c_int) +intptr_t = { + sizeof(x): x + for x in (c_int, c_long, c_longlong) +}[sizeof(c_void_p)] + +LITTLE_ENDIAN = sys.byteorder == 'little' + +class timeval(Structure): + _fields_ = [('tv_sec', c_long), + ('tv_usec', c_long)] +timeval_p = POINTER(timeval) + +if platform.system() == 'Windows': + LIBUSB_CALL_FUNCTYPE = ctypes.WINFUNCTYPE +else: + LIBUSB_CALL_FUNCTYPE = CFUNCTYPE + +def __getLibrary(): + my_dir = os.path.dirname(__file__) + system = platform.system() + # If this is a binary wheel, try to use an integrated libusb first. + # To use the libusb from the Python installation or the OS, install + # from sdist: + # > pip install --no-binary :all: libusb1 + if system == 'Windows': + dll_loader = ctypes.WinDLL + libusb_list = [ + os.path.join(my_dir, 'libusb-1.0.dll'), + 'libusb-1.0.dll', + ] + find_library = None + else: + dll_loader = CDLL + if system == 'Darwin': + libusb_list = [ + os.path.join(my_dir, 'libusb-1.0.dylib'), + 'libusb-1.0.dylib', + # macport standard path + '/opt/local/lib/libusb-1.0.dylib', + # fink standard path + '/sw/lib/libusb-1.0.dylib', + # homebrew standard path for symlink (Apple M1 Silicon) + '/opt/homebrew/opt/libusb/lib/libusb-1.0.dylib', + ] + find_library = None + else: + # .so.0 should be the optimal suffix + # .so is for BBB, especially if libusb-1.0.so was bundled in some + # uses of this module. + libusb_list = [ + os.path.join(my_dir, 'libusb-1.0.so.0'), + os.path.join(my_dir, 'libusb-1.0.so'), + 'libusb-1.0.so.0', + 'libusb-1.0.so', + ] + find_library = ( + # libusb.so.2 on FreeBSD: load('libusb.so') would work fine, but... + # libusb.so.2debian on Debian GNU/kFreeBSD: here it wouldn't work. + 'usb' + if 'FreeBSD' in system else + 'usb-1.0' + ) + for filename in libusb_list: + try: + return dll_loader(filename, use_errno=True, use_last_error=True) + except OSError: + pass + if find_library is not None: + filename = ctypes.util.find_library(find_library) + if filename is not None: + return dll_loader(filename, use_errno=True, use_last_error=True) + raise OSError( + errno.ENOENT, + 'cannot find a suitable libusb-1.0', + libusb_list, + ) + +__load_lock = Lock() +__loaded = False + +def loadLibrary(libusb=None): + """ + Load C library. + + libusb (ctypes.CDLL, ctypes.WinDLL, None) + If None, the C library will be searched for in several platform- + dependent paths. + Otherwise, this is the C library to use as libusb. This is to allow + customising library lookup logic. + + Only the first call actually loads the library. + + Returns True if any of the following is true: + - the library was loaded by this call + - libusb argument is None + - libusb argument is the same object that was initially loaded. + """ + global __loaded # pylint: disable=global-statement + if not __loaded: # avoid touching the lock if we are already loaded + with __load_lock: + if not __loaded: # we were not loaded, check again under lock + __loadLibrary(libusb) + __loaded = True + return True + return libusb is None or globals()['libusb'] is libusb + +def __loadLibrary(libusb): # pylint: disable=too-many-locals,too-many-branches,too-many-statements + # WARNING: every local in this function will be stored in + # globals . Treat this namespace the same as the module's. + if libusb is None: + libusb = __getLibrary() + + _libusb_transfer_fields = [ + ('dev_handle', libusb_device_handle_p), + ('flags', c_uint8), + ('endpoint', c_uchar), + ('type', c_uchar), + ('timeout', c_uint), + ('status', c_int), # enum libusb_transfer_status + ('length', c_int), + ('actual_length', c_int), + ('callback', libusb_transfer_cb_fn_p), + ('user_data', c_void_p), + ('buffer', c_void_p), + ('num_iso_packets', c_int), + ('iso_packet_desc', libusb_iso_packet_descriptor) + ] + if 'FreeBSD' in platform.system() and getattr( + libusb, 'libusb_get_string_descriptor', None + ) is None: + # Old FreeBSD version has a slight ABI incompatibility. + # Work around it unless libusb_get_string_descriptor is available, as it + # is only available on fixed versions. + assert _libusb_transfer_fields[2][0] == 'endpoint' + _libusb_transfer_fields[2] = ('endpoint', c_uint32) + assert _libusb_transfer_fields[11][0] == 'num_iso_packets' + _libusb_transfer_fields.insert(11, ('os_priv', c_void_p)) + + # pylint: disable=protected-access + libusb_transfer._fields_ = _libusb_transfer_fields + # pylint: enable=protected-access + + #int libusb_init_context(libusb_context **ctx, + # const struct libusb_init_option options[], int num_options); + try: + libusb_init_context = libusb.libusb_init_context + except AttributeError: + # Note: libusb_init is deprecated, only retrieve it when + # libusb_init_context is absent. + #int libusb_init(libusb_context **ctx); + libusb_init = libusb.libusb_init + libusb_init.argtypes = [libusb_context_p_p] + def libusb_init_context(ctx, _unused_options, num_options): + if num_options: + raise ValueError( + 'libusb_init_context is not available in the current ' + 'libusb version, initialisation options are not available', + ) + return libusb_init(ctx) + else: + libusb_init_context.argtypes = [ + libusb_context_p_p, + POINTER(libusb_init_option), + c_int, + ] + + #void libusb_exit(libusb_context *ctx); + libusb_exit = libusb.libusb_exit + libusb_exit.argtypes = [libusb_context_p] + libusb_exit.restype = None + #void libusb_set_debug(libusb_context *ctx, int level); + libusb_set_debug = libusb.libusb_set_debug + libusb_set_debug.argtypes = [libusb_context_p, c_int] + libusb_set_debug.restype = None + #void libusb_set_log_cb(libusb_context *ctx, libusb_log_cb cb, int mode); + try: + libusb_set_log_cb = libusb.libusb_set_log_cb + except AttributeError: + def libusb_set_log_cb(ctx, cb, mode): + pass + else: + libusb_set_log_cb.argtypes = [libusb_context_p, libusb_log_cb_p, c_int] + libusb_set_log_cb.restype = None + #const struct libusb_version * libusb_get_version(void); + try: + libusb_get_version = libusb.libusb_get_version + except AttributeError: + _dummy_version = libusb_version(0, 0, 0, 0, b'', b'') + _dummy_version_p = pointer(_dummy_version) + def libusb_get_version(): + return _dummy_version_p + else: + libusb_get_version.argtypes = [] + libusb_get_version.restype = POINTER(libusb_version) + #int libusb_has_capability(uint32_t capability); + try: + libusb_has_capability = libusb.libusb_has_capability + except AttributeError: + def libusb_has_capability(_): + return 0 + else: + libusb_has_capability.argtypes = [c_uint32] + libusb_has_capability.restype = c_int + try: + # Note: Should be equivalent to libusb_error.get (except libusb_error.get + # one raises on unknown values). + #char *libusb_error_name(int errcode); + libusb_error_name = libusb.libusb_error_name + except AttributeError: + # pylint: disable=unused-argument + def libusb_error_name(errcode): + return None + # pylint: enable=unused-argument + else: + libusb_error_name.argtypes = [c_int] + libusb_error_name.restype = c_char_p + #int libusb_setlocale(const char *locale); + try: + libusb_setlocale = libusb.libusb_setlocale + except AttributeError: + def libusb_setlocale(_unused_locale): + pass + else: + libusb_setlocale.argtypes = [c_char_p] + libusb_setlocale.restype = int + #const char *libusb_strerror(int errcode); + try: + # pylint: disable=redefined-outer-name + libusb_strerror = libusb.libusb_strerror + # pylint: enable=redefined-outer-name + except AttributeError: + pass + else: + libusb_strerror.argtypes = [c_int] + libusb_strerror.restype = c_char_p + #ssize_t libusb_get_device_list(libusb_context *ctx, + # libusb_device ***list); + libusb_get_device_list = libusb.libusb_get_device_list + libusb_get_device_list.argtypes = [libusb_context_p, libusb_device_p_p_p] + libusb_get_device_list.restype = c_ssize_t + #void libusb_free_device_list(libusb_device **list, int unref_devices); + libusb_free_device_list = libusb.libusb_free_device_list + libusb_free_device_list.argtypes = [libusb_device_p_p, c_int] + libusb_free_device_list.restype = None + #libusb_device *libusb_ref_device(libusb_device *dev); + libusb_ref_device = libusb.libusb_ref_device + libusb_ref_device.argtypes = [libusb_device_p] + libusb_ref_device.restype = libusb_device_p + #void libusb_unref_device(libusb_device *dev); + libusb_unref_device = libusb.libusb_unref_device + libusb_unref_device.argtypes = [libusb_device_p] + libusb_unref_device.restype = None + + #int libusb_get_configuration(libusb_device_handle *dev, int *config); + libusb_get_configuration = libusb.libusb_get_configuration + libusb_get_configuration.argtypes = [libusb_device_handle_p, c_int_p] + #int libusb_get_device_descriptor(libusb_device *dev, + # struct libusb_device_descriptor *desc); + libusb_get_device_descriptor = libusb.libusb_get_device_descriptor + libusb_get_device_descriptor.argtypes = [ + libusb_device_p, libusb_device_descriptor_p] + #int libusb_get_active_config_descriptor(libusb_device *dev, + # struct libusb_config_descriptor **config); + libusb_get_active_config_descriptor = libusb.libusb_get_active_config_descriptor + libusb_get_active_config_descriptor.argtypes = [ + libusb_device_p, libusb_config_descriptor_p_p] + #int libusb_get_config_descriptor(libusb_device *dev, uint8_t config_index, + # struct libusb_config_descriptor **config); + libusb_get_config_descriptor = libusb.libusb_get_config_descriptor + libusb_get_config_descriptor.argtypes = [ + libusb_device_p, c_uint8, libusb_config_descriptor_p_p] + #int libusb_get_config_descriptor_by_value(libusb_device *dev, + # uint8_t bConfigurationValue, struct libusb_config_descriptor **config); + libusb_get_config_descriptor_by_value = \ + libusb.libusb_get_config_descriptor_by_value + libusb_get_config_descriptor_by_value.argtypes = [ + libusb_device_p, c_uint8, libusb_config_descriptor_p_p] + #void libusb_free_config_descriptor(struct libusb_config_descriptor *config); + libusb_free_config_descriptor = libusb.libusb_free_config_descriptor + libusb_free_config_descriptor.argtypes = [libusb_config_descriptor_p] + libusb_free_config_descriptor.restype = None + #uint8_t libusb_get_bus_number(libusb_device *dev); + libusb_get_bus_number = libusb.libusb_get_bus_number + libusb_get_bus_number.argtypes = [libusb_device_p] + libusb_get_bus_number.restype = c_uint8 + try: + #uint8_t libusb_get_port_number(libusb_device *dev); + libusb_get_port_number = libusb.libusb_get_port_number + except AttributeError: + pass + else: + libusb_get_port_number.argtypes = [libusb_device_p] + libusb_get_port_number.restype = c_uint8 + try: + #int libusb_get_port_numbers(libusb_device *dev, + # uint8_t* port_numbers, int port_numbers_len); + libusb_get_port_numbers = libusb.libusb_get_port_numbers + except AttributeError: + pass + else: + libusb_get_port_numbers.argtypes = [ + libusb_device_p, POINTER(c_uint8), c_int] + libusb_get_port_numbers.restype = c_int + # Missing: libusb_get_port_path (deprecated since 1.0.16) + try: + #libusb_device *libusb_get_parent(libusb_device *dev); + libusb_get_parent = libusb.libusb_get_parent + except AttributeError: + pass + else: + libusb_get_parent.argtypes = [libusb_device_p] + libusb_get_parent.restype = libusb_device_p + #uint8_t libusb_get_device_address(libusb_device *dev); + libusb_get_device_address = libusb.libusb_get_device_address + libusb_get_device_address.argtypes = [libusb_device_p] + libusb_get_device_address.restype = c_uint8 + try: + #int libusb_get_device_speed(libusb_device *dev); + libusb_get_device_speed = libusb.libusb_get_device_speed + except AttributeError: + # Place holder + def libusb_get_device_speed(_): + # pylint: disable=undefined-variable + return LIBUSB_SPEED_UNKNOWN + # pylint: enable=undefined-variable + else: + libusb_get_device_speed.argtypes = [libusb_device_p] + #int libusb_get_max_packet_size(libusb_device *dev, unsigned char endpoint); + libusb_get_max_packet_size = libusb.libusb_get_max_packet_size + libusb_get_max_packet_size.argtypes = [libusb_device_p, c_uchar] + #int libusb_get_max_iso_packet_size(libusb_device *dev, unsigned char endpoint); + try: + libusb_get_max_iso_packet_size = libusb.libusb_get_max_iso_packet_size + except AttributeError: + # FreeBSD's reimplementation of the API [used to ]lack[s] this function. + # It has been added in r234193, but is lacking in default 9.x install as + # of this change. Provide a fallback to error-out only if actually used. + # pylint: disable=unused-argument + def libusb_get_max_iso_packet_size(_, __): + raise NotImplementedError + # pylint: enable=unused-argument + else: + libusb_get_max_iso_packet_size.argtypes = [libusb_device_p, c_uchar] + + #int libusb_wrap_sys_device(libusb_context *ctx, intptr_t sys_dev, + # libusb_device_handle **dev_handle); + try: + libusb_wrap_sys_device = libusb.libusb_wrap_sys_device + except AttributeError: + # pylint: enable=unused-argument + def libusb_wrap_sys_device(_, __, ___): + raise NotImplementedError + # pylint: disable=unused-argument + else: + libusb_wrap_sys_device.restype = c_int + libusb_wrap_sys_device.argtypes = [ + libusb_context_p, + intptr_t, + libusb_device_handle_p_p, + ] + + #int libusb_open(libusb_device *dev, libusb_device_handle **handle); + libusb_open = libusb.libusb_open + libusb_open.argtypes = [libusb_device_p, libusb_device_handle_p_p] + #void libusb_close(libusb_device_handle *dev_handle); + libusb_close = libusb.libusb_close + libusb_close.argtypes = [libusb_device_handle_p] + libusb_close.restype = None + #libusb_device *libusb_get_device(libusb_device_handle *dev_handle); + libusb_get_device = libusb.libusb_get_device + libusb_get_device.argtypes = [libusb_device_handle_p] + libusb_get_device.restype = libusb_device_p + + #int libusb_set_configuration(libusb_device_handle *dev, int configuration); + libusb_set_configuration = libusb.libusb_set_configuration + libusb_set_configuration.argtypes = [libusb_device_handle_p, c_int] + #int libusb_claim_interface(libusb_device_handle *dev, int iface); + libusb_claim_interface = libusb.libusb_claim_interface + libusb_claim_interface.argtypes = [libusb_device_handle_p, c_int] + #int libusb_release_interface(libusb_device_handle *dev, int iface); + libusb_release_interface = libusb.libusb_release_interface + libusb_release_interface.argtypes = [libusb_device_handle_p, c_int] + + #libusb_device_handle *libusb_open_device_with_vid_pid(libusb_context *ctx, + # uint16_t vendor_id, uint16_t product_id); + libusb_open_device_with_vid_pid = libusb.libusb_open_device_with_vid_pid + libusb_open_device_with_vid_pid.argtypes = [ + libusb_context_p, c_uint16, c_uint16] + libusb_open_device_with_vid_pid.restype = libusb_device_handle_p + + #int libusb_set_interface_alt_setting(libusb_device_handle *dev, + # int interface_number, int alternate_setting); + libusb_set_interface_alt_setting = libusb.libusb_set_interface_alt_setting + libusb_set_interface_alt_setting.argtypes = [ + libusb_device_handle_p, c_int, c_int] + #int libusb_clear_halt(libusb_device_handle *dev, unsigned char endpoint); + libusb_clear_halt = libusb.libusb_clear_halt + libusb_clear_halt.argtypes = [libusb_device_handle_p, c_uchar] + #int libusb_reset_device(libusb_device_handle *dev); + libusb_reset_device = libusb.libusb_reset_device + libusb_reset_device.argtypes = [libusb_device_handle_p] + + #int libusb_kernel_driver_active(libusb_device_handle *dev, int interface); + libusb_kernel_driver_active = libusb.libusb_kernel_driver_active + libusb_kernel_driver_active.argtypes = [libusb_device_handle_p, c_int] + #int libusb_detach_kernel_driver(libusb_device_handle *dev, int interface); + libusb_detach_kernel_driver = libusb.libusb_detach_kernel_driver + libusb_detach_kernel_driver.argtypes = [libusb_device_handle_p, c_int] + #int libusb_attach_kernel_driver(libusb_device_handle *dev, int interface); + libusb_attach_kernel_driver = libusb.libusb_attach_kernel_driver + libusb_attach_kernel_driver.argtypes = [libusb_device_handle_p, c_int] + try: + #int libusb_set_auto_detach_kernel_driver( + # libusb_device_handle *dev, int enable); + libusb_set_auto_detach_kernel_driver = \ + libusb.libusb_set_auto_detach_kernel_driver + except AttributeError: + pass + else: + libusb_set_auto_detach_kernel_driver.argtypes = [ + libusb_device_handle_p, c_int] + libusb_set_auto_detach_kernel_driver.restype = c_int + + #struct libusb_transfer *libusb_alloc_transfer(int iso_packets); + libusb_alloc_transfer = libusb.libusb_alloc_transfer + libusb_alloc_transfer.argtypes = [c_int] + libusb_alloc_transfer.restype = libusb_transfer_p + #int libusb_submit_transfer(struct libusb_transfer *transfer); + libusb_submit_transfer = libusb.libusb_submit_transfer + libusb_submit_transfer.argtypes = [libusb_transfer_p] + #int libusb_cancel_transfer(struct libusb_transfer *transfer); + libusb_cancel_transfer = libusb.libusb_cancel_transfer + libusb_cancel_transfer.argtypes = [libusb_transfer_p] + #void libusb_free_transfer(struct libusb_transfer *transfer); + libusb_free_transfer = libusb.libusb_free_transfer + libusb_free_transfer.argtypes = [libusb_transfer_p] + libusb_free_transfer.restype = None + + # sync I/O + + #int libusb_control_transfer(libusb_device_handle *dev_handle, + # uint8_t request_type, uint8_t request, uint16_t value, uint16_t index, + # unsigned char *data, uint16_t length, unsigned int timeout); + libusb_control_transfer = libusb.libusb_control_transfer + libusb_control_transfer.argtypes = [libusb_device_handle_p, c_uint8, c_uint8, + c_uint16, c_uint16, c_void_p, c_uint16, + c_uint] + + #int libusb_bulk_transfer(libusb_device_handle *dev_handle, + # unsigned char endpoint, unsigned char *data, int length, + # int *actual_length, unsigned int timeout); + libusb_bulk_transfer = libusb.libusb_bulk_transfer + libusb_bulk_transfer.argtypes = [libusb_device_handle_p, c_uchar, c_void_p, + c_int, c_int_p, c_uint] + + #int libusb_interrupt_transfer(libusb_device_handle *dev_handle, + # unsigned char endpoint, unsigned char *data, int length, + # int *actual_length, unsigned int timeout); + libusb_interrupt_transfer = libusb.libusb_interrupt_transfer + libusb_interrupt_transfer.argtypes = [libusb_device_handle_p, c_uchar, + c_void_p, c_int, c_int_p, c_uint] + + #int libusb_get_string_descriptor_ascii(libusb_device_handle *dev, + # uint8_t index, unsigned char *data, int length); + libusb_get_string_descriptor_ascii = libusb.libusb_get_string_descriptor_ascii + libusb_get_string_descriptor_ascii.argtypes = [libusb_device_handle_p, + c_uint8, c_void_p, c_int] + + # polling and timeouts + + #int libusb_try_lock_events(libusb_context *ctx); + libusb_try_lock_events = libusb.libusb_try_lock_events + libusb_try_lock_events.argtypes = [libusb_context_p] + #void libusb_lock_events(libusb_context *ctx); + libusb_lock_events = libusb.libusb_lock_events + libusb_lock_events.argtypes = [libusb_context_p] + #void libusb_unlock_events(libusb_context *ctx); + libusb_unlock_events = libusb.libusb_unlock_events + libusb_unlock_events.argtypes = [libusb_context_p] + libusb_unlock_events.restype = None + #int libusb_event_handling_ok(libusb_context *ctx); + libusb_event_handling_ok = libusb.libusb_event_handling_ok + libusb_event_handling_ok.argtypes = [libusb_context_p] + #int libusb_event_handler_active(libusb_context *ctx); + libusb_event_handler_active = libusb.libusb_event_handler_active + libusb_event_handler_active.argtypes = [libusb_context_p] + #void libusb_lock_event_waiters(libusb_context *ctx); + libusb_lock_event_waiters = libusb.libusb_lock_event_waiters + libusb_lock_event_waiters.argtypes = [libusb_context_p] + libusb_lock_event_waiters.restype = None + #void libusb_unlock_event_waiters(libusb_context *ctx); + libusb_unlock_event_waiters = libusb.libusb_unlock_event_waiters + libusb_unlock_event_waiters.argtypes = [] + libusb_unlock_event_waiters.restype = None + #int libusb_wait_for_event(libusb_context *ctx, struct timeval *tv); + libusb_wait_for_event = libusb.libusb_wait_for_event + libusb_wait_for_event.argtypes = [libusb_context_p, timeval_p] + + #int libusb_handle_events_timeout(libusb_context *ctx, struct timeval *tv); + libusb_handle_events_timeout = libusb.libusb_handle_events_timeout + libusb_handle_events_timeout.argtypes = [libusb_context_p, timeval_p] + #int libusb_handle_events_timeout_completed(libusb_context *ctx, + # struct timeval *tv, int *completed); + try: + libusb_handle_events_timeout_completed = libusb.\ + libusb_handle_events_timeout_completed + except AttributeError: + # No safe replacement possible. + pass + else: + libusb_handle_events_timeout_completed.argtypes = [ + libusb_context_p, timeval_p, c_int_p] + #int libusb_handle_events(libusb_context *ctx); + libusb_handle_events = libusb.libusb_handle_events + libusb_handle_events.argtypes = [libusb_context_p] + #int libusb_handle_events_completed(libusb_context *ctx, int *completed); + try: + libusb_handle_events_completed = libusb.libusb_handle_events_completed + except AttributeError: + # No safe replacement possible. + pass + else: + libusb_handle_events_completed.argtypes = [libusb_context_p, c_int_p] + #int libusb_handle_events_locked(libusb_context *ctx, struct timeval *tv); + libusb_handle_events_locked = libusb.libusb_handle_events_locked + libusb_handle_events_locked.argtypes = [libusb_context_p, timeval_p] + #void libusb_interrupt_event_handler(libusb_context *ctx); + try: + libusb_interrupt_event_handler = libusb.libusb_interrupt_event_handler + except AttributeError: + def libusb_interrupt_event_handler(_): + raise NotImplementedError + else: + libusb_interrupt_event_handler.argtypes = [libusb_context_p] + #int libusb_get_next_timeout(libusb_context *ctx, struct timeval *tv); + libusb_get_next_timeout = libusb.libusb_get_next_timeout + libusb_get_next_timeout.argtypes = [libusb_context_p, timeval_p] + + #const struct libusb_pollfd **libusb_get_pollfds(libusb_context *ctx); + libusb_get_pollfds = libusb.libusb_get_pollfds + libusb_get_pollfds.argtypes = [libusb_context_p] + libusb_get_pollfds.restype = libusb_pollfd_p_p + #void libusb_set_pollfd_notifiers(libusb_context *ctx, + # libusb_pollfd_added_cb added_cb, libusb_pollfd_removed_cb removed_cb, + # void *user_data); + libusb_set_pollfd_notifiers = libusb.libusb_set_pollfd_notifiers + libusb_set_pollfd_notifiers.argtypes = [libusb_context_p, + libusb_pollfd_added_cb_p, + libusb_pollfd_removed_cb_p, py_object] + libusb_set_pollfd_notifiers.restype = None + try: + #void libusb_get_pollfds(const struct libusb_pollfd **); + libusb_free_pollfds = libusb.libusb_free_pollfds + libusb_free_pollfds.argtypes = [libusb_pollfd_p_p] + libusb_free_pollfds.restype = None + except AttributeError: + # Not a safe replacement in general, but the versions of libusb that lack + # libusb_free_pollfds() only provide that function on *nix, where + # Python's free() and libusb's free() are ~always the same anyways. + libusb_free_pollfds = CDLL(None).free + + #int libusb_hotplug_register_callback(libusb_context *ctx, + # libusb_hotplug_event events, libusb_hotplug_flag flags, + # int vendor_id, int product_id, int dev_class, + # libusb_hotplug_callback_fn cb_fn, void *user_data, + # libusb_hotplug_callback_handle *handle); + try: + libusb_hotplug_register_callback = libusb.libusb_hotplug_register_callback + except AttributeError: + pass + else: + libusb_hotplug_register_callback.argtypes = [ + libusb_context_p, + c_int, c_int, + c_int, c_int, c_int, + libusb_hotplug_callback_fn_p, c_void_p, + POINTER(libusb_hotplug_callback_handle), + ] + libusb_hotplug_register_callback.restype = c_int + + #void libusb_hotplug_deregister_callback(libusb_context *ctx, + # libusb_hotplug_callback_handle handle); + try: + libusb_hotplug_deregister_callback = \ + libusb.libusb_hotplug_deregister_callback + except AttributeError: + pass + else: + libusb_hotplug_deregister_callback.argtypes = [ + libusb_context_p, + libusb_hotplug_callback_handle, + ] + libusb_hotplug_deregister_callback.restype = None + + # Note: no support possible for libusb_set_option as it uses a variadic + + globals().update(locals()) + +# libusb.h +def bswap16(x): + return ((x & 0xff) << 8) | (x >> 8) + +if LITTLE_ENDIAN: + def libusb_cpu_to_le16(x): + return x + def libusb_le16_to_cpu(x): + return x +else: + libusb_cpu_to_le16 = bswap16 + libusb_le16_to_cpu = bswap16 + +# standard USB stuff + +# Device and/or Interface Class codes +libusb_class_code = Enum({ + # In the context of a device descriptor, + # this bDeviceClass value indicates that each interface specifies its + # own class information and all interfaces operate independently. + 'LIBUSB_CLASS_PER_INTERFACE': 0, + # Audio class + 'LIBUSB_CLASS_AUDIO': 1, + # Communications class + 'LIBUSB_CLASS_COMM': 2, + # Human Interface Device class + 'LIBUSB_CLASS_HID': 3, + # Physical + 'LIBUSB_CLASS_PHYSICAL': 5, + # Printer class + 'LIBUSB_CLASS_PRINTER': 7, + # Picture transfer protocol class + 'LIBUSB_CLASS_PTP': 6, + # Mass storage class + 'LIBUSB_CLASS_MASS_STORAGE': 8, + # Hub class + 'LIBUSB_CLASS_HUB': 9, + # Data class + 'LIBUSB_CLASS_DATA': 10, + # Smart Card + 'LIBUSB_CLASS_SMART_CARD': 0x0b, + # Content Security + 'LIBUSB_CLASS_CONTENT_SECURITY': 0x0d, + # Video + 'LIBUSB_CLASS_VIDEO': 0x0e, + # Personal Healthcare + 'LIBUSB_CLASS_PERSONAL_HEALTHCARE': 0x0f, + # Diagnostic Device + 'LIBUSB_CLASS_DIAGNOSTIC_DEVICE': 0xdc, + # Wireless class + 'LIBUSB_CLASS_WIRELESS': 0xe0, + # Application class + 'LIBUSB_CLASS_APPLICATION': 0xfe, + # Class is vendor-specific + 'LIBUSB_CLASS_VENDOR_SPEC': 0xff +}) +# pylint: disable=undefined-variable +LIBUSB_CLASS_IMAGE = LIBUSB_CLASS_PTP +# pylint: enable=undefined-variable + +# Descriptor types as defined by the USB specification. +libusb_descriptor_type = Enum({ + # Device descriptor. See libusb_device_descriptor. + 'LIBUSB_DT_DEVICE': 0x01, + # Configuration descriptor. See libusb_config_descriptor. + 'LIBUSB_DT_CONFIG': 0x02, + # String descriptor + 'LIBUSB_DT_STRING': 0x03, + # Interface descriptor. See libusb_interface_descriptor. + 'LIBUSB_DT_INTERFACE': 0x04, + # Endpoint descriptor. See libusb_endpoint_descriptor. + 'LIBUSB_DT_ENDPOINT': 0x05, + # HID descriptor + 'LIBUSB_DT_HID': 0x21, + # HID report descriptor + 'LIBUSB_DT_REPORT': 0x22, + # Physical descriptor + 'LIBUSB_DT_PHYSICAL': 0x23, + # Hub descriptor + 'LIBUSB_DT_HUB': 0x29, +}) + +# Descriptor sizes per descriptor type +LIBUSB_DT_DEVICE_SIZE = 18 +LIBUSB_DT_CONFIG_SIZE = 9 +LIBUSB_DT_INTERFACE_SIZE = 9 +LIBUSB_DT_ENDPOINT_SIZE = 7 +LIBUSB_DT_ENDPOINT_AUDIO_SIZE = 9 # Audio extension +LIBUSB_DT_HUB_NONVAR_SIZE = 7 +LIBUSB_DT_SS_ENDPOINT_COMPANION_SIZE = 6 +LIBUSB_DT_BOS_SIZE = 5 +LIBUSB_DT_DEVICE_CAPABILITY_SIZE = 3 +LIBUSB_DT_INTERFACE_ASSOCIATION_SIZE = 8 + +# BOS descriptor sizes +LIBUSB_BT_USB_2_0_EXTENSION_SIZE = 7 +LIBUSB_BT_SS_USB_DEVICE_CAPABILITY_SIZE = 10 +LIBUSB_BT_SSPLUS_USB_DEVICE_CAPABILITY_SIZE = 12 +LIBUSB_BT_CONTAINER_ID_SIZE = 20 +LIBUSB_BT_PLATFORM_DESCRIPTOR_MIN_SIZE = 20 + +LIBUSB_DT_BOS_MAX_SIZE = ( + LIBUSB_DT_BOS_SIZE + + LIBUSB_BT_USB_2_0_EXTENSION_SIZE + + LIBUSB_BT_SS_USB_DEVICE_CAPABILITY_SIZE + + LIBUSB_BT_CONTAINER_ID_SIZE +) + +LIBUSB_ENDPOINT_ADDRESS_MASK = 0x0f # in bEndpointAddress +LIBUSB_ENDPOINT_DIR_MASK = 0x80 +# BBB +USB_ENDPOINT_ADDRESS_MASK = LIBUSB_ENDPOINT_ADDRESS_MASK +USB_ENDPOINT_DIR_MASK = LIBUSB_ENDPOINT_DIR_MASK + +# Endpoint direction. Values for bit 7 of the endpoint address scheme. +libusb_endpoint_direction = Enum({ + # In: device-to-host + 'LIBUSB_ENDPOINT_IN': 0x80, + # Out: host-to-device + 'LIBUSB_ENDPOINT_OUT': 0x00 +}) + +LIBUSB_TRANSFER_TYPE_MASK = 0x03 # in bmAttributes + +# Endpoint transfer type. Values for bits 0:1 of the endpoint attributes field. +libusb_transfer_type = Enum({ + # Control endpoint + 'LIBUSB_TRANSFER_TYPE_CONTROL': 0, + # Isochronous endpoint + 'LIBUSB_TRANSFER_TYPE_ISOCHRONOUS': 1, + # Bulk endpoint + 'LIBUSB_TRANSFER_TYPE_BULK': 2, + # Interrupt endpoint + 'LIBUSB_TRANSFER_TYPE_INTERRUPT': 3, +}) + +# Standard requests, as defined in table 9-3 of the USB2 specifications +libusb_standard_request = Enum({ + # Request status of the specific recipient + 'LIBUSB_REQUEST_GET_STATUS': 0x00, + # Clear or disable a specific feature + 'LIBUSB_REQUEST_CLEAR_FEATURE': 0x01, + # 0x02 is reserved + # Set or enable a specific feature + 'LIBUSB_REQUEST_SET_FEATURE': 0x03, + # 0x04 is reserved + # Set device address for all future accesses + 'LIBUSB_REQUEST_SET_ADDRESS': 0x05, + # Get the specified descriptor + 'LIBUSB_REQUEST_GET_DESCRIPTOR': 0x06, + # Used to update existing descriptors or add new descriptors + 'LIBUSB_REQUEST_SET_DESCRIPTOR': 0x07, + # Get the current device configuration value + 'LIBUSB_REQUEST_GET_CONFIGURATION': 0x08, + # Set device configuration + 'LIBUSB_REQUEST_SET_CONFIGURATION': 0x09, + # Return the selected alternate setting for the specified interface + 'LIBUSB_REQUEST_GET_INTERFACE': 0x0a, + # Select an alternate interface for the specified interface + 'LIBUSB_REQUEST_SET_INTERFACE': 0x0b, + # Set then report an endpoint's synchronization frame + 'LIBUSB_REQUEST_SYNCH_FRAME': 0x0c, +}) + +# Request type bits of the bmRequestType field in control transfers. +libusb_request_type = Enum({ + # Standard + 'LIBUSB_REQUEST_TYPE_STANDARD': (0x00 << 5), + # Class + 'LIBUSB_REQUEST_TYPE_CLASS': (0x01 << 5), + # Vendor + 'LIBUSB_REQUEST_TYPE_VENDOR': (0x02 << 5), + # Reserved + 'LIBUSB_REQUEST_TYPE_RESERVED': (0x03 << 5), +}) + +# BBB +# pylint: disable=undefined-variable +LIBUSB_TYPE_STANDARD = LIBUSB_REQUEST_TYPE_STANDARD +LIBUSB_TYPE_CLASS = LIBUSB_REQUEST_TYPE_CLASS +LIBUSB_TYPE_VENDOR = LIBUSB_REQUEST_TYPE_VENDOR +LIBUSB_TYPE_RESERVED = LIBUSB_REQUEST_TYPE_RESERVED +# pylint: enable=undefined-variable + +# Recipient bits of the bmRequestType field in control transfers. Values 4 +# through 31 are reserved. +libusb_request_recipient = Enum({ + # Device + 'LIBUSB_RECIPIENT_DEVICE': 0x00, + # Interface + 'LIBUSB_RECIPIENT_INTERFACE': 0x01, + # Endpoint + 'LIBUSB_RECIPIENT_ENDPOINT': 0x02, + # Other + 'LIBUSB_RECIPIENT_OTHER': 0x03, +}) + +LIBUSB_ISO_SYNC_TYPE_MASK = 0x0c + +# Synchronization type for isochronous endpoints. Values for bits 2:3 of the +# bmAttributes field in libusb_endpoint_descriptor. +libusb_iso_sync_type = Enum({ + # No synchronization + 'LIBUSB_ISO_SYNC_TYPE_NONE': 0, + # Asynchronous + 'LIBUSB_ISO_SYNC_TYPE_ASYNC': 1, + # Adaptive + 'LIBUSB_ISO_SYNC_TYPE_ADAPTIVE': 2, + # Synchronous + 'LIBUSB_ISO_SYNC_TYPE_SYNC': 3, +}) + +LIBUSB_ISO_USAGE_TYPE_MASK = 0x30 + +# Usage type for isochronous endpoints. Values for bits 4:5 of the +# bmAttributes field in libusb_endpoint_descriptor. +libusb_iso_usage_type = Enum({ + # Data endpoint + 'LIBUSB_ISO_USAGE_TYPE_DATA': 0, + # Feedback endpoint + 'LIBUSB_ISO_USAGE_TYPE_FEEDBACK': 1, + # Implicit feedback Data endpoint + 'LIBUSB_ISO_USAGE_TYPE_IMPLICIT': 2, +}) + +# A structure representing the standard USB device descriptor. This +# descriptor is documented in section 9.6.1 of the USB 2.0 specification. +# All multiple-byte fields are represented in host-endian format. +class libusb_device_descriptor(Structure): + _fields_ = [ + # Size of this descriptor (in bytes) + ('bLength', c_uint8), + # Descriptor type. Will have value LIBUSB_DT_DEVICE in this + # context. + ('bDescriptorType', c_uint8), + # USB specification release number in binary-coded decimal. A + # value of 0x0200 indicates USB 2.0, 0x0110 indicates USB 1.1, + # etc. + ('bcdUSB', c_uint16), + # USB-IF class code for the device. See libusb_class_code. + ('bDeviceClass', c_uint8), + # USB-IF subclass code for the device, qualified by the + # bDeviceClass value + ('bDeviceSubClass', c_uint8), + # USB-IF protocol code for the device, qualified by the + # bDeviceClass and bDeviceSubClass values + ('bDeviceProtocol', c_uint8), + # Maximum packet size for endpoint 0 + ('bMaxPacketSize0', c_uint8), + # USB-IF vendor ID + ('idVendor', c_uint16), + # USB-IF product ID + ('idProduct', c_uint16), + # Device release number in binary-coded decimal + ('bcdDevice', c_uint16), + # Index of string descriptor describing manufacturer + ('iManufacturer', c_uint8), + # Index of string descriptor describing product + ('iProduct', c_uint8), + # Index of string descriptor containing device serial number + ('iSerialNumber', c_uint8), + # Number of possible configurations + ('bNumConfigurations', c_uint8)] +libusb_device_descriptor_p = POINTER(libusb_device_descriptor) + +class libusb_endpoint_descriptor(Structure): + _fields_ = [ + ('bLength', c_uint8), + ('bDescriptorType', c_uint8), + ('bEndpointAddress', c_uint8), + ('bmAttributes', c_uint8), + ('wMaxPacketSize', c_uint16), + ('bInterval', c_uint8), + ('bRefresh', c_uint8), + ('bSynchAddress', c_uint8), + ('extra', c_void_p), + ('extra_length', c_int)] +libusb_endpoint_descriptor_p = POINTER(libusb_endpoint_descriptor) + +class libusb_interface_descriptor(Structure): + _fields_ = [ + ('bLength', c_uint8), + ('bDescriptorType', c_uint8), + ('bInterfaceNumber', c_uint8), + ('bAlternateSetting', c_uint8), + ('bNumEndpoints', c_uint8), + ('bInterfaceClass', c_uint8), + ('bInterfaceSubClass', c_uint8), + ('bInterfaceProtocol', c_uint8), + ('iInterface', c_uint8), + ('endpoint', libusb_endpoint_descriptor_p), + ('extra', c_void_p), + ('extra_length', c_int)] +libusb_interface_descriptor_p = POINTER(libusb_interface_descriptor) + +class libusb_interface(Structure): + _fields_ = [('altsetting', libusb_interface_descriptor_p), + ('num_altsetting', c_int)] +libusb_interface_p = POINTER(libusb_interface) + +class libusb_config_descriptor(Structure): + _fields_ = [ + ('bLength', c_uint8), + ('bDescriptorType', c_uint8), + ('wTotalLength', c_uint16), + ('bNumInterfaces', c_uint8), + ('bConfigurationValue', c_uint8), + ('iConfiguration', c_uint8), + ('bmAttributes', c_uint8), + ('MaxPower', c_uint8), + ('interface', libusb_interface_p), + ('extra', c_void_p), + ('extra_length', c_int)] +libusb_config_descriptor_p = POINTER(libusb_config_descriptor) +libusb_config_descriptor_p_p = POINTER(libusb_config_descriptor_p) + +class libusb_control_setup(Structure): + _pack_ = 1 + _fields_ = [ + ('bmRequestType', c_uint8), + ('bRequest', c_uint8), + ('wValue', c_uint16), + ('wIndex', c_uint16), + ('wLength', c_uint16)] +libusb_control_setup_p = POINTER(libusb_control_setup) + +LIBUSB_CONTROL_SETUP_SIZE = sizeof(libusb_control_setup) + +# Structure representing a libusb session. The concept of individual libusb +# sessions allows for your program to use two libraries (or dynamically +# load two modules) which both independently use libusb. This will prevent +# interference between the individual libusb users - for example +# libusb_set_debug() will not affect the other user of the library, and +# libusb_exit() will not destroy resources that the other user is still +# using. +# +# Sessions are created by libusb_init() and destroyed through libusb_exit(). +# If your application is guaranteed to only ever include a single libusb +# user (i.e. you), you do not have to worry about contexts: pass NULL in +# every function call where a context is required. The default context +# will be used. +# +# For more information, see \ref contexts. +#class libusb_context(Structure): +# pass +#libusb_context_p = POINTER(libusb_context) +libusb_context_p = c_void_p # Any pointer is fine +libusb_context_p_p = POINTER(libusb_context_p) + +# Structure representing a USB device detected on the system. This is an +# opaque type for which you are only ever provided with a pointer, usually +# originating from libusb_get_device_list(). +# +# Certain operations can be performed on a device, but in order to do any +# I/O you will have to first obtain a device handle using libusb_open(). +# +# Devices are reference counted with libusb_device_ref() and +# libusb_device_unref(), and are freed when the reference count reaches 0. +# New devices presented by libusb_get_device_list() have a reference count of +# 1, and libusb_free_device_list() can optionally decrease the reference count +# on all devices in the list. libusb_open() adds another reference which is +# later destroyed by libusb_close(). +#class libusb_device(Structure): +# pass +#libusb_device_p = POINTER(libusb_device) +libusb_device_p = c_void_p # Any pointer is fine +libusb_device_p_p = POINTER(libusb_device_p) +libusb_device_p_p_p = POINTER(libusb_device_p_p) + +# Structure representing a handle on a USB device. This is an opaque type for +# which you are only ever provided with a pointer, usually originating from +# libusb_open(). +# +# A device handle is used to perform I/O and other operations. When finished +# with a device handle, you should call libusb_close(). +class libusb_device_handle(Structure): + pass +libusb_device_handle_p = POINTER(libusb_device_handle) +libusb_device_handle_p_p = POINTER(libusb_device_handle_p) + +class libusb_version(Structure): + _fields_ = [ + ('major', c_uint16), + ('minor', c_uint16), + ('micro', c_uint16), + ('nano', c_uint16), + ('rc', c_char_p), + ('describe', c_char_p), + ] + +#typedef void (*libusb_log_cb)(libusb_context *ctx, +# enum libusb_log_level level, const char *str); +libusb_log_cb_p = LIBUSB_CALL_FUNCTYPE(None, libusb_context_p, c_int, c_char_p) + +class libusb_init_option_value(Union): + _fields_ = [ + ('ival', c_int), + ('log_cbval', libusb_log_cb_p), + ] + +class libusb_init_option(Structure): + _fields_ = [ + ('option', c_int), + ('value', libusb_init_option_value), + ] + +libusb_speed = Enum({ + # The OS doesn't report or know the device speed. + 'LIBUSB_SPEED_UNKNOWN': 0, + # The device is operating at low speed (1.5MBit/s). + 'LIBUSB_SPEED_LOW': 1, + # The device is operating at full speed (12MBit/s). + 'LIBUSB_SPEED_FULL': 2, + # The device is operating at high speed (480MBit/s). + 'LIBUSB_SPEED_HIGH': 3, + # The device is operating at super speed (5000MBit/s). + 'LIBUSB_SPEED_SUPER': 4, + # The device is operating at super speed plus (10000MBit/s). + 'LIBUSB_SPEED_SUPER_PLUS': 5, + # The device is operating at super speed plus x2 (20000MBit/s). + 'LIBUSB_SPEED_SUPER_PLUS_X2': 6, +}) + +libusb_supported_speed = Enum({ + # Low speed operation supported (1.5MBit/s). + 'LIBUSB_LOW_SPEED_OPERATION': 1, + # Full speed operation supported (12MBit/s). + 'LIBUSB_FULL_SPEED_OPERATION': 2, + # High speed operation supported (480MBit/s). + 'LIBUSB_HIGH_SPEED_OPERATION': 4, + # Superspeed operation supported (5000MBit/s). + 'LIBUSB_5GBPS_OPERATION': 8, +}) + +# Error codes. Most libusb functions return 0 on success or one of these +# codes on failure. +libusb_error = Enum({ + # Success (no error) + 'LIBUSB_SUCCESS': 0, + # Input/output error + 'LIBUSB_ERROR_IO': -1, + # Invalid parameter + 'LIBUSB_ERROR_INVALID_PARAM': -2, + # Access denied (insufficient permissions) + 'LIBUSB_ERROR_ACCESS': -3, + # No such device (it may have been disconnected) + 'LIBUSB_ERROR_NO_DEVICE': -4, + # Entity not found + 'LIBUSB_ERROR_NOT_FOUND': -5, + # Resource busy + 'LIBUSB_ERROR_BUSY': -6, + # Operation timed out + 'LIBUSB_ERROR_TIMEOUT': -7, + # Overflow + 'LIBUSB_ERROR_OVERFLOW': -8, + # Pipe error + 'LIBUSB_ERROR_PIPE': -9, + # System call interrupted (perhaps due to signal) + 'LIBUSB_ERROR_INTERRUPTED': -10, + # Insufficient memory + 'LIBUSB_ERROR_NO_MEM': -11, + # Operation not supported or unimplemented on this platform + 'LIBUSB_ERROR_NOT_SUPPORTED': -12, + # Other error + 'LIBUSB_ERROR_OTHER': -99, +}) + +# Transfer status codes +libusb_transfer_status = Enum({ + # Transfer completed without error. Note that this does not indicate + # that the entire amount of requested data was transferred. + 'LIBUSB_TRANSFER_COMPLETED': 0, + # Transfer failed + 'LIBUSB_TRANSFER_ERROR': 1, + # Transfer timed out + 'LIBUSB_TRANSFER_TIMED_OUT': 2, + # Transfer was cancelled + 'LIBUSB_TRANSFER_CANCELLED': 3, + # For bulk/interrupt endpoints: halt condition detected (endpoint + # stalled). For control endpoints: control request not supported. + 'LIBUSB_TRANSFER_STALL': 4, + # Device was disconnected + 'LIBUSB_TRANSFER_NO_DEVICE': 5, + # Device sent more data than requested + 'LIBUSB_TRANSFER_OVERFLOW': 6, +}) + +# libusb_transfer.flags values +libusb_transfer_flags = Enum({ + # Report short frames as errors + 'LIBUSB_TRANSFER_SHORT_NOT_OK': 1 << 0, + # Automatically free() transfer buffer during libusb_free_transfer() + 'LIBUSB_TRANSFER_FREE_BUFFER': 1 << 1, + # Automatically call libusb_free_transfer() after callback returns. + # If this flag is set, it is illegal to call libusb_free_transfer() + # from your transfer callback, as this will result in a double-free + # when this flag is acted upon. + 'LIBUSB_TRANSFER_FREE_TRANSFER': 1 << 2, + # Terminate transfers that are a multiple of the endpoint's + # wMaxPacketSize with an extra zero length packet. + 'LIBUSB_TRANSFER_ADD_ZERO_PACKET': 1 << 3, +}) + +# Isochronous packet descriptor. +class libusb_iso_packet_descriptor(Structure): + _fields_ = [('length', c_uint), + ('actual_length', c_uint), + ('status', c_int)] # enum libusb_transfer_status +libusb_iso_packet_descriptor_p = POINTER(libusb_iso_packet_descriptor) + +class libusb_transfer(Structure): + pass +libusb_transfer_p = POINTER(libusb_transfer) + +libusb_transfer_cb_fn_p = LIBUSB_CALL_FUNCTYPE(None, libusb_transfer_p) + +libusb_capability = Enum({ + # The libusb_has_capability() API is available. + 'LIBUSB_CAP_HAS_CAPABILITY': 0x0000, + # Hotplug support is available. + 'LIBUSB_CAP_HAS_HOTPLUG': 0x0001, + # The library can access HID devices without requiring user intervention. + 'LIBUSB_CAP_HAS_HID_ACCESS': 0x0100, + # The library supports detaching of the default USB driver. + 'LIBUSB_CAP_SUPPORTS_DETACH_KERNEL_DRIVER': 0x0101, +}) + +libusb_log_level = Enum({ + 'LIBUSB_LOG_LEVEL_NONE': 0, + 'LIBUSB_LOG_LEVEL_ERROR': 1, + 'LIBUSB_LOG_LEVEL_WARNING': 2, + 'LIBUSB_LOG_LEVEL_INFO': 3, + 'LIBUSB_LOG_LEVEL_DEBUG': 4, +}) + +libusb_log = Enum({ + 'LIBUSB_LOG_CB_GLOBAL': 1 << 0, + 'LIBUSB_LOG_CB_CONTEXT': 1 << 1, +}) + +libusb_option = Enum({ + 'LIBUSB_OPTION_LOG_LEVEL': 0, + 'LIBUSB_OPTION_USE_USBDK': 1, + 'LIBUSB_OPTION_NO_DEVICE_DISCOVERY': 2, + 'LIBUSB_OPTION_LOG_CB': 3, + 'LIBUSB_OPTION_MAX': 4, +}) + +# Get the data section of a control transfer. This convenience function is here +# to remind you that the data does not start until 8 bytes into the actual +# buffer, as the setup packet comes first. +# +# Calling this function only makes sense from a transfer callback function, +# or situations where you have already allocated a suitably sized buffer at +# transfer->buffer. +# +# \param transfer a transfer +# \returns pointer to the first byte of the data section + +def libusb_control_transfer_get_data(transfer_p): + transfer = transfer_p.contents + return buffer_at(transfer.buffer, transfer.length)[ + LIBUSB_CONTROL_SETUP_SIZE:] + +def libusb_control_transfer_get_setup(transfer_p): + return cast(transfer_p.contents.buffer, libusb_control_setup_p) + +def libusb_fill_control_setup( + setup_p, bmRequestType, bRequest, wValue, wIndex, wLength): + setup = cast(setup_p, libusb_control_setup_p).contents + setup.bmRequestType = bmRequestType + setup.bRequest = bRequest + setup.wValue = libusb_cpu_to_le16(wValue) + setup.wIndex = libusb_cpu_to_le16(wIndex) + setup.wLength = libusb_cpu_to_le16(wLength) + +# pylint: disable=redefined-builtin +def libusb_fill_control_transfer( + transfer_p, dev_handle, buffer, callback, user_data, timeout): + transfer = transfer_p.contents + transfer.dev_handle = dev_handle + transfer.endpoint = 0 + # pylint: disable=undefined-variable + transfer.type = LIBUSB_TRANSFER_TYPE_CONTROL + # pylint: enable=undefined-variable + transfer.timeout = timeout + transfer.buffer = cast(buffer, c_void_p) + if buffer is not None: + setup = cast(buffer, libusb_control_setup_p).contents + # pylint: disable=undefined-variable + transfer.length = LIBUSB_CONTROL_SETUP_SIZE + \ + libusb_le16_to_cpu(setup.wLength) + # pylint: enable=undefined-variable + transfer.user_data = user_data + transfer.callback = callback +# pylint: enable=redefined-builtin + +# pylint: disable=redefined-builtin +def libusb_fill_bulk_transfer( + transfer_p, dev_handle, endpoint, buffer, length, + callback, user_data, timeout): + transfer = transfer_p.contents + transfer.dev_handle = dev_handle + transfer.endpoint = endpoint + # pylint: disable=undefined-variable + transfer.type = LIBUSB_TRANSFER_TYPE_BULK + # pylint: enable=undefined-variable + transfer.timeout = timeout + transfer.buffer = cast(buffer, c_void_p) + transfer.length = length + transfer.user_data = user_data + transfer.callback = callback +# pylint: enable=redefined-builtin + +# pylint: disable=redefined-builtin +def libusb_fill_interrupt_transfer( + transfer_p, dev_handle, endpoint, buffer, + length, callback, user_data, timeout): + transfer = transfer_p.contents + transfer.dev_handle = dev_handle + transfer.endpoint = endpoint + # pylint: disable=undefined-variable + transfer.type = LIBUSB_TRANSFER_TYPE_INTERRUPT + # pylint: enable=undefined-variable + transfer.timeout = timeout + transfer.buffer = cast(buffer, c_void_p) + transfer.length = length + transfer.user_data = user_data + transfer.callback = callback +# pylint: enable=redefined-builtin + +# pylint: disable=redefined-builtin +def libusb_fill_iso_transfer( + transfer_p, dev_handle, endpoint, buffer, length, + num_iso_packets, callback, user_data, timeout): + transfer = transfer_p.contents + transfer.dev_handle = dev_handle + transfer.endpoint = endpoint + # pylint: disable=undefined-variable + transfer.type = LIBUSB_TRANSFER_TYPE_ISOCHRONOUS + # pylint: enable=undefined-variable + transfer.timeout = timeout + transfer.buffer = cast(buffer, c_void_p) + transfer.length = length + transfer.num_iso_packets = num_iso_packets + transfer.user_data = user_data + transfer.callback = callback +# pylint: enable=redefined-builtin + +def _get_iso_packet_list(transfer): + list_type = libusb_iso_packet_descriptor * transfer.num_iso_packets + return list_type.from_address(addressof(transfer.iso_packet_desc)) + +def get_iso_packet_list(transfer_p): + """ + Python-specific helper extracting a list of iso packet descriptors, + because it's not as straight-forward as in C. + """ + return _get_iso_packet_list(transfer_p.contents) + +def _get_iso_packet_buffer(transfer, offset, length): + return buffer_at(transfer.buffer + offset, length) + +def get_iso_packet_buffer_list(transfer_p): + """ + Python-specific helper extracting a list of iso packet buffers. + """ + transfer = transfer_p.contents + offset = 0 + result = [] + append = result.append + for iso_transfer in _get_iso_packet_list(transfer): + length = iso_transfer.length + append(_get_iso_packet_buffer(transfer, offset, length)) + offset += length + return result + +def get_extra(descriptor): + """ + Python-specific helper to access "extra" field of descriptors, + because it's not as straight-forward as in C. + Returns a list, where each entry is an individual extra descriptor. + """ + result = [] + extra_length = descriptor.extra_length + if extra_length: + extra = buffer_at(descriptor.extra, extra_length) + append = result.append + while extra: + length = extra[0] + if not 0 < length <= len(extra): + raise ValueError( + f'Extra descriptor {len(result)} is incomplete/invalid', + ) + append(extra[:length]) + extra = extra[length:] + return result + +def libusb_set_iso_packet_lengths(transfer_p, length): + transfer = transfer_p.contents + for iso_packet_desc in _get_iso_packet_list(transfer): + iso_packet_desc.length = length + +def libusb_get_iso_packet_buffer(transfer_p, packet): + transfer = transfer_p.contents + offset = 0 + if packet >= transfer.num_iso_packets: + return None + iso_packet_desc_list = _get_iso_packet_list(transfer) + for i in range(packet): + offset += iso_packet_desc_list[i].length + return _get_iso_packet_buffer( + transfer, offset, iso_packet_desc_list[packet].length) + +def libusb_get_iso_packet_buffer_simple(transfer_p, packet): + transfer = transfer_p.contents + if packet >= transfer.num_iso_packets: + return None + iso_length = transfer.iso_packet_desc.length + return _get_iso_packet_buffer(transfer, iso_length * packet, iso_length) + +# sync I/O + +# pylint: disable=undefined-variable +def libusb_get_descriptor(dev, desc_type, desc_index, data, length): + return libusb_control_transfer(dev, LIBUSB_ENDPOINT_IN, + LIBUSB_REQUEST_GET_DESCRIPTOR, + (desc_type << 8) | desc_index, 0, data, + length, 1000) +# pylint: enable=undefined-variable + +# pylint: disable=undefined-variable +def libusb_get_string_descriptor(dev, desc_index, langid, data, length): + return libusb_control_transfer(dev, LIBUSB_ENDPOINT_IN, + LIBUSB_REQUEST_GET_DESCRIPTOR, + (LIBUSB_DT_STRING << 8) | desc_index, + langid, data, length, 1000) +# pylint: enable=undefined-variable + +# polling and timeouts + +class libusb_pollfd(Structure): + _fields_ = [ + ('fd', c_int), + ('events', c_short), + ] +libusb_pollfd_p = POINTER(libusb_pollfd) +libusb_pollfd_p_p = POINTER(libusb_pollfd_p) + +libusb_pollfd_added_cb_p = LIBUSB_CALL_FUNCTYPE(None, c_int, c_short, py_object) +libusb_pollfd_removed_cb_p = LIBUSB_CALL_FUNCTYPE(None, c_int, py_object) + +#typedef int libusb_hotplug_callback_handle; +libusb_hotplug_callback_handle = c_int + +libusb_hotplug_flag = Enum({ + 'LIBUSB_HOTPLUG_ENUMERATE': 1, +}) + +libusb_hotplug_event = Enum({ + 'LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED': 0x01, + 'LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT': 0x02, +}) + +LIBUSB_HOTPLUG_NO_FLAGS = 0 +LIBUSB_HOTPLUG_MATCH_ANY = -1 + +#typedef int (*libusb_hotplug_callback_fn)(libusb_context *ctx, +# libusb_device *device, libusb_hotplug_event event, void *user_data); +libusb_hotplug_callback_fn_p = LIBUSB_CALL_FUNCTYPE( + c_int, libusb_context_p, libusb_device_p, c_int, c_void_p) + +# /libusb.h diff --git a/usb1/_version.py b/usb1/_version.py new file mode 100644 index 0000000..41b6004 --- /dev/null +++ b/usb1/_version.py @@ -0,0 +1,683 @@ + +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys +from typing import Any, Callable, Dict, List, Optional, Tuple +import functools + + +def get_keywords() -> Dict[str, str]: + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "$Format:%d$" + git_full = "$Format:%H$" + git_date = "$Format:%ci$" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool + + +def get_config() -> VersioneerConfig: + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "" + cfg.parentdir_prefix = "usb1-" + cfg.versionfile_source = "usb1/_version.py" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} + + +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: + """Call the given command(s).""" + assert isinstance(commands, list) + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: + try: + dispcmd = str([command] + args) + # remember shell=False, so use git.cmd on windows, not just git + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) + break + except OSError as e: + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + print("stdout was %s" % stdout) + return None, process.returncode + return stdout, process.returncode + + +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for _ in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords: Dict[str, str] = {} + try: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: + """Get version information from git keywords.""" + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") + date = keywords.get("date") + if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = {r.strip() for r in refnames.strip("()").split(",")} + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = {r for r in refs if re.search(r'\d', r)} + if verbose: + print("discarding '%s', no digits" % ",".join(refs - tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces: Dict[str, Any] = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparsable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def plus_or_dot(pieces: Dict[str, Any]) -> str: + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces: Dict[str, Any]) -> str: + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). + + Exceptions: + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: + if pieces["distance"]: + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] + else: + # exception #1 + rendered = "0.post0.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces: Dict[str, Any]) -> str: + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces: Dict[str, Any]) -> str: + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +def get_versions() -> Dict[str, Any]: + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for _ in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", "date": None} diff --git a/usb1/libusb1.py b/usb1/libusb1.py index f56a9a7..a4f529f 100644 --- a/usb1/libusb1.py +++ b/usb1/libusb1.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2018 Vincent Pelletier +# Copyright (C) 2010-2021 Vincent Pelletier # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -14,1314 +14,9 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -# pylint: disable=invalid-name, too-few-public-methods, too-many-arguments -# pylint: disable=missing-docstring -""" -Python ctypes bindings for libusb-1.0. - -You should not need to import this if you use usb1 module. - -Declares all constants, data structures and exported symbols. -Locates and loads libusb1 dynamic library. -""" -from ctypes import Structure, LittleEndianStructure, \ - CFUNCTYPE, POINTER, addressof, sizeof, cast, \ - c_short, c_int, c_uint, c_size_t, c_long, \ - c_uint8, c_uint16, c_uint32, \ - c_void_p, c_char_p, py_object, pointer, c_char -try: - from ctypes import c_ssize_t -except ImportError: - from ctypes import c_longlong - # c_ssize_t is new in Python 2.7 - if sizeof(c_int) == sizeof(c_size_t): - c_ssize_t = c_int - elif sizeof(c_long) == sizeof(c_size_t): - c_ssize_t = c_long - elif sizeof(c_longlong) == sizeof(c_size_t): - c_ssize_t = c_longlong - else: - raise ValueError('Unsupported arch: sizeof(c_size_t) = %r' % ( - sizeof(c_size_t), )) -import ctypes.util -import platform -import os.path -import sys - -class Enum(object): - def __init__(self, member_dict, scope_dict=None): - if scope_dict is None: - # Affect caller's locals, not this module's. - # pylint: disable=protected-access - scope_dict = sys._getframe(1).f_locals - # pylint: enable=protected-access - forward_dict = {} - reverse_dict = {} - next_value = 0 - for name, value in member_dict.items(): - if value is None: - value = next_value - next_value += 1 - forward_dict[name] = value - if value in reverse_dict: - raise ValueError('Multiple names for value %r: %r, %r' % ( - value, reverse_dict[value], name - )) - reverse_dict[value] = name - scope_dict[name] = value - self.forward_dict = forward_dict - self.reverse_dict = reverse_dict - - def __call__(self, value): - return self.reverse_dict[value] - - def get(self, value, default=None): - return self.reverse_dict.get(value, default) - -def buffer_at(address, length): - """ - Simular to ctypes.string_at, but zero-copy and requires an integer address. - """ - return bytearray((c_char * length).from_address(address)) - -_desc_type_dict = { - 'b': c_uint8, - 'bcd': c_uint16, - 'bm': c_uint8, - 'dw': c_uint32, - 'i': c_uint8, - 'id': c_uint16, - 'w': c_uint16, -} - -def newStruct(field_name_list): - """ - Create a ctype structure class based on USB standard field naming - (type-prefixed). - """ - field_list = [] - append = field_list.append - for field in field_name_list: - type_prefix = '' - for char in field: - if not char.islower(): - break - type_prefix += char - append((field, _desc_type_dict[type_prefix])) - result = type('some_descriptor', (LittleEndianStructure, ), {}) - # Not using type()'s 3rd param to initialise class, as per ctypes - # documentation: - # _pack_ must already be defined when _fields_ is assigned, otherwise it - # will have no effect. - # pylint: disable=protected-access - result._pack_ = 1 - result._fields_ = field_list - # pylint: enable=protected-access - return result - -def newDescriptor(field_name_list): - """ - Create a USB descriptor ctype structure, ie starting with bLength and - bDescriptorType fields. - - See newStruct(). - """ - return newStruct(['bLength', 'bDescriptorType'] + list(field_name_list)) - -class USBError(Exception): - value = None - - def __init__(self, value=None): - Exception.__init__(self) - if value is not None: - self.value = value - - def __str__(self): - return '%s [%s]' % (libusb_error.get(self.value, 'Unknown error'), - self.value) - -if sys.version_info[0] == 3: - _string_item_to_int = lambda x: x - _empty_char_p = bytes() -else: - _string_item_to_int = ord - _empty_char_p = '' - -c_uchar = c_uint8 -c_int_p = POINTER(c_int) - -LITTLE_ENDIAN = sys.byteorder == 'little' - -class timeval(Structure): - _fields_ = [('tv_sec', c_long), - ('tv_usec', c_long)] -timeval_p = POINTER(timeval) - -def _loadLibrary(): - system = platform.system() - if system == 'Windows': - dll_loader = ctypes.WinDLL - suffix = '.dll' - else: - dll_loader = ctypes.CDLL - suffix = system == 'Darwin' and '.dylib' or '.so' - loader_kw = {} - if sys.version_info[:2] >= (2, 6): - loader_kw['use_errno'] = True - loader_kw['use_last_error'] = True - try: - return dll_loader('libusb-1.0' + suffix, **loader_kw) - except OSError: - libusb_path = None - base_name = 'usb-1.0' - if 'FreeBSD' in system: - # libusb.so.2 on FreeBSD: load('libusb.so') would work fine, but... - # libusb.so.2debian on Debian GNU/kFreeBSD: here it wouldn't work. - # So use find_library instead. - base_name = 'usb' - elif system == 'Darwin': - for libusb_path in ( - # macport standard path - '/opt/local/lib/libusb-1.0.dylib', - # fink standard path - '/sw/lib/libusb-1.0.dylib', - ): - if os.path.exists(libusb_path): - break - else: - libusb_path = None - if libusb_path is None: - libusb_path = ctypes.util.find_library(base_name) - if libusb_path is None: - raise - return dll_loader(libusb_path, **loader_kw) - -libusb = _loadLibrary() - -# libusb.h -def bswap16(x): - return ((x & 0xff) << 8) | (x >> 8) - -if LITTLE_ENDIAN: - def libusb_cpu_to_le16(x): - return x - def libusb_le16_to_cpu(x): - return x -else: - libusb_cpu_to_le16 = bswap16 - libusb_le16_to_cpu = bswap16 - -# standard USB stuff - -# Device and/or Interface Class codes -libusb_class_code = Enum({ - # In the context of a device descriptor, - # this bDeviceClass value indicates that each interface specifies its - # own class information and all interfaces operate independently. - 'LIBUSB_CLASS_PER_INTERFACE': 0, - # Audio class - 'LIBUSB_CLASS_AUDIO': 1, - # Communications class - 'LIBUSB_CLASS_COMM': 2, - # Human Interface Device class - 'LIBUSB_CLASS_HID': 3, - # Physical - 'LIBUSB_CLASS_PHYSICAL': 5, - # Printer class - 'LIBUSB_CLASS_PRINTER': 7, - # Picture transfer protocol class - 'LIBUSB_CLASS_PTP': 6, - # Mass storage class - 'LIBUSB_CLASS_MASS_STORAGE': 8, - # Hub class - 'LIBUSB_CLASS_HUB': 9, - # Data class - 'LIBUSB_CLASS_DATA': 10, - # Smart Card - 'LIBUSB_CLASS_SMART_CARD': 0x0b, - # Content Security - 'LIBUSB_CLASS_CONTENT_SECURITY': 0x0d, - # Video - 'LIBUSB_CLASS_VIDEO': 0x0e, - # Personal Healthcare - 'LIBUSB_CLASS_PERSONAL_HEALTHCARE': 0x0f, - # Diagnostic Device - 'LIBUSB_CLASS_DIAGNOSTIC_DEVICE': 0xdc, - # Wireless class - 'LIBUSB_CLASS_WIRELESS': 0xe0, - # Application class - 'LIBUSB_CLASS_APPLICATION': 0xfe, - # Class is vendor-specific - 'LIBUSB_CLASS_VENDOR_SPEC': 0xff -}) -# pylint: disable=undefined-variable -LIBUSB_CLASS_IMAGE = LIBUSB_CLASS_PTP -# pylint: enable=undefined-variable - -# Descriptor types as defined by the USB specification. -libusb_descriptor_type = Enum({ - # Device descriptor. See libusb_device_descriptor. - 'LIBUSB_DT_DEVICE': 0x01, - # Configuration descriptor. See libusb_config_descriptor. - 'LIBUSB_DT_CONFIG': 0x02, - # String descriptor - 'LIBUSB_DT_STRING': 0x03, - # Interface descriptor. See libusb_interface_descriptor. - 'LIBUSB_DT_INTERFACE': 0x04, - # Endpoint descriptor. See libusb_endpoint_descriptor. - 'LIBUSB_DT_ENDPOINT': 0x05, - # HID descriptor - 'LIBUSB_DT_HID': 0x21, - # HID report descriptor - 'LIBUSB_DT_REPORT': 0x22, - # Physical descriptor - 'LIBUSB_DT_PHYSICAL': 0x23, - # Hub descriptor - 'LIBUSB_DT_HUB': 0x29, -}) - -# Descriptor sizes per descriptor type -LIBUSB_DT_DEVICE_SIZE = 18 -LIBUSB_DT_CONFIG_SIZE = 9 -LIBUSB_DT_INTERFACE_SIZE = 9 -LIBUSB_DT_ENDPOINT_SIZE = 7 -LIBUSB_DT_ENDPOINT_AUDIO_SIZE = 9 # Audio extension -LIBUSB_DT_HUB_NONVAR_SIZE = 7 - -LIBUSB_ENDPOINT_ADDRESS_MASK = 0x0f # in bEndpointAddress -LIBUSB_ENDPOINT_DIR_MASK = 0x80 -# BBB -USB_ENDPOINT_ADDRESS_MASK = LIBUSB_ENDPOINT_ADDRESS_MASK -USB_ENDPOINT_DIR_MASK = LIBUSB_ENDPOINT_DIR_MASK - -# Endpoint direction. Values for bit 7 of the endpoint address scheme. -libusb_endpoint_direction = Enum({ - # In: device-to-host - 'LIBUSB_ENDPOINT_IN': 0x80, - # Out: host-to-device - 'LIBUSB_ENDPOINT_OUT': 0x00 -}) - -LIBUSB_TRANSFER_TYPE_MASK = 0x03 # in bmAttributes - -# Endpoint transfer type. Values for bits 0:1 of the endpoint attributes field. -libusb_transfer_type = Enum({ - # Control endpoint - 'LIBUSB_TRANSFER_TYPE_CONTROL': 0, - # Isochronous endpoint - 'LIBUSB_TRANSFER_TYPE_ISOCHRONOUS': 1, - # Bulk endpoint - 'LIBUSB_TRANSFER_TYPE_BULK': 2, - # Interrupt endpoint - 'LIBUSB_TRANSFER_TYPE_INTERRUPT': 3, -}) - -# Standard requests, as defined in table 9-3 of the USB2 specifications -libusb_standard_request = Enum({ - # Request status of the specific recipient - 'LIBUSB_REQUEST_GET_STATUS': 0x00, - # Clear or disable a specific feature - 'LIBUSB_REQUEST_CLEAR_FEATURE': 0x01, - # 0x02 is reserved - # Set or enable a specific feature - 'LIBUSB_REQUEST_SET_FEATURE': 0x03, - # 0x04 is reserved - # Set device address for all future accesses - 'LIBUSB_REQUEST_SET_ADDRESS': 0x05, - # Get the specified descriptor - 'LIBUSB_REQUEST_GET_DESCRIPTOR': 0x06, - # Used to update existing descriptors or add new descriptors - 'LIBUSB_REQUEST_SET_DESCRIPTOR': 0x07, - # Get the current device configuration value - 'LIBUSB_REQUEST_GET_CONFIGURATION': 0x08, - # Set device configuration - 'LIBUSB_REQUEST_SET_CONFIGURATION': 0x09, - # Return the selected alternate setting for the specified interface - 'LIBUSB_REQUEST_GET_INTERFACE': 0x0a, - # Select an alternate interface for the specified interface - 'LIBUSB_REQUEST_SET_INTERFACE': 0x0b, - # Set then report an endpoint's synchronization frame - 'LIBUSB_REQUEST_SYNCH_FRAME': 0x0c, -}) - -# Request type bits of the bmRequestType field in control transfers. -libusb_request_type = Enum({ - # Standard - 'LIBUSB_REQUEST_TYPE_STANDARD': (0x00 << 5), - # Class - 'LIBUSB_REQUEST_TYPE_CLASS': (0x01 << 5), - # Vendor - 'LIBUSB_REQUEST_TYPE_VENDOR': (0x02 << 5), - # Reserved - 'LIBUSB_REQUEST_TYPE_RESERVED': (0x03 << 5), -}) - -# BBB -# pylint: disable=bad-whitespace,undefined-variable -LIBUSB_TYPE_STANDARD = LIBUSB_REQUEST_TYPE_STANDARD -LIBUSB_TYPE_CLASS = LIBUSB_REQUEST_TYPE_CLASS -LIBUSB_TYPE_VENDOR = LIBUSB_REQUEST_TYPE_VENDOR -LIBUSB_TYPE_RESERVED = LIBUSB_REQUEST_TYPE_RESERVED -# pylint: enable=bad-whitespace,undefined-variable - -# Recipient bits of the bmRequestType field in control transfers. Values 4 -# through 31 are reserved. -libusb_request_recipient = Enum({ - # Device - 'LIBUSB_RECIPIENT_DEVICE': 0x00, - # Interface - 'LIBUSB_RECIPIENT_INTERFACE': 0x01, - # Endpoint - 'LIBUSB_RECIPIENT_ENDPOINT': 0x02, - # Other - 'LIBUSB_RECIPIENT_OTHER': 0x03, -}) - -LIBUSB_ISO_SYNC_TYPE_MASK = 0x0c - -# Synchronization type for isochronous endpoints. Values for bits 2:3 of the -# bmAttributes field in libusb_endpoint_descriptor. -libusb_iso_sync_type = Enum({ - # No synchronization - 'LIBUSB_ISO_SYNC_TYPE_NONE': 0, - # Asynchronous - 'LIBUSB_ISO_SYNC_TYPE_ASYNC': 1, - # Adaptive - 'LIBUSB_ISO_SYNC_TYPE_ADAPTIVE': 2, - # Synchronous - 'LIBUSB_ISO_SYNC_TYPE_SYNC': 3, -}) - -LIBUSB_ISO_USAGE_TYPE_MASK = 0x30 - -# Usage type for isochronous endpoints. Values for bits 4:5 of the -# bmAttributes field in libusb_endpoint_descriptor. -libusb_iso_usage_type = Enum({ - # Data endpoint - 'LIBUSB_ISO_USAGE_TYPE_DATA': 0, - # Feedback endpoint - 'LIBUSB_ISO_USAGE_TYPE_FEEDBACK': 1, - # Implicit feedback Data endpoint - 'LIBUSB_ISO_USAGE_TYPE_IMPLICIT': 2, -}) - -# A structure representing the standard USB device descriptor. This -# descriptor is documented in section 9.6.1 of the USB 2.0 specification. -# All multiple-byte fields are represented in host-endian format. -class libusb_device_descriptor(Structure): - _fields_ = [ - # Size of this descriptor (in bytes) - ('bLength', c_uint8), - # Descriptor type. Will have value LIBUSB_DT_DEVICE in this - # context. - ('bDescriptorType', c_uint8), - # USB specification release number in binary-coded decimal. A - # value of 0x0200 indicates USB 2.0, 0x0110 indicates USB 1.1, - # etc. - ('bcdUSB', c_uint16), - # USB-IF class code for the device. See libusb_class_code. - ('bDeviceClass', c_uint8), - # USB-IF subclass code for the device, qualified by the - # bDeviceClass value - ('bDeviceSubClass', c_uint8), - # USB-IF protocol code for the device, qualified by the - # bDeviceClass and bDeviceSubClass values - ('bDeviceProtocol', c_uint8), - # Maximum packet size for endpoint 0 - ('bMaxPacketSize0', c_uint8), - # USB-IF vendor ID - ('idVendor', c_uint16), - # USB-IF product ID - ('idProduct', c_uint16), - # Device release number in binary-coded decimal - ('bcdDevice', c_uint16), - # Index of string descriptor describing manufacturer - ('iManufacturer', c_uint8), - # Index of string descriptor describing product - ('iProduct', c_uint8), - # Index of string descriptor containing device serial number - ('iSerialNumber', c_uint8), - # Number of possible configurations - ('bNumConfigurations', c_uint8)] -libusb_device_descriptor_p = POINTER(libusb_device_descriptor) - -class libusb_endpoint_descriptor(Structure): - _fields_ = [ - ('bLength', c_uint8), - ('bDescriptorType', c_uint8), - ('bEndpointAddress', c_uint8), - ('bmAttributes', c_uint8), - ('wMaxPacketSize', c_uint16), - ('bInterval', c_uint8), - ('bRefresh', c_uint8), - ('bSynchAddress', c_uint8), - ('extra', c_void_p), - ('extra_length', c_int)] -libusb_endpoint_descriptor_p = POINTER(libusb_endpoint_descriptor) - -class libusb_interface_descriptor(Structure): - _fields_ = [ - ('bLength', c_uint8), - ('bDescriptorType', c_uint8), - ('bInterfaceNumber', c_uint8), - ('bAlternateSetting', c_uint8), - ('bNumEndpoints', c_uint8), - ('bInterfaceClass', c_uint8), - ('bInterfaceSubClass', c_uint8), - ('bInterfaceProtocol', c_uint8), - ('iInterface', c_uint8), - ('endpoint', libusb_endpoint_descriptor_p), - ('extra', c_void_p), - ('extra_length', c_int)] -libusb_interface_descriptor_p = POINTER(libusb_interface_descriptor) - -class libusb_interface(Structure): - _fields_ = [('altsetting', libusb_interface_descriptor_p), - ('num_altsetting', c_int)] -libusb_interface_p = POINTER(libusb_interface) - -class libusb_config_descriptor(Structure): - _fields_ = [ - ('bLength', c_uint8), - ('bDescriptorType', c_uint8), - ('wTotalLength', c_uint16), - ('bNumInterfaces', c_uint8), - ('bConfigurationValue', c_uint8), - ('iConfiguration', c_uint8), - ('bmAttributes', c_uint8), - ('MaxPower', c_uint8), - ('interface', libusb_interface_p), - ('extra', c_void_p), - ('extra_length', c_int)] -libusb_config_descriptor_p = POINTER(libusb_config_descriptor) -libusb_config_descriptor_p_p = POINTER(libusb_config_descriptor_p) - -class libusb_control_setup(Structure): - _fields_ = [ - ('bmRequestType', c_uint8), - ('bRequest', c_uint8), - ('wValue', c_uint16), - ('wIndex', c_uint16), - ('wLength', c_uint16)] -libusb_control_setup_p = POINTER(libusb_control_setup) - -LIBUSB_CONTROL_SETUP_SIZE = sizeof(libusb_control_setup) - -# Structure representing a libusb session. The concept of individual libusb -# sessions allows for your program to use two libraries (or dynamically -# load two modules) which both independently use libusb. This will prevent -# interference between the individual libusb users - for example -# libusb_set_debug() will not affect the other user of the library, and -# libusb_exit() will not destroy resources that the other user is still -# using. -# -# Sessions are created by libusb_init() and destroyed through libusb_exit(). -# If your application is guaranteed to only ever include a single libusb -# user (i.e. you), you do not have to worry about contexts: pass NULL in -# every function call where a context is required. The default context -# will be used. -# -# For more information, see \ref contexts. -class libusb_context(Structure): - pass -libusb_context_p = POINTER(libusb_context) -libusb_context_p_p = POINTER(libusb_context_p) - -# Structure representing a USB device detected on the system. This is an -# opaque type for which you are only ever provided with a pointer, usually -# originating from libusb_get_device_list(). -# -# Certain operations can be performed on a device, but in order to do any -# I/O you will have to first obtain a device handle using libusb_open(). -# -# Devices are reference counted with libusb_device_ref() and -# libusb_device_unref(), and are freed when the reference count reaches 0. -# New devices presented by libusb_get_device_list() have a reference count of -# 1, and libusb_free_device_list() can optionally decrease the reference count -# on all devices in the list. libusb_open() adds another reference which is -# later destroyed by libusb_close(). -class libusb_device(Structure): - pass -libusb_device_p = POINTER(libusb_device) -libusb_device_p_p = POINTER(libusb_device_p) -libusb_device_p_p_p = POINTER(libusb_device_p_p) - -# Structure representing a handle on a USB device. This is an opaque type for -# which you are only ever provided with a pointer, usually originating from -# libusb_open(). -# -# A device handle is used to perform I/O and other operations. When finished -# with a device handle, you should call libusb_close(). -class libusb_device_handle(Structure): - pass -libusb_device_handle_p = POINTER(libusb_device_handle) -libusb_device_handle_p_p = POINTER(libusb_device_handle_p) - -class libusb_version(Structure): - _fields_ = [ - ('major', c_uint16), - ('minor', c_uint16), - ('micro', c_uint16), - ('nano', c_uint16), - ('rc', c_char_p), - ('describe', c_char_p), - ] - -libusb_speed = Enum({ - # The OS doesn't report or know the device speed. - 'LIBUSB_SPEED_UNKNOWN': 0, - # The device is operating at low speed (1.5MBit/s). - 'LIBUSB_SPEED_LOW': 1, - # The device is operating at full speed (12MBit/s). - 'LIBUSB_SPEED_FULL': 2, - # The device is operating at high speed (480MBit/s). - 'LIBUSB_SPEED_HIGH': 3, - # The device is operating at super speed (5000MBit/s). - 'LIBUSB_SPEED_SUPER': 4, -}) - -libusb_supported_speed = Enum({ - # Low speed operation supported (1.5MBit/s). - 'LIBUSB_LOW_SPEED_OPERATION': 1, - # Full speed operation supported (12MBit/s). - 'LIBUSB_FULL_SPEED_OPERATION': 2, - # High speed operation supported (480MBit/s). - 'LIBUSB_HIGH_SPEED_OPERATION': 4, - # Superspeed operation supported (5000MBit/s). - 'LIBUSB_5GBPS_OPERATION': 8, -}) - -# Error codes. Most libusb functions return 0 on success or one of these -# codes on failure. -libusb_error = Enum({ - # Success (no error) - 'LIBUSB_SUCCESS': 0, - # Input/output error - 'LIBUSB_ERROR_IO': -1, - # Invalid parameter - 'LIBUSB_ERROR_INVALID_PARAM': -2, - # Access denied (insufficient permissions) - 'LIBUSB_ERROR_ACCESS': -3, - # No such device (it may have been disconnected) - 'LIBUSB_ERROR_NO_DEVICE': -4, - # Entity not found - 'LIBUSB_ERROR_NOT_FOUND': -5, - # Resource busy - 'LIBUSB_ERROR_BUSY': -6, - # Operation timed out - 'LIBUSB_ERROR_TIMEOUT': -7, - # Overflow - 'LIBUSB_ERROR_OVERFLOW': -8, - # Pipe error - 'LIBUSB_ERROR_PIPE': -9, - # System call interrupted (perhaps due to signal) - 'LIBUSB_ERROR_INTERRUPTED': -10, - # Insufficient memory - 'LIBUSB_ERROR_NO_MEM': -11, - # Operation not supported or unimplemented on this platform - 'LIBUSB_ERROR_NOT_SUPPORTED': -12, - # Other error - 'LIBUSB_ERROR_OTHER': -99, -}) - -# Transfer status codes -libusb_transfer_status = Enum({ - # Transfer completed without error. Note that this does not indicate - # that the entire amount of requested data was transferred. - 'LIBUSB_TRANSFER_COMPLETED': 0, - # Transfer failed - 'LIBUSB_TRANSFER_ERROR': 1, - # Transfer timed out - 'LIBUSB_TRANSFER_TIMED_OUT': 2, - # Transfer was cancelled - 'LIBUSB_TRANSFER_CANCELLED': 3, - # For bulk/interrupt endpoints: halt condition detected (endpoint - # stalled). For control endpoints: control request not supported. - 'LIBUSB_TRANSFER_STALL': 4, - # Device was disconnected - 'LIBUSB_TRANSFER_NO_DEVICE': 5, - # Device sent more data than requested - 'LIBUSB_TRANSFER_OVERFLOW': 6, -}) - -# libusb_transfer.flags values -libusb_transfer_flags = Enum({ - # Report short frames as errors - 'LIBUSB_TRANSFER_SHORT_NOT_OK': 1 << 0, - # Automatically free() transfer buffer during libusb_free_transfer() - 'LIBUSB_TRANSFER_FREE_BUFFER': 1 << 1, - # Automatically call libusb_free_transfer() after callback returns. - # If this flag is set, it is illegal to call libusb_free_transfer() - # from your transfer callback, as this will result in a double-free - # when this flag is acted upon. - 'LIBUSB_TRANSFER_FREE_TRANSFER': 1 << 2, - # Terminate transfers that are a multiple of the endpoint's - # wMaxPacketSize with an extra zero length packet. - 'LIBUSB_TRANSFER_ADD_ZERO_PACKET': 1 << 3, -}) - -# Isochronous packet descriptor. -class libusb_iso_packet_descriptor(Structure): - _fields_ = [('length', c_uint), - ('actual_length', c_uint), - ('status', c_int)] # enum libusb_transfer_status -libusb_iso_packet_descriptor_p = POINTER(libusb_iso_packet_descriptor) - -class libusb_transfer(Structure): - pass -libusb_transfer_p = POINTER(libusb_transfer) - -libusb_transfer_cb_fn_p = CFUNCTYPE(None, libusb_transfer_p) - -_libusb_transfer_fields = [ - ('dev_handle', libusb_device_handle_p), - ('flags', c_uint8), - ('endpoint', c_uchar), - ('type', c_uchar), - ('timeout', c_uint), - ('status', c_int), # enum libusb_transfer_status - ('length', c_int), - ('actual_length', c_int), - ('callback', libusb_transfer_cb_fn_p), - ('user_data', c_void_p), - ('buffer', c_void_p), - ('num_iso_packets', c_int), - ('iso_packet_desc', libusb_iso_packet_descriptor) -] -if 'FreeBSD' in platform.system() and getattr( - libusb, 'libusb_get_string_descriptor', None - ) is None: - # Old FreeBSD version has a slight ABI incompatibility. - # Work around it unless libusb_get_string_descriptor is available, as it - # is only available on fixed versions. - assert _libusb_transfer_fields[2][0] == 'endpoint' - _libusb_transfer_fields[2] = ('endpoint', c_uint32) - assert _libusb_transfer_fields[11][0] == 'num_iso_packets' - _libusb_transfer_fields.insert(11, ('os_priv', c_void_p)) - -# pylint: disable=protected-access -libusb_transfer._fields_ = _libusb_transfer_fields -# pylint: enable=protected-access - -libusb_capability = Enum({ - # The libusb_has_capability() API is available. - 'LIBUSB_CAP_HAS_CAPABILITY': 0x0000, - # Hotplug support is available. - 'LIBUSB_CAP_HAS_HOTPLUG': 0x0001, - # The library can access HID devices without requiring user intervention. - 'LIBUSB_CAP_HAS_HID_ACCESS': 0x0100, - # The library supports detaching of the default USB driver. - 'LIBUSB_CAP_SUPPORTS_DETACH_KERNEL_DRIVER': 0x0101, -}) - -libusb_log_level = Enum({ - 'LIBUSB_LOG_LEVEL_NONE': 0, - 'LIBUSB_LOG_LEVEL_ERROR': 1, - 'LIBUSB_LOG_LEVEL_WARNING': 2, - 'LIBUSB_LOG_LEVEL_INFO': 3, - 'LIBUSB_LOG_LEVEL_DEBUG': 4, -}) - -#int libusb_init(libusb_context **ctx); -libusb_init = libusb.libusb_init -libusb_init.argtypes = [libusb_context_p_p] -#void libusb_exit(libusb_context *ctx); -libusb_exit = libusb.libusb_exit -libusb_exit.argtypes = [libusb_context_p] -libusb_exit.restype = None -#void libusb_set_debug(libusb_context *ctx, int level); -libusb_set_debug = libusb.libusb_set_debug -libusb_set_debug.argtypes = [libusb_context_p, c_int] -libusb_set_debug.restype = None -#const struct libusb_version * libusb_get_version(void); -try: - libusb_get_version = libusb.libusb_get_version -except AttributeError: - _dummy_version = libusb_version(0, 0, 0, 0, _empty_char_p, _empty_char_p) - _dummy_version_p = pointer(_dummy_version) - def libusb_get_version(): - return _dummy_version_p -else: - libusb_get_version.argtypes = [] - libusb_get_version.restype = POINTER(libusb_version) -#int libusb_has_capability(uint32_t capability); -try: - libusb_has_capability = libusb.libusb_has_capability -except AttributeError: - def libusb_has_capability(_): - return 0 -else: - libusb_has_capability.argtypes = [c_uint32] - libusb_has_capability.restype = c_int -try: - # Note: Should be equivalent to libusb_error.get (except libusb_error.get - # one raises on unknown values). - #char *libusb_error_name(int errcode); - libusb_error_name = libusb.libusb_error_name -except AttributeError: - # pylint: disable=unused-argument - def libusb_error_name(errcode): - return None - # pylint: enable=unused-argument -else: - libusb_error_name.argtypes = [c_int] - libusb_error_name.restype = c_char_p - -# Note on libusb_strerror, libusb_setlocale and future functions in the -# same spirit: -# I do not think end-user-facing messages belong to a technical library. -# Such features bring a new, non essential set of problems, and is a luxury -# I do not want to spend time supporting considering limited resources and -# more important stuff to work on. -# For backward compatibility, expose libusb_strerror placeholder. -# pylint: disable=unused-argument -def libusb_strerror(errcode): - return None -# pylint: enable=unused-argument - -#ssize_t libusb_get_device_list(libusb_context *ctx, -# libusb_device ***list); -libusb_get_device_list = libusb.libusb_get_device_list -libusb_get_device_list.argtypes = [libusb_context_p, libusb_device_p_p_p] -libusb_get_device_list.restype = c_ssize_t -#void libusb_free_device_list(libusb_device **list, int unref_devices); -libusb_free_device_list = libusb.libusb_free_device_list -libusb_free_device_list.argtypes = [libusb_device_p_p, c_int] -libusb_free_device_list.restype = None -#libusb_device *libusb_ref_device(libusb_device *dev); -libusb_ref_device = libusb.libusb_ref_device -libusb_ref_device.argtypes = [libusb_device_p] -libusb_ref_device.restype = libusb_device_p -#void libusb_unref_device(libusb_device *dev); -libusb_unref_device = libusb.libusb_unref_device -libusb_unref_device.argtypes = [libusb_device_p] -libusb_unref_device.restype = None - -#int libusb_get_configuration(libusb_device_handle *dev, int *config); -libusb_get_configuration = libusb.libusb_get_configuration -libusb_get_configuration.argtypes = [libusb_device_handle_p, c_int_p] -#int libusb_get_device_descriptor(libusb_device *dev, -# struct libusb_device_descriptor *desc); -libusb_get_device_descriptor = libusb.libusb_get_device_descriptor -libusb_get_device_descriptor.argtypes = [ - libusb_device_p, libusb_device_descriptor_p] -#int libusb_get_active_config_descriptor(libusb_device *dev, -# struct libusb_config_descriptor **config); -libusb_get_active_config_descriptor = libusb.libusb_get_active_config_descriptor -libusb_get_active_config_descriptor.argtypes = [ - libusb_device_p, libusb_config_descriptor_p_p] -#int libusb_get_config_descriptor(libusb_device *dev, uint8_t config_index, -# struct libusb_config_descriptor **config); -libusb_get_config_descriptor = libusb.libusb_get_config_descriptor -libusb_get_config_descriptor.argtypes = [ - libusb_device_p, c_uint8, libusb_config_descriptor_p_p] -#int libusb_get_config_descriptor_by_value(libusb_device *dev, -# uint8_t bConfigurationValue, struct libusb_config_descriptor **config); -libusb_get_config_descriptor_by_value = \ - libusb.libusb_get_config_descriptor_by_value -libusb_get_config_descriptor_by_value.argtypes = [ - libusb_device_p, c_uint8, libusb_config_descriptor_p_p] -#void libusb_free_config_descriptor(struct libusb_config_descriptor *config); -libusb_free_config_descriptor = libusb.libusb_free_config_descriptor -libusb_free_config_descriptor.argtypes = [libusb_config_descriptor_p] -libusb_free_config_descriptor.restype = None -#uint8_t libusb_get_bus_number(libusb_device *dev); -libusb_get_bus_number = libusb.libusb_get_bus_number -libusb_get_bus_number.argtypes = [libusb_device_p] -libusb_get_bus_number.restype = c_uint8 -try: - #uint8_t libusb_get_port_number(libusb_device *dev); - libusb_get_port_number = libusb.libusb_get_port_number -except AttributeError: - pass -else: - libusb_get_port_number.argtypes = [libusb_device_p] - libusb_get_port_number.restype = c_uint8 -try: - #int libusb_get_port_numbers(libusb_device *dev, - # uint8_t* port_numbers, int port_numbers_len); - libusb_get_port_numbers = libusb.libusb_get_port_numbers -except AttributeError: - pass -else: - libusb_get_port_numbers.argtypes = [ - libusb_device_p, POINTER(c_uint8), c_int] - libusb_get_port_numbers.restype = c_int -# Missing: libusb_get_port_path (deprecated since 1.0.16) -try: - #libusb_device * LIBUSB_CALL libusb_get_parent(libusb_device *dev); - libusb_get_parent = libusb.libusb_get_parent -except AttributeError: - pass -else: - libusb_get_parent.argtypes = [libusb_device_p] - libusb_get_parent.restype = libusb_device_p -#uint8_t libusb_get_device_address(libusb_device *dev); -libusb_get_device_address = libusb.libusb_get_device_address -libusb_get_device_address.argtypes = [libusb_device_p] -libusb_get_device_address.restype = c_uint8 -try: - #int libusb_get_device_speed(libusb_device *dev); - libusb_get_device_speed = libusb.libusb_get_device_speed -except AttributeError: - # Place holder - def libusb_get_device_speed(_): - # pylint: disable=undefined-variable - return LIBUSB_SPEED_UNKNOWN - # pylint: enable=undefined-variable -else: - libusb_get_device_speed.argtypes = [libusb_device_p] -#int libusb_get_max_packet_size(libusb_device *dev, unsigned char endpoint); -libusb_get_max_packet_size = libusb.libusb_get_max_packet_size -libusb_get_max_packet_size.argtypes = [libusb_device_p, c_uchar] -#int libusb_get_max_iso_packet_size(libusb_device *dev, unsigned char endpoint); -try: - libusb_get_max_iso_packet_size = libusb.libusb_get_max_iso_packet_size -except AttributeError: - # FreeBSD's reimplementation of the API [used to ]lack[s] this function. - # It has been added in r234193, but is lacking in default 9.x install as - # of this change. Provide a fallback to error-out only if actually used. - # pylint: disable=unused-argument - def libusb_get_max_iso_packet_size(_, __): - raise NotImplementedError - # pylint: enable=unused-argument -else: - libusb_get_max_iso_packet_size.argtypes = [libusb_device_p, c_uchar] - -#int libusb_open(libusb_device *dev, libusb_device_handle **handle); -libusb_open = libusb.libusb_open -libusb_open.argtypes = [libusb_device_p, libusb_device_handle_p_p] -#void libusb_close(libusb_device_handle *dev_handle); -libusb_close = libusb.libusb_close -libusb_close.argtypes = [libusb_device_handle_p] -libusb_close.restype = None -#libusb_device *libusb_get_device(libusb_device_handle *dev_handle); -libusb_get_device = libusb.libusb_get_device -libusb_get_device.argtypes = [libusb_device_handle_p] -libusb_get_device.restype = libusb_device_p - -#int libusb_set_configuration(libusb_device_handle *dev, int configuration); -libusb_set_configuration = libusb.libusb_set_configuration -libusb_set_configuration.argtypes = [libusb_device_handle_p, c_int] -#int libusb_claim_interface(libusb_device_handle *dev, int iface); -libusb_claim_interface = libusb.libusb_claim_interface -libusb_claim_interface.argtypes = [libusb_device_handle_p, c_int] -#int libusb_release_interface(libusb_device_handle *dev, int iface); -libusb_release_interface = libusb.libusb_release_interface -libusb_release_interface.argtypes = [libusb_device_handle_p, c_int] - -#libusb_device_handle *libusb_open_device_with_vid_pid(libusb_context *ctx, -# uint16_t vendor_id, uint16_t product_id); -libusb_open_device_with_vid_pid = libusb.libusb_open_device_with_vid_pid -libusb_open_device_with_vid_pid.argtypes = [ - libusb_context_p, c_uint16, c_uint16] -libusb_open_device_with_vid_pid.restype = libusb_device_handle_p - -#int libusb_set_interface_alt_setting(libusb_device_handle *dev, -# int interface_number, int alternate_setting); -libusb_set_interface_alt_setting = libusb.libusb_set_interface_alt_setting -libusb_set_interface_alt_setting.argtypes = [ - libusb_device_handle_p, c_int, c_int] -#int libusb_clear_halt(libusb_device_handle *dev, unsigned char endpoint); -libusb_clear_halt = libusb.libusb_clear_halt -libusb_clear_halt.argtypes = [libusb_device_handle_p, c_uchar] -#int libusb_reset_device(libusb_device_handle *dev); -libusb_reset_device = libusb.libusb_reset_device -libusb_reset_device.argtypes = [libusb_device_handle_p] - -#int libusb_kernel_driver_active(libusb_device_handle *dev, int interface); -libusb_kernel_driver_active = libusb.libusb_kernel_driver_active -libusb_kernel_driver_active.argtypes = [libusb_device_handle_p, c_int] -#int libusb_detach_kernel_driver(libusb_device_handle *dev, int interface); -libusb_detach_kernel_driver = libusb.libusb_detach_kernel_driver -libusb_detach_kernel_driver.argtypes = [libusb_device_handle_p, c_int] -#int libusb_attach_kernel_driver(libusb_device_handle *dev, int interface); -libusb_attach_kernel_driver = libusb.libusb_attach_kernel_driver -libusb_attach_kernel_driver.argtypes = [libusb_device_handle_p, c_int] -try: - #int libusb_set_auto_detach_kernel_driver( - # libusb_device_handle *dev, int enable); - libusb_set_auto_detach_kernel_driver = \ - libusb.libusb_set_auto_detach_kernel_driver -except AttributeError: - pass -else: - libusb_set_auto_detach_kernel_driver.argtypes = [ - libusb_device_handle_p, c_int] - libusb_set_auto_detach_kernel_driver.restype = c_int - -# Get the data section of a control transfer. This convenience function is here -# to remind you that the data does not start until 8 bytes into the actual -# buffer, as the setup packet comes first. -# -# Calling this function only makes sense from a transfer callback function, -# or situations where you have already allocated a suitably sized buffer at -# transfer->buffer. -# -# \param transfer a transfer -# \returns pointer to the first byte of the data section - -def libusb_control_transfer_get_data(transfer_p): - transfer = transfer_p.contents - return buffer_at(transfer.buffer.value, transfer.length)[ - LIBUSB_CONTROL_SETUP_SIZE:] - -def libusb_control_transfer_get_setup(transfer_p): - return cast(transfer_p.contents.buffer, libusb_control_setup_p) - -def libusb_fill_control_setup( - setup_p, bmRequestType, bRequest, wValue, wIndex, wLength): - setup = cast(setup_p, libusb_control_setup_p).contents - setup.bmRequestType = bmRequestType - setup.bRequest = bRequest - setup.wValue = libusb_cpu_to_le16(wValue) - setup.wIndex = libusb_cpu_to_le16(wIndex) - setup.wLength = libusb_cpu_to_le16(wLength) - -#struct libusb_transfer *libusb_alloc_transfer(int iso_packets); -libusb_alloc_transfer = libusb.libusb_alloc_transfer -libusb_alloc_transfer.argtypes = [c_int] -libusb_alloc_transfer.restype = libusb_transfer_p -#int libusb_submit_transfer(struct libusb_transfer *transfer); -libusb_submit_transfer = libusb.libusb_submit_transfer -libusb_submit_transfer.argtypes = [libusb_transfer_p] -#int libusb_cancel_transfer(struct libusb_transfer *transfer); -libusb_cancel_transfer = libusb.libusb_cancel_transfer -libusb_cancel_transfer.argtypes = [libusb_transfer_p] -#void libusb_free_transfer(struct libusb_transfer *transfer); -libusb_free_transfer = libusb.libusb_free_transfer -libusb_free_transfer.argtypes = [libusb_transfer_p] -libusb_free_transfer.restype = None - -# pylint: disable=redefined-builtin -def libusb_fill_control_transfer( - transfer_p, dev_handle, buffer, callback, user_data, timeout): - transfer = transfer_p.contents - transfer.dev_handle = dev_handle - transfer.endpoint = 0 - # pylint: disable=undefined-variable - transfer.type = LIBUSB_TRANSFER_TYPE_CONTROL - # pylint: enable=undefined-variable - transfer.timeout = timeout - transfer.buffer = cast(buffer, c_void_p) - if buffer is not None: - setup = cast(buffer, libusb_control_setup_p).contents - # pylint: disable=undefined-variable - transfer.length = LIBUSB_CONTROL_SETUP_SIZE + \ - libusb_le16_to_cpu(setup.wLength) - # pylint: enable=undefined-variable - transfer.user_data = user_data - transfer.callback = callback -# pylint: enable=redefined-builtin - -# pylint: disable=redefined-builtin -def libusb_fill_bulk_transfer( - transfer_p, dev_handle, endpoint, buffer, length, - callback, user_data, timeout): - transfer = transfer_p.contents - transfer.dev_handle = dev_handle - transfer.endpoint = endpoint - # pylint: disable=undefined-variable - transfer.type = LIBUSB_TRANSFER_TYPE_BULK - # pylint: enable=undefined-variable - transfer.timeout = timeout - transfer.buffer = cast(buffer, c_void_p) - transfer.length = length - transfer.user_data = user_data - transfer.callback = callback -# pylint: enable=redefined-builtin - -# pylint: disable=redefined-builtin -def libusb_fill_interrupt_transfer( - transfer_p, dev_handle, endpoint, buffer, - length, callback, user_data, timeout): - transfer = transfer_p.contents - transfer.dev_handle = dev_handle - transfer.endpoint = endpoint - # pylint: disable=undefined-variable - transfer.type = LIBUSB_TRANSFER_TYPE_INTERRUPT - # pylint: enable=undefined-variable - transfer.timeout = timeout - transfer.buffer = cast(buffer, c_void_p) - transfer.length = length - transfer.user_data = user_data - transfer.callback = callback -# pylint: enable=redefined-builtin - -# pylint: disable=redefined-builtin -def libusb_fill_iso_transfer( - transfer_p, dev_handle, endpoint, buffer, length, - num_iso_packets, callback, user_data, timeout): - transfer = transfer_p.contents - transfer.dev_handle = dev_handle - transfer.endpoint = endpoint - # pylint: disable=undefined-variable - transfer.type = LIBUSB_TRANSFER_TYPE_ISOCHRONOUS - # pylint: enable=undefined-variable - transfer.timeout = timeout - transfer.buffer = cast(buffer, c_void_p) - transfer.length = length - transfer.num_iso_packets = num_iso_packets - transfer.user_data = user_data - transfer.callback = callback -# pylint: enable=redefined-builtin - -def _get_iso_packet_list(transfer): - list_type = libusb_iso_packet_descriptor * transfer.num_iso_packets - return list_type.from_address(addressof(transfer.iso_packet_desc)) - -def get_iso_packet_list(transfer_p): - """ - Python-specific helper extracting a list of iso packet descriptors, - because it's not as straight-forward as in C. - """ - return _get_iso_packet_list(transfer_p.contents) - -def _get_iso_packet_buffer(transfer, offset, length): - return buffer_at(transfer.buffer + offset, length) - -def get_iso_packet_buffer_list(transfer_p): - """ - Python-specific helper extracting a list of iso packet buffers. - """ - transfer = transfer_p.contents - offset = 0 - result = [] - append = result.append - for iso_transfer in _get_iso_packet_list(transfer): - length = iso_transfer.length - append(_get_iso_packet_buffer(transfer, offset, length)) - offset += length - return result - -def get_extra(descriptor): - """ - Python-specific helper to access "extra" field of descriptors, - because it's not as straight-forward as in C. - Returns a list, where each entry is an individual extra descriptor. - """ - result = [] - extra_length = descriptor.extra_length - if extra_length: - extra = buffer_at(descriptor.extra.value, extra_length) - append = result.append - while extra: - length = _string_item_to_int(extra[0]) - if not 0 < length <= len(extra): - raise ValueError( - 'Extra descriptor %i is incomplete/invalid' % ( - len(result), - ), - ) - append(extra[:length]) - extra = extra[length:] - return result - -def libusb_set_iso_packet_lengths(transfer_p, length): - transfer = transfer_p.contents - for iso_packet_desc in _get_iso_packet_list(transfer): - iso_packet_desc.length = length - -def libusb_get_iso_packet_buffer(transfer_p, packet): - transfer = transfer_p.contents - offset = 0 - if packet >= transfer.num_iso_packets: - return None - iso_packet_desc_list = _get_iso_packet_list(transfer) - for i in xrange(packet): - offset += iso_packet_desc_list[i].length - return _get_iso_packet_buffer( - transfer, offset, iso_packet_desc_list[packet].length) - -def libusb_get_iso_packet_buffer_simple(transfer_p, packet): - transfer = transfer_p.contents - if packet >= transfer.num_iso_packets: - return None - iso_length = transfer.iso_packet_desc.length - return _get_iso_packet_buffer(transfer, iso_length * packet, iso_length) - -# sync I/O - -#int libusb_control_transfer(libusb_device_handle *dev_handle, -# uint8_t request_type, uint8_t request, uint16_t value, uint16_t index, -# unsigned char *data, uint16_t length, unsigned int timeout); -libusb_control_transfer = libusb.libusb_control_transfer -libusb_control_transfer.argtypes = [libusb_device_handle_p, c_uint8, c_uint8, - c_uint16, c_uint16, c_void_p, c_uint16, - c_uint] - -#int libusb_bulk_transfer(libusb_device_handle *dev_handle, -# unsigned char endpoint, unsigned char *data, int length, -# int *actual_length, unsigned int timeout); -libusb_bulk_transfer = libusb.libusb_bulk_transfer -libusb_bulk_transfer.argtypes = [libusb_device_handle_p, c_uchar, c_void_p, - c_int, c_int_p, c_uint] - -#int libusb_interrupt_transfer(libusb_device_handle *dev_handle, -# unsigned char endpoint, unsigned char *data, int length, -# int *actual_length, unsigned int timeout); -libusb_interrupt_transfer = libusb.libusb_interrupt_transfer -libusb_interrupt_transfer.argtypes = [libusb_device_handle_p, c_uchar, - c_void_p, c_int, c_int_p, c_uint] - -# pylint: disable=undefined-variable -def libusb_get_descriptor(dev, desc_type, desc_index, data, length): - return libusb_control_transfer(dev, LIBUSB_ENDPOINT_IN, - LIBUSB_REQUEST_GET_DESCRIPTOR, - (desc_type << 8) | desc_index, 0, data, - length, 1000) -# pylint: enable=undefined-variable - -# pylint: disable=undefined-variable -def libusb_get_string_descriptor(dev, desc_index, langid, data, length): - return libusb_control_transfer(dev, LIBUSB_ENDPOINT_IN, - LIBUSB_REQUEST_GET_DESCRIPTOR, - (LIBUSB_DT_STRING << 8) | desc_index, - langid, data, length, 1000) -# pylint: enable=undefined-variable - -#int libusb_get_string_descriptor_ascii(libusb_device_handle *dev, -# uint8_t index, unsigned char *data, int length); -libusb_get_string_descriptor_ascii = libusb.libusb_get_string_descriptor_ascii -libusb_get_string_descriptor_ascii.argtypes = [libusb_device_handle_p, - c_uint8, c_void_p, c_int] - -# polling and timeouts - -#int libusb_try_lock_events(libusb_context *ctx); -libusb_try_lock_events = libusb.libusb_try_lock_events -libusb_try_lock_events.argtypes = [libusb_context_p] -#void libusb_lock_events(libusb_context *ctx); -libusb_lock_events = libusb.libusb_lock_events -libusb_lock_events.argtypes = [libusb_context_p] -#void libusb_unlock_events(libusb_context *ctx); -libusb_unlock_events = libusb.libusb_unlock_events -libusb_unlock_events.argtypes = [libusb_context_p] -libusb_unlock_events.restype = None -#int libusb_event_handling_ok(libusb_context *ctx); -libusb_event_handling_ok = libusb.libusb_event_handling_ok -libusb_event_handling_ok.argtypes = [libusb_context_p] -#int libusb_event_handler_active(libusb_context *ctx); -libusb_event_handler_active = libusb.libusb_event_handler_active -libusb_event_handler_active.argtypes = [libusb_context_p] -#void libusb_lock_event_waiters(libusb_context *ctx); -libusb_lock_event_waiters = libusb.libusb_lock_event_waiters -libusb_lock_event_waiters.argtypes = [libusb_context_p] -libusb_lock_event_waiters.restype = None -#void libusb_unlock_event_waiters(libusb_context *ctx); -libusb_unlock_event_waiters = libusb.libusb_unlock_event_waiters -libusb_unlock_event_waiters.argtypes = [] -libusb_unlock_event_waiters.restype = None -#int libusb_wait_for_event(libusb_context *ctx, struct timeval *tv); -libusb_wait_for_event = libusb.libusb_wait_for_event -libusb_wait_for_event.argtypes = [libusb_context_p, timeval_p] - -#int libusb_handle_events_timeout(libusb_context *ctx, struct timeval *tv); -libusb_handle_events_timeout = libusb.libusb_handle_events_timeout -libusb_handle_events_timeout.argtypes = [libusb_context_p, timeval_p] -#int libusb_handle_events_timeout_completed(libusb_context *ctx, -# struct timeval *tv, int *completed); -try: - libusb_handle_events_timeout_completed = libusb.\ - libusb_handle_events_timeout_completed -except AttributeError: - # No safe replacement possible. - pass -else: - libusb_handle_events_timeout_completed.argtypes = [ - libusb_context_p, timeval_p, c_int_p] -#int libusb_handle_events(libusb_context *ctx); -libusb_handle_events = libusb.libusb_handle_events -libusb_handle_events.argtypes = [libusb_context_p] -#int libusb_handle_events_completed(libusb_context *ctx, int *completed); -try: - libusb_handle_events_completed = libusb.libusb_handle_events_completed -except AttributeError: - # No safe replacement possible. - pass -else: - libusb_handle_events_completed.argtypes = [libusb_context_p, c_int_p] -#int libusb_handle_events_locked(libusb_context *ctx, struct timeval *tv); -libusb_handle_events_locked = libusb.libusb_handle_events_locked -libusb_handle_events_locked.argtypes = [libusb_context_p, timeval_p] -#int libusb_get_next_timeout(libusb_context *ctx, struct timeval *tv); -libusb_get_next_timeout = libusb.libusb_get_next_timeout -libusb_get_next_timeout.argtypes = [libusb_context_p, timeval_p] - -class libusb_pollfd(Structure): - _fields_ = [ - ('fd', c_int), - ('events', c_short), - ] -libusb_pollfd_p = POINTER(libusb_pollfd) -libusb_pollfd_p_p = POINTER(libusb_pollfd_p) - -libusb_pollfd_added_cb_p = CFUNCTYPE(None, c_int, c_short, py_object) -libusb_pollfd_removed_cb_p = CFUNCTYPE(None, c_int, py_object) - -#const struct libusb_pollfd **libusb_get_pollfds(libusb_context *ctx); -libusb_get_pollfds = libusb.libusb_get_pollfds -libusb_get_pollfds.argtypes = [libusb_context_p] -libusb_get_pollfds.restype = libusb_pollfd_p_p -#void libusb_set_pollfd_notifiers(libusb_context *ctx, -# libusb_pollfd_added_cb added_cb, libusb_pollfd_removed_cb removed_cb, -# void *user_data); -libusb_set_pollfd_notifiers = libusb.libusb_set_pollfd_notifiers -libusb_set_pollfd_notifiers.argtypes = [libusb_context_p, - libusb_pollfd_added_cb_p, - libusb_pollfd_removed_cb_p, py_object] -libusb_set_pollfd_notifiers.restype = None - -#typedef int libusb_hotplug_callback_handle; -libusb_hotplug_callback_handle = c_int - -libusb_hotplug_flag = Enum({ - 'LIBUSB_HOTPLUG_ENUMERATE': 1, -}) - -libusb_hotplug_event = Enum({ - 'LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED': 0x01, - 'LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT': 0x02, -}) - -LIBUSB_HOTPLUG_MATCH_ANY = -1 - -#typedef int (*libusb_hotplug_callback_fn)(libusb_context *ctx, -# libusb_device *device, libusb_hotplug_event event, void *user_data); -libusb_hotplug_callback_fn_p = CFUNCTYPE( - c_int, libusb_context_p, libusb_device_p, c_int, c_void_p) - -#int libusb_hotplug_register_callback(libusb_context *ctx, -# libusb_hotplug_event events, libusb_hotplug_flag flags, -# int vendor_id, int product_id, int dev_class, -# libusb_hotplug_callback_fn cb_fn, void *user_data, -# libusb_hotplug_callback_handle *handle); -try: - libusb_hotplug_register_callback = libusb.libusb_hotplug_register_callback -except AttributeError: - pass -else: - libusb_hotplug_register_callback.argtypes = [ - libusb_context_p, - c_int, c_int, - c_int, c_int, c_int, - libusb_hotplug_callback_fn_p, c_void_p, - POINTER(libusb_hotplug_callback_handle), - ] - libusb_hotplug_register_callback.restype = c_int - -#void libusb_hotplug_deregister_callback(libusb_context *ctx, -# libusb_hotplug_callback_handle handle); -try: - libusb_hotplug_deregister_callback = \ - libusb.libusb_hotplug_deregister_callback -except AttributeError: - pass -else: - libusb_hotplug_deregister_callback.argtypes = [ - libusb_context_p, - libusb_hotplug_callback_handle, - ] - libusb_hotplug_deregister_callback.restype = None - -# /libusb.h +# pylint: disable=unused-wildcard-import,wrong-import-position +# pylint: disable=wildcard-import,missing-module-docstring +from __future__ import absolute_import +from ._libusb1 import loadLibrary +loadLibrary() +from ._libusb1 import * diff --git a/usb1/testUSB1.py b/usb1/testUSB1.py index d9bb51f..b6d4527 100644 --- a/usb1/testUSB1.py +++ b/usb1/testUSB1.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2018 Vincent Pelletier +# Copyright (C) 2010-2021 Vincent Pelletier # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -15,60 +15,91 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # pylint: disable=invalid-name, missing-docstring, too-many-public-methods -import unittest -import sys + +from ctypes import pointer, sizeof +import functools +import gc import itertools -import select -import threading +import unittest +import warnings +import weakref import usb1 -import libusb1 -from ctypes import pointer +from . import libusb1 buff_len = 1024 buffer_base = [x % 256 for x in range(buff_len)] -if sys.version_info[0] == 3: - buff = bytes(buffer_base) - other_buff = bytes(reversed(buffer_base)) -else: - buff = ''.join(chr(x) for x in buffer_base) - other_buff = ''.join(chr(x) for x in reversed(buffer_base)) +buff = bytes(buffer_base) +other_buff = bytes(reversed(buffer_base)) bytearray_buff = bytearray(buffer_base) class USBContext(usb1.USBContext): def open(self): try: - return super(USBContext, self).open() - except usb1.USBError: + return super().open() + except usb1.USBError as exc: raise unittest.SkipTest( 'usb1.USBContext() fails - no USB bus on system ?' - ) + ) from exc -class PollDetector(object): - def __init__(self, *args, **kw): +def checkTransferAllocCount(func): + @functools.wraps(func) + def wrapper(self, *args, **kw): + before = self.transfer_alloc_count + libusb_free_transfer = libusb1.libusb_free_transfer + libusb_alloc_transfer = libusb1.libusb_alloc_transfer try: - poll = select.poll - except AttributeError: - raise unittest.SkipTest('select.poll missing') - self.__poll = poll(*args, **kw) - self.__event = threading.Event() + # pylint: disable=protected-access + libusb1.libusb_free_transfer = self._fakeFreeTransfer + libusb1.libusb_alloc_transfer = self._fakeAllocTransfer + # pylint: enable=protected-access + result = func(self, *args, **kw) + finally: + libusb1.libusb_free_transfer = libusb_free_transfer + libusb1.libusb_alloc_transfer = libusb_alloc_transfer + gc.collect() + self.assertEqual(self.transfer_alloc_count, before) + return result + return wrapper - def poll(self, *args, **kw): - self.__event.set() - return self.__poll.poll(*args, **kw) +class USBTransferTests(unittest.TestCase): + def __init__(self, *args, **kw): + super().__init__(*args, **kw) + usb1.loadLibrary() + self.transfer_alloc_count = 0 - def wait(self, *args, **kw): - self.__event.wait(*args, **kw) + def _fakeFreeTransfer(self, _): + self.transfer_alloc_count -= 1 - def __getattr__(self, name): - return getattr(self.__poll, name) + def _fakeAllocTransfer(self, isochronous_count): + self.transfer_alloc_count += 1 + buffer = bytearray( + sizeof( + libusb1.libusb_transfer, + ) + sizeof( + libusb1.libusb_iso_packet_descriptor, + ) * max(0, isochronous_count - 1), + ) + transfer = libusb1.libusb_transfer.from_buffer(buffer) + # Keep a reference (in the finalizer itself) to the buffer for as long + # as transfer is alive. + weakref.finalize(transfer, lambda _: None, buffer) + return pointer(transfer) -class USBTransferTests(unittest.TestCase): @staticmethod - def getTransfer(iso_packets=0): + def getTransfer(iso_packets=0, short_is_error=False, add_zero_packet=False): # Dummy handle return usb1.USBTransfer( - pointer(libusb1.libusb_device_handle()), - iso_packets, lambda x: None, lambda x: None) + context=None, + handle=None, + iso_packets=iso_packets, + before_submit=lambda x: None, + after_completion=lambda x: None, + getFinalizer=( + lambda obj, func, **kw: weakref.finalize(obj, func, **kw) + ), + short_is_error=short_is_error, + add_zero_packet=add_zero_packet, + ) @staticmethod def testGetVersion(): @@ -82,18 +113,19 @@ def testHasCapability(): """ Just testing hasCapability doesn't raise... """ - usb1.hasCapability(usb1.CAP_HAS_CAPABILITY) + usb1.hasCapability(usb1.CAP_HAS_CAPABILITY) # pylint: disable=no-member + @checkTransferAllocCount def testSetControl(self): """ Simplest test: feed some data, must not raise. """ transfer = self.getTransfer() - request_type = usb1.TYPE_STANDARD - request = usb1.REQUEST_GET_STATUS + request_type = usb1.TYPE_STANDARD # pylint: disable=no-member + request = usb1.REQUEST_GET_STATUS # pylint: disable=no-member value = 0 index = 0 - def callback(transfer): + def callback(_): pass user_data = [] timeout = 1000 @@ -117,10 +149,24 @@ def callback(transfer): request_type, request, value, index, buff, callback=callback) # No callback transfer.setControl(request_type, request, value, index, buff) + self.assertFalse(transfer.isShortAnError()) + self.assertFalse(transfer.isZeroPacketAdded()) + transfer.setShortIsError(True) + self.assertTrue(transfer.isShortAnError()) + self.assertFalse(transfer.isZeroPacketAdded()) + transfer.setAddZeroPacket(True) + self.assertTrue(transfer.isShortAnError()) + self.assertTrue(transfer.isZeroPacketAdded()) + transfer.setShortIsError(False) + self.assertFalse(transfer.isShortAnError()) + self.assertTrue(transfer.isZeroPacketAdded()) + transfer.setAddZeroPacket(False) + self.assertFalse(transfer.isShortAnError()) + self.assertFalse(transfer.isZeroPacketAdded()) def _testTransferSetter(self, transfer, setter_id): endpoint = 0x81 - def callback(transfer): + def callback(_): pass user_data = [] timeout = 1000 @@ -147,6 +193,7 @@ def callback(transfer): # No callback setter(endpoint, buff) + @checkTransferAllocCount def testSetBulk(self): """ Simplest test: feed some data, must not raise. @@ -154,6 +201,7 @@ def testSetBulk(self): """ self._testTransferSetter(self.getTransfer(), 'setBulk') + @checkTransferAllocCount def testSetInterrupt(self): """ Simplest test: feed some data, must not raise. @@ -161,6 +209,7 @@ def testSetInterrupt(self): """ self._testTransferSetter(self.getTransfer(), 'setInterrupt') + @checkTransferAllocCount def testSetIsochronous(self): """ Simplest test: feed some data, must not raise. @@ -192,67 +241,24 @@ def testSetIsochronous(self): buff, ) + @checkTransferAllocCount def testSetGetCallback(self): transfer = self.getTransfer() - def callback(transfer): + def callback(_): pass transfer.setCallback(callback) got_callback = transfer.getCallback() self.assertEqual(callback, got_callback) - def testUSBPollerThreadExit(self): - """ - USBPollerThread must exit by itself when context is destroyed. - """ - with USBContext() as context: - poll_detector = PollDetector() - try: - poller = usb1.USBPollerThread(context, poll_detector) - except OSError: - raise unittest.SkipTest('libusb without file descriptor events') - poller.start() - poll_detector.wait(1) - poller.join(1) - self.assertFalse(poller.is_alive()) - - def testUSBPollerThreadException(self): - """ - USBPollerThread exception handling. - """ - class FakeEventPoll(PollDetector): - # pylint: disable=method-hidden - def poll(self, *args, **kw): - self.poll = super(FakeEventPoll, self).poll - return ['dummy'] - # pylint: enable=method-hidden - with USBContext() as context: - def fakeHandleEventsLocked(): - raise usb1.USBError(0) - context.handleEventsLocked = fakeHandleEventsLocked - exception_event = threading.Event() - exception_list = [] - def exceptionHandler(exc): - exception_list.append(exc) - exception_event.set() - try: - poller = usb1.USBPollerThread( - context, FakeEventPoll(), exceptionHandler) - except OSError: - raise unittest.SkipTest('libusb without file descriptor events') - poller.start() - exception_event.wait(1) - self.assertTrue(exception_list, exception_list) - self.assertTrue(poller.is_alive()) - - @staticmethod - def testDescriptors(): + def _testDescriptors(self, get_extra=False): """ Test descriptor walk. Needs any usb device, which won't be opened. """ - with USBContext() as context: + with USBContext() as context: # pylint: disable=too-many-nested-blocks device_list = context.getDeviceList(skip_on_error=True) found = False + seen_extra = False for device in device_list: device.getBusNumber() device.getPortNumber() @@ -262,12 +268,26 @@ def testDescriptors(): for endpoint in settings: pass for configuration in device.iterConfigurations(): + if get_extra and len(configuration.getExtra()) > 0: + seen_extra = True for interface in configuration: for settings in interface: + if get_extra and len(settings.getExtra()) > 0: + seen_extra = True for endpoint in settings: + if get_extra and len(endpoint.getExtra()) > 0: + seen_extra = True found = True if not found: raise unittest.SkipTest('descriptor walk test did not complete') + if get_extra and not seen_extra: + raise unittest.SkipTest('did not see any extra descriptors') + + def testDescriptors(self): + self._testDescriptors() + + def testDescriptorsWithExtra(self): + self._testDescriptors(get_extra=True) def testDefaultEnumScope(self): """ @@ -275,15 +295,12 @@ def testDefaultEnumScope(self): """ ENUM_NAME = 'THE_ANSWER' ENUM_VALUE = 42 - local_dict = locals() global_dict = globals() - self.assertEqual(local_dict.get(ENUM_NAME), None) + self.assertEqual(locals().get(ENUM_NAME), None) self.assertEqual(global_dict.get(ENUM_NAME), None) self.assertEqual(getattr(libusb1, ENUM_NAME, None), None) - # pylint: disable=unused-variable - TEST_ENUM = libusb1.Enum({ENUM_NAME: ENUM_VALUE}) - # pylint: enable=unused-variable - self.assertEqual(local_dict.get(ENUM_NAME), ENUM_VALUE) + _ = libusb1.Enum({ENUM_NAME: ENUM_VALUE}) + self.assertEqual(locals().get(ENUM_NAME), ENUM_VALUE) self.assertEqual(global_dict.get(ENUM_NAME), None) self.assertEqual(getattr(libusb1, ENUM_NAME, None), None) @@ -293,16 +310,13 @@ def testExplicitEnumScope(self): """ ENUM_NAME = 'THE_ANSWER' ENUM_VALUE = 42 - local_dict = locals() global_dict = globals() - self.assertEqual(local_dict.get(ENUM_NAME), None) + self.assertEqual(locals().get(ENUM_NAME), None) self.assertEqual(global_dict.get(ENUM_NAME), None) self.assertEqual(getattr(libusb1, ENUM_NAME, None), None) - # pylint: disable=unused-variable - TEST_ENUM = libusb1.Enum({ENUM_NAME: ENUM_VALUE}, global_dict) - # pylint: enable=unused-variable + _ = libusb1.Enum({ENUM_NAME: ENUM_VALUE}, global_dict) try: - self.assertEqual(local_dict.get(ENUM_NAME), None) + self.assertEqual(locals().get(ENUM_NAME), None) self.assertEqual(global_dict.get(ENUM_NAME), ENUM_VALUE) self.assertEqual(getattr(libusb1, ENUM_NAME, None), None) finally: @@ -316,30 +330,74 @@ def testImplicitUSBContextOpening(self): """ context = USBContext() # Deprecated try: + warnings.filterwarnings('ignore', category=DeprecationWarning) fd_list = context.getPollFDList() except NotImplementedError: - raise unittest.SkipTest('libusb without file descriptor events') + raise unittest.SkipTest( + 'libusb without file descriptor events', + ) from None + finally: + warnings.resetwarnings() self.assertNotEqual(fd_list, None) context.exit() # Deprecated self.assertEqual(context.getPollFDList(), None) - def testUSBTransferMayRaiseUSBError(self): - """ - mayRaiseUSBError needs to be a class property to be reliably able - to call it during interpreter shutdown. But setting a function as - property makes it bound to the instance when accessed. This is not - obviously visible because mayRaiseUSBError has a should-never-be-used - second argument for exactly the same purpose, but which accidentally - receives the value intended as the first parameter (the first parameter - being replaced by "self" as for any bound method). - So this test verifies that USBTranfer.__mayRaiseUSBError behaves the - intended way. - And to make this test work when there is no USB device around, directly - instanciate USBTransfer and access tested private property. - """ - transfer = usb1.USBTransfer(None, 0, None, None) - transfer._USBTransfer__mayRaiseUSBError(0) - self.assertRaises(usb1.USBErrorIO, transfer._USBTransfer__mayRaiseUSBError, usb1.ERROR_IO) + def testHasVersion(self): + # Property is present and non-empty + self.assertTrue(usb1.__version__) + + def testGlobalLogCallback(self): + if hasattr(libusb1, 'libusb_init'): + raise unittest.SkipTest('libusb without libusb_init_context') + message_list = [] + def callback(context, level, message): + message_list.append((context, level, message)) + try: + usb1.setLogCallback(callback) + with USBContext( + log_level=usb1.LOG_LEVEL_DEBUG, # pylint: disable=no-member + ): + pass + finally: + usb1.setLogCallback(None) + self.assertTrue(message_list) + + def testContextLogCallback(self): + if hasattr(libusb1, 'libusb_init'): + raise unittest.SkipTest('libusb without libusb_init_context') + message_list = [] + def callback(context, level, message): + message_list.append((context, level, message)) + def log_silencer(_, __, ___): + pass + try: + # Note: silencing global logs is needed here because we lower the + # log level to debug, which causes the log line to also be emitted + # to the global logger, which when no callback is set may output + # to stderr. + usb1.setLogCallback(log_silencer) + with USBContext( + log_level=usb1.LOG_LEVEL_DEBUG, # pylint: disable=no-member + log_callback=callback, + ) as ctx: + ctx.setLogCallback(None) + finally: + usb1.setLogCallback(None) + self.assertTrue(message_list) + + def testSetLocale(self): + if not hasattr(libusb1, 'libusb_setlocale'): + raise unittest.SkipTest('libusb without libusb_setlocale') + err = usb1.USBErrorIO() # pylint: disable=no-member + usb1.setLocale('en') + caption_en = err.getMessage() + self.assertTrue(caption_en) + usb1.setLocale('fr') + try: + caption_fr = err.getMessage() + self.assertNotEqual(caption_en, caption_fr) + finally: + usb1.setLocale('en') if __name__ == '__main__': unittest.main() diff --git a/versioneer.py b/versioneer.py new file mode 100644 index 0000000..1e3753e --- /dev/null +++ b/versioneer.py @@ -0,0 +1,2277 @@ + +# Version: 0.29 + +"""The Versioneer - like a rocketeer, but for versions. + +The Versioneer +============== + +* like a rocketeer, but for versions! +* https://github.com/python-versioneer/python-versioneer +* Brian Warner +* License: Public Domain (Unlicense) +* Compatible with: Python 3.7, 3.8, 3.9, 3.10, 3.11 and pypy3 +* [![Latest Version][pypi-image]][pypi-url] +* [![Build Status][travis-image]][travis-url] + +This is a tool for managing a recorded version number in setuptools-based +python projects. The goal is to remove the tedious and error-prone "update +the embedded version string" step from your release process. Making a new +release should be as easy as recording a new tag in your version-control +system, and maybe making new tarballs. + + +## Quick Install + +Versioneer provides two installation modes. The "classic" vendored mode installs +a copy of versioneer into your repository. The experimental build-time dependency mode +is intended to allow you to skip this step and simplify the process of upgrading. + +### Vendored mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) + * Note that you will need to add `tomli; python_version < "3.11"` to your + build-time dependencies if you use `pyproject.toml` +* run `versioneer install --vendor` in your source tree, commit the results +* verify version information with `python setup.py version` + +### Build-time dependency mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) +* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) + to the `requires` key of the `build-system` table in `pyproject.toml`: + ```toml + [build-system] + requires = ["setuptools", "versioneer[toml]"] + build-backend = "setuptools.build_meta" + ``` +* run `versioneer install --no-vendor` in your source tree, commit the results +* verify version information with `python setup.py version` + +## Version Identifiers + +Source trees come from a variety of places: + +* a version-control system checkout (mostly used by developers) +* a nightly tarball, produced by build automation +* a snapshot tarball, produced by a web-based VCS browser, like github's + "tarball from tag" feature +* a release tarball, produced by "setup.py sdist", distributed through PyPI + +Within each source tree, the version identifier (either a string or a number, +this tool is format-agnostic) can come from a variety of places: + +* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows + about recent "tags" and an absolute revision-id +* the name of the directory into which the tarball was unpacked +* an expanded VCS keyword ($Id$, etc) +* a `_version.py` created by some earlier build step + +For released software, the version identifier is closely related to a VCS +tag. Some projects use tag names that include more than just the version +string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool +needs to strip the tag prefix to extract the version identifier. For +unreleased software (between tags), the version identifier should provide +enough information to help developers recreate the same tree, while also +giving them an idea of roughly how old the tree is (after version 1.2, before +version 1.3). Many VCS systems can report a description that captures this, +for example `git describe --tags --dirty --always` reports things like +"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the +0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has +uncommitted changes). + +The version identifier is used for multiple purposes: + +* to allow the module to self-identify its version: `myproject.__version__` +* to choose a name and prefix for a 'setup.py sdist' tarball + +## Theory of Operation + +Versioneer works by adding a special `_version.py` file into your source +tree, where your `__init__.py` can import it. This `_version.py` knows how to +dynamically ask the VCS tool for version information at import time. + +`_version.py` also contains `$Revision$` markers, and the installation +process marks `_version.py` to have this marker rewritten with a tag name +during the `git archive` command. As a result, generated tarballs will +contain enough information to get the proper version. + +To allow `setup.py` to compute a version too, a `versioneer.py` is added to +the top level of your source tree, next to `setup.py` and the `setup.cfg` +that configures it. This overrides several distutils/setuptools commands to +compute the version when invoked, and changes `setup.py build` and `setup.py +sdist` to replace `_version.py` with a small static file that contains just +the generated version data. + +## Installation + +See [INSTALL.md](./INSTALL.md) for detailed installation instructions. + +## Version-String Flavors + +Code which uses Versioneer can learn about its version string at runtime by +importing `_version` from your main `__init__.py` file and running the +`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can +import the top-level `versioneer.py` and run `get_versions()`. + +Both functions return a dictionary with different flavors of version +information: + +* `['version']`: A condensed version string, rendered using the selected + style. This is the most commonly used value for the project's version + string. The default "pep440" style yields strings like `0.11`, + `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section + below for alternative styles. + +* `['full-revisionid']`: detailed revision identifier. For Git, this is the + full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". + +* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the + commit date in ISO 8601 format. This will be None if the date is not + available. + +* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that + this is only accurate if run in a VCS checkout, otherwise it is likely to + be False or None + +* `['error']`: if the version string could not be computed, this will be set + to a string describing the problem, otherwise it will be None. It may be + useful to throw an exception in setup.py if this is set, to avoid e.g. + creating tarballs with a version string of "unknown". + +Some variants are more useful than others. Including `full-revisionid` in a +bug report should allow developers to reconstruct the exact code being tested +(or indicate the presence of local changes that should be shared with the +developers). `version` is suitable for display in an "about" box or a CLI +`--version` output: it can be easily compared against release notes and lists +of bugs fixed in various releases. + +The installer adds the following text to your `__init__.py` to place a basic +version in `YOURPROJECT.__version__`: + + from ._version import get_versions + __version__ = get_versions()['version'] + del get_versions + +## Styles + +The setup.cfg `style=` configuration controls how the VCS information is +rendered into a version string. + +The default style, "pep440", produces a PEP440-compliant string, equal to the +un-prefixed tag name for actual releases, and containing an additional "local +version" section with more detail for in-between builds. For Git, this is +TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags +--dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the +tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and +that this commit is two revisions ("+2") beyond the "0.11" tag. For released +software (exactly equal to a known tag), the identifier will only contain the +stripped tag, e.g. "0.11". + +Other styles are available. See [details.md](details.md) in the Versioneer +source tree for descriptions. + +## Debugging + +Versioneer tries to avoid fatal errors: if something goes wrong, it will tend +to return a version of "0+unknown". To investigate the problem, run `setup.py +version`, which will run the version-lookup code in a verbose mode, and will +display the full contents of `get_versions()` (including the `error` string, +which may help identify what went wrong). + +## Known Limitations + +Some situations are known to cause problems for Versioneer. This details the +most significant ones. More can be found on Github +[issues page](https://github.com/python-versioneer/python-versioneer/issues). + +### Subprojects + +Versioneer has limited support for source trees in which `setup.py` is not in +the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are +two common reasons why `setup.py` might not be in the root: + +* Source trees which contain multiple subprojects, such as + [Buildbot](https://github.com/buildbot/buildbot), which contains both + "master" and "slave" subprojects, each with their own `setup.py`, + `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI + distributions (and upload multiple independently-installable tarballs). +* Source trees whose main purpose is to contain a C library, but which also + provide bindings to Python (and perhaps other languages) in subdirectories. + +Versioneer will look for `.git` in parent directories, and most operations +should get the right version string. However `pip` and `setuptools` have bugs +and implementation details which frequently cause `pip install .` from a +subproject directory to fail to find a correct version string (so it usually +defaults to `0+unknown`). + +`pip install --editable .` should work correctly. `setup.py install` might +work too. + +Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in +some later version. + +[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking +this issue. The discussion in +[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the +issue from the Versioneer side in more detail. +[pip PR#3176](https://github.com/pypa/pip/pull/3176) and +[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve +pip to let Versioneer work correctly. + +Versioneer-0.16 and earlier only looked for a `.git` directory next to the +`setup.cfg`, so subprojects were completely unsupported with those releases. + +### Editable installs with setuptools <= 18.5 + +`setup.py develop` and `pip install --editable .` allow you to install a +project into a virtualenv once, then continue editing the source code (and +test) without re-installing after every change. + +"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a +convenient way to specify executable scripts that should be installed along +with the python package. + +These both work as expected when using modern setuptools. When using +setuptools-18.5 or earlier, however, certain operations will cause +`pkg_resources.DistributionNotFound` errors when running the entrypoint +script, which must be resolved by re-installing the package. This happens +when the install happens with one version, then the egg_info data is +regenerated while a different version is checked out. Many setup.py commands +cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into +a different virtualenv), so this can be surprising. + +[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes +this one, but upgrading to a newer version of setuptools should probably +resolve it. + + +## Updating Versioneer + +To upgrade your project to a new release of Versioneer, do the following: + +* install the new Versioneer (`pip install -U versioneer` or equivalent) +* edit `setup.cfg` and `pyproject.toml`, if necessary, + to include any new configuration settings indicated by the release notes. + See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install --[no-]vendor` in your source tree, to replace + `SRC/_version.py` +* commit any changed files + +## Future Directions + +This tool is designed to make it easily extended to other version-control +systems: all VCS-specific components are in separate directories like +src/git/ . The top-level `versioneer.py` script is assembled from these +components by running make-versioneer.py . In the future, make-versioneer.py +will take a VCS name as an argument, and will construct a version of +`versioneer.py` that is specific to the given VCS. It might also take the +configuration arguments that are currently provided manually during +installation by editing setup.py . Alternatively, it might go the other +direction and include code from all supported VCS systems, reducing the +number of intermediate scripts. + +## Similar projects + +* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time + dependency +* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of + versioneer +* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools + plugin + +## License + +To make Versioneer easier to embed, all its code is dedicated to the public +domain. The `_version.py` that it creates is also in the public domain. +Specifically, both are released under the "Unlicense", as described in +https://unlicense.org/. + +[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg +[pypi-url]: https://pypi.python.org/pypi/versioneer/ +[travis-image]: +https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg +[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer + +""" +# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring +# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements +# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error +# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with +# pylint:disable=attribute-defined-outside-init,too-many-arguments + +import configparser +import errno +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union +from typing import NoReturn +import functools + +have_tomllib = True +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomli as tomllib + except ImportError: + have_tomllib = False + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + VCS: str + style: str + tag_prefix: str + versionfile_source: str + versionfile_build: Optional[str] + parentdir_prefix: Optional[str] + verbose: Optional[bool] + + +def get_root() -> str: + """Get the project root directory. + + We require that all commands are run from the project root, i.e. the + directory that contains setup.py, setup.cfg, and versioneer.py . + """ + root = os.path.realpath(os.path.abspath(os.getcwd())) + setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") + versioneer_py = os.path.join(root, "versioneer.py") + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): + # allow 'python path/to/setup.py COMMAND' + root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) + setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") + versioneer_py = os.path.join(root, "versioneer.py") + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): + err = ("Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND').") + raise VersioneerBadRootError(err) + try: + # Certain runtime workflows (setup.py install/develop in a setuptools + # tree) execute all dependencies in a single python process, so + # "versioneer" may be imported multiple times, and python's shared + # module-import table will cache the first one. So we can't use + # os.path.dirname(__file__), as that will find whichever + # versioneer.py was first imported, even in later projects. + my_path = os.path.realpath(os.path.abspath(__file__)) + me_dir = os.path.normcase(os.path.splitext(my_path)[0]) + vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) + if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): + print("Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(my_path), versioneer_py)) + except NameError: + pass + return root + + +def get_config_from_root(root: str) -> VersioneerConfig: + """Read the project setup.cfg file to determine Versioneer config.""" + # This might raise OSError (if setup.cfg is missing), or + # configparser.NoSectionError (if it lacks a [versioneer] section), or + # configparser.NoOptionError (if it lacks "VCS="). See the docstring at + # the top of versioneer.py for instructions on writing your setup.cfg . + root_pth = Path(root) + pyproject_toml = root_pth / "pyproject.toml" + setup_cfg = root_pth / "setup.cfg" + section: Union[Dict[str, Any], configparser.SectionProxy, None] = None + if pyproject_toml.exists() and have_tomllib: + try: + with open(pyproject_toml, 'rb') as fobj: + pp = tomllib.load(fobj) + section = pp['tool']['versioneer'] + except (tomllib.TOMLDecodeError, KeyError) as e: + print(f"Failed to load config from {pyproject_toml}: {e}") + print("Try to load it from setup.cfg") + if not section: + parser = configparser.ConfigParser() + with open(setup_cfg) as cfg_file: + parser.read_file(cfg_file) + parser.get("versioneer", "VCS") # raise error if missing + + section = parser["versioneer"] + + # `cast`` really shouldn't be used, but its simplest for the + # common VersioneerConfig users at the moment. We verify against + # `None` values elsewhere where it matters + + cfg = VersioneerConfig() + cfg.VCS = section['VCS'] + cfg.style = section.get("style", "") + cfg.versionfile_source = cast(str, section.get("versionfile_source")) + cfg.versionfile_build = section.get("versionfile_build") + cfg.tag_prefix = cast(str, section.get("tag_prefix")) + if cfg.tag_prefix in ("''", '""', None): + cfg.tag_prefix = "" + cfg.parentdir_prefix = section.get("parentdir_prefix") + if isinstance(section, configparser.SectionProxy): + # Make sure configparser translates to bool + cfg.verbose = section.getboolean("verbose") + else: + cfg.verbose = section.get("verbose") + + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +# these dictionaries contain VCS-specific tools +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} + + +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: + """Store f in HANDLERS[vcs][method].""" + HANDLERS.setdefault(vcs, {})[method] = f + return f + return decorate + + +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: + """Call the given command(s).""" + assert isinstance(commands, list) + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: + try: + dispcmd = str([command] + args) + # remember shell=False, so use git.cmd on windows, not just git + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) + break + except OSError as e: + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None, None + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + print("stdout was %s" % stdout) + return None, process.returncode + return stdout, process.returncode + + +LONG_VERSION_PY['git'] = r''' +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys +from typing import Any, Callable, Dict, List, Optional, Tuple +import functools + + +def get_keywords() -> Dict[str, str]: + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" + git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" + git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" + keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool + + +def get_config() -> VersioneerConfig: + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "%(STYLE)s" + cfg.tag_prefix = "%(TAG_PREFIX)s" + cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" + cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} + + +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: + """Call the given command(s).""" + assert isinstance(commands, list) + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: + try: + dispcmd = str([command] + args) + # remember shell=False, so use git.cmd on windows, not just git + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) + break + except OSError as e: + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %%s" %% dispcmd) + print(e) + return None, None + else: + if verbose: + print("unable to find command, tried %%s" %% (commands,)) + return None, None + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: + if verbose: + print("unable to run %%s (error)" %% dispcmd) + print("stdout was %%s" %% stdout) + return None, process.returncode + return stdout, process.returncode + + +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for _ in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %%s but none started with prefix %%s" %% + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords: Dict[str, str] = {} + try: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: + """Get version information from git keywords.""" + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") + date = keywords.get("date") + if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + + # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = {r.strip() for r in refnames.strip("()").split(",")} + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %%d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = {r for r in refs if re.search(r'\d', r)} + if verbose: + print("discarding '%%s', no digits" %% ",".join(refs - tags)) + if verbose: + print("likely tags: %%s" %% ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue + if verbose: + print("picking %%s" %% r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) + if rc != 0: + if verbose: + print("Directory %%s not under git control" %% root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces: Dict[str, Any] = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparsable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%%s'" + %% describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%%s' doesn't start with prefix '%%s'" + print(fmt %% (full_tag, tag_prefix)) + pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" + %% (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def plus_or_dot(pieces: Dict[str, Any]) -> str: + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces: Dict[str, Any]) -> str: + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). + + Exceptions: + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: + if pieces["distance"]: + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%%d" %% (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] + else: + # exception #1 + rendered = "0.post0.dev%%d" %% pieces["distance"] + return rendered + + +def render_pep440_post(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + return rendered + + +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces: Dict[str, Any]) -> str: + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces: Dict[str, Any]) -> str: + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%%s'" %% style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +def get_versions() -> Dict[str, Any]: + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for _ in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", "date": None} +''' + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords: Dict[str, str] = {} + try: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: + """Get version information from git keywords.""" + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") + date = keywords.get("date") + if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant + # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 + # -like" string, which we must then edit to make compliant), because + # it's been around since git-1.5.3, and it's too difficult to + # discover which version we're using, or to work around using an + # older one. + date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = {r.strip() for r in refnames.strip("()").split(",")} + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = {r for r in refs if re.search(r'\d', r)} + if verbose: + print("discarding '%s', no digits" % ",".join(refs - tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None, + "date": date} + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags", "date": None} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) + if rc != 0: + if verbose: + print("Directory %s not under git control" % root) + raise NotThisMethod("'git rev-parse --git-dir' returned error") + + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces: Dict[str, Any] = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparsable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits + + # commit date: see ISO-8601 comment in git_versions_from_keywords() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) + + return pieces + + +def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None: + """Git-specific installation logic for Versioneer. + + For Git, this means creating/changing .gitattributes to mark _version.py + for export-subst keyword substitution. + """ + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + files = [versionfile_source] + if ipy: + files.append(ipy) + if "VERSIONEER_PEP518" not in globals(): + try: + my_path = __file__ + if my_path.endswith((".pyc", ".pyo")): + my_path = os.path.splitext(my_path)[0] + ".py" + versioneer_file = os.path.relpath(my_path) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) + present = False + try: + with open(".gitattributes", "r") as fobj: + for line in fobj: + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + break + except OSError: + pass + if not present: + with open(".gitattributes", "a+") as fobj: + fobj.write(f"{versionfile_source} export-subst\n") + files.append(".gitattributes") + run_command(GITS, ["add", "--"] + files) + + +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes both + the project name and a version string. We will also support searching up + two directory levels for an appropriately named parent directory + """ + rootdirs = [] + + for _ in range(3): + dirname = os.path.basename(root) + if dirname.startswith(parentdir_prefix): + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None, "date": None} + rootdirs.append(root) + root = os.path.dirname(root) # up a level + + if verbose: + print("Tried directories %s but none started with prefix %s" % + (str(rootdirs), parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + + +SHORT_VERSION_PY = """ +# This file was generated by 'versioneer.py' (0.29) from +# revision-control system data, or from the parent directory name of an +# unpacked source archive. Distribution tarballs contain a pre-generated copy +# of this file. + +import json + +version_json = ''' +%s +''' # END VERSION_JSON + + +def get_versions(): + return json.loads(version_json) +""" + + +def versions_from_file(filename: str) -> Dict[str, Any]: + """Try to determine the version from _version.py if present.""" + try: + with open(filename) as f: + contents = f.read() + except OSError: + raise NotThisMethod("unable to read _version.py") + mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", + contents, re.M | re.S) + if not mo: + raise NotThisMethod("no version_json in _version.py") + return json.loads(mo.group(1)) + + +def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None: + """Write the given version number to the given _version.py file.""" + contents = json.dumps(versions, sort_keys=True, + indent=1, separators=(",", ": ")) + with open(filename, "w") as f: + f.write(SHORT_VERSION_PY % contents) + + print("set %s to '%s'" % (filename, versions["version"])) + + +def plus_or_dot(pieces: Dict[str, Any]) -> str: + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces: Dict[str, Any]) -> str: + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). + + Exceptions: + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: + if pieces["distance"]: + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] + else: + # exception #1 + rendered = "0.post0.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces: Dict[str, Any]) -> str: + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces: Dict[str, Any]) -> str: + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None, + "date": pieces.get("date")} + + +class VersioneerBadRootError(Exception): + """The project root directory is unknown or missing key files.""" + + +def get_versions(verbose: bool = False) -> Dict[str, Any]: + """Get the project version from whatever source is available. + + Returns dict with two keys: 'version' and 'full'. + """ + if "versioneer" in sys.modules: + # see the discussion in cmdclass.py:get_cmdclass() + del sys.modules["versioneer"] + + root = get_root() + cfg = get_config_from_root(root) + + assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" + handlers = HANDLERS.get(cfg.VCS) + assert handlers, "unrecognized VCS '%s'" % cfg.VCS + verbose = verbose or bool(cfg.verbose) # `bool()` used to avoid `None` + assert cfg.versionfile_source is not None, \ + "please set versioneer.versionfile_source" + assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" + + versionfile_abs = os.path.join(root, cfg.versionfile_source) + + # extract version from first of: _version.py, VCS command (e.g. 'git + # describe'), parentdir. This is meant to work for developers using a + # source checkout, for users of a tarball created by 'setup.py sdist', + # and for users of a tarball/zipball created by 'git archive' or github's + # download-from-tag feature or the equivalent in other VCSes. + + get_keywords_f = handlers.get("get_keywords") + from_keywords_f = handlers.get("keywords") + if get_keywords_f and from_keywords_f: + try: + keywords = get_keywords_f(versionfile_abs) + ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) + if verbose: + print("got version from expanded keyword %s" % ver) + return ver + except NotThisMethod: + pass + + try: + ver = versions_from_file(versionfile_abs) + if verbose: + print("got version from file %s %s" % (versionfile_abs, ver)) + return ver + except NotThisMethod: + pass + + from_vcs_f = handlers.get("pieces_from_vcs") + if from_vcs_f: + try: + pieces = from_vcs_f(cfg.tag_prefix, root, verbose) + ver = render(pieces, cfg.style) + if verbose: + print("got version from VCS %s" % ver) + return ver + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + if verbose: + print("got version from parentdir %s" % ver) + return ver + except NotThisMethod: + pass + + if verbose: + print("unable to compute version") + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, "error": "unable to compute version", + "date": None} + + +def get_version() -> str: + """Get the short version string for this project.""" + return get_versions()["version"] + + +def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None): + """Get the custom setuptools subclasses used by Versioneer. + + If the package uses a different cmdclass (e.g. one from numpy), it + should be provide as an argument. + """ + if "versioneer" in sys.modules: + del sys.modules["versioneer"] + # this fixes the "python setup.py develop" case (also 'install' and + # 'easy_install .'), in which subdependencies of the main project are + # built (using setup.py bdist_egg) in the same python process. Assume + # a main project A and a dependency B, which use different versions + # of Versioneer. A's setup.py imports A's Versioneer, leaving it in + # sys.modules by the time B's setup.py is executed, causing B to run + # with the wrong versioneer. Setuptools wraps the sub-dep builds in a + # sandbox that restores sys.modules to it's pre-build state, so the + # parent is protected against the child's "import versioneer". By + # removing ourselves from sys.modules here, before the child build + # happens, we protect the child from the parent's versioneer too. + # Also see https://github.com/python-versioneer/python-versioneer/issues/52 + + cmds = {} if cmdclass is None else cmdclass.copy() + + # we add "version" to setuptools + from setuptools import Command + + class cmd_version(Command): + description = "report generated version string" + user_options: List[Tuple[str, str, str]] = [] + boolean_options: List[str] = [] + + def initialize_options(self) -> None: + pass + + def finalize_options(self) -> None: + pass + + def run(self) -> None: + vers = get_versions(verbose=True) + print("Version: %s" % vers["version"]) + print(" full-revisionid: %s" % vers.get("full-revisionid")) + print(" dirty: %s" % vers.get("dirty")) + print(" date: %s" % vers.get("date")) + if vers["error"]: + print(" error: %s" % vers["error"]) + cmds["version"] = cmd_version + + # we override "build_py" in setuptools + # + # most invocation pathways end up running build_py: + # distutils/build -> build_py + # distutils/install -> distutils/build ->.. + # setuptools/bdist_wheel -> distutils/install ->.. + # setuptools/bdist_egg -> distutils/install_lib -> build_py + # setuptools/install -> bdist_egg ->.. + # setuptools/develop -> ? + # pip install: + # copies source tree to a tempdir before running egg_info/etc + # if .git isn't copied too, 'git describe' will fail + # then does setup.py bdist_wheel, or sometimes setup.py install + # setup.py egg_info -> ? + + # pip install -e . and setuptool/editable_wheel will invoke build_py + # but the build_py command is not expected to copy any files. + + # we override different "build_py" commands for both environments + if 'build_py' in cmds: + _build_py: Any = cmds['build_py'] + else: + from setuptools.command.build_py import build_py as _build_py + + class cmd_build_py(_build_py): + def run(self) -> None: + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_py.run(self) + if getattr(self, "editable_mode", False): + # During editable installs `.py` and data files are + # not copied to build_lib + return + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if cfg.versionfile_build: + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_py"] = cmd_build_py + + if 'build_ext' in cmds: + _build_ext: Any = cmds['build_ext'] + else: + from setuptools.command.build_ext import build_ext as _build_ext + + class cmd_build_ext(_build_ext): + def run(self) -> None: + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_ext.run(self) + if self.inplace: + # build_ext --inplace will only build extensions in + # build/lib<..> dir with no _version.py to write to. + # As in place builds will already have a _version.py + # in the module dir, we do not need to write one. + return + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if not cfg.versionfile_build: + return + target_versionfile = os.path.join(self.build_lib, + cfg.versionfile_build) + if not os.path.exists(target_versionfile): + print(f"Warning: {target_versionfile} does not exist, skipping " + "version update. This can happen if you are running build_ext " + "without first running build_py.") + return + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + cmds["build_ext"] = cmd_build_ext + + if "cx_Freeze" in sys.modules: # cx_freeze enabled? + from cx_Freeze.dist import build_exe as _build_exe # type: ignore + # nczeczulin reports that py2exe won't like the pep440-style string + # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. + # setup(console=[{ + # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION + # "product_version": versioneer.get_version(), + # ... + + class cmd_build_exe(_build_exe): + def run(self) -> None: + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _build_exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["build_exe"] = cmd_build_exe + del cmds["build_py"] + + if 'py2exe' in sys.modules: # py2exe enabled? + try: + from py2exe.setuptools_buildexe import py2exe as _py2exe # type: ignore + except ImportError: + from py2exe.distutils_buildexe import py2exe as _py2exe # type: ignore + + class cmd_py2exe(_py2exe): + def run(self) -> None: + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + target_versionfile = cfg.versionfile_source + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + _py2exe.run(self) + os.unlink(target_versionfile) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % + {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + cmds["py2exe"] = cmd_py2exe + + # sdist farms its file list building out to egg_info + if 'egg_info' in cmds: + _egg_info: Any = cmds['egg_info'] + else: + from setuptools.command.egg_info import egg_info as _egg_info + + class cmd_egg_info(_egg_info): + def find_sources(self) -> None: + # egg_info.find_sources builds the manifest list and writes it + # in one shot + super().find_sources() + + # Modify the filelist and normalize it + root = get_root() + cfg = get_config_from_root(root) + self.filelist.append('versioneer.py') + if cfg.versionfile_source: + # There are rare cases where versionfile_source might not be + # included by default, so we must be explicit + self.filelist.append(cfg.versionfile_source) + self.filelist.sort() + self.filelist.remove_duplicates() + + # The write method is hidden in the manifest_maker instance that + # generated the filelist and was thrown away + # We will instead replicate their final normalization (to unicode, + # and POSIX-style paths) + from setuptools import unicode_utils + normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') + for f in self.filelist.files] + + manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') + with open(manifest_filename, 'w') as fobj: + fobj.write('\n'.join(normalized)) + + cmds['egg_info'] = cmd_egg_info + + # we override different "sdist" commands for both environments + if 'sdist' in cmds: + _sdist: Any = cmds['sdist'] + else: + from setuptools.command.sdist import sdist as _sdist + + class cmd_sdist(_sdist): + def run(self) -> None: + versions = get_versions() + self._versioneer_generated_versions = versions + # unless we update this, the command will keep using the old + # version + self.distribution.metadata.version = versions["version"] + return _sdist.run(self) + + def make_release_tree(self, base_dir: str, files: List[str]) -> None: + root = get_root() + cfg = get_config_from_root(root) + _sdist.make_release_tree(self, base_dir, files) + # now locate _version.py in the new base_dir directory + # (remembering that it may be a hardlink) and replace it with an + # updated value + target_versionfile = os.path.join(base_dir, cfg.versionfile_source) + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, + self._versioneer_generated_versions) + cmds["sdist"] = cmd_sdist + + return cmds + + +CONFIG_ERROR = """ +setup.cfg is missing the necessary Versioneer configuration. You need +a section like: + + [versioneer] + VCS = git + style = pep440 + versionfile_source = src/myproject/_version.py + versionfile_build = myproject/_version.py + tag_prefix = + parentdir_prefix = myproject- + +You will also need to edit your setup.py to use the results: + + import versioneer + setup(version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), ...) + +Please read the docstring in ./versioneer.py for configuration instructions, +edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. +""" + +SAMPLE_CONFIG = """ +# See the docstring in versioneer.py for instructions. Note that you must +# re-run 'versioneer.py setup' after changing this section, and commit the +# resulting files. + +[versioneer] +#VCS = git +#style = pep440 +#versionfile_source = +#versionfile_build = +#tag_prefix = +#parentdir_prefix = + +""" + +OLD_SNIPPET = """ +from ._version import get_versions +__version__ = get_versions()['version'] +del get_versions +""" + +INIT_PY_SNIPPET = """ +from . import {0} +__version__ = {0}.get_versions()['version'] +""" + + +def do_setup() -> int: + """Do main VCS-independent setup function for installing Versioneer.""" + root = get_root() + try: + cfg = get_config_from_root(root) + except (OSError, configparser.NoSectionError, + configparser.NoOptionError) as e: + if isinstance(e, (OSError, configparser.NoSectionError)): + print("Adding sample versioneer config to setup.cfg", + file=sys.stderr) + with open(os.path.join(root, "setup.cfg"), "a") as f: + f.write(SAMPLE_CONFIG) + print(CONFIG_ERROR, file=sys.stderr) + return 1 + + print(" creating %s" % cfg.versionfile_source) + with open(cfg.versionfile_source, "w") as f: + LONG = LONG_VERSION_PY[cfg.VCS] + f.write(LONG % {"DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + }) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), + "__init__.py") + maybe_ipy: Optional[str] = ipy + if os.path.exists(ipy): + try: + with open(ipy, "r") as f: + old = f.read() + except OSError: + old = "" + module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] + snippet = INIT_PY_SNIPPET.format(module) + if OLD_SNIPPET in old: + print(" replacing boilerplate in %s" % ipy) + with open(ipy, "w") as f: + f.write(old.replace(OLD_SNIPPET, snippet)) + elif snippet not in old: + print(" appending to %s" % ipy) + with open(ipy, "a") as f: + f.write(snippet) + else: + print(" %s unmodified" % ipy) + else: + print(" %s doesn't exist, ok" % ipy) + maybe_ipy = None + + # Make VCS-specific changes. For git, this means creating/changing + # .gitattributes to mark _version.py for export-subst keyword + # substitution. + do_vcs_install(cfg.versionfile_source, maybe_ipy) + return 0 + + +def scan_setup_py() -> int: + """Validate the contents of setup.py against Versioneer's expectations.""" + found = set() + setters = False + errors = 0 + with open("setup.py", "r") as f: + for line in f.readlines(): + if "import versioneer" in line: + found.add("import") + if "versioneer.get_cmdclass()" in line: + found.add("cmdclass") + if "versioneer.get_version()" in line: + found.add("get_version") + if "versioneer.VCS" in line: + setters = True + if "versioneer.versionfile_source" in line: + setters = True + if len(found) != 3: + print("") + print("Your setup.py appears to be missing some important items") + print("(but I might be wrong). Please make sure it has something") + print("roughly like the following:") + print("") + print(" import versioneer") + print(" setup( version=versioneer.get_version(),") + print(" cmdclass=versioneer.get_cmdclass(), ...)") + print("") + errors += 1 + if setters: + print("You should remove lines like 'versioneer.VCS = ' and") + print("'versioneer.versionfile_source = ' . This configuration") + print("now lives in setup.cfg, and should be removed from setup.py") + print("") + errors += 1 + return errors + + +def setup_command() -> NoReturn: + """Set up Versioneer and exit with appropriate error code.""" + errors = do_setup() + errors += scan_setup_py() + sys.exit(1 if errors else 0) + + +if __name__ == "__main__": + cmd = sys.argv[1] + if cmd == "setup": + setup_command()