logxpy 0.1.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.
Files changed (72) hide show
  1. logxpy/__init__.py +126 -0
  2. logxpy/_action.py +958 -0
  3. logxpy/_async.py +186 -0
  4. logxpy/_base.py +80 -0
  5. logxpy/_compat.py +71 -0
  6. logxpy/_config.py +45 -0
  7. logxpy/_dest.py +88 -0
  8. logxpy/_errors.py +58 -0
  9. logxpy/_fmt.py +68 -0
  10. logxpy/_generators.py +136 -0
  11. logxpy/_mask.py +23 -0
  12. logxpy/_message.py +195 -0
  13. logxpy/_output.py +517 -0
  14. logxpy/_pool.py +93 -0
  15. logxpy/_traceback.py +126 -0
  16. logxpy/_types.py +71 -0
  17. logxpy/_util.py +56 -0
  18. logxpy/_validation.py +486 -0
  19. logxpy/_version.py +21 -0
  20. logxpy/cli.py +61 -0
  21. logxpy/dask.py +172 -0
  22. logxpy/decorators.py +268 -0
  23. logxpy/filter.py +124 -0
  24. logxpy/journald.py +88 -0
  25. logxpy/json.py +149 -0
  26. logxpy/loggerx.py +253 -0
  27. logxpy/logwriter.py +84 -0
  28. logxpy/parse.py +191 -0
  29. logxpy/prettyprint.py +173 -0
  30. logxpy/serializers.py +36 -0
  31. logxpy/stdlib.py +23 -0
  32. logxpy/tai64n.py +45 -0
  33. logxpy/testing.py +472 -0
  34. logxpy/tests/__init__.py +9 -0
  35. logxpy/tests/common.py +36 -0
  36. logxpy/tests/strategies.py +231 -0
  37. logxpy/tests/test_action.py +1751 -0
  38. logxpy/tests/test_api.py +86 -0
  39. logxpy/tests/test_async.py +67 -0
  40. logxpy/tests/test_compat.py +13 -0
  41. logxpy/tests/test_config.py +21 -0
  42. logxpy/tests/test_coroutines.py +105 -0
  43. logxpy/tests/test_dask.py +211 -0
  44. logxpy/tests/test_decorators.py +54 -0
  45. logxpy/tests/test_filter.py +122 -0
  46. logxpy/tests/test_fmt.py +42 -0
  47. logxpy/tests/test_generators.py +292 -0
  48. logxpy/tests/test_journald.py +246 -0
  49. logxpy/tests/test_json.py +208 -0
  50. logxpy/tests/test_loggerx.py +44 -0
  51. logxpy/tests/test_logwriter.py +262 -0
  52. logxpy/tests/test_message.py +334 -0
  53. logxpy/tests/test_output.py +921 -0
  54. logxpy/tests/test_parse.py +309 -0
  55. logxpy/tests/test_pool.py +55 -0
  56. logxpy/tests/test_prettyprint.py +303 -0
  57. logxpy/tests/test_pyinstaller.py +35 -0
  58. logxpy/tests/test_serializers.py +36 -0
  59. logxpy/tests/test_stdlib.py +73 -0
  60. logxpy/tests/test_tai64n.py +66 -0
  61. logxpy/tests/test_testing.py +1051 -0
  62. logxpy/tests/test_traceback.py +251 -0
  63. logxpy/tests/test_twisted.py +814 -0
  64. logxpy/tests/test_util.py +45 -0
  65. logxpy/tests/test_validation.py +989 -0
  66. logxpy/twisted.py +265 -0
  67. logxpy-0.1.0.dist-info/METADATA +100 -0
  68. logxpy-0.1.0.dist-info/RECORD +72 -0
  69. logxpy-0.1.0.dist-info/WHEEL +5 -0
  70. logxpy-0.1.0.dist-info/entry_points.txt +2 -0
  71. logxpy-0.1.0.dist-info/licenses/LICENSE +201 -0
  72. logxpy-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,208 @@
