async-solipsism 0.5__tar.gz → 0.7__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 (31) hide show
  1. async_solipsism-0.7/.github/workflows/test.yml +36 -0
  2. {async-solipsism-0.5 → async_solipsism-0.7}/.gitignore +1 -0
  3. async_solipsism-0.7/.pre-commit-config.yaml +12 -0
  4. {async-solipsism-0.5/src/async_solipsism.egg-info → async_solipsism-0.7}/PKG-INFO +57 -15
  5. async-solipsism-0.5/PKG-INFO → async_solipsism-0.7/README.md +47 -24
  6. async_solipsism-0.7/pyproject.toml +35 -0
  7. async_solipsism-0.7/requirements.in +4 -0
  8. async_solipsism-0.7/requirements.txt +46 -0
  9. async_solipsism-0.7/setup.cfg +4 -0
  10. {async-solipsism-0.5 → async_solipsism-0.7}/src/async_solipsism/__init__.py +2 -2
  11. {async-solipsism-0.5 → async_solipsism-0.7}/src/async_solipsism/loop.py +42 -2
  12. {async-solipsism-0.5 → async_solipsism-0.7}/src/async_solipsism/socket.py +28 -14
  13. async-solipsism-0.5/README.md → async_solipsism-0.7/src/async_solipsism.egg-info/PKG-INFO +66 -8
  14. {async-solipsism-0.5 → async_solipsism-0.7}/src/async_solipsism.egg-info/SOURCES.txt +2 -1
  15. async_solipsism-0.7/src/async_solipsism.egg-info/requires.txt +6 -0
  16. {async-solipsism-0.5 → async_solipsism-0.7}/test/test_loop.py +31 -17
  17. {async-solipsism-0.5 → async_solipsism-0.7}/test/test_socket.py +41 -1
  18. async-solipsism-0.5/.github/workflows/test.yml +0 -25
  19. async-solipsism-0.5/pyproject.toml +0 -2
  20. async-solipsism-0.5/requirements.txt +0 -4
  21. async-solipsism-0.5/setup.cfg +0 -34
  22. async-solipsism-0.5/src/async_solipsism.egg-info/requires.txt +0 -5
  23. {async-solipsism-0.5 → async_solipsism-0.7}/.flake8 +0 -0
  24. {async-solipsism-0.5 → async_solipsism-0.7}/LICENSE +0 -0
  25. {async-solipsism-0.5 → async_solipsism-0.7}/pytest.ini +0 -0
  26. {async-solipsism-0.5 → async_solipsism-0.7}/setup.py +0 -0
  27. {async-solipsism-0.5 → async_solipsism-0.7}/src/async_solipsism/clock.py +0 -0
  28. {async-solipsism-0.5 → async_solipsism-0.7}/src/async_solipsism/exceptions.py +0 -0
  29. {async-solipsism-0.5 → async_solipsism-0.7}/src/async_solipsism/selector.py +0 -0
  30. {async-solipsism-0.5 → async_solipsism-0.7}/src/async_solipsism.egg-info/dependency_links.txt +0 -0
  31. {async-solipsism-0.5 → async_solipsism-0.7}/src/async_solipsism.egg-info/top_level.txt +0 -0
@@ -0,0 +1,36 @@
1
+ name: Run tests
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ lint:
7
+ runs-on: ubuntu-22.04
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - name: Set up Python 3.8
11
+ uses: actions/setup-python@v5
12
+ with:
13
+ python-version: "3.8"
14
+ - name: Install dependencies
15
+ run: pip install -r requirements.txt
16
+ - name: Run pre-commit
17
+ run: pre-commit run --all-files
18
+
19
+ build:
20
+ runs-on: ubuntu-22.04
21
+ strategy:
22
+ matrix:
23
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - name: Set up Python ${{ matrix.python-version }}
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+ allow-prereleases: true
31
+ - name: Install dependencies
32
+ run: pip install -r requirements.txt
33
+ - name: Install package
34
+ run: pip install .
35
+ - name: Run pytest
36
+ run: pytest
@@ -1,2 +1,3 @@
1
1
  *.egg-info
2
2
  __pycache__
3
+ /dist
@@ -0,0 +1,12 @@
1
+ repos:
2
+ - repo: https://github.com/pycqa/flake8
3
+ rev: "7.0.0"
4
+ hooks:
5
+ - id: flake8
6
+ - repo: https://github.com/jazzband/pip-tools
7
+ rev: 7.4.1
8
+ hooks:
9
+ - id: pip-compile
10
+ language_version: python3.8
11
+ args: ["--output-file=requirements.txt", "--extra=test", "--strip-extras", "pyproject.toml"]
12
+ files: '^(?:pyproject\.toml|requirements\.txt)$'
@@ -1,22 +1,25 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: async-solipsism
3
- Version: 0.5
3
+ Version: 0.7
4
4
  Summary: Asyncio event loop that doesn't interact with the outside world
