async-solipsism 0.4__tar.gz → 0.6__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.6/.github/workflows/test.yml +35 -0
  2. {async-solipsism-0.4 → async-solipsism-0.6}/.gitignore +1 -0
  3. async-solipsism-0.6/.pre-commit-config.yaml +12 -0
  4. {async-solipsism-0.4/src/async_solipsism.egg-info → async-solipsism-0.6}/PKG-INFO +48 -19
  5. async-solipsism-0.4/PKG-INFO → async-solipsism-0.6/README.md +39 -29
  6. async-solipsism-0.6/pyproject.toml +35 -0
  7. async-solipsism-0.6/requirements.in +4 -0
  8. async-solipsism-0.6/requirements.txt +46 -0
  9. async-solipsism-0.6/setup.cfg +4 -0
  10. {async-solipsism-0.4 → async-solipsism-0.6}/src/async_solipsism/__init__.py +2 -2
  11. {async-solipsism-0.4 → async-solipsism-0.6}/src/async_solipsism/loop.py +14 -3
  12. {async-solipsism-0.4 → async-solipsism-0.6}/src/async_solipsism/socket.py +27 -13
  13. async-solipsism-0.4/README.md → async-solipsism-0.6/src/async_solipsism.egg-info/PKG-INFO +58 -13
  14. {async-solipsism-0.4 → async-solipsism-0.6}/src/async_solipsism.egg-info/SOURCES.txt +2 -1
  15. async-solipsism-0.6/src/async_solipsism.egg-info/requires.txt +6 -0
  16. {async-solipsism-0.4 → async-solipsism-0.6}/test/test_loop.py +51 -17
  17. {async-solipsism-0.4 → async-solipsism-0.6}/test/test_socket.py +41 -1
  18. async-solipsism-0.4/.github/workflows/test.yml +0 -25
  19. async-solipsism-0.4/pyproject.toml +0 -2
  20. async-solipsism-0.4/requirements.txt +0 -4
  21. async-solipsism-0.4/setup.cfg +0 -34
  22. async-solipsism-0.4/src/async_solipsism.egg-info/requires.txt +0 -5
  23. {async-solipsism-0.4 → async-solipsism-0.6}/.flake8 +0 -0
  24. {async-solipsism-0.4 → async-solipsism-0.6}/LICENSE +0 -0
  25. {async-solipsism-0.4 → async-solipsism-0.6}/pytest.ini +0 -0
  26. {async-solipsism-0.4 → async-solipsism-0.6}/setup.py +0 -0
  27. {async-solipsism-0.4 → async-solipsism-0.6}/src/async_solipsism/clock.py +0 -0
  28. {async-solipsism-0.4 → async-solipsism-0.6}/src/async_solipsism/exceptions.py +0 -0
  29. {async-solipsism-0.4 → async-solipsism-0.6}/src/async_solipsism/selector.py +0 -0
  30. {async-solipsism-0.4 → async-solipsism-0.6}/src/async_solipsism.egg-info/dependency_links.txt +0 -0
  31. {async-solipsism-0.4 → async-solipsism-0.6}/src/async_solipsism.egg-info/top_level.txt +0 -0
@@ -0,0 +1,35 @@
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"]
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
+ - name: Install dependencies
31
+ run: pip install -r requirements.txt
32
+ - name: Install package
33
+ run: pip install .
34
+ - name: Run pytest
35
+ 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.4
3
+ Version: 0.6
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
5
+ Author-email: Bruce Merry <bmerry@gmail.com>
6
+ Project-URL: Homepage, https://github.com/bmerry/async-solipsism
8
7
  Classifier: Development Status :: 3 - Alpha
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
@@ -100,11 +103,24 @@ The host and port are associated with the event loop, and are remembered until
100
103
  the server is closed. Attempting to connect after closing the server, or to an
101
104
  address that hasn't been registered, will raise a `ConnectionRefusedError`.
102
105
 
