tremors 0.4.2__tar.gz → 0.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tremors
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Tremors is a library for logging while collecting metrics.
5
5
  Author-email: Narvin Singh <Narvin.A.Singh@gmail.com>
6
6
  License: Tremors is a library for logging with metrics.
@@ -32,10 +32,10 @@ Description-Content-Type: text/x-rst
32
32
  License-File: LICENSE
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: coverage~=7.11; extra == "dev"
35
- Requires-Dist: mypy~=1.18; extra == "dev"
35
+ Requires-Dist: mypy~=1.19; extra == "dev"
36
36
  Requires-Dist: pre-commit~=4.3; extra == "dev"
37
37
  Requires-Dist: pytest~=9.0; extra == "dev"
38
- Requires-Dist: ruff~=0.14.2; extra == "dev"
38
+ Requires-Dist: ruff~=0.15.0; extra == "dev"
39
39
  Requires-Dist: sphinx-lint~=1.0; extra == "dev"
40
40
  Requires-Dist: yamllint~=1.37; extra == "dev"
41
41
  Provides-Extra: doc
@@ -233,9 +233,16 @@ In the previous example, a new counter collector was used each time the
233
233
  function is called. Let's reuse the same collector to keep a tally of errors
234
234
  across *all* calls to the function.
235
235
 
236
+ .. note::
237
+
238
+ The counter factory returns a ``CollectorFactory`` that will result in a
239
+ new collector being created for each ``fn`` call. To use the *same*
240
+ collector, we must call the CollectorFactory, hence the trailing
241
+ parentheses on the line where we set ``fn_errors``.
242
+
236
243
  .. code-block:: python
237
244
 
238
- fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)
245
+ fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)()
239
246
 
240
247
 
241
248
  @tremors.logged(fn_errors)
@@ -283,6 +290,47 @@ single logger used in both function calls maintains its state between calls.
283
290
  errors=2 ERROR:root:uh-ho!
284
291
  errors=2 INFO:root:exited: context
285
292
 
293
+ Collectors may be inherited by descendant loggers. Let's count errors across
294
+ nested loggers.
295
+
296
+ .. code-block:: python
297
+
298
+ @tremors.logged(
299
+ collector.counter.factory(
300
+ name="errors", level=logging.ERROR, inherit=True
301
+ )
302
+ )
303
+ def parent(*, logger: tremors.Logger = tremors.from_logged) -> None:
304
+ logger.error("uh-ho!")
305
+ child()
306
+ child()
307
+
308
+
309
+ @tremors.logged
310
+ def child(*, logger: tremors.Logger = tremors.from_logged) -> None:
311
+ logger.error("doh!")
312
+ grandchild()
313
+
314
+
315
+ @tremors.logged
316
+ def grandchild(*, logger: tremors.Logger = tremors.from_logged) -> None:
317
+ logger.info("so far, so good")
318
+ logger.error("spoke too soon!")
319
+
320
+
321
+ parent()
322
+
323
+ The entered and exited lines have been omitted. The ``parent`` counter is used the ``child`` and ``grandchild`` functions.
324
+ .. code-block:: shell
325
+
326
+ errors=1 ERROR:root:uh-ho!
327
+ errors=2 ERROR:root:doh!
328
+ errors=2 INFO:root:so far, so good
329
+ errors=3 ERROR:root:spoke too soon!
330
+ errors=4 ERROR:root:doh!
331
+ errors=4 INFO:root:so far, so good
332
+ errors=5 ERROR:root:spoke too soon!
333
+
286
334
  See the `collector module`_ in the full `documentation`_ for how you can
287
335
  define your own collectors, and bundles.
288
336
 
@@ -185,9 +185,16 @@ In the previous example, a new counter collector was used each time the
185
185
  function is called. Let's reuse the same collector to keep a tally of errors
186
186
  across *all* calls to the function.
187
187
 
188
+ .. note::
189
+
190
+ The counter factory returns a ``CollectorFactory`` that will result in a
191
+ new collector being created for each ``fn`` call. To use the *same*
192
+ collector, we must call the CollectorFactory, hence the trailing
193
+ parentheses on the line where we set ``fn_errors``.
194
+
188
195
  .. code-block:: python
189
196
 
190
- fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)
197
+ fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)()
191
198
 
192
199
 
