emtest 0.0.4__py3-none-any.whl → 0.0.5__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.
Potentially problematic release.
This version of emtest might be problematic. Click here for more details.
- emtest/log_recording.py +154 -0
- {emtest-0.0.4.dist-info → emtest-0.0.5.dist-info}/METADATA +10 -7
- emtest-0.0.5.dist-info/RECORD +11 -0
- emtest-0.0.4.dist-info/RECORD +0 -10
- {emtest-0.0.4.dist-info → emtest-0.0.5.dist-info}/WHEEL +0 -0
- {emtest-0.0.4.dist-info → emtest-0.0.5.dist-info}/licenses/LICENSE +0 -0
- {emtest-0.0.4.dist-info → emtest-0.0.5.dist-info}/licenses/LICENSE-CC0 +0 -0
- {emtest-0.0.4.dist-info → emtest-0.0.5.dist-info}/top_level.txt +0 -0
emtest/log_recording.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""In-memory log recording utilities for Python's logging framework.
|
|
2
|
+
|
|
3
|
+
This module provides a `RecordingHandler` and helper methods to
|
|
4
|
+
dynamically attach and detach in-memory log recorders to standard
|
|
5
|
+
`logging.Logger` instances. It allows recording log output programmatically
|
|
6
|
+
for testing, debugging, or analysis without relying on files or streams.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> import logging
|
|
10
|
+
>>> import emtest.log_recording
|
|
11
|
+
>>> logger = logging.getLogger("demo")
|
|
12
|
+
>>> logger.setLevel(logging.INFO)
|
|
13
|
+
>>> logger.start_recording() # Start recording logs
|
|
14
|
+
>>> logger.info("Hello world")
|
|
15
|
+
>>> logs = logger.get_recording()
|
|
16
|
+
>>> print(logs)
|
|
17
|
+
['2025-10-18 10:00:00,000 [INFO] Hello world']
|
|
18
|
+
>>> logger.stop_recording() # Stop and remove recorder
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RecordingHandler(logging.Handler):
|
|
25
|
+
"""A logging handler that buffers formatted log records in memory.
|
|
26
|
+
|
|
27
|
+
This handler captures each emitted log record, formats it,
|
|
28
|
+
and stores the resulting string in an internal list.
|
|
29
|
+
It is useful for unit tests or scenarios where log output
|
|
30
|
+
needs to be inspected programmatically.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
formatter (logging.Formatter): The formatter used to format log messages.
|
|
34
|
+
_records (list[str]): Internal buffer of recorded log messages.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, formatter: logging.Formatter):
|
|
38
|
+
"""Initialize the handler with a specific formatter.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
formatter (logging.Formatter): Formatter to apply to each log record.
|
|
42
|
+
"""
|
|
43
|
+
super().__init__()
|
|
44
|
+
self.formatter = formatter
|
|
45
|
+
self._records: list[str] = []
|
|
46
|
+
|
|
47
|
+
def emit(self, record: logging.LogRecord):
|
|
48
|
+
"""Store a formatted log record in the internal buffer.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
record (logging.LogRecord): The log record to handle.
|
|
52
|
+
"""
|
|
53
|
+
self._records.append(self.format(record))
|
|
54
|
+
|
|
55
|
+
def get_records(self) -> list[str]:
|
|
56
|
+
"""Retrieve the recorded log messages.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
list[str]: A copy of the list of formatted log messages.
|
|
60
|
+
"""
|
|
61
|
+
return list(self._records)
|
|
62
|
+
|
|
63
|
+
def clear(self):
|
|
64
|
+
"""Clear all recorded log messages from the buffer."""
|
|
65
|
+
self._records.clear()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def start_recording(self: logging.Logger, name: str | None = None):
|
|
69
|
+
"""Start recording logs to a named recorder.
|
|
70
|
+
|
|
71
|
+
This function dynamically attaches a `RecordingHandler` to the logger.
|
|
72
|
+
If a recorder with the given name already exists, it does nothing.
|
|
73
|
+
|
|
74
|
+
The recorder uses the formatter from the first existing handler
|
|
75
|
+
if available, or falls back to a default formatter.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
self (logging.Logger): The logger instance.
|
|
79
|
+
name (str | None): Optional name of the recorder. Defaults to `"__default__"`.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> logger.start_recording("test")
|
|
83
|
+
>>> logger.info("Message")
|
|
84
|
+
"""
|
|
85
|
+
if name is None:
|
|
86
|
+
name = "__default__"
|
|
87
|
+
|
|
88
|
+
# Ensure the logger has a dict of recording handlers attached
|
|
89
|
+
if not hasattr(self, "_recording_handlers"):
|
|
90
|
+
self._recording_handlers = {}
|
|
91
|
+
if name in self._recording_handlers:
|
|
92
|
+
return # already recording under this name
|
|
93
|
+
|
|
94
|
+
# Use same formatter as first handler, or fallback
|
|
95
|
+
if self.handlers:
|
|
96
|
+
formatter = self.handlers[0].formatter
|
|
97
|
+
else:
|
|
98
|
+
formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
|
|
99
|
+
|
|
100
|
+
handler = RecordingHandler(formatter)
|
|
101
|
+
self.addHandler(handler)
|
|
102
|
+
self._recording_handlers.update({name: handler})
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def stop_recording(self: logging.Logger, name: str | None = None):
|
|
106
|
+
"""Stop and remove a named recorder from the logger.
|
|
107
|
+
|
|
108
|
+
If no recorder exists for the given name, the function does nothing.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
self (logging.Logger): The logger instance.
|
|
112
|
+
name (str | None): Optional name of the recorder to stop.
|
|
113
|
+
Defaults to `"__default__"`.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> logger.stop_recording("test")
|
|
117
|
+
"""
|
|
118
|
+
if name is None:
|
|
119
|
+
name = "__default__"
|
|
120
|
+
|
|
121
|
+
handler = self._recording_handlers.get(name, None)
|
|
122
|
+
if handler:
|
|
123
|
+
self.removeHandler(handler)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_recording(self: logging.Logger, name: str | None = None) -> list[str]:
|
|
127
|
+
"""Retrieve the recorded log messages for a named recorder.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
self (logging.Logger): The logger instance.
|
|
131
|
+
name (str | None): Optional name of the recorder. Defaults to `"__default__"`.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
list[str]: A list of formatted log messages recorded so far.
|
|
135
|
+
Returns an empty list if no handler is found for the name.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
KeyError: If the recorder with the specified name does not exist.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> messages = logger.get_recording("test")
|
|
142
|
+
>>> print(messages)
|
|
143
|
+
"""
|
|
144
|
+
if name is None:
|
|
145
|
+
name = "__default__"
|
|
146
|
+
|
|
147
|
+
handler = self._recording_handlers[name]
|
|
148
|
+
return handler.get_records() if handler else []
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# Monkey-patch Logger with recording methods
|
|
152
|
+
logging.Logger.start_recording = start_recording
|
|
153
|
+
logging.Logger.stop_recording = stop_recording
|
|
154
|
+
logging.Logger.get_recording = get_recording
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: emtest
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Testing utilities which I find useful.
|
|
5
5
|
Author-email: Emendir <dev@emendir.tech>
|
|
6
6
|
License-Expression: MIT-0 OR CC0-1.0
|
|
@@ -41,17 +41,21 @@ pip install emtest
|
|
|
41
41
|
|
|
42
42
|
## Usage
|
|
43
43
|
|
|
44
|
-
See the
|
|
44
|
+
See the [Usage docs](docs/Usage/PytestUtils.md) for explanations and a complete working example showing:
|
|
45
45
|
- Basic test setup with `conftest.py`
|
|
46
|
+
- Showing and hiding logs
|
|
46
47
|
- Dual execution pattern implementation
|
|
47
48
|
- Source loading validation
|
|
48
49
|
- Thread cleanup testing
|
|
49
|
-
|
|
50
|
+
- Options like minising output, python-debugger breakpoints and more
|
|
50
51
|
|
|
51
52
|
## Documentation
|
|
52
53
|
|
|
53
54
|
- [Full Documentation](docs/README.md):
|
|
54
55
|
- [API-Reference](docs/API-Reference/README.html)
|
|
56
|
+
- Usage:
|
|
57
|
+
- [PytestUtils](docs/Usage/PytestUtils.md)
|
|
58
|
+
- [LogRecording](docs/Usage/LogRecording.md)
|
|
55
59
|
|
|
56
60
|
## Roadmap
|
|
57
61
|
|
|
@@ -63,18 +67,17 @@ See the `examples/` directory for complete working examples showing:
|
|
|
63
67
|
- GitHub Issues: if you find bugs, other issues, or would like to submit feature requests
|
|
64
68
|
- GitHub Merge Requests: if you think you know what you're doing, you're very welcome!
|
|
65
69
|
|
|
66
|
-
###
|
|
70
|
+
### Donate
|
|
67
71
|
|
|
68
72
|
To support me in my work on this and other projects, you can make donations with the following currencies:
|
|
69
73
|
|
|
70
74
|
- **Bitcoin:** `BC1Q45QEE6YTNGRC5TSZ42ZL3MWV8798ZEF70H2DG0`
|
|
71
75
|
- **Ethereum:** `0xA32C3bBC2106C986317f202B3aa8eBc3063323D4`
|
|
72
|
-
- [
|
|
76
|
+
- [Credit Card, Debit Card, Bank Transfer, Apple Pay, Google Pay, Revolut Pay)](https://checkout.revolut.com/pay/4e4d24de-26cf-4e7d-9e84-ede89ec67f32)
|
|
73
77
|
|
|
74
78
|
Donations help me:
|
|
75
79
|
- dedicate more time to developing and maintaining open-source projects
|
|
76
|
-
- cover costs for IT
|
|
77
|
-
- finance projects requiring additional hardware & compute
|
|
80
|
+
- cover costs for IT resources
|
|
78
81
|
|
|
79
82
|
## About the Developer
|
|
80
83
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
_auto_run_with_pytest/__init__.py,sha256=P2Tgn9TbVJvNiyOygCrBso4uAd7Og9suuHmKE0-CEtU,753
|
|
2
|
+
emtest/__init__.py,sha256=G_EnVC57IL_OjVJAjaz75y9owlCcdbvXfvR-ncO1icU,344
|
|
3
|
+
emtest/log_recording.py,sha256=vNBRHmjTbYPv2JNj8QPbWzLKabDHZI7G5JDXOG79FnI,5062
|
|
4
|
+
emtest/pytest_utils.py,sha256=qGKGlQTIgKi3BjmUTfcqzPE_0QfWkWfefy8Oitq3sdQ,4844
|
|
5
|
+
emtest/testing_utils.py,sha256=lzXRXwA50YQBHO35Y2s6NcbvgrdTcwGJjJ79CkzJKkk,4780
|
|
6
|
+
emtest-0.0.5.dist-info/licenses/LICENSE,sha256=i720OgyQLs68DSskm7ZLzaGKMnfskBIIHOuPWfwT2q4,907
|
|
7
|
+
emtest-0.0.5.dist-info/licenses/LICENSE-CC0,sha256=kXBMw0w0VVXQAbLjqMKmis4OngrPaK4HY9ScH6MdtxM,7038
|
|
8
|
+
emtest-0.0.5.dist-info/METADATA,sha256=m-lFcX-8XVye2BuMPg2u8Ohcnvsn83myKm0-vaO1hVI,3569
|
|
9
|
+
emtest-0.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
emtest-0.0.5.dist-info/top_level.txt,sha256=sfToBhqMjk35PDheZ42yiMGYNYatT3V5Mwhb79fp5dE,29
|
|
11
|
+
emtest-0.0.5.dist-info/RECORD,,
|
emtest-0.0.4.dist-info/RECORD
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
_auto_run_with_pytest/__init__.py,sha256=P2Tgn9TbVJvNiyOygCrBso4uAd7Og9suuHmKE0-CEtU,753
|
|
2
|
-
emtest/__init__.py,sha256=G_EnVC57IL_OjVJAjaz75y9owlCcdbvXfvR-ncO1icU,344
|
|
3
|
-
emtest/pytest_utils.py,sha256=qGKGlQTIgKi3BjmUTfcqzPE_0QfWkWfefy8Oitq3sdQ,4844
|
|
4
|
-
emtest/testing_utils.py,sha256=lzXRXwA50YQBHO35Y2s6NcbvgrdTcwGJjJ79CkzJKkk,4780
|
|
5
|
-
emtest-0.0.4.dist-info/licenses/LICENSE,sha256=i720OgyQLs68DSskm7ZLzaGKMnfskBIIHOuPWfwT2q4,907
|
|
6
|
-
emtest-0.0.4.dist-info/licenses/LICENSE-CC0,sha256=kXBMw0w0VVXQAbLjqMKmis4OngrPaK4HY9ScH6MdtxM,7038
|
|
7
|
-
emtest-0.0.4.dist-info/METADATA,sha256=Brr2oyYeArMDrP0K17UTUELzQ-LiUURmAQgDlfXMUXE,3395
|
|
8
|
-
emtest-0.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
emtest-0.0.4.dist-info/top_level.txt,sha256=sfToBhqMjk35PDheZ42yiMGYNYatT3V5Mwhb79fp5dE,29
|
|
10
|
-
emtest-0.0.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|