5
- Home-page: https://github.com/bmerry/async-solipsism
6
- Author: Bruce Merry
7
- Author-email: bmerry@gmail.com
8
- Classifier: Development Status :: 3 - Alpha
5
+ Author-email: Bruce Merry <bmerry@gmail.com>
6
+ Project-URL: Homepage, https://github.com/bmerry/async-solipsism
7
+ Classifier: Development Status :: 5 - Production/Stable
9
8
  Classifier: Framework :: AsyncIO
10
9
  Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
10
  Classifier: Topic :: Software Development :: Testing :: Mocking
12
- Requires-Python: >=3.6
11
+ Requires-Python: >=3.8
13
12
  Description-Content-Type: text/markdown
14
- Provides-Extra: test
15
13
  License-File: LICENSE
14
+ Provides-Extra: test
15
+ Requires-Dist: pre-commit; extra == "test"
16
+ Requires-Dist: pytest; extra == "test"
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
18
+ Requires-Dist: pytest-mock; extra == "test"
16
19
 
17
20
  # A solipsist event loop
18
21
 
19
- async-solipsism provide a Python asyncio event loop that does not interact with
22
+ async-solipsism provides a Python asyncio event loop that does not interact with
20
23
  the outside world at all. This is ideal for writing unit tests that intend to
21
24
  mock out real-world interactions. It makes for tests that are reliable
