Skip to content

Commit 7b85beb

Browse files
authored
Add context-aware logging in DoctypeTransformer (#108)
* Add context-aware logging in DoctypeTransformer * Revert consolidating imports in test This could have reduced the noise when refactoring stuff [1] but the value seems dubious and mypy rightly complains here about `_docstrings` not exporting `PyImport`. [1] https://learn.scientific-python.org/development/principles/testing/#importing-in-test-files
1 parent 0707ef7 commit 7b85beb

File tree

5 files changed

+71
-25
lines changed

5 files changed

+71
-25
lines changed

src/docstub-stubs/_docstrings.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class DoctypeTransformer(lark.visitors.Transformer):
6767
self, *, matcher: TypeMatcher | None = ..., **kwargs: dict[Any, Any]
6868
) -> None: ...
6969
def doctype_to_annotation(
70-
self, doctype: str
70+
self, doctype: str, *, reporter: ContextReporter | None = ...
7171
) -> tuple[Annotation, list[tuple[str, int, int]]]: ...
7272
def qualname(self, tree: lark.Tree) -> lark.Token: ...
7373
def rst_role(self, tree: lark.Tree) -> lark.Token: ...

src/docstub-stubs/_report.pyi

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,24 @@ class ContextReporter:
2626
line_offset: int | None = ...
2727
) -> Self: ...
2828
def report(
29-
self, short: str, *, log_level: int, details: str | None = ..., **log_kw: Any
29+
self,
30+
short: str,
31+
*args: Any,
32+
log_level: int,
33+
details: str | None = ...,
34+
**log_kw: Any
3035
) -> None: ...
3136
def debug(
32-
self, short: str, *, details: str | None = ..., **log_kw: Any
37+
self, short: str, *args: Any, details: str | None = ..., **log_kw: Any
38+
) -> None: ...
39+
def info(
40+
self, short: str, *args: Any, details: str | None = ..., **log_kw: Any
41+
) -> None: ...
42+
def warn(
43+
self, short: str, *args: Any, details: str | None = ..., **log_kw: Any
3344
) -> None: ...
34-
def info(self, short: str, *, details: str | None = ..., **log_kw: Any) -> None: ...
35-
def warn(self, short: str, *, details: str | None = ..., **log_kw: Any) -> None: ...
3645
def error(
37-
self, short: str, *, details: str | None = ..., **log_kw: Any
46+
self, short: str, *args: Any, details: str | None = ..., **log_kw: Any
3847
) -> None: ...
3948
def __post_init__(self) -> None: ...
4049
@staticmethod

src/docstub/_docstrings.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ def __init__(self, *, matcher=None, **kwargs):
266266

267267
self.matcher = matcher
268268

269+
self._reporter = None
269270
self._collected_imports = None
270271
self._unknown_qualnames = None
271272

@@ -276,13 +277,14 @@ def __init__(self, *, matcher=None, **kwargs):
276277
"transformed": 0,
277278
}
278279

279-
def doctype_to_annotation(self, doctype):
280+
def doctype_to_annotation(self, doctype, *, reporter=None):
280281
"""Turn a type description in a docstring into a type annotation.
281282
282283
Parameters
283284
----------
284285
doctype : str
285286
The doctype to parse.
287+
reporter : ~.ContextReporter
286288
287289
Returns
288290
-------
@@ -293,6 +295,7 @@ def doctype_to_annotation(self, doctype):
293295
end index relative to the given `doctype`.
294296
"""
295297
try:
298+
self._reporter = reporter or ContextReporter(logger=logger)
296299
self._collected_imports = set()
297300
self._unknown_qualnames = []
298301
tree = _lark.parse(doctype)
@@ -310,6 +313,7 @@ def doctype_to_annotation(self, doctype):
310313
self.stats["syntax_errors"] += 1
311314
raise
312315
finally:
316+
self._reporter = None
313317
self._collected_imports = None
314318
self._unknown_qualnames = None
315319

@@ -394,11 +398,10 @@ def natlang_literal(self, tree):
394398
out = f"Literal[{out}]"
395399