1
+ """
2
+ Tests for L{eliot.json}.
3
+ """
4
+
5
+ from unittest import TestCase, skipUnless, skipIf
6
+ from json import loads
7
+ from importlib.metadata import PackageNotFoundError, version as package_version
8
+
9
+ try:
10
+ import numpy as np
11
+ except ImportError:
12
+ np = None
13
+
14
+ from logxpy.json import (
15
+ EliotJSONEncoder,
16
+ json_default,
17
+ _encoder_to_default_function,
18
+ _dumps_unicode as dumps,
19
+ )
20
+
21
+
22
+ def package_installed(name: str) -> bool:
23
+ """Return whether the package is installed."""
24
+ try:
25
+ package_version(name)
26
+ return True
27
+ except PackageNotFoundError:
28
+ return False
29
+
30
+
31
+ class EliotJSONEncoderTests(TestCase):
32
+ """Tests for L{EliotJSONEncoder} and L{json_default}."""
33
+
34
+ @skipUnless(np, "NumPy not installed.")
35
+ def test_numpy(self):
36
+ """NumPy objects get serialized to readable JSON."""
37
+ encoder_default = _encoder_to_default_function(EliotJSONEncoder())
38
+ l = [
39
+ np.float32(12.5),
40
+ np.float64(2.0),
41
+ np.float16(0.5),
42
+ np.bool_(True),
43
+ np.str_("hello"),
44
+ np.byte(12),
45
+ np.short(12),
46
+ np.intc(-13),
47
+ np.int_(0),
48
+ np.longlong(100),
49
+ np.intp(7),
50
+ np.ubyte(12),
51
+ np.ushort(12),
52
+ np.uintc(13),
53
+ np.ulonglong(100),
54
+ np.uintp(7),
55
+ np.int8(1),
56
+ np.int16(3),
57
+ np.int32(4),
58
+ np.int64(5),
59
+ np.uint8(1),
60
+ np.uint16(3),
61
+ np.uint32(4),
62
+ np.uint64(5),
63
+ ]
64
+ l2 = [l, np.array([1, 2, 3])]
65
+ roundtripped = loads(dumps(l2, default=encoder_default))
66
+ self.assertEqual([l, [1, 2, 3]], roundtripped)
67
+ roundtripped2 = loads(dumps(l2, default=json_default))
68
+ self.assertEqual([l, [1, 2, 3]], roundtripped2)
69
+
70
+ @skipIf(np, "NumPy is installed.")
71
+ def test_numpy_not_imported(self):
72
+ """If NumPy is not available, C{json_default} continues to work.
73
+
74
+ This ensures NumPy isn't a hard dependency.
75
+ """
76
+ with self.assertRaises(TypeError):
77
+ dumps([object()], default=json_default)
78
+ self.assertEqual(dumps(12, default=json_default), "12")
79
+
80
+ @skipUnless(np, "NumPy is not installed.")
81
+ def test_large_numpy_array(self):
82
+ """
83
+ Large NumPy arrays are not serialized completely, since this is (A) a
84
+ performance hit (B) probably a mistake on the user's part.
85
+ """
86
+ a1000 = np.array([0] * 10000)
87
+ self.assertEqual(loads(dumps(a1000, default=json_default)), a1000.tolist())
88
+ a1002 = np.zeros((2, 5001))
89
+ a1002[0][0] = 12
90
+ a1002[0][1] = 13
91
+ a1002[1][1] = 500
92
+ self.assertEqual(
93
+ loads(dumps(a1002, default=json_default)),
94
+ {"array_start": a1002.flat[:10000].tolist(), "original_shape": [2, 5001]},
95
+ )
96
+
97
+ def test_basic_types(self):
98
+ """Test serialization of basic Python types."""
99
+ from pathlib import Path
100
+ from datetime import datetime, date, time
101
+ from uuid import UUID
102
+ from collections import defaultdict, OrderedDict, Counter
103
+ from enum import Enum
104
+
105
+ class TestEnum(Enum):
106
+ A = 1
107
+ B = "test"
108
+
109
+ test_data = {
110
+ "path": Path("/tmp/test"),
111
+ "datetime": datetime(2024, 1, 1, 12, 0),
112
+ "date": date(2024, 1, 1),
113
+ "time": time(12, 0),
114
+ "uuid": UUID("12345678-1234-5678-1234-567812345678"),
115
+ "set": {1, 2, 3},
116
+ "defaultdict": defaultdict(list, {"a": [1, 2]}),
117
+ "ordered_dict": OrderedDict([("a", 1), ("b", 2)]),
118
+ "counter": Counter(["a", "a", "b"]),
119
+ "complex": 1 + 2j,
120
+ "enum": TestEnum.A,
121
+ "enum2": TestEnum.B,
122
+ }
123
+
124
+ serialized = loads(dumps(test_data, default=json_default))
125
+
126
+ self.assertEqual(serialized["path"], "/tmp/test")
127
+ self.assertEqual(serialized["datetime"], "2024-01-01T12:00:00")
128
+ self.assertEqual(serialized["date"], "2024-01-01")
129
+ self.assertEqual(serialized["time"], "12:00:00")
130
+ self.assertEqual(serialized["uuid"], "12345678-1234-5678-1234-567812345678")
131
+ self.assertEqual(serialized["set"], [1, 2, 3])
132
+ self.assertEqual(serialized["defaultdict"], {"a": [1, 2]})
133
+ self.assertEqual(serialized["ordered_dict"], {"a": 1, "b": 2})
134
+ self.assertEqual(serialized["counter"], {"a": 2, "b": 1})
135
+ self.assertEqual(serialized["complex"], {"real": 1.0, "imag": 2.0})
136
+ self.assertEqual(serialized["enum"], 1)
137
+ self.assertEqual(serialized["enum2"], "test")
138
+
139
+ @skipUnless(package_installed("pydantic"), "Pydantic not installed.")
140
+ def test_pydantic(self):
141
+ """Test serialization of Pydantic models."""
142
+ from pydantic import BaseModel
143
+
144
+ class TestModel(BaseModel):
145
+ name: str
146
+ value: int
147
+
148
+ model = TestModel(name="test", value=42)
149
+ serialized = loads(dumps(model, default=json_default))
150
+ self.assertEqual(serialized, {"name": "test", "value": 42})
151
+
152
+ @skipUnless(package_installed("pandas"), "Pandas not installed.")
153
+ def test_pandas(self):
154
+ """Test serialization of Pandas objects."""
155
+ import pandas as pd
156
+
157
+ # Test Timestamp
158
+ ts = pd.Timestamp("2024-01-01 12:00:00")
159
+ self.assertEqual(loads(dumps(ts, default=json_default)), "2024-01-01T12:00:00")
160
+
161
+ # Test Series
162
+ series = pd.Series([1, 2, 3])
163
+ self.assertEqual(loads(dumps(series, default=json_default)), [1, 2, 3])
164
+
165
+ # Test DataFrame
166
+ df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
167
+ self.assertEqual(
168
+ loads(dumps(df, default=json_default)), [{"a": 1, "b": 3}, {"a": 2, "b": 4}]
169
+ )
170
+
171
+ # Test Interval
172
+ interval = pd.Interval(0, 1, closed="both")
173
+ self.assertEqual(
174
+ loads(dumps(interval, default=json_default)),
175
+ {"left": 0, "right": 1, "closed": "both"},
176
+ )
177
+
178
+ # Test Period
179
+ period = pd.Period("2024-01")
180
+ self.assertEqual(loads(dumps(period, default=json_default)), "2024-01")
181
+
182
+ @skipUnless(package_installed("polars"), "Polars not installed.")
183
+ def test_polars(self):
184
+ """Test serialization of Polars objects."""
185
+ import polars as pl
186
+
187
+ # Test Series
188
+ series = pl.Series("a", [1, 2, 3])
189
+ self.assertEqual(loads(dumps(series, default=json_default)), [1, 2, 3])
190
+
191
+ # Test DataFrame
192
+ df = pl.DataFrame({"a": [1, 2], "b": [3, 4]})
193
+ self.assertEqual(
194
+ loads(dumps(df, default=json_default)), [{"a": 1, "b": 3}, {"a": 2, "b": 4}]
195
+ )
196
+
197
+ def test_dataclass(self):
198
+ """Test serialization of dataclasses."""
199
+ from dataclasses import dataclass
200
+
201
+ @dataclass
202
+ class TestDataClass:
203
+ name: str
204
+ value: int
205
+
206
+ obj = TestDataClass(name="test", value=42)
207
+ serialized = loads(dumps(obj, default=json_default))
208
+ self.assertEqual(serialized, {"name": "test", "value": 42})
@@ -0,0 +1,44 @@
1
+ """Tests for eliot.loggerx."""
2
+ from unittest import TestCase
3
+ from logxpy.loggerx import Logger, log
4
+ from logxpy._async import _emit_handlers
5
+ from logxpy._types import Level
6
+
7
+ class LoggerTests(TestCase):
8
+ def setUp(self):
9
+ self.records = []
10
+ _emit_handlers.append(self.records.append)
11
+
12
+ def tearDown(self):
13
+ _emit_handlers.remove(self.records.append)
14
+
15
+ def test_levels(self):
16
+ """Logger methods emit correct levels."""
17
+ log.info("info msg")
18
+ log.error("error msg")
19
+
20
+ self.assertEqual(len(self.records), 2)
21
+ self.assertEqual(self.records[0].level, Level.INFO)
22
+ self.assertEqual(self.records[1].level, Level.ERROR)
23
+
24
+ def test_fluent(self):
25
+ """Logger methods return self."""
26
+ l = Logger()
27
+ self.assertIs(l.info("msg"), l)
28
+
29
+ def test_context(self):
30
+ """Logger respects scope."""
31
+ with log.scope(ctx=1):
32
+ log.info("msg")
33
+
34
+ self.assertEqual(self.records[0].context["ctx"], 1)
35
+
36
+ def test_types(self):
37
+ """Type methods format data."""
38
+ log.json({"a": 1})
39
+ self.assertIn('"a": 1', self.records[0].fields["content"])
40
+
41
+ def test_child(self):
42
+ """Child logger inherits settings."""
43
+ child = log.new("child")
44
+ self.assertEqual(child._name, "root.child")
@@ -0,0 +1,262 @@
1
+ """
2
+ Tests for L{eliot.logwriter}.
3
+ """
4
+
5
+ import time
6
+ import threading
7
+
8
+ from io import BytesIO
9
+
10
+ try:
11
+ from zope.interface.verify import verifyClass
12
+ from twisted.internet import reactor
13
+ from twisted.trial.unittest import TestCase
14
+ from twisted.application.service import IService
15
+ from twisted.python import threadable
16
+ except ImportError:
17
+ # Make tests not run at all.
18
+ TestCase = object
19
+ else:
20
+ # Make sure we always import this if Twisted is available, so broken
21
+ # logwriter.py causes a failure:
22
+ from ..logwriter import ThreadedWriter
23
+
24
+ from .. import Logger, removeDestination, FileDestination
25
+
26
+
27
+ class BlockingFile(object):
28
+ """
29
+ A file-like whose writes can be blocked.
30
+
31
+ Also, allow calling C{getvalue} after C{close}, unlike L{BytesIO}.
32
+ """
33
+
34
+ def __init__(self):
35
+ self.file = BytesIO()
36
+ self.lock = threading.Lock()
37
+ self.data = b""
38
+
39
+ def block(self):
40
+ """
41
+ Prevent writes until L{unblock} is called.
42
+ """
43
+ self.lock.acquire()
44
+
45
+ def unblock(self):
46
+ """
47
+ Allow writes if L{block} was previous called.
48
+ """
49
+ self.lock.release()
50
+
51
+ def getvalue(self):
52
+ """
53
+ Get written bytes.
54
+
55
+ @return: Written bytes.
56
+ """
57
+ return self.data
58
+
59
+ def write(self, data):
60
+ with self.lock:
61
+ self.file.write(data)
62
+
63
+ def flush(self):
64
+ self.data = self.file.getvalue()
65
+
66
+ def close(self):
67
+ self.file.close()
68
+
69
+
70
+ class ThreadedWriterTests(TestCase):
71
+ """
72
+ Tests for L{ThreadedWriter}.
73
+
74
+ Many of these tests involve interactions across threads, so they
75
+ arbitrarily wait for up to 5 seconds to reduce chances of slow thread
76
+ switching causing the test to fail.
77
+ """
78
+
79
+ def test_interface(self):
80
+ """
81
+ L{ThreadedWriter} provides L{IService}.
82
+ """
83
+ verifyClass(IService, ThreadedWriter)
84
+
85
+ def test_name(self):
86
+ """
87
+ L{ThreadedWriter} has a name.
88
+ """
89
+ self.assertEqual(ThreadedWriter.name, "Eliot Log Writer")
90
+
91
+ def test_startServiceRunning(self):
92
+ """
93
+ L{ThreadedWriter.startService} starts the service as required by the
94
+ L{IService} interface.
95
+ """
96
+ writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
97
+ self.assertFalse(writer.running)
98
+ writer.startService()
99
+ self.addCleanup(writer.stopService)
100
+ self.assertTrue(writer.running)
101
+
102
+ def test_stopServiceRunning(self):
103
+ """
104
+ L{ThreadedWriter.stopService} stops the service as required by the
105
+ L{IService} interface.
106
+ """
107
+ writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
108
+ writer.startService()
109
+ d = writer.stopService()
110
+ d.addCallback(lambda _: self.assertFalse(writer.running))
111
+ return d
112
+
113
+ def test_startServiceStartsThread(self):
114
+ """
115
+ L{ThreadedWriter.startService} starts up a thread running
116
+ L{ThreadedWriter._writer}.
117
+ """
118
+ previousThreads = threading.enumerate()
119
+ result = []
120
+ event = threading.Event()
121
+
122
+ def _reader():
123
+ current = threading.current_thread()
124
+ if current not in previousThreads:
125
+ result.append(current)
126
+ event.set()
127
+
128
+ writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
129
+ writer._reader = _reader
130
+ writer.startService()
131
+ event.wait()
132
+ self.assertTrue(result)
133
+ # Make sure thread is dead so it doesn't die half way through another
134
+ # test:
135
+ result[0].join(5)
136
+
137
+ def test_stopServiceStopsThread(self):
138
+ """
139
+ L{ThreadedWriter.stopService} stops the writer thread.
140
+ """
141
+ previousThreads = set(threading.enumerate())
142
+ writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
143
+ writer.startService()
144
+ start = time.time()
145
+ while set(threading.enumerate()) == previousThreads and (
146
+ time.time() - start < 5
147
+ ):
148
+ time.sleep(0.0001)
149
+ # If not true the next assertion might pass by mistake:
150
+ self.assertNotEqual(set(threading.enumerate()), previousThreads)
151
+ writer.stopService()
152
+ while set(threading.enumerate()) != previousThreads and (
153
+ time.time() - start < 5
154
+ ):
155
+ time.sleep(0.0001)
156
+ self.assertEqual(set(threading.enumerate()), previousThreads)
157
+
158
+ def test_stopServiceFinishesWriting(self):
159
+ """
160
+ L{ThreadedWriter.stopService} stops the writer thread, but only after
161
+ all queued writes are written out.
162
+ """
163
+ f = BlockingFile()
164
+ writer = ThreadedWriter(FileDestination(file=f), reactor)
165
+ f.block()
166
+ writer.startService()
167
+ for i in range(100):
168
+ writer({"write": 123})
169
+ threads = threading.enumerate()
170
+ writer.stopService()
171
+ # Make sure writes didn't happen before the stopService, thus making the
172
+ # test pointless:
173
+ self.assertEqual(f.getvalue(), b"")
174
+ f.unblock()
175
+ start = time.time()
176
+ while threading.enumerate() == threads and time.time() - start < 5:
177
+ time.sleep(0.0001)
178
+ self.assertEqual(f.getvalue(), b'{"write":123}\n' * 100)
179
+
180
+ def test_stopServiceResult(self):
181
+ """
182
+ L{ThreadedWriter.stopService} returns a L{Deferred} that fires only
183
+ after the thread has shut down.
184
+ """
185
+ f = BlockingFile()
186
+ writer = ThreadedWriter(FileDestination(file=f), reactor)
187
+ f.block()
188
+ writer.startService()
189
+
190
+ writer({"hello": 123})
191
+ threads = threading.enumerate()
192
+ d = writer.stopService()
193
+ f.unblock()
194
+
195
+ def done(_):
196
+ self.assertEqual(f.getvalue(), b'{"hello":123}\n')
197
+ self.assertNotEqual(threading.enumerate(), threads)
198
+
199
+ d.addCallback(done)
200
+ return d
201
+
202
+ def test_noChangeToIOThread(self):
203
+ """
204
+ Running a L{ThreadedWriter} doesn't modify the Twisted registered IO
205
+ thread.
206
+ """
207
+ writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
208
+ writer.startService()
209
+ d = writer.stopService()
210
+ # Either the current thread (the one running the tests) is the the I/O
211
+ # thread or the I/O thread was never set. Either may happen depending on
212
+ # how and whether the reactor has been started by the unittesting
213
+ # framework.
214
+ d.addCallback(
215
+ lambda _: self.assertIn(
216
+ threadable.ioThread, (None, threading.current_thread().ident)
217
+ )
218
+ )
219
+ return d
220
+
221
+ def test_startServiceRegistersDestination(self):
222
+ """
223
+ L{ThreadedWriter.startService} registers itself as an Eliot log
224
+ destination.
225
+ """
226
+ f = BlockingFile()
227
+ writer = ThreadedWriter(FileDestination(file=f), reactor)
228
+ writer.startService()
229
+ Logger().write({"x": "abc"})
230
+ d = writer.stopService()
231
+ d.addCallback(lambda _: self.assertIn(b"abc", f.getvalue()))
232
+ return d
233
+
234
+ def test_stopServiceUnregistersDestination(self):
235
+ """
236
+ L{ThreadedWriter.stopService} unregisters itself as an Eliot log
237
+ destination.
238
+ """
239
+ writer = ThreadedWriter(FileDestination(file=BytesIO()), reactor)
240
+ writer.startService()
241
+ d = writer.stopService()
242
+ d.addCallback(lambda _: removeDestination(writer))
243
+ return self.assertFailure(d, ValueError)
244
+
245
+ def test_call(self):
246
+ """
247
+ The message passed to L{ThreadedWriter.__call__} is passed to the
248
+ underlying destination in the writer thread.
249
+ """
250
+ result = []
251
+
252
+ def destination(message):
253
+ result.append((message, threading.current_thread().ident))
254
+
255
+ writer = ThreadedWriter(destination, reactor)
256
+ writer.startService()
257
+ thread_ident = writer._thread.ident
258
+ msg = {"key": 123}
259
+ writer(msg)
260
+ d = writer.stopService()
261
+ d.addCallback(lambda _: self.assertEqual(result, [(msg, thread_ident)]))
262
+ return d