22
25
  (unaffected by network outages), reproducible (not affected by random timing
@@ -107,8 +110,17 @@ port number of 0, and async-solipsism will bind the first port number above
107
110
  ### Integration with pytest-asyncio
108
111
 
109
112
  async-solipsism and pytest-asyncio complement each other well: just write a
110
- custom `event_loop` fixture in your test file or `conftest.py` and it will
111
- override the default provided by pytest-asyncio:
113
+ custom `event_loop_policy` fixture in your test file or `conftest.py` and it
114
+ will override the default provided by pytest-asyncio:
115
+
116
+ ```python
117
+ @pytest.fixture
118
+ def event_loop_policy():
119
+ return async_solipsism.EventLoopPolicy()
120
+ ```
121
+
122
+ The above is for pytest-asyncio 0.23+. If you're using an older version, then
123
+ instead you should do this:
112
124
 
113
125
  ```python
114
126
  @pytest.fixture
@@ -118,7 +130,14 @@ def event_loop():
118
130
  loop.close()
119
131
  ```
120
132
 
121
- ### Integration with pytest-aiohttp
133
+ ### Integration with aiohappyeyeballs
134
+
135
+ Unfortunately `aiohappyeyeballs.start_connection` doesn't work out of the box
136
+ because it relies on creating a real socket. I've created a replacement
137
+ `async_solipsism.aiohappyeyeballs_start_connection` that can be used to
138
+ replace it. See the aiohttp section below for example code.
139
+
140
+ ### Integration with aiohttp
122
141
 
123
142
  A little extra work is required to work with aiohttp's test utilities, but it
124
143
  is possible. The example below requires at least aiohttp 3.8.0.
@@ -130,10 +149,14 @@ from aiohttp import web, test_utils
130
149
 
131
150
 
132
151
  @pytest.fixture
133
- def event_loop():
134
- loop = async_solipsism.EventLoop()
135
- yield loop
136
- loop.close()
152
+ def event_loop_policy():
153
+ return async_solipsism.EventLoopPolicy()
154
+
155
+
156
+ @pytest.fixture(autouse=True)
157
+ def mock_start_connection(monkeypatch):
158
+ monkeypatch.setattr("aiohappyeyeballs.start_connection",
159
+ async_solipsism.aiohappyeyeballs_start_connection)
137
160
 
138
161
 
139
162
  def socket_factory(host, port, family):
@@ -183,6 +206,25 @@ Calling functions that are not supported will generally raise
183
206
 
184
207
  ## Changelog
185
208
 
209
+ ### 0.7
210
+
211
+ - Add a replacement function for `aiohappyeyeballs.start_connection`.
212
+
213
+ ### 0.6
214
+
215
+ - Drop support for Python 3.6 and 3.7, which have reached end of life.
216
+ - Add `EventLoopPolicy` to simplify integration with pytest-asyncio 0.23+,
217
+ and use it for the internal tests.
218
+ - Make `Socket.write` and `Socket.recv_into` accept arbitrary buffer-protocol
219
+ objects.
220
+ - Various packaging and developer experience improvements:
221
+ - Put the packaging metadata in pyproject.toml, removing setup.cfg.
222
+ - Pin versions for CI in requirements.txt using pip-compile.
223
+ - Use pre-commit to enforce flake8 and pip-compile.
224
+ - Remove wheel from `build-system.requires` (on setuptools advice).
225
+ - Test against Python 3.11 and 3.12.
226
+ - Run CI workflow against pull requests.
227
+
186
228
  ### 0.5
187
229
 
188
230
  - Map port 0 to an unused port.
@@ -1,22 +1,6 @@
1
- Metadata-Version: 2.1
2
- Name: async-solipsism
3
- Version: 0.5
4
- Summary: Asyncio event loop that doesn't interact with the outside world
5
- Home-page: https://github.com/bmerry/async-solipsism
6
- Author: Bruce Merry
7
- Author-email: bmerry@gmail.com
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Framework :: AsyncIO
10
- Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
11
- Classifier: Topic :: Software Development :: Testing :: Mocking
12
- Requires-Python: >=3.6
13
- Description-Content-Type: text/markdown
14
- Provides-Extra: test
15
- License-File: LICENSE
16
-
17
1
  # A solipsist event loop
18
2
 
19
- async-solipsism provide a Python asyncio event loop that does not interact with
3
+ async-solipsism provides a Python asyncio event loop that does not interact with
20
4
  the outside world at all. This is ideal for writing unit tests that intend to
21
5
  mock out real-world interactions. It makes for tests that are reliable
22
6
  (unaffected by network outages), reproducible (not affected by random timing
@@ -107,8 +91,17 @@ port number of 0, and async-solipsism will bind the first port number above
107
91
  ### Integration with pytest-asyncio
108
92
 
109
93
  async-solipsism and pytest-asyncio complement each other well: just write a
110
- custom `event_loop` fixture in your test file or `conftest.py` and it will
111
- override the default provided by pytest-asyncio:
94
+ custom `event_loop_policy` fixture in your test file or `conftest.py` and it
95
+ will override the default provided by pytest-asyncio:
96
+
97
+ ```python
98
+ @pytest.fixture
99
+ def event_loop_policy():
100
+ return async_solipsism.EventLoopPolicy()
101
+ ```
102
+
103
+ The above is for pytest-asyncio 0.23+. If you're using an older version, then
104
+ instead you should do this:
112
105
 
113
106
  ```python
114
107
  @pytest.fixture
@@ -118,7 +111,14 @@ def event_loop():
118
111
  loop.close()
119
112
  ```
120
113
 
121
- ### Integration with pytest-aiohttp
114
+ ### Integration with aiohappyeyeballs
115
+
116
+ Unfortunately `aiohappyeyeballs.start_connection` doesn't work out of the box
117
+ because it relies on creating a real socket. I've created a replacement
118
+ `async_solipsism.aiohappyeyeballs_start_connection` that can be used to
119
+ replace it. See the aiohttp section below for example code.
120
+
121
+ ### Integration with aiohttp
122
122
 
123
123
  A little extra work is required to work with aiohttp's test utilities, but it
124
124
  is possible. The example below requires at least aiohttp 3.8.0.
@@ -130,10 +130,14 @@ from aiohttp import web, test_utils
130
130
 
131
131
 
132
132
  @pytest.fixture
133
- def event_loop():
134
- loop = async_solipsism.EventLoop()
135
- yield loop
136
- loop.close()
133
+ def event_loop_policy():
134
+ return async_solipsism.EventLoopPolicy()
135
+
136
+
137
+ @pytest.fixture(autouse=True)
138
+ def mock_start_connection(monkeypatch):
139
+ monkeypatch.setattr("aiohappyeyeballs.start_connection",
140
+ async_solipsism.aiohappyeyeballs_start_connection)
137
141
 
138
142
 
139
143
  def socket_factory(host, port, family):
@@ -183,6 +187,25 @@ Calling functions that are not supported will generally raise
183
187
 
184
188
  ## Changelog
185
189
 
190
+ ### 0.7
191
+
192
+ - Add a replacement function for `aiohappyeyeballs.start_connection`.
193
+
194
+ ### 0.6
195
+
196
+ - Drop support for Python 3.6 and 3.7, which have reached end of life.
197
+ - Add `EventLoopPolicy` to simplify integration with pytest-asyncio 0.23+,
198
+ and use it for the internal tests.
199
+ - Make `Socket.write` and `Socket.recv_into` accept arbitrary buffer-protocol
200
+ objects.
201
+ - Various packaging and developer experience improvements:
202
+ - Put the packaging metadata in pyproject.toml, removing setup.cfg.
203
+ - Pin versions for CI in requirements.txt using pip-compile.
204
+ - Use pre-commit to enforce flake8 and pip-compile.
205
+ - Remove wheel from `build-system.requires` (on setuptools advice).
206
+ - Test against Python 3.11 and 3.12.
207
+ - Run CI workflow against pull requests.
208
+
186
209
  ### 0.5
187
210
 
188
211
  - Map port 0 to an unused port.
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "setuptools-scm>=8.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "async-solipsism"
7
+ dynamic = ["version", "readme"]
8
+ description = "Asyncio event loop that doesn't interact with the outside world"
9
+ authors = [
10
+ {name = "Bruce Merry", email = "bmerry@gmail.com"}
11
+ ]
12
+ requires-python = ">=3.8"
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Framework :: AsyncIO",
16
+ "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
17
+ "Topic :: Software Development :: Testing :: Mocking",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/bmerry/async-solipsism"
22
+
23
+ [project.optional-dependencies]
24
+ test = [
25
+ "pre-commit",
26
+ "pytest",
27
+ "pytest-asyncio>=0.23",
28
+ "pytest-mock",
29
+ ]
30
+
31
+ [tool.setuptools_scm]
32
+
33
+ [tool.setuptools.dynamic]
34
+ version = {attr = "async_solipsism.__version__"}
35
+ readme = {file = ["README.md"], content-type = "text/markdown"}
@@ -0,0 +1,4 @@
1
+ pre-commit
2
+ pytest
3
+ pytest-asyncio>=0.23
4
+ pytest-mock
@@ -0,0 +1,46 @@
1
+ #
2
+ # This file is autogenerated by pip-compile with Python 3.8
3
+ # by the following command:
4
+ #
5
+ # pip-compile --extra=test --output-file=requirements.txt --strip-extras pyproject.toml
6
+ #
7
+ cfgv==3.4.0
8
+ # via pre-commit
9
+ distlib==0.3.8
10
+ # via virtualenv
11
+ exceptiongroup==1.2.0
12
+ # via pytest
13
+ filelock==3.13.1
14
+ # via virtualenv
15
+ identify==2.5.35
16
+ # via pre-commit
17
+ iniconfig==2.0.0
18
+ # via pytest
19
+ nodeenv==1.8.0
20
+ # via pre-commit
21
+ packaging==24.0
22
+ # via pytest
23
+ platformdirs==4.2.0
24
+ # via virtualenv
25
+ pluggy==1.4.0
26
+ # via pytest
27
+ pre-commit==3.5.0
28
+ # via async-solipsism (pyproject.toml)
29
+ pytest==8.1.1
30
+ # via
31
+ # async-solipsism (pyproject.toml)
32
+ # pytest-asyncio
33
+ # pytest-mock
34
+ pytest-asyncio==0.23.6
35
+ # via async-solipsism (pyproject.toml)
36
+ pytest-mock==3.14.0
37
+ # via async-solipsism (pyproject.toml)
38
+ pyyaml==6.0.1
39
+ # via pre-commit
40
+ tomli==2.0.1
41
+ # via pytest
42
+ virtualenv==20.25.1
43
+ # via pre-commit
44
+
45
+ # The following packages are considered to be unsafe in a requirements file:
46
+ # setuptools
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -1,4 +1,4 @@
1
- # Copyright 2020-2022 Bruce Merry
1
+ # Copyright 2020-2022, 2024 Bruce Merry
2
2
  #
3
3
  # This file is part of async-solipsism.
4
4
  #
@@ -19,4 +19,4 @@ from .exceptions import * # noqa
19
19
  from .socket import * # noqa
20
20
  from .loop import * # noqa
21
21
 
22
- __version__ = '0.5'
22
+ __version__ = '0.7'
@@ -1,4 +1,4 @@
1
- # Copyright 2020, 2022 Bruce Merry
1
+ # Copyright 2020, 2022, 2024 Bruce Merry
2
2
  #
3
3
  # This file is part of async-solipsism.
4
4
  #
@@ -24,7 +24,7 @@ from . import selector, socket as _socket
24
24
  from .exceptions import SolipsismError
25
25
 
26
26
 
27
- __all__ = ('EventLoop', 'stream_pairs')
27
+ __all__ = ('EventLoop', 'EventLoopPolicy', 'aiohappyeyeballs_start_connection', 'stream_pairs')
28
28
 
29
29
 
30
30
  class EventLoop(asyncio.selector_events.BaseSelectorEventLoop):
@@ -133,6 +133,27 @@ class EventLoop(asyncio.selector_events.BaseSelectorEventLoop):
133
133
  **kwargs
134
134
  )
135
135
 
136
+ async def aiohappyeyeballs_start_connection(self,
137
+ addr_infos,
138
+ *,
139
+ local_addr_infos=None,
140
+ happy_eyeballs_delay=None,
141
+ interleave=None):
142
+ for addr_info in addr_infos:
143
+ addr = addr_info[4]
144
+ addr = _socket._normalise_ipv6_sockaddr(addr)
145
+ if addr in self.__listening_sockets:
146
+ listener = self.__listening_sockets[addr]
147
+ if local_addr_infos:
148
+ local_addr_info = local_addr_infos[0]
149
+ else:
150
+ port = self.__next_port
151
+ self.__next_port += 1
152
+ local_addr_info = ('::1', port, 0, 0)
153
+ sock = await listener.make_connection(local_addr_info)
154
+ return sock
155
+ raise ConnectionRefusedError('No socket listening on requested addresses')
156
+
136
157
  async def connect_read_pipe(self, protocol_factory, pipe):
137
158
  raise SolipsismError("connect_read_pipe is not supported")
138
159
 
@@ -197,8 +218,27 @@ class EventLoop(asyncio.selector_events.BaseSelectorEventLoop):
197
218
  super()._stop_serving(sock)
198
219
 
199
220
 
221
+ class EventLoopPolicy(asyncio.DefaultEventLoopPolicy):
222
+ def new_event_loop(self) -> EventLoop:
223
+ return EventLoop()
224
+
225
+
200
226
  async def stream_pairs(capacity=None):
201
227
  sock1, sock2 = _socket.socketpair(capacity=capacity)
202
228
  streams1 = await asyncio.open_connection(sock=sock1)
203
229
  streams2 = await asyncio.open_connection(sock=sock2)
204
230
  return streams1, streams2
231
+
232
+
233
+ async def aiohappyeyeballs_start_connection(addr_infos,
234
+ *,
235
+ local_addr_infos=None,
236
+ happy_eyeballs_delay=None,
237
+ interleave=None,
238
+ loop=None):
239
+ if loop is None:
240
+ loop = asyncio.get_running_loop()
241
+ return await loop.aiohappyeyeballs_start_connection(addr_infos,
242
+ local_addr_infos=local_addr_infos,
243
+ happy_eyeballs_delay=happy_eyeballs_delay,
244
+ interleave=interleave)
@@ -1,4 +1,4 @@
1
- # Copyright 2020 Bruce Merry
1
+ # Copyright 2020, 2024 Bruce Merry
2
2
  #
3
3
  # This file is part of async-solipsism.
4
4
  #
@@ -30,6 +30,11 @@ DEFAULT_CAPACITY = 65536
30
30
  __all__ = ('Socket', 'ListenSocket', 'Queue', 'socketpair')
31
31
 
32
32
 
33
+ def _view(bytes):
34
+ """Get a 1D byte-typed memoryview of an object implementing the buffer protocol."""
35
+ return memoryview(bytes).cast("B")
36
+
37
+
33
38
  class Queue:
34
39
  def __init__(self, capacity=None):
35
40
  self.capacity = capacity or DEFAULT_CAPACITY
@@ -50,11 +55,14 @@ class Queue:
50
55
  raise BrokenPipeError(errno.EPIPE, 'Broken pipe')
51
56
  if len(self) >= self.capacity:
52
57
  return None
53
- n = len(data)
54
- if len(self) + n > self.capacity:
55
- n = self.capacity - len(self)
56
- data = memoryview(data)[:n]
57
- self._buffer += data
58
+ with _view(data) as view:
59
+ n = len(view)
60
+ if len(self) + n > self.capacity:
61
+ n = self.capacity - len(self)
62
+ with view[:n] as prefix:
63
+ self._buffer += prefix
64
+ else:
65
+ self._buffer += view
58
66
  return n
59
67
 
60
68
  def read(self, size=-1):
@@ -68,7 +76,8 @@ class Queue:
68
76
  self._buffer = bytearray()
69
77
  else:
70
78
  n = min(size, len(self._buffer))
71
- ret = bytes(memoryview(self._buffer))[:n]
79
+ with memoryview(self._buffer) as view, view[:n] as prefix:
80
+ ret = bytes(prefix)
72
81
  self._buffer = self._buffer[n:]
73
82
  return ret
74
83
 
@@ -93,7 +102,7 @@ class SocketFd:
93
102
  return hash(self.socket)
94
103
 
95
104
  def __eq__(self, other):
96
- if type(other) == SocketFd:
105
+ if type(other) is SocketFd:
97
106
  return self.socket is other.socket
98
107
  return NotImplemented
99
108
 
@@ -111,7 +120,7 @@ class _SocketBase:
111
120
 
112
121
  def setblocking(self, flag):
113
122
  if flag:
114
- raise SolipsismError('Socket only support non-blocking operation')
123
+ raise SolipsismError('Socket only supports non-blocking operation')
115
124
 
116
125
  def setsockopt(self, level, optname, value, optlen=None):
117
126
  key = (level, optname)
@@ -164,11 +173,16 @@ class Socket(_SocketBase):
164
173
 
165
174
  def recv_into(self, buffer, nbytes=0, flags=0):
166
175
  # TODO: implement more efficiently?
167
- if not nbytes:
168
- nbytes = len(buffer)
169
- data = self.recv(nbytes)
170
- buffer[:len(data)] = data
171
- return len(data)
176
+ with _view(buffer) as view:
177
+ if not nbytes:
178
+ nbytes = len(view)
179
+ if nbytes < 0:
180
+ raise ValueError("negative buffersize in recv_into")
181
+ if nbytes > len(view):
182
+ raise ValueError("buffer too small for requested bytes")
183
+ data = self.recv(nbytes)
184
+ view[:len(data)] = data
185
+ return len(data)
172
186
 
173
187
  def send(self, bytes, flags=0):
174
188
  self._check_closed()
@@ -1,6 +1,25 @@
1
+ Metadata-Version: 2.1
2
+ Name: async-solipsism
3
+ Version: 0.7
4
+ Summary: Asyncio event loop that doesn't interact with the outside world
5
+ Author-email: Bruce Merry <bmerry@gmail.com>
6
+ Project-URL: Homepage, https://github.com/bmerry/async-solipsism
7
+ Classifier: Development Status :: 5 - Production/Stable
8
+ Classifier: Framework :: AsyncIO
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
10
+ Classifier: Topic :: Software Development :: Testing :: Mocking
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Provides-Extra: test
15
+ Requires-Dist: pre-commit; extra == "test"
16
+ Requires-Dist: pytest; extra == "test"
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
18
+ Requires-Dist: pytest-mock; extra == "test"
19
+
1
20
  # A solipsist event loop
2
21
 
3
- async-solipsism provide a Python asyncio event loop that does not interact with
22
+ async-solipsism provides a Python asyncio event loop that does not interact with
4
23
  the outside world at all. This is ideal for writing unit tests that intend to
5
24
  mock out real-world interactions. It makes for tests that are reliable
6
25
  (unaffected by network outages), reproducible (not affected by random timing
@@ -91,8 +110,17 @@ port number of 0, and async-solipsism will bind the first port number above
91
110
  ### Integration with pytest-asyncio
92
111
 
93
112
  async-solipsism and pytest-asyncio complement each other well: just write a
94
- custom `event_loop` fixture in your test file or `conftest.py` and it will
95
- override the default provided by pytest-asyncio:
113
+ custom `event_loop_policy` fixture in your test file or `conftest.py` and it
114
+ will override the default provided by pytest-asyncio:
115
+
116
+ ```python
117
+ @pytest.fixture
118
+ def event_loop_policy():
119
+ return async_solipsism.EventLoopPolicy()
120
+ ```
121
+
122
+ The above is for pytest-asyncio 0.23+. If you're using an older version, then
123
+ instead you should do this:
96
124
 
97
125
  ```python
98
126
  @pytest.fixture
@@ -102,7 +130,14 @@ def event_loop():
102
130
  loop.close()
103
131
  ```
104
132
 
105
- ### Integration with pytest-aiohttp
133
+ ### Integration with aiohappyeyeballs
134
+
135
+ Unfortunately `aiohappyeyeballs.start_connection` doesn't work out of the box
136
+ because it relies on creating a real socket. I've created a replacement
137
+ `async_solipsism.aiohappyeyeballs_start_connection` that can be used to
138
+ replace it. See the aiohttp section below for example code.
139
+
140
+ ### Integration with aiohttp
106
141
 
107
142
  A little extra work is required to work with aiohttp's test utilities, but it
108
143
  is possible. The example below requires at least aiohttp 3.8.0.
@@ -114,10 +149,14 @@ from aiohttp import web, test_utils
114
149
 
115
150
 
116
151
  @pytest.fixture
117
- def event_loop():
118
- loop = async_solipsism.EventLoop()
119
- yield loop
120
- loop.close()
152
+ def event_loop_policy():
153
+ return async_solipsism.EventLoopPolicy()
154
+
155
+
156
+ @pytest.fixture(autouse=True)
157
+ def mock_start_connection(monkeypatch):
158
+ monkeypatch.setattr("aiohappyeyeballs.start_connection",
159
+ async_solipsism.aiohappyeyeballs_start_connection)
121
160
 
122
161
 
123
162
  def socket_factory(host, port, family):
@@ -167,6 +206,25 @@ Calling functions that are not supported will generally raise
167
206
 
168
207
  ## Changelog
169
208
 
209
+ ### 0.7
210
+
211
+ - Add a replacement function for `aiohappyeyeballs.start_connection`.
212
+
213
+ ### 0.6
214
+
215
+ - Drop support for Python 3.6 and 3.7, which have reached end of life.
216
+ - Add `EventLoopPolicy` to simplify integration with pytest-asyncio 0.23+,
217
+ and use it for the internal tests.
218
+ - Make `Socket.write` and `Socket.recv_into` accept arbitrary buffer-protocol
219
+ objects.
220
+ - Various packaging and developer experience improvements:
221
+ - Put the packaging metadata in pyproject.toml, removing setup.cfg.
222
+ - Pin versions for CI in requirements.txt using pip-compile.
223
+ - Use pre-commit to enforce flake8 and pip-compile.
224
+ - Remove wheel from `build-system.requires` (on setuptools advice).
225
+ - Test against Python 3.11 and 3.12.
226
+ - Run CI workflow against pull requests.
227
+
170
228
  ### 0.5
171
229
 
172
230
  - Map port 0 to an unused port.
@@ -1,11 +1,12 @@
1
1
  .flake8
2
2
  .gitignore
3
+ .pre-commit-config.yaml
3
4
  LICENSE
4
5
  README.md
5
6
  pyproject.toml
6
7
  pytest.ini
8
+ requirements.in
7
9
  requirements.txt
8
- setup.cfg
9
10
  setup.py
10
11
  .github/workflows/test.yml
11
12
  src/async_solipsism/__init__.py
@@ -0,0 +1,6 @@
1
+
2
+ [test]
3
+ pre-commit
4
+ pytest
5
+ pytest-asyncio>=0.23
6
+ pytest-mock
@@ -25,13 +25,12 @@ import async_solipsism
25
25
 
26
26
 
27
27
  @pytest.fixture
28
- def event_loop():
29
- loop = async_solipsism.EventLoop()
30
- yield loop
31
- loop.close()
28
+ def event_loop_policy():
29
+ return async_solipsism.EventLoopPolicy()
32
30
 
33
31
 
34
- async def test_sleep(event_loop):
32
+ async def test_sleep():
33
+ event_loop = asyncio.get_running_loop()
35
34
  assert event_loop.time() == 0.0
36
35
  await asyncio.sleep(2)
37
36
  assert event_loop.time() == 2.0
@@ -53,7 +52,9 @@ def test_sleep_forever(event_loop):
53
52
  ]
54
53
  )