396400
if len(tree.children) == 1:
397-
logger.warning(
398-
"Natural language literal with one item `%s`, "
399-
"consider using `%s` to improve readability",
401+
self._reporter.warn(
402+
"Natural language literal with one item: `{%s}`",
400403
tree.children[0],
401-
out,
404+
details=f"Consider using `{out}` to improve readability",
402405
)
403406

404407
if self.matcher is not None:
@@ -463,7 +466,7 @@ def shape(self, tree):
463466
-------
464467
out : lark.visitors._DiscardType
465468
"""
466-
logger.debug("Dropping shape information %r", tree)
469+
# self._reporter.debug("Dropping shape information %r", tree)
467470
return lark.Discard
468471

469472
def optional_info(self, tree):
@@ -476,7 +479,7 @@ def optional_info(self, tree):
476479
-------
477480
out : lark.visitors._DiscardType
478481
"""
479-
# logger.debug("Dropping optional info %r", tree)
482+
# self._reporter.debug("Dropping optional info %r", tree)
480483
return lark.Discard
481484

482485
def __default__(self, data, children, meta):
@@ -629,7 +632,7 @@ def _doctype_to_annotation(self, doctype, ds_line=0):
629632

630633
try:
631634
annotation, unknown_qualnames = self.transformer.doctype_to_annotation(
632-
doctype
635+
doctype, reporter=reporter
633636
)
634637
reporter.debug(
635638
"Transformed doctype", details=(" %s\n-> %s", doctype, annotation)

src/docstub/_report.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ def copy_with(self, *, logger=None, path=None, line=None, line_offset=None):
7979
new = type(self)(**kwargs)
8080
return new
8181

82-
def report(self, short, *, log_level, details=None, **log_kw):
82+
def report(self, short, *args, log_level, details=None, **log_kw):
8383
"""Log a report in context of the saved location.
8484
8585
Parameters
8686
----------
8787
short : str
8888
A short summarizing report that shouldn't wrap over multiple lines.
89+
*args : Any
90+
Optional formatting arguments for `short`.
8991
log_level : int
9092
The logging level.
9193
details : str, optional
@@ -100,59 +102,75 @@ def report(self, short, *, log_level, details=None, **log_kw):
100102
location = f"{location}:{self.line}"
101103
extra["src_location"] = location
102104

103-
self.logger.log(log_level, msg=short, extra=extra, **log_kw)
105+
self.logger.log(log_level, short, *args, extra=extra, **log_kw)
104106

105-
def debug(self, short, *, details=None, **log_kw):
107+
def debug(self, short, *args, details=None, **log_kw):
106108
"""Log information with context of the relevant source.
107109
108110
Parameters
109111
----------
110112
short : str
111113
A short summarizing report that shouldn't wrap over multiple lines.
114+
*args : Any
115+
Optional formatting arguments for `short`.
112116
details : str, optional
113117
An optional multiline report with more details.
114118
**log_kw : Any
115119
"""
116-
return self.report(short, log_level=logging.DEBUG, details=details, **log_kw)
120+
return self.report(
121+
short, *args, log_level=logging.DEBUG, details=details, **log_kw
122+
)
117123

118-
def info(self, short, *, details=None, **log_kw):
124+
def info(self, short, *args, details=None, **log_kw):
119125
"""Log information with context of the relevant source.
120126
121127
Parameters
122128
----------
123129
short : str
124130
A short summarizing report that shouldn't wrap over multiple lines.
131+
*args : Any
132+
Optional formatting arguments for `short`.
125133
details : str, optional
126134
An optional multiline report with more details.
127135
**log_kw : Any
128136
"""
129-
return self.report(short, log_level=logging.INFO, details=details, **log_kw)
137+
return self.report(
138+
short, *args, log_level=logging.INFO, details=details, **log_kw
139+
)
130140

131-
def warn(self, short, *, details=None, **log_kw):
141+
def warn(self, short, *args, details=None, **log_kw):
132142
"""Log a warning with context of the relevant source.
133143
134144
Parameters
135145
----------
136146
short : str
137147
A short summarizing report that shouldn't wrap over multiple lines.
148+
*args : Any
149+
Optional formatting arguments for `short`.
138150
details : str, optional
139151
An optional multiline report with more details.
140152
**log_kw : Any
141153
"""
142-
return self.report(short, log_level=logging.WARNING, details=details, **log_kw)
154+
return self.report(
155+
short, *args, log_level=logging.WARNING, details=details, **log_kw
156+
)
143157

144-
def error(self, short, *, details=None, **log_kw):
158+
def error(self, short, *args, details=None, **log_kw):
145159
"""Log an error with context of the relevant source.
146160
147161
Parameters
148162
----------
149163
short : str
150164
A short summarizing report that shouldn't wrap over multiple lines.
165+
*args : Any
166+
Optional formatting arguments for `short`.
151167
details : str, optional
152168
An optional multiline report with more details.
153169
**log_kw : Any
154170
"""
155-
return self.report(short, log_level=logging.ERROR, details=details, **log_kw)
171+
return self.report(
172+
short, *args, log_level=logging.ERROR, details=details, **log_kw
173+
)
156174

157175
def __post_init__(self):
158176
if self.path is not None and not isinstance(self.path, Path):

tests/test_docstrings.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import logging
12
from textwrap import dedent
23

34
import lark
45
import pytest
56

67
from docstub._analysis import PyImport
7-
from docstub._docstrings import Annotation, DocstringAnnotations, DoctypeTransformer
8+
from docstub._docstrings import (
9+
Annotation,
10+
DocstringAnnotations,
11+
DoctypeTransformer,
12+
)
813

914

1015
class Test_Annotation:
@@ -180,6 +185,17 @@ def test_literals(self, doctype, expected):
180185
annotation, _ = transformer.doctype_to_annotation(doctype)
181186
assert annotation.value == expected
182187

188+
def test_single_natlang_literal_warning(self, caplog):
189+
transformer = DoctypeTransformer()
190+
annotation, _ = transformer.doctype_to_annotation("{True}")
191+
assert annotation.value == "Literal[True]"
192+
assert caplog.messages == ["Natural language literal with one item: `{True}`"]
193+
assert caplog.records[0].levelno == logging.WARNING
194+
assert (
195+
caplog.records[0].details
196+
== "Consider using `Literal[True]` to improve readability"
197+
)
198+
183199
@pytest.mark.parametrize(
184200
("doctype", "expected"),
185201
[

0 commit comments

Comments
 (0)