python-devlog 1.1__py3-none-any.whl → 2.0__py3-none-any.whl

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.
devlog/__init__.py CHANGED
@@ -1,9 +1,8 @@
1
1
  from .custom_excepthook import system_excepthook_overwrite
2
- from .decorator import LogOnStart, LogOnError, LogOnEnd
3
- from .stack_trace import set_stack_removal_frames, set_stack_start_frames
2
+ from .decorators import LogOnStart, LogOnError, LogOnEnd
3
+ from .sanitize import Sensitive
4
4
 
5
- __all__ = ["log_on_start", "log_on_end", "log_on_error", "system_excepthook_overwrite", "set_stack_removal_frames",
6
- "set_stack_start_frames"]
5
+ __all__ = ["log_on_start", "log_on_end", "log_on_error", "system_excepthook_overwrite", "Sensitive"]
7
6
 
8
7
 
9
8
  def log_on_start(*args, **kwargs):
devlog/base.py ADDED
@@ -0,0 +1,184 @@
1
+ import asyncio
2
+ import inspect
3
+ import logging
4
+ import sys
5
+ import traceback
6
+ from functools import wraps
7
+ from logging import Logger, Handler
8
+ from types import FunctionType
9
+ from typing import Any, Callable, Dict, Optional, Set, Tuple, Type, Union
10
+ from warnings import warn
11
+
12
+ from .sanitize import Sensitive, unwrap_sensitive, format_value
13
+
14
+
15
+ class WrapCallback:
16
+ r"""A callback that wraps the function and executes it.
17
+
18
+ This class is designed to be used as a mix-in for logging callables,
19
+ such as functions or methods. These are not created manually, instead
20
+ they are created from other log decorators in this package.
21
+ """
22
+
23
+ # default execute wrapped function
24
+ def _devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
25
+ __tracebackhide__ = True
26
+ return fn(*args, **kwargs)
27
+
28
+ async def _async_devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
29
+ __tracebackhide__ = True
30
+ return await fn(*args, **kwargs)
31
+
32
+ def __call__(self, fn: FunctionType) -> Callable[..., Any]:
33
+ if asyncio.iscoroutinefunction(fn):
34
+ @wraps(fn)
35
+ async def devlog_wrapper(*args: Any, **kwargs: Any) -> Any:
36
+ __tracebackhide__ = True
37
+ return await self._async_devlog_executor(fn, *args, **kwargs)
38
+ else:
39
+ @wraps(fn)
40
+ def devlog_wrapper(*args: Any, **kwargs: Any) -> Any:
41
+ __tracebackhide__ = True
42
+ return self._devlog_executor(fn, *args, **kwargs)
43
+
44
+ return devlog_wrapper
45
+
46
+
47
+ class LoggingDecorator(WrapCallback):
48
+ r"""A class that implements the protocol for a logging callable.
49
+
50
+ This class are responsible for create logging message for the function
51
+ and log it.
52
+
53
+ Attributes:
54
+ log_level: The log level to use for logging.
55
+ message: The message format for the log.
56
+ logger: The logger to use for logging.
57
+ If not set, the logger will be created using the module name of the function.
58
+ handler: The handler to use for logging.
59
+ callable_format_variable: The name of the variable to use for the callable.
60
+ args_kwargs: If True, the message will accept {args} {kwargs} format.
61
+ trace_stack: Whether to include the stack trace in the log.
62
+ capture_locals: Capture the locals of the function.
63
+ include_decorator: Whether to include the decorator in the trace log.
64
+ sanitize_params: Set of parameter names to auto-redact in logs.
65
+ """
66
+
67
+ # Global default for sanitize_params
68
+ default_sanitize_params: Optional[Set[str]] = None
69
+
70
+ # Files to exclude from stack traces (populated by submodules)
71
+ _internal_files: Set[str] = set()
72
+
73
+ def __init__(self, log_level: int, message: str, *, logger: Optional[Logger] = None,
74
+ handler: Optional[Handler] = None, args_kwargs: bool = True,
75
+ callable_format_variable: str = "callable",
76
+ trace_stack: bool = False, capture_locals: bool = False,
77
+ include_decorator: bool = False,
78
+ sanitize_params: Optional[Set[str]] = None):
79
+ self.log_level = log_level
80
+ self.message = message
81
+
82
+ if logger is not None and handler is not None:
83
+ warn("logger and handler are both set, the handler will be ignored")
84
+ handler = None
85
+
86
+ self._logger = logger
87
+ self._handler = handler
88
+
89
+ self.callable_format_variable = callable_format_variable
90
+ self.include_decorator = include_decorator
91
+ self.trace_stack = trace_stack or capture_locals
92
+ self.capture_locals = capture_locals
93
+ self.args_kwargs = args_kwargs
94
+ self.sanitize_params = sanitize_params if sanitize_params is not None else self.default_sanitize_params
95
+
96
+ @staticmethod
97
+ def log(logger: Logger, log_level: int, msg: str) -> None:
98
+ logger.log(log_level, msg)
99
+
100
+ def get_logger(self, fn: FunctionType) -> Logger:
101
+ """
102
+ Returns the logger to use for logging.
103
+ if the logger is not set, the logger will be created using the module name of the function.
104
+ and the handler will be added to the logger if any.
105
+ """
106
+ if self._logger is None:
107
+ self._logger = logging.getLogger(fn.__module__)
108
+
109
+ if self._handler is not None:
110
+ self._logger.addHandler(self._handler)
111
+
112
+ return self._logger
113
+
114
+ @classmethod
115
+ def _is_internal_file(cls, filename: str) -> bool:
116
+ return filename in cls._internal_files
117
+
118
+ @classmethod
119
+ def get_stack_summary(cls, include_decorator: bool, *args: Any, **kwargs: Any):
120
+ stack = traceback.StackSummary.extract(traceback.walk_stack(None), *args, **kwargs)
121
+ stack.reverse()
122
+
123
+ for frame in stack:
124
+ if include_decorator or not cls._is_internal_file(frame.filename):
125
+ yield frame
126
+
127
+ @staticmethod
128
+ def bind_param(fn: FunctionType, *args: Any, **kwargs: Any) -> Dict[str, Any]:
129
+ """
130
+ Returns a dictionary with all the `parameter`: `value` in the function.
131
+ """
132
+ callable_signature = inspect.signature(fn)
133
+ bound_arguments = callable_signature.bind(*args, **kwargs)
134
+ bounded_param = {param_name: bound_arguments.arguments.get(param_name, param_object.default) for
135
+ param_name, param_object in bound_arguments.signature.parameters.items()}
136
+
137
+ return bounded_param
138
+
139
+ def _sanitize_bound_params(self, bound_params: Dict[str, Any]) -> Dict[str, str]:
140
+ """Format bound params with sanitization applied."""
141
+ result = {}
142
+ for name, value in bound_params.items():
143
+ result[name] = format_value(value, name, self.sanitize_params)
144
+ return result
145
+
146
+ def build_msg(self, fn: FunctionType, fn_args: Any, fn_kwargs: Any, **extra: Any) -> str:
147
+ """
148
+ Builds the message log using the message format and the function arguments.
149
+ """
150
+ format_kwargs = extra
151
+
152
+ if self.args_kwargs:
153
+ # Sanitize args and kwargs for display
154
+ sanitized_args = tuple(
155
+ format_value(a, sanitize_params=self.sanitize_params) for a in fn_args
156
+ ) if self.sanitize_params or any(isinstance(a, Sensitive) for a in fn_args) else fn_args
157
+ sanitized_kwargs = {
158
+ k: format_value(v, k, self.sanitize_params) for k, v in fn_kwargs.items()
159
+ } if self.sanitize_params or any(isinstance(v, Sensitive) for v in fn_kwargs.values()) else fn_kwargs
160
+ format_kwargs["args"] = sanitized_args
161
+ format_kwargs["kwargs"] = sanitized_kwargs
162
+ else:
163
+ bound = self.bind_param(fn, *fn_args, **fn_kwargs)
164
+ if self.sanitize_params or any(isinstance(v, Sensitive) for v in bound.values()):
165
+ format_kwargs.update(self._sanitize_bound_params(bound))
166
+ else:
167
+ format_kwargs.update(bound)
168
+ return self.message.format(**format_kwargs)
169
+
170
+ @staticmethod
171
+ def _unwrap_args(args: tuple, kwargs: dict) -> Tuple[tuple, dict]:
172
+ """Unwrap Sensitive values before passing to the actual function."""
173
+ unwrapped_args = tuple(unwrap_sensitive(a) for a in args)
174
+ unwrapped_kwargs = {k: unwrap_sensitive(v) for k, v in kwargs.items()}
175
+ return unwrapped_args, unwrapped_kwargs
176
+
177
+ def _has_sensitive(self, args: tuple, kwargs: dict) -> bool:
178
+ """Check if any args/kwargs contain Sensitive values."""
179
+ return any(isinstance(a, Sensitive) for a in args) or \
180
+ any(isinstance(v, Sensitive) for v in kwargs.values())
181
+
182
+
183
+ # Register this file as internal
184
+ LoggingDecorator._internal_files.add(__file__)
@@ -1,33 +1,47 @@
1
+ import sys
1
2
  import traceback
