diff --git a/src/docstub-stubs/_docstrings.pyi b/src/docstub-stubs/_docstrings.pyi index 4fda48d..ea5e0c7 100644 --- a/src/docstub-stubs/_docstrings.pyi +++ b/src/docstub-stubs/_docstrings.pyi @@ -67,7 +67,7 @@ class DoctypeTransformer(lark.visitors.Transformer): self, *, matcher: TypeMatcher | None = ..., **kwargs: dict[Any, Any] ) -> None: ... def doctype_to_annotation( - self, doctype: str + self, doctype: str, *, reporter: ContextReporter | None = ... ) -> tuple[Annotation, list[tuple[str, int, int]]]: ... def qualname(self, tree: lark.Tree) -> lark.Token: ... def rst_role(self, tree: lark.Tree) -> lark.Token: ... diff --git a/src/docstub-stubs/_report.pyi b/src/docstub-stubs/_report.pyi index 2c16253..09b2131 100644 --- a/src/docstub-stubs/_report.pyi +++ b/src/docstub-stubs/_report.pyi @@ -26,15 +26,24 @@ class ContextReporter: line_offset: int | None = ... ) -> Self: ... def report( - self, short: str, *, log_level: int, details: str | None = ..., **log_kw: Any + self, + short: str, + *args: Any, + log_level: int, + details: str | None = ..., + **log_kw: Any ) -> None: ... def debug( - self, short: str, *, details: str | None = ..., **log_kw: Any + self, short: str, *args: Any, details: str | None = ..., **log_kw: Any + ) -> None: ... + def info( + self, short: str, *args: Any, details: str | None = ..., **log_kw: Any + ) -> None: ... + def warn( + self, short: str, *args: Any, details: str | None = ..., **log_kw: Any ) -> None: ... - def info(self, short: str, *, details: str | None = ..., **log_kw: Any) -> None: ... - def warn(self, short: str, *, details: str | None = ..., **log_kw: Any) -> None: ... def error( - self, short: str, *, details: str | None = ..., **log_kw: Any + self, short: str, *args: Any, details: str | None = ..., **log_kw: Any ) -> None: ... def __post_init__(self) -> None: ... @staticmethod diff --git a/src/docstub/_docstrings.py b/src/docstub/_docstrings.py index 20012c7..d4a11a4 100644 --- a/src/docstub/_docstrings.py +++ b/src/docstub/_docstrings.py @@ -266,6 +266,7 @@ def __init__(self, *, matcher=None, **kwargs): self.matcher = matcher + self._reporter = None self._collected_imports = None self._unknown_qualnames = None @@ -276,13 +277,14 @@ def __init__(self, *, matcher=None, **kwargs): "transformed": 0, } - def doctype_to_annotation(self, doctype): + def doctype_to_annotation(self, doctype, *, reporter=None): """Turn a type description in a docstring into a type annotation. Parameters ---------- doctype : str The doctype to parse. + reporter : ~.ContextReporter Returns ------- @@ -293,6 +295,7 @@ def doctype_to_annotation(self, doctype): end index relative to the given `doctype`. """ try: + self._reporter = reporter or ContextReporter(logger=logger) self._collected_imports = set() self._unknown_qualnames = [] tree = _lark.parse(doctype) @@ -310,6 +313,7 @@ def doctype_to_annotation(self, doctype): self.stats["syntax_errors"] += 1 raise finally: + self._reporter = None self._collected_imports = None self._unknown_qualnames = None @@ -394,11 +398,10 @@ def natlang_literal(self, tree): out = f"Literal[{out}]" if len(tree.children) == 1: - logger.warning( - "Natural language literal with one item `%s`, " - "consider using `%s` to improve readability", + self._reporter.warn( + "Natural language literal with one item: `{%s}`", tree.children[0], - out, + details=f"Consider using `{out}` to improve readability", ) if self.matcher is not None: @@ -463,7 +466,7 @@ def shape(self, tree): ------- out : lark.visitors._DiscardType """ - logger.debug("Dropping shape information %r", tree) + # self._reporter.debug("Dropping shape information %r", tree) return lark.Discard def optional_info(self, tree): @@ -476,7 +479,7 @@ def optional_info(self, tree): ------- out : lark.visitors._DiscardType """ - # logger.debug("Dropping optional info %r", tree) + # self._reporter.debug("Dropping optional info %r", tree) return lark.Discard def __default__(self, data, children, meta): @@ -629,7 +632,7 @@ def _doctype_to_annotation(self, doctype, ds_line=0): try: annotation, unknown_qualnames = self.transformer.doctype_to_annotation( - doctype + doctype, reporter=reporter ) reporter.debug( "Transformed doctype", details=(" %s\n-> %s", doctype, annotation) diff --git a/src/docstub/_report.py b/src/docstub/_report.py index e2c377d..04644cb 100644 --- a/src/docstub/_report.py +++ b/src/docstub/_report.py @@ -79,13 +79,15 @@ def copy_with(self, *, logger=None, path=None, line=None, line_offset=None): new = type(self)(**kwargs) return new - def report(self, short, *, log_level, details=None, **log_kw): + def report(self, short, *args, log_level, details=None, **log_kw): """Log a report in context of the saved location. Parameters ---------- short : str A short summarizing report that shouldn't wrap over multiple lines. + *args : Any + Optional formatting arguments for `short`. log_level : int The logging level. details : str, optional @@ -100,59 +102,75 @@ def report(self, short, *, log_level, details=None, **log_kw): location = f"{location}:{self.line}" extra["src_location"] = location - self.logger.log(log_level, msg=short, extra=extra, **log_kw) + self.logger.log(log_level, short, *args, extra=extra, **log_kw) - def debug(self, short, *, details=None, **log_kw): + def debug(self, short, *args, details=None, **log_kw): """Log information with context of the relevant source. Parameters ---------- short : str A short summarizing report that shouldn't wrap over multiple lines. + *args : Any + Optional formatting arguments for `short`. details : str, optional An optional multiline report with more details. **log_kw : Any """ - return self.report(short, log_level=logging.DEBUG, details=details, **log_kw) + return self.report( + short, *args, log_level=logging.DEBUG, details=details, **log_kw + ) - def info(self, short, *, details=None, **log_kw): + def info(self, short, *args, details=None, **log_kw): """Log information with context of the relevant source. Parameters ---------- short : str A short summarizing report that shouldn't wrap over multiple lines. + *args : Any + Optional formatting arguments for `short`. details : str, optional An optional multiline report with more details. **log_kw : Any """ - return self.report(short, log_level=logging.INFO, details=details, **log_kw) + return self.report( + short, *args, log_level=logging.INFO, details=details, **log_kw + ) - def warn(self, short, *, details=None, **log_kw): + def warn(self, short, *args, details=None, **log_kw): """Log a warning with context of the relevant source. Parameters ---------- short : str A short summarizing report that shouldn't wrap over multiple lines. + *args : Any + Optional formatting arguments for `short`. details : str, optional An optional multiline report with more details. **log_kw : Any """ - return self.report(short, log_level=logging.WARNING, details=details, **log_kw) + return self.report( + short, *args, log_level=logging.WARNING, details=details, **log_kw + ) - def error(self, short, *, details=None, **log_kw): + def error(self, short, *args, details=None, **log_kw): """Log an error with context of the relevant source. Parameters ---------- short : str A short summarizing report that shouldn't wrap over multiple lines. + *args : Any + Optional formatting arguments for `short`. details : str, optional An optional multiline report with more details. **log_kw : Any """ - return self.report(short, log_level=logging.ERROR, details=details, **log_kw) + return self.report( + short, *args, log_level=logging.ERROR, details=details, **log_kw + ) def __post_init__(self): if self.path is not None and not isinstance(self.path, Path): diff --git a/tests/test_docstrings.py b/tests/test_docstrings.py index 3c7e988..c988fac 100644 --- a/tests/test_docstrings.py +++ b/tests/test_docstrings.py @@ -1,10 +1,15 @@ +import logging from textwrap import dedent import lark import pytest from docstub._analysis import PyImport -from docstub._docstrings import Annotation, DocstringAnnotations, DoctypeTransformer +from docstub._docstrings import ( + Annotation, + DocstringAnnotations, + DoctypeTransformer, +) class Test_Annotation: @@ -180,6 +185,17 @@ def test_literals(self, doctype, expected): annotation, _ = transformer.doctype_to_annotation(doctype) assert annotation.value == expected + def test_single_natlang_literal_warning(self, caplog): + transformer = DoctypeTransformer() + annotation, _ = transformer.doctype_to_annotation("{True}") + assert annotation.value == "Literal[True]" + assert caplog.messages == ["Natural language literal with one item: `{True}`"] + assert caplog.records[0].levelno == logging.WARNING + assert ( + caplog.records[0].details + == "Consider using `Literal[True]` to improve readability" + ) + @pytest.mark.parametrize( ("doctype", "expected"), [