55
54
  @pytest.mark.parametrize('delay', [False, True])
56
- async def test_delayed_sock_recv(method, delay, event_loop):
55
+ async def test_delayed_sock_recv(method, delay):
56
+ event_loop = asyncio.get_running_loop()
57
+
57
58
  async def delayed_write(wsock):
58
59
  await asyncio.sleep(1)
59
60
  wsock.send(b'Hello')
@@ -76,7 +77,9 @@ async def test_delayed_sock_recv(method, delay, event_loop):
76
77
 
77
78
 
78
79
  @pytest.mark.parametrize('size', [10, 10**7])
79
- async def test_sock_sendall(size, event_loop):
80
+ async def test_sock_sendall(size):
81
+ event_loop = asyncio.get_running_loop()
82
+
80
83
  async def delayed_read(rsock):
81
84
  n = 0
82
85
  while True:
@@ -99,7 +102,8 @@ async def test_sock_sendall(size, event_loop):
99
102
  assert n == size
100
103
 
101
104
 
102
- async def test_connect_existing(event_loop, mocker):
105
+ async def test_connect_existing(mocker):
106
+ event_loop = asyncio.get_running_loop()
103
107
  sock1, sock2 = async_solipsism.socketpair()
104
108
  transport1, protocol1 = await event_loop.connect_accepted_socket(
105
109
  mocker.MagicMock, sock1)
