emtest 0.0.4__py3-none-any.whl → 0.0.6__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.

@@ -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.4
3
+ Version: 0.0.6
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
@@ -11,6 +11,7 @@ Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
12
  License-File: LICENSE-CC0
13
13
  Requires-Dist: tqdm
14
+ Requires-Dist: environs
14
15
  Dynamic: license-file
15
16
 
16
17
  # emtest - Python Testing Utilities
@@ -41,17 +42,21 @@ pip install emtest
41
42
 
42
43
  ## Usage
43
44
 
44
- See the `examples/` directory for complete working examples showing:
45
+ See the [Usage docs](docs/Usage/PytestUtils.md) for explanations and a complete working example showing:
45
46
  - Basic test setup with `conftest.py`
47
+ - Showing and hiding logs
46
48
  - Dual execution pattern implementation
47
49
  - Source loading validation
48
50
  - Thread cleanup testing
49
-
51
+ - Options like minising output, python-debugger breakpoints and more
50
52
 
51
53
  ## Documentation
52
54
 
53
55
  - [Full Documentation](docs/README.md):
54
56
  - [API-Reference](docs/API-Reference/README.html)
57
+ - Usage:
58
+ - [PytestUtils](docs/Usage/PytestUtils.md)
59
+ - [LogRecording](docs/Usage/LogRecording.md)
55
60
 
56
61
  ## Roadmap
57
62
 
@@ -63,18 +68,17 @@ See the `examples/` directory for complete working examples showing:
63
68
  - GitHub Issues: if you find bugs, other issues, or would like to submit feature requests
64
69
  - GitHub Merge Requests: if you think you know what you're doing, you're very welcome!
65
70
 
66
- ### Donations
71
+ ### Donate
67
72
 
68
73
  To support me in my work on this and other projects, you can make donations with the following currencies:
69
74
 
70
75
  - **Bitcoin:** `BC1Q45QEE6YTNGRC5TSZ42ZL3MWV8798ZEF70H2DG0`
71
76
  - **Ethereum:** `0xA32C3bBC2106C986317f202B3aa8eBc3063323D4`
72
- - [**Fiat** (via Credit or Debit Card, Apple Pay, Google Pay, Revolut Pay)](https://checkout.revolut.com/pay/4e4d24de-26cf-4e7d-9e84-ede89ec67f32)
77
+ - [Credit Card, Debit Card, Bank Transfer, Apple Pay, Google Pay, Revolut Pay)](https://checkout.revolut.com/pay/4e4d24de-26cf-4e7d-9e84-ede89ec67f32)
73
78
 
74
79
  Donations help me:
75
80
  - dedicate more time to developing and maintaining open-source projects
76
- - cover costs for IT infrastructure
77
- - finance projects requiring additional hardware & compute
81
+ - cover costs for IT resources
78
82
 
79
83
  ## About the Developer
80
84
 
@@ -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.6.dist-info/licenses/LICENSE,sha256=i720OgyQLs68DSskm7ZLzaGKMnfskBIIHOuPWfwT2q4,907
7
+ emtest-0.0.6.dist-info/licenses/LICENSE-CC0,sha256=kXBMw0w0VVXQAbLjqMKmis4OngrPaK4HY9ScH6MdtxM,7038
8
+ emtest-0.0.6.dist-info/METADATA,sha256=RWSqEYKApX776uR3u-K8Z10zImp8eF69_lhqkU-MY6k,3593
9
+ emtest-0.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ emtest-0.0.6.dist-info/top_level.txt,sha256=sfToBhqMjk35PDheZ42yiMGYNYatT3V5Mwhb79fp5dE,29
11
+ emtest-0.0.6.dist-info/RECORD,,
@@ -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