globalstash 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- globalstash-0.1.0/LICENSE +21 -0
- globalstash-0.1.0/PKG-INFO +78 -0
- globalstash-0.1.0/README.md +60 -0
- globalstash-0.1.0/pyproject.toml +29 -0
- globalstash-0.1.0/setup.cfg +4 -0
- globalstash-0.1.0/src/globalstash/__init__.py +5 -0
- globalstash-0.1.0/src/globalstash/core.py +178 -0
- globalstash-0.1.0/src/globalstash/py.typed +0 -0
- globalstash-0.1.0/src/globalstash.egg-info/PKG-INFO +78 -0
- globalstash-0.1.0/src/globalstash.egg-info/SOURCES.txt +11 -0
- globalstash-0.1.0/src/globalstash.egg-info/dependency_links.txt +1 -0
- globalstash-0.1.0/src/globalstash.egg-info/top_level.txt +1 -0
- globalstash-0.1.0/tests/test_core.py +88 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: globalstash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Thread-safe, process-wide global state you can import from any module at any depth.
|
|
5
|
+
Author-email: Your Name <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/your-username/globalstash
|
|
8
|
+
Keywords: global,state,flask.g,config,singleton,thread-safe
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# globalstash
|
|
20
|
+
|
|
21
|
+
Thread-safe, process-wide global state you can import from **any module at any depth**, with no `sys.path` tricks.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install globalstash
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from globalstash import g
|
|
31
|
+
|
|
32
|
+
# anywhere in your app
|
|
33
|
+
g.db_pool = create_pool()
|
|
34
|
+
g["config"] = {"debug": True}
|
|
35
|
+
|
|
36
|
+
g.get("missing", "default")
|
|
37
|
+
g.setdefault("cache", {})
|
|
38
|
+
g.update(env="prod", version="1.0")
|
|
39
|
+
"db_pool" in g
|
|
40
|
+
del g.db_pool
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Because Python caches modules, every `from globalstash import g` returns the same object.
|
|
44
|
+
|
|
45
|
+
### Atomic and compound operations
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
g.incr("requests") # atomic counter
|
|
49
|
+
|
|
50
|
+
with g.lock: # read-modify-write
|
|
51
|
+
g.total = g.total + 1
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Named namespaces
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from globalstash import get_state
|
|
58
|
+
|
|
59
|
+
cache = get_state("cache") # same name -> same object everywhere
|
|
60
|
+
cache.ttl = 60
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Tests
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
with g.temporary(debug=True):
|
|
67
|
+
... # values restored on exit
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Notes
|
|
71
|
+
|
|
72
|
+
- Method names (`get`, `pop`, `update`, ...) can't be used as attribute keys; use `g["get"]`.
|
|
73
|
+
- State lives **per process**. Gunicorn/Uvicorn workers each have their own copy. Use Redis or a database for state shared across processes.
|
|
74
|
+
- For per-request state (like `flask.g`), use `contextvars` instead.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# globalstash
|
|
2
|
+
|
|
3
|
+
Thread-safe, process-wide global state you can import from **any module at any depth**, with no `sys.path` tricks.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install globalstash
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from globalstash import g
|
|
13
|
+
|
|
14
|
+
# anywhere in your app
|
|
15
|
+
g.db_pool = create_pool()
|
|
16
|
+
g["config"] = {"debug": True}
|
|
17
|
+
|
|
18
|
+
g.get("missing", "default")
|
|
19
|
+
g.setdefault("cache", {})
|
|
20
|
+
g.update(env="prod", version="1.0")
|
|
21
|
+
"db_pool" in g
|
|
22
|
+
del g.db_pool
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Because Python caches modules, every `from globalstash import g` returns the same object.
|
|
26
|
+
|
|
27
|
+
### Atomic and compound operations
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
g.incr("requests") # atomic counter
|
|
31
|
+
|
|
32
|
+
with g.lock: # read-modify-write
|
|
33
|
+
g.total = g.total + 1
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Named namespaces
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from globalstash import get_state
|
|
40
|
+
|
|
41
|
+
cache = get_state("cache") # same name -> same object everywhere
|
|
42
|
+
cache.ttl = 60
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Tests
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
with g.temporary(debug=True):
|
|
49
|
+
... # values restored on exit
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Notes
|
|
53
|
+
|
|
54
|
+
- Method names (`get`, `pop`, `update`, ...) can't be used as attribute keys; use `g["get"]`.
|
|
55
|
+
- State lives **per process**. Gunicorn/Uvicorn workers each have their own copy. Use Redis or a database for state shared across processes.
|
|
56
|
+
- For per-request state (like `flask.g`), use `contextvars` instead.
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "globalstash"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Thread-safe, process-wide global state you can import from any module at any depth."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Your Name", email = "you@example.com" }]
|
|
13
|
+
keywords = ["global", "state", "flask.g", "config", "singleton", "thread-safe"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/your-username/globalstash"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
globalstash = ["py.typed"]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Core implementation of globalstash."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Any, Dict, Iterator, Tuple
|
|
7
|
+
|
|
8
|
+
_MISSING = object()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GlobalState:
|
|
12
|
+
"""A thread-safe namespace for process-wide global values.
|
|
13
|
+
|
|
14
|
+
Values can be accessed as attributes or as items::
|
|
15
|
+
|
|
16
|
+
g.db = create_pool()
|
|
17
|
+
g["db"]
|
|
18
|
+
g.get("missing", None)
|
|
19
|
+
|
|
20
|
+
Individual operations are atomic. For compound operations
|
|
21
|
+
(read-modify-write), use ``incr`` or the ``lock`` context manager::
|
|
22
|
+
|
|
23
|
+
with g.lock:
|
|
24
|
+
g.total = g.total + 1
|
|
25
|
+
|
|
26
|
+
Note: names of methods (``get``, ``pop``, ``update``...) can't be used
|
|
27
|
+
as attribute keys; use item access (``g["get"]``) for those.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
__slots__ = ("_data", "_lock")
|
|
31
|
+
|
|
32
|
+
def __init__(self, **initial: Any) -> None:
|
|
33
|
+
object.__setattr__(self, "_data", dict(initial))
|
|
34
|
+
object.__setattr__(self, "_lock", threading.RLock())
|
|
35
|
+
|
|
36
|
+
# ---- attribute access -------------------------------------------------
|
|
37
|
+
def __getattr__(self, name: str) -> Any:
|
|
38
|
+
if name.startswith("__") or name in ("_data", "_lock"):
|
|
39
|
+
raise AttributeError(name)
|
|
40
|
+
with self._lock:
|
|
41
|
+
try:
|
|
42
|
+
return self._data[name]
|
|
43
|
+
except KeyError:
|
|
44
|
+
raise AttributeError(
|
|
45
|
+
f"global state has no attribute {name!r}"
|
|
46
|
+
) from None
|
|
47
|
+
|
|
48
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
49
|
+
with self._lock:
|
|
50
|
+
self._data[name] = value
|
|
51
|
+
|
|
52
|
+
def __delattr__(self, name: str) -> None:
|
|
53
|
+
with self._lock:
|
|
54
|
+
try:
|
|
55
|
+
del self._data[name]
|
|
56
|
+
except KeyError:
|
|
57
|
+
raise AttributeError(name) from None
|
|
58
|
+
|
|
59
|
+
# ---- item access ------------------------------------------------------
|
|
60
|
+
def __getitem__(self, key: str) -> Any:
|
|
61
|
+
with self._lock:
|
|
62
|
+
return self._data[key]
|
|
63
|
+
|
|
64
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
65
|
+
with self._lock:
|
|
66
|
+
self._data[key] = value
|
|
67
|
+
|
|
68
|
+
def __delitem__(self, key: str) -> None:
|
|
69
|
+
with self._lock:
|
|
70
|
+
del self._data[key]
|
|
71
|
+
|
|
72
|
+
def __contains__(self, key: object) -> bool:
|
|
73
|
+
with self._lock:
|
|
74
|
+
return key in self._data
|
|
75
|
+
|
|
76
|
+
def __iter__(self) -> Iterator[str]:
|
|
77
|
+
with self._lock:
|
|
78
|
+
return iter(list(self._data))
|
|
79
|
+
|
|
80
|
+
def __len__(self) -> int:
|
|
81
|
+
with self._lock:
|
|
82
|
+
return len(self._data)
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
with self._lock:
|
|
86
|
+
return f"GlobalState({self._data!r})"
|
|
87
|
+
|
|
88
|
+
# ---- dict-like helpers ------------------------------------------------
|
|
89
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
90
|
+
with self._lock:
|
|
91
|
+
return self._data.get(key, default)
|
|
92
|
+
|
|
93
|
+
def set(self, key: str, value: Any) -> None:
|
|
94
|
+
self[key] = value
|
|
95
|
+
|
|
96
|
+
def setdefault(self, key: str, default: Any = None) -> Any:
|
|
97
|
+
with self._lock:
|
|
98
|
+
return self._data.setdefault(key, default)
|
|
99
|
+
|
|
100
|
+
def pop(self, key: str, default: Any = _MISSING) -> Any:
|
|
101
|
+
with self._lock:
|
|
102
|
+
if default is _MISSING:
|
|
103
|
+
return self._data.pop(key)
|
|
104
|
+
return self._data.pop(key, default)
|
|
105
|
+
|
|
106
|
+
def update(self, *args: Any, **kwargs: Any) -> None:
|
|
107
|
+
with self._lock:
|
|
108
|
+
self._data.update(*args, **kwargs)
|
|
109
|
+
|
|
110
|
+
def keys(self) -> Tuple[str, ...]:
|
|
111
|
+
with self._lock:
|
|
112
|
+
return tuple(self._data.keys())
|
|
113
|
+
|
|
114
|
+
def values(self) -> Tuple[Any, ...]:
|
|
115
|
+
with self._lock:
|
|
116
|
+
return tuple(self._data.values())
|
|
117
|
+
|
|
118
|
+
def items(self) -> Tuple[Tuple[str, Any], ...]:
|
|
119
|
+
with self._lock:
|
|
120
|
+
return tuple(self._data.items())
|
|
121
|
+
|
|
122
|
+
def clear(self) -> None:
|
|
123
|
+
with self._lock:
|
|
124
|
+
self._data.clear()
|
|
125
|
+
|
|
126
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
127
|
+
"""Return a shallow copy of the current state."""
|
|
128
|
+
with self._lock:
|
|
129
|
+
return dict(self._data)
|
|
130
|
+
|
|
131
|
+
# ---- atomic / compound operations ------------------------------------
|
|
132
|
+
def incr(self, key: str, amount: int = 1, default: int = 0) -> int:
|
|
133
|
+
"""Atomically add ``amount`` to ``key`` and return the new value."""
|
|
134
|
+
with self._lock:
|
|
135
|
+
value = self._data.get(key, default) + amount
|
|
136
|
+
self._data[key] = value
|
|
137
|
+
return value
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def lock(self) -> "threading.RLock": # type: ignore[valid-type]
|
|
141
|
+
"""The re-entrant lock, for compound operations: ``with g.lock: ...``"""
|
|
142
|
+
return self._lock
|
|
143
|
+
|
|
144
|
+
@contextmanager
|
|
145
|
+
def temporary(self, **overrides: Any) -> Iterator["GlobalState"]:
|
|
146
|
+
"""Temporarily override values (handy in tests); restored on exit."""
|
|
147
|
+
with self._lock:
|
|
148
|
+
saved = {k: self._data.get(k, _MISSING) for k in overrides}
|
|
149
|
+
self._data.update(overrides)
|
|
150
|
+
try:
|
|
151
|
+
yield self
|
|
152
|
+
finally:
|
|
153
|
+
with self._lock:
|
|
154
|
+
for k, v in saved.items():
|
|
155
|
+
if v is _MISSING:
|
|
156
|
+
self._data.pop(k, None)
|
|
157
|
+
else:
|
|
158
|
+
self._data[k] = v
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---- default instance and named registry ---------------------------------
|
|
162
|
+
g = GlobalState()
|
|
163
|
+
|
|
164
|
+
_registry: Dict[str, GlobalState] = {}
|
|
165
|
+
_registry_lock = threading.Lock()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_state(name: str) -> GlobalState:
|
|
169
|
+
"""Return the shared ``GlobalState`` registered under ``name``.
|
|
170
|
+
|
|
171
|
+
The same name always returns the same object, so independent libraries
|
|
172
|
+
or components can each keep their own namespace without collisions.
|
|
173
|
+
"""
|
|
174
|
+
with _registry_lock:
|
|
175
|
+
state = _registry.get(name)
|
|
176
|
+
if state is None:
|
|
177
|
+
state = _registry[name] = GlobalState()
|
|
178
|
+
return state
|
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: globalstash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Thread-safe, process-wide global state you can import from any module at any depth.
|
|
5
|
+
Author-email: Your Name <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/your-username/globalstash
|
|
8
|
+
Keywords: global,state,flask.g,config,singleton,thread-safe
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# globalstash
|
|
20
|
+
|
|
21
|
+
Thread-safe, process-wide global state you can import from **any module at any depth**, with no `sys.path` tricks.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install globalstash
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from globalstash import g
|
|
31
|
+
|
|
32
|
+
# anywhere in your app
|
|
33
|
+
g.db_pool = create_pool()
|
|
34
|
+
g["config"] = {"debug": True}
|
|
35
|
+
|
|
36
|
+
g.get("missing", "default")
|
|
37
|
+
g.setdefault("cache", {})
|
|
38
|
+
g.update(env="prod", version="1.0")
|
|
39
|
+
"db_pool" in g
|
|
40
|
+
del g.db_pool
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Because Python caches modules, every `from globalstash import g` returns the same object.
|
|
44
|
+
|
|
45
|
+
### Atomic and compound operations
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
g.incr("requests") # atomic counter
|
|
49
|
+
|
|
50
|
+
with g.lock: # read-modify-write
|
|
51
|
+
g.total = g.total + 1
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Named namespaces
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from globalstash import get_state
|
|
58
|
+
|
|
59
|
+
cache = get_state("cache") # same name -> same object everywhere
|
|
60
|
+
cache.ttl = 60
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Tests
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
with g.temporary(debug=True):
|
|
67
|
+
... # values restored on exit
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Notes
|
|
71
|
+
|
|
72
|
+
- Method names (`get`, `pop`, `update`, ...) can't be used as attribute keys; use `g["get"]`.
|
|
73
|
+
- State lives **per process**. Gunicorn/Uvicorn workers each have their own copy. Use Redis or a database for state shared across processes.
|
|
74
|
+
- For per-request state (like `flask.g`), use `contextvars` instead.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/globalstash/__init__.py
|
|
5
|
+
src/globalstash/core.py
|
|
6
|
+
src/globalstash/py.typed
|
|
7
|
+
src/globalstash.egg-info/PKG-INFO
|
|
8
|
+
src/globalstash.egg-info/SOURCES.txt
|
|
9
|
+
src/globalstash.egg-info/dependency_links.txt
|
|
10
|
+
src/globalstash.egg-info/top_level.txt
|
|
11
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
globalstash
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from globalstash import GlobalState, g, get_state
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.fixture(autouse=True)
|
|
9
|
+
def _clean():
|
|
10
|
+
g.clear()
|
|
11
|
+
yield
|
|
12
|
+
g.clear()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_attr_and_item_access():
|
|
16
|
+
g.a = 1
|
|
17
|
+
g["b"] = 2
|
|
18
|
+
assert g.a == 1 and g["a"] == 1 and g.b == 2
|
|
19
|
+
assert "a" in g and len(g) == 2
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_missing_attr_raises():
|
|
23
|
+
with pytest.raises(AttributeError):
|
|
24
|
+
g.nope
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_get_pop_setdefault_update():
|
|
28
|
+
assert g.get("x", 5) == 5
|
|
29
|
+
assert g.setdefault("x", 1) == 1
|
|
30
|
+
assert g.setdefault("x", 2) == 1
|
|
31
|
+
g.update(y=3, z=4)
|
|
32
|
+
assert g.pop("y") == 3
|
|
33
|
+
assert g.pop("missing", None) is None
|
|
34
|
+
assert g.to_dict() == {"x": 1, "z": 4}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_delete():
|
|
38
|
+
g.a = 1
|
|
39
|
+
del g.a
|
|
40
|
+
assert "a" not in g
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_same_object_across_imports():
|
|
44
|
+
from globalstash import g as g2
|
|
45
|
+
g.v = 1
|
|
46
|
+
assert g2.v == 1
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_named_registry():
|
|
50
|
+
assert get_state("app") is get_state("app")
|
|
51
|
+
assert get_state("app") is not get_state("other")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_temporary():
|
|
55
|
+
g.a = 1
|
|
56
|
+
with g.temporary(a=2, b=3):
|
|
57
|
+
assert g.a == 2 and g.b == 3
|
|
58
|
+
assert g.a == 1 and "b" not in g
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_incr_is_atomic_across_threads():
|
|
62
|
+
def work():
|
|
63
|
+
for _ in range(2000):
|
|
64
|
+
g.incr("n")
|
|
65
|
+
|
|
66
|
+
threads = [threading.Thread(target=work) for _ in range(8)]
|
|
67
|
+
[t.start() for t in threads]
|
|
68
|
+
[t.join() for t in threads]
|
|
69
|
+
assert g.n == 16000
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_lock_compound_op():
|
|
73
|
+
g.total = 0
|
|
74
|
+
|
|
75
|
+
def work():
|
|
76
|
+
for _ in range(2000):
|
|
77
|
+
with g.lock:
|
|
78
|
+
g.total = g.total + 1
|
|
79
|
+
|
|
80
|
+
threads = [threading.Thread(target=work) for _ in range(8)]
|
|
81
|
+
[t.start() for t in threads]
|
|
82
|
+
[t.join() for t in threads]
|
|
83
|
+
assert g.total == 16000
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_independent_instances():
|
|
87
|
+
a, b = GlobalState(x=1), GlobalState()
|
|
88
|
+
assert "x" not in b and a.x == 1
|