106
+ If you don't want the bother of picking non-colliding ports, you can pass a
107
+ port number of 0, and async-solipsism will bind the first port number above
108
+ 60000 that is unused.
109
+
103
110
  ### Integration with pytest-asyncio
104
111
 
105
112
  async-solipsism and pytest-asyncio complement each other well: just write a
106
- custom `event_loop` fixture in your test file or `conftest.py` and it will
107
- 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:
108
124
 
109
125
  ```python
110
126
  @pytest.fixture
@@ -126,14 +142,12 @@ from aiohttp import web, test_utils
126
142
 
127
143
 
128
144
  @pytest.fixture
129
- def event_loop():
130
- loop = async_solipsism.EventLoop()
131
- yield loop
132
- loop.close()
145
+ def event_loop_policy():
146
+ return async_solipsism.EventLoopPolicy()
133
147
 
134
148
 
135
149
  def socket_factory(host, port, family):
136
- return async_solipsism.ListenSocket((host, port if port else 80))
150
+ return async_solipsism.ListenSocket((host, port))
137
151
 
138
152
 
139
153
  async def test_integration():
@@ -150,10 +164,6 @@ the socket factory, although it may be possible to do by monkeypatching. In
150
164
  practice you will probably want to define your own fixtures for the client
151
165
  and server.
152
166
 
153
- If you need to run more than one test server concurrently, you'll need to
154
- extend the socket factory to assign them each a different port
155
- (async-solipsism does not currently handle mapping port 0 to an unused port).
156
-
157
167
  ## Limitations
158
168
 
159
169
  The requirement to have no interaction with the outside world naturally
@@ -183,12 +193,31 @@ Calling functions that are not supported will generally raise
183
193
 
184
194
  ## Changelog
185
195
 
196
+ ### 0.6
197
+
198
+ - Drop support for Python 3.6 and 3.7, which have reached end of life.
199
+ - Add `EventLoopPolicy` to simplify integration with pytest-asyncio 0.23+,
200
+ and use it for the internal tests.
201
+ - Make `Socket.write` and `Socket.recv_into` accept arbitrary buffer-protocol
202
+ objects.
203
+ - Various packaging and developer experience improvements:
204
+ - Put the packaging metadata in pyproject.toml, removing setup.cfg.
205
+ - Pin versions for CI in requirements.txt using pip-compile.
206
+ - Use pre-commit to enforce flake8 and pip-compile.
207
+ - Remove wheel from `build-system.requires` (on setuptools advice).
208
+ - Test against Python 3.11 and 3.12.
209
+ - Run CI workflow against pull requests.
210
+
211
+ ### 0.5
212
+
213
+ - Map port 0 to an unused port.
214
+
186
215
  ### 0.4
187
216
 
188
217
  - Allow `call_soon_threadsafe` from the same thread.
189
218
  - Don't warn when `SO_KEEPALIVE` is set on a socket.
190
219
  - Update instructions for use with aiohttp.
191
- - Add a pyproject.toml
220
+ - Add a pyproject.toml.
192
221
 
193
222
  ### 0.3
194
223
 
@@ -1,22 +1,6 @@
1
- Metadata-Version: 2.1
2
- Name: async-solipsism
3
- Version: 0.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
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
@@ -100,11 +84,24 @@ The host and port are associated with the event loop, and are remembered until
100
84
  the server is closed. Attempting to connect after closing the server, or to an
101
85
  address that hasn't been registered, will raise a `ConnectionRefusedError`.
102
86
 
87
+ If you don't want the bother of picking non-colliding ports, you can pass a
88
+ port number of 0, and async-solipsism will bind the first port number above
89
+ 60000 that is unused.
90
+
103
91
  ### Integration with pytest-asyncio
104
92
 
105
93
  async-solipsism and pytest-asyncio complement each other well: just write a
106
- custom `event_loop` fixture in your test file or `conftest.py` and it will
107
- 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:
108
105
 
109
106
  ```python
110
107
  @pytest.fixture
@@ -126,14 +123,12 @@ from aiohttp import web, test_utils
126
123
 
