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,42 @@
1
+ """Tests for eliot._fmt."""
2
+ from unittest import TestCase
3
+ from logxpy._fmt import format_value, DFFormatter, TensorFormatter
4
+
5
+ class MockDF:
6
+ shape = (10, 2)
7
+ columns = ["a", "b"]
8
+ dtypes = {"a": "int", "b": "float"}
9
+ def head(self, n): return self
10
+ def to_dict(self, orient): return [{"a": 1, "b": 1.1}]
11
+
12
+ class MockTensor:
13
+ shape = (2, 2)
14
+ dtype = "float32"
15
+ device = "cpu"
16
+ def min(self): return 0.0
17
+ def max(self): return 1.0
18
+ def mean(self): return 0.5
19
+
20
+ class FormatTests(TestCase):
21
+ def test_truncate(self):
22
+ """Values are truncated."""
23
+ long_str = "x" * 1000
24
+ res = format_value(long_str, max_len=10)
25
+ self.assertEqual(len(res), 13) # 10 + "..."
26
+
27
+ def test_df_formatter(self):
28
+ """DataFrames are formatted."""
29
+ # Mocking get_module is hard without dependency injection,
30
+ # so we'll test the formatter directly assuming support check passed
31
+ fmt = DFFormatter()
32
+ res = fmt.format(MockDF())
33
+ self.assertEqual(res["_type"], "DataFrame")
34
+ self.assertEqual(res["shape"], [10, 2])
35
+
36
+ def test_tensor_formatter(self):
37
+ """Tensors are formatted."""
38
+ fmt = TensorFormatter()
39
+ self.assertTrue(fmt.supports(MockTensor()))
40
+ res = fmt.format(MockTensor())
41
+ self.assertEqual(res["_type"], "MockTensor")
42
+ self.assertEqual(res["dtype"], "float32")
@@ -0,0 +1,292 @@
1
+ """
2
+ Tests for L{eliot._generators}.
3
+ """
4
+
5
+ from pprint import pformat
6
+ from unittest import TestCase
7
+
8
+ from logxpy import Message, start_action
9
+ from ..testing import capture_logging, assertHasAction
10
+
11
+ from .._generators import eliot_friendly_generator_function
12
+
13
+
14
+ def assert_expected_action_tree(
15
+ testcase, logger, expected_action_type, expected_type_tree
16
+ ):
17
+ """
18
+ Assert that a logger has a certain logged action with certain children.
19
+
20
+ @see: L{assert_generator_logs_action_tree}
21
+ """
22
+ logged_action = assertHasAction(testcase, logger, expected_action_type, True)
23
+ type_tree = logged_action.type_tree()
24
+ testcase.assertEqual(
25
+ {expected_action_type: expected_type_tree},
26
+ type_tree,
27
+ "Logger had messages:\n{}".format(pformat(logger.messages, indent=4)),
28
+ )
29
+
30
+
31
+ def assert_generator_logs_action_tree(
32
+ testcase, generator_function, logger, expected_action_type, expected_type_tree
33
+ ):
34
+ """
35
+ Assert that exhausting a generator from the given function logs an action
36
+ of the given type with children matching the given type tree.
37
+
38
+ @param testcase: A test case instance to use to make assertions.
39
+ @type testcase: L{unittest.TestCase}
40
+
41
+ @param generator_function: A no-argument callable that returns a generator
42
+ to be exhausted.
43
+
44
+ @param logger: A logger to inspect for logged messages.
45
+ @type logger: L{MemoryLogger}
46
+
47
+ @param expected_action_type: An action type which should be logged by the
48
+ generator.
49
+ @type expected_action_type: L{unicode}
50
+
51
+ @param expected_type_tree: The types of actions and messages which should
52
+ be logged beneath the expected action. The structure of this value
53
+ matches the structure returned by L{LoggedAction.type_tree}.
54
+ @type expected_type_tree: L{list}
55
+ """
56
+ list(eliot_friendly_generator_function(generator_function)())
57
+ assert_expected_action_tree(
58
+ testcase, logger, expected_action_type, expected_type_tree
59
+ )
60
+
61
+
62
+ class EliotFriendlyGeneratorFunctionTests(TestCase):
63
+ """
64
+ Tests for L{eliot_friendly_generator_function}.
65
+ """
66
+
67
+ # Get our custom assertion failure messages *and* the standard ones.
68
+ longMessage = True
69
+
70
+ @capture_logging(None)
71
+ def test_yield_none(self, logger):
72
+ @eliot_friendly_generator_function
73
+ def g():
74
+ Message.log(message_type="hello")
75
+ yield
76
+ Message.log(message_type="goodbye")
77
+
78
+ g.debug = True # output yielded messages
79
+
80
+ with start_action(action_type="the-action"):
81
+ list(g())
82
+
83
+ assert_expected_action_tree(
84
+ self, logger, "the-action", ["hello", "yielded", "goodbye"]
85
+ )
86
+
87
+ @capture_logging(None)
88
+ def test_yield_value(self, logger):
89
+ expected = object()
90
+
91
+ @eliot_friendly_generator_function
92
+ def g():
93
+ Message.log(message_type="hello")
94
+ yield expected
95
+ Message.log(message_type="goodbye")
96
+
97
+ g.debug = True # output yielded messages
98
+
99
+ with start_action(action_type="the-action"):
100
+ self.assertEqual([expected], list(g()))
101
+
102
+ assert_expected_action_tree(
103
+ self, logger, "the-action", ["hello", "yielded", "goodbye"]
104
+ )
105
+
106
+ @capture_logging(None)
107
+ def test_yield_inside_another_action(self, logger):
108
+ @eliot_friendly_generator_function
109
+ def g():
110
+ Message.log(message_type="a")
111
+ with start_action(action_type="confounding-factor"):
112
+ Message.log(message_type="b")
113
+ yield None
114
+ Message.log(message_type="c")
115
+ Message.log(message_type="d")
116
+
117
+ g.debug = True # output yielded messages
118
+
119
+ with start_action(action_type="the-action"):
120
+ list(g())
121
+
122
+ assert_expected_action_tree(
123
+ self,
124
+ logger,
125
+ "the-action",
126
+ ["a", {"confounding-factor": ["b", "yielded", "c"]}, "d"],
127
+ )
128
+
129
+ @capture_logging(None)
130
+ def test_yield_inside_nested_actions(self, logger):
131
+ @eliot_friendly_generator_function
132
+ def g():
133
+ Message.log(message_type="a")
134
+ with start_action(action_type="confounding-factor"):
135
+ Message.log(message_type="b")
136
+ yield None
137
+ with start_action(action_type="double-confounding-factor"):
138
+ yield None
139
+ Message.log(message_type="c")
140
+ Message.log(message_type="d")
141
+ Message.log(message_type="e")
142
+
143
+ g.debug = True # output yielded messages
144
+
145
+ with start_action(action_type="the-action"):
146
+ list(g())
147
+
148
+ assert_expected_action_tree(
149
+ self,
150
+ logger,
151
+ "the-action",
152
+ [
153
+ "a",
154
+ {
155
+ "confounding-factor": [
156
+ "b",
157
+ "yielded",
158
+ {"double-confounding-factor": ["yielded", "c"]},
159
+ "d",
160
+ ]
161
+ },
162
+ "e",
163
+ ],
164
+ )
165
+
166
+ @capture_logging(None)
167
+ def test_generator_and_non_generator(self, logger):
168
+ @eliot_friendly_generator_function
169
+ def g():
170
+ Message.log(message_type="a")
171
+ yield
172
+ with start_action(action_type="action-a"):
173
+ Message.log(message_type="b")
174
+ yield
175
+ Message.log(message_type="c")
176
+
177
+ Message.log(message_type="d")
178
+ yield
179
+
180
+ g.debug = True # output yielded messages
181
+
182
+ with start_action(action_type="the-action"):
183
+ generator = g()
184
+ next(generator)
185
+ Message.log(message_type="0")
186
+ next(generator)
187
+ Message.log(message_type="1")
188
+ next(generator)
189
+ Message.log(message_type="2")
190
+ self.assertRaises(StopIteration, lambda: next(generator))
191
+
192
+ assert_expected_action_tree(
193
+ self,
194
+ logger,
195
+ "the-action",
196
+ [
197
+ "a",
198
+ "yielded",
199
+ "0",
200
+ {"action-a": ["b", "yielded", "c"]},
201
+ "1",
202
+ "d",
203
+ "yielded",
204
+ "2",
205
+ ],
206
+ )
207
+
208
+ @capture_logging(None)
209
+ def test_concurrent_generators(self, logger):
210
+ @eliot_friendly_generator_function
211
+ def g(which):
212
+ Message.log(message_type="{}-a".format(which))
213
+ with start_action(action_type=which):
214
+ Message.log(message_type="{}-b".format(which))
215
+ yield
216
+ Message.log(message_type="{}-c".format(which))
217
+ Message.log(message_type="{}-d".format(which))
218
+
219
+ g.debug = True # output yielded messages
220
+
221
+ gens = [g("1"), g("2")]
222
+ with start_action(action_type="the-action"):
223
+ while gens:
224
+ for g in gens[:]:
225
+ try:
226
+ next(g)
227
+ except StopIteration:
228
+ gens.remove(g)
229
+
230
+ assert_expected_action_tree(
231
+ self,
232
+ logger,
233
+ "the-action",
234
+ [
235
+ "1-a",
236
+ {"1": ["1-b", "yielded", "1-c"]},
237
+ "2-a",
238
+ {"2": ["2-b", "yielded", "2-c"]},
239
+ "1-d",
240
+ "2-d",
241
+ ],
242
+ )
243
+
244
+ @capture_logging(None)
245
+ def test_close_generator(self, logger):
246
+ @eliot_friendly_generator_function
247
+ def g():
248
+ Message.log(message_type="a")
249
+ try:
250
+ yield
251
+ Message.log(message_type="b")
252
+ finally:
253
+ Message.log(message_type="c")
254
+
255
+ g.debug = True # output yielded messages
256
+
257
+ with start_action(action_type="the-action"):
258
+ gen = g()
259
+ next(gen)
260
+ gen.close()
261
+
262
+ assert_expected_action_tree(self, logger, "the-action", ["a", "yielded", "c"])
263
+
264
+ @capture_logging(None)
265
+ def test_nested_generators(self, logger):
266
+ @eliot_friendly_generator_function
267
+ def g(recurse):
268
+ with start_action(action_type="a-recurse={}".format(recurse)):
269
+ Message.log(message_type="m-recurse={}".format(recurse))
270
+ if recurse:
271
+ set(g(False))
272
+ else:
273
+ yield
274
+
275
+ g.debug = True # output yielded messages
276
+
277
+ with start_action(action_type="the-action"):
278
+ set(g(True))
279
+
280
+ assert_expected_action_tree(
281
+ self,
282
+ logger,
283
+ "the-action",
284
+ [
285
+ {
286
+ "a-recurse=True": [
287
+ "m-recurse=True",
288
+ {"a-recurse=False": ["m-recurse=False", "yielded"]},
289
+ ]
290
+ }
291
+ ],
292
+ )
@@ -0,0 +1,246 @@
1
+ """
2
+ Tests for L{eliot.journald}.
3
+ """
4
+
5
+ from os import getpid, strerror
6
+ from unittest import skipUnless, TestCase
7
+ from subprocess import check_output, CalledProcessError, STDOUT
8
+ from errno import EINVAL
9
+ from sys import argv
10
+ from uuid import uuid4
11
+ from time import sleep
12
+ from json import loads
13
+
14
+ from .._output import MemoryLogger
15
+ from .._message import TASK_UUID_FIELD
16
+ from .. import start_action, Message, write_traceback
17
+
18
+ try:
19
+ from ..journald import sd_journal_send, JournaldDestination
20
+ except ImportError:
21
+ sd_journal_send = None
22
+
23
+
24
+ def _journald_available():
25
+ """
26
+ :return: Boolean indicating whether journald is available to use.
27
+ """
28
+ if sd_journal_send is None:
29
+ return False
30
+ try:
31
+ check_output(["journalctl", "-b", "-n1"], stderr=STDOUT)
32
+ except (OSError, CalledProcessError):
33
+ return False
34
+ return True
35
+
36
+
37
+ def last_journald_message():
38
+ """
39
+ @return: Last journald message from this process as a dictionary in
40
+ journald JSON format.
41
+ """
42
+ # It may take a little for messages to actually reach journald, so we
43
+ # write out marker message and wait until it arrives. We can then be
44
+ # sure the message right before it is the one we want.
45
+ marker = str(uuid4())
46
+ sd_journal_send(MESSAGE=marker.encode("ascii"))
47
+ for i in range(500):
48
+ messages = check_output(
49
+ [
50
+ b"journalctl",
51
+ b"-a",
52
+ b"-o",
53
+ b"json",
54
+ b"-n2",
55
+ b"_PID=" + str(getpid()).encode("ascii"),
56
+ ]
57
+ )
58
+ messages = [loads(m) for m in messages.splitlines()]
59
+ if len(messages) == 2 and messages[1]["MESSAGE"] == marker:
60
+ return messages[0]
61
+ sleep(0.01)
62
+ raise RuntimeError("Message never arrived?!")
63
+
64
+
65
+ class SdJournaldSendTests(TestCase):
66
+ """
67
+ Functional tests for L{sd_journal_send}.
68
+ """
69
+
70
+ @skipUnless(
71
+ _journald_available(), "journald unavailable or inactive on this machine."
72
+ )
73
+ def setUp(self):
74
+ pass
75
+
76
+ def assert_roundtrip(self, value):
77
+ """
78
+ Write a value as a C{MESSAGE} field, assert it is output.
79
+
80
+ @param value: Value to write as unicode.
81
+ """
82
+ sd_journal_send(MESSAGE=value)
83
+ result = last_journald_message()
84
+ self.assertEqual(value, result["MESSAGE"].encode("utf-8"))
85
+
86
+ def test_message(self):
87
+ """
88
+ L{sd_journal_send} can write a C{MESSAGE} field.
89
+ """
90
+ self.assert_roundtrip(b"hello")
91
+
92
+ def test_percent(self):
93
+ """
94
+ L{sd_journal_send} can write a C{MESSAGE} field with a percent.
95
+
96
+ Underlying C API calls does printf formatting so this is a
97
+ plausible failure mode.
98
+ """
99
+ self.assert_roundtrip(b"hello%world")
100
+
101
+ def test_large(self):
102
+ """
103
+ L{sd_journal_send} can write a C{MESSAGE} field with a large message.
104
+ """
105
+ self.assert_roundtrip(b"hello world" * 20000)
106
+
107
+ def test_multiple_fields(self):
108
+ """
109
+ L{sd_journal_send} can send multiple fields.
110
+ """
111
+ sd_journal_send(MESSAGE=b"hello", BONUS_FIELD=b"world")
112
+ result = last_journald_message()
113
+ self.assertEqual(
114
+ (b"hello", b"world"),
115
+ (result["MESSAGE"].encode("ascii"), result["BONUS_FIELD"].encode("ascii")),
116
+ )
117
+
118
+ def test_error(self):
119
+ """
120
+ L{sd_journal_send} raises an error when it gets a non-0 result
121
+ from the underlying API.
122
+ """
123
+ with self.assertRaises(IOError) as context:
124
+ sd_journal_send(**{"": b"123"})
125
+ exc = context.exception
126
+ self.assertEqual((exc.errno, exc.strerror), (EINVAL, strerror(EINVAL)))
127
+
128
+
129
+ class JournaldDestinationTests(TestCase):
130
+ """
131
+ Tests for L{JournaldDestination}.
132
+ """
133
+
134
+ @skipUnless(
135
+ _journald_available(), "journald unavailable or inactive on this machine."
136
+ )
137
+ def setUp(self):
138
+ self.destination = JournaldDestination()
139
+ self.logger = MemoryLogger()
140
+
141
+ def test_json(self):
142
+ """
143
+ The message is stored as JSON in the MESSAGE field.
144
+ """
145
+ Message.new(hello="world", key=123).write(self.logger)
146
+ message = self.logger.messages[0]
147
+ self.destination(message)
148
+ self.assertEqual(loads(last_journald_message()["MESSAGE"]), message)
149
+
150
+ def assert_field_for(self, message, field_name, field_value):
151
+ """
152
+ If the given message is logged by Eliot, the given journald field has
153
+ the expected value.
154
+
155
+ @param message: Dictionary to log.
156
+ @param field_name: Journald field name to check.
157
+ @param field_value: Expected value for the field.
158
+ """
159
+ self.destination(message)
160
+ self.assertEqual(last_journald_message()[field_name], field_value)
161
+
162
+ def test_action_type(self):
163
+ """
164
+ The C{action_type} is stored in the ELIOT_TYPE field.
165
+ """
166
+ action_type = "test:type"
167
+ start_action(self.logger, action_type=action_type)
168
+ self.assert_field_for(self.logger.messages[0], "ELIOT_TYPE", action_type)
169
+
170
+ def test_message_type(self):
171
+ """
172
+ The C{message_type} is stored in the ELIOT_TYPE field.
173
+ """
174
+ message_type = "test:type:message"
175
+ Message.new(message_type=message_type).write(self.logger)
176
+ self.assert_field_for(self.logger.messages[0], "ELIOT_TYPE", message_type)
177
+
178
+ def test_no_type(self):
179
+ """
180
+ An empty string is stored in ELIOT_TYPE if no type is known.
181
+ """
182
+ Message.new().write(self.logger)
183
+ self.assert_field_for(self.logger.messages[0], "ELIOT_TYPE", "")
184
+
185
+ def test_uuid(self):
186
+ """
187
+ The task UUID is stored in the ELIOT_TASK field.
188
+ """
189
+ start_action(self.logger, action_type="xxx")
190
+ self.assert_field_for(
191
+ self.logger.messages[0],
192
+ "ELIOT_TASK",
193
+ self.logger.messages[0][TASK_UUID_FIELD],
194
+ )
195
+
196
+ def test_info_priorities(self):
197
+ """
198
+ Untyped messages, action start, successful action end, random typed
199
+ message all get priority 6 ("info").
200
+ """
201
+ with start_action(self.logger, action_type="xxx"):
202
+ Message.new(message_type="msg").write(self.logger)
203
+ Message.new(x=123).write(self.logger)
204
+ priorities = []
205
+ for message in self.logger.messages:
206
+ self.destination(message)
207
+ priorities.append(last_journald_message()["PRIORITY"])
208
+ self.assertEqual(priorities, ["6", "6", "6", "6"])
209
+
210
+ def test_error_priority(self):
211
+ """
212
+ A failed action gets priority 3 ("error").
213
+ """
214
+ try:
215
+ with start_action(self.logger, action_type="xxx"):
216
+ raise ZeroDivisionError()
217
+ except ZeroDivisionError:
218
+ pass
219
+ self.assert_field_for(self.logger.messages[-1], "PRIORITY", "3")
220
+
221
+ def test_critical_priority(self):
222
+ """
223
+ A traceback gets priority 2 ("critical").
224
+ """
225
+ try:
226
+ raise ZeroDivisionError()
227
+ except ZeroDivisionError:
228
+ write_traceback(logger=self.logger)
229
+ self.assert_field_for(self.logger.serialize()[-1], "PRIORITY", "2")
230
+
231
+ def test_identifier(self):
232
+ """
233
+ C{SYSLOG_IDENTIFIER} defaults to C{os.path.basename(sys.argv[0])}.
234
+ """
235
+ identifier = "/usr/bin/testing123"
236
+ try:
237
+ original = argv[0]
238
+ argv[0] = identifier
239
+ # Recreate JournaldDestination with the newly set argv[0].
240
+ self.destination = JournaldDestination()
241
+ Message.new(message_type="msg").write(self.logger)
242
+ self.assert_field_for(
243
+ self.logger.messages[0], "SYSLOG_IDENTIFIER", "testing123"
244
+ )
245
+ finally:
246
+ argv[0] = original