forked from Checkmk/checkmk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathctx_stack.py
188 lines (136 loc) · 6.04 KB
/
ctx_stack.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#!/usr/bin/env python3
# Copyright (C) 2019 tribe29 GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.
from __future__ import annotations
from functools import partial
from typing import Any, Literal, TypeVar
from flask import g, request, session # pylint: disable=unused-import # noqa: F401
from typing_extensions import assert_never
from werkzeug.local import LocalProxy
T = TypeVar("T")
VarName = Literal[
"config",
"display_options",
"endpoint",
"html",
"output_format",
"output_funnel",
"permission_tracking",
"response",
"theme",
"timeout_manager",
"translation",
"url_filter",
"user_errors",
]
#
# RATIONALE
#
# It appears that there is a bug in Flask that prevents the application context
# from being properly copied when using `copy_current_request_context`.
#
# This bug is tracked in GitHub issue #3306 (https://github.com/pallets/flask/issues/3306),
# and it has not yet been resolved as of the latest version of Flask (1.1.5 as of this writing).
#
# Because of this, we can't store our request local variables on the `g` object, because that would
# make it impossible to have access to them from other threads whenever we would access it via
# `copy_current_request_context`.
#
# Aside:
# There is another bug in `copy_current_request_context`, therefore use `copy_request_context`
# in this repository. See: cmk.gui.utils.request_context:copy_request_context
#
# Because of these issues, we introduce a new meta storage dict on the Request (see cmk.gui.http)
# and store our request local variables there.
#
# NOTE:
# The `g` object can still be used, but keep in mind that accessing those values from other
# threads will be IMPOSSIBLE.
#
def set_global_var(name: VarName, obj: Any) -> None:
# We ignore this, so we don't have to introduce cyclical dependencies.
request.meta[name] = obj # type: ignore[attr-defined]
UNBOUND_MESSAGE = """"[checkmk] Working outside of request context.
You probably tried to access a global resource (e.g. config, Theme, etc.) without being
in a request context. There are various solutions, depending on your context.
If you're operating in the GUI and try to run a thread:
The request context is not automatically copied over whenever you spawn a thread.
You have to do that manually by using `cmk.gui.utils.request_context:copy_request_context`
Example:
from cmk.gui.utils.request_context import copy_request_context
# Bad example. Don't use threading.Thread directly. Crashes won't get propagated this way.
thread = Thread(target=copy_request_context(target_func))
thread.start()
WARNING:
Global resources which use the `g` global objects will not be accessible from newly spawned
threads, even when using `copy_request_context`. Only resources using `set_global_var`
will work.
Some resources (like g.live) are intentionally left this way, due to unclear thread safety!
If you're in a script without a request context:
Create your request context with the `script_helpers:gui_context` context manager.
"""
class Unset:
pass
unset = Unset()
def global_var(name: VarName, default: Any | Unset = unset) -> Any:
if default is unset:
try:
return request.meta[name] # type: ignore[attr-defined]
except RuntimeError as exc:
raise RuntimeError(UNBOUND_MESSAGE) from exc
return request.meta.get(name, default) # type: ignore[attr-defined]
def session_attr(
name: str | tuple[Literal["user"], Literal["transactions"]], type_class: type[T]
) -> T:
def get_attr_or_item(obj, key):
if hasattr(obj, key):
return getattr(obj, key)
try:
return obj[key]
except TypeError:
return None
def maybe_tuple_lookup(attr_names: tuple[str, ...]) -> T | None:
rv = session
for attr in attr_names:
rv = get_attr_or_item(rv, attr)
if rv is None:
return None
if not isinstance(rv, type_class):
raise ValueError(
f"Object session[\"{'.'.join(attr_names)}\"] is not of type {type_class}"
)
return rv
def maybe_str_lookup(_name: str) -> T | None:
return getattr(session, _name)
if isinstance(name, tuple): # pylint: disable=no-else-return
return LocalProxy(partial(maybe_tuple_lookup, name), unbound_message=UNBOUND_MESSAGE) # type: ignore[return-value]
if isinstance(name, str):
return LocalProxy(partial(maybe_str_lookup, name), unbound_message=UNBOUND_MESSAGE) # type: ignore[return-value]
assert_never(name)
# NOTE: Flask offers the proxies below, and we should go into that direction,
# too. But currently our html class is a swiss army knife with tons of
# responsibilities which we should really, really split up...
def request_local_attr(name: str, type_class: type[T]) -> T:
"""Delegate access to the corresponding attribute on RequestContext
When the returned object is accessed, the Proxy will fetch the current
RequestContext from the LocalStack and return the attribute given by `name`.
Args:
name (str): The name of the attribute on RequestContext
type_class (type): The type of the return value. No checking is done.
Returns:
A proxy which wraps the value stored on RequestContext.
"""
def maybe_str_lookup(_name: str) -> T | None:
try:
rv = request.meta[_name] # type: ignore[attr-defined]
except KeyError:
return None
except RuntimeError as exc:
raise RuntimeError(UNBOUND_MESSAGE) from exc
if rv is None:
return rv
if not isinstance(rv, type_class):
raise ValueError(f'Object request.meta["{_name}"] ({rv!r}) is not of type {type_class}')
return rv
return LocalProxy(partial(maybe_str_lookup, name)) # type: ignore[return-value]