python-libei 0.5.1__tar.gz → 0.5.2__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.
Files changed (34) hide show
  1. {python_libei-0.5.1 → python_libei-0.5.2}/PKG-INFO +13 -28
  2. {python_libei-0.5.1 → python_libei-0.5.2}/README.md +12 -27
  3. {python_libei-0.5.1 → python_libei-0.5.2}/pyproject.toml +33 -2
  4. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/__init__.py +1 -1
  5. python_libei-0.5.2/src/libei/_capi/__init__.py +9 -0
  6. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/ei.py +71 -15
  7. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/eis.py +65 -17
  8. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/oeffis.py +7 -2
  9. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/portal.py +108 -30
  10. {python_libei-0.5.1 → python_libei-0.5.2}/src/python_libei.egg-info/PKG-INFO +13 -28
  11. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_documentation_shape.py +55 -0
  12. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_ei_objects.py +2 -3
  13. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_eis_objects.py +2 -3
  14. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_inputcapture.py +135 -12
  15. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_integration_extras.py +4 -4
  16. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_oeffis.py +3 -3
  17. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_portal.py +50 -7
  18. python_libei-0.5.1/src/libei/_capi/__init__.py +0 -6
  19. {python_libei-0.5.1 → python_libei-0.5.2}/LICENSE +0 -0
  20. {python_libei-0.5.1 → python_libei-0.5.2}/setup.cfg +0 -0
  21. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/_capi/libei.py +0 -0
  22. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/_capi/libeis.py +0 -0
  23. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/_capi/liboeffis.py +0 -0
  24. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/_capi/loader.py +0 -0
  25. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/_cobject.py +0 -0
  26. {python_libei-0.5.1 → python_libei-0.5.2}/src/libei/py.typed +0 -0
  27. {python_libei-0.5.1 → python_libei-0.5.2}/src/python_libei.egg-info/SOURCES.txt +0 -0
  28. {python_libei-0.5.1 → python_libei-0.5.2}/src/python_libei.egg-info/dependency_links.txt +0 -0
  29. {python_libei-0.5.1 → python_libei-0.5.2}/src/python_libei.egg-info/requires.txt +0 -0
  30. {python_libei-0.5.1 → python_libei-0.5.2}/src/python_libei.egg-info/top_level.txt +0 -0
  31. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_cobject.py +0 -0
  32. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_documented_examples.py +0 -0
  33. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_integration_socketpair.py +0 -0
  34. {python_libei-0.5.1 → python_libei-0.5.2}/tests/test_loader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-libei
3
- Version: 0.5.1
3
+ Version: 0.5.2
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
@@ -156,7 +156,7 @@ and why, is in
156
156
 
157
157
  ## Status
158
158
 
