Skip to content

Commit 2372c14

Browse files
committed
Split docname into KnownImport and replace
Reduce responsibility of the former DocName class. Replacing docstring specific type description should be handled separately.
1 parent aa7b736 commit 2372c14

2 files changed

Lines changed: 63 additions & 42 deletions

File tree

src/docstub/_docstrings.py

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""Transform types defined in docstrings to Python parsable types."""
22

33
import logging
4+
import textwrap
45
from dataclasses import dataclass, field
56
from pathlib import Path
67

8+
import click
79
import lark
810
import lark.visitors
911
from numpydoc.docscrape import NumpyDocString
@@ -33,7 +35,7 @@ def _find_one_token(tree: lark.Tree, *, name: str) -> lark.Token:
3335
return tokens[0]
3436

3537

36-
@dataclass(frozen=True, slots=True)
38+
@dataclass(frozen=True, slots=True, kw_only=True)
3739
class Annotation:
3840
"""Python-ready type annotation with attached import information."""
3941

@@ -148,6 +150,30 @@ def __init__(self, *, inspector, replace_doctypes, **kwargs):
148150
self._collected_imports = None
149151
super().__init__(**kwargs)
150152

153+
def transform(self, doctype):
154+
"""Turn a type description in a docstring into a type annotation.
155+
156+
Parameters
157+
----------
158+
doctype : str
159+
The doctype to parse.
160+
161+
Returns
162+
-------
163+
annotation : Annotation
164+
The parsed annotation.
165+
"""
166+
try:
167+
self._collected_imports = set()
168+
tree = _lark.parse(doctype)
169+
value = super().transform(tree=tree)
170+
annotation = Annotation(
171+
value=value, imports=frozenset(self._collected_imports)
172+
)
173+
return annotation
174+
finally:
175+
self._collected_imports = None
176+
151177
def __default__(self, data, children, meta):
152178
"""Unpack children of rule nodes by default.
153179
@@ -172,30 +198,6 @@ def __default__(self, data, children, meta):
172198
out = children
173199
return out
174200

175-
def transform(self, tree):
176-
"""
177-
178-
Parameters
179-
----------
180-
tree : lark.Tree
181-
The
182-
183-
Returns
184-
-------
185-
annotation : Annotation
186-
The doctype formatted as a stub-file compatible string with
187-
necessary imports attached.
188-
"""
189-
try:
190-
self._collected_imports = set()
191-
value = super().transform(tree=tree)
192-
annotation = Annotation(
193-
value=value, imports=frozenset(self._collected_imports)
194-
)
195-
return annotation
196-
finally:
197-
self._collected_imports = None
198-
199201
def annotation(self, tree):
200202
out = " | ".join(tree.children)
201203
return out
@@ -293,10 +295,20 @@ def _find_import(self, qualname):
293295

294296

295297
class DocstringAnnotations:
296-
def __init__(self, docstring, *, transformer):
298+
def __init__(self, docstring, *, transformer, source=None):
297299
self.docstring = docstring
298300
self.np_docstring = NumpyDocString(docstring)
299301
self.transformer = transformer
302+
self.source = source
303+
304+
def _format_grammar_error(self, error, doctype):
305+
msg = "doctype doesn't conform to grammar"
306+
details = doctype
307+
if hasattr(error, "get_context"):
308+
details = error.get_context(doctype)
309+
details = textwrap.indent(details, prefix=" ")
310+
out = f"{click.style(self.source, bold=True)} {msg}\n{details}"
311+
return out
300312