@@ -128,7 +132,9 @@ async def test_stream():
128
132
 
129
133
 
130
134
  @pytest.mark.parametrize('manual_socket', [False, True])
131
- async def test_server(event_loop, manual_socket):
135
+ async def test_server(manual_socket):
136
+ event_loop = asyncio.get_running_loop()
137
+
132
138
  def callback(reader, writer):
133
139
  server_conn.set_result((reader, writer))
134
140
 
@@ -156,7 +162,7 @@ async def test_server(event_loop, manual_socket):
156
162
  await server.wait_closed()
157
163
 
158
164
 
159
- async def test_unused_port(event_loop):
165
+ async def test_unused_port():
160
166
  def callback(reader, writer):
161
167
  pass
162
168
 
@@ -172,7 +178,8 @@ async def test_unused_port(event_loop):
172
178
  await server2.wait_closed()
173
179
 
174
180
 
175
- async def test_close_server(event_loop):
181
+ async def test_close_server():
182
+ event_loop = asyncio.get_running_loop()
176
183
  server = await asyncio.start_server(lambda reader, writer: None, 'test.invalid', 1234)
177
184
  server.close()
178
185
  await server.wait_closed()
@@ -180,18 +187,21 @@ async def test_close_server(event_loop):
180
187
  await event_loop.create_connection('test.invalid', 1234)