193
200
  @tremors.logged(fn_errors)
@@ -235,6 +242,47 @@ single logger used in both function calls maintains its state between calls.
235
242
  errors=2 ERROR:root:uh-ho!
236
243
  errors=2 INFO:root:exited: context
237
244
 
245
+ Collectors may be inherited by descendant loggers. Let's count errors across
246
+ nested loggers.
247
+
248
+ .. code-block:: python
249
+
250
+ @tremors.logged(
251
+ collector.counter.factory(
252
+ name="errors", level=logging.ERROR, inherit=True
253
+ )
254
+ )
255
+ def parent(*, logger: tremors.Logger = tremors.from_logged) -> None:
256
+ logger.error("uh-ho!")
257
+ child()
258
+ child()
259
+
260
+
261
+ @tremors.logged
262
+ def child(*, logger: tremors.Logger = tremors.from_logged) -> None:
263
+ logger.error("doh!")
264
+ grandchild()
265
+
266
+
267
+ @tremors.logged
268
+ def grandchild(*, logger: tremors.Logger = tremors.from_logged) -> None:
269
+ logger.info("so far, so good")
270
+ logger.error("spoke too soon!")
271
+
272
+
273
+ parent()
274
+
275
+ The entered and exited lines have been omitted. The ``parent`` counter is used the ``child`` and ``grandchild`` functions.
276
+ .. code-block:: shell
277
+
278
+ errors=1 ERROR:root:uh-ho!
279
+ errors=2 ERROR:root:doh!
280
+ errors=2 INFO:root:so far, so good
281
+ errors=3 ERROR:root:spoke too soon!
282
+ errors=4 ERROR:root:doh!
283
+ errors=4 INFO:root:so far, so good
284
+ errors=5 ERROR:root:spoke too soon!
285
+
238
286
  See the `collector module`_ in the full `documentation`_ for how you can
239
287
  define your own collectors, and bundles.
240
288
 