301313
def _doctype_to_annotation(self, doctype):
302314
"""Convert a type description to a Python-ready type.
@@ -316,14 +328,19 @@ def _doctype_to_annotation(self, doctype):
316328
necessary imports attached.
317329
"""
318330
try:
319-
tree = _lark.parse(doctype)
320-
annotation = self.transformer.transform(tree)
331+
annotation = self.transformer.transform(doctype)
321332
return annotation
322-
except lark.visitors.VisitError as e:
323-
logger.exception("couldn't parse doctype: %r", doctype, exc_info=e.orig_exc)
333+
except (lark.exceptions.LexError, lark.exceptions.ParseError) as error:
334+
msg = self._format_grammar_error(error=error, doctype=doctype)
335+
click.echo(msg)
324336
return ErrorFallbackAnnotation
325-
except Exception:
326-
logger.exception("couldn't parse doctype: %r", doctype)
337+
except lark.visitors.VisitError as e:
338+
logger.exception(
339+
"unexpected error parsing doctype %r in %s, falling back to Any",
340+
doctype,
341+
self.source,
342+
exc_info=e.orig_exc,
343+
)
327344
return ErrorFallbackAnnotation
328345

329346
@property

src/docstub/_stubs.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@ class Py2StubTransformer(cst.CSTTransformer):
191191
inspector : ~._analysis.StaticInspector
192192
"""
193193

194+
METADATA_DEPENDENCIES = (cst.metadata.PositionProvider,)
195+
194196
# Equivalent to ` ...`, to replace the body of callables with
195197
_body_replacement = cst.SimpleStatementSuite(
196198
leading_whitespace=cst.SimpleWhitespace(value=" "),
@@ -240,6 +242,7 @@ def python_to_stub(self, source, *, module_path=None):
240242
self.inspector.current_source = module_path
241243

242244
source_tree = cst.parse_module(source)
245+
source_tree = cst.metadata.MetadataWrapper(source_tree)
243246
stub_tree = source_tree.visit(self)
244247
stub = stub_tree.code
245248
stub = try_format_stub(stub)
@@ -262,7 +265,7 @@ def visit_ClassDef(self, node):
262265
out : Literal[True]
263266
"""
264267
self._scope_stack.append(_Scope(type=FuncType.CLASS, node=node))
265-
pytypes = self._pytypes_from_node(node)
268+
pytypes = self._annotations_from_node(node)
266269
self._pytypes_stack.append(pytypes)
267270
return True
268271

@@ -295,7 +298,7 @@ def visit_FunctionDef(self, node):
295298
"""
296299
func_type = self._function_type(node)
297300
self._scope_stack.append(_Scope(type=func_type, node=node))
298-
pytypes = self._pytypes_from_node(node)
301+
pytypes = self._annotations_from_node(node)
299302
self._pytypes_stack.append(pytypes)
300303
return True
301304

@@ -436,7 +439,7 @@ def visit_Module(self, node):
436439
Literal[True]
437440
"""
438441
self._scope_stack.append(_Scope(type=FuncType.MODULE, node=node))
439-
pytypes = self._pytypes_from_node(node)
442+
pytypes = self._annotations_from_node(node)
440443
self._pytypes_stack.append(pytypes)
441444
return True
442445

@@ -551,7 +554,7 @@ def _function_type(self, func_def):
551554
break
552555
return func_type
553556

554-
def _pytypes_from_node(self, node):
557+
def _annotations_from_node(self, node):
555558
"""Extract types from function, class or module docstrings.
556559
557560
Parameters
@@ -560,20 +563,21 @@ def _pytypes_from_node(self, node):
560563
561564
Returns
562565
-------
563-
pytypes : dict[str, ~._docstrings.PyType]
566+
annotations : DocstringAnnotations
564567
"""
565-
pytypes = None
568+
annotations = None
566569
docstring = node.get_docstring()
567570
if docstring:
571+
position = self.get_metadata(cst.metadata.PositionProvider, node).start
572+
source = f"{self.inspector.current_source}:{position.line}"
568573
try:
569-
pytypes = DocstringAnnotations(
570-
docstring,
571-
transformer=self.transformer,
574+
annotations = DocstringAnnotations(
575+
docstring, transformer=self.transformer, source=source
572576
)
573577
except Exception as e:
574578
logger.exception(
575579
"error while parsing docstring of `%s`:\n\n%s",
576580
node.name.value,
577581
e,
578582
)
579-
return pytypes
583+
return annotations

0 commit comments

Comments
 (0)