3
+ from typing import Optional, Type
4
+ from types import TracebackType
2
5
 
3
- output_file = "crash.log"
4
-
5
-
6
- def my_except_hook(exception_type, exception_value, traceback_message):
7
- # traceback.print_tb(traceback_message)
8
- with open(output_file, encoding="utf-8") as f:
9
- # save to file
10
- traceback.print_exception(exception_type, exception_value, traceback_message, file=f)
11
- print("Stack (most recent stack last):", file=f)
12
- # output to stdout
13
- traceback.print_exception(exception_type, exception_value, traceback_message)
14
- for frame in traceback.TracebackException.from_exception(exception_value, capture_locals=True).stack[1:]:
15
- # save to a file
16
- message = "\t{filename}:{lineno} on {line}\n\t\t{locals}".format(
17
- filename=frame.filename,
18
- lineno=frame.lineno,
19
- line=frame.line,
20
- locals=frame.locals
21
- )
22
- print(message, file=f)
23
- # output to stdout
24
- print(message)
25
-
26
-
27
- def system_excepthook_overwrite(out_file=None):
28
- import sys
29
- global output_file
6
+
7
+ _output_file: str = "crash.log"
8
+
9
+
10
+ def my_except_hook(exception_type: Type[BaseException],
11
+ exception_value: BaseException,
12
+ traceback_message: Optional[TracebackType]) -> None:
13
+ """Custom exception hook that writes crash info to a file and stdout."""
14
+ tb_exception = traceback.TracebackException(
15
+ exception_type, exception_value, traceback_message,
16
+ capture_locals=True
17
+ )
18
+
19
+ formatted = "".join(tb_exception.format())
20
+
21
+ # Print to stdout
22
+ print(formatted, end="")
23
+
24
+ # Write to file
25
+ try:
26
+ with open(_output_file, encoding="utf-8", mode="w") as f:
27
+ f.write(formatted)
28
+ f.write("\nStack (most recent stack last):\n")
29
+ for frame in tb_exception.stack:
30
+ message = "\t{filename}:{lineno} on {line}\n\t\t{locals}".format(
31
+ filename=frame.filename,
32
+ lineno=frame.lineno,
33
+ line=frame.line,
34
+ locals=frame.locals
35
+ )
36
+ f.write(message + "\n")
37
+ except OSError as e:
38
+ print(f"devlog: Failed to write crash log to {_output_file}: {e}", file=sys.stderr)
39
+
40
+
41
+ def system_excepthook_overwrite(out_file: Optional[str] = None) -> None:
42
+ """Override sys.excepthook to write crash logs with local variable capture."""
43
+ global _output_file
30
44
 