181
188
 
182
189
 
183
- async def test_create_connection_no_listener(event_loop):
190
+ async def test_create_connection_no_listener():
191
+ event_loop = asyncio.get_running_loop()
184
192
  with pytest.raises(ConnectionRefusedError):
185
193
  await event_loop.create_connection('test.invalid', 1234)
186
194
 
187
195
 
188
- async def test_run_in_executor_implicit(event_loop):
196
+ async def test_run_in_executor_implicit():
197
+ event_loop = asyncio.get_running_loop()
189
198
  thread_id = await event_loop.run_in_executor(None, threading.get_ident)
190
199
  assert isinstance(thread_id, int)
191
200
  assert thread_id != threading.get_ident()
192
201
 
193
202
 
194
- async def test_run_in_executor_explicit(event_loop):
203
+ async def test_run_in_executor_explicit():
204
+ event_loop = asyncio.get_running_loop()
195
205
  my_executor = concurrent.futures.ThreadPoolExecutor(1)
196
206
  expected_thread_id = my_executor.submit(threading.get_ident).result()
197
207
  assert isinstance(expected_thread_id, int)
@@ -201,7 +211,8 @@ async def test_run_in_executor_explicit(event_loop):
201
211
 
202
212
 
203
213
  @pytest.mark.skipif("sys.version_info < (3, 7)")
