python-libei 0.8.0__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- libei/__init__.py +1 -1
- libei/_capi/libei.py +7 -1
- libei/ei.py +5 -3
- libei/eis.py +21 -5
- libei/portal.py +145 -19
- {python_libei-0.8.0.dist-info → python_libei-0.8.1.dist-info}/METADATA +17 -7
- python_libei-0.8.1.dist-info/RECORD +17 -0
- python_libei-0.8.0.dist-info/RECORD +0 -17
- {python_libei-0.8.0.dist-info → python_libei-0.8.1.dist-info}/WHEEL +0 -0
- {python_libei-0.8.0.dist-info → python_libei-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {python_libei-0.8.0.dist-info → python_libei-0.8.1.dist-info}/top_level.txt +0 -0
libei/__init__.py
CHANGED
libei/_capi/libei.py
CHANGED
|
@@ -219,7 +219,13 @@ region_get_mapping_id = lib.function( # libei 1.1+
|
|
|
219
219
|
"ei_region_get_mapping_id", (c_void_p,), c_char_p
|
|
220
220
|
)
|
|
221
221
|
# x and y are in/out parameters, overwritten only when the point is inside.
|
|
222
|
-
|
|
222
|
+
# No version marker: this one is in libei 1.0.0's own public header and
|
|
223
|
+
# implemented there, so every libei at or above the declared floor exports
|
|
224
|
+
# it. It carried `# libei 1.1+` alongside its genuine 1.1 neighbours
|
|
225
|
+
# `ei_region_get_mapping_id` and `ei_device_region_at`, and the weekly
|
|
226
|
+
# floor job caught it -- building 1.0.0 from source found the symbol
|
|
227
|
+
# present where the marker promised it would be absent.
|
|
228
|
+
region_convert_point = lib.function(
|
|
223
229
|
"ei_region_convert_point",
|
|
224
230
|
(c_void_p, POINTER(c_double), POINTER(c_double)),
|
|
225
231
|
c_bool,
|
libei/ei.py
CHANGED
|
@@ -372,8 +372,7 @@ class Region(CObject):
|
|
|
372
372
|
|
|
373
373
|
Returns the point with the region's offset subtracted, or ``None``
|
|
374
374
|
if it falls outside the region -- which is also how you test
|
|
375
|
-
membership without a second :meth:`contains` call.
|
|
376
|
-
libei 1.1.
|
|
375
|
+
membership without a second :meth:`contains` call.
|
|
377
376
|
"""
|
|
378
377
|
# x/y are in-out parameters: libei overwrites them only when the
|
|
379
378
|
# point is inside, so the return value has to gate reading them.
|
|
@@ -1127,7 +1126,10 @@ class Context(CObject):
|
|
|
1127
1126
|
pointer = _capi.libei.get_event(self)
|
|
1128
1127
|
if not pointer:
|
|
1129
1128
|
break
|
|
1130
|
-
|
|
1129
|
+
# adopt(), not wrap(): ei_get_event() transfers ownership of
|
|
1130
|
+
# the returned reference to the caller, and the unref in
|
|
1131
|
+
# release() below is the one that balances it.
|
|
1132
|
+
event = Event.adopt(pointer)
|
|
1131
1133
|
assert event is not None
|
|
1132
1134
|
# try/finally, not a bare call after yield: breaking out of a
|
|
1133
1135
|
# `for event in ctx.events:` loop (or an exception propagating
|
libei/eis.py
CHANGED
|
@@ -944,7 +944,18 @@ class Event(CObject):
|
|
|
944
944
|
|
|
945
945
|
@property
|
|
946
946
|
def emulating_sequence(self) -> int:
|
|
947
|
-
"""Sequence number of the start_emulating transaction.
|
|
947
|
+
"""Sequence number of the start_emulating transaction.
|
|
948
|
+
|
|
949
|
+
Reads 0 on libei older than 1.6, whatever the client sent. libeis
|
|
950
|
+
queued the event without ever copying the sequence into it, so the
|
|
951
|
+
getter returned a zeroed struct field; upstream added the missing
|
|
952
|
+
assignment in libei 1.6.0. Every version from this package's 1.0.0
|
|
953
|
+
floor up exports the symbol, so there is nothing to raise and no
|
|
954
|
+
way to tell an old library apart from a client that genuinely sent
|
|
955
|
+
0 -- treat a 0 here as "unknown" unless you know both ends are on
|
|
956
|
+
1.6. :attr:`libei.ei.Event.emulating_sequence`, the receiver side
|
|
957
|
+
of the same transaction, is unaffected: libei has always stored it.
|
|
958
|
+
"""
|
|
948
959
|
self._require("emulating_sequence", EventType.DEVICE_START_EMULATING)
|
|
949
960
|
return _capi.libeis.event_emulating_get_sequence(self)
|
|
950
961
|
|
|
@@ -1150,7 +1161,10 @@ class Eis(CObject):
|
|
|
1150
1161
|
pointer = _capi.libeis.get_event(self)
|
|
1151
1162
|
if not pointer:
|
|
1152
1163
|
break
|
|
1153
|
-
|
|
1164
|
+
# adopt(), not wrap(): eis_get_event() transfers ownership of
|
|
1165
|
+
# the returned reference to the caller, and the unref in
|
|
1166
|
+
# release() below is the one that balances it.
|
|
1167
|
+
event = Event.adopt(pointer)
|
|
1154
1168
|
assert event is not None
|
|
1155
1169
|
# See ei.Context.events: try/finally so release() still runs
|
|
1156
1170
|
# if the caller breaks out of the loop (GeneratorExit at the
|
|
@@ -1168,9 +1182,11 @@ class Eis(CObject):
|
|
|
1168
1182
|
def set_flag(self, flag: Flag) -> None:
|
|
1169
1183
|
"""Change this context's protocol behavior. Requires libei 1.6.
|
|
1170
1184
|
|
|
1171
|
-
Must be called before the backend is set up, so
|
|
1172
|
-
|
|
1173
|
-
|
|
1185
|
+
Must be called before the backend is set up, so the supported
|
|
1186
|
+
way to use it is the ``flags`` parameter of
|
|
1187
|
+
:meth:`create_for_fd` / :meth:`create_for_socket`, which apply
|
|
1188
|
+
each flag at exactly that point; calling it directly only works
|
|
1189
|
+
on a context built by hand, before its backend is set up.
|
|
1174
1190
|
Takes one flag, never a bitmask; call it again for another.
|
|
1175
1191
|
"""
|
|
1176
1192
|
result = _capi.libeis.set_flag(self, flag)
|
libei/portal.py
CHANGED
|
@@ -207,6 +207,14 @@ class PortalProtocolError(PortalError):
|
|
|
207
207
|
default message names the expected shape), a phrase for a value fault
|
|
208
208
|
(naming the field). ``payload`` is whatever best shows the fault -- the
|
|
209
209
|
whole signal tuple for a shape fault, the options dict for a value one.
|
|
210
|
+
|
|
211
|
+
:func:`_request` raises it the same way for a ``Request``'s
|
|
212
|
+
``Response`` reply that does not match its own ``(u response, a{sv}
|
|
213
|
+
results)`` shape, and :meth:`InputCaptureSession.zones` for a result
|
|
214
|
+
value that is not the type the spec gives it -- always with a
|
|
215
|
+
``detail`` phrase, since the default shape message above is specific
|
|
216
|
+
to the session signals. ``step`` is then the method name, matching the
|
|
217
|
+
sibling errors raised from the same call.
|
|
210
218
|
"""
|
|
211
219
|
|
|
212
220
|
def __init__(self, step: str, payload: object, detail: str | None = None) -> None:
|
|
@@ -457,6 +465,18 @@ def _request(
|
|
|
457
465
|
for 60 seconds means the answer arrives inside 60 seconds, not inside
|
|
458
466
|
however many 60-second waits the sequence happens to be built from.
|
|
459
467
|
|
|
468
|
+
A ``Response`` whose payload does not match the spec's ``(u response,
|
|
469
|
+
a{sv} results)`` shape raises :class:`PortalProtocolError` rather than
|
|
470
|
+
being unpacked on faith -- the same hardening `_wait_for_signal`
|
|
471
|
+
applies to the signals it waits on, and for the same two reasons: a
|
|
472
|
+
wrong-arity payload used to raise inside the GLib callback, where PyGI
|
|
473
|
+
swallows it, degrading the step to a full-timeout
|
|
474
|
+
:class:`PortalTimeoutError`; a non-dict ``results`` instead slipped
|
|
475
|
+
through to the caller, whose ``.get()`` crashed with a raw
|
|
476
|
+
``AttributeError`` past ``except PortalError``. The guarantee callers
|
|
477
|
+
rely on is therefore structural: what this returns is always an
|
|
478
|
+
``int`` code and a ``dict`` of results.
|
|
479
|
+
|
|
460
480
|
``trailing_args``, appended after ``options`` in the call's parameter
|
|
461
481
|
tuple, exists for ``InputCapture.SetPointerBarriers`` -- the one
|
|
462
482
|
Request-returning method in either portal whose ``options`` is not its
|
|
@@ -474,12 +494,28 @@ def _request(
|
|
|
474
494
|
|
|
475
495
|
loop = GLib.MainLoop()
|
|
476
496
|
result: dict[str, Any] = {}
|
|
497
|
+
malformed: dict[str, Any] = {}
|
|
477
498
|
subscriptions: list[Any] = []
|
|
478
499
|
# Set inside the timeout callback rather than inferred from an empty
|
|
479
500
|
# `result` afterwards: a Response carrying no results is legitimate
|
|
480
501
|
# (SelectDevices answers with an empty dict), so "did the loop end
|
|
481
502
|
# because it timed out" has to be recorded, not deduced.
|
|
482
503
|
timed_out = False
|
|
504
|
+
# Distinct from `timed_out`: whether `on_timeout` ran at all, either
|
|
505
|
+
# path -- the flag the cleanup below keys "already removed?" on. See
|
|
506
|
+
# the matching pair in `_wait_for_signal`.
|
|
507
|
+
timeout_fired = False
|
|
508
|
+
|
|
509
|
+
def _settled() -> bool:
|
|
510
|
+
"""Whether some terminal outcome has already been recorded.
|
|
511
|
+
|
|
512
|
+
Same role as in `_wait_for_signal`: GLib dispatches every source
|
|
513
|
+
ready in one main-context iteration regardless of an earlier
|
|
514
|
+
``loop.quit()``, so a Response and the deadline can both fire in
|
|
515
|
+
the same poll -- the first recorded outcome wins, and a delivered
|
|
516
|
+
reply is never dropped for a timeout (or vice versa).
|
|
517
|
+
"""
|
|
518
|
+
return bool(result) or bool(malformed) or timed_out
|
|
483
519
|
|
|
484
520
|
def on_response(
|
|
485
521
|
_conn: Any,
|
|
@@ -491,14 +527,42 @@ def _request(
|
|
|
491
527
|
*_a: Any,
|
|
492
528
|
) -> None:
|
|
493
529
|
"""Record the first reply and quit the loop; later replies are ignored."""
|
|
494
|
-
if
|
|
530
|
+
if _settled(): # both subscriptions may fire; the first reply wins
|
|
495
531
|
return
|
|
496
|
-
|
|
532
|
+
args = params.unpack()
|
|
533
|
+
# Checked here, raised only after the loop: this runs inside a
|
|
534
|
+
# GLib callback, where PyGI prints an exception to stderr and
|
|
535
|
+
# swallows it -- a bare raise on a malformed Response would leave
|
|
536
|
+
# the loop running until the full timeout, misreported as
|
|
537
|
+
# PortalTimeoutError; a non-dict `results` let through instead
|
|
538
|
+
# crashed the caller's `.get()` with a raw AttributeError past
|
|
539
|
+
# `except PortalError`. Same hardening `_wait_for_signal` applies
|
|
540
|
+
# to the signals it waits on.
|
|
541
|
+
if (
|
|
542
|
+
not isinstance(args, tuple)
|
|
543
|
+
or len(args) != 2
|
|
544
|
+
or isinstance(args[0], bool)
|
|
545
|
+
or not isinstance(args[0], int)
|
|
546
|
+
or not isinstance(args[1], dict)
|
|
547
|
+
):
|
|
548
|
+
malformed["args"] = args
|
|
549
|
+
loop.quit()
|
|
550
|
+
return
|
|
551
|
+
result["code"], result["results"] = args
|
|
497
552
|
loop.quit()
|
|
498
553
|
|
|
499
554
|
def on_timeout() -> bool:
|
|
500
555
|
"""Stop the loop and record that it timed out rather than finished."""
|
|
501
|
-
nonlocal timed_out
|
|
556
|
+
nonlocal timed_out, timeout_fired
|
|
557
|
+
# Set first, before the early return: GLib removes the source the
|
|
558
|
+
# moment this returns False (which both paths do), so the finally
|
|
559
|
+
# block keys "already removed" on this, not on `timed_out` -- see
|
|
560
|
+
# the same dance in `_wait_for_signal`.
|
|
561
|
+
timeout_fired = True
|
|
562
|
+
if _settled():
|
|
563
|
+
# A Response delivered in the same poll as the deadline has
|
|
564
|
+
# already recorded its outcome; do not overwrite it.
|
|
565
|
+
return False
|
|
502
566
|
timed_out = True
|
|
503
567
|
loop.quit()
|
|
504
568
|
return False # one-shot; GLib removes the source when this is False
|
|
@@ -542,12 +606,14 @@ def _request(
|
|
|
542
606
|
handle = _returned_handle(reply)
|
|
543
607
|
if handle is not None and handle != expected_path:
|
|
544
608
|
subscribe(handle)
|
|
545
|
-
# `if not
|
|
609
|
+
# `if not _settled()` because a synchronous answer (a fast
|
|
546
610
|
# non-interactive Response, or a test double) can arrive during the
|
|
547
611
|
# call above, before run() is reached -- and quit() on a loop that
|
|
548
612
|
# is not running yet does not stop the later run(), so running it
|
|
549
|
-
# then would block with the reply already delivered.
|
|
550
|
-
|
|
613
|
+
# then would block with the reply already delivered. A malformed
|
|
614
|
+
# synchronous reply counts: it too has already quit a loop that
|
|
615
|
+
# was not running yet.
|
|
616
|
+
if not _settled():
|
|
551
617
|
timeout_source = GLib.timeout_add(_msec_until(deadline), on_timeout)
|
|
552
618
|
try:
|
|
553
619
|
loop.run()
|
|
@@ -559,12 +625,27 @@ def _request(
|
|
|
559
625
|
# fixed for _wait_for_signal below. Only remove it when the
|
|
560
626
|
# *Response* woke the loop instead; leaving a live source in
|
|
561
627
|
# that case holds a reference to this closure and fires it
|
|
562
|
-
# into a dead loop on some later request.
|
|
563
|
-
|
|
628
|
+
# into a dead loop on some later request. Keyed on
|
|
629
|
+
# `timeout_fired` (did on_timeout run), not `timed_out` (did
|
|
630
|
+
# the wait time out): a Response landing in the same poll as
|
|
631
|
+
# the deadline runs on_timeout down its `_settled` early
|
|
632
|
+
# return, which removes the source yet leaves `timed_out`
|
|
633
|
+
# False.
|
|
634
|
+
if not timeout_fired:
|
|
564
635
|
GLib.source_remove(timeout_source)
|
|
565
636
|
finally:
|
|
566
637
|
for subscription in subscriptions:
|
|
567
638
|
connection.signal_unsubscribe(subscription)
|
|
639
|
+
# Mirrors `_signal_outcome`: only one of these is ever set (every
|
|
640
|
+
# callback checks `_settled()` first), so the order here only decides
|
|
641
|
+
# between genuinely simultaneous dispatches.
|
|
642
|
+
if malformed:
|
|
643
|
+
raise PortalProtocolError(
|
|
644
|
+
method,
|
|
645
|
+
malformed["args"],
|
|
646
|
+
"the Request's Response signal is specified as "
|
|
647
|
+
"(uint response, results dict)",
|
|
648
|
+
)
|
|
568
649
|
if timed_out:
|
|
569
650
|
raise PortalTimeoutError(method, timeout)
|
|
570
651
|
return result["code"], result["results"]
|
|
@@ -1100,6 +1181,23 @@ def _classify_signal_payload(
|
|
|
1100
1181
|
return "take"
|
|
1101
1182
|
|
|
1102
1183
|
|
|
1184
|
+
def _spec_int(value: Any) -> int:
|
|
1185
|
+
"""``int(value)`` for a spec ``u``/``i``, refusing a silent truncation.
|
|
1186
|
+
|
|
1187
|
+
A portal integer field arrives as a Python ``int``. A malformed portal
|
|
1188
|
+
that put a ``d`` (double) there would still reach ``int()``, and
|
|
1189
|
+
``int(0.9) == 0`` / ``int(1.9) == 1`` truncates it *silently* -- a zone
|
|
1190
|
+
coordinate or a refused barrier id quietly becomes a different,
|
|
1191
|
+
valid-looking value rather than an error. Reject any ``float``
|
|
1192
|
+
(``inf``/``nan`` included) with the ``ValueError`` the surrounding
|
|
1193
|
+
guards already turn into a :class:`PortalProtocolError`, so an off-spec
|
|
1194
|
+
fractional value is refused on the same footing as a non-numeric one.
|
|
1195
|
+
"""
|
|
1196
|
+
if isinstance(value, float):
|
|
1197
|
+
raise ValueError("a fractional value where the spec gives an integer")
|
|
1198
|
+
return int(value)
|
|
1199
|
+
|
|
1200
|
+
|
|
1103
1201
|
def _deactivation_id(options: dict[str, Any], signal: str) -> int:
|
|
1104
1202
|
"""The ``activation_id`` from Deactivated's options, as the spec's ``u``.
|
|
1105
1203
|
|
|
@@ -1107,13 +1205,12 @@ def _deactivation_id(options: dict[str, Any], signal: str) -> int:
|
|
|
1107
1205
|
dict -- but not the types of the values in it. ``activation_id`` is a
|
|
1108
1206
|
``u``, and a non-conforming portal that put something else there would
|
|
1109
1207
|
crash the caller with ``ValueError``/``TypeError``/``OverflowError``
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
catches one error, not two.
|
|
1208
|
+
past the shape check. Converting it here turns that into the same typed
|
|
1209
|
+
:class:`PortalProtocolError` the shape check raises, so a caller that
|
|
1210
|
+
guards against malformed signals catches one error, not two.
|
|
1114
1211
|
"""
|
|
1115
1212
|
try:
|
|
1116
|
-
return
|
|
1213
|
+
return _spec_int(options.get("activation_id", 0))
|
|
1117
1214
|
except (ValueError, TypeError, OverflowError) as exc:
|
|
1118
1215
|
raise PortalProtocolError(
|
|
1119
1216
|
signal, options, "activation_id is not an integer"
|
|
@@ -1134,7 +1231,7 @@ def _activation_from_options(options: dict[str, Any], signal: str) -> Activation
|
|
|
1134
1231
|
fourth hand-rolled check nobody remembers to write.
|
|
1135
1232
|
"""
|
|
1136
1233
|
try:
|
|
1137
|
-
activation_id =
|
|
1234
|
+
activation_id = _spec_int(options.get("activation_id", 0))
|
|
1138
1235
|
cursor = options.get("cursor_position")
|
|
1139
1236
|
if cursor is None:
|
|
1140
1237
|
cursor_position = None
|
|
@@ -1142,7 +1239,7 @@ def _activation_from_options(options: dict[str, Any], signal: str) -> Activation
|
|
|
1142
1239
|
cx, cy = cursor # (dd): exactly two, or this unpacking raises
|
|
1143
1240
|
cursor_position = (float(cx), float(cy))
|
|
1144
1241
|
barrier = options.get("barrier_id")
|
|
1145
|
-
barrier_id = None if barrier is None else
|
|
1242
|
+
barrier_id = None if barrier is None else _spec_int(barrier)
|
|
1146
1243
|
except (ValueError, TypeError, OverflowError) as exc:
|
|
1147
1244
|
raise PortalProtocolError(
|
|
1148
1245
|
signal, options, "an option value is not the type the spec gives it"
|
|
@@ -1608,9 +1705,26 @@ class InputCaptureSession:
|
|
|
1608
1705
|
)
|
|
1609
1706
|
if code != 0:
|
|
1610
1707
|
raise PortalDeniedError("GetZones")
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1708
|
+
# `results` is a dict by `_request`'s own guarantee; the *values*
|
|
1709
|
+
# are still the portal's to malform. The spec types them --
|
|
1710
|
+
# `zone_set` is a `u`, `zones` an `a(uuii)` -- so convert them
|
|
1711
|
+
# under one guard, the same way `_activation_from_options` covers
|
|
1712
|
+
# the Activated signal's values: a non-conforming value becomes
|
|
1713
|
+
# the typed PortalProtocolError rather than a raw
|
|
1714
|
+
# TypeError/ValueError past `except PortalError` in the caller.
|
|
1715
|
+
try:
|
|
1716
|
+
zone_set = _spec_int(results.get("zone_set", 0))
|
|
1717
|
+
zones = [
|
|
1718
|
+
(_spec_int(w), _spec_int(h), _spec_int(x), _spec_int(y))
|
|
1719
|
+
for w, h, x, y in results.get("zones", [])
|
|
1720
|
+
]
|
|
1721
|
+
except (ValueError, TypeError, OverflowError) as exc:
|
|
1722
|
+
raise PortalProtocolError(
|
|
1723
|
+
"GetZones",
|
|
1724
|
+
results,
|
|
1725
|
+
"a result value is not the type the spec gives it",
|
|
1726
|
+
) from exc
|
|
1727
|
+
return zone_set, zones
|
|
1614
1728
|
|
|
1615
1729
|
def set_pointer_barriers(
|
|
1616
1730
|
self,
|
|
@@ -1659,7 +1773,19 @@ class InputCaptureSession:
|
|
|
1659
1773
|
)
|
|
1660
1774
|
if code != 0:
|
|
1661
1775
|
raise PortalDeniedError("SetPointerBarriers")
|
|
1662
|
-
|
|
1776
|
+
# `results` is a dict by `_request`'s own guarantee; the *value* at
|
|
1777
|
+
# `failed_barriers` (an `au` per the spec) is not. A non-iterable
|
|
1778
|
+
# there would crash the caller with a raw TypeError past
|
|
1779
|
+
# `except PortalError` -- the same value-level hole `zones()` guards
|
|
1780
|
+
# -- so a malformed one becomes the typed PortalProtocolError.
|
|
1781
|
+
try:
|
|
1782
|
+
failed = [_spec_int(b) for b in results.get("failed_barriers", [])]
|
|
1783
|
+
except (ValueError, TypeError, OverflowError) as exc:
|
|
1784
|
+
raise PortalProtocolError(
|
|
1785
|
+
"SetPointerBarriers",
|
|
1786
|
+
results,
|
|
1787
|
+
"a result value is not the type the spec gives it",
|
|
1788
|
+
) from exc
|
|
1663
1789
|
# A partially-refused set is the quiet failure here: the call
|
|
1664
1790
|
# succeeds, some barriers stand, and an edge the caller believes is
|
|
1665
1791
|
# armed simply never triggers. Callers see only the returned list,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: Inject, receive and capture input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis, plus the RemoteDesktop and InputCapture portals
|
|
5
5
|
Author: Dennis K. Paulsen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -175,7 +175,7 @@ and why, is in
|
|
|
175
175
|
|
|
176
176
|
## Status
|
|
177
177
|
|
|
178
|
-
Beta (`0.8.
|
|
178
|
+
Beta (`0.8.1`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
179
179
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
180
180
|
|
|
181
181
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -228,17 +228,27 @@ Exactly what was run, when, and against which versions:
|
|
|
228
228
|
|
|
229
229
|
| Needs | For |
|
|
230
230
|
| --- | --- |
|
|
231
|
-
| 1.1 | `Region.mapping_id`, `
|
|
231
|
+
| 1.1 | `Region.mapping_id`, `Device.region_at()` |
|
|
232
232
|
| 1.4 | ping/pong round trips (`Context.new_ping()`), `Context.disconnect()`, touch cancellation (`Touch.cancel()`) |
|
|
233
233
|
| 1.5 | `eis.Client.pid` |
|
|
234
234
|
| 1.6 | `Device.text_utf8()` / `text_keysym()` and TEXT events, `Seat.request_device()`, `Eis.set_flag()` |
|
|
235
235
|
|
|
236
236
|
Nothing is resolved until it is called, so a build without one of these
|
|
237
237
|
costs you only that call — it raises `LibraryNotFoundError` naming the
|
|
238
|
-
missing symbol, while the rest of the package keeps working.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
missing symbol, while the rest of the package keeps working. Two
|
|
239
|
+
accessors degrade instead of raising, because every supported version
|
|
240
|
+
exports the symbol and only the answer changed — so there is nothing to
|
|
241
|
+
resolve and nothing to report:
|
|
242
|
+
|
|
243
|
+
- `Event.touch_up_event` reports `is_cancel=False` on libei older than
|
|
244
|
+
1.4, since a library with no notion of cancellation genuinely never
|
|
245
|
+
sends one.
|
|
246
|
+
- `eis.Event.emulating_sequence` reads 0 on libei older than 1.6
|
|
247
|
+
whatever the client sent: libeis queued the start_emulating event
|
|
248
|
+
without ever copying the sequence into it, until 1.6.0 added the
|
|
249
|
+
missing assignment. A 0 on an older build is indistinguishable from a
|
|
250
|
+
client that really sent 0. The client-side
|
|
251
|
+
`ei.Event.emulating_sequence` is unaffected.
|
|
242
252
|
|
|
243
253
|
These versions were established by building libei 1.2.1 and diffing its
|
|
244
254
|
exported symbols against every binding here, not by reading `@since`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
libei/__init__.py,sha256=7fJVf_RPxcEEeOM2YTQHtJPP3ilPhFREQKNmCoJqOL4,1858
|
|
2
|
+
libei/_cobject.py,sha256=XjbUNJpZ87jAyPWh6dmRslk6cMy831oMRFhWmfjLDFo,13344
|
|
3
|
+
libei/ei.py,sha256=3oaZ4dxo-ytBHtES3_1_LUca2ZbR8r3SwA50Rj0hGxo,49541
|
|
4
|
+
libei/eis.py,sha256=kcFi6vfw9F9uBt6Bz-blGD-0M6mLEMTYDCgC_SKK7vU,46843
|
|
5
|
+
libei/oeffis.py,sha256=gJHR90s9VhQrgkjWUMDx-lxZ8qKxkttnqbx_xo-7oxs,10035
|
|
6
|
+
libei/portal.py,sha256=95f3I5q8BF2ehGP6LEEMzRKTisrwBxkf20rY1MvD1ic,88168
|
|
7
|
+
libei/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
libei/_capi/__init__.py,sha256=raISSWSV9hAcE92DcLCz-vUb1IPcsmxRZaS-iAoQJBE,211
|
|
9
|
+
libei/_capi/libei.py,sha256=AQgVZ2ZsHH8rywwcmq1EJ-IauuNHbBMhwauMelX83yE,11754
|
|
10
|
+
libei/_capi/libeis.py,sha256=C_Uh7DwNncFC8A4wuMhviAj5C_TUmKHtLJYq0Qhhz1E,13142
|
|
11
|
+
libei/_capi/liboeffis.py,sha256=V8jdmJm3qOQUzzNiCAXt4Jr2JY11Y4-w6NIfIxbxGI4,1218
|
|
12
|
+
libei/_capi/loader.py,sha256=xW67uk35ckgeFaheDGrHC05ZM5w_cxBGvX5lG_GUSu8,6625
|
|
13
|
+
python_libei-0.8.1.dist-info/licenses/LICENSE,sha256=l6xbMU6Y-JZDzmBciBj-J5t6h6jNg_Bcyp4bHxDepqU,1074
|
|
14
|
+
python_libei-0.8.1.dist-info/METADATA,sha256=gGc0OayDeot7yFnA2wmURo_xZjjr1UoQxpK7mdynrWk,23364
|
|
15
|
+
python_libei-0.8.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
16
|
+
python_libei-0.8.1.dist-info/top_level.txt,sha256=_DQXzGjDsUBENI_cNkiOxPB4xi8coCbQS1lq18FMudQ,6
|
|
17
|
+
python_libei-0.8.1.dist-info/RECORD,,
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
libei/__init__.py,sha256=8k-acXmeIea963zVksZW5Ymby8ZYL_aO6B4o7g4_Y9w,1858
|
|
2
|
-
libei/_cobject.py,sha256=XjbUNJpZ87jAyPWh6dmRslk6cMy831oMRFhWmfjLDFo,13344
|
|
3
|
-
libei/ei.py,sha256=W4akKc_BNVfL0FroSXLGD1Z_L3OmYjZT5QDHNnEtMP8,49367
|
|
4
|
-
libei/eis.py,sha256=X_umAaAAbAMC9d2RdnMReeJZI61WsnKz3cySY5YrGr0,45841
|
|
5
|
-
libei/oeffis.py,sha256=gJHR90s9VhQrgkjWUMDx-lxZ8qKxkttnqbx_xo-7oxs,10035
|
|
6
|
-
libei/portal.py,sha256=FdtVdUUKrq2GOcDBtunLCISw7RYO4Mwyk3ci26OH330,81621
|
|
7
|
-
libei/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
libei/_capi/__init__.py,sha256=raISSWSV9hAcE92DcLCz-vUb1IPcsmxRZaS-iAoQJBE,211
|
|
9
|
-
libei/_capi/libei.py,sha256=qsccHa_komCxtvPYi6AobszhFbEG2qA8QYJJVr8SaGM,11356
|
|
10
|
-
libei/_capi/libeis.py,sha256=C_Uh7DwNncFC8A4wuMhviAj5C_TUmKHtLJYq0Qhhz1E,13142
|
|
11
|
-
libei/_capi/liboeffis.py,sha256=V8jdmJm3qOQUzzNiCAXt4Jr2JY11Y4-w6NIfIxbxGI4,1218
|
|
12
|
-
libei/_capi/loader.py,sha256=xW67uk35ckgeFaheDGrHC05ZM5w_cxBGvX5lG_GUSu8,6625
|
|
13
|
-
python_libei-0.8.0.dist-info/licenses/LICENSE,sha256=l6xbMU6Y-JZDzmBciBj-J5t6h6jNg_Bcyp4bHxDepqU,1074
|
|
14
|
-
python_libei-0.8.0.dist-info/METADATA,sha256=SNey10DqR4IKZ_T_XCKmafkR0ioMfxHbdbaSl9IWG-c,22882
|
|
15
|
-
python_libei-0.8.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
16
|
-
python_libei-0.8.0.dist-info/top_level.txt,sha256=_DQXzGjDsUBENI_cNkiOxPB4xi8coCbQS1lq18FMudQ,6
|
|
17
|
-
python_libei-0.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|