127
124
 
128
125
  @pytest.fixture
129
- def event_loop():
130
- loop = async_solipsism.EventLoop()
131
- yield loop
132
- loop.close()
126
+ def event_loop_policy():
127
+ return async_solipsism.EventLoopPolicy()
133
128
 
134
129
 
135
130
  def socket_factory(host, port, family):
136
- return async_solipsism.ListenSocket((host, port if port else 80))
131
+ return async_solipsism.ListenSocket((host, port))
137
132
 
138
133
 
139
134
  async def test_integration():
@@ -150,10 +145,6 @@ the socket factory, although it may be possible to do by monkeypatching. In
150
145
  practice you will probably want to define your own fixtures for the client
151
146
  and server.
152
147
 
153
- If you need to run more than one test server concurrently, you'll need to
154
- extend the socket factory to assign them each a different port
155
- (async-solipsism does not currently handle mapping port 0 to an unused port).
156
-
157
148
  ## Limitations
158
149
 
159
150
  The requirement to have no interaction with the outside world naturally
@@ -183,12 +174,31 @@ Calling functions that are not supported will generally raise
183
174
 
184
175
  ## Changelog
185
176
 
177
+ ### 0.6
178
+
179
+ - Drop support for Python 3.6 and 3.7, which have reached end of life.
180
+ - Add `EventLoopPolicy` to simplify integration with pytest-asyncio 0.23+,
181
+ and use it for the internal tests.
182
+ - Make `Socket.write` and `Socket.recv_into` accept arbitrary buffer-protocol
183
+ objects.
184
+ - Various packaging and developer experience improvements:
185
+ - Put the packaging metadata in pyproject.toml, removing setup.cfg.
186
+ - Pin versions for CI in requirements.txt using pip-compile.
187
+ - Use pre-commit to enforce flake8 and pip-compile.
188
+ - Remove wheel from `build-system.requires` (on setuptools advice).
189
+ - Test against Python 3.11 and 3.12.
190
+ - Run CI workflow against pull requests.
191
+
192
+ ### 0.5
193
+
194
+ - Map port 0 to an unused port.
195
+
186
196
  ### 0.4
187
197
 
188
198
  - Allow `call_soon_threadsafe` from the same thread.
189
199
  - Don't warn when `SO_KEEPALIVE` is set on a socket.
190
200
  - Update instructions for use with aiohttp.
191
- - Add a pyproject.toml
201
+ - Add a pyproject.toml.
192
202
 
193
203
  ### 0.3
194
204
 
@@ -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 :: 3 - Alpha",
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-2021 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.4'
22
+ __version__ = '0.6'
@@ -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', 'stream_pairs')
28
28
 
29
29
 
30
30
  class EventLoop(asyncio.selector_events.BaseSelectorEventLoop):
@@ -113,8 +113,14 @@ class EventLoop(asyncio.selector_events.BaseSelectorEventLoop):
113
113
  # TODO: what if host is actually a list?
114
114
  addr = (host, port, 0, 0)
115
115
  sock = _socket.ListenSocket(addr)
116
- else:
117
- addr = sock.getsockname()
116
+ addr = sock.getsockname()
117
+ if addr[1] == 0: # Port 0: pick a free port
118
+ addr_list = list(addr)
119
+ addr_list[1] = 60000
120
+ while tuple(addr_list) in self.__listening_sockets:
121
+ addr_list[1] += 1
122
+ addr = tuple(addr_list)
123
+ sock._sockname = addr
118
124
  if addr in self.__listening_sockets:
119
125
  raise SolipsismError("Reuse of listening addresses is not supported")
120
126
  self.__listening_sockets[addr] = sock
@@ -191,6 +197,11 @@ class EventLoop(asyncio.selector_events.BaseSelectorEventLoop):
191
197
  super()._stop_serving(sock)
192
198
 
193
199
 
