We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
weakrefs to dead objects occuring when changing backends in pyhf benchmark
### Summary

I try to perform a benchmark of `pyhf` using `pytest-benchmark` quite similarly to the benchmark in the `tests/benchmarks` directory.
A short example of such a simple benchmark is given below. To reproduce this bug, the python code needs to be saved in a file of the format `test_<name>.py` and executed via `pytest test_<name>.py`.
The bug occurs only sometimes when the backend is changed between different benchmarking cases. Since the occurence of the bug includes some amount of randomness, it may happen that it doesn't occur on the first try but that the benchmark must be executed multiple times. The full benchmark takes around 1 min on my machine.

The suspected origin of this bug is that every time the backend is changed, an event called `tensorlib_changed` is triggered that in turn leads to the execution of some `_precompute()` functions on different objects (e.g. a `TensorViewer` object as in the error message). The problem occurs, when the referenced object no longer exists, as all references to it have been removed. The reference used to call the function does not change this as it is a weakref.

A proposed solution can be found in PR #2310. 

### OS / Environment

```console
PRETTY_NAME="Ubuntu 22.04.3 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.3 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
```


### Steps to Reproduce

<!--- Paste your minimal failing Python example code between the quotes below -->
```python (paste below)
# content of test_benchmark.py
import pytest
import pyhf

@pytest.fixture(
    scope='function',
    params=[
        (pyhf.tensor.numpy_backend(), None),
        (pyhf.tensor.pytorch_backend(), None),
        (pyhf.tensor.pytorch_backend(precision='64b'), None),
        (pyhf.tensor.tensorflow_backend(), None),
        (pyhf.tensor.jax_backend(), None),
        (
            pyhf.tensor.numpy_backend(poisson_from_normal=True),
            pyhf.optimize.minuit_optimizer(),
        ),
    ],
    ids=['numpy', 'pytorch', 'pytorch64',
         'tensorflow',
         'jax', 'numpy_minuit'],
)
def backend(request):
    # get the ids of all the backends
    param_ids = request._fixturedef.ids
    # the backend we're using: numpy, tensorflow, etc...
    param_id = param_ids[request.param_index]
    # name of function being called (with params), the original name is .originalname
    func_name = request._pyfuncitem.name

    pyhf.set_backend(*request.param)

    yield request.param

def hypotest(data, pdf):
    return pyhf.infer.hypotest(1.0, data, pdf, test_stat="qtilde", return_expected=True)

bins = [1, 2, 4, 8, 16, 32]
bin_ids = [f'{n_bins}_bins' for n_bins in bins]

@pytest.mark.parametrize('n_bins', bins, ids=bin_ids)
def test_hypotest(benchmark, backend, n_bins):
    model = pyhf.simplemodels.uncorrelated_background(signal=[12.0]*n_bins, bkg=[50.0]*n_bins, bkg_uncertainty=[5.0]*n_bins)
    data = [51.0]*n_bins + model.config.auxdata

    assert benchmark(hypotest, data, model)
```

<!--- ...or if you have a failing CLI command paste it between the quotes below -->
```console (paste below)
pytest test_benchmark.py
```


### File Upload (optional)

_No response_

### Expected Results

The expected behavior is to output the benchmarking results for all considered cases as it can be observed when executing `pytest` in `pyhf/tests/benchmarks/`.
This output should not show any "test failures" as no normal tests are performed but only functions that run without an error, when called outside of the benchmark.

### Actual Results

