python-libei 0.5.1__tar.gz → 0.6.0__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.1 → python_libei-0.6.0}/PKG-INFO +33 -36
- {python_libei-0.5.1 → python_libei-0.6.0}/README.md +32 -35
- {python_libei-0.5.1 → python_libei-0.6.0}/pyproject.toml +41 -2
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/__init__.py +1 -1
- python_libei-0.6.0/src/libei/_capi/__init__.py +9 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/_capi/loader.py +7 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/_cobject.py +6 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/ei.py +90 -15
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/eis.py +82 -17
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/oeffis.py +19 -3
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/portal.py +127 -33
- {python_libei-0.5.1 → python_libei-0.6.0}/src/python_libei.egg-info/PKG-INFO +33 -36
- python_libei-0.6.0/tests/test_documentation_shape.py +478 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_documented_examples.py +0 -27
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_ei_objects.py +2 -3
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_eis_objects.py +2 -3
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_inputcapture.py +135 -12
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_integration_extras.py +4 -4
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_oeffis.py +3 -3
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_portal.py +64 -7
- python_libei-0.5.1/src/libei/_capi/__init__.py +0 -6
- python_libei-0.5.1/tests/test_documentation_shape.py +0 -270
- {python_libei-0.5.1 → python_libei-0.6.0}/LICENSE +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/setup.cfg +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/_capi/libei.py +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/_capi/libeis.py +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/_capi/liboeffis.py +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/libei/py.typed +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/python_libei.egg-info/SOURCES.txt +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/python_libei.egg-info/dependency_links.txt +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/python_libei.egg-info/requires.txt +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/src/python_libei.egg-info/top_level.txt +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_cobject.py +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_integration_socketpair.py +0 -0
- {python_libei-0.5.1 → python_libei-0.6.0}/tests/test_loader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-libei
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Inject and receive input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis
|
|
5
5
|
Author: Dennis K. Paulsen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,6 +37,10 @@ Dynamic: license-file
|
|
|
37
37
|
|
|
38
38
|
# python-libei
|
|
39
39
|
|
|
40
|
+
[](https://github.com/ctrondlp/python-libei/actions/workflows/ci.yml)
|
|
41
|
+
[](https://pypi.org/project/python-libei/)
|
|
42
|
+
[](https://github.com/ctrondlp/python-libei/blob/main/LICENSE)
|
|
43
|
+
|
|
40
44
|
Python bindings for [libei, libeis and liboeffis](https://libinput.pages.freedesktop.org/libei/) —
|
|
41
45
|
the Wayland input-emulation libraries. Use this to **move the pointer, click,
|
|
42
46
|
type, or scroll on a Wayland desktop** from Python, the way `xdotool` did on
|
|
@@ -103,11 +107,15 @@ to get permission, then `ei` to inject.
|
|
|
103
107
|
| **Get permission**, and not be asked again | `libei.portal` | Same handshake over D-Bus directly, with `persist_mode` / `restore_token`. Needs PyGObject |
|
|
104
108
|
| **Be the server**, for tests or a compositor | `libei.eis` | Drives your client code with no real compositor and no consent dialog |
|
|
105
109
|
|
|
106
|
-
Each module has `is_available()
|
|
107
|
-
`
|
|
108
|
-
`
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
Each module has `is_available()`. `ei` and `eis` share the shapes around them:
|
|
111
|
+
an `Error` exception, an `EventType` and `DeviceCapability` enum, and `Device`,
|
|
112
|
+
`Seat`, `Region`, `Keymap`, `Touch`, `Ping`, `Event` with the frozen dataclasses
|
|
113
|
+
its accessors return. `oeffis` and `portal` are smaller — `DeviceType`, no
|
|
114
|
+
`EventType` or `DeviceCapability`, `Activation` as the one frozen result a
|
|
115
|
+
portal wait hands back, and their own exception classes rather than an `Error`.
|
|
116
|
+
[docs/troubleshooting.md](docs/troubleshooting.md) names every class they raise.
|
|
117
|
+
The package ships `py.typed`, so callers type-check against real
|
|
118
|
+
annotations rather than `Any`.
|
|
111
119
|
|
|
112
120
|
## What's implemented
|
|
113
121
|
|
|
@@ -149,14 +157,16 @@ Beyond sending input, the wrapper also covers ping/pong round trips
|
|
|
149
157
|
(`Device.keymap`), region mapping ids and coordinate conversion,
|
|
150
158
|
`Context.disconnect()`, `Context.peek_event_type()`, and
|
|
151
159
|
`Seat.request_device()`. On the server side, `libei.eis` mirrors all of it and
|
|
152
|
-
adds `Eis.set_flag()`
|
|
160
|
+
adds `Eis.set_flag()` (with the `Flag` values it takes), `Client.pid`, and
|
|
161
|
+
`Device.configure()` with the `ConfigureRegion` descriptions it accepts.
|
|
162
|
+
Underneath, the ctypes layer binds 250
|
|
153
163
|
of the 302 functions the three libraries export as of 1.6.0; what is left out,
|
|
154
164
|
and why, is in
|
|
155
165
|
[docs/developers/architecture.md](docs/developers/architecture.md#what-is-bound-and-what-is-deliberately-not).
|
|
156
166
|
|
|
157
167
|
## Status
|
|
158
168
|
|
|
159
|
-
Beta (`0.
|
|
169
|
+
Beta (`0.6.0`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
160
170
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
161
171
|
|
|
162
172
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -187,11 +197,11 @@ Exactly what was run, when, and against which versions:
|
|
|
187
197
|
portal paths are the part likeliest to come up short off Linux, since
|
|
188
198
|
they need an xdg-desktop-portal RemoteDesktop backend to talk to.
|
|
189
199
|
- CPython 3.10 or newer (tested on 3.13)
|
|
190
|
-
- The native
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
- `libei.portal` only: PyGObject
|
|
194
|
-
|
|
200
|
+
- The native `libei`, `libeis` and `liboeffis` libraries, which `pip` cannot
|
|
201
|
+
supply. Which package provides them on your distribution — and what to do
|
|
202
|
+
when the name does not match — is in [docs/install.md](docs/install.md).
|
|
203
|
+
- `libei.portal` only: PyGObject, via the `portal` extra, plus whatever
|
|
204
|
+
GObject-introspection libraries your distribution needs for `Gio`, since
|
|
195
205
|
PyPI's PyGObject wheel supplies the Python side only. Not needed for
|
|
196
206
|
`libei.ei`, `libei.eis` or `libei.oeffis`.
|
|
197
207
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
@@ -224,22 +234,11 @@ Exactly what was run, when, and against which versions:
|
|
|
224
234
|
|
|
225
235
|
## Install
|
|
226
236
|
|
|
227
|
-
From [PyPI](https://pypi.org/project/python-libei/):
|
|
228
|
-
|
|
229
237
|
```sh
|
|
230
238
|
pip install python-libei
|
|
231
239
|
```
|
|
232
240
|
|
|
233
|
-
|
|
234
|
-
`pip show python-libei`, but `from libei import ei`.
|
|
235
|
-
|
|
236
|
-
Pure Python, no build step: the wheel is `py3-none-any` and ctypes talks to
|
|
237
|
-
the native libraries directly, so there is no compiler, no headers and no
|
|
238
|
-
`libei-devel` involved at install time. What `pip` does *not* bring is the
|
|
239
|
-
native libraries themselves -- see [Requirements](#requirements) above; on
|
|
240
|
-
Fedora, `sudo dnf install libei libeis liboeffis`.
|
|
241
|
-
|
|
242
|
-
To track `main` instead, or to hack on it, install from a checkout:
|
|
241
|
+
From a checkout instead, to track `main` or to work on the package:
|
|
243
242
|
|
|
244
243
|
```sh
|
|
245
244
|
git clone https://github.com/ctrondlp/python-libei.git
|
|
@@ -247,15 +246,11 @@ cd python-libei
|
|
|
247
246
|
pip install . # or `pip install -e '.[dev]'` to develop
|
|
248
247
|
```
|
|
249
248
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if not ei.is_available():
|
|
257
|
-
... # fall back to another input backend
|
|
258
|
-
```
|
|
249
|
+
That is the half `pip` can do. The native `libei`/`libeis`/`liboeffis`
|
|
250
|
+
libraries it cannot supply, the `portal` extra, and how to check what actually
|
|
251
|
+
loaded are all in [docs/install.md](docs/install.md) — see
|
|
252
|
+
[Requirements](#requirements) above for the version floors. Importing is safe
|
|
253
|
+
without any of it: those libraries are loaded on first *use*, not at import.
|
|
259
254
|
|
|
260
255
|
## Concepts
|
|
261
256
|
|
|
@@ -409,8 +404,10 @@ a list of error messages. Start there when nothing happens.
|
|
|
409
404
|
happens" checklist
|
|
410
405
|
- [docs/vs-snegg.md](docs/vs-snegg.md) — how this differs from the reference
|
|
411
406
|
bindings, and two signature issues found by cross-checking the C source
|
|
412
|
-
- [docs/developers/](docs/developers/)
|
|
413
|
-
|
|
407
|
+
- [docs/developers/architecture.md](docs/developers/architecture.md) and
|
|
408
|
+
[docs/developers/verification.md](docs/developers/verification.md) — the
|
|
409
|
+
four-layer architecture, and what has actually been verified against which
|
|
410
|
+
libei versions
|
|
414
411
|
- [CONTRIBUTING.md](CONTRIBUTING.md) — setup, checks, testing against an old
|
|
415
412
|
libei, releasing
|
|
416
413
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# python-libei
|
|
2
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
|
+
|
|
3
7
|
Python bindings for [libei, libeis and liboeffis](https://libinput.pages.freedesktop.org/libei/) —
|
|
4
8
|
the Wayland input-emulation libraries. Use this to **move the pointer, click,
|
|
5
9
|
type, or scroll on a Wayland desktop** from Python, the way `xdotool` did on
|
|
@@ -66,11 +70,15 @@ to get permission, then `ei` to inject.
|
|
|
66
70
|
| **Get permission**, and not be asked again | `libei.portal` | Same handshake over D-Bus directly, with `persist_mode` / `restore_token`. Needs PyGObject |
|
|
67
71
|
| **Be the server**, for tests or a compositor | `libei.eis` | Drives your client code with no real compositor and no consent dialog |
|
|
68
72
|
|
|
69
|
-
Each module has `is_available()
|
|
70
|
-
`
|
|
71
|
-
`
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
Each module has `is_available()`. `ei` and `eis` share the shapes around them:
|
|
74
|
+
an `Error` exception, an `EventType` and `DeviceCapability` enum, and `Device`,
|
|
75
|
+
`Seat`, `Region`, `Keymap`, `Touch`, `Ping`, `Event` with the frozen dataclasses
|
|
76
|
+
its accessors return. `oeffis` and `portal` are smaller — `DeviceType`, no
|
|
77
|
+
`EventType` or `DeviceCapability`, `Activation` as the one frozen result a
|
|
78
|
+
portal wait hands back, and their own exception classes rather than an `Error`.
|
|
79
|
+
[docs/troubleshooting.md](docs/troubleshooting.md) names every class they raise.
|
|
80
|
+
The package ships `py.typed`, so callers type-check against real
|
|
81
|
+
annotations rather than `Any`.
|
|
74
82
|
|
|
75
83
|
## What's implemented
|
|
76
84
|
|
|
@@ -112,14 +120,16 @@ Beyond sending input, the wrapper also covers ping/pong round trips
|
|
|
112
120
|
(`Device.keymap`), region mapping ids and coordinate conversion,
|
|
113
121
|
`Context.disconnect()`, `Context.peek_event_type()`, and
|
|
114
122
|
`Seat.request_device()`. On the server side, `libei.eis` mirrors all of it and
|
|
115
|
-
adds `Eis.set_flag()`
|
|
123
|
+
adds `Eis.set_flag()` (with the `Flag` values it takes), `Client.pid`, and
|
|
124
|
+
`Device.configure()` with the `ConfigureRegion` descriptions it accepts.
|
|
125
|
+
Underneath, the ctypes layer binds 250
|
|
116
126
|
of the 302 functions the three libraries export as of 1.6.0; what is left out,
|
|
117
127
|
and why, is in
|
|
118
128
|
[docs/developers/architecture.md](docs/developers/architecture.md#what-is-bound-and-what-is-deliberately-not).
|
|
119
129
|
|
|
120
130
|
## Status
|
|
121
131
|
|
|
122
|
-
Beta (`0.
|
|
132
|
+
Beta (`0.6.0`), published on [PyPI](https://pypi.org/project/python-libei/)
|
|
123
133
|
since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
|
|
124
134
|
|
|
125
135
|
The injection path is exercised end to end against the real libraries by the
|
|
@@ -150,11 +160,11 @@ Exactly what was run, when, and against which versions:
|
|
|
150
160
|
portal paths are the part likeliest to come up short off Linux, since
|
|
151
161
|
they need an xdg-desktop-portal RemoteDesktop backend to talk to.
|
|
152
162
|
- CPython 3.10 or newer (tested on 3.13)
|
|
153
|
-
- The native
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
- `libei.portal` only: PyGObject
|
|
157
|
-
|
|
163
|
+
- The native `libei`, `libeis` and `liboeffis` libraries, which `pip` cannot
|
|
164
|
+
supply. Which package provides them on your distribution — and what to do
|
|
165
|
+
when the name does not match — is in [docs/install.md](docs/install.md).
|
|
166
|
+
- `libei.portal` only: PyGObject, via the `portal` extra, plus whatever
|
|
167
|
+
GObject-introspection libraries your distribution needs for `Gio`, since
|
|
158
168
|
PyPI's PyGObject wheel supplies the Python side only. Not needed for
|
|
159
169
|
`libei.ei`, `libei.eis` or `libei.oeffis`.
|
|
160
170
|
- libei 1.0.0 or newer for the core: connecting, binding a seat, and
|
|
@@ -187,22 +197,11 @@ Exactly what was run, when, and against which versions:
|
|
|
187
197
|
|
|
188
198
|
## Install
|
|
189
199
|
|
|
190
|
-
From [PyPI](https://pypi.org/project/python-libei/):
|
|
191
|
-
|
|
192
200
|
```sh
|
|
193
201
|
pip install python-libei
|
|
194
202
|
```
|
|
195
203
|
|
|
196
|
-
|
|
197
|
-
`pip show python-libei`, but `from libei import ei`.
|
|
198
|
-
|
|
199
|
-
Pure Python, no build step: the wheel is `py3-none-any` and ctypes talks to
|
|
200
|
-
the native libraries directly, so there is no compiler, no headers and no
|
|
201
|
-
`libei-devel` involved at install time. What `pip` does *not* bring is the
|
|
202
|
-
native libraries themselves -- see [Requirements](#requirements) above; on
|
|
203
|
-
Fedora, `sudo dnf install libei libeis liboeffis`.
|
|
204
|
-
|
|
205
|
-
To track `main` instead, or to hack on it, install from a checkout:
|
|
204
|
+
From a checkout instead, to track `main` or to work on the package:
|
|
206
205
|
|
|
207
206
|
```sh
|
|
208
207
|
git clone https://github.com/ctrondlp/python-libei.git
|
|
@@ -210,15 +209,11 @@ cd python-libei
|
|
|
210
209
|
pip install . # or `pip install -e '.[dev]'` to develop
|
|
211
210
|
```
|
|
212
211
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if not ei.is_available():
|
|
220
|
-
... # fall back to another input backend
|
|
221
|
-
```
|
|
212
|
+
That is the half `pip` can do. The native `libei`/`libeis`/`liboeffis`
|
|
213
|
+
libraries it cannot supply, the `portal` extra, and how to check what actually
|
|
214
|
+
loaded are all in [docs/install.md](docs/install.md) — see
|
|
215
|
+
[Requirements](#requirements) above for the version floors. Importing is safe
|
|
216
|
+
without any of it: those libraries are loaded on first *use*, not at import.
|
|
222
217
|
|
|
223
218
|
## Concepts
|
|
224
219
|
|
|
@@ -372,8 +367,10 @@ a list of error messages. Start there when nothing happens.
|
|
|
372
367
|
happens" checklist
|
|
373
368
|
- [docs/vs-snegg.md](docs/vs-snegg.md) — how this differs from the reference
|
|
374
369
|
bindings, and two signature issues found by cross-checking the C source
|
|
375
|
-
- [docs/developers/](docs/developers/)
|
|
376
|
-
|
|
370
|
+
- [docs/developers/architecture.md](docs/developers/architecture.md) and
|
|
371
|
+
[docs/developers/verification.md](docs/developers/verification.md) — the
|
|
372
|
+
four-layer architecture, and what has actually been verified against which
|
|
373
|
+
libei versions
|
|
377
374
|
- [CONTRIBUTING.md](CONTRIBUTING.md) — setup, checks, testing against an old
|
|
378
375
|
libei, releasing
|
|
379
376
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-libei"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Inject and receive input on Wayland from Python: ctypes bindings for libei, libeis and liboeffis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -73,6 +73,14 @@ libei = ["py.typed"]
|
|
|
73
73
|
|
|
74
74
|
[tool.pytest.ini_options]
|
|
75
75
|
testpaths = ["tests"]
|
|
76
|
+
# The suite imports `libei` from the checkout rather than requiring an
|
|
77
|
+
# install, so `scripts/pre-commit-test.sh` runs on an interpreter with only
|
|
78
|
+
# the dev tools in it -- which is what that script's own header asks for, and
|
|
79
|
+
# what pyguitest and pyguitest-recorder already do. CI installs the package,
|
|
80
|
+
# so the installed path is still covered there. Without this, conftest's
|
|
81
|
+
# `from libei import ei` fails at collection and the whole check reports an
|
|
82
|
+
# ImportError instead of running anything.
|
|
83
|
+
pythonpath = ["src"]
|
|
76
84
|
markers = [
|
|
77
85
|
"integration: requires the real libei/libeis/liboeffis shared libraries to be installed",
|
|
78
86
|
]
|
|
@@ -87,8 +95,39 @@ select = [
|
|
|
87
95
|
"E", "W", # pycodestyle
|
|
88
96
|
"F", # pyflakes
|
|
89
97
|
"I", # isort
|
|
90
|
-
"
|
|
98
|
+
"D", # pydocstyle
|
|
91
99
|
"B", # bugbear
|
|
100
|
+
"UP", # pyupgrade
|
|
101
|
+
"C4", # comprehensions
|
|
102
|
+
"SIM", # simplification
|
|
103
|
+
"RET", # return consistency
|
|
104
|
+
"ARG", # unused arguments
|
|
105
|
+
"C901", # cyclomatic complexity
|
|
106
|
+
]
|
|
107
|
+
ignore = [
|
|
108
|
+
"D203", # incompatible with D211; keep no blank line before class docstring
|
|
109
|
+
"D213", # incompatible with D212; keep the summary on the first line
|
|
110
|
+
"D401", # properties read better as noun phrases than imperatives
|
|
111
|
+
"D105", # __enter__/__exit__ and friends document themselves
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
[tool.ruff.lint.mccabe]
|
|
115
|
+
# pyguitest's ceiling; python-libei's own worst case (InputCaptureSession.
|
|
116
|
+
# negotiate(), forking on portal version) sits at 14, just under it. A
|
|
117
|
+
# ceiling on what gets added, not a claim everything else is close to it.
|
|
118
|
+
max-complexity = 15
|
|
119
|
+
|
|
120
|
+
[tool.ruff.lint.pydocstyle]
|
|
121
|
+
convention = "pep257"
|
|
122
|
+
|
|
123
|
+
[tool.ruff.lint.per-file-ignores]
|
|
124
|
+
# Tests use fakes with deliberately unused arguments and multi-with blocks
|
|
125
|
+
# that read better nested; matching pyguitest's/pyguitest-recorder's own
|
|
126
|
+
# per-file-ignores for the same reasons.
|
|
127
|
+
"tests/*" = [
|
|
128
|
+
"ARG001", "ARG002", "ARG003", "ARG005", # fakes accept arguments they ignore
|
|
129
|
+
"SIM117", # nested with-blocks read better in tests
|
|
130
|
+
"D100", "D101", "D102", "D103", "D104", "D107",
|
|
92
131
|
]
|
|
93
132
|
|
|
94
133
|
[tool.mypy]
|
|
@@ -25,12 +25,18 @@ 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()
|
|
32
33
|
|
|
33
34
|
def _ensure_loaded(self) -> ctypes.CDLL:
|
|
35
|
+
"""Return the opened library, opening it on first call.
|
|
36
|
+
|
|
37
|
+
Raises LibraryNotFoundError on every later call too after a failure:
|
|
38
|
+
the failed load is cached rather than retried.
|
|
39
|
+
"""
|
|
34
40
|
# Double-checked: the unlocked read is the fast path taken by every
|
|
35
41
|
# call after the first, and the repeated check inside the lock is
|
|
36
42
|
# what makes it safe -- two threads can both fall through the first
|
|
@@ -91,6 +97,7 @@ class LazyLibrary:
|
|
|
91
97
|
cache: dict[str, Any] = {}
|
|
92
98
|
|
|
93
99
|
def call(*args: Any) -> Any:
|
|
100
|
+
"""Resolve the C function on first call, then pass straight through."""
|
|
94
101
|
# Resolution happens here, on first call, not at bind time --
|
|
95
102
|
# that is the whole point of this module (see its docstring).
|
|
96
103
|
bound = cache.get("f")
|
|
@@ -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
|