0001-gi.events._Selector.get_map-look-up-file-objects-by-.patch 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. From 3bb03bc5d095cf51c8605521fcc2ce8cad36995f Mon Sep 17 00:00:00 2001
  2. From: Fiona Klute <fiona.klute@gmx.de>
  3. Date: Tue, 25 Mar 2025 13:29:04 +0100
  4. Subject: [PATCH] gi.events._Selector.get_map(): look up file objects by file
  5. descriptor
  6. File objects and their underlying file descriptors must be treated as
  7. equivalent for lookup, or one may incorrectly be treated as not
  8. registered when the other was used for the registration, leading to
  9. bugs.
  10. Signed-off-by: Fiona Klute <fiona.klute@gmx.de>
  11. Upstream: https://gitlab.gnome.org/GNOME/pygobject/-/commit/3bb03bc5d095cf51c8605521fcc2ce8cad36995f
  12. ---
  13. gi/events.py | 36 +++++++++++++++++++++-----
  14. tests/test_events.py | 61 ++++++++++++++++++++++++++++++++++++++++++++
  15. 2 files changed, 90 insertions(+), 7 deletions(-)
  16. diff --git a/gi/events.py b/gi/events.py
  17. index 9f00059ec..980cae0fe 100644
  18. --- a/gi/events.py
  19. +++ b/gi/events.py
  20. @@ -28,6 +28,7 @@ import threading
  21. import selectors
  22. import weakref
  23. import warnings
  24. +from collections.abc import Mapping
  25. from contextlib import contextmanager
  26. from . import _ossighelper
  27. @@ -381,9 +382,33 @@ if sys.platform != 'win32':
  28. # Subclass to attach _tag
  29. pass
  30. + class _FileObjectMapping(Mapping):
  31. + def __init__(self, fd_dict):
  32. + self.fd_dict = fd_dict
  33. +
  34. + def __len__(self):
  35. + return len(self.fd_dict)
  36. +
  37. + def get(self, fileobj, default=None):
  38. + fd = _fileobj_to_fd(fileobj)
  39. + return self.fd_dict.get(fd, default)
  40. +
  41. + def __getitem__(self, fileobj):
  42. + value = self.get(fileobj)
  43. + if value is None:
  44. + raise KeyError("{!r} is not registered".format(fileobj))
  45. + return value
  46. +
  47. + def __iter__(self):
  48. + return iter(self.fd_dict)
  49. +
  50. class _Selector(_SelectorMixin, selectors.BaseSelector):
  51. """A Selector for gi.events.GLibEventLoop registering python IO with GLib."""
  52. + def __init__(self, context, loop):
  53. + super().__init__(context, loop)
  54. + self._map = _FileObjectMapping(self._fd_to_key)
  55. +
  56. def attach(self):
  57. self._source.attach(self._loop._context)
  58. @@ -432,15 +457,12 @@ if sys.platform != 'win32':
  59. # We could override modify, but it is only slightly when the "events" change.
  60. def get_key(self, fileobj):
  61. - fd = _fileobj_to_fd(fileobj)
  62. - return self._fd_to_key[fd]
  63. + return self._map[fileobj]
  64. def get_map(self):
  65. - """Return a mapping of file objects to selector keys."""
  66. - # Horribly inefficient
  67. - # It should never be called and exists just to prevent issues if e.g.
  68. - # python decides to use it for debug purposes.
  69. - return {k.fileobj: k for k in self._fd_to_key.values()}
  70. + """Return a mapping of file objects or file descriptors to
  71. + selector keys."""
  72. + return self._map
  73. else:
  74. diff --git a/tests/test_events.py b/tests/test_events.py
  75. index 81409abae..c075af095 100644
  76. --- a/tests/test_events.py
  77. +++ b/tests/test_events.py
  78. @@ -45,6 +45,7 @@ import sys
  79. import gi
  80. import gi.events
  81. import asyncio
  82. +import socket
  83. import threading
  84. from gi.repository import GLib
  85. @@ -262,3 +263,63 @@ class GLibEventLoopPolicyTests(unittest.TestCase):
  86. GLib.MainLoop().run()
  87. loop.close()
  88. +
  89. + @unittest.skipIf(sys.platform == 'win32', 'add reader/writer not implemented')
  90. + def test_source_fileobj_fd(self):
  91. + """Regression test for
  92. + https://gitlab.gnome.org/GNOME/pygobject/-/issues/689
  93. + """
  94. + class Echo:
  95. + def __init__(self, sock, expect_bytes):
  96. + self.sock = sock
  97. + self.sent_bytes = 0
  98. + self.expect_bytes = expect_bytes
  99. + self.done = asyncio.Future()
  100. + self.data = bytes()
  101. +
  102. + def send(self):
  103. + if self.done.done():
  104. + return
  105. + if self.sent_bytes < len(self.data):
  106. + self.sent_bytes += self.sock.send(
  107. + self.data[self.sent_bytes:])
  108. + print('sent', self.data)
  109. + if self.sent_bytes >= self.expect_bytes:
  110. + self.done.set_result(None)
  111. + self.sock.shutdown(socket.SHUT_WR)
  112. +
  113. + def recv(self):
  114. + if self.done.done():
  115. + return
  116. + self.data += self.sock.recv(self.expect_bytes)
  117. + print('received', self.data)
  118. + if len(self.data) >= self.expect_bytes:
  119. + self.sock.shutdown(socket.SHUT_RD)
  120. +
  121. + async def run():
  122. + loop = asyncio.get_running_loop()
  123. + s1, s2 = socket.socketpair()
  124. + sample = b'Hello!'
  125. + e = Echo(s1, len(sample))
  126. + # register using file object and file descriptor
  127. + loop.add_reader(s1, e.recv)
  128. + loop.add_writer(s1.fileno(), e.send)
  129. + s2.sendall(sample)
  130. + await asyncio.wait_for(e.done, timeout=2.0)
  131. + echo = bytes()
  132. + for _ in range(len(sample)):
  133. + echo += s2.recv(len(sample))
  134. + if len(echo) == len(sample):
  135. + break
  136. + # remove using file object and file descriptor
  137. + loop.remove_reader(s1)
  138. + loop.remove_writer(s1.fileno())
  139. + s1.close()
  140. + s2.close()
  141. + # check if the data was echoed correctly
  142. + self.assertEqual(sample, echo)
  143. +
  144. + policy = self.create_policy()
  145. + loop = policy.get_event_loop()
  146. + loop.run_until_complete(run())
  147. + loop.close()
  148. --
  149. GitLab