curl-cffi-patch 0.15.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.
Files changed (70) hide show
  1. curl_cffi_patch-0.15.1/LICENSE +23 -0
  2. curl_cffi_patch-0.15.1/MANIFEST.in +7 -0
  3. curl_cffi_patch-0.15.1/Makefile +70 -0
  4. curl_cffi_patch-0.15.1/PKG-INFO +64 -0
  5. curl_cffi_patch-0.15.1/README.md +5 -0
  6. curl_cffi_patch-0.15.1/curl_cffi/__init__.py +96 -0
  7. curl_cffi_patch-0.15.1/curl_cffi/__version__.py +8 -0
  8. curl_cffi_patch-0.15.1/curl_cffi/_asyncio_selector.py +344 -0
  9. curl_cffi_patch-0.15.1/curl_cffi/aio.py +343 -0
  10. curl_cffi_patch-0.15.1/curl_cffi/cli.py +32 -0
  11. curl_cffi_patch-0.15.1/curl_cffi/const.py +613 -0
  12. curl_cffi_patch-0.15.1/curl_cffi/curl.py +693 -0
  13. curl_cffi_patch-0.15.1/curl_cffi/py.typed +1 -0
  14. curl_cffi_patch-0.15.1/curl_cffi/requests/__init__.py +171 -0
  15. curl_cffi_patch-0.15.1/curl_cffi/requests/cookies.py +364 -0
  16. curl_cffi_patch-0.15.1/curl_cffi/requests/errors.py +7 -0
  17. curl_cffi_patch-0.15.1/curl_cffi/requests/exceptions.py +227 -0
  18. curl_cffi_patch-0.15.1/curl_cffi/requests/headers.py +347 -0
  19. curl_cffi_patch-0.15.1/curl_cffi/requests/impersonate.py +441 -0
  20. curl_cffi_patch-0.15.1/curl_cffi/requests/models.py +325 -0
  21. curl_cffi_patch-0.15.1/curl_cffi/requests/session.py +1187 -0
  22. curl_cffi_patch-0.15.1/curl_cffi/requests/utils.py +699 -0
  23. curl_cffi_patch-0.15.1/curl_cffi/requests/websockets.py +1432 -0
  24. curl_cffi_patch-0.15.1/curl_cffi/utils.py +16 -0
  25. curl_cffi_patch-0.15.1/curl_cffi_patch.egg-info/PKG-INFO +64 -0
  26. curl_cffi_patch-0.15.1/curl_cffi_patch.egg-info/SOURCES.txt +68 -0
  27. curl_cffi_patch-0.15.1/curl_cffi_patch.egg-info/dependency_links.txt +1 -0
  28. curl_cffi_patch-0.15.1/curl_cffi_patch.egg-info/entry_points.txt +2 -0
  29. curl_cffi_patch-0.15.1/curl_cffi_patch.egg-info/requires.txt +43 -0
  30. curl_cffi_patch-0.15.1/curl_cffi_patch.egg-info/top_level.txt +1 -0
  31. curl_cffi_patch-0.15.1/ffi/cdef.c +76 -0
  32. curl_cffi_patch-0.15.1/ffi/shim.c +18 -0
  33. curl_cffi_patch-0.15.1/ffi/shim.h +7 -0
  34. curl_cffi_patch-0.15.1/include/curl/Makefile.am +43 -0
  35. curl_cffi_patch-0.15.1/include/curl/Makefile.in +662 -0
  36. curl_cffi_patch-0.15.1/include/curl/curl.h +3423 -0
  37. curl_cffi_patch-0.15.1/include/curl/curlver.h +79 -0
  38. curl_cffi_patch-0.15.1/include/curl/easy.h +136 -0
  39. curl_cffi_patch-0.15.1/include/curl/header.h +74 -0
  40. curl_cffi_patch-0.15.1/include/curl/mprintf.h +85 -0
  41. curl_cffi_patch-0.15.1/include/curl/multi.h +481 -0
  42. curl_cffi_patch-0.15.1/include/curl/options.h +70 -0
  43. curl_cffi_patch-0.15.1/include/curl/stdcheaders.h +35 -0
  44. curl_cffi_patch-0.15.1/include/curl/system.h +402 -0
  45. curl_cffi_patch-0.15.1/include/curl/typecheck-gcc.h +867 -0
  46. curl_cffi_patch-0.15.1/include/curl/urlapi.h +155 -0
  47. curl_cffi_patch-0.15.1/include/curl/websockets.h +85 -0
  48. curl_cffi_patch-0.15.1/libs.json +135 -0
  49. curl_cffi_patch-0.15.1/pyproject.toml +157 -0
  50. curl_cffi_patch-0.15.1/scripts/build.py +177 -0
  51. curl_cffi_patch-0.15.1/setup.cfg +4 -0
  52. curl_cffi_patch-0.15.1/setup.py +22 -0
  53. curl_cffi_patch-0.15.1/tests/integration/test_fingerprints.py +36 -0
  54. curl_cffi_patch-0.15.1/tests/integration/test_httpbin.py +72 -0
  55. curl_cffi_patch-0.15.1/tests/integration/test_real_world.py +8 -0
  56. curl_cffi_patch-0.15.1/tests/integration/test_response_class.py +31 -0
  57. curl_cffi_patch-0.15.1/tests/threads/test_eventlet.py +35 -0
  58. curl_cffi_patch-0.15.1/tests/threads/test_gevent.py +35 -0
  59. curl_cffi_patch-0.15.1/tests/unittest/conftest.py +790 -0
  60. curl_cffi_patch-0.15.1/tests/unittest/test_async.py +30 -0
  61. curl_cffi_patch-0.15.1/tests/unittest/test_async_session.py +481 -0
  62. curl_cffi_patch-0.15.1/tests/unittest/test_cli.py +13 -0
  63. curl_cffi_patch-0.15.1/tests/unittest/test_cookies.py +67 -0
  64. curl_cffi_patch-0.15.1/tests/unittest/test_curl.py +350 -0
  65. curl_cffi_patch-0.15.1/tests/unittest/test_headers.py +60 -0
  66. curl_cffi_patch-0.15.1/tests/unittest/test_impersonate.py +251 -0
  67. curl_cffi_patch-0.15.1/tests/unittest/test_requests.py +1004 -0
  68. curl_cffi_patch-0.15.1/tests/unittest/test_smoke.py +26 -0
  69. curl_cffi_patch-0.15.1/tests/unittest/test_upload.py +102 -0
  70. curl_cffi_patch-0.15.1/tests/unittest/test_websockets.py +120 -0
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+
4
+ Copyright (c) 2018 multippt
5
+ Copyright (c) 2022 curl_cffi developers
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ recursive-include tests *
2
+
3
+ include ffi/*
4
+ include include/curl/*
5
+ include scripts/build.py
6
+ include Makefile
7
+ include libs.json
@@ -0,0 +1,70 @@
1
+ .ONESHELL:
2
+ SHELL := bash
3
+
4
+ # this is the upstream libcurl-impersonate version
5
+ VERSION := 1.2.5
6
+ CURL_VERSION := curl-8_15_0
7
+
8
+ $(CURL_VERSION):
9
+ curl -L https://github.com/curl/curl/archive/$(CURL_VERSION).zip -o curl.zip
10
+ unzip -q -o curl.zip
11
+ mv curl-$(CURL_VERSION) $(CURL_VERSION)
12
+
13
+ curl-impersonate-$(VERSION)/patches: $(CURL_VERSION)
14
+ curl -L "https://github.com/lexiforest/curl-impersonate/archive/refs/tags/v$(VERSION).tar.gz" \
15
+ -o "curl-impersonate-$(VERSION).tar.gz"
16
+ tar -xf curl-impersonate-$(VERSION).tar.gz
17
+
18
+ .preprocessed: curl-impersonate-$(VERSION)/patches
19
+ cd $(CURL_VERSION)
20
+ # for p in $</curl*.patch; do patch -p1 < ../$$p; done
21
+ patch -p1 < ../$</curl.patch
22
+ # Re-generate the configure script
23
+ autoreconf -fi
24
+ mkdir -p ../include/curl
25
+ cp -R include/curl/* ../include/curl/
26
+ # Sentinel files: https://tech.davis-hansson.com/p/make/
27
+ cd ..
28
+ touch .preprocessed
29
+
30
+ local-curl: $(CURL_VERSION)
31
+ cp /usr/local/lib/libcurl-impersonate* /Users/runner/work/_temp/install/lib/
32
+ cd $(CURL_VERSION)
33
+ for p in ../curl-impersonate/patches/curl*.patch; do patch -p1 < ../$$p; done
34
+ # Re-generate the configure script
35
+ autoreconf -fi
36
+ mkdir -p ../include/curl
37
+ cp -R include/curl/* ../include/curl/
38
+ # Sentinel files: https://tech.davis-hansson.com/p/make/
39
+ touch .preprocessed
40
+
41
+ gen-const:
42
+ python scripts/generate_consts.py $(CURL_VERSION)
43
+
44
+ preprocess: .preprocessed
45
+ @echo generating patched libcurl header files
46
+
47
+ install-editable:
48
+ pip install -e .
49
+
50
+ build: .preprocessed
51
+ rm -rf dist/
52
+ pip install build
53
+ python -m build --wheel
54
+
55
+ lint:
56
+ ruff check --exclude issues
57
+
58
+ format:
59
+ ruff format --exclude issues
60
+
61
+ test:
62
+ python -bb -m pytest tests/unittest
63
+
64
+ clean:
65
+ rm -rf build/ dist/ curl_cffi.egg-info/ $(CURL_VERSION)/ curl-impersonate-$(VERSION)/
66
+ rm -rf curl_cffi/*.o curl_cffi/*.so curl_cffi/_wrapper.c
67
+ rm -rf .preprocessed $(CURL_VERSION).tar.xz curl-impersonate-$(VERSION).tar.gz
68
+ rm -rf include/
69
+
70
+ .PHONY: clean build test install-editable preprocess gen-const
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: curl_cffi_patch
3
+ Version: 0.15.1
4
+ Summary: libcurl ffi bindings for Python, with impersonation support.
5
+ Author-email: lexiforest <infinitesheldon@gmail.com>
6
+ License: MIT License
7
+ Project-URL: repository, https://github.com/Ehsan-U/curl-cffi-patch
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: cffi>=1.12.0
20
+ Requires-Dist: certifi>=2024.2.2
21
+ Provides-Extra: extra
22
+ Requires-Dist: readability-lxml>=0.8.1; extra == "extra"
23
+ Requires-Dist: markdownify>=1.1.0; extra == "extra"
24
+ Requires-Dist: lxml_html_clean; extra == "extra"
25
+ Provides-Extra: dev
26
+ Requires-Dist: charset_normalizer<4.0,>=3.3.2; extra == "dev"
27
+ Requires-Dist: coverage<7.0,>=6.4.1; extra == "dev"
28
+ Requires-Dist: cryptography<43.0,>=42.0.5; extra == "dev"
29
+ Requires-Dist: httpx==0.23.1; extra == "dev"
30
+ Requires-Dist: mypy<2.0,>=1.9.0; extra == "dev"
31
+ Requires-Dist: pytest<9.0,>=8.1.1; extra == "dev"
32
+ Requires-Dist: pytest-asyncio<1.0,>=0.23.6; extra == "dev"
33
+ Requires-Dist: pytest-trio<1.0,>=0.8.0; extra == "dev"
34
+ Requires-Dist: ruff<1.0,>=0.3.5; extra == "dev"
35
+ Requires-Dist: trio<1.0,>=0.25.0; extra == "dev"
36
+ Requires-Dist: trustme<2.0,>=1.1.0; extra == "dev"
37
+ Requires-Dist: uvicorn<1.0,>=0.29.0; extra == "dev"
38
+ Requires-Dist: websockets>=14.0; extra == "dev"
39
+ Requires-Dist: typing_extensions; extra == "dev"
40
+ Provides-Extra: build
41
+ Requires-Dist: cibuildwheel; extra == "build"
42
+ Requires-Dist: wheel; extra == "build"
43
+ Provides-Extra: test
44
+ Requires-Dist: charset_normalizer<4.0,>=3.3.2; extra == "test"
45
+ Requires-Dist: cryptography<43.0,>=42.0.5; extra == "test"
46
+ Requires-Dist: fastapi<1.0,>=0.110.0; extra == "test"
47
+ Requires-Dist: httpx==0.23.1; extra == "test"
48
+ Requires-Dist: proxy.py<3.0,>=2.4.3; extra == "test"
49
+ Requires-Dist: pytest<9.0,>=8.1.1; extra == "test"
50
+ Requires-Dist: pytest-asyncio<1.0,>=0.23.6; extra == "test"
51
+ Requires-Dist: pytest-trio<1.0,>=0.8.0; extra == "test"
52
+ Requires-Dist: python-multipart<1.0,>=0.0.9; extra == "test"
53
+ Requires-Dist: trio<1.0,>=0.25.0; extra == "test"
54
+ Requires-Dist: trustme<2.0,>=1.1.0; extra == "test"
55
+ Requires-Dist: uvicorn<1.0,>=0.29.0; extra == "test"
56
+ Requires-Dist: websockets>=14.0; extra == "test"
57
+ Requires-Dist: typing_extensions; extra == "test"
58
+ Dynamic: license-file
59
+
60
+ # curl_cffi_patch
61
+
62
+ A patched version of [curl_cffi](https://github.com/lexiforest/curl_cffi) that fixes a memory leak.
63
+
64
+ See [issue #677](https://github.com/lexiforest/curl_cffi/issues/677) for details.
@@ -0,0 +1,5 @@
1
+ # curl_cffi_patch
2
+
3
+ A patched version of [curl_cffi](https://github.com/lexiforest/curl_cffi) that fixes a memory leak.
4
+
5
+ See [issue #677](https://github.com/lexiforest/curl_cffi/issues/677) for details.
@@ -0,0 +1,96 @@
1
+ __all__ = [
2
+ "Curl",
3
+ "AsyncCurl",
4
+ "CurlMime",
5
+ "CurlError",
6
+ "CurlInfo",
7
+ "CurlOpt",
8
+ "CurlMOpt",
9
+ "CurlECode",
10
+ "CurlHttpVersion",
11
+ "CurlSslVersion",
12
+ "CurlWsFlag",
13
+ "config_warnings",
14
+ "ffi",
15
+ "is_pro",
16
+ "lib",
17
+ "Session",
18
+ "AsyncSession",
19
+ "BrowserType",
20
+ "BrowserTypeLiteral",
21
+ "request",
22
+ "head",
23
+ "get",
24
+ "post",
25
+ "put",
26
+ "patch",
27
+ "delete",
28
+ "options",
29
+ "Cookies",
30
+ "Headers",
31
+ "Request",
32
+ "Response",
33
+ "AsyncWebSocket",
34
+ "WebSocket",
35
+ "WebSocketError",
36
+ "WebSocketClosed",
37
+ "WebSocketTimeout",
38
+ "WsCloseCode",
39
+ "ExtraFingerprints",
40
+ "CookieTypes",
41
+ "HeaderTypes",
42
+ "ProxySpec",
43
+ "exceptions",
44
+ ]
45
+
46
+ import _cffi_backend # noqa: F401 # required by _wrapper
47
+
48
+ from .__version__ import __curl_version__, __description__, __title__, __version__ # noqa: F401
49
+
50
+ # This line includes _wrapper.so into the wheel
51
+ from ._wrapper import ffi, lib
52
+ from .aio import AsyncCurl
53
+ from .const import (
54
+ CurlECode,
55
+ CurlHttpVersion,
56
+ CurlInfo,
57
+ CurlMOpt,
58
+ CurlOpt,
59
+ CurlSslVersion,
60
+ CurlWsFlag,
61
+ )
62
+ from .curl import Curl, CurlError, CurlMime
63
+
64
+ from .requests import (
65
+ AsyncSession,
66
+ AsyncWebSocket,
67
+ BrowserType,
68
+ BrowserTypeLiteral,
69
+ Cookies,
70
+ CookieTypes,
71
+ ExtraFingerprints,
72
+ Headers,
73
+ HeaderTypes,
74
+ ProxySpec,
75
+ Request,
76
+ Response,
77
+ Session,
78
+ WebSocket,
79
+ WebSocketClosed,
80
+ WebSocketError,
81
+ WebSocketTimeout,
82
+ WsCloseCode,
83
+ delete,
84
+ exceptions,
85
+ get,
86
+ head,
87
+ options,
88
+ patch,
89
+ post,
90
+ put,
91
+ request,
92
+ )
93
+
94
+ from .utils import config_warnings, is_pro
95
+
96
+ config_warnings(on=False)
@@ -0,0 +1,8 @@
1
+ from importlib import metadata
2
+
3
+ from .curl import Curl
4
+
5
+ __title__ = "curl_cffi_patch"
6
+ __description__ = metadata.metadata("curl_cffi_patch")["Summary"]
7
+ __version__ = metadata.version("curl_cffi_patch")
8
+ __curl_version__ = Curl().version().decode()
@@ -0,0 +1,344 @@
1
+ """Ensure asyncio selector methods (add_reader, etc.) are available
2
+ tornado 6.1 adds AddThreadSelectorEventLoop event loop,
3
+ running select in a thread and defining these methods on the running event loop.
4
+ This factors out the functionality of AddThreadSelectorEventLoop
5
+ into a standalone SelectorThread object which can be attached to any running event loop.
6
+ Vendored from tornado v6.4.0
7
+ Redistributed under license Apache-2.0
8
+ """
9
+
10
+ import asyncio
11
+ import atexit
12
+ import errno
13
+ import functools
14
+ import select
15
+ import socket
16
+ import threading
17
+ import typing
18
+ from collections.abc import Callable
19
+ from contextlib import suppress
20
+ from typing import (
21
+ Any,
22
+ Optional,
23
+ Protocol,
24
+ TypeVar,
25
+ Union,
26
+ )
27
+
28
+ _T = TypeVar("_T")
29
+
30
+
31
+ class _HasFileno(Protocol):
32
+ def fileno(self) -> int:
33
+ return 0
34
+
35
+
36
+ _FileDescriptorLike = Union[int, _HasFileno]
37
+
38
+
39
+ # Collection of selector thread event loops to shut down on exit.
40
+ _selector_loops: set["SelectorThread"] = set()
41
+
42
+
43
+ def _atexit_callback() -> None:
44
+ for loop in _selector_loops:
45
+ with loop._select_cond:
46
+ loop._closing_selector = True
47
+ loop._select_cond.notify()
48
+ with suppress(BlockingIOError):
49
+ loop._waker_w.send(b"a")
50
+ if loop._thread is not None:
51
+ # If we don't join our (daemon) thread here, we may get a deadlock
52
+ # during interpreter shutdown. I don't really understand why. This
53
+ # deadlock happens every time in CI (both travis and appveyor) but
54
+ # I've never been able to reproduce locally.
55
+ loop._thread.join()
56
+ _selector_loops.clear()
57
+
58
+
59
+ atexit.register(_atexit_callback)
60
+
61
+
62
+ class SelectorThread:
63
+ """Define ``add_reader`` methods to be called in a background select thread.
64
+
65
+ Instances of this class start a second thread to run a selector.
66
+ This thread is completely hidden from the user;
67
+ all callbacks are run on the wrapped event loop's thread.
68
+
69
+ Typically used via ``AddThreadSelectorEventLoop``,
70
+ but can be attached to a running asyncio loop.
71
+ """
72
+
73
+ _closed = False
74
+
75
+ def __init__(self, real_loop: asyncio.AbstractEventLoop) -> None:
76
+ self._real_loop = real_loop
77
+
78
+ self._select_cond = threading.Condition()
79
+ self._select_args: Optional[
80
+ tuple[list[_FileDescriptorLike], list[_FileDescriptorLike]]
81
+ ] = None
82
+ self._closing_selector = False
83
+ self._thread: Optional[threading.Thread] = None
84
+ self._thread_manager_handle = self._thread_manager()
85
+
86
+ async def thread_manager_anext() -> None:
87
+ # the anext builtin wasn't added until 3.10. We just need to iterate
88
+ # this generator one step.
89
+ await self._thread_manager_handle.__anext__()
90
+
91
+ # When the loop starts, start the thread. Not too soon because we can't
92
+ # clean up if we get to this point but the event loop is closed without
93
+ # starting.
94
+ self._real_loop.call_soon(
95
+ lambda: self._real_loop.create_task(thread_manager_anext())
96
+ )
97
+
98
+ self._readers: dict[_FileDescriptorLike, Callable] = {}
99
+ self._writers: dict[_FileDescriptorLike, Callable] = {}
100
+
101
+ # Writing to _waker_w will wake up the selector thread, which
102
+ # watches for _waker_r to be readable.
103
+ self._waker_r, self._waker_w = socket.socketpair()
104
+ self._waker_r.setblocking(False)
105
+ self._waker_w.setblocking(False)
106
+ _selector_loops.add(self)
107
+ self.add_reader(self._waker_r, self._consume_waker)
108
+
109
+ def close(self) -> None:
110
+ if self._closed:
111
+ return
112
+ with self._select_cond:
113
+ self._closing_selector = True
114
+ self._select_cond.notify()
115
+ self._wake_selector()
116
+ if self._thread is not None:
117
+ self._thread.join()
118
+ _selector_loops.discard(self)
119
+ self.remove_reader(self._waker_r)
120
+ self._waker_r.close()
121
+ self._waker_w.close()
122
+ self._closed = True
123
+
124
+ async def _thread_manager(self) -> typing.AsyncGenerator[None, None]:
125
+ # Create a thread to run the select system call. We manage this thread
126
+ # manually so we can trigger a clean shutdown from an atexit hook. Note
127
+ # that due to the order of operations at shutdown, only daemon threads
128
+ # can be shut down in this way (non-daemon threads would require the
129
+ # introduction of a new hook: https://bugs.python.org/issue41962)
130
+ self._thread = threading.Thread(
131
+ name="Tornado selector",
132
+ daemon=True,
133
+ target=self._run_select,
134
+ )
135
+ self._thread.start()
136
+ self._start_select()
137
+ try:
138
+ # The presense of this yield statement means that this coroutine
139
+ # is actually an asynchronous generator, which has a special
140
+ # shutdown protocol. We wait at this yield point until the
141
+ # event loop's shutdown_asyncgens method is called, at which point
142
+ # we will get a GeneratorExit exception and can shut down the
143
+ # selector thread.
144
+ yield
145
+ except GeneratorExit:
146
+ self.close()
147
+ raise
148
+
149
+ def _wake_selector(self) -> None:
150
+ if self._closed:
151
+ return
152
+ with suppress(BlockingIOError):
153
+ self._waker_w.send(b"a")
154
+
155
+ def _consume_waker(self) -> None:
156
+ with suppress(BlockingIOError):
157
+ self._waker_r.recv(1024)
158
+
159
+ def _start_select(self) -> None:
160
+ # Capture reader and writer sets here in the event loop
161
+ # thread to avoid any problems with concurrent
162
+ # modification while the select loop uses them.
163
+ with self._select_cond:
164
+ assert self._select_args is None
165
+ self._select_args = (list(self._readers.keys()), list(self._writers.keys()))
166
+ self._select_cond.notify()
167
+
168
+ def _run_select(self) -> None:
169
+ while True:
170
+ with self._select_cond:
171
+ while self._select_args is None and not self._closing_selector:
172
+ self._select_cond.wait()
173
+ if self._closing_selector:
174
+ return
175
+ assert self._select_args is not None
176
+ to_read, to_write = self._select_args
177
+ self._select_args = None
178
+
179
+ # We use the simpler interface of the select module instead of
180
+ # the more stateful interface in the selectors module because
181
+ # this class is only intended for use on windows, where
182
+ # select.select is the only option. The selector interface
183
+ # does not have well-documented thread-safety semantics that
184
+ # we can rely on so ensuring proper synchronization would be
185
+ # tricky.
186
+ try:
187
+ # On windows, selecting on a socket for write will not
188
+ # return the socket when there is an error (but selecting
189
+ # for reads works). Also select for errors when selecting
190
+ # for writes, and merge the results.
191
+ #
192
+ # This pattern is also used in
193
+ # https://github.com/python/cpython/blob/v3.8.0/Lib/selectors.py#L312-L317
194
+ rs, ws, xs = select.select(to_read, to_write, to_write)
195
+ ws = ws + xs
196
+ except OSError as e:
197
+ # After remove_reader or remove_writer is called, the file
198
+ # descriptor may subsequently be closed on the event loop
199
+ # thread. It's possible that this select thread hasn't
200
+ # gotten into the select system call by the time that
201
+ # happens in which case (at least on macOS), select may
202
+ # raise a "bad file descriptor" error. If we get that
203
+ # error, check and see if we're also being woken up by
204
+ # polling the waker alone. If we are, just return to the
205
+ # event loop and we'll get the updated set of file
206
+ # descriptors on the next iteration. Otherwise, raise the
207
+ # original error.
208
+ if e.errno == getattr(errno, "WSAENOTSOCK", errno.EBADF):
209
+ rs, _, _ = select.select([self._waker_r.fileno()], [], [], 0)
210
+ if rs:
211
+ ws = []
212
+ else:
213
+ raise
214
+ else:
215
+ raise
216
+
217
+ try:
218
+ self._real_loop.call_soon_threadsafe(self._handle_select, rs, ws)
219
+ except RuntimeError:
220
+ # "Event loop is closed". Swallow the exception for
221
+ # consistency with PollIOLoop (and logical consistency
222
+ # with the fact that we can't guarantee that an
223
+ # add_callback that completes without error will
224
+ # eventually execute).
225
+ pass
226
+ except AttributeError:
227
+ # ProactorEventLoop may raise this instead of RuntimeError
228
+ # if call_soon_threadsafe races with a call to close().
229
+ # Swallow it too for consistency.
230
+ pass
231
+
232
+ def _handle_select(
233
+ self, rs: list[_FileDescriptorLike], ws: list[_FileDescriptorLike]
234
+ ) -> None:
235
+ for r in rs:
236
+ self._handle_event(r, self._readers)
237
+ for w in ws:
238
+ self._handle_event(w, self._writers)
239
+ self._start_select()
240
+
241
+ def _handle_event(
242
+ self,
243
+ fd: _FileDescriptorLike,
244
+ cb_map: dict[_FileDescriptorLike, Callable],
245
+ ) -> None:
246
+ try:
247
+ callback = cb_map[fd]
248
+ except KeyError:
249
+ return
250
+ callback()
251
+
252
+ def add_reader(
253
+ self, fd: _FileDescriptorLike, callback: Callable[..., None], *args: Any
254
+ ) -> None:
255
+ self._readers[fd] = functools.partial(callback, *args)
256
+ self._wake_selector()
257
+
258
+ def add_writer(
259
+ self, fd: _FileDescriptorLike, callback: Callable[..., None], *args: Any
260
+ ) -> None:
261
+ self._writers[fd] = functools.partial(callback, *args)
262
+ self._wake_selector()
263
+
264
+ def remove_reader(self, fd: _FileDescriptorLike) -> bool:
265
+ try:
266
+ del self._readers[fd]
267
+ except KeyError:
268
+ return False
269
+ self._wake_selector()
270
+ return True
271
+
272
+ def remove_writer(self, fd: _FileDescriptorLike) -> bool:
273
+ try:
274
+ del self._writers[fd]
275
+ except KeyError:
276
+ return False
277
+ self._wake_selector()
278
+ return True
279
+
280
+
281
+ class AddThreadSelectorEventLoop(asyncio.AbstractEventLoop):
282
+ """Wrap an event loop to add implementations of the ``add_reader`` method family.
283
+
284
+ Instances of this class start a second thread to run a selector.
285
+ This thread is completely hidden from the user; all callbacks are
286
+ run on the wrapped event loop's thread.
287
+
288
+ This class is used automatically by Tornado; applications should not need
289
+ to refer to it directly.
290
+
291
+ It is safe to wrap any event loop with this class, although it only makes sense
292
+ for event loops that do not implement the ``add_reader`` family of methods
293
+ themselves (i.e. ``WindowsProactorEventLoop``)
294
+
295
+ Closing the ``AddThreadSelectorEventLoop`` also closes the wrapped event loop.
296
+
297
+ """
298
+
299
+ # This class is a __getattribute__-based proxy. All attributes other than those
300
+ # in this set are proxied through to the underlying loop.
301
+ MY_ATTRIBUTES = {
302
+ "_real_loop",
303
+ "_selector",
304
+ "add_reader",
305
+ "add_writer",
306
+ "close",
307
+ "remove_reader",
308
+ "remove_writer",
309
+ }
310
+
311
+ def __getattribute__(self, name: str) -> Any:
312
+ if name in AddThreadSelectorEventLoop.MY_ATTRIBUTES:
313
+ return super().__getattribute__(name)
314
+ return getattr(self._real_loop, name)
315
+
316
+ def __init__(self, real_loop: asyncio.AbstractEventLoop) -> None:
317
+ self._real_loop = real_loop
318
+ self._selector = SelectorThread(real_loop)
319
+
320
+ def close(self) -> None:
321
+ self._selector.close()
322
+ self._real_loop.close()
323
+
324
+ def add_reader( # type: ignore
325
+ self,
326
+ fd: "_FileDescriptorLike",
327
+ callback: Callable[..., None],
328
+ *args: Any,
329
+ ) -> None:
330
+ return self._selector.add_reader(fd, callback, *args)
331
+
332
+ def add_writer( # type: ignore
333
+ self,
334
+ fd: "_FileDescriptorLike",
335
+ callback: Callable[..., None],
336
+ *args: Any, # type: ignore
337
+ ) -> None:
338
+ return self._selector.add_writer(fd, callback, *args)
339
+
340
+ def remove_reader(self, fd: "_FileDescriptorLike") -> bool:
341
+ return self._selector.remove_reader(fd)
342
+
343
+ def remove_writer(self, fd: "_FileDescriptorLike") -> bool:
344
+ return self._selector.remove_writer(fd)