@@ -25,10 +25,10 @@ dependencies = [
25
25
  [project.optional-dependencies]
26
26
  dev = [
27
27
  "coverage ~= 7.11",
28
- "mypy ~= 1.18",
28
+ "mypy ~= 1.19",
29
29
  "pre-commit ~= 4.3",
30
30
  "pytest ~= 9.0",
31
- "ruff ~= 0.14.2",
31
+ "ruff ~= 0.15.0",
32
32
  "sphinx-lint ~= 1.0",
33
33
  "yamllint ~= 1.37",
34
34
  ]
@@ -5,6 +5,6 @@ from tremors.logger import EXTRA_KEY, Collector, Logger
5
5
 
6
6
  __authors__ = ["Narvin Singh"]
7
7
  __project__ = "Tremors"
8
- __version__ = "0.4.2"
8
+ __version__ = "0.5.0"
9
9
 
10
10
  __all__ = ["EXTRA_KEY", "Collector", "Logger", "__version__", "from_logged", "logged"]
@@ -15,7 +15,7 @@ fixed signature as described in :class:`Formatter`.
15
15
  import dataclasses
16
16
  import logging
17
17
  import time
18
- import uuid # noqa: TC003
18
+ import uuid
19
19
  from collections.abc import Callable, Mapping
20
20
  from typing import NamedTuple, Protocol
21
21
 
@@ -73,7 +73,8 @@ class CollectorBundle[T, **P](NamedTuple):
73
73
  """A bundle to create, and work with a collector."""
74
74
 
75
75
  state: type[T]
76
- factory: Callable[P, tremors.logger.Collector[T]]
76
+ factory_cls: type[tremors.logger.CollectorFactory[T]]
77
+ factory: Callable[P, tremors.logger.CollectorFactory[T]]
77
78
  formatter: Formatter
78
79
 
79
80
 
@@ -96,19 +97,50 @@ def _identifier_collect(
96
97
  return IndentifierState(group_id=logger.group_id, parent=logger.parent, path=logger.path)
97
98
 
98
99
 
100
+ class IdentifierFactory(tremors.logger.CollectorFactory[IndentifierState | None]):
101
+ """Create collectors to gather logger identifiers."""
102
+
103
+ def __init__(
104
+ self, name: str = "elapsed", *, level: int = logging.INFO, inherit: bool = False
105
+ ) -> None:
106
+ """Initialize the state to create collectors.
107
+
108
+ Args:
109
+ name: The name of collectors created by the factory.
110
+ level: The level of collectors created by the factory.
111
+ inherit: If True, collectors created by the factory will be
112
+ inherited.
113
+ """
114
+ self._name = name
115
+ self._level = level
116
+ self._inherit = inherit
117
+
118
+ def __call__(self) -> tremors.logger.Collector[IndentifierState | None]:
119
+ """Create a collector."""
120
+ return tremors.logger.Collector(
121
+ id=uuid.uuid4(),
122
+ name=self._name,
123
+ level=self._level,
124
+ inherit=self._inherit,
125
+ state=None,
126
+ collect=_identifier_collect,
127
+ )
128
+
129
+
99
130
  def identifier_factory(
100
- name: str = "identifier", *, level: int = logging.INFO
101
- ) -> tremors.logger.Collector[IndentifierState | None]:
102
- """Create a collector to gather logger identifiers.
131
+ name: str = "identifier", *, level: int = logging.INFO, inherit: bool = False
132
+ ) -> IdentifierFactory:
133
+ """Create a factory for collectors to gather logger identifiers.
103
134
 
104
135
  Args:
105
- name: The collector name.
106
- level: The collector level.
136
+ name: The name of collectors created by the factory.
137
+ level: The level of collectors created by the factory.
138
+ inherit: If True, collectors created by the factory will be inherited.
107
139
 
108
140
  Returns:
109
- An identifier collector with the specified name, and level.
141
+ A factory to create an identifer collector with the specified attributes.
110
142
  """
111
- return tremors.logger.Collector(name=name, level=level, state=None, collect=_identifier_collect)
143
+ return IdentifierFactory(name, level=level, inherit=inherit)
112
144
 
113
145
 
114
146
  def identifier_formatter(
@@ -154,7 +186,10 @@ def identifier_formatter(
154
186
 
155
187
 
156
188
  identifier = CollectorBundle(
157
- state=IndentifierState, factory=identifier_factory, formatter=identifier_formatter
189
+ state=IndentifierState,
190
+ factory_cls=IdentifierFactory,
191
+ factory=identifier_factory,
192
+ formatter=identifier_formatter,
158
193
  )
159
194
  """The identifier collector bundle.
160
195
 
@@ -283,26 +318,69 @@ def _counter_collect(state: CounterState, _: tremors.logger.LogItem) -> CounterS
283
318
  return state
284
319
 
285
320
 
321
+ class CounterFactory(tremors.logger.CollectorFactory[CounterState]):
322
+ """Create collectors to count logging calls."""
323
+
324
+ def __init__(
325
+ self,
326
+ name: str = "counter",
327
+ *,
328
+ level: int = logging.INFO,
329
+ inherit: bool = False,
330
+ initial: int = 0,
331
+ step: int = 1,
332
+ ) -> None:
333
+ """Initialize the state to create collectors.
334
+
335
+ Args:
336
+ name: The name of collectors created by the factory.
337
+ level: The level of collectors created by the factory.
338
+ inherit: If True, collectors created by the factory will be
339
+ inherited.
340
+ initial: The initial value of the count of collectors created by
341
+ the factory.
342
+ step: How much to increase the count by each time the collectors
343
+ created by the factory run.
344
+ """
345
+ self._name = name
346
+ self._level = level
347
+ self._inherit = inherit
348
+ self._initial = initial
349
+ self._step = step
350
+
351
+ def __call__(self) -> tremors.logger.Collector[CounterState]:
352
+ """Create a collector."""
353
+ return tremors.logger.Collector(
354
+ id=uuid.uuid4(),
355
+ name=self._name,
356
+ level=self._level,
357
+ inherit=self._inherit,
358
+ state=CounterState(count=self._initial, step=self._step),
359
+ collect=_counter_collect,
360
+ )
361
+
362
+
286
363
  def counter_factory(
287
- name: str = "counter", *, level: int = logging.INFO, initial: int = 0, step: int = 1
288
- ) -> tremors.logger.Collector[CounterState]:
364
+ name: str = "counter",
365
+ *,
366
+ level: int = logging.INFO,
367
+ inherit: bool = False,
368
+ initial: int = 0,
369
+ step: int = 1,
370
+ ) -> CounterFactory:
289
371
  """Create a collector to count logging calls.
290
372
 
291
373
  Args:
292
374
  name: The collector name.
293
375
  level: The collector level.
376
+ inherit: If True, the collector will be inherited.
294
377
  initial: The initial value of the count.
295
378
  step: How much to increase the count by each time the collector runs.
296
379
 
297
380
  Returns:
298
- A counter collector with the specified name, level, behavior, and mutable state.
381
+ A factory to create counter collector with the specified attributes.
299
382
  """
300
- return tremors.logger.Collector(
301
- name=name,
302
- level=level,
303
- state=CounterState(count=initial, step=step),
304
- collect=_counter_collect,
305
- )
383
+ return CounterFactory(name, level=level, inherit=inherit, initial=initial, step=step)
306
384
 
307
385
 
308
386
  def counter_formatter(
@@ -325,7 +403,12 @@ def counter_formatter(
325
403
  return str(fmt).format(counter=state.count)
326
404
 
327
405
 
328
- counter = CollectorBundle(state=CounterState, factory=counter_factory, formatter=counter_formatter)
406
+ counter = CollectorBundle(
407
+ state=CounterState,
408
+ factory_cls=CounterFactory,
409
+ factory=counter_factory,
410
+ formatter=counter_formatter,
411
+ )
329
412
  """The counter collector bundle.
330
413
 
331
414
  Examples:
@@ -432,21 +515,50 @@ def _collect_elapsed(state: ElapsedState, _: tremors.logger.LogItem) -> ElapsedS
432
515
  return state
433
516
 
434
517
 
518
+ class ElapsedFactory(tremors.logger.CollectorFactory[ElapsedState]):
519
+ """Create collectors to measure how much time has elapsed."""
520
+
521
+ def __init__(
522
+ self, name: str = "elapsed", *, level: int = logging.INFO, inherit: bool = False
523
+ ) -> None:
524
+ """Initialize the state to create collectors.
525
+
526
+ Args:
527
+ name: The name of collectors created by the factory.
528
+ level: The level of collectors created by the factory.
529
+ inherit: If True, collectors created by the factory will be
530
+ inherited.
531
+ """
532
+ self._name = name
533
+ self._level = level
534
+ self._inherit = inherit
535
+
536
+ def __call__(self) -> tremors.logger.Collector[ElapsedState]:
537
+ """Create a collector."""
538
+ return tremors.logger.Collector(
539
+ id=uuid.uuid4(),
540
+ name=self._name,
541
+ level=self._level,
542
+ inherit=self._inherit,
543
+ state=ElapsedState(t0=None, t=None),
544
+ collect=_collect_elapsed,
545
+ )
546
+
547
+
435
548
  def elapsed_factory(
436
- name: str = "elapsed", *, level: int = logging.INFO
437
- ) -> tremors.logger.Collector[ElapsedState]:
438
- """Create a collector to measure how much time has elapsed.
549
+ name: str = "elapsed", *, level: int = logging.INFO, inherit: bool = False
550
+ ) -> ElapsedFactory:
551
+ """Create a factory for collectors to measure how much time has elapsed.
439
552
 
440
553
  Args:
441
- name: The collector name.
442
- level: The collector level.
554
+ name: The name of collectors created by the factory.
555
+ level: The level of collectors created by the factory.
556
+ inherit: If True, collectors created by the factory will be inherited.
443
557
 
444
558
  Returns:
445
- An elapsed collector with the specified name, and level.
559
+ A factory to create an elapsed collector with the specified attributes.
446
560
  """
447
- return tremors.logger.Collector(
448
- name=name, level=level, state=ElapsedState(t0=None, t=None), collect=_collect_elapsed
449
- )
561
+ return ElapsedFactory(name, level=level, inherit=inherit)
450
562
 
451
563
 
452
564
  def elapsed_formatter(
@@ -479,7 +591,12 @@ def elapsed_formatter(
479
591
  return str(fmt).format(elapsed=elapsed_s)
480
592
 
481
593
 
482
- elapsed = CollectorBundle(state=ElapsedState, factory=elapsed_factory, formatter=elapsed_formatter)
594
+ elapsed = CollectorBundle(
595
+ state=ElapsedState,
596
+ factory_cls=ElapsedFactory,
597
+ factory=elapsed_factory,
598
+ formatter=elapsed_formatter,
599
+ )
483
600
  """The elapsed collector bundle.
484
601
 
485
602
  Examples:
@@ -6,20 +6,20 @@ and injects that logger into the callable.
6
6
 
7
7
  import functools
8
8
  from collections.abc import Callable # noqa: TC003
9
- from typing import Any, cast, overload
9
+ from typing import Any, overload
10
10
 
11
11
  import tremors.logger
12
12
 
13
13
 
14
14
  def _logged[TRet, **P](
15
15
  fn: Callable[P, TRet],
16
- *collectors: tremors.logger.Collector[Any],
16
+ *collectors: tremors.logger.Collector[Any] | tremors.logger.CollectorFactory[Any],
17
17
  name: str,
18
18
  logger_name: str | None = None,
19
19
  ) -> Callable[P, TRet]:
20
20
  @functools.wraps(fn)
21
21
  def logged_wrapper(*args: P.args, **kwargs: P.kwargs) -> TRet:
22
- if "logger" in kwargs:
22
+ if (logger_arg := kwargs.get("logger")) and logger_arg is not from_logged:
23
23
  return fn(*args, **kwargs)
24
24
  with tremors.logger.Logger(*collectors, name=name, logger_name=logger_name) as logger:
25
25
  kwargs["logger"] = logger
@@ -30,8 +30,8 @@ def _logged[TRet, **P](
30
30
 
31
31
  @overload
32
32
  def logged[TRet, **P](
33
- *collectors: tremors.logger.Collector[Any],
34
- name: str | None = None,
33
+ *collectors: tremors.logger.Collector[Any] | tremors.logger.CollectorFactory[Any],
34
+ name: str,
35
35
  logger_name: str | None = None,
36
36
  ) -> Callable[[Callable[P, TRet]], Callable[P, TRet]]: ...
37
37
 
@@ -41,8 +41,11 @@ def logged[TRet, **P](fn_or_collector: Callable[P, TRet]) -> Callable[P, TRet]:
41
41
 
42
42
 
43
43
  def logged[TRet, **P](
44
- fn_or_collector: Callable[P, TRet] | tremors.logger.Collector[Any] | None = None,
45
- *collectors: tremors.logger.Collector[Any],
44
+ fn_or_collector: Callable[P, TRet]
45
+ | tremors.logger.Collector[Any]
46
+ | tremors.logger.CollectorFactory[Any]
47
+ | None = None,
48
+ *collectors: tremors.logger.Collector[Any] | tremors.logger.CollectorFactory[Any],
46
49
  name: str | None = None,
47
50
  logger_name: str | None = None,
48
51
  ) -> Callable[P, TRet] | Callable[[Callable[P, TRet]], Callable[P, TRet]]:
@@ -66,7 +69,9 @@ def logged[TRet, **P](
66
69
  logger_name: The name of the underlying logger. If None, the standard
67
70
  root logger is used.
68
71
  """
69
- if callable(fn_or_collector):
72
+ if callable(fn_or_collector) and not isinstance(
73
+ fn_or_collector, tremors.logger.CollectorFactory
74
+ ):
70
75
  default_collectors = () # TODO @NAS: Implement default collectors.
71
76
  return _logged(fn_or_collector, *default_collectors, name=fn_or_collector.__name__)
72
77
 
@@ -91,7 +96,7 @@ def logged[TRet, **P](
91
96
  return decorator
92
97
 
93
98
 
94
- from_logged: tremors.logger.Logger = cast("tremors.logger.Logger", None)
99
+ from_logged = tremors.logger.Logger(name="__from_logged__")
95
100
  """A sentinel logger value.
96
101
 
97
102
  When used as the ``logger`` argument to a :func:`logged` callable, the
@@ -41,6 +41,7 @@ define custom collectors, and bundles.
41
41
 
42
42
  from __future__ import annotations
43
43
 
44
+ import abc
44
45
  import contextvars
45
46
  import functools
46
47
  import itertools
@@ -82,23 +83,38 @@ class Collector[TState](NamedTuple):
82
83
  """The collector specification.
83
84
 
84
85
  Attributes:
86
+ id: A unique identifier for the collector.
85
87
  name: The name of the collector. When the collector runs
86
88
  for a logged message, the collector's state is added to the
87
89
  :class:`~logging.LogRecord`, and may be retrieved using this name.
88
90
  level: The minimum level a logged message must be for the collector
89
91
  to run.
92
+ inherit: If True, the collector will also be added to descendant
93
+ loggers, provided its name does not conflict that of any of the
94
+ other collectors on the descendant loggers.
90
95
  state: The initial state of the collector. Each time the collector
91
96
  runs, it may update this state.
92
97
  collect: When the collector runs, this function is called with the
93
98
  current state, and the LogItem. It returns the new state.
94
99
  """
95
100
 
101
+ id: uuid.UUID
96
102
  name: str
97
103
  level: int
104
+ inherit: bool
98
105
  state: TState
99
106
  collect: Callable[[TState, LogItem], TState]
100
107
 
101
108
 
109
+ class CollectorFactory[TState](metaclass=abc.ABCMeta):
110
+ """A factory that creates a collector."""
111
+
112
+ @abc.abstractmethod
113
+ def __call__(self) -> Collector[TState]:
114
+ """Create a collector."""
115
+ raise NotImplementedError
116
+
117
+
102
118
  def _collectors_reducer[T](
103
119
  acc: tuple[MutableMapping[str, Collector[T]], MutableMapping[str, T]],
104
120
  curr: tuple[LogItem, tuple[str, Collector[T]]],
@@ -110,7 +126,9 @@ def _collectors_reducer[T](
110
126
  acc_extra[name] = curr_c.state
111
127
  return acc_collectors, acc_extra
112
128
  state = curr_c.collect(curr_c.state, log_item)
113
- acc_collectors[name] = Collector(curr_c.name, curr_c.level, state, curr_c.collect)
129
+ acc_collectors[name] = Collector(
130
+ curr_c.id, curr_c.name, curr_c.level, curr_c.inherit, state, curr_c.collect
131
+ )
114
132
  acc_extra[name] = state
115
133
  return acc_collectors, acc_extra
116
134
 
@@ -122,8 +140,9 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
122
140
  """The Tremors logger.
123
141
 
124
142
  Args:
125
- *collectors: Collectors that will be attached to the logger, and will
126
- be run for every logged message for which the collector is enabled.
143
+ *collectors: Collectors or CollectorFactories. The collectors, or
144
+ resultant collectors will be attached to the logger, and will be run
145
+ for every logged message for which the collector is enabled.
127
146
  name: The name of the logger. This name is logged in entering, and
128
147
  exiting messages. This name is used to generate the path, but
129
148
  may be altered in the path to be unique in the hierarchy.
@@ -138,7 +157,7 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
138
157
 
139
158
  def __init__(
140
159
  self,
141
- *collectors: Collector[Any],
160
+ *collectors: Collector[Any] | CollectorFactory[Any],
142
161
  name: str,
143
162
  logger_name: str | None = None,
144
163
  ctx_level: int = logging.INFO,
@@ -147,7 +166,12 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
147
166
  """Initialize the logger."""
148
167
  super().__init__(logging.getLogger(logger_name))
149
168
  self._name = name
150
- self._collectors: Mapping[str, Collector[Any]] = {c.name: c for c in collectors}
169
+ self._collectors: MutableMapping[str, Collector[Any]] = {
170
+ (coll := c_or_fact()).name
171
+ if isinstance(c_or_fact, CollectorFactory)
172
+ else (coll := c_or_fact).name: coll
173
+ for c_or_fact in collectors
174
+ }
151
175
  self._entered = 0
152
176
  self._ctx_level = ctx_level
153
177
  self._cv_token: contextvars.Token[Logger | None] | None = None
@@ -180,6 +204,9 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
180
204
  if self._parent:
181
205
  self._group_id = self._parent.group_id
182
206
  self._path = self._parent.register_path(self)
207
+ for name, collector in self._parent.collectors.items():
208
+ if collector.inherit and name not in self._collectors:
209
+ self._collectors[name] = collector
183
210
  else:
184
211
  self._group_id = uuid.uuid4()
185
212
  self._path = (self._name,)
@@ -211,6 +238,11 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
211
238
  if self._entered == 0 and self._cv_token:
212
239
  _current_logger.reset(self._cv_token)
213
240
 
241
+ @property
242
+ def collectors(self) -> Mapping[str, Collector[Any]]:
243
+ """The logger collectors."""
244
+ return self._collectors
245
+
214
246
  @property
215
247
  def group_id(self) -> uuid.UUID | None:
216
248
  """The group ID for the hierarchy assigned by the root logger."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tremors
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: Tremors is a library for logging while collecting metrics.
5
5
  Author-email: Narvin Singh <Narvin.A.Singh@gmail.com>
6
6
  License: Tremors is a library for logging with metrics.
@@ -32,10 +32,10 @@ Description-Content-Type: text/x-rst
32
32
  License-File: LICENSE
33
33
  Provides-Extra: dev
34
34
  Requires-Dist: coverage~=7.11; extra == "dev"
35
- Requires-Dist: mypy~=1.18; extra == "dev"
35
+ Requires-Dist: mypy~=1.19; extra == "dev"
36
36
  Requires-Dist: pre-commit~=4.3; extra == "dev"
37
37
  Requires-Dist: pytest~=9.0; extra == "dev"
38
- Requires-Dist: ruff~=0.14.2; extra == "dev"
38
+ Requires-Dist: ruff~=0.15.0; extra == "dev"
39
39
  Requires-Dist: sphinx-lint~=1.0; extra == "dev"
40
40
  Requires-Dist: yamllint~=1.37; extra == "dev"
41
41
  Provides-Extra: doc
@@ -233,9 +233,16 @@ In the previous example, a new counter collector was used each time the
233
233
  function is called. Let's reuse the same collector to keep a tally of errors
234
234
  across *all* calls to the function.
235
235
 
236
+ .. note::
237
+
238
+ The counter factory returns a ``CollectorFactory`` that will result in a
239
+ new collector being created for each ``fn`` call. To use the *same*
240
+ collector, we must call the CollectorFactory, hence the trailing
241
+ parentheses on the line where we set ``fn_errors``.
242
+
236
243
  .. code-block:: python
237
244
 
238
- fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)
245
+ fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)()
239
246
 
240
247
 
241
248
  @tremors.logged(fn_errors)
@@ -283,6 +290,47 @@ single logger used in both function calls maintains its state between calls.
283
290
  errors=2 ERROR:root:uh-ho!
284
291
  errors=2 INFO:root:exited: context
285
292
 
293
+ Collectors may be inherited by descendant loggers. Let's count errors across
294
+ nested loggers.
295
+
296
+ .. code-block:: python
297
+
298
+ @tremors.logged(
299
+ collector.counter.factory(
300
+ name="errors", level=logging.ERROR, inherit=True
301
+ )
302
+ )
303
+ def parent(*, logger: tremors.Logger = tremors.from_logged) -> None:
304
+ logger.error("uh-ho!")
305
+ child()
306
+ child()
307
+
308
+
309
+ @tremors.logged
310
+ def child(*, logger: tremors.Logger = tremors.from_logged) -> None:
311
+ logger.error("doh!")
312
+ grandchild()
313
+
314
+
315
+ @tremors.logged
316
+ def grandchild(*, logger: tremors.Logger = tremors.from_logged) -> None:
317
+ logger.info("so far, so good")
318
+ logger.error("spoke too soon!")
319
+
320
+
321
+ parent()
322
+
323
+ The entered and exited lines have been omitted. The ``parent`` counter is used the ``child`` and ``grandchild`` functions.
324
+ .. code-block:: shell
325
+
326
+ errors=1 ERROR:root:uh-ho!
327
+ errors=2 ERROR:root:doh!
328
+ errors=2 INFO:root:so far, so good
329
+ errors=3 ERROR:root:spoke too soon!
330
+ errors=4 ERROR:root:doh!
331
+ errors=4 INFO:root:so far, so good
332
+ errors=5 ERROR:root:spoke too soon!
333
+
286
334
  See the `collector module`_ in the full `documentation`_ for how you can
287
335
  define your own collectors, and bundles.
288
336
 
@@ -5,10 +5,10 @@ twine~=6.2
5
5
 
6
6
  [dev]
7
7
  coverage~=7.11
8
- mypy~=1.18
8
+ mypy~=1.19
9
9
  pre-commit~=4.3
10
10
  pytest~=9.0
11
- ruff~=0.14.2
11
+ ruff~=0.15.0
12
12
  sphinx-lint~=1.0
13
13
  yamllint~=1.37
14
14
 
File without changes
File without changes
File without changes