```console
_________________________ ERROR at setup of test_hypotest[jax-1_bins] _________________________

request = <SubRequest 'backend' for <Function test_hypotest[jax-1_bins]>>

    @pytest.fixture(
        scope='function',
        params=[
            (pyhf.tensor.numpy_backend(), None),
            (pyhf.tensor.pytorch_backend(), None),
            (pyhf.tensor.pytorch_backend(precision='64b'), None),
            (pyhf.tensor.tensorflow_backend(), None),
            (pyhf.tensor.jax_backend(), None),
            (
                pyhf.tensor.numpy_backend(poisson_from_normal=True),
                pyhf.optimize.minuit_optimizer(),
            ),
        ],
        ids=['numpy', 'pytorch', 'pytorch64',
             'tensorflow',
             'jax', 'numpy_minuit'],
    )
    def backend(request):
        # get the ids of all the backends
        param_ids = request._fixturedef.ids
        # the backend we're using: numpy, tensorflow, etc...
        param_id = param_ids[request.param_index]
        # name of function being called (with params), the original name is .originalname
        func_name = request._pyfuncitem.name
    
>       pyhf.set_backend(*request.param)

test_hypo_pyhf.py:29: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../pyhfDev/pyhf/src/pyhf/events.py:161: in register_wrapper
    result = func(*args, **kwargs)
../../pyhfDev/pyhf/src/pyhf/tensor/manager.py:193: in set_backend
    events.trigger("tensorlib_changed")()
../../pyhfDev/pyhf/src/pyhf/events.py:70: in __call__
    func()(arg(), *args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = None

    def _precompute(self):
        tensorlib, _ = get_backend()
>       self.sorted_indices = tensorlib.astensor(self._sorted_indices, dtype='int')
E       AttributeError: 'NoneType' object has no attribute '_sorted_indices'

../../pyhfDev/pyhf/src/pyhf/tensor/common.py:33: AttributeError
```


### pyhf Version

```console
pyhf, version 0.7.1.dev116
```


### Code of Conduct

- [X] I agree to follow the Code of Conduct

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/pyhf/events.py`
Content:
```
1 from __future__ import annotations
2 
3 import weakref
4 from functools import wraps
5 from typing import Callable, TypeVar, cast
6 
7 # See https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators
8 TCallable = TypeVar("TCallable", bound=Callable)
9 
10 
11 __events = {}
12 __disabled_events = set()
13 
14 __all__ = ["Callables", "disable", "enable", "noop", "register", "subscribe", "trigger"]
15 
16 
17 def __dir__():
18     return __all__
19 
20 
21 def noop(*args, **kwargs):
22     pass
23 
24 
25 class Callables:
26     def __init__(self):
27         self._callbacks = []
28 
29     @property
30     def callbacks(self):
31         """
32         Get the current list of living callbacks.
33         """
34         self._flush()
35         return self._callbacks
36 
37     def append(self, callback):
38         """
39         Append a new bound method as a callback to the list of callables.
40         """
41         try:
42             # methods
43             callback_ref = weakref.ref(callback.__func__), weakref.ref(
44                 callback.__self__
45             )
46         except AttributeError:
47             callback_ref = weakref.ref(callback), None
48         self._callbacks.append(callback_ref)
49 
50     def _flush(self):
51         """
52         Flush the list of callbacks with those who are weakly-referencing deleted objects.
53 
54         Note: must interact with the self._callbacks directly, and not
55         self.callbacks, to avoid infinite recursion.
56         """
57         _callbacks = []
58         for func, arg in self._callbacks:
59             if arg is not None:
60                 arg_ref = arg()
61                 if arg_ref is None:
62                     continue
63             _callbacks.append((func, arg))
64         self._callbacks = _callbacks
65 
66     def __call__(self, *args, **kwargs):
67         for func, arg in self.callbacks:
68             # weakref: needs to be de-ref'd first before calling
69             if arg is not None:
70                 func()(arg(), *args, **kwargs)
71             else:
72                 func()(*args, **kwargs)
73 
74     def __iter__(self):
75         return iter(self.callbacks)
76 
77     def __getitem__(self, index):
78         return self.callbacks[index]
79 
80     def __len__(self):
81         return len(self.callbacks)
82 
83     def __repr__(self):
84         return f"Callables({self.callbacks})"
85 
86 
87 def subscribe(event: str):
88     """
89     Subscribe a function or object method as a callback to an event.
90 
91     .. note::
92 
93      This is meant to be used as a decorator.
94 
95     Args:
96         event (:obj:`str`): The name of the event to subscribe to.
97 
98     Returns:
99         :obj:`function`: Decorated function.
100 
101     Example:
102         >>> import pyhf
103         >>> @pyhf.events.subscribe("myevent")
104         ... def test(a, b):
105         ...     print(a + b)
106         ...
107         >>> pyhf.events.trigger("myevent")(1, 2)
108         3
109 
110     """
111 
112     global __events
113 
114     def __decorator(func: TCallable) -> TCallable:
115         __events.setdefault(event, Callables()).append(func)
116         return func
117 
118     return cast(TCallable, __decorator)
119 
120 
121 def register(event: str) -> Callable[[TCallable], TCallable]:
122     """
123     Register a function or object method to trigger an event.  This creates two
124     events: ``{event_name}::before`` and ``{event_name}::after``.
125 
126     .. note::
127 
128      This is meant to be used as a decorator.
129 
130     Args:
131         event (:obj:`str`): The name of the event to subscribe to.
132 
133     Returns:
134         :obj:`function`: Decorated function.
135 
136     Example:
137         >>> import pyhf
138         >>> @pyhf.events.register("test_func")
139         ... def test(a, b):
140         ...     print(a + b)
141         ...
142         >>> @pyhf.events.subscribe("test_func::before")
143         ... def precall():
144         ...     print("before call")
145         ...
146         >>> @pyhf.events.subscribe("test_func::after")
147         ... def postcall():
148         ...     print("after call")
149         ...
150         >>> test(1, 2)
151         before call
152         3
153         after call
154 
155     """
156 
157     def _register(func: TCallable) -> TCallable:
158         @wraps(func)
159         def register_wrapper(*args, **kwargs):  # type: ignore
160             trigger(f"{event:s}::before")()
161             result = func(*args, **kwargs)
162             trigger(f"{event:s}::after")()
163             return result
164 
165         return register_wrapper
166 
167     return cast(TCallable, _register)
168 
169 
170 def trigger(event: str) -> Callables:
171     """
172     Trigger an event if not disabled.
173     """
174     global __events, __disabled_events, noop
175     is_noop = bool(event in __disabled_events or event not in __events)
176     return noop if is_noop else __events.get(event)
177 
178 
179 def disable(event: str):
180     """
181     Disable an event from firing.
182     """
183     global __disabled_events
184     __disabled_events.add(event)
185 
186 
187 def enable(event: str):
188     """
189     Enable an event to be fired if disabled.
190     """
191     global __disabled_events
192     __disabled_events.remove(event)
193
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/pyhf/events.py b/src/pyhf/events.py
--- a/src/pyhf/events.py
+++ b/src/pyhf/events.py
@@ -64,12 +64,20 @@
         self._callbacks = _callbacks
 
     def __call__(self, *args, **kwargs):
