python-libei 0.5.2__tar.gz → 0.6.1__tar.gz
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.
- {python_libei-0.5.2/src/python_libei.egg-info → python_libei-0.6.1}/PKG-INFO +58 -28
- python_libei-0.5.2/PKG-INFO → python_libei-0.6.1/README.md +52 -62
- {python_libei-0.5.2 → python_libei-0.6.1}/pyproject.toml +15 -2
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/__init__.py +1 -1
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/_capi/libei.py +5 -3
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/_capi/libeis.py +6 -4
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/_capi/loader.py +41 -1
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/_cobject.py +6 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/ei.py +19 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/eis.py +17 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/oeffis.py +12 -1
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/portal.py +26 -9
- python_libei-0.5.2/README.md → python_libei-0.6.1/src/python_libei.egg-info/PKG-INFO +92 -25
- {python_libei-0.5.2 → python_libei-0.6.1}/src/python_libei.egg-info/SOURCES.txt +4 -0
- python_libei-0.6.1/tests/test_abi.py +496 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_documentation_shape.py +167 -14
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_documented_examples.py +0 -27
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_ei_objects.py +4 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_eis_objects.py +4 -0
- python_libei-0.6.1/tests/test_event_accessors.py +177 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_integration_extras.py +4 -0
- python_libei-0.6.1/tests/test_integration_frame.py +104 -0
- python_libei-0.6.1/tests/test_integration_lifetime.py +49 -0
- python_libei-0.6.1/tests/test_loader.py +198 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_portal.py +14 -0
- python_libei-0.5.2/tests/test_loader.py +0 -81
- {python_libei-0.5.2 → python_libei-0.6.1}/LICENSE +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/setup.cfg +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/_capi/__init__.py +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/_capi/liboeffis.py +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/src/libei/py.typed +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/src/python_libei.egg-info/dependency_links.txt +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/src/python_libei.egg-info/requires.txt +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/src/python_libei.egg-info/top_level.txt +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_cobject.py +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_inputcapture.py +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_integration_socketpair.py +0 -0
- {python_libei-0.5.2 → python_libei-0.6.1}/tests/test_oeffis.py +0 -0
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Inject and
|
|
3
|
+
Version: 0.6.1
|
|
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
|
|
7
7
|
Project-URL: homepage, https://github.com/ctrondlp/python-libei
|
|
8
8
|
Project-URL: repository, https://github.com/ctrondlp/python-libei
|
|
9
9
|
Project-URL: issues, https://github.com/ctrondlp/python-libei/issues
|
|
10
|
+
Project-URL: documentation, https://github.com/ctrondlp/python-libei/tree/main/docs
|
|
11
|
+
Project-URL: pyguitest, https://github.com/ctrondlp/pyguitest
|
|
10
12
|
Project-URL: changelog, https://github.com/ctrondlp/python-libei/blob/main/CHANGELOG.md
|
|
11
|
-
Keywords: wayland,libei,libeis,liboeffis,input,input-emulation,emulated-input,portal,xdg-desktop-portal,remote-desktop,automation,gui-testing,accessibility,ctypes
|
|
13
|
+
Keywords: wayland,libei,libeis,liboeffis,input,input-emulation,emulated-input,input-capture,eis,portal,xdg-desktop-portal,remote-desktop,automation,gui-testing,accessibility,ctypes
|
|
12
14
|
Classifier: Development Status :: 4 - Beta
|
|
13
15
|
Classifier: Intended Audience :: Developers
|
|
14
16
|
Classifier: Operating System :: POSIX :: BSD :: FreeBSD
|
|
15
17
|
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Operating System :: POSIX
|
|
16
19
|
Classifier: Programming Language :: Python :: 3
|
|
17
20
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -37,6 +40,10 @@ Dynamic: license-file
|
|
|
37
40
|
|
|
38
41
|
# python-libei
|
|
39
42
|
|
|
43
|
+
[](https://github.com/ctrondlp/python-libei/actions/workflows/ci.yml)
|
|
44
|
+
[](https://pypi.org/project/python-libei/)
|
|
45
|
+
[](https://github.com/ctrondlp/python-libei/blob/main/LICENSE)
|
|
46
|
+
|
|
40
47
|
Python bindings for [libei, libeis and liboeffis](https://libinput.pages.freedesktop.org/libei/) —
|
|
41
48
|
the Wayland input-emulation libraries. Use this to **move the pointer, click,
|
|
42
49
|
type, or scroll on a Wayland desktop** from Python, the way `xdotool` did on
|
|
@@ -49,8 +56,11 @@ device.start_emulating().pointer_motion(5, 0).frame().stop_emulating()
|
|
|
49
56
|
```
|
|
50
57
|
|
|
51
58
|
**The one concept:** events queue up, and **`frame()` is what sends them** as
|
|
52
|
-
one logical hardware event. Forget it and
|
|
53
|
-
warning, no movement.
|
|
59
|
+
one logical hardware event. Forget it and the motion is usually lost — no
|
|
60
|
+
exception, no warning, no movement. (Ending the chain with `stop_emulating()`
|
|
61
|
+
makes libei frame the queue for you, but it logs an error-level `Bug:` line
|
|
62
|
+
when it does; see [Things that will bite you](#things-that-will-bite-you).) Fill
|
|
63
|
+
the package, then post it.
|
|
54
64
|
|
|
55
65
|
New here? [docs/getting-started.md](docs/getting-started.md) is install
|
|
56
66
|
through a first real pointer motion, in five minutes.
|
|
@@ -92,8 +102,8 @@ than a widget-tree back door:
|
|
|
92
102
|
|
|
93
103
|
## Which API do I need?
|
|
94
104
|
|
|
95
|
-
|
|
96
|
-
to get permission, then `ei` to inject.
|
|
105
|
+
Four modules (`ei`, `eis`, `oeffis`, `portal`), and most callers need exactly
|
|
106
|
+
two of them: `oeffis` or `portal` to get permission, then `ei` to inject.
|
|
97
107
|
|
|
98
108
|
| You want to… | Use | Notes |
|
|
99
109
|
| --- | --- | --- |
|
|
@@ -101,13 +111,18 @@ to get permission, then `ei` to inject.
|
|
|
101
111
|
| **Consume input** from a compositor | `libei.ei` → `Receiver` | Compositor-side or input-capture code; same connection dance |
|
|
102
112
|
| **Get permission**, simply | `libei.oeffis` | One call, pollable fd, no dependencies. The consent dialog appears on **every** run |
|
|
103
113
|
| **Get permission**, and not be asked again | `libei.portal` | Same handshake over D-Bus directly, with `persist_mode` / `restore_token`. Needs PyGObject |
|
|
114
|
+
| **Capture the user's real input** | `libei.portal` → `InputCaptureSession` | The read direction, through the InputCapture portal: once the compositor decides to, the user's pointer and keyboard are diverted to you over EIS. Exclusive while active, so hold it briefly. Only its negotiation half has been run against a real desktop — see [verification](docs/developers/verification.md) |
|
|
104
115
|
| **Be the server**, for tests or a compositor | `libei.eis` | Drives your client code with no real compositor and no consent dialog |
|
|
105
116
|
|
|
106
|
-
Each module has `is_available()
|
|
107
|
-
`
|
|
108
|
-
`
|
|
109
|
-
|
|
110
|
-
|
|
117
|
+
Each module has `is_available()`. `ei` and `eis` share the shapes around them:
|
|
118
|
+
an `Error` exception, an `EventType` and `DeviceCapability` enum, and `Device`,
|
|
119
|
+
`Seat`, `Region`, `Keymap`, `Touch`, `Ping`, `Event` with the frozen dataclasses
|
|
120
|
+
its accessors return. `oeffis` and `portal` are smaller — `DeviceType`, no
|
|
121
|
+
`EventType` or `DeviceCapability`, `Activation` as the one frozen result a
|
|
122
|
+
portal wait hands back, and their own exception classes rather than an `Error`.
|
|
123
|
+
[docs/troubleshooting.md](docs/troubleshooting.md) names every class they raise.
|
|
124
|
+
The package ships `py.typed`, so callers type-check against real
|
|
125
|
+
annotations rather than `Any`.
|
|
111
126
|
|
|
112
127
|
## What's implemented
|
|
113
128
|
|
|
@@ -135,8 +150,9 @@ because a skimming reader could otherwise take them as supported. 1.6.0's own
|
|
|
135
150
|
`enum ei_device_capability` stops at `TEXT`, and its `enum ei_event_type`
|
|
136
151
|
stops at `EI_EVENT_TEXT_UTF8` — so the swipe/pinch/hold/stylus members of
|
|
137
152
|
`EventType` cannot arrive either. The values here match upstream `main`
|
|
138
|
-
exactly, so they are ready for whatever release adds them. The
|
|
139
|
-
|
|
153
|
+
exactly, so they are ready for whatever release adds them. The gesture
|
|
154
|
+
and stylus functions `main` adds -- 46 in libei and 51 in libeis against
|
|
155
|
+
1.6.0's headers, counted 2026-09-30 -- are deliberately not bound:
|
|
140
156
|
nothing that ships today exports them, so nothing here could be verified
|
|
141
157
|
against a real library, which is the bar every other binding in this package
|
|
142
158
|
was held to.
|
|
@@ -149,14 +165,16 @@ Beyond sending input, the wrapper also covers ping/pong round trips
|
|
|
149
165
|
(`Device.keymap`), region mapping ids and coordinate conversion,
|
|
150
166
|
`Context.disconnect()`, `Context.peek_event_type()`, and
|
|
151
167
|
`Seat.request_device()`. On the server side, `libei.eis` mirrors all of it and
|
|
152
|
-
adds `Eis.set_flag()`
|
|
168
|
+
adds `Eis.set_flag()` (with the `Flag` values it takes), `Client.pid`, and
|
|
169
|
+
`Device.configure()` with the `ConfigureRegion` descriptions it accepts.
|
|
170
|
+
Underneath, the ctypes layer binds 250
|
|
153
171
|
of the 302 functions the three libraries export as of 1.6.0; what is left out,
|
|
154
172
|
and why, is in
|
|
155
173
|
[docs/developers/architecture.md](docs/developers/architecture.md#what-is-bound-and-what-is-deliberately-not).
|
|
156
174
|
|
|
157
175
|
## Status
|
|
158
176
|
|
|
159
|
-
Beta (`0.
|
|
177
|
+
Beta (`0.6.1`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
160
178
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
161
179
|
|
|
162
180
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -197,9 +215,13 @@ Exactly what was run, when, and against which versions:
|
|
|
197
215
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
198
216
|
sending pointer, button, keyboard, scroll and touch input all use symbols
|
|
199
217
|
that have existed with a stable signature since 1.0.0, and upstream keeps
|
|
200
|
-
API/ABI back-compatible within the 1.x series.
|
|
201
|
-
|
|
202
|
-
the injection path passes the full suite with
|
|
218
|
+
API/ABI back-compatible within the 1.x series. The suite has been run
|
|
219
|
+
against 1.2.1 (Ubuntu 24.04, in CI) and 1.6.0 -- the latter on Fedora 44 and
|
|
220
|
+
45 and FreeBSD 15, where the injection path passes the full suite with
|
|
221
|
+
nothing skipped. Every binding's name, argument types and return type, and
|
|
222
|
+
every enum value, is also checked against the upstream headers of 1.0.0,
|
|
223
|
+
1.2.1, 1.4.0, 1.5.0 and 1.6.0 (`tests/test_abi.py`), which is the only
|
|
224
|
+
coverage 1.0.0, 1.4.0 and 1.5.0 have had.
|
|
203
225
|
|
|
204
226
|
Newer libei buys you more, per feature:
|
|
205
227
|
|
|
@@ -357,8 +379,12 @@ All four, with working code:
|
|
|
357
379
|
soon as the loop moves on, and using it afterwards raises `RuntimeError`.
|
|
358
380
|
Objects you pull *off* an event (`event.device`, `event.seat`) are safe to
|
|
359
381
|
keep — copy out `event.pointer_event` and friends rather than the event.
|
|
360
|
-
- **`frame()` or
|
|
361
|
-
them
|
|
382
|
+
- **`frame()` or the input is lost.** Events queue up until a `frame()` commits
|
|
383
|
+
them, and queued events that are never framed are dropped without an
|
|
384
|
+
exception or a log line. The one exception is a chain that ends in
|
|
385
|
+
`stop_emulating()`: libei frames what is queued, delivers it, and logs
|
|
386
|
+
`Bug: ei_device_stop_emulating: missing call to ei_device_frame()` at error
|
|
387
|
+
level. Measured against libei 1.2.1 and 1.6.0; don't rely on it.
|
|
362
388
|
- **`bind()` needs at least one capability.** Binding an empty set sends
|
|
363
389
|
nothing, so the device you are waiting for never arrives; this raises
|
|
364
390
|
`ValueError` rather than hanging.
|
|
@@ -367,20 +393,22 @@ All four, with working code:
|
|
|
367
393
|
- **One seat can resume several devices.** Bind both `POINTER` and
|
|
368
394
|
`POINTER_ABSOLUTE` and GNOME gives you two, relative first. Taking
|
|
369
395
|
whichever resumes first is a coin flip — select on `device.capabilities`
|
|
370
|
-
instead. Sending an event the device lacks the capability for is
|
|
371
|
-
|
|
396
|
+
instead. Sending an event the device lacks the capability for is dropped with
|
|
397
|
+
no exception — libei only logs an error-level `Bug: ... device is not a
|
|
398
|
+
keyboard` line — which makes this look like the injection simply not working.
|
|
372
399
|
- **Read the accessor that matches the event type.** `event.key_event` on a
|
|
373
400
|
`POINTER_MOTION` event raises `TypeError` naming both types. libei itself
|
|
374
401
|
would have returned `KeyEvent(key=0, is_press=False)` — a real-looking
|
|
375
|
-
value —
|
|
376
|
-
`event_type` first. `TOUCH_UP` has its own `touch_up_event`, since it
|
|
402
|
+
value — and raised nothing, only logging a `Bug:` line at error level, so
|
|
403
|
+
branch on `event_type` first. `TOUCH_UP` has its own `touch_up_event`, since it
|
|
377
404
|
carries no coordinates.
|
|
378
405
|
- **`GESTURES` and `STYLUS` are not in any released libei.** They match
|
|
379
406
|
upstream `main` and are here ready for it, but 1.6.0's capability enum
|
|
380
407
|
stops at `TEXT`. Binding them against a shipping library silently does
|
|
381
408
|
nothing — no error, no device, no events.
|
|
382
409
|
|
|
383
|
-
Nearly all of these fail
|
|
410
|
+
Nearly all of these fail without raising — some only log an error-level `Bug:`
|
|
411
|
+
line, some not even that — which is why
|
|
384
412
|
[docs/troubleshooting.md](docs/troubleshooting.md) is a checklist rather than
|
|
385
413
|
a list of error messages. Start there when nothing happens.
|
|
386
414
|
|
|
@@ -394,8 +422,10 @@ a list of error messages. Start there when nothing happens.
|
|
|
394
422
|
happens" checklist
|
|
395
423
|
- [docs/vs-snegg.md](docs/vs-snegg.md) — how this differs from the reference
|
|
396
424
|
bindings, and two signature issues found by cross-checking the C source
|
|
397
|
-
- [docs/developers/](docs/developers/)
|
|
398
|
-
|
|
425
|
+
- [docs/developers/architecture.md](docs/developers/architecture.md) and
|
|
426
|
+
[docs/developers/verification.md](docs/developers/verification.md) — the
|
|
427
|
+
four-layer architecture, and what has actually been verified against which
|
|
428
|
+
libei versions
|
|
399
429
|
- [CONTRIBUTING.md](CONTRIBUTING.md) — setup, checks, testing against an old
|
|
400
430
|
libei, releasing
|
|
401
431
|
|
|
@@ -1,42 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: python-libei
|
|
3
|
-
Version: 0.5.2
|
|
4
|
-
Summary: Inject and receive input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis
|
|
5
|
-
Author: Dennis K. Paulsen
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Project-URL: homepage, https://github.com/ctrondlp/python-libei
|
|
8
|
-
Project-URL: repository, https://github.com/ctrondlp/python-libei
|
|
9
|
-
Project-URL: issues, https://github.com/ctrondlp/python-libei/issues
|
|
10
|
-
Project-URL: changelog, https://github.com/ctrondlp/python-libei/blob/main/CHANGELOG.md
|
|
11
|
-
Keywords: wayland,libei,libeis,liboeffis,input,input-emulation,emulated-input,portal,xdg-desktop-portal,remote-desktop,automation,gui-testing,accessibility,ctypes
|
|
12
|
-
Classifier: Development Status :: 4 - Beta
|
|
13
|
-
Classifier: Intended Audience :: Developers
|
|
14
|
-
Classifier: Operating System :: POSIX :: BSD :: FreeBSD
|
|
15
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
-
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
-
Classifier: Topic :: Desktop Environment
|
|
23
|
-
Classifier: Topic :: Software Development :: Libraries
|
|
24
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
-
Classifier: Topic :: Software Development :: Testing
|
|
26
|
-
Classifier: Typing :: Typed
|
|
27
|
-
Requires-Python: >=3.10
|
|
28
|
-
Description-Content-Type: text/markdown
|
|
29
|
-
License-File: LICENSE
|
|
30
|
-
Provides-Extra: portal
|
|
31
|
-
Requires-Dist: PyGObject>=3.42; extra == "portal"
|
|
32
|
-
Provides-Extra: dev
|
|
33
|
-
Requires-Dist: pytest>=7; extra == "dev"
|
|
34
|
-
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
35
|
-
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
36
|
-
Dynamic: license-file
|
|
37
|
-
|
|
38
1
|
# python-libei
|
|
39
2
|
|
|
3
|
+
[](https://github.com/ctrondlp/python-libei/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/python-libei/)
|
|
5
|
+
[](https://github.com/ctrondlp/python-libei/blob/main/LICENSE)
|
|
6
|
+
|
|
40
7
|
Python bindings for [libei, libeis and liboeffis](https://libinput.pages.freedesktop.org/libei/) —
|
|
41
8
|
the Wayland input-emulation libraries. Use this to **move the pointer, click,
|
|
42
9
|
type, or scroll on a Wayland desktop** from Python, the way `xdotool` did on
|
|
@@ -49,8 +16,11 @@ device.start_emulating().pointer_motion(5, 0).frame().stop_emulating()
|
|
|
49
16
|
```
|
|
50
17
|
|
|
51
18
|
**The one concept:** events queue up, and **`frame()` is what sends them** as
|
|
52
|
-
one logical hardware event. Forget it and
|
|
53
|
-
warning, no movement.
|
|
19
|
+
one logical hardware event. Forget it and the motion is usually lost — no
|
|
20
|
+
exception, no warning, no movement. (Ending the chain with `stop_emulating()`
|
|
21
|
+
makes libei frame the queue for you, but it logs an error-level `Bug:` line
|
|
22
|
+
when it does; see [Things that will bite you](#things-that-will-bite-you).) Fill
|
|
23
|
+
the package, then post it.
|
|
54
24
|
|
|
55
25
|
New here? [docs/getting-started.md](docs/getting-started.md) is install
|
|
56
26
|
through a first real pointer motion, in five minutes.
|
|
@@ -92,8 +62,8 @@ than a widget-tree back door:
|
|
|
92
62
|
|
|
93
63
|
## Which API do I need?
|
|
94
64
|
|
|
95
|
-
|
|
96
|
-
to get permission, then `ei` to inject.
|
|
65
|
+
Four modules (`ei`, `eis`, `oeffis`, `portal`), and most callers need exactly
|
|
66
|
+
two of them: `oeffis` or `portal` to get permission, then `ei` to inject.
|
|
97
67
|
|
|
98
68
|
| You want to… | Use | Notes |
|
|
99
69
|
| --- | --- | --- |
|
|
@@ -101,13 +71,18 @@ to get permission, then `ei` to inject.
|
|
|
101
71
|
| **Consume input** from a compositor | `libei.ei` → `Receiver` | Compositor-side or input-capture code; same connection dance |
|
|
102
72
|
| **Get permission**, simply | `libei.oeffis` | One call, pollable fd, no dependencies. The consent dialog appears on **every** run |
|
|
103
73
|
| **Get permission**, and not be asked again | `libei.portal` | Same handshake over D-Bus directly, with `persist_mode` / `restore_token`. Needs PyGObject |
|
|
74
|
+
| **Capture the user's real input** | `libei.portal` → `InputCaptureSession` | The read direction, through the InputCapture portal: once the compositor decides to, the user's pointer and keyboard are diverted to you over EIS. Exclusive while active, so hold it briefly. Only its negotiation half has been run against a real desktop — see [verification](docs/developers/verification.md) |
|
|
104
75
|
| **Be the server**, for tests or a compositor | `libei.eis` | Drives your client code with no real compositor and no consent dialog |
|
|
105
76
|
|
|
106
|
-
Each module has `is_available()
|
|
107
|
-
`
|
|
108
|
-
`
|
|
109
|
-
|
|
110
|
-
|
|
77
|
+
Each module has `is_available()`. `ei` and `eis` share the shapes around them:
|
|
78
|
+
an `Error` exception, an `EventType` and `DeviceCapability` enum, and `Device`,
|
|
79
|
+
`Seat`, `Region`, `Keymap`, `Touch`, `Ping`, `Event` with the frozen dataclasses
|
|
80
|
+
its accessors return. `oeffis` and `portal` are smaller — `DeviceType`, no
|
|
81
|
+
`EventType` or `DeviceCapability`, `Activation` as the one frozen result a
|
|
82
|
+
portal wait hands back, and their own exception classes rather than an `Error`.
|
|
83
|
+
[docs/troubleshooting.md](docs/troubleshooting.md) names every class they raise.
|
|
84
|
+
The package ships `py.typed`, so callers type-check against real
|
|
85
|
+
annotations rather than `Any`.
|
|
111
86
|
|
|
112
87
|
## What's implemented
|
|
113
88
|
|
|
@@ -135,8 +110,9 @@ because a skimming reader could otherwise take them as supported. 1.6.0's own
|
|
|
135
110
|
`enum ei_device_capability` stops at `TEXT`, and its `enum ei_event_type`
|
|
136
111
|
stops at `EI_EVENT_TEXT_UTF8` — so the swipe/pinch/hold/stylus members of
|
|
137
112
|
`EventType` cannot arrive either. The values here match upstream `main`
|
|
138
|
-
exactly, so they are ready for whatever release adds them. The
|
|
139
|
-
|
|
113
|
+
exactly, so they are ready for whatever release adds them. The gesture
|
|
114
|
+
and stylus functions `main` adds -- 46 in libei and 51 in libeis against
|
|
115
|
+
1.6.0's headers, counted 2026-09-30 -- are deliberately not bound:
|
|
140
116
|
nothing that ships today exports them, so nothing here could be verified
|
|
141
117
|
against a real library, which is the bar every other binding in this package
|
|
142
118
|
was held to.
|
|
@@ -149,14 +125,16 @@ Beyond sending input, the wrapper also covers ping/pong round trips
|
|
|
149
125
|
(`Device.keymap`), region mapping ids and coordinate conversion,
|
|
150
126
|
`Context.disconnect()`, `Context.peek_event_type()`, and
|
|
151
127
|
`Seat.request_device()`. On the server side, `libei.eis` mirrors all of it and
|
|
152
|
-
adds `Eis.set_flag()`
|
|
128
|
+
adds `Eis.set_flag()` (with the `Flag` values it takes), `Client.pid`, and
|
|
129
|
+
`Device.configure()` with the `ConfigureRegion` descriptions it accepts.
|
|
130
|
+
Underneath, the ctypes layer binds 250
|
|
153
131
|
of the 302 functions the three libraries export as of 1.6.0; what is left out,
|
|
154
132
|
and why, is in
|
|
155
133
|
[docs/developers/architecture.md](docs/developers/architecture.md#what-is-bound-and-what-is-deliberately-not).
|
|
156
134
|
|
|
157
135
|
## Status
|
|
158
136
|
|
|
159
|
-
Beta (`0.
|
|
137
|
+
Beta (`0.6.1`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
160
138
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
161
139
|
|
|
162
140
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -197,9 +175,13 @@ Exactly what was run, when, and against which versions:
|
|
|
197
175
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
198
176
|
sending pointer, button, keyboard, scroll and touch input all use symbols
|
|
199
177
|
that have existed with a stable signature since 1.0.0, and upstream keeps
|
|
200
|
-
API/ABI back-compatible within the 1.x series.
|
|
201
|
-
|
|
202
|
-
the injection path passes the full suite with
|
|
178
|
+
API/ABI back-compatible within the 1.x series. The suite has been run
|
|
179
|
+
against 1.2.1 (Ubuntu 24.04, in CI) and 1.6.0 -- the latter on Fedora 44 and
|
|
180
|
+
45 and FreeBSD 15, where the injection path passes the full suite with
|
|
181
|
+
nothing skipped. Every binding's name, argument types and return type, and
|
|
182
|
+
every enum value, is also checked against the upstream headers of 1.0.0,
|
|
183
|
+
1.2.1, 1.4.0, 1.5.0 and 1.6.0 (`tests/test_abi.py`), which is the only
|
|
184
|
+
coverage 1.0.0, 1.4.0 and 1.5.0 have had.
|
|
203
185
|
|
|
204
186
|
Newer libei buys you more, per feature:
|
|
205
187
|
|
|
@@ -357,8 +339,12 @@ All four, with working code:
|
|
|
357
339
|
soon as the loop moves on, and using it afterwards raises `RuntimeError`.
|
|
358
340
|
Objects you pull *off* an event (`event.device`, `event.seat`) are safe to
|
|
359
341
|
keep — copy out `event.pointer_event` and friends rather than the event.
|
|
360
|
-
- **`frame()` or
|
|
361
|
-
them
|
|
342
|
+
- **`frame()` or the input is lost.** Events queue up until a `frame()` commits
|
|
343
|
+
them, and queued events that are never framed are dropped without an
|
|
344
|
+
exception or a log line. The one exception is a chain that ends in
|
|
345
|
+
`stop_emulating()`: libei frames what is queued, delivers it, and logs
|
|
346
|
+
`Bug: ei_device_stop_emulating: missing call to ei_device_frame()` at error
|
|
347
|
+
level. Measured against libei 1.2.1 and 1.6.0; don't rely on it.
|
|
362
348
|
- **`bind()` needs at least one capability.** Binding an empty set sends
|
|
363
349
|
nothing, so the device you are waiting for never arrives; this raises
|
|
364
350
|
`ValueError` rather than hanging.
|
|
@@ -367,20 +353,22 @@ All four, with working code:
|
|
|
367
353
|
- **One seat can resume several devices.** Bind both `POINTER` and
|
|
368
354
|
`POINTER_ABSOLUTE` and GNOME gives you two, relative first. Taking
|
|
369
355
|
whichever resumes first is a coin flip — select on `device.capabilities`
|
|
370
|
-
instead. Sending an event the device lacks the capability for is
|
|
371
|
-
|
|
356
|
+
instead. Sending an event the device lacks the capability for is dropped with
|
|
357
|
+
no exception — libei only logs an error-level `Bug: ... device is not a
|
|
358
|
+
keyboard` line — which makes this look like the injection simply not working.
|
|
372
359
|
- **Read the accessor that matches the event type.** `event.key_event` on a
|
|
373
360
|
`POINTER_MOTION` event raises `TypeError` naming both types. libei itself
|
|
374
361
|
would have returned `KeyEvent(key=0, is_press=False)` — a real-looking
|
|
375
|
-
value —
|
|
376
|
-
`event_type` first. `TOUCH_UP` has its own `touch_up_event`, since it
|
|
362
|
+
value — and raised nothing, only logging a `Bug:` line at error level, so
|
|
363
|
+
branch on `event_type` first. `TOUCH_UP` has its own `touch_up_event`, since it
|
|
377
364
|
carries no coordinates.
|
|
378
365
|
- **`GESTURES` and `STYLUS` are not in any released libei.** They match
|
|
379
366
|
upstream `main` and are here ready for it, but 1.6.0's capability enum
|
|
380
367
|
stops at `TEXT`. Binding them against a shipping library silently does
|
|
381
368
|
nothing — no error, no device, no events.
|
|
382
369
|
|
|
383
|
-
Nearly all of these fail
|
|
370
|
+
Nearly all of these fail without raising — some only log an error-level `Bug:`
|
|
371
|
+
line, some not even that — which is why
|
|
384
372
|
[docs/troubleshooting.md](docs/troubleshooting.md) is a checklist rather than
|
|
385
373
|
a list of error messages. Start there when nothing happens.
|
|
386
374
|
|
|
@@ -394,8 +382,10 @@ a list of error messages. Start there when nothing happens.
|
|
|
394
382
|
happens" checklist
|
|
395
383
|
- [docs/vs-snegg.md](docs/vs-snegg.md) — how this differs from the reference
|
|
396
384
|
bindings, and two signature issues found by cross-checking the C source
|
|
397
|
-
- [docs/developers/](docs/developers/)
|
|
398
|
-
|
|
385
|
+
- [docs/developers/architecture.md](docs/developers/architecture.md) and
|
|
386
|
+
[docs/developers/verification.md](docs/developers/verification.md) — the
|
|
387
|
+
four-layer architecture, and what has actually been verified against which
|
|
388
|
+
libei versions
|
|
399
389
|
- [CONTRIBUTING.md](CONTRIBUTING.md) — setup, checks, testing against an old
|
|
400
390
|
libei, releasing
|
|
401
391
|
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-libei"
|
|
7
|
-
version = "0.
|
|
8
|
-
description = "Inject and
|
|
7
|
+
version = "0.6.1"
|
|
8
|
+
description = "Inject, receive and capture input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis, plus the RemoteDesktop and InputCapture portals"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -18,6 +18,8 @@ keywords = [
|
|
|
18
18
|
"input",
|
|
19
19
|
"input-emulation",
|
|
20
20
|
"emulated-input",
|
|
21
|
+
"input-capture",
|
|
22
|
+
"eis",
|
|
21
23
|
"portal",
|
|
22
24
|
"xdg-desktop-portal",
|
|
23
25
|
"remote-desktop",
|
|
@@ -31,6 +33,7 @@ classifiers = [
|
|
|
31
33
|
"Intended Audience :: Developers",
|
|
32
34
|
"Operating System :: POSIX :: BSD :: FreeBSD",
|
|
33
35
|
"Operating System :: POSIX :: Linux",
|
|
36
|
+
"Operating System :: POSIX",
|
|
34
37
|
"Programming Language :: Python :: 3",
|
|
35
38
|
"Programming Language :: Python :: 3 :: Only",
|
|
36
39
|
"Programming Language :: Python :: 3.10",
|
|
@@ -62,6 +65,8 @@ dev = ["pytest>=7", "ruff>=0.6", "mypy>=1.11"]
|
|
|
62
65
|
homepage = "https://github.com/ctrondlp/python-libei"
|
|
63
66
|
repository = "https://github.com/ctrondlp/python-libei"
|
|
64
67
|
issues = "https://github.com/ctrondlp/python-libei/issues"
|
|
68
|
+
documentation = "https://github.com/ctrondlp/python-libei/tree/main/docs"
|
|
69
|
+
pyguitest = "https://github.com/ctrondlp/pyguitest"
|
|
65
70
|
changelog = "https://github.com/ctrondlp/python-libei/blob/main/CHANGELOG.md"
|
|
66
71
|
|
|
67
72
|
[tool.setuptools.packages.find]
|
|
@@ -73,6 +78,14 @@ libei = ["py.typed"]
|
|
|
73
78
|
|
|
74
79
|
[tool.pytest.ini_options]
|
|
75
80
|
testpaths = ["tests"]
|
|
81
|
+
# The suite imports `libei` from the checkout rather than requiring an
|
|
82
|
+
# install, so `scripts/pre-commit-test.sh` runs on an interpreter with only
|
|
83
|
+
# the dev tools in it -- which is what that script's own header asks for, and
|
|
84
|
+
# what pyguitest and pyguitest-recorder already do. CI installs the package,
|
|
85
|
+
# so the installed path is still covered there. Without this, conftest's
|
|
86
|
+
# `from libei import ei` fails at collection and the whole check reports an
|
|
87
|
+
# ImportError instead of running anything.
|
|
88
|
+
pythonpath = ["src"]
|
|
76
89
|
markers = [
|
|
77
90
|
"integration: requires the real libei/libeis/liboeffis shared libraries to be installed",
|
|
78
91
|
]
|
|
@@ -128,7 +128,7 @@ event_scroll_get_discrete_dy = lib.function(
|
|
|
128
128
|
event_touch_get_id = lib.function("ei_event_touch_get_id", (c_void_p,), c_uint32)
|
|
129
129
|
event_touch_get_x = lib.function("ei_event_touch_get_x", (c_void_p,), c_double)
|
|
130
130
|
event_touch_get_y = lib.function("ei_event_touch_get_y", (c_void_p,), c_double)
|
|
131
|
-
event_touch_get_is_cancel = lib.function(
|
|
131
|
+
event_touch_get_is_cancel = lib.function( # libei 1.4+
|
|
132
132
|
"ei_event_touch_get_is_cancel", (c_void_p,), c_bool
|
|
133
133
|
)
|
|
134
134
|
event_text_get_utf8 = lib.function( # libei 1.6+
|
|
@@ -141,7 +141,9 @@ event_text_get_keysym_is_press = lib.function( # libei 1.6+
|
|
|
141
141
|
"ei_event_text_get_keysym_is_press", (c_void_p,), c_bool
|
|
142
142
|
)
|
|
143
143
|
# Borrowed reference -- the event still owns it, so wrap() rather than adopt().
|
|
144
|
-
event_pong_get_ping = lib.function(
|
|
144
|
+
event_pong_get_ping = lib.function( # libei 1.4+
|
|
145
|
+
"ei_event_pong_get_ping", (c_void_p,), c_void_p
|
|
146
|
+
)
|
|
145
147
|
|
|
146
148
|
device_ref = lib.function("ei_device_ref", (c_void_p,), c_void_p)
|
|
147
149
|
device_unref = lib.function("ei_device_unref", (c_void_p,), c_void_p)
|
|
@@ -236,7 +238,7 @@ touch_get_device = lib.function("ei_touch_get_device", (c_void_p,), c_void_p)
|
|
|
236
238
|
touch_down = lib.function("ei_touch_down", (c_void_p, c_double, c_double), None)
|
|
237
239
|
touch_motion = lib.function("ei_touch_motion", (c_void_p, c_double, c_double), None)
|
|
238
240
|
touch_up = lib.function("ei_touch_up", (c_void_p,), None)
|
|
239
|
-
touch_cancel = lib.function("ei_touch_cancel", (c_void_p,), None)
|
|
241
|
+
touch_cancel = lib.function("ei_touch_cancel", (c_void_p,), None) # libei 1.4+
|
|
240
242
|
|
|
241
243
|
# ei_new_ping() returns an owned reference; ei_ping() then triggers the round
|
|
242
244
|
# trip that comes back as an EI_EVENT_PONG.
|
|
@@ -104,7 +104,7 @@ event_scroll_get_discrete_dy = lib.function(
|
|
|
104
104
|
event_touch_get_id = lib.function("eis_event_touch_get_id", (c_void_p,), c_uint32)
|
|
105
105
|
event_touch_get_x = lib.function("eis_event_touch_get_x", (c_void_p,), c_double)
|
|
106
106
|
event_touch_get_y = lib.function("eis_event_touch_get_y", (c_void_p,), c_double)
|
|
107
|
-
event_touch_get_is_cancel = lib.function(
|
|
107
|
+
event_touch_get_is_cancel = lib.function( # libei 1.4+
|
|
108
108
|
"eis_event_touch_get_is_cancel", (c_void_p,), c_bool
|
|
109
109
|
)
|
|
110
110
|
event_text_get_utf8 = lib.function( # libei 1.6+
|
|
@@ -117,14 +117,16 @@ event_text_get_keysym_is_press = lib.function( # libei 1.6+
|
|
|
117
117
|
"eis_event_text_get_keysym_is_press", (c_void_p,), c_bool
|
|
118
118
|
)
|
|
119
119
|
# Borrowed reference -- the event still owns it, so wrap() rather than adopt().
|
|
120
|
-
event_pong_get_ping = lib.function(
|
|
120
|
+
event_pong_get_ping = lib.function( # libei 1.4+
|
|
121
|
+
"eis_event_pong_get_ping", (c_void_p,), c_void_p
|
|
122
|
+
)
|
|
121
123
|
|
|
122
124
|
client_ref = lib.function("eis_client_ref", (c_void_p,), c_void_p)
|
|
123
125
|
client_unref = lib.function("eis_client_unref", (c_void_p,), c_void_p)
|
|
124
126
|
client_is_sender = lib.function("eis_client_is_sender", (c_void_p,), c_bool)
|
|
125
127
|
# pid_t, i.e. a 32-bit signed int on Linux. Socket backend only, and
|
|
126
128
|
# negative errno on failure.
|
|
127
|
-
backend_socket_get_client_pid = lib.function(
|
|
129
|
+
backend_socket_get_client_pid = lib.function( # libei 1.5+
|
|
128
130
|
"eis_backend_socket_get_client_pid", (c_void_p,), c_int32
|
|
129
131
|
)
|
|
130
132
|
client_get_name = lib.function("eis_client_get_name", (c_void_p,), c_char_p)
|
|
@@ -277,7 +279,7 @@ touch_get_device = lib.function("eis_touch_get_device", (c_void_p,), c_void_p)
|
|
|
277
279
|
touch_down = lib.function("eis_touch_down", (c_void_p, c_double, c_double), None)
|
|
278
280
|
touch_motion = lib.function("eis_touch_motion", (c_void_p, c_double, c_double), None)
|
|
279
281
|
touch_up = lib.function("eis_touch_up", (c_void_p,), None)
|
|
280
|
-
touch_cancel = lib.function("eis_touch_cancel", (c_void_p,), None)
|
|
282
|
+
touch_cancel = lib.function("eis_touch_cancel", (c_void_p,), None) # libei 1.4+
|
|
281
283
|
|
|
282
284
|
ping = lib.function("eis_ping", (c_void_p,), None) # libei 1.4+
|
|
283
285
|
ping_get_id = lib.function("eis_ping_get_id", (c_void_p,), c_uint64) # libei 1.4+
|
|
@@ -25,12 +25,19 @@ class LazyLibrary:
|
|
|
25
25
|
"""A ctypes.CDLL that only opens the library on first real use."""
|
|
26
26
|
|
|
27
27
|
def __init__(self, soname: str) -> None:
|
|
28
|
+
"""Remember the soname; nothing is opened until first use."""
|
|
28
29
|
self._soname = soname
|
|
29
30
|
self._lib: ctypes.CDLL | None = None
|
|
30
31
|
self._load_error: OSError | None = None
|
|
31
32
|
self._lock = threading.Lock()
|
|
33
|
+
self._declared: dict[str, tuple[tuple[type, ...], type | None]] = {}
|
|
32
34
|
|
|
33
35
|
def _ensure_loaded(self) -> ctypes.CDLL:
|
|
36
|
+
"""Return the opened library, opening it on first call.
|
|
37
|
+
|
|
38
|
+
Raises LibraryNotFoundError on every later call too after a failure:
|
|
39
|
+
the failed load is cached rather than retried.
|
|
40
|
+
"""
|
|
34
41
|
# Double-checked: the unlocked read is the fast path taken by every
|
|
35
42
|
# call after the first, and the repeated check inside the lock is
|
|
36
43
|
# what makes it safe -- two threads can both fall through the first
|
|
@@ -69,6 +76,21 @@ class LazyLibrary:
|
|
|
69
76
|
return False
|
|
70
77
|
return True
|
|
71
78
|
|
|
79
|
+
@property
|
|
80
|
+
def soname(self) -> str:
|
|
81
|
+
"""The shared library this binds, as it is passed to ``dlopen``."""
|
|
82
|
+
return self._soname
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def declared(self) -> dict[str, tuple[tuple[type, ...], type | None]]:
|
|
86
|
+
"""Every function bound so far: C name -> (argtypes, restype).
|
|
87
|
+
|
|
88
|
+
Read by the ABI tests, which compare it with the library's exports and
|
|
89
|
+
with the upstream headers. Declaring a binding records it here and does
|
|
90
|
+
nothing else -- the library is still not opened until a call.
|
|
91
|
+
"""
|
|
92
|
+
return dict(self._declared)
|
|
93
|
+
|
|
72
94
|
def function(
|
|
73
95
|
self,
|
|
74
96
|
name: str,
|
|
@@ -88,9 +110,11 @@ class LazyLibrary:
|
|
|
88
110
|
# the dict is chosen only because "absent from the dict" already
|
|
89
111
|
# means "not resolved yet", with no None sentinel to confuse with a
|
|
90
112
|
# legitimately-None value.
|
|
113
|
+
self._declared[name] = (tuple(argtypes), restype)
|
|
91
114
|
cache: dict[str, Any] = {}
|
|
92
115
|
|
|
93
116
|
def call(*args: Any) -> Any:
|
|
117
|
+
"""Resolve the C function on first call, then pass straight through."""
|
|
94
118
|
# Resolution happens here, on first call, not at bind time --
|
|
95
119
|
# that is the whole point of this module (see its docstring).
|
|
96
120
|
bound = cache.get("f")
|
|
@@ -106,7 +130,23 @@ class LazyLibrary:
|
|
|
106
130
|
bound.argtypes = list(argtypes)
|
|
107
131
|
bound.restype = restype
|
|
108
132
|
cache["f"] = bound
|
|
109
|
-
|
|
133
|
+
try:
|
|
134
|
+
return bound(*args)
|
|
135
|
+
except ctypes.ArgumentError:
|
|
136
|
+
# ctypes turns *any* exception raised while converting an
|
|
137
|
+
# argument into an ArgumentError carrying only its text -- no
|
|
138
|
+
# __cause__, no __context__. For a released object that loses
|
|
139
|
+
# the RuntimeError its ``_as_parameter_`` raised, so a caller
|
|
140
|
+
# catching RuntimeError (as documented) would miss it. Ask the
|
|
141
|
+
# arguments again and re-raise the real one.
|
|
142
|
+
for arg in args:
|
|
143
|
+
try:
|
|
144
|
+
arg._as_parameter_ # noqa: B018 - evaluated for its error
|
|
145
|
+
except RuntimeError as released:
|
|
146
|
+
raise released from None
|
|
147
|
+
except AttributeError:
|
|
148
|
+
continue
|
|
149
|
+
raise
|
|
110
150
|
|
|
111
151
|
call.__name__ = name
|
|
112
152
|
return call
|
|
@@ -66,6 +66,7 @@ class CObject:
|
|
|
66
66
|
_instances_lock: ClassVar[threading.RLock]
|
|
67
67
|
|
|
68
68
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
69
|
+
"""Give each wrapper hierarchy one identity cache, shared by subclasses."""
|
|
69
70
|
super().__init_subclass__(**kwargs)
|
|
70
71
|
# One cache per wrapper *hierarchy*, not per class. Only a root
|
|
71
72
|
# wrapper class -- one whose only CObject ancestor is CObject
|
|
@@ -98,6 +99,7 @@ class CObject:
|
|
|
98
99
|
cls._instances_lock = threading.RLock()
|
|
99
100
|
|
|
100
101
|
def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
|
|
102
|
+
"""Take a reference on a non-NULL pointer and cache this wrapper for it."""
|
|
101
103
|
if not pointer:
|
|
102
104
|
raise ValueError(f"{type(self).__name__} cannot wrap a NULL pointer")
|
|
103
105
|
self._pointer = pointer
|
|
@@ -121,6 +123,7 @@ class CObject:
|
|
|
121
123
|
|
|
122
124
|
@property
|
|
123
125
|
def _as_parameter_(self) -> int:
|
|
126
|
+
"""The raw pointer ctypes passes to C -- an error once it is released."""
|
|
124
127
|
if self._pointer == 0:
|
|
125
128
|
raise RuntimeError(
|
|
126
129
|
f"{type(self).__name__} has already been released; "
|
|
@@ -159,6 +162,7 @@ class CObject:
|
|
|
159
162
|
|
|
160
163
|
@classmethod
|
|
161
164
|
def _get_or_create(cls: type[T], pointer: int | None, *, adopt: bool) -> T | None:
|
|
165
|
+
"""The wrapper already caching this pointer, or a new one; None for NULL."""
|
|
162
166
|
if not pointer:
|
|
163
167
|
return None
|
|
164
168
|
if not cls._wrappable:
|
|
@@ -232,6 +236,7 @@ class CObject:
|
|
|
232
236
|
return cls._get_or_create(pointer, adopt=True)
|
|
233
237
|
|
|
234
238
|
def __eq__(self, other: object) -> bool:
|
|
239
|
+
"""Equal when the types match and the wrapped pointers are the same."""
|
|
235
240
|
if not isinstance(other, CObject):
|
|
236
241
|
return NotImplemented
|
|
237
242
|
if type(self) is not type(other):
|
|
@@ -245,6 +250,7 @@ class CObject:
|
|
|
245
250
|
return self._pointer == other._pointer
|
|
246
251
|
|
|
247
252
|
def __hash__(self) -> int:
|
|
253
|
+
"""Stable for the object's life: the original pointer, not the live one."""
|
|
248
254
|
# Deliberately keyed on _hash_key, not _pointer: release() zeroes
|
|
249
255
|
# _pointer, and an object whose hash changes mid-life vanishes
|
|
250
256
|
# from any set or dict it was placed in. Two wrappers can share a
|