200
+ class EventLoopPolicy(asyncio.DefaultEventLoopPolicy):
201
+ def new_event_loop(self) -> EventLoop:
202
+ return EventLoop()
203
+
204
+
194
205
  async def stream_pairs(capacity=None):
195
206
  sock1, sock2 = _socket.socketpair(capacity=capacity)
196
207
  streams1 = await asyncio.open_connection(sock=sock1)
@@ -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
 
@@ -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.6
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 :: 3 - Alpha
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
@@ -84,11 +103,24 @@ The host and port are associated with the event loop, and are remembered until
84
103
  the server is closed. Attempting to connect after closing the server, or to an
85
104
  address that hasn't been registered, will raise a `ConnectionRefusedError`.
86
105
 
106
+ If you don't want the bother of picking non-colliding ports, you can pass a
107
+ port number of 0, and async-solipsism will bind the first port number above
108
+ 60000 that is unused.
109
+
87
110
  ### Integration with pytest-asyncio
88
111
 
89
112
  async-solipsism and pytest-asyncio complement each other well: just write a
90
- custom `event_loop` fixture in your test file or `conftest.py` and it will
91
- 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:
92
124
 
93
125
  ```python
94
126
  @pytest.fixture
@@ -110,14 +142,12 @@ from aiohttp import web, test_utils
110
142
 
111
143
 
112
144
  @pytest.fixture
113
- def event_loop():
114
- loop = async_solipsism.EventLoop()
115
- yield loop
116
- loop.close()
145
+ def event_loop_policy():
146
+ return async_solipsism.EventLoopPolicy()
117
147
 
118
148
 
119
149
  def socket_factory(host, port, family):
120
- return async_solipsism.ListenSocket((host, port if port else 80))
150
+ return async_solipsism.ListenSocket((host, port))
121
151
 
122
152
 
123
153
  async def test_integration():
@@ -134,10 +164,6 @@ the socket factory, although it may be possible to do by monkeypatching. In
134
164
  practice you will probably want to define your own fixtures for the client
135
165
  and server.
136
166
 
137
- If you need to run more than one test server concurrently, you'll need to
138
- extend the socket factory to assign them each a different port
139
- (async-solipsism does not currently handle mapping port 0 to an unused port).
140
-
141
167
  ## Limitations
142
168
 
143
169
  The requirement to have no interaction with the outside world naturally
@@ -167,12 +193,31 @@ Calling functions that are not supported will generally raise
167
193
 
168
194
  ## Changelog
169
195
 
196
+ ### 0.6
197
+
198
+ - Drop support for Python 3.6 and 3.7, which have reached end of life.
199
+ - Add `EventLoopPolicy` to simplify integration with pytest-asyncio 0.23+,
200
+ and use it for the internal tests.
201
+ - Make `Socket.write` and `Socket.recv_into` accept arbitrary buffer-protocol
202
+ objects.
203
+ - Various packaging and developer experience improvements:
204
+ - Put the packaging metadata in pyproject.toml, removing setup.cfg.
205
+ - Pin versions for CI in requirements.txt using pip-compile.
206
+ - Use pre-commit to enforce flake8 and pip-compile.
207
+ - Remove wheel from `build-system.requires` (on setuptools advice).
208
+ - Test against Python 3.11 and 3.12.
209
+ - Run CI workflow against pull requests.
210
+
211
+ ### 0.5
212
+
213
+ - Map port 0 to an unused port.
214
+
170
215
  ### 0.4
171
216
 
172
217
  - Allow `call_soon_threadsafe` from the same thread.
173
218
  - Don't warn when `SO_KEEPALIVE` is set on a socket.
174
219
  - Update instructions for use with aiohttp.
175
- - Add a pyproject.toml
220
+ - Add a pyproject.toml.
176
221
 
177
222
  ### 0.3
178
223
 
@@ -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
@@ -1,4 +1,4 @@
1
- # Copyright 2020 Bruce Merry
1
+ # Copyright 2020, 2022 Bruce Merry
2
2
  #
3
3
  # This file is part of async-solipsism.
4
4
  #
@@ -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,24 @@ async def test_server(event_loop, manual_socket):
156
162
  await server.wait_closed()
157
163
 
158
164
 
159
- async def test_close_server(event_loop):
165
+ async def test_unused_port():
166
+ def callback(reader, writer):
167
+ pass
168
+
169
+ listen_socket1 = async_solipsism.ListenSocket(('localhost', 0))
170
+ listen_socket2 = async_solipsism.ListenSocket(('localhost', 0))
171
+ server1 = await asyncio.start_server(callback, sock=listen_socket1)
172
+ server2 = await asyncio.start_server(callback, sock=listen_socket2)
173
+ assert listen_socket1.getsockname()[1] == 60000
174
+ assert listen_socket2.getsockname()[1] == 60001
175
+ server1.close()
176
+ server2.close()
177
+ await server1.wait_closed()
178
+ await server2.wait_closed()
179
+
180
+
181
+ async def test_close_server():
182
+ event_loop = asyncio.get_running_loop()
160
183
  server = await asyncio.start_server(lambda reader, writer: None, 'test.invalid', 1234)
161
184
  server.close()
162
185
  await server.wait_closed()
@@ -164,18 +187,21 @@ async def test_close_server(event_loop):
164
187
  await event_loop.create_connection('test.invalid', 1234)
165
188
 
166
189
 
167
- async def test_create_connection_no_listener(event_loop):
190
+ async def test_create_connection_no_listener():
191
+ event_loop = asyncio.get_running_loop()
168
192
  with pytest.raises(ConnectionRefusedError):
169
193
  await event_loop.create_connection('test.invalid', 1234)
170
194
 
171
195
 
172
- async def test_run_in_executor_implicit(event_loop):
196
+ async def test_run_in_executor_implicit():
197
+ event_loop = asyncio.get_running_loop()
173
198
  thread_id = await event_loop.run_in_executor(None, threading.get_ident)
174
199
  assert isinstance(thread_id, int)
175
200
  assert thread_id != threading.get_ident()
176
201
 
177
202
 
178
- async def test_run_in_executor_explicit(event_loop):
203
+ async def test_run_in_executor_explicit():
204
+ event_loop = asyncio.get_running_loop()
179
205
  my_executor = concurrent.futures.ThreadPoolExecutor(1)
180
206
  expected_thread_id = my_executor.submit(threading.get_ident).result()
181
207
  assert isinstance(expected_thread_id, int)
@@ -185,7 +211,8 @@ async def test_run_in_executor_explicit(event_loop):
185
211
 
186
212
 
187
213
  @pytest.mark.skipif("sys.version_info < (3, 7)")
188
- async def test_sendfile(event_loop, tmp_path):
214
+ async def test_sendfile(tmp_path):
215
+ event_loop = asyncio.get_running_loop()
189
216
  tmp_file = tmp_path / 'test_sendfile.txt'
190
217
  tmp_file.write_bytes(b'Hello world\n')
191
218
  ((reader1, writer1), (reader2, writer2)) = await async_solipsism.stream_pairs()
@@ -193,16 +220,23 @@ async def test_sendfile(event_loop, tmp_path):
193
220
  await event_loop.sendfile(writer1.transport, f)
194
221
  line = await reader2.readline()
195
222
  assert line == b'Hello world\n'
223
+ writer1.close()
224
+ writer2.close()
225
+ await writer1.wait_closed()
226
+ await writer2.wait_closed()
196
227
 
197
228
 
198
- async def test_call_soon_threadsafe(event_loop):
229
+ async def test_call_soon_threadsafe():
230
+ event_loop = asyncio.get_running_loop()
199
231
  future = event_loop.create_future()
200
232
  event_loop.call_soon_threadsafe(future.set_result, 3)
201
233
  result = await future
202
234
  assert result == 3
203
235
 
204
236
 
205
- 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
+
206
240
  def thread_func():
207
241
  event_loop.call_soon_threadsafe(lambda: None)
208
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