-        for func, arg in self.callbacks:
+        for func, arg in self._callbacks:
             # weakref: needs to be de-ref'd first before calling
             if arg is not None:
-                func()(arg(), *args, **kwargs)
+                arg_ref = arg()
+                if arg_ref is not None:
+                    func()(arg_ref, *args, **kwargs)
             else:
                 func()(*args, **kwargs)
+        # Flush after calling all the callbacks, not before, as callbacks in the
+        # beginning of the iteration might cause new dead arg weakrefs in
+        # callbacks that are iterated over later.
+        # Checking for dead weakrefs in each iteration and flushing at the end
+        # avoids redundant dead weakref checking in subsequent calls.
+        self._flush()
 
     def __iter__(self):
         return iter(self.callbacks)

{"golden_diff": "diff --git a/src/pyhf/events.py b/src/pyhf/events.py\n--- a/src/pyhf/events.py\n+++ b/src/pyhf/events.py\n@@ -64,12 +64,20 @@\n         self._callbacks = _callbacks\n \n     def __call__(self, *args, **kwargs):\n-        for func, arg in self.callbacks:\n+        for func, arg in self._callbacks:\n             # weakref: needs to be de-ref'd first before calling\n             if arg is not None:\n-                func()(arg(), *args, **kwargs)\n+                arg_ref = arg()\n+                if arg_ref is not None:\n+                    func()(arg_ref, *args, **kwargs)\n             else:\n                 func()(*args, **kwargs)\n+        # Flush after calling all the callbacks, not before, as callbacks in the\n+        # beginning of the iteration might cause new dead arg weakrefs in\n+        # callbacks that are iterated over later.\n+        # Checking for dead weakrefs in each iteration and flushing at the end\n+        # avoids redundant dead weakref checking in subsequent calls.\n+        self._flush()\n \n     def __iter__(self):\n         return iter(self.callbacks)\n", "issue": "weakrefs to dead objects occuring when changing backends in pyhf benchmark\n### Summary\n\nI try to perform a benchmark of `pyhf` using `pytest-benchmark` quite similarly to the benchmark in the `tests/benchmarks` directory.\r\nA short example of such a simple benchmark is given below. To reproduce this bug, the python code needs to be saved in a file of the format `test_<name>.py` and executed via `pytest test_<name>.py`.\r\nThe bug occurs only sometimes when the backend is changed between different benchmarking cases. Since the occurence of the bug includes some amount of randomness, it may happen that it doesn't occur on the first try but that the benchmark must be executed multiple times. The full benchmark takes around 1 min on my machine.\r\n\r\nThe suspected origin of this bug is that every time the backend is changed, an event called `tensorlib_changed` is triggered that in turn leads to the execution of some `_precompute()` functions on different objects (e.g. a `TensorViewer` object as in the error message). The problem occurs, when the referenced object no longer exists, as all references to it have been removed. The reference used to call the function does not change this as it is a weakref.\r\n\r\nA proposed solution can be found in PR #2310. \n\n### OS / Environment\n\n```console\nPRETTY_NAME=\"Ubuntu 22.04.3 LTS\"\r\nNAME=\"Ubuntu\"\r\nVERSION_ID=\"22.04\"\r\nVERSION=\"22.04.3 LTS (Jammy Jellyfish)\"\r\nVERSION_CODENAME=jammy\r\nID=ubuntu\r\nID_LIKE=debian\r\nHOME_URL=\"https://www.ubuntu.com/\"\r\nSUPPORT_URL=\"https://help.ubuntu.com/\"\r\nBUG_REPORT_URL=\"https://bugs.launchpad.net/ubuntu/\"\r\nPRIVACY_POLICY_URL=\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\"\r\nUBUNTU_CODENAME=jammy\n```\n\n\n### Steps to Reproduce\n\n<!--- Paste your minimal failing Python example code between the quotes below -->\r\n```python (paste below)\r\n# content of test_benchmark.py\r\nimport pytest\r\nimport pyhf\r\n\r\n@pytest.fixture(\r\n    scope='function',\r\n    params=[\r\n        (pyhf.tensor.numpy_backend(), None),\r\n        (pyhf.tensor.pytorch_backend(), None),\r\n        (pyhf.tensor.pytorch_backend(precision='64b'), None),\r\n        (pyhf.tensor.tensorflow_backend(), None),\r\n        (pyhf.tensor.jax_backend(), None),\r\n        (\r\n            pyhf.tensor.numpy_backend(poisson_from_normal=True),\r\n            pyhf.optimize.minuit_optimizer(),\r\n        ),\r\n    ],\r\n    ids=['numpy', 'pytorch', 'pytorch64',\r\n         'tensorflow',\r\n         'jax', 'numpy_minuit'],\r\n)\r\ndef backend(request):\r\n    # get the ids of all the backends\r\n    param_ids = request._fixturedef.ids\r\n    # the backend we're using: numpy, tensorflow, etc...\r\n    param_id = param_ids[request.param_index]\r\n    # name of function being called (with params), the original name is .originalname\r\n    func_name = request._pyfuncitem.name\r\n\r\n    pyhf.set_backend(*request.param)\r\n\r\n    yield request.param\r\n\r\ndef hypotest(data, pdf):\r\n    return pyhf.infer.hypotest(1.0, data, pdf, test_stat=\"qtilde\", return_expected=True)\r\n\r\nbins = [1, 2, 4, 8, 16, 32]\r\nbin_ids = [f'{n_bins}_bins' for n_bins in bins]\r\n\r\n@pytest.mark.parametrize('n_bins', bins, ids=bin_ids)\r\ndef test_hypotest(benchmark, backend, n_bins):\r\n    model = pyhf.simplemodels.uncorrelated_background(signal=[12.0]*n_bins, bkg=[50.0]*n_bins, bkg_uncertainty=[5.0]*n_bins)\r\n    data = [51.0]*n_bins + model.config.auxdata\r\n\r\n    assert benchmark(hypotest, data, model)\r\n```\r\n\r\n<!--- ...or if you have a failing CLI command paste it between the quotes below -->\r\n```console (paste below)\r\npytest test_benchmark.py\r\n```\r\n\n\n### File Upload (optional)\n\n_No response_\n\n### Expected Results\n\nThe expected behavior is to output the benchmarking results for all considered cases as it can be observed when executing `pytest` in `pyhf/tests/benchmarks/`.\r\nThis output should not show any \"test failures\" as no normal tests are performed but only functions that run without an error, when called outside of the benchmark.\n\n### Actual Results\n\n```console\n_________________________ ERROR at setup of test_hypotest[jax-1_bins] _________________________\r\n\r\nrequest = <SubRequest 'backend' for <Function test_hypotest[jax-1_bins]>>\r\n\r\n    @pytest.fixture(\r\n        scope='function',\r\n        params=[\r\n            (pyhf.tensor.numpy_backend(), None),\r\n            (pyhf.tensor.pytorch_backend(), None),\r\n            (pyhf.tensor.pytorch_backend(precision='64b'), None),\r\n            (pyhf.tensor.tensorflow_backend(), None),\r\n            (pyhf.tensor.jax_backend(), None),\r\n            (\r\n                pyhf.tensor.numpy_backend(poisson_from_normal=True),\r\n                pyhf.optimize.minuit_optimizer(),\r\n            ),\r\n        ],\r\n        ids=['numpy', 'pytorch', 'pytorch64',\r\n             'tensorflow',\r\n             'jax', 'numpy_minuit'],\r\n    )\r\n    def backend(request):\r\n        # get the ids of all the backends\r\n        param_ids = request._fixturedef.ids\r\n        # the backend we're using: numpy, tensorflow, etc...\r\n        param_id = param_ids[request.param_index]\r\n        # name of function being called (with params), the original name is .originalname\r\n        func_name = request._pyfuncitem.name\r\n    \r\n>       pyhf.set_backend(*request.param)\r\n\r\ntest_hypo_pyhf.py:29: \r\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\r\n../../pyhfDev/pyhf/src/pyhf/events.py:161: in register_wrapper\r\n    result = func(*args, **kwargs)\r\n../../pyhfDev/pyhf/src/pyhf/tensor/manager.py:193: in set_backend\r\n    events.trigger(\"tensorlib_changed\")()\r\n../../pyhfDev/pyhf/src/pyhf/events.py:70: in __call__\r\n    func()(arg(), *args, **kwargs)\r\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _\r\n\r\nself = None\r\n\r\n    def _precompute(self):\r\n        tensorlib, _ = get_backend()\r\n>       self.sorted_indices = tensorlib.astensor(self._sorted_indices, dtype='int')\r\nE       AttributeError: 'NoneType' object has no attribute '_sorted_indices'\r\n\r\n../../pyhfDev/pyhf/src/pyhf/tensor/common.py:33: AttributeError\n```\n\n\n### pyhf Version\n\n```console\npyhf, version 0.7.1.dev116\n```\n\n\n### Code of Conduct\n\n- [X] I agree to follow the Code of Conduct\n", "before_files": [{"content": "from __future__ import annotations\n\nimport weakref\nfrom functools import wraps\nfrom typing import Callable, TypeVar, cast\n\n# See https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators\nTCallable = TypeVar(\"TCallable\", bound=Callable)\n\n\n__events = {}\n__disabled_events = set()\n\n__all__ = [\"Callables\", \"disable\", \"enable\", \"noop\", \"register\", \"subscribe\", \"trigger\"]\n\n\ndef __dir__():\n    return __all__\n\n\ndef noop(*args, **kwargs):\n    pass\n\n\nclass Callables:\n    def __init__(self):\n        self._callbacks = []\n\n    @property\n    def callbacks(self):\n        \"\"\"\n        Get the current list of living callbacks.\n        \"\"\"\n        self._flush()\n        return self._callbacks\n\n    def append(self, callback):\n        \"\"\"\n        Append a new bound method as a callback to the list of callables.\n        \"\"\"\n        try:\n            # methods\n            callback_ref = weakref.ref(callback.__func__), weakref.ref(\n                callback.__self__\n            )\n        except AttributeError:\n            callback_ref = weakref.ref(callback), None\n        self._callbacks.append(callback_ref)\n\n    def _flush(self):\n        \"\"\"\n        Flush the list of callbacks with those who are weakly-referencing deleted objects.\n\n        Note: must interact with the self._callbacks directly, and not\n        self.callbacks, to avoid infinite recursion.\n        \"\"\"\n        _callbacks = []\n        for func, arg in self._callbacks:\n            if arg is not None:\n                arg_ref = arg()\n                if arg_ref is None:\n                    continue\n            _callbacks.append((func, arg))\n        self._callbacks = _callbacks\n\n    def __call__(self, *args, **kwargs):\n        for func, arg in self.callbacks:\n            # weakref: needs to be de-ref'd first before calling\n            if arg is not None:\n                func()(arg(), *args, **kwargs)\n            else:\n                func()(*args, **kwargs)\n\n    def __iter__(self):\n        return iter(self.callbacks)\n\n    def __getitem__(self, index):\n        return self.callbacks[index]\n\n    def __len__(self):\n        return len(self.callbacks)\n\n    def __repr__(self):\n        return f\"Callables({self.callbacks})\"\n\n\ndef subscribe(event: str):\n    \"\"\"\n    Subscribe a function or object method as a callback to an event.\n\n    .. note::\n\n     This is meant to be used as a decorator.\n\n    Args:\n        event (:obj:`str`): The name of the event to subscribe to.\n\n    Returns:\n        :obj:`function`: Decorated function.\n\n    Example:\n        >>> import pyhf\n        >>> @pyhf.events.subscribe(\"myevent\")\n        ... def test(a, b):\n        ...     print(a + b)\n        ...\n        >>> pyhf.events.trigger(\"myevent\")(1, 2)\n        3\n\n    \"\"\"\n\n    global __events\n\n    def __decorator(func: TCallable) -> TCallable:\n        __events.setdefault(event, Callables()).append(func)\n        return func\n\n    return cast(TCallable, __decorator)\n\n\ndef register(event: str) -> Callable[[TCallable], TCallable]:\n    \"\"\"\n    Register a function or object method to trigger an event.  This creates two\n    events: ``{event_name}::before`` and ``{event_name}::after``.\n\n    .. note::\n\n     This is meant to be used as a decorator.\n\n    Args:\n        event (:obj:`str`): The name of the event to subscribe to.\n\n    Returns:\n        :obj:`function`: Decorated function.\n\n    Example:\n        >>> import pyhf\n        >>> @pyhf.events.register(\"test_func\")\n        ... def test(a, b):\n        ...     print(a + b)\n        ...\n        >>> @pyhf.events.subscribe(\"test_func::before\")\n        ... def precall():\n        ...     print(\"before call\")\n        ...\n        >>> @pyhf.events.subscribe(\"test_func::after\")\n        ... def postcall():\n        ...     print(\"after call\")\n        ...\n        >>> test(1, 2)\n        before call\n        3\n        after call\n\n    \"\"\"\n\n    def _register(func: TCallable) -> TCallable:\n        @wraps(func)\n        def register_wrapper(*args, **kwargs):  # type: ignore\n            trigger(f\"{event:s}::before\")()\n            result = func(*args, **kwargs)\n            trigger(f\"{event:s}::after\")()\n            return result\n\n        return register_wrapper\n\n    return cast(TCallable, _register)\n\n\ndef trigger(event: str) -> Callables:\n    \"\"\"\n    Trigger an event if not disabled.\n    \"\"\"\n    global __events, __disabled_events, noop\n    is_noop = bool(event in __disabled_events or event not in __events)\n    return noop if is_noop else __events.get(event)\n\n\ndef disable(event: str):\n    \"\"\"\n    Disable an event from firing.\n    \"\"\"\n    global __disabled_events\n    __disabled_events.add(event)\n\n\ndef enable(event: str):\n    \"\"\"\n    Enable an event to be fired if disabled.\n    \"\"\"\n    global __disabled_events\n    __disabled_events.remove(event)\n", "path": "src/pyhf/events.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport weakref\nfrom functools import wraps\nfrom typing import Callable, TypeVar, cast\n\n# See https://mypy.readthedocs.io/en/stable/generics.html#declaring-decorators\nTCallable = TypeVar(\"TCallable\", bound=Callable)\n\n\n__events = {}\n__disabled_events = set()\n\n__all__ = [\"Callables\", \"disable\", \"enable\", \"noop\", \"register\", \"subscribe\", \"trigger\"]\n\n\ndef __dir__():\n    return __all__\n\n\ndef noop(*args, **kwargs):\n    pass\n\n\nclass Callables:\n    def __init__(self):\n        self._callbacks = []\n\n    @property\n    def callbacks(self):\n        \"\"\"\n        Get the current list of living callbacks.\n        \"\"\"\n        self._flush()\n        return self._callbacks\n\n    def append(self, callback):\n        \"\"\"\n        Append a new bound method as a callback to the list of callables.\n        \"\"\"\n        try:\n            # methods\n            callback_ref = weakref.ref(callback.__func__), weakref.ref(\n                callback.__self__\n            )\n        except AttributeError:\n            callback_ref = weakref.ref(callback), None\n        self._callbacks.append(callback_ref)\n\n    def _flush(self):\n        \"\"\"\n        Flush the list of callbacks with those who are weakly-referencing deleted objects.\n\n        Note: must interact with the self._callbacks directly, and not\n        self.callbacks, to avoid infinite recursion.\n        \"\"\"\n        _callbacks = []\n        for func, arg in self._callbacks:\n            if arg is not None:\n                arg_ref = arg()\n                if arg_ref is None:\n                    continue\n            _callbacks.append((func, arg))\n        self._callbacks = _callbacks\n\n    def __call__(self, *args, **kwargs):\n        for func, arg in self._callbacks:\n            # weakref: needs to be de-ref'd first before calling\n            if arg is not None:\n                arg_ref = arg()\n                if arg_ref is not None:\n                    func()(arg_ref, *args, **kwargs)\n            else:\n                func()(*args, **kwargs)\n        # Flush after calling all the callbacks, not before, as callbacks in the\n        # beginning of the iteration might cause new dead arg weakrefs in\n        # callbacks that are iterated over later.\n        # Checking for dead weakrefs in each iteration and flushing at the end\n        # avoids redundant dead weakref checking in subsequent calls.\n        self._flush()\n\n    def __iter__(self):\n        return iter(self.callbacks)\n\n    def __getitem__(self, index):\n        return self.callbacks[index]\n\n    def __len__(self):\n        return len(self.callbacks)\n\n    def __repr__(self):\n        return f\"Callables({self.callbacks})\"\n\n\ndef subscribe(event: str):\n    \"\"\"\n    Subscribe a function or object method as a callback to an event.\n\n    .. note::\n\n     This is meant to be used as a decorator.\n\n    Args:\n        event (:obj:`str`): The name of the event to subscribe to.\n\n    Returns:\n        :obj:`function`: Decorated function.\n\n    Example:\n        >>> import pyhf\n        >>> @pyhf.events.subscribe(\"myevent\")\n        ... def test(a, b):\n        ...     print(a + b)\n        ...\n        >>> pyhf.events.trigger(\"myevent\")(1, 2)\n        3\n\n    \"\"\"\n\n    global __events\n\n    def __decorator(func: TCallable) -> TCallable:\n        __events.setdefault(event, Callables()).append(func)\n        return func\n\n    return cast(TCallable, __decorator)\n\n\ndef register(event: str) -> Callable[[TCallable], TCallable]:\n    \"\"\"\n    Register a function or object method to trigger an event.  This creates two\n    events: ``{event_name}::before`` and ``{event_name}::after``.\n\n    .. note::\n\n     This is meant to be used as a decorator.\n\n    Args:\n        event (:obj:`str`): The name of the event to subscribe to.\n\n    Returns:\n        :obj:`function`: Decorated function.\n\n    Example:\n        >>> import pyhf\n        >>> @pyhf.events.register(\"test_func\")\n        ... def test(a, b):\n        ...     print(a + b)\n        ...\n        >>> @pyhf.events.subscribe(\"test_func::before\")\n        ... def precall():\n        ...     print(\"before call\")\n        ...\n        >>> @pyhf.events.subscribe(\"test_func::after\")\n        ... def postcall():\n        ...     print(\"after call\")\n        ...\n        >>> test(1, 2)\n        before call\n        3\n        after call\n\n    \"\"\"\n\n    def _register(func: TCallable) -> TCallable:\n        @wraps(func)\n        def register_wrapper(*args, **kwargs):  # type: ignore\n            trigger(f\"{event:s}::before\")()\n            result = func(*args, **kwargs)\n            trigger(f\"{event:s}::after\")()\n            return result\n\n        return register_wrapper\n\n    return cast(TCallable, _register)\n\n\ndef trigger(event: str) -> Callables:\n    \"\"\"\n    Trigger an event if not disabled.\n    \"\"\"\n    global __events, __disabled_events, noop\n    is_noop = bool(event in __disabled_events or event not in __events)\n    return noop if is_noop else __events.get(event)\n\n\ndef disable(event: str):\n    \"\"\"\n    Disable an event from firing.\n    \"\"\"\n    global __disabled_events\n    __disabled_events.add(event)\n\n\ndef enable(event: str):\n    \"\"\"\n    Enable an event to be fired if disabled.\n    \"\"\"\n    global __disabled_events\n    __disabled_events.remove(event)\n", "path": "src/pyhf/events.py"}]}