{"id":18317048,"url":"https://github.com/eliahkagan/subaudit","last_synced_at":"2025-04-05T21:32:13.258Z","repository":{"id":162663461,"uuid":"637169529","full_name":"EliahKagan/subaudit","owner":"EliahKagan","description":"Subscribe and unsubscribe for specific audit events","archived":false,"fork":false,"pushed_at":"2025-03-01T12:46:37.000Z","size":716,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-04T18:40:46.260Z","etag":null,"topics":["audit","context-manager","events","python","subscribe","testing","unit-testing","unsubscribe"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"0bsd","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/EliahKagan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-06T18:10:43.000Z","updated_at":"2024-12-30T16:39:48.000Z","dependencies_parsed_at":"2023-11-27T17:26:00.637Z","dependency_job_id":"cd5ec0e7-5c46-476e-be0f-873548a2f8a5","html_url":"https://github.com/EliahKagan/subaudit","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EliahKagan%2Fsubaudit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EliahKagan%2Fsubaudit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EliahKagan%2Fsubaudit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/EliahKagan%2Fsubaudit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/EliahKagan","download_url":"https://codeload.github.com/EliahKagan/subaudit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247406030,"owners_count":20933803,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audit","context-manager","events","python","subscribe","testing","unit-testing","unsubscribe"],"created_at":"2024-11-05T18:04:47.542Z","updated_at":"2025-04-05T21:32:12.818Z","avatar_url":"https://github.com/EliahKagan.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!--\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--\u003e\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—you 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 [“public-domain\nequivalent”](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’t hold.\n\n## Installation  \u003c!-- Maybe remove this section once badges are added. --\u003e\n\nInstall [the `subaudit` package (PyPI)](https://pypi.org/project/subaudit/) in\nyour project’s 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—here, `listen_open`—is 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—here, `PathAndMode`—can 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’s 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’s 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—merely 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’s 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 [“built in”\nevents](https://docs.python.org/3.8/library/audit_events.html)—those raised by\nthe Python interpreter or standard library. But the sysaudit library doesn’t\nbackport those events, which would not really be feasible to do.\n\nFor this reason, tests that particular audit events did or didn’t occur—such as\na test that a file has been opened by listening to the `open` event—should\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 \u003c 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 \u003c 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—and, 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)—but\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 \u003c (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’d 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 “subaudit” 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","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feliahkagan%2Fsubaudit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feliahkagan%2Fsubaudit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feliahkagan%2Fsubaudit/lists"}