31
45
  if out_file is not None:
32
- output_file = out_file
46
+ _output_file = out_file
33
47
  sys.excepthook = my_except_hook
devlog/decorators.py ADDED
@@ -0,0 +1,212 @@
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+ import traceback
5
+ from types import FunctionType
6
+ from typing import Any, Optional, Tuple, Type, Union
7
+
8
+ from .base import WrapCallback, LoggingDecorator
9
+
10
+
11
+ class LogOnStart(LoggingDecorator):
12
+ r"""A logging decorator that logs the start of the function.
13
+
14
+ This decorator will log the start of the function using the logger and the handler
15
+ provided in the constructor.
16
+
17
+ Attributes:
18
+ log_level: The log level to use for logging.
19
+ message: The message format for the log.
20
+ logger: The logger to use for logging.
21
+ If not set, the logger will be created using the module name of the function.
22
+ handler: The handler to use for logging.
23
+ callable_format_variable: The name of the variable to use for the callable.
24
+ args_kwargs: If True, the message will accept {args} {kwargs} format.
25
+ trace_stack: Whether to include the stack trace in the log.
26
+ trace_stack_message:The message format for the stack trace log.
27
+ """
28
+
29
+ def __init__(self, log_level: int = logging.INFO,
30
+ message: Optional[str] = None,
31
+ **kwargs: Any):
32
+ super().__init__(log_level, message, **kwargs)
33
+ if message is None:
34
+ self.message = "Start func {{{cal_var}.__name__}} " \
35
+ "with args {{args}}, kwargs {{kwargs}}".format(cal_var=self.callable_format_variable)
36
+
37
+ def _devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
38
+ __tracebackhide__ = True
39
+ self._do_logging(fn, *args, **kwargs)
40
+ if self._has_sensitive(args, kwargs):
41
+ args, kwargs = self._unwrap_args(args, kwargs)
42
+ return super()._devlog_executor(fn, *args, **kwargs)
43
+
44
+ async def _async_devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
45
+ __tracebackhide__ = True
46
+ self._do_logging(fn, *args, **kwargs)
47
+ if self._has_sensitive(args, kwargs):
48
+ args, kwargs = self._unwrap_args(args, kwargs)
49
+ return await super()._async_devlog_executor(fn, *args, **kwargs)
50
+
51
+ def _do_logging(self, fn: FunctionType, *args: Any, **kwargs: Any) -> None:
52
+ logger = self.get_logger(fn)
53
+ extra = {self.callable_format_variable: fn}
54
+ msg = self.build_msg(fn, fn_args=args, fn_kwargs=kwargs, **extra)
55
+
56
+ self.log(logger, self.log_level, msg)
57
+ if self.trace_stack:
58
+ stack = traceback.StackSummary(
59
+ LoggingDecorator.get_stack_summary(self.include_decorator, capture_locals=self.capture_locals)
60
+ )
61
+ self.log(logger, logging.DEBUG, "".join(stack.format()).strip())
62
+
63
+
64
+ class LogOnEnd(LoggingDecorator):
65
+ r"""A logging decorator that logs the end of the function.
66
+
67
+ This decorator will log the end of the function using the logger and the handler
68
+ provided in the constructor.
69
+
70
+ Attributes:
71
+ log_level: The log level to use for logging.
72
+ message: The message format for the log.
73
+ logger: The logger to use for logging.
74
+ If not set, the logger will be created using the module name of the function.
75
+ handler: The handler to use for logging.
76
+ callable_format_variable: The name of the variable to use for the callable.
77
+ args_kwargs: If True, the message will accept {args} {kwargs} format.
78
+ trace_stack: Whether to include the stack trace in the log.
79
+ trace_stack_message:The message format for the stack trace log.
80
+ result_format_variable: The variable to use for the result.
81
+ """
82
+
83
+ def __init__(self, log_level: int = logging.INFO,
84
+ message: Optional[str] = None, result_format_variable: str = "result",
85
+ **kwargs: Any):
86
+ super().__init__(log_level, message, **kwargs)
87
+ if message is None:
88
+ self.message = "Successfully run func {{{cal_var}.__name__}} " \
89
+ "with args {{args}}, kwargs {{kwargs}}".format(cal_var=self.callable_format_variable)
90
+ self.result_format_variable = result_format_variable
91
+
92
+ def _devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
93
+ __tracebackhide__ = True
94
+ original_args, original_kwargs = args, kwargs
95
+ if self._has_sensitive(args, kwargs):
96
+ args, kwargs = self._unwrap_args(args, kwargs)
97
+ result = super()._devlog_executor(fn, *args, **kwargs)
98
+ self._do_logging(fn, result, *original_args, **original_kwargs)
99
+ return result
100
+
101
+ async def _async_devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
102
+ __tracebackhide__ = True
103
+ original_args, original_kwargs = args, kwargs
104
+ if self._has_sensitive(args, kwargs):
105
+ args, kwargs = self._unwrap_args(args, kwargs)
106
+ result = await super()._async_devlog_executor(fn, *args, **kwargs)
107
+ self._do_logging(fn, result, *original_args, **original_kwargs)
108
+ return result
109
+
110
+ def _do_logging(self, fn: FunctionType, result: Any, *args: Any, **kwargs: Any) -> None:
111
+ logger = self.get_logger(fn)
112
+
113
+ extra = {self.result_format_variable: result, self.callable_format_variable: fn}
114
+ msg = self.build_msg(fn, fn_args=args, fn_kwargs=kwargs, **extra)
115
+
116
+ self.log(logger, self.log_level, msg)
117
+ if self.trace_stack:
118
+ stack = traceback.StackSummary(
119
+ LoggingDecorator.get_stack_summary(self.include_decorator, capture_locals=self.capture_locals)
120
+ )
121
+ self.log(logger, logging.DEBUG, "".join(stack.format()).strip())
122
+
123
+
124
+ class LogOnError(LoggingDecorator):
125
+ r"""A logging decorator that logs the error of the function.
126
+
127
+ This decorator will log the error of the function using the logger and the handler
128
+ provided in the constructor.
129
+
130
+ Attributes:
131
+ log_level: The log level to use for logging.
132
+ message: The message format for the log.
133
+ logger: The logger to use for logging.
134
+ If not set, the logger will be created using the module name of the function.
135
+ handler: The handler to use for logging.
136
+ args_kwargs: If True, the message will accept {args} {kwargs} format.
137
+ trace_stack: Whether to include the stack trace in the log.
138
+ trace_stack_message:The message format for the stack trace log.
139
+ on_exception: The exception that will catch. Empty mean everything.
140
+ reraise: Whether to reraise the exception or supress it.
141
+ exception_format_variable: The variable to use for the error.
142
+ """
143
+
144
+ def __init__(self, log_level: int = logging.ERROR,
145
+ message: Optional[str] = None,
146
+ on_exceptions: Optional[Union[Type[BaseException], Tuple[Type[BaseException], ...]]] = None,
147
+ reraise: bool = True, exception_format_variable: str = "error", **kwargs: Any):
148
+ super().__init__(log_level, message, **kwargs)
149
+ if message is None:
150
+ self.message = "Error in func {{{cal_var}.__name__}} " \
151
+ "with args {{args}}, kwargs {{kwargs}}\n{{{except_var}}}.".format(
152
+ cal_var=self.callable_format_variable,
153
+ except_var=exception_format_variable
154
+ )
155
+ self.on_exceptions: Union[Type[BaseException], Tuple[Type[BaseException], ...]] = on_exceptions if \
156
+ on_exceptions is not None else BaseException
157
+ self.reraise = reraise
158
+ self.exception_format_variable = exception_format_variable
159
+ self.capture_locals = self.trace_stack or self.capture_locals
160
+
161
+ def _devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
162
+ __tracebackhide__ = True
163
+ original_args, original_kwargs = args, kwargs
164
+ if self._has_sensitive(args, kwargs):
165
+ args, kwargs = self._unwrap_args(args, kwargs)
166
+ try:
167
+ return super()._devlog_executor(fn, *args, **kwargs)
168
+ except BaseException as e:
169
+ self._on_error(fn, e, *original_args, **original_kwargs)
170
+
171
+ async def _async_devlog_executor(self, fn: FunctionType, *args: Any, **kwargs: Any) -> Any:
172
+ __tracebackhide__ = True
173
+ original_args, original_kwargs = args, kwargs
174
+ if self._has_sensitive(args, kwargs):
175
+ args, kwargs = self._unwrap_args(args, kwargs)
176
+ try:
177
+ return await super()._async_devlog_executor(fn, *args, **kwargs)
178
+ except BaseException as e:
179
+ self._on_error(fn, e, *original_args, **original_kwargs)
180
+
181
+ def _do_logging(self, fn: FunctionType, *args: Any, **kwargs: Any) -> None:
182
+ logger = self.get_logger(fn)
183
+ full_traceback = traceback.TracebackException(*sys.exc_info(), capture_locals=self.capture_locals)
184
+ custom_traceback = list(LoggingDecorator.get_stack_summary(
185
+ self.include_decorator, capture_locals=self.capture_locals)
186
+ )
187
+
188
+ # exclude all the stack trace that are in this module
189
+ for frame in full_traceback.stack:
190
+ if not LoggingDecorator._is_internal_file(frame.filename) or self.include_decorator:
191
+ custom_traceback.append(frame)
192
+
193
+ full_traceback.stack = traceback.StackSummary(custom_traceback)
194
+
195
+ extra = {self.callable_format_variable: fn,
196
+ self.exception_format_variable: "".join(list(full_traceback.format(chain=True))).strip()}
197
+
198
+ msg = self.build_msg(fn, fn_args=args, fn_kwargs=kwargs, **extra)
199
+
200
+ self.log(logger, self.log_level, msg)
201
+
202
+ def _on_error(self, fn: FunctionType, exception: BaseException, *args: Any, **kwargs: Any) -> None:
203
+ __tracebackhide__ = True
204
+ if issubclass(exception.__class__, self.on_exceptions) and not hasattr(exception, "_devlog_logged"):
205
+ self._do_logging(fn, *args, **kwargs)
206
+ exception._devlog_logged = True
207
+ if self.reraise:
208
+ raise
209
+
210
+
211
+ # Register this file as internal
212
+ LoggingDecorator._internal_files.add(__file__)
devlog/sanitize.py ADDED
@@ -0,0 +1,139 @@
1
+ import operator
2
+ from typing import Any
3
+
4
+
5
+ class Sensitive:
6
+ """Transparent proxy that marks a value for redaction in devlog logs only.
7
+
8
+ The wrapped value passes through to functions unchanged — all attribute access,
9
+ operators, iteration, etc. are delegated to the real value. Redaction only
10
+ happens when devlog formats log messages.
11
+
12
+ Usage:
13
+ password = Sensitive("hunter2")
14
+ login(password) # function receives "hunter2" normally
15
+ # but devlog logs show: login(password='***')
16
+ """
17
+
18
+ __slots__ = ('_devlog_value', '_devlog_mask')
19
+
20
+ def __init__(self, value: Any, mask: str = "***"):
21
+ object.__setattr__(self, '_devlog_value', value)
22
+ object.__setattr__(self, '_devlog_mask', mask)
23
+
24
+ @property
25
+ def real_value(self) -> Any:
26
+ return object.__getattribute__(self, '_devlog_value')
27
+
28
+ @property
29
+ def mask(self) -> str:
30
+ return object.__getattribute__(self, '_devlog_mask')
31
+
32
+ # --- transparent proxy ---
33
+ def __getattr__(self, name: str) -> Any:
34
+ return getattr(object.__getattribute__(self, '_devlog_value'), name)
35
+
36
+ def __setattr__(self, name: str, value: Any) -> None:
37
+ setattr(object.__getattribute__(self, '_devlog_value'), name, value)
38
+
39
+ def __delattr__(self, name: str) -> None:
40
+ delattr(object.__getattribute__(self, '_devlog_value'), name)
41
+
42
+ def __str__(self) -> str:
43
+ return str(object.__getattribute__(self, '_devlog_value'))
44
+
45
+ def __repr__(self) -> str:
46
+ return repr(object.__getattribute__(self, '_devlog_value'))
47
+
48
+ def __bool__(self) -> bool:
49
+ return bool(object.__getattribute__(self, '_devlog_value'))
50
+
51
+ def __len__(self) -> int:
52
+ return len(object.__getattribute__(self, '_devlog_value'))
53
+
54
+ def __iter__(self):
55
+ return iter(object.__getattribute__(self, '_devlog_value'))
56
+
57
+ def __contains__(self, item: Any) -> bool:
58
+ return item in object.__getattribute__(self, '_devlog_value')
59
+
60
+ def __getitem__(self, key: Any) -> Any:
61
+ return object.__getattribute__(self, '_devlog_value')[key]
62
+
63
+ def __setitem__(self, key: Any, value: Any) -> None:
64
+ object.__getattribute__(self, '_devlog_value')[key] = value
65
+
66
+ def __delitem__(self, key: Any) -> None:
67
+ del object.__getattribute__(self, '_devlog_value')[key]
68
+
69
+ def __eq__(self, other: Any) -> bool:
70
+ val = object.__getattribute__(self, '_devlog_value')
71
+ other_val = other.real_value if isinstance(other, Sensitive) else other
72
+ return val == other_val
73
+
74
+ def __hash__(self) -> int:
75
+ return hash(object.__getattribute__(self, '_devlog_value'))
76
+
77
+ def __lt__(self, other: Any) -> bool:
78
+ val = object.__getattribute__(self, '_devlog_value')
79
+ other_val = other.real_value if isinstance(other, Sensitive) else other
80
+ return val < other_val
81
+
82
+ def __le__(self, other: Any) -> bool:
83
+ val = object.__getattribute__(self, '_devlog_value')
84
+ other_val = other.real_value if isinstance(other, Sensitive) else other
85
+ return val <= other_val
86
+
87
+ def __gt__(self, other: Any) -> bool:
88
+ val = object.__getattribute__(self, '_devlog_value')
89
+ other_val = other.real_value if isinstance(other, Sensitive) else other
90
+ return val > other_val
91
+
92
+ def __ge__(self, other: Any) -> bool:
93
+ val = object.__getattribute__(self, '_devlog_value')
94
+ other_val = other.real_value if isinstance(other, Sensitive) else other
95
+ return val >= other_val
96
+
97
+ def __add__(self, other: Any) -> Any:
98
+ val = object.__getattribute__(self, '_devlog_value')
99
+ other_val = other.real_value if isinstance(other, Sensitive) else other
100
+ return val + other_val
101
+
102
+ def __radd__(self, other: Any) -> Any:
103
+ val = object.__getattribute__(self, '_devlog_value')
104
+ return other + val
105
+
106
+ def __mul__(self, other: Any) -> Any:
107
+ val = object.__getattribute__(self, '_devlog_value')
108
+ other_val = other.real_value if isinstance(other, Sensitive) else other
109
+ return val * other_val
110
+
111
+ def __rmul__(self, other: Any) -> Any:
112
+ val = object.__getattribute__(self, '_devlog_value')
113
+ return other * val
114
+
115
+ def __int__(self) -> int:
116
+ return int(object.__getattribute__(self, '_devlog_value'))
117
+
118
+ def __float__(self) -> float:
119
+ return float(object.__getattribute__(self, '_devlog_value'))
120
+
121
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
122
+ return object.__getattribute__(self, '_devlog_value')(*args, **kwargs)
123
+
124
+
125
+ def unwrap_sensitive(value: Any) -> Any:
126
+ """Unwrap a Sensitive value to its real value, or return as-is."""
127
+ if isinstance(value, Sensitive):
128
+ return value.real_value
129
+ return value
130
+
131
+
132
+ def format_value(value: Any, param_name: str = "",
133
+ sanitize_params: set = None) -> str:
134
+ """Format a value for log display, applying redaction as needed."""
135
+ if isinstance(value, Sensitive):
136
+ return object.__getattribute__(value, '_devlog_mask')
137
+ if sanitize_params and param_name in sanitize_params:
138
+ return "***"
139
+ return repr(value)