204
- async def test_sendfile(event_loop, tmp_path):
214
+ async def test_sendfile(tmp_path):
215
+ event_loop = asyncio.get_running_loop()
205
216
  tmp_file = tmp_path / 'test_sendfile.txt'
206
217
  tmp_file.write_bytes(b'Hello world\n')
207
218
  ((reader1, writer1), (reader2, writer2)) = await async_solipsism.stream_pairs()
@@ -215,14 +226,17 @@ async def test_sendfile(event_loop, tmp_path):
215
226
  await writer2.wait_closed()
216
227
 
217
228
 
218
- async def test_call_soon_threadsafe(event_loop):
229
+ async def test_call_soon_threadsafe():
230
+ event_loop = asyncio.get_running_loop()
219
231
  future = event_loop.create_future()
220
232
  event_loop.call_soon_threadsafe(future.set_result, 3)
221
233
  result = await future
222
234
  assert result == 3
223
235
 
224
236
 
225
- async def test_call_soon_threadsafe_wrong_thread(event_loop):
237
+ async def test_call_soon_threadsafe_wrong_thread():
238
+ event_loop = asyncio.get_running_loop()
239
+
226
240
  def thread_func():
227
241
  event_loop.call_soon_threadsafe(lambda: None)
228
242
 
@@ -1,4 +1,4 @@
1
- # Copyright 2020 Bruce Merry
1
+ # Copyright 2020, 2024 Bruce Merry
2
2
  #