159
- Beta (`0.5.1`), published on [PyPI](https://pypi.org/project/python-libei/)
159
+ Beta (`0.5.2`), published on [PyPI](https://pypi.org/project/python-libei/)
160
160
  since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
161
161
 
162
162
  The injection path is exercised end to end against the real libraries by the
@@ -187,11 +187,11 @@ Exactly what was run, when, and against which versions:
187
187
  portal paths are the part likeliest to come up short off Linux, since
188
188
  they need an xdg-desktop-portal RemoteDesktop backend to talk to.
189
189
  - CPython 3.10 or newer (tested on 3.13)
190
- - The native libraries: on Fedora, `sudo dnf install libei libeis liboeffis`;
191
- on FreeBSD, `pkg install libei` (the `x11/libei` port), which supplies all
192
- three sonames including `liboeffis`
193
- - `libei.portal` only: PyGObject (`pip install 'python-libei[portal]'`), plus
194
- whatever GObject-introspection libraries your distro needs for `Gio` --
190
+ - The native `libei`, `libeis` and `liboeffis` libraries, which `pip` cannot
191
+ supply. Which package provides them on your distribution — and what to do
192
+ when the name does not match — is in [docs/install.md](docs/install.md).
193
+ - `libei.portal` only: PyGObject, via the `portal` extra, plus whatever
194
+ GObject-introspection libraries your distribution needs for `Gio`, since
195
195
  PyPI's PyGObject wheel supplies the Python side only. Not needed for
196
196
  `libei.ei`, `libei.eis` or `libei.oeffis`.
197
197
  - libei 1.0.0 or newer for the core: connecting, binding a seat, and
@@ -224,22 +224,11 @@ Exactly what was run, when, and against which versions:
224
224
 
225
225
  ## Install
226
226
 
227
- From [PyPI](https://pypi.org/project/python-libei/):
228
-
229
227
  ```sh
230
228
  pip install python-libei
231
229
  ```
232
230
 
233
- The distribution is named `python-libei`, the import is `libei` -- so
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:
231
+ From a checkout instead, to track `main` or to work on the package:
243
232
 
244
233
  ```sh
245
234
  git clone https://github.com/ctrondlp/python-libei.git
@@ -247,15 +236,11 @@ cd python-libei
247
236
  pip install . # or `pip install -e '.[dev]'` to develop
248
237
  ```
249
238
 
250
- Importing is always safe, even where the native libraries are missing — they
251
- are loaded on first use, not at import. Check before you rely on them:
252
-
253
- ```python
254
- from libei import ei
255
-
256
- if not ei.is_available():
257
- ... # fall back to another input backend
258
- ```
239
+ That is the half `pip` can do. The native `libei`/`libeis`/`liboeffis`
240
+ libraries it cannot supply, the `portal` extra, and how to check what actually
241
+ loaded are all in [docs/install.md](docs/install.md) — see
242
+ [Requirements](#requirements) above for the version floors. Importing is safe
243
+ without any of it: those libraries are loaded on first *use*, not at import.
259
244
 
260
245
  ## Concepts
261
246
 
@@ -119,7 +119,7 @@ and why, is in
119
119
 
120
120
  ## Status
121
121
 
122
- Beta (`0.5.1`), published on [PyPI](https://pypi.org/project/python-libei/)
122
+ Beta (`0.5.2`), published on [PyPI](https://pypi.org/project/python-libei/)
123
123
  since `0.1.0`, and **the API is not frozen** — expect renames before 1.0.
124
124
 
125
125
  The injection path is exercised end to end against the real libraries by the
@@ -150,11 +150,11 @@ Exactly what was run, when, and against which versions:
150
150
  portal paths are the part likeliest to come up short off Linux, since
151
151
  they need an xdg-desktop-portal RemoteDesktop backend to talk to.
152
152
  - CPython 3.10 or newer (tested on 3.13)
153
- - The native libraries: on Fedora, `sudo dnf install libei libeis liboeffis`;
154
- on FreeBSD, `pkg install libei` (the `x11/libei` port), which supplies all
155
- three sonames including `liboeffis`
156
- - `libei.portal` only: PyGObject (`pip install 'python-libei[portal]'`), plus
157
- whatever GObject-introspection libraries your distro needs for `Gio` --
153
+ - The native `libei`, `libeis` and `liboeffis` libraries, which `pip` cannot
154
+ supply. Which package provides them on your distribution — and what to do
155
+ when the name does not match — is in [docs/install.md](docs/install.md).
156
+ - `libei.portal` only: PyGObject, via the `portal` extra, plus whatever
157
+ GObject-introspection libraries your distribution needs for `Gio`, since
158
158
  PyPI's PyGObject wheel supplies the Python side only. Not needed for
159
159
  `libei.ei`, `libei.eis` or `libei.oeffis`.
160
160
  - libei 1.0.0 or newer for the core: connecting, binding a seat, and
@@ -187,22 +187,11 @@ Exactly what was run, when, and against which versions:
187
187
 
188
188
  ## Install
189
189
 
190
- From [PyPI](https://pypi.org/project/python-libei/):
191
-
192
190
  ```sh
193
191
  pip install python-libei
194
192
  ```
195
193
 
196
- The distribution is named `python-libei`, the import is `libei` -- so
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:
194
+ From a checkout instead, to track `main` or to work on the package:
206
195
 
207
196
  ```sh
208
197
  git clone https://github.com/ctrondlp/python-libei.git
@@ -210,15 +199,11 @@ cd python-libei
210
199
  pip install . # or `pip install -e '.[dev]'` to develop
211
200
  ```
212
201
 
213
- Importing is always safe, even where the native libraries are missing — they
214
- are loaded on first use, not at import. Check before you rely on them:
215
-
216
- ```python
217
- from libei import ei
218
-
219
- if not ei.is_available():
220
- ... # fall back to another input backend
221
- ```
202
+ That is the half `pip` can do. The native `libei`/`libeis`/`liboeffis`
203
+ libraries it cannot supply, the `portal` extra, and how to check what actually
204
+ loaded are all in [docs/install.md](docs/install.md) — see
205
+ [Requirements](#requirements) above for the version floors. Importing is safe
206
+ without any of it: those libraries are loaded on first *use*, not at import.
222
207
 
223
208
  ## Concepts
224
209
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-libei"
7
- version = "0.5.1"
7
+ version = "0.5.2"
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"
@@ -87,8 +87,39 @@ select = [
87
87
  "E", "W", # pycodestyle
88
88
  "F", # pyflakes
89
89
  "I", # isort
90
- "UP", # pyupgrade
90
+ "D", # pydocstyle
91
91
  "B", # bugbear
92
+ "UP", # pyupgrade
93
+ "C4", # comprehensions
94
+ "SIM", # simplification
95
+ "RET", # return consistency
96
+ "ARG", # unused arguments
97
+ "C901", # cyclomatic complexity
98
+ ]
99
+ ignore = [
100
+ "D203", # incompatible with D211; keep no blank line before class docstring
101
+ "D213", # incompatible with D212; keep the summary on the first line
102
+ "D401", # properties read better as noun phrases than imperatives
103
+ "D105", # __enter__/__exit__ and friends document themselves
104
+ ]
105
+
106
+ [tool.ruff.lint.mccabe]
107
+ # pyguitest's ceiling; python-libei's own worst case (InputCaptureSession.
108
+ # negotiate(), forking on portal version) sits at 14, just under it. A
109
+ # ceiling on what gets added, not a claim everything else is close to it.
110
+ max-complexity = 15
111
+
112
+ [tool.ruff.lint.pydocstyle]
113
+ convention = "pep257"
114
+
115
+ [tool.ruff.lint.per-file-ignores]
116
+ # Tests use fakes with deliberately unused arguments and multi-with blocks
117
+ # that read better nested; matching pyguitest's/pyguitest-recorder's own
118
+ # per-file-ignores for the same reasons.
119
+ "tests/*" = [
120
+ "ARG001", "ARG002", "ARG003", "ARG005", # fakes accept arguments they ignore
121
+ "SIM117", # nested with-blocks read better in tests
122
+ "D100", "D101", "D102", "D103", "D104", "D107",
92
123
  ]
93
124
 
94
125
  [tool.mypy]
@@ -31,6 +31,6 @@ the full breakdown, including which features need which libei version.
31
31
  Beta: the API is not frozen.
32
32
  """
33
33
 
34
- __version__ = "0.5.1"
34
+ __version__ = "0.5.2"
35
35
 
36
36
  __all__ = ["__version__"]
@@ -0,0 +1,9 @@
1
+ """Low-level ctypes bindings.
2
+
3
+ Not part of the public API -- use ``libei.ei``, ``libei.eis`` and
4
+ ``libei.oeffis`` instead.
5
+ """
6
+
7
+ from . import libei, libeis, liboeffis
8
+
9
+ __all__ = ["libei", "libeis", "liboeffis"]
@@ -87,6 +87,7 @@ class Error(Exception):
87
87
  """
88
88
 
89
89
  def __init__(self, message: str, errno: int | None = None) -> None:
90
+ """Record the failure message and, where libei reported one, errno."""
90
91
  super().__init__(message)
91
92
  self.message = message
92
93
  self.errno = errno
@@ -199,6 +200,12 @@ class _LogPriority(enum.IntEnum):
199
200
 
200
201
  @dataclasses.dataclass(frozen=True, slots=True)
201
202
  class XkbModifiersEvent:
203
+ """XKB modifier state from a KEYBOARD_MODIFIERS event.
204
+
205
+ ``depressed``/``latched``/``locked`` are XKB's own mod-state bitmasks;
206
+ ``group`` is the active keyboard layout group.
207
+ """
208
+
202
209
  depressed: int
203
210
  latched: int
204
211
  locked: int
@@ -207,48 +214,76 @@ class XkbModifiersEvent:
207
214
 
208
215
  @dataclasses.dataclass(frozen=True, slots=True)
209
216
  class KeyEvent:
217
+ """Key code and press state from a KEYBOARD_KEY event.
218
+
219
+ ``key`` is a Linux ``KEY_*`` code, the same numbering
220
+ :meth:`Device.keyboard_key` sends.
221
+ """
222
+
210
223
  key: int
211
224
  is_press: bool
212
225
 
213
226
 
214
227
  @dataclasses.dataclass(frozen=True, slots=True)
215
228
  class ButtonEvent:
229
+ """Button code and press state from a BUTTON_BUTTON event.
230
+
231
+ ``button`` is a Linux ``BTN_*`` code, the same numbering
232
+ :meth:`Device.button` sends.
233
+ """
234
+
216
235
  button: int
217
236
  is_press: bool
218
237
 
219
238
 
220
239
  @dataclasses.dataclass(frozen=True, slots=True)
221
240
  class PointerEvent:
241
+ """Relative motion deltas, in logical pixels, from a POINTER_MOTION event."""
242
+
222
243
  dx: float
223
244
  dy: float
224
245
 
225
246
 
226
247
  @dataclasses.dataclass(frozen=True, slots=True)
227
248
  class PointerAbsoluteEvent:
249
+ """Absolute position from a POINTER_MOTION_ABSOLUTE event.
250
+
251
+ In the logical pixel space of the :class:`Region` the emitting device
252
+ covers -- see :meth:`Event.pointer_absolute_event`.
253
+ """
254
+
228
255
  x: float
229
256
  y: float
230
257
 
231
258
 
232
259
  @dataclasses.dataclass(frozen=True, slots=True)
233
260
  class ScrollEvent:
261
+ """Smooth scroll deltas from a SCROLL_DELTA event."""
262
+
234
263
  dx: float
235
264
  dy: float
236
265
 
237
266
 
238
267
  @dataclasses.dataclass(frozen=True, slots=True)
239
268
  class ScrollDiscreteEvent:
269
+ """Detent scroll deltas (120 per detent) from a SCROLL_DISCRETE event."""
270
+
240
271
  dx: int
241
272
  dy: int
242
273
 
243
274
 
244
275
  @dataclasses.dataclass(frozen=True, slots=True)
245
276
  class ScrollStopEvent:
277
+ """Which axes stopped scrolling, from a SCROLL_STOP/SCROLL_CANCEL event."""
278
+
246
279
  stop_x: bool
247
280
  stop_y: bool
248
281
 
249
282
 
250
283
  @dataclasses.dataclass(frozen=True, slots=True)
251
284
  class TouchEvent:
285
+ """Touch id and position from a TOUCH_DOWN or TOUCH_MOTION event."""
286
+
252
287
  touchid: int
253
288
  x: float
254
289
  y: float
@@ -256,17 +291,26 @@ class TouchEvent:
256
291
 
257
292
  @dataclasses.dataclass(frozen=True, slots=True)
258
293
  class TouchUpEvent:
294
+ """Touch id and cancellation flag from a TOUCH_UP event.
295
+
296
+ See :meth:`Event.touch_up_event` for when ``is_cancel`` is trustworthy.
297
+ """
298
+
259
299
  touchid: int
260
300
  is_cancel: bool
261
301
 
262
302
 
263
303
  @dataclasses.dataclass(frozen=True, slots=True)
264
304
  class TextUtf8Event:
305
+ """UTF-8 text carried by a TEXT_UTF8 event."""
306
+
265
307
  text: str
266
308
 
267
309
 
268
310
  @dataclasses.dataclass(frozen=True, slots=True)
269
311
  class TextKeysymEvent:
312
+ """Keysym and press state from a TEXT_KEYSYM event."""
313
+
270
314
  keysym: int
271
315
  is_press: bool
272
316
 
@@ -545,9 +589,10 @@ class Device(CObject):
545
589
  return self
546
590
 
547
591
  def frame(self, timestamp: int | None = None) -> Device:
548
- """Commit the events queued since the last frame as one logical
549
- hardware event. ``timestamp`` defaults to the context's current
550
- time."""
592
+ """Commit the events queued since the last frame as one logical hardware event.
593
+
594
+ ``timestamp`` defaults to the context's current time.
595
+ """
551
596
  if timestamp is None:
552
597
  timestamp = _capi.libei.now(_capi.libei.device_get_context(self))
553
598
  _capi.libei.device_frame(self, timestamp)
@@ -564,8 +609,10 @@ class Device(CObject):
564
609
  return self
565
610
 
566
611
  def button(self, button: int, is_press: bool) -> Device:
567
- """Queue a button press or release. ``button`` is a Linux
568
- ``BTN_*`` code (e.g. ``0x110`` for ``BTN_LEFT``)."""
612
+ """Queue a button press or release.
613
+
614
+ ``button`` is a Linux ``BTN_*`` code (e.g. ``0x110`` for ``BTN_LEFT``).
615
+ """
569
616
  _capi.libei.device_button_button(self, button, is_press)
570
617
  return self
571
618
 
@@ -760,8 +807,11 @@ class Event(CObject):
760
807
 
761
808
  @property
762
809
  def event_type(self) -> EventType | int:
763
- """The event's type, or a raw int for a value newer than this
764
- package's :class:`EventType` table -- see its docstring."""
810
+ """The event's type.
811
+
812
+ Returns a raw int for a value newer than this package's
813
+ :class:`EventType` table -- see its docstring.
814
+ """
765
815
  raw = _capi.libei.event_get_type(self)
766
816
  try:
767
817
  return EventType(raw)
@@ -1009,11 +1059,14 @@ class Context(CObject):
1009
1059
  _wrappable = False
1010
1060
 
1011
1061
  def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
1012
- # _adopt is accepted and forwarded for signature consistency with
1013
- # CObject, but with _wrappable = False, _get_or_create() never
1014
- # actually reaches this constructor -- Context (and Sender/
1015
- # Receiver) are always built directly via cls(cls._new()) in
1016
- # create_for_fd()/create_for_socket().
1062
+ """Wrap a freshly created ``struct ei *`` and arm its log handler.
1063
+
1064
+ _adopt is accepted and forwarded for signature consistency with
1065
+ CObject, but with _wrappable = False, _get_or_create() never
1066
+ actually reaches this constructor -- Context (and Sender/
1067
+ Receiver) are always built directly via cls(cls._new()) in
1068
+ create_for_fd()/create_for_socket().
1069
+ """
1017
1070
  super().__init__(pointer, _adopt=_adopt)
1018
1071
  self._name: str | None = None
1019
1072
  _capi.libei.log_set_handler(self, _log_handler)
@@ -1081,7 +1134,8 @@ class Context(CObject):
1081
1134
  """Use an already-connected socket as the transport.
1082
1135
 
1083
1136
  libei takes ownership of a raw int fd and closes it itself; a file
1084
- object is duplicated first, so the caller's own object stays valid."""
1137
+ object is duplicated first, so the caller's own object stays valid.
1138
+ """
1085
1139
  # ei_setup_backend_fd() takes ownership of the fd and will close it
1086
1140
  # itself. A raw int is assumed to already be one the caller is
1087
1141
  # handing off (matching what eis.Eis.add_client()/oeffis.eis_fd
@@ -1099,7 +1153,8 @@ class Context(CObject):
1099
1153
  """Connect to an EIS socket by path.
1100
1154
 
1101
1155
  ``None`` uses ``$LIBEI_SOCKET``; a relative path is resolved
1102
- against ``$XDG_RUNTIME_DIR``."""
1156
+ against ``$XDG_RUNTIME_DIR``.
1157
+ """
1103
1158
  encoded = os.fspath(path).encode("utf-8") if path else None
1104
1159
  err = _capi.libei.setup_backend_socket(self, encoded)
1105
1160
  if err < 0:
@@ -1162,7 +1217,8 @@ class Context(CObject):
1162
1217
  """Read from the connection and queue any events that arrive.
1163
1218
 
1164
1219
  Call this before iterating :attr:`events`, which only drains what
1165
- is already queued."""
1220
+ is already queued.
1221
+ """
1166
1222
  _capi.libei.dispatch(self)
1167
1223
 
1168
1224
 
@@ -55,6 +55,7 @@ class Error(Exception):
55
55
  """
56
56
 
57
57
  def __init__(self, message: str, errno: int | None = None) -> None:
58
+ """Record the failure message and, where libeis reported one, errno."""
58
59
  super().__init__(message)
59
60
  self.message = message
60
61
  self.errno = errno
@@ -166,48 +167,75 @@ class _LogPriority(enum.IntEnum):
166
167
 
167
168
  @dataclasses.dataclass(frozen=True, slots=True)
168
169
  class KeyEvent:
170
+ """Key code and press state received on a KEYBOARD_KEY event.
171
+
172
+ ``key`` is a Linux ``KEY_*`` code, as sent by the client's
173
+ ``ei.Device.keyboard_key``.
174
+ """
175
+
169
176
  key: int
170
177
  is_press: bool
171
178
 
172
179
 
173
180
  @dataclasses.dataclass(frozen=True, slots=True)
174
181
  class ButtonEvent:
182
+ """Button code and press state received on a BUTTON_BUTTON event.
183
+
184
+ ``button`` is a Linux ``BTN_*`` code, as sent by the client's
185
+ ``ei.Device.button``.
186
+ """
187
+
175
188
  button: int
176
189
  is_press: bool
177
190
 
178
191
 
179
192
  @dataclasses.dataclass(frozen=True, slots=True)
180
193
  class PointerEvent:
194
+ """Relative motion deltas, in logical pixels, from a POINTER_MOTION event."""
195
+
181
196
  dx: float
182
197
  dy: float
183
198
 
184
199
 
185
200
  @dataclasses.dataclass(frozen=True, slots=True)
186
201
  class PointerAbsoluteEvent:
202
+ """Absolute position from a POINTER_MOTION_ABSOLUTE event.
203
+
204
+ In the logical pixel space of the region the sending device covers.
205
+ """
206
+
187
207
  x: float
188
208
  y: float
189
209
 
190
210
 
191
211
  @dataclasses.dataclass(frozen=True, slots=True)
192
212
  class ScrollEvent:
213
+ """Smooth scroll deltas from a SCROLL_DELTA event."""
214
+
193
215
  dx: float
194
216
  dy: float
195
217
 
196
218
 
197
219
  @dataclasses.dataclass(frozen=True, slots=True)
198
220
  class ScrollDiscreteEvent:
221
+ """Detent scroll deltas (120 per detent) from a SCROLL_DISCRETE event."""
222
+
199
223
  dx: int
200
224
  dy: int
201
225
 
202
226
 
203
227
  @dataclasses.dataclass(frozen=True, slots=True)
204
228
  class ScrollStopEvent:
229
+ """Which axes stopped scrolling, from a SCROLL_STOP/SCROLL_CANCEL event."""
230
+
205
231
  stop_x: bool
206
232
  stop_y: bool
207
233
 
208
234
 
209
235
  @dataclasses.dataclass(frozen=True, slots=True)
210
236
  class TouchEvent:
237
+ """Touch id and position from a TOUCH_DOWN or TOUCH_MOTION event."""
238
+
211
239
  touchid: int
212
240
  x: float
213
241
  y: float
@@ -215,17 +243,23 @@ class TouchEvent:
215
243
 
216
244
  @dataclasses.dataclass(frozen=True, slots=True)
217
245
  class TouchUpEvent:
246
+ """Touch id and cancellation flag from a TOUCH_UP event."""
247
+
218
248
  touchid: int
219
249
  is_cancel: bool
220
250
 
221
251
 
222
252
  @dataclasses.dataclass(frozen=True, slots=True)
223
253
  class TextUtf8Event:
254
+ """UTF-8 text carried by a TEXT_UTF8 event."""
255
+
224
256
  text: str
225
257
 
226
258
 
227
259
  @dataclasses.dataclass(frozen=True, slots=True)
228
260
  class TextKeysymEvent:
261
+ """Keysym and press state from a TEXT_KEYSYM event."""
262
+
229
263
  keysym: int
230
264
  is_press: bool
231
265
 
@@ -576,8 +610,10 @@ class Device(CObject):
576
610
  return self
577
611
 
578
612
  def frame(self, timestamp: int | None = None) -> Device:
579
- """Commit the events queued since the last frame as one logical
580
- hardware event. ``timestamp`` defaults to the context's current time."""
613
+ """Commit the events queued since the last frame as one logical hardware event.
614
+
615
+ ``timestamp`` defaults to the context's current time.
616
+ """
581
617
  if timestamp is None:
582
618
  timestamp = _capi.libeis.now(_capi.libeis.device_get_context(self))
583
619
  _capi.libeis.device_frame(self, timestamp)
@@ -837,8 +873,11 @@ class Event(CObject):
837
873
 
838
874
  @property
839
875
  def event_type(self) -> EventType | int:
840
- """The event's type, or a raw int for a value newer than this
841
- package's :class:`EventType` table -- see its docstring."""
876
+ """The event's type.
877
+
878
+ Returns a raw int for a value newer than this package's
879
+ :class:`EventType` table -- see its docstring.
880
+ """
842
881
  raw = _capi.libeis.event_get_type(self)
843
882
  try:
844
883
  return EventType(raw)
@@ -1069,10 +1108,13 @@ class Eis(CObject):
1069
1108
  _wrappable = False
1070
1109
 
1071
1110
  def __init__(self, pointer: int, *, _adopt: bool = False) -> None:
1072
- # _adopt is accepted and forwarded for signature consistency with
1073
- # CObject, but with _wrappable = False, _get_or_create() never
1074
- # actually reaches this constructor -- Eis is always built directly
1075
- # via cls(cls._new()) in create_for_fd().
1111
+ """Wrap a freshly created ``struct eis *`` and arm its log handler.
1112
+
1113
+ _adopt is accepted and forwarded for signature consistency with
1114
+ CObject, but with _wrappable = False, _get_or_create() never
1115
+ actually reaches this constructor -- Eis is always built directly
1116
+ via cls(cls._new()) in create_for_fd().
1117
+ """
1076
1118
  super().__init__(pointer, _adopt=_adopt)
1077
1119
  _capi.libeis.log_set_handler(self, _log_handler)
1078
1120
  _capi.libeis.log_set_priority(self, _LogPriority.DEBUG)
@@ -1146,7 +1188,8 @@ class Eis(CObject):
1146
1188
  """Read from the connection and queue any events that arrive.
1147
1189
 
1148
1190
  Call this before iterating :attr:`events`, which only drains what
1149
- is already queued."""
1191
+ is already queued.
1192
+ """
1150
1193
  _capi.libeis.dispatch(self)
1151
1194
 
1152
1195
  def add_client(self) -> int:
@@ -1172,14 +1215,16 @@ class Eis(CObject):
1172
1215
 
1173
1216
  @classmethod
1174
1217
  def create_for_fd(cls, flags: Sequence[Flag] = ()) -> Eis:
1175
- """Create a server using the fd backend -- the one real compositors
1176
- use, since it keeps each client's fd private rather than exposing a
1177
- connectable socket path. Call :meth:`add_client` once per
1178
- connection you want to accept.
1218
+ """Create a server using the fd backend.
1219
+
1220
+ The one real compositors use, since it keeps each client's fd
1221
+ private rather than exposing a connectable socket path. Call
1222
+ :meth:`add_client` once per connection you want to accept.
1179
1223
 
1180
1224
  ``flags`` are applied here rather than left to the caller because
1181
1225
  :meth:`set_flag` has to run before the backend is set up, and this
1182
- method does both."""
1226
+ method does both.
1227
+ """
1183
1228
  server = cls(cls._new())
1184
1229
  for flag in flags:
1185
1230
  server.set_flag(flag)
@@ -1190,9 +1235,12 @@ class Eis(CObject):
1190
1235
 
1191
1236
  @classmethod
1192
1237
  def create_for_socket(cls, path: Path, flags: Sequence[Flag] = ()) -> Eis:
1193
- """Create a server listening on a Unix socket, as a compositor
1194
- would (this is the path a real ``ei_setup_backend_socket()`` client
1195
- connects to). See :meth:`create_for_fd` on ``flags``."""
1238
+ """Create a server listening on a Unix socket.
1239
+
1240
+ As a compositor would (this is the path a real
1241
+ ``ei_setup_backend_socket()`` client connects to). See
1242
+ :meth:`create_for_fd` on ``flags``.
1243
+ """
1196
1244
  server = cls(cls._new())
1197
1245
  for flag in flags:
1198
1246
  server.set_flag(flag)
@@ -1,5 +1,7 @@
1
- """Pythonic wrapper around liboeffis -- negotiates an EIS connection through
2
- the ``org.freedesktop.portal.RemoteDesktop`` XDG desktop portal.
1
+ """Pythonic wrapper around liboeffis.
2
+
3
+ Negotiates an EIS connection through the
4
+ ``org.freedesktop.portal.RemoteDesktop`` XDG desktop portal.
3
5
 
4
6
  This is the path a sandboxed or otherwise non-privileged client uses to get
5
7
  an EI socket: it asks the portal, the user is shown a consent dialog, and on
@@ -52,6 +54,7 @@ class DisconnectedError(Exception):
52
54
  """The portal session ended unexpectedly (error, or denied by the user)."""
53
55
 
54
56
  def __init__(self, message: str | None) -> None:
57
+ """Record why the session ended."""
55
58
  super().__init__(message)
56
59
  self.message = message
57
60
 
@@ -60,6 +63,7 @@ class SessionClosedError(DisconnectedError):
60
63
  """The portal explicitly closed the session (not necessarily an error)."""
61
64
 
62
65
  def __init__(self) -> None:
66
+ """Build the fixed "Session closed" message."""
63
67
  super().__init__(message="Session closed")
64
68
 
65
69
 
@@ -100,6 +104,7 @@ class Oeffis:
100
104
  """
101
105
 
102
106
  def __init__(self) -> None:
107
+ """Create the underlying liboeffis context (no portal call yet)."""
103
108
  pointer = _capi.liboeffis.new(None)
104
109
  if not pointer:
105
110
  raise DisconnectedError("oeffis_new() returned NULL")