<!--
Copyright (c) 2023 Eliah Kagan
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
-->
# subaudit: Subscribe and unsubscribe for specific audit events
[Audit hooks](https://docs.python.org/3/library/audit_events.html) in Python
are called on all events, and they remain in place until the interpreter shuts
down.
This library provides a higher-level interface that allows listeners to be
subscribed to specific audit events, and unsubscribed from them. It provides
[context managers](https://github.com/EliahKagan/subaudit#basic-usage) for
using that interface with a convenient notation that ensures the listener is
unsubscribed. The context managers are reentrant—you can nest `with`-statements
that listen to events. By default, a single audit hook is used for any number
of events and listeners.
The primary use case for this library is in writing test code.
## License
subaudit is licensed under [0BSD](https://spdx.org/licenses/0BSD.html), which
is a [“public-domain
equivalent”](https://en.wikipedia.org/wiki/Public-domain-equivalent_license)
license. See
[**`LICENSE`**](https://github.com/EliahKagan/subaudit/blob/main/LICENSE).
## Compatibility
The subaudit library can be used to observe [audit events generated by the
Python interpreter and standard
library](https://docs.python.org/3/library/audit_events.html), as well as
custom audit events. It requires Python 3.7 or later. It is most useful on
Python 3.8 or later, because [audit events were introduced in Python
3.8](https://peps.python.org/pep-0578/). On Python 3.7, subaudit uses [the
*sysaudit* library](https://pypi.org/project/sysaudit/) to support audit
events, but the Python interpreter and standard library still do not provide
any events, so only custom events can be used on Python 3.7.
To avoid the performance cost of explicit locking in the audit hook, [some
operations are assumed atomic](https://github.com/EliahKagan/subaudit#locking).
I believe these assumptions are correct for CPython, as well as PyPy and some
other implementations, but there may exist Python implementations on which
these assumptions don’t hold.
## Installation <!-- Maybe remove this section once badges are added. -->
Install [the `subaudit` package (PyPI)](https://pypi.org/project/subaudit/) in
your project’s environment.
## Basic usage
### The `subaudit.listening` context manager
The best way to use subaudit is usually the `listening` context manager.
```python
import subaudit
def listen_open(path, mode, flags):
... # Handle the event.
with subaudit.listening('open', listen_open):
... # Do something that may raise the event.
```
The listener—here, `listen_open`—is called with the event arguments each time
the event is raised. They are passed to the listener as separate positional
arguments (not as an `args` tuple).
In tests, it is convenient to use [`Mock`
objects](https://docs.python.org/3/library/unittest.mock.html#the-mock-class)
as listeners, because they record calls, provide a
[`mock_calls`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.mock_calls)
attribute to see the calls, and provide [various `assert_*`
methods](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called)
to make assertions about the calls:
```python
from unittest.mock import ANY, Mock
import subaudit
with subaudit.listening('open', Mock()) as listener:
... # Do something that may raise the event.
listener.assert_any_call('/path/to/file.txt', 'r', ANY)
```
Note how, when the `listening` context manager is entered, it returns the
`listener` that was passed in, for convenience.
### The `subaudit.extracting` context manager
You may want to extract some information about calls to a list:
```python
from dataclasses import InitVar, dataclass
import subaudit
@dataclass(frozen=True)
class PathAndMode: # Usually strings. See examples/notebooks/open_event.ipynb.
path: str
mode: str
flags: InitVar = None # Opt not to record this argument.
with subaudit.extracting('open', PathAndMode) as extracts:
... # Do something that may raise the event.
assert PathAndMode('/path/to/file.txt', 'r') in extracts
```
The extractor—here, `PathAndMode`—can be any callable that accepts the event
args as separate positional arguments. Entering the context manager returns an
initially empty list, which will be populated with *extracts* gleaned from the
event args. Each time the event is raised, the extractor is called and the
object it returns is appended to the list.
### `subaudit.subscribe` and `subaudit.unsubscribe`
Although you should usually use the `listening` or `extracting` context
managers instead, you can subscribe and unsubscribe listeners without a context
manager:
```python
import subaudit
def listen_open(path, mode, flags):
... # Handle the event.
subaudit.subscribe('open', listen_open)
try:
... # Do something that may raise the event.
finally:
subaudit.unsubscribe('open', listen_open)
```
Attempting to unsubscribe a listener that is not subscribed raises
`ValueError`. Currently, subaudit provides no feature to make this succeed
silently instead. But you can suppress the exception:
```python
with contextlib.suppress(ValueError):
subaudit.unsubscribe('glob.glob', possibly_subscribed_listener)
```
## Nesting
To unsubscribe a listener from an event, it must be subscribed to the event.
Subject to this restriction, calls to [`subscribe` and
`unsubscribe`](https://github.com/EliahKagan/subaudit#subauditsubscribe-and-subauditunsubscribe)
can happen in any order, and
[`listening`](https://github.com/EliahKagan/subaudit#the-subauditlistening-context-manager)
and
[`extracting`](https://github.com/EliahKagan/subaudit#the-subauditextracting-context-manager)
may be arbitrarily nested.
`listening` and `extracting` support reentrant use with both the same event and
different events. Here’s an example with three `listening` contexts:
```python
from unittest.mock import Mock, call
listen_to = Mock() # Let us assert calls to child mocks in a specific order.
with subaudit.listening('open', print): # Print all open events' arguments.
with subaudit.listening('open', listen_to.open): # Log opening.
with subaudit.listening('glob.glob', listen_to.glob): # Log globbing.
... # Do something that may raise the events.
assert listen_to.mock_calls == ... # Assert a specific order of calls.
```
(That is written out to make the nesting clear. You could also use a single
`with`-statement with commas.)
Here’s an example with both `listening` and `extracting` contexts:
```python
from unittest.mock import Mock, call
def extract(*args):
return args
with (
subaudit.extracting('pathlib.Path.glob', extract) as glob_extracts,
subaudit.listening('pathlib.Path.glob', Mock()) as glob_listener,
subaudit.extracting('pathlib.Path.rglob', extract) as rglob_extracts,
subaudit.listening('pathlib.Path.rglob', Mock()) as rglob_listener,
):
... # Do something that may raise the events.
# Assert something about, or otherwise use, the mocks glob_listener and
# rglob_listener, as well as the lists glob_extracts and rglob_extracts.
...
```
(That example uses [parenthesized context
managers](https://docs.python.org/3/whatsnew/3.10.html#parenthesized-context-managers),
which were introduced in Python 3.10.)
## Specialized usage
### `subaudit.Hook` objects
Each instance of the `subaudit.Hook` class represents a single audit hook that
supports subscribing and unsubscribing listeners for any number of events, with
methods corresponding to the four top-level functions listed above. Separate
`Hook` instances use separate audit hooks. The `Hook` class exists for three
purposes:
- It supplies the behavior of [the top-level `listening`, `extracting`,
`subscribe`, and `unsubscribe`
functions](https://github.com/EliahKagan/subaudit#basic-usage), which
correspond to the same-named methods on a global `Hook` instance.
- It allows multiple audit hooks to be used, for special cases where that might
be desired.
- It facilitates customization, as detailed below.
The actual audit hook that a `Hook` object encapsulates is not installed until
the first listener is subscribed. This happens on the first call to its
`subscribe` method, or the first time one of its context managers (from calling
its `listening` or `extracting` method) is entered. This is also true of the
global `Hook` instance used by the top-level functions—merely importing
`subaudit` does not install an audit hook.
Whether the top-level functions are bound methods of a `Hook` instance, or
delegate in some other way to those methods on an instance, is currently
considered an implementation detail.
### Deriving from `Hook`
You can derive from `Hook` to provide custom behavior for subscribing and
unsubscribing, by overriding the `subscribe` and `unsubscribe` methods. You can
also override the `listening` and `extracting` methods, though that may be less
useful. Overridden `subscribe` and `unsubscribe` methods are automatically used
by `listening` and `extracting`.
Whether `extracting` uses `listening`, or directly calls `subscribe` and
`unsubscribe`, is currently considered an implementation detail.
### Locking
Consider two possible cases of race conditions:
#### 1. Between audit hook and `subscribe`/`unsubscribe` (audit hook does not lock)
In this scenario, a `Hook` object’s installed audit hook runs at the same time
as a listener is subscribed or unsubscribed.
This is likely to occur often and it cannot be prevented, because audit hooks
are called for all audit events. For the same reason, locking in the audit hook
has performance implications. Instead of having audit hooks take locks,
subaudit relies on each of these operations being atomic:
- *Writing an attribute reference, when it is a simple write to an instance
dictionary or a slot.* Writing an attribute need not be atomic when, for
example, `__setattr__` has been overridden.
- *Writing or deleting a ``str`` key in a dictionary whose keys are all of the
built-in ``str`` type.* Note that the search need not be atomic, but the
dictionary must always be observed to be in a valid state.
The audit hook is written, and the data structures it uses are selected, to
avoid relying on more than these assumptions.
#### 2. Between calls to `subscribe`/`unsubscribe` (by default, they lock)
In this scenario, two listeners are subscribed at a time, or unsubscribed at a
time, or one listener is subscribed while another (or the same) listener is
unsubscribed.
This is less likely to occur and much easier to avoid. But it is also harder to
make safe without a lock. Subscribing and unsubscribing are unlikely to happen
at a *sustained* high rate, so locking is unlikely to be a performance
bottleneck. So, *by default*, subscribing and unsubscribing are synchronized
with a
[`threading.Lock`](https://docs.python.org/3/library/threading.html#threading.Lock),
to ensure that shared state is not corrupted.
You should not usually change this. But if you want to, you can construct a
`Hook` object by calling `Hook(sub_lock_factory=...)` instead of `Hook`, where
`...` is a type, or other context manager factory, to be used instead of
`threading.Lock`. In particular, to disable locking, pass
[`contextlib.nullcontext`](https://docs.python.org/3/library/contextlib.html#contextlib.nullcontext).
## Functions related to compatibility
As [noted above](https://github.com/EliahKagan/subaudit#compatibility), Python
supports audit hooks [since 3.8](https://peps.python.org/pep-0578/). For Python
3.7, but not Python 3.8 or later, the subaudit library declares
[sysaudit](https://pypi.org/project/sysaudit/) as a dependency.
### `subaudit.addaudithook` and `subaudit.audit`
subaudit exports `addaudithook` and `audit` functions.
- On Python 3.8 and later, they are
[`sys.addaudithook`](https://docs.python.org/3/library/sys.html#sys.addaudithook)
and [`sys.audit`](https://docs.python.org/3/library/sys.html#sys.audit).
- On Python 3.7, they are
[`sysaudit.addaudithook`](https://sysaudit.readthedocs.io/en/latest/#sysaudit.addaudithook)
and
[`sysaudit.audit`](https://sysaudit.readthedocs.io/en/latest/#sysaudit.audit).
subaudit uses `subaudit.addaudithook` when it adds its own audit hook (or all
its own hooks, if you use additional
[`Hook`](https://github.com/EliahKagan/subaudit#subaudithook-objects) instances
besides the global one implicitly used by [the top-level
functions](https://github.com/EliahKagan/subaudit#basic-usage)). subaudit does
not itself use `subaudit.audit`, but it is whichever `audit` function
corresponds to `subaudit.addaudithook`.
### `@subaudit.skip_if_unavailable`
The primary use case for subaudit is in writing unit tests, to assert that
particular events have been raised or not raised. Usually these are [“built in”
events](https://docs.python.org/3.8/library/audit_events.html)—those raised by
the Python interpreter or standard library. But the sysaudit library doesn’t
backport those events, which would not really be feasible to do.
For this reason, tests that particular audit events did or didn’t occur—such as
a test that a file has been opened by listening to the `open` event—should
typically be skipped when running a test suite on Python 3.7.
**When using the [unittest](https://docs.python.org/3/library/unittest.html)
framework**, you can apply the `@skip_if_unavailable` decorator to a test class
or test method, so it is skipped prior to Python 3.8 with a message explaining
why. For example:
```python
import unittest
from unittest.mock import ANY, Mock
import subaudit
class TestSomeThings(unittest.TestCase):
...
@subaudit.skip_if_unavailable # Skip this test if < 3.8, with a message.
def test_file_is_opened_for_read(self):
with subaudit.listening('open', Mock()) as listener:
... # Do something that may raise the event.
listener.assert_any_call('/path/to/file.txt', 'r', ANY)
...
@subaudit.skip_if_unavailable # Skip the whole class if < 3.8, with a message.
class TestSomeMoreThings(unittest.TestCase):
...
```
It could be useful also to have a conditional xfail ([expected
failure](https://docs.python.org/3/library/unittest.html#unittest.expectedFailure))
decorator for unittest—and, more so,
[marks](https://docs.pytest.org/en/7.1.x/how-to/mark.html) for
[pytest](https://docs.pytest.org/) providing specialized
[skip](https://docs.pytest.org/en/7.3.x/how-to/skipping.html#skipping-test-functions)/[skipif](https://docs.pytest.org/en/7.3.x/how-to/skipping.html#id1)
and
[xfail](https://docs.pytest.org/en/7.3.x/how-to/skipping.html#xfail-mark-test-functions-as-expected-to-fail)—but
subaudit does not currently provide them. Of course, in pytest, you can still
use the [`@pytest.mark.skip` and
`@pytest.mark.xfail`](https://docs.pytest.org/en/7.3.x/how-to/skipping.html)
decorators, by passing `sys.version_info < (3, 8)` as the condition.
## Overview by level of abstraction
From higher to lower level, from the perspective of the top-level
[`listening`](https://github.com/EliahKagan/subaudit#the-subauditlistening-context-manager)
and
[`extracting`](https://github.com/EliahKagan/subaudit#the-subauditextracting-context-manager)
functions:
- [`subaudit.extracting`](https://github.com/EliahKagan/subaudit#the-subauditextracting-context-manager)
\- context manager that listens and extracts to a list
- [`subaudit.listening`](https://github.com/EliahKagan/subaudit#the-subauditlistening-context-manager)
\- context manager to subscribe and unsubscribe a custom listener *(usually
use this)*
- [`subaudit.subscribe` and
`subaudit.unsubscribe`](https://github.com/EliahKagan/subaudit#subauditsubscribe-and-subauditunsubscribe)
\- manually subscribe/unsubscribe a listener
- [`subaudit.Hook`](https://github.com/EliahKagan/subaudit#subaudithook-objects)
\- abstraction around an audit hook allowing subscribing and unsubscribing
for specific events, with `extracting`, `listening`, `subscribe`, and
`unsubscribe` instance methods
- [`subaudit.addaudithook`](https://github.com/EliahKagan/subaudit#subauditaddaudithook-and-subauditaudit)
\- trivial abstraction representing whether the function from `sys` or
`sysaudit` is used
- [`sys.addaudithook`](https://docs.python.org/3/library/sys.html#sys.addaudithook)
or
[`sysaudit.addaudithook`](https://sysaudit.readthedocs.io/en/latest/#sysaudit.addaudithook)
\- *not part of subaudit* \- install a [PEP
578](https://peps.python.org/pep-0578/) audit hook
This list is not exhaustive. For example,
[`@skip_if_unavailable`](https://github.com/EliahKagan/subaudit#subauditskip_if_unavailable)
is not part of that conceptual hierarchy.
## Acknowledgements
I’d like to thank:
- [**Brett Langdon**](https://github.com/brettlangdon), who wrote the
[sysaudit](https://github.com/brettlangdon/sysaudit) library (which subaudit
[uses on 3.7](https://github.com/EliahKagan/subaudit#compatibility)).
- [**David Vassallo**](https://github.com/dmvassallo), for reviewing pull
requests about testing using audit hooks in [a project we have collaborated
on](https://github.com/dmvassallo/EmbeddingScratchwork), which helped me to
recognize what kinds of usage were more or less clear and that it could be
good to have a library like subaudit; and for coauthoring a
`@skip_if_unavailable` decorator that had been used there, which motivated
the one here.
## About the name
This library is called “subaudit” because it provides a way to effectively
*sub*scribe to and un*sub*scribe from a *sub*set of audit events rather than
all of them.
Raw data
{
"_id": null,
"home_page": "https://github.com/EliahKagan/subaudit",
"name": "subaudit",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.7,<4.0",
"maintainer_email": "",
"keywords": "audit,auditing,events,subscribe,unsubscribe",
"author": "Eliah Kagan",
"author_email": "degeneracypressure@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/8e/37/0f2de8c0478b8047250dd853042bb349aec2d5d357621c8785f7aae685ee/subaudit-0.1.0.tar.gz",
"platform": null,
"description": "<!--\n Copyright (c) 2023 Eliah Kagan\n\n Permission to use, copy, modify, and/or distribute this software for any\n purpose with or without fee is hereby granted.\n\n THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\n REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\n AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\n INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\n LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\n OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\n PERFORMANCE OF THIS SOFTWARE.\n-->\n\n# subaudit: Subscribe and unsubscribe for specific audit events\n\n[Audit hooks](https://docs.python.org/3/library/audit_events.html) in Python\nare called on all events, and they remain in place until the interpreter shuts\ndown.\n\nThis library provides a higher-level interface that allows listeners to be\nsubscribed to specific audit events, and unsubscribed from them. It provides\n[context managers](https://github.com/EliahKagan/subaudit#basic-usage) for\nusing that interface with a convenient notation that ensures the listener is\nunsubscribed. The context managers are reentrant\u2014you can nest `with`-statements\nthat listen to events. By default, a single audit hook is used for any number\nof events and listeners.\n\nThe primary use case for this library is in writing test code.\n\n## License\n\nsubaudit is licensed under [0BSD](https://spdx.org/licenses/0BSD.html), which\nis a [\u201cpublic-domain\nequivalent\u201d](https://en.wikipedia.org/wiki/Public-domain-equivalent_license)\nlicense. See\n[**`LICENSE`**](https://github.com/EliahKagan/subaudit/blob/main/LICENSE).\n\n## Compatibility\n\nThe subaudit library can be used to observe [audit events generated by the\nPython interpreter and standard\nlibrary](https://docs.python.org/3/library/audit_events.html), as well as\ncustom audit events. It requires Python 3.7 or later. It is most useful on\nPython 3.8 or later, because [audit events were introduced in Python\n3.8](https://peps.python.org/pep-0578/). On Python 3.7, subaudit uses [the\n*sysaudit* library](https://pypi.org/project/sysaudit/) to support audit\nevents, but the Python interpreter and standard library still do not provide\nany events, so only custom events can be used on Python 3.7.\n\nTo avoid the performance cost of explicit locking in the audit hook, [some\noperations are assumed atomic](https://github.com/EliahKagan/subaudit#locking).\nI believe these assumptions are correct for CPython, as well as PyPy and some\nother implementations, but there may exist Python implementations on which\nthese assumptions don\u2019t hold.\n\n## Installation <!-- Maybe remove this section once badges are added. -->\n\nInstall [the `subaudit` package (PyPI)](https://pypi.org/project/subaudit/) in\nyour project\u2019s environment.\n\n## Basic usage\n\n### The `subaudit.listening` context manager\n\nThe best way to use subaudit is usually the `listening` context manager.\n\n```python\nimport subaudit\n\ndef listen_open(path, mode, flags):\n ... # Handle the event.\n\nwith subaudit.listening('open', listen_open):\n ... # Do something that may raise the event.\n```\n\nThe listener\u2014here, `listen_open`\u2014is called with the event arguments each time\nthe event is raised. They are passed to the listener as separate positional\narguments (not as an `args` tuple).\n\nIn tests, it is convenient to use [`Mock`\nobjects](https://docs.python.org/3/library/unittest.mock.html#the-mock-class)\nas listeners, because they record calls, provide a\n[`mock_calls`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.mock_calls)\nattribute to see the calls, and provide [various `assert_*`\nmethods](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.Mock.assert_called)\nto make assertions about the calls:\n\n```python\nfrom unittest.mock import ANY, Mock\nimport subaudit\n\nwith subaudit.listening('open', Mock()) as listener:\n ... # Do something that may raise the event.\n\nlistener.assert_any_call('/path/to/file.txt', 'r', ANY)\n```\n\nNote how, when the `listening` context manager is entered, it returns the\n`listener` that was passed in, for convenience.\n\n### The `subaudit.extracting` context manager\n\nYou may want to extract some information about calls to a list:\n\n```python\nfrom dataclasses import InitVar, dataclass\nimport subaudit\n\n@dataclass(frozen=True)\nclass PathAndMode: # Usually strings. See examples/notebooks/open_event.ipynb.\n path: str\n mode: str\n flags: InitVar = None # Opt not to record this argument.\n\nwith subaudit.extracting('open', PathAndMode) as extracts:\n ... # Do something that may raise the event.\n\nassert PathAndMode('/path/to/file.txt', 'r') in extracts\n```\n\nThe extractor\u2014here, `PathAndMode`\u2014can be any callable that accepts the event\nargs as separate positional arguments. Entering the context manager returns an\ninitially empty list, which will be populated with *extracts* gleaned from the\nevent args. Each time the event is raised, the extractor is called and the\nobject it returns is appended to the list.\n\n### `subaudit.subscribe` and `subaudit.unsubscribe`\n\nAlthough you should usually use the `listening` or `extracting` context\nmanagers instead, you can subscribe and unsubscribe listeners without a context\nmanager:\n\n```python\nimport subaudit\n\ndef listen_open(path, mode, flags):\n ... # Handle the event.\n\nsubaudit.subscribe('open', listen_open)\ntry:\n ... # Do something that may raise the event.\nfinally:\n subaudit.unsubscribe('open', listen_open)\n```\n\nAttempting to unsubscribe a listener that is not subscribed raises\n`ValueError`. Currently, subaudit provides no feature to make this succeed\nsilently instead. But you can suppress the exception:\n\n```python\nwith contextlib.suppress(ValueError):\n subaudit.unsubscribe('glob.glob', possibly_subscribed_listener)\n```\n\n## Nesting\n\nTo unsubscribe a listener from an event, it must be subscribed to the event.\nSubject to this restriction, calls to [`subscribe` and\n`unsubscribe`](https://github.com/EliahKagan/subaudit#subauditsubscribe-and-subauditunsubscribe)\ncan happen in any order, and\n[`listening`](https://github.com/EliahKagan/subaudit#the-subauditlistening-context-manager)\nand\n[`extracting`](https://github.com/EliahKagan/subaudit#the-subauditextracting-context-manager)\nmay be arbitrarily nested.\n\n`listening` and `extracting` support reentrant use with both the same event and\ndifferent events. Here\u2019s an example with three `listening` contexts:\n\n```python\nfrom unittest.mock import Mock, call\n\nlisten_to = Mock() # Let us assert calls to child mocks in a specific order.\n\nwith subaudit.listening('open', print): # Print all open events' arguments.\n with subaudit.listening('open', listen_to.open): # Log opening.\n with subaudit.listening('glob.glob', listen_to.glob): # Log globbing.\n ... # Do something that may raise the events.\n\nassert listen_to.mock_calls == ... # Assert a specific order of calls.\n```\n\n(That is written out to make the nesting clear. You could also use a single\n`with`-statement with commas.)\n\nHere\u2019s an example with both `listening` and `extracting` contexts:\n\n```python\nfrom unittest.mock import Mock, call\n\ndef extract(*args):\n return args\n\nwith (\n subaudit.extracting('pathlib.Path.glob', extract) as glob_extracts,\n subaudit.listening('pathlib.Path.glob', Mock()) as glob_listener,\n subaudit.extracting('pathlib.Path.rglob', extract) as rglob_extracts,\n subaudit.listening('pathlib.Path.rglob', Mock()) as rglob_listener,\n):\n ... # Do something that may raise the events.\n\n# Assert something about, or otherwise use, the mocks glob_listener and\n# rglob_listener, as well as the lists glob_extracts and rglob_extracts.\n...\n```\n\n(That example uses [parenthesized context\nmanagers](https://docs.python.org/3/whatsnew/3.10.html#parenthesized-context-managers),\nwhich were introduced in Python 3.10.)\n\n## Specialized usage\n\n### `subaudit.Hook` objects\n\nEach instance of the `subaudit.Hook` class represents a single audit hook that\nsupports subscribing and unsubscribing listeners for any number of events, with\nmethods corresponding to the four top-level functions listed above. Separate\n`Hook` instances use separate audit hooks. The `Hook` class exists for three\npurposes:\n\n- It supplies the behavior of [the top-level `listening`, `extracting`,\n `subscribe`, and `unsubscribe`\n functions](https://github.com/EliahKagan/subaudit#basic-usage), which\n correspond to the same-named methods on a global `Hook` instance.\n- It allows multiple audit hooks to be used, for special cases where that might\n be desired.\n- It facilitates customization, as detailed below.\n\nThe actual audit hook that a `Hook` object encapsulates is not installed until\nthe first listener is subscribed. This happens on the first call to its\n`subscribe` method, or the first time one of its context managers (from calling\nits `listening` or `extracting` method) is entered. This is also true of the\nglobal `Hook` instance used by the top-level functions\u2014merely importing\n`subaudit` does not install an audit hook.\n\nWhether the top-level functions are bound methods of a `Hook` instance, or\ndelegate in some other way to those methods on an instance, is currently\nconsidered an implementation detail.\n\n### Deriving from `Hook`\n\nYou can derive from `Hook` to provide custom behavior for subscribing and\nunsubscribing, by overriding the `subscribe` and `unsubscribe` methods. You can\nalso override the `listening` and `extracting` methods, though that may be less\nuseful. Overridden `subscribe` and `unsubscribe` methods are automatically used\nby `listening` and `extracting`.\n\nWhether `extracting` uses `listening`, or directly calls `subscribe` and\n`unsubscribe`, is currently considered an implementation detail.\n\n### Locking\n\nConsider two possible cases of race conditions:\n\n#### 1. Between audit hook and `subscribe`/`unsubscribe` (audit hook does not lock)\n\nIn this scenario, a `Hook` object\u2019s installed audit hook runs at the same time\nas a listener is subscribed or unsubscribed.\n\nThis is likely to occur often and it cannot be prevented, because audit hooks\nare called for all audit events. For the same reason, locking in the audit hook\nhas performance implications. Instead of having audit hooks take locks,\nsubaudit relies on each of these operations being atomic:\n\n- *Writing an attribute reference, when it is a simple write to an instance\n dictionary or a slot.* Writing an attribute need not be atomic when, for\n example, `__setattr__` has been overridden.\n- *Writing or deleting a ``str`` key in a dictionary whose keys are all of the\n built-in ``str`` type.* Note that the search need not be atomic, but the\n dictionary must always be observed to be in a valid state.\n\nThe audit hook is written, and the data structures it uses are selected, to\navoid relying on more than these assumptions.\n\n#### 2. Between calls to `subscribe`/`unsubscribe` (by default, they lock)\n\nIn this scenario, two listeners are subscribed at a time, or unsubscribed at a\ntime, or one listener is subscribed while another (or the same) listener is\nunsubscribed.\n\nThis is less likely to occur and much easier to avoid. But it is also harder to\nmake safe without a lock. Subscribing and unsubscribing are unlikely to happen\nat a *sustained* high rate, so locking is unlikely to be a performance\nbottleneck. So, *by default*, subscribing and unsubscribing are synchronized\nwith a\n[`threading.Lock`](https://docs.python.org/3/library/threading.html#threading.Lock),\nto ensure that shared state is not corrupted.\n\nYou should not usually change this. But if you want to, you can construct a\n`Hook` object by calling `Hook(sub_lock_factory=...)` instead of `Hook`, where\n`...` is a type, or other context manager factory, to be used instead of\n`threading.Lock`. In particular, to disable locking, pass\n[`contextlib.nullcontext`](https://docs.python.org/3/library/contextlib.html#contextlib.nullcontext).\n\n## Functions related to compatibility\n\nAs [noted above](https://github.com/EliahKagan/subaudit#compatibility), Python\nsupports audit hooks [since 3.8](https://peps.python.org/pep-0578/). For Python\n3.7, but not Python 3.8 or later, the subaudit library declares\n[sysaudit](https://pypi.org/project/sysaudit/) as a dependency.\n\n### `subaudit.addaudithook` and `subaudit.audit`\n\nsubaudit exports `addaudithook` and `audit` functions.\n\n- On Python 3.8 and later, they are\n [`sys.addaudithook`](https://docs.python.org/3/library/sys.html#sys.addaudithook)\n and [`sys.audit`](https://docs.python.org/3/library/sys.html#sys.audit).\n- On Python 3.7, they are\n [`sysaudit.addaudithook`](https://sysaudit.readthedocs.io/en/latest/#sysaudit.addaudithook)\n and\n [`sysaudit.audit`](https://sysaudit.readthedocs.io/en/latest/#sysaudit.audit).\n\nsubaudit uses `subaudit.addaudithook` when it adds its own audit hook (or all\nits own hooks, if you use additional\n[`Hook`](https://github.com/EliahKagan/subaudit#subaudithook-objects) instances\nbesides the global one implicitly used by [the top-level\nfunctions](https://github.com/EliahKagan/subaudit#basic-usage)). subaudit does\nnot itself use `subaudit.audit`, but it is whichever `audit` function\ncorresponds to `subaudit.addaudithook`.\n\n### `@subaudit.skip_if_unavailable`\n\nThe primary use case for subaudit is in writing unit tests, to assert that\nparticular events have been raised or not raised. Usually these are [\u201cbuilt in\u201d\nevents](https://docs.python.org/3.8/library/audit_events.html)\u2014those raised by\nthe Python interpreter or standard library. But the sysaudit library doesn\u2019t\nbackport those events, which would not really be feasible to do.\n\nFor this reason, tests that particular audit events did or didn\u2019t occur\u2014such as\na test that a file has been opened by listening to the `open` event\u2014should\ntypically be skipped when running a test suite on Python 3.7.\n\n**When using the [unittest](https://docs.python.org/3/library/unittest.html)\nframework**, you can apply the `@skip_if_unavailable` decorator to a test class\nor test method, so it is skipped prior to Python 3.8 with a message explaining\nwhy. For example:\n\n```python\nimport unittest\nfrom unittest.mock import ANY, Mock\nimport subaudit\n\nclass TestSomeThings(unittest.TestCase):\n ...\n\n @subaudit.skip_if_unavailable # Skip this test if < 3.8, with a message.\n def test_file_is_opened_for_read(self):\n with subaudit.listening('open', Mock()) as listener:\n ... # Do something that may raise the event.\n\n listener.assert_any_call('/path/to/file.txt', 'r', ANY)\n\n ...\n\n@subaudit.skip_if_unavailable # Skip the whole class if < 3.8, with a message.\nclass TestSomeMoreThings(unittest.TestCase):\n ...\n```\n\nIt could be useful also to have a conditional xfail ([expected\nfailure](https://docs.python.org/3/library/unittest.html#unittest.expectedFailure))\ndecorator for unittest\u2014and, more so,\n[marks](https://docs.pytest.org/en/7.1.x/how-to/mark.html) for\n[pytest](https://docs.pytest.org/) providing specialized\n[skip](https://docs.pytest.org/en/7.3.x/how-to/skipping.html#skipping-test-functions)/[skipif](https://docs.pytest.org/en/7.3.x/how-to/skipping.html#id1)\nand\n[xfail](https://docs.pytest.org/en/7.3.x/how-to/skipping.html#xfail-mark-test-functions-as-expected-to-fail)\u2014but\nsubaudit does not currently provide them. Of course, in pytest, you can still\nuse the [`@pytest.mark.skip` and\n`@pytest.mark.xfail`](https://docs.pytest.org/en/7.3.x/how-to/skipping.html)\ndecorators, by passing `sys.version_info < (3, 8)` as the condition.\n\n## Overview by level of abstraction\n\nFrom higher to lower level, from the perspective of the top-level\n[`listening`](https://github.com/EliahKagan/subaudit#the-subauditlistening-context-manager)\nand\n[`extracting`](https://github.com/EliahKagan/subaudit#the-subauditextracting-context-manager)\nfunctions:\n\n- [`subaudit.extracting`](https://github.com/EliahKagan/subaudit#the-subauditextracting-context-manager)\n \\- context manager that listens and extracts to a list\n- [`subaudit.listening`](https://github.com/EliahKagan/subaudit#the-subauditlistening-context-manager)\n \\- context manager to subscribe and unsubscribe a custom listener *(usually\n use this)*\n- [`subaudit.subscribe` and\n `subaudit.unsubscribe`](https://github.com/EliahKagan/subaudit#subauditsubscribe-and-subauditunsubscribe)\n \\- manually subscribe/unsubscribe a listener\n- [`subaudit.Hook`](https://github.com/EliahKagan/subaudit#subaudithook-objects)\n \\- abstraction around an audit hook allowing subscribing and unsubscribing\n for specific events, with `extracting`, `listening`, `subscribe`, and\n `unsubscribe` instance methods\n- [`subaudit.addaudithook`](https://github.com/EliahKagan/subaudit#subauditaddaudithook-and-subauditaudit)\n \\- trivial abstraction representing whether the function from `sys` or\n `sysaudit` is used\n- [`sys.addaudithook`](https://docs.python.org/3/library/sys.html#sys.addaudithook)\n or\n [`sysaudit.addaudithook`](https://sysaudit.readthedocs.io/en/latest/#sysaudit.addaudithook)\n \\- *not part of subaudit* \\- install a [PEP\n 578](https://peps.python.org/pep-0578/) audit hook\n\nThis list is not exhaustive. For example,\n[`@skip_if_unavailable`](https://github.com/EliahKagan/subaudit#subauditskip_if_unavailable)\nis not part of that conceptual hierarchy.\n\n## Acknowledgements\n\nI\u2019d like to thank:\n\n- [**Brett Langdon**](https://github.com/brettlangdon), who wrote the\n [sysaudit](https://github.com/brettlangdon/sysaudit) library (which subaudit\n [uses on 3.7](https://github.com/EliahKagan/subaudit#compatibility)).\n\n- [**David Vassallo**](https://github.com/dmvassallo), for reviewing pull\n requests about testing using audit hooks in [a project we have collaborated\n on](https://github.com/dmvassallo/EmbeddingScratchwork), which helped me to\n recognize what kinds of usage were more or less clear and that it could be\n good to have a library like subaudit; and for coauthoring a\n `@skip_if_unavailable` decorator that had been used there, which motivated\n the one here.\n\n## About the name\n\nThis library is called \u201csubaudit\u201d because it provides a way to effectively\n*sub*scribe to and un*sub*scribe from a *sub*set of audit events rather than\nall of them.\n",
"bugtrack_url": null,
"license": "0BSD",
"summary": "Subscribe and unsubscribe for specific audit events",
"version": "0.1.0",
"project_urls": {
"Homepage": "https://github.com/EliahKagan/subaudit",
"Repository": "https://github.com/EliahKagan/subaudit"
},
"split_keywords": [
"audit",
"auditing",
"events",
"subscribe",
"unsubscribe"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "2269505f5ecda29ac16ece0b1af729503a63c79738713c3d4d8ea30f682f04a5",
"md5": "c3ddef912e9720569d3cb865dfcacee0",
"sha256": "eab9aee5dcc798000ab0db3bb0a9978f82583c0fc278c4cbfcfffb05dbea7f44"
},
"downloads": -1,
"filename": "subaudit-0.1.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "c3ddef912e9720569d3cb865dfcacee0",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7,<4.0",
"size": 12715,
"upload_time": "2023-05-06T18:14:14",
"upload_time_iso_8601": "2023-05-06T18:14:14.620435Z",
"url": "https://files.pythonhosted.org/packages/22/69/505f5ecda29ac16ece0b1af729503a63c79738713c3d4d8ea30f682f04a5/subaudit-0.1.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "8e370f2de8c0478b8047250dd853042bb349aec2d5d357621c8785f7aae685ee",
"md5": "4363507681699cfd947b7f08deb72d67",
"sha256": "74f8cd0d3800105d3cd0ce5259282f621d06f121de7143e587b126b128e040dd"
},
"downloads": -1,
"filename": "subaudit-0.1.0.tar.gz",
"has_sig": false,
"md5_digest": "4363507681699cfd947b7f08deb72d67",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7,<4.0",
"size": 16172,
"upload_time": "2023-05-06T18:14:16",
"upload_time_iso_8601": "2023-05-06T18:14:16.732320Z",
"url": "https://files.pythonhosted.org/packages/8e/37/0f2de8c0478b8047250dd853042bb349aec2d5d357621c8785f7aae685ee/subaudit-0.1.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-05-06 18:14:16",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "EliahKagan",
"github_project": "subaudit",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"tox": true,
"lcname": "subaudit"
}