3
3
  # This file is part of async-solipsism.
4
4
  #
@@ -15,7 +15,9 @@
15
15
  # You should have received a copy of the GNU General Public License
16
16
  # along with async-solipsism. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
+ import array
18
19
  import socket
20
+ import struct
19
21
 
20
22
  import pytest
21
23
 
@@ -69,3 +71,41 @@ def test_use_after_shutdown(end):
69
71
  assert sock0.recv(1) == b''
70
72
  with pytest.raises(BrokenPipeError):
71
73
  sock1.send(b'hello')
74
+
75
+
76
+ def test_recv_into_array():
77
+ sock1, sock2 = async_solipsism.socketpair()
78
+ data = array.array("I", [1, 2, 3])
79
+ sock1.send(b"\xDE\xAD\xBE\xEF")
80
+ nbytes = sock2.recv_into(data)
81
+ assert nbytes == 4
82
+ assert data[0] == struct.unpack("I", b"\xDE\xAD\xBE\xEF")[0]
83
+ assert data[1] == 2
84
+ assert data[2] == 3
85
+
86
+
87
+ def test_recv_into_array_partial():
88
+ sock1, sock2 = async_solipsism.socketpair()
89
+ data = array.array("I", [1, 2, 3])
90
+ sock1.send(b"\xDE\xAD\xBE\xEF\xCA\xFE")
91
+ nbytes = sock2.recv_into(data, 4)
92
+ assert nbytes == 4
93
+ assert data[0] == struct.unpack("I", b"\xDE\xAD\xBE\xEF")[0]
94
+ assert data[1] == 2
95
+ assert data[2] == 3
96
+
97
+
98
+ def test_write_array():
99
+ sock1, sock2 = async_solipsism.socketpair(capacity=100)
100
+ data = array.array("I", range(64))
101
+ nbytes = sock1.send(data)
102
+ assert nbytes == 100
103
+ received = sock2.recv(-1)
104
+ assert len(received) == 100
105
+ assert memoryview(received) == memoryview(data).cast("B")[:100]
106
+
107
+
108
+ @pytest.mark.parametrize("nbytes", [-1, 5])
109
+ def test_recv_into_bad_nbytes(sock, nbytes):
110
+ with pytest.raises(ValueError):
111
+ sock.recv_into(bytearray(4), nbytes=nbytes)
@@ -1,25 +0,0 @@
1
- name: Run tests
2
-
3
- on: [push]
4
-
5
- jobs:
6
- build:
7
- runs-on: ubuntu-latest
8
- strategy:
9
- matrix:
10
- python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
11
-
12
- steps:
13
- - uses: actions/checkout@v2
14
- - name: Set up Python ${{ matrix.python-version }}
15
- uses: actions/setup-python@v2
16
- with:
17
- python-version: ${{ matrix.python-version }}
18
- - name: Install dependencies
19
- run: pip install -r requirements.txt
20
- - name: Run flake8
21
- run: flake8
22
- - name: Install package
23
- run: pip install .
24
- - name: Run pytest
25
- run: pytest
@@ -1,2 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools", "wheel", "setuptools-scm"]
@@ -1,4 +0,0 @@
1
- flake8
2
- pytest
3
- pytest-asyncio
4
- pytest-mock
@@ -1,34 +0,0 @@
1
- [metadata]
2
- name = async-solipsism
3
- version = attr: src.async_solipsism.__version__
4
- description = Asyncio event loop that doesn't interact with the outside world
5
- long_description = file:README.md
6
- long_description_content_type = text/markdown
7
- author = Bruce Merry
8
- author_email = bmerry@gmail.com
9
- url = https://github.com/bmerry/async-solipsism
10
- classifiers =
11
- Development Status :: 3 - Alpha
12
- Framework :: AsyncIO
13
- License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
14
- Topic :: Software Development :: Testing :: Mocking
15
-
16
- [options]
17
- package_dir =
18
- = src
19
- packages = find:
20
- python_requires = >= 3.6
21
-
22
- [options.packages.find]
23
- where = src
24
-
25
- [options.extras_require]
26
- test =
27
- pytest
28
- pytest-asyncio
29
- pytest-mock
30
-
31
- [egg_info]
32
- tag_build =
33
- tag_date = 0
34
-
@@ -1,5 +0,0 @@
1
-
2
- [test]
3
- pytest
4
- pytest-asyncio
5
- pytest-mock
File without changes
File without changes
File without changes
File without changes