async-solipsism


Nameasync-solipsism JSON
Version 0.6 PyPI version JSON
download
home_pageNone
SummaryAsyncio event loop that doesn't interact with the outside world
upload_time2024-03-23 20:25:47
maintainerNone
docs_urlNone
authorNone
requires_python>=3.8
licenseNone
keywords
VCS
bugtrack_url
requirements cfgv distlib exceptiongroup filelock identify iniconfig nodeenv packaging platformdirs pluggy pre-commit pytest pytest-asyncio pytest-mock pyyaml tomli virtualenv
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # A solipsist event loop

async-solipsism provides a Python asyncio event loop that does not interact with
the outside world at all. This is ideal for writing unit tests that intend to
mock out real-world interactions. It makes for tests that are reliable
(unaffected by network outages), reproducible (not affected by random timing
effects) and portable (run the same everywhere).

## Features

### Clock

A very handy feature is that time runs infinitely fast! What's more, time
advances only when explicitly waiting. For example, this code will print out
two times that are exactly 60s apart, and will take negligible real time to
run:

```python
print(loop.time())
await asyncio.sleep(60)
print(loop.time())
```

This also provides a handy way to ensure that all pending callbacks have a
chance to run: just sleep for a second.

The simulated clock has microsecond resolution, independent of whatever
resolution the system clock has. This helps ensure that tests behave the same
across operating systems.

Sometimes buggy code or a buggy test will await an event that will never
happen. For example, it might wait for data to arrive on a socket, but forget
to insert data into the other end. If async-solipsism detects that it will
never wake up again, it will raise a `SleepForeverError` rather than leaving
your test to hang.

### Sockets

While real sockets cannot be used, async-solipsism provides mock sockets that
implement just enough functionality to be used with the event loop. Sockets
are obtained by calling `async_solipsism.socketpair()`, which returns two
sockets that are connected to each other. They can then be used with event
loop functions like `sock_sendall` or `create_connection`.

Because the socket implementation is minimal, you may run into cases where
the internals of asyncio try to call methods that aren't implemented. Pull
requests are welcome.

Each direction of flow implements a buffer that holds data written to the one
socket but not yet received by the other. If this buffer fills up, write calls
will raise `BlockingIOError`, just like a real non-blocking socket. This can
be used to test that your protocol properly handles flow control. The size of
these buffers can be changed with the optional `capacity` argument to
`socketpair`.

### Streams

As a convenience, it is possible to open two pairs of streams that are
connected to each other, with

```
((reader1, writer1), (reader2, writer2)) = await async_solipsism.stream_pairs()
```

Anything written to `writer1` will be received by `reader2`, and anything
written to `writer2` will be received by `reader1`.

### Servers

It is also possible to use the asyncio functions for starting servers and
connecting to them. You can supply any host name and port, even if they're not
actually associated with the machine! For example,

```python
server = await asyncio.start_server(callback, 'test.invalid', 1234)
reader, writer = await asyncio.open_connection('test.invalid', 1234)
```

will start a server, then open a client connection to it. The `reader` and
`writer` represent the client end of the connection, and the `callback` will
be given the server end of the connection.

The host and port are associated with the event loop, and are remembered until
the server is closed. Attempting to connect after closing the server, or to an
address that hasn't been registered, will raise a `ConnectionRefusedError`.

If you don't want the bother of picking non-colliding ports, you can pass a
port number of 0, and async-solipsism will bind the first port number above
60000 that is unused.

### Integration with pytest-asyncio

async-solipsism and pytest-asyncio complement each other well: just write a
custom `event_loop_policy` fixture in your test file or `conftest.py` and it
will override the default provided by pytest-asyncio:

```python
@pytest.fixture
def event_loop_policy():
    return async_solipsism.EventLoopPolicy()
```

The above is for pytest-asyncio 0.23+. If you're using an older version, then
instead you should do this:

```python
@pytest.fixture
def event_loop():
    loop = async_solipsism.EventLoop()
    yield loop
    loop.close()
```

### Integration with pytest-aiohttp

A little extra work is required to work with aiohttp's test utilities, but it
is possible. The example below requires at least aiohttp 3.8.0.

```python
import async_solipsism
import pytest
from aiohttp import web, test_utils


@pytest.fixture
def event_loop_policy():
    return async_solipsism.EventLoopPolicy()


def socket_factory(host, port, family):
    return async_solipsism.ListenSocket((host, port))


async def test_integration():
    app = web.Application()
    async with test_utils.TestServer(app, socket_factory=socket_factory) as server:
        async with test_utils.TestClient(server) as client:
            resp = await client.post("/hey", json={})
            assert resp.status == 404
```

Note that this relies on pytest-asyncio (in auto mode) and does not use
pytest-aiohttp. The fixtures provided by the latter do not support overriding
the socket factory, although it may be possible to do by monkeypatching. In
practice you will probably want to define your own fixtures for the client
and server.

## Limitations

The requirement to have no interaction with the outside world naturally
imposes some restrictions. Other restrictions exist purely because I haven't
gotten around to figuring out what a fake version should look like and
implementing it. The following are all unsupported:

- `call_soon_threadsafe`, except when called from the thread running the
  event loop (it just forwards to `call_soon`). Multithreading is
  fundamentally incompatible with the fast-forward clock.
- `getaddrinfo` and `getnameinfo`
- `connect_read_pipe` and `connect_write_pipe`
- signal handlers
- subprocesses
- TLS/SSL
- datagrams (UDP)
- UNIX domain sockets
- any Windows-specific features

`run_in_executor` is supported, but it blocks the event loop while the task
runs in the executor. This works fine for short-running tasks like reading
some data from a file, but is not suitable if the task is a long-running one
such as a sidecar server.

Calling functions that are not supported will generally raise
`SolipsismError`.

## Changelog

### 0.6

- Drop support for Python 3.6 and 3.7, which have reached end of life.
- Add `EventLoopPolicy` to simplify integration with pytest-asyncio 0.23+,
  and use it for the internal tests.
- Make `Socket.write` and `Socket.recv_into` accept arbitrary buffer-protocol
  objects.
- Various packaging and developer experience improvements:
  - Put the packaging metadata in pyproject.toml, removing setup.cfg.
  - Pin versions for CI in requirements.txt using pip-compile.
  - Use pre-commit to enforce flake8 and pip-compile.
  - Remove wheel from `build-system.requires` (on setuptools advice).
  - Test against Python 3.11 and 3.12.
  - Run CI workflow against pull requests.

### 0.5

- Map port 0 to an unused port.

### 0.4

- Allow `call_soon_threadsafe` from the same thread.
- Don't warn when `SO_KEEPALIVE` is set on a socket.
- Update instructions for use with aiohttp.
- Add a pyproject.toml.

### 0.3

- Fix `start_server` with an explicit socket.
- Update README with an example of aiohttp integration.

### 0.2

- Numerous fixes to make the fake sockets behave more like real ones.
- Sockets now return IPv6 addresses from `getsockname`.
- Implement `setsockopt`.
- Introduce `SolipsismWarning` base class for warnings.

### 0.1

First release.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "async-solipsism",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.8",
    "maintainer_email": null,
    "keywords": null,
    "author": null,
    "author_email": "Bruce Merry <bmerry@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/7b/9b/8082b7518a8ae7526e9afbc052ffbad89b52d72cb888838917834a0ca7f4/async-solipsism-0.6.tar.gz",
    "platform": null,
    "description": "# A solipsist event loop\n\nasync-solipsism provides a Python asyncio event loop that does not interact with\nthe outside world at all. This is ideal for writing unit tests that intend to\nmock out real-world interactions. It makes for tests that are reliable\n(unaffected by network outages), reproducible (not affected by random timing\neffects) and portable (run the same everywhere).\n\n## Features\n\n### Clock\n\nA very handy feature is that time runs infinitely fast! What's more, time\nadvances only when explicitly waiting. For example, this code will print out\ntwo times that are exactly 60s apart, and will take negligible real time to\nrun:\n\n```python\nprint(loop.time())\nawait asyncio.sleep(60)\nprint(loop.time())\n```\n\nThis also provides a handy way to ensure that all pending callbacks have a\nchance to run: just sleep for a second.\n\nThe simulated clock has microsecond resolution, independent of whatever\nresolution the system clock has. This helps ensure that tests behave the same\nacross operating systems.\n\nSometimes buggy code or a buggy test will await an event that will never\nhappen. For example, it might wait for data to arrive on a socket, but forget\nto insert data into the other end. If async-solipsism detects that it will\nnever wake up again, it will raise a `SleepForeverError` rather than leaving\nyour test to hang.\n\n### Sockets\n\nWhile real sockets cannot be used, async-solipsism provides mock sockets that\nimplement just enough functionality to be used with the event loop. Sockets\nare obtained by calling `async_solipsism.socketpair()`, which returns two\nsockets that are connected to each other. They can then be used with event\nloop functions like `sock_sendall` or `create_connection`.\n\nBecause the socket implementation is minimal, you may run into cases where\nthe internals of asyncio try to call methods that aren't implemented. Pull\nrequests are welcome.\n\nEach direction of flow implements a buffer that holds data written to the one\nsocket but not yet received by the other. If this buffer fills up, write calls\nwill raise `BlockingIOError`, just like a real non-blocking socket. This can\nbe used to test that your protocol properly handles flow control. The size of\nthese buffers can be changed with the optional `capacity` argument to\n`socketpair`.\n\n### Streams\n\nAs a convenience, it is possible to open two pairs of streams that are\nconnected to each other, with\n\n```\n((reader1, writer1), (reader2, writer2)) = await async_solipsism.stream_pairs()\n```\n\nAnything written to `writer1` will be received by `reader2`, and anything\nwritten to `writer2` will be received by `reader1`.\n\n### Servers\n\nIt is also possible to use the asyncio functions for starting servers and\nconnecting to them. You can supply any host name and port, even if they're not\nactually associated with the machine! For example,\n\n```python\nserver = await asyncio.start_server(callback, 'test.invalid', 1234)\nreader, writer = await asyncio.open_connection('test.invalid', 1234)\n```\n\nwill start a server, then open a client connection to it. The `reader` and\n`writer` represent the client end of the connection, and the `callback` will\nbe given the server end of the connection.\n\nThe host and port are associated with the event loop, and are remembered until\nthe server is closed. Attempting to connect after closing the server, or to an\naddress that hasn't been registered, will raise a `ConnectionRefusedError`.\n\nIf you don't want the bother of picking non-colliding ports, you can pass a\nport number of 0, and async-solipsism will bind the first port number above\n60000 that is unused.\n\n### Integration with pytest-asyncio\n\nasync-solipsism and pytest-asyncio complement each other well: just write a\ncustom `event_loop_policy` fixture in your test file or `conftest.py` and it\nwill override the default provided by pytest-asyncio:\n\n```python\n@pytest.fixture\ndef event_loop_policy():\n    return async_solipsism.EventLoopPolicy()\n```\n\nThe above is for pytest-asyncio 0.23+. If you're using an older version, then\ninstead you should do this:\n\n```python\n@pytest.fixture\ndef event_loop():\n    loop = async_solipsism.EventLoop()\n    yield loop\n    loop.close()\n```\n\n### Integration with pytest-aiohttp\n\nA little extra work is required to work with aiohttp's test utilities, but it\nis possible. The example below requires at least aiohttp 3.8.0.\n\n```python\nimport async_solipsism\nimport pytest\nfrom aiohttp import web, test_utils\n\n\n@pytest.fixture\ndef event_loop_policy():\n    return async_solipsism.EventLoopPolicy()\n\n\ndef socket_factory(host, port, family):\n    return async_solipsism.ListenSocket((host, port))\n\n\nasync def test_integration():\n    app = web.Application()\n    async with test_utils.TestServer(app, socket_factory=socket_factory) as server:\n        async with test_utils.TestClient(server) as client:\n            resp = await client.post(\"/hey\", json={})\n            assert resp.status == 404\n```\n\nNote that this relies on pytest-asyncio (in auto mode) and does not use\npytest-aiohttp. The fixtures provided by the latter do not support overriding\nthe socket factory, although it may be possible to do by monkeypatching. In\npractice you will probably want to define your own fixtures for the client\nand server.\n\n## Limitations\n\nThe requirement to have no interaction with the outside world naturally\nimposes some restrictions. Other restrictions exist purely because I haven't\ngotten around to figuring out what a fake version should look like and\nimplementing it. The following are all unsupported:\n\n- `call_soon_threadsafe`, except when called from the thread running the\n  event loop (it just forwards to `call_soon`). Multithreading is\n  fundamentally incompatible with the fast-forward clock.\n- `getaddrinfo` and `getnameinfo`\n- `connect_read_pipe` and `connect_write_pipe`\n- signal handlers\n- subprocesses\n- TLS/SSL\n- datagrams (UDP)\n- UNIX domain sockets\n- any Windows-specific features\n\n`run_in_executor` is supported, but it blocks the event loop while the task\nruns in the executor. This works fine for short-running tasks like reading\nsome data from a file, but is not suitable if the task is a long-running one\nsuch as a sidecar server.\n\nCalling functions that are not supported will generally raise\n`SolipsismError`.\n\n## Changelog\n\n### 0.6\n\n- Drop support for Python 3.6 and 3.7, which have reached end of life.\n- Add `EventLoopPolicy` to simplify integration with pytest-asyncio 0.23+,\n  and use it for the internal tests.\n- Make `Socket.write` and `Socket.recv_into` accept arbitrary buffer-protocol\n  objects.\n- Various packaging and developer experience improvements:\n  - Put the packaging metadata in pyproject.toml, removing setup.cfg.\n  - Pin versions for CI in requirements.txt using pip-compile.\n  - Use pre-commit to enforce flake8 and pip-compile.\n  - Remove wheel from `build-system.requires` (on setuptools advice).\n  - Test against Python 3.11 and 3.12.\n  - Run CI workflow against pull requests.\n\n### 0.5\n\n- Map port 0 to an unused port.\n\n### 0.4\n\n- Allow `call_soon_threadsafe` from the same thread.\n- Don't warn when `SO_KEEPALIVE` is set on a socket.\n- Update instructions for use with aiohttp.\n- Add a pyproject.toml.\n\n### 0.3\n\n- Fix `start_server` with an explicit socket.\n- Update README with an example of aiohttp integration.\n\n### 0.2\n\n- Numerous fixes to make the fake sockets behave more like real ones.\n- Sockets now return IPv6 addresses from `getsockname`.\n- Implement `setsockopt`.\n- Introduce `SolipsismWarning` base class for warnings.\n\n### 0.1\n\nFirst release.\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Asyncio event loop that doesn't interact with the outside world",
    "version": "0.6",
    "project_urls": {
        "Homepage": "https://github.com/bmerry/async-solipsism"
    },
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "5d7e98874bbc41f0fbe03528ca0ed60d0bad4ec4eda72e5c8309e205883cc4c0",
                "md5": "566990e0ed994e3bd3e147f5f2132345",
                "sha256": "8566ac1a524f4fcea37fac2f1ae94ac777e348e66e6336792b73f678276efd09"
            },
            "downloads": -1,
            "filename": "async_solipsism-0.6-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "566990e0ed994e3bd3e147f5f2132345",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.8",
            "size": 25362,
            "upload_time": "2024-03-23T20:25:45",
            "upload_time_iso_8601": "2024-03-23T20:25:45.313952Z",
            "url": "https://files.pythonhosted.org/packages/5d/7e/98874bbc41f0fbe03528ca0ed60d0bad4ec4eda72e5c8309e205883cc4c0/async_solipsism-0.6-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "7b9b8082b7518a8ae7526e9afbc052ffbad89b52d72cb888838917834a0ca7f4",
                "md5": "90003b655f7bd68189173a8a62f7dbc3",
                "sha256": "f03b8dbb661bab667c926637abb9cc3b81db3d10a83d8217def1576a30134c90"
            },
            "downloads": -1,
            "filename": "async-solipsism-0.6.tar.gz",
            "has_sig": false,
            "md5_digest": "90003b655f7bd68189173a8a62f7dbc3",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.8",
            "size": 28955,
            "upload_time": "2024-03-23T20:25:47",
            "upload_time_iso_8601": "2024-03-23T20:25:47.348280Z",
            "url": "https://files.pythonhosted.org/packages/7b/9b/8082b7518a8ae7526e9afbc052ffbad89b52d72cb888838917834a0ca7f4/async-solipsism-0.6.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-03-23 20:25:47",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "bmerry",
    "github_project": "async-solipsism",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [
        {
            "name": "cfgv",
            "specs": [
                [
                    "==",
                    "3.4.0"
                ]
            ]
        },
        {
            "name": "distlib",
            "specs": [
                [
                    "==",
                    "0.3.8"
                ]
            ]
        },
        {
            "name": "exceptiongroup",
            "specs": [
                [
                    "==",
                    "1.2.0"
                ]
            ]
        },
        {
            "name": "filelock",
            "specs": [
                [
                    "==",
                    "3.13.1"
                ]
            ]
        },
        {
            "name": "identify",
            "specs": [
                [
                    "==",
                    "2.5.35"
                ]
            ]
        },
        {
            "name": "iniconfig",
            "specs": [
                [
                    "==",
                    "2.0.0"
                ]
            ]
        },
        {
            "name": "nodeenv",
            "specs": [
                [
                    "==",
                    "1.8.0"
                ]
            ]
        },
        {
            "name": "packaging",
            "specs": [
                [
                    "==",
                    "24.0"
                ]
            ]
        },
        {
            "name": "platformdirs",
            "specs": [
                [
                    "==",
                    "4.2.0"
                ]
            ]
        },
        {
            "name": "pluggy",
            "specs": [
                [
                    "==",
                    "1.4.0"
                ]
            ]
        },
        {
            "name": "pre-commit",
            "specs": [
                [
                    "==",
                    "3.5.0"
                ]
            ]
        },
        {
            "name": "pytest",
            "specs": [
                [
                    "==",
                    "8.1.1"
                ]
            ]
        },
        {
            "name": "pytest-asyncio",
            "specs": [
                [
                    "==",
                    "0.23.6"
                ]
            ]
        },
        {
            "name": "pytest-mock",
            "specs": [
                [
                    "==",
                    "3.14.0"
                ]
            ]
        },
        {
            "name": "pyyaml",
            "specs": [
                [
                    "==",
                    "6.0.1"
                ]
            ]
        },
        {
            "name": "tomli",
            "specs": [
                [
                    "==",
                    "2.0.1"
                ]
            ]
        },
        {
            "name": "virtualenv",
            "specs": [
                [
                    "==",
                    "20.25.1"
                ]
            ]
        }
    ],
    "lcname": "async-solipsism"
}
        
Elapsed time: 0.26869s