1818import inspect
1919import logging
2020import re
21+ from ast import literal_eval
2122from pathlib import Path
23+ from textwrap import dedent
2224from typing import Callable , Optional
2325
2426import pytest
25- from griffe import Class , Docstring , Function , Module , Object
27+ from griffe import Class , Docstring , Function , Module , Object , LinesCollection
2628
2729# noinspection PyProtectedMember
2830from mkdocstrings_handlers .python_xref .crossref import (
2931 _RE_CROSSREF ,
3032 _RE_REL_CROSSREF ,
3133 _RelativeCrossrefProcessor ,
32- substitute_relative_crossrefs ,
34+ substitute_relative_crossrefs , doc_value_offset_to_location ,
3335)
3436
3537def test_RelativeCrossrefProcessor (caplog : pytest .LogCaptureFixture ) -> None :
@@ -153,6 +155,7 @@ def test_substitute_relative_crossrefs(caplog: pytest.LogCaptureFixture) -> None
153155 """ ,
154156 parent = meth1 ,
155157 lineno = 42 ,
158+ endlineno = 45 ,
156159 )
157160
158161 mod1 .docstring = Docstring (
@@ -161,6 +164,7 @@ def test_substitute_relative_crossrefs(caplog: pytest.LogCaptureFixture) -> None
161164 """ ,
162165 parent = mod1 ,
163166 lineno = 23 ,
167+ endlineno = 25 ,
164168 )
165169
166170 substitute_relative_crossrefs (mod1 )
@@ -173,3 +177,88 @@ def test_substitute_relative_crossrefs(caplog: pytest.LogCaptureFixture) -> None
173177 )
174178
175179 assert len (caplog .records ) == 0
180+
181+ def make_docstring_from_source (
182+ source : str ,
183+ * ,
184+ lineno : int = 1 ,
185+ mod_name : str = "mod" ,
186+ mod_dir : Path = Path ("" ),
187+ ) -> Docstring :
188+ """
189+ Create a docstring object from source code.
190+
191+ Args:
192+ source: raw source code containing docstring source lines
193+ lineno: line number of docstring starting quotes
194+ mod_name: name of module
195+ mod_dir: module directory
196+ """
197+ filepath = mod_dir .joinpath (mod_name ).with_suffix (".py" )
198+ parent = Object ("" , lines_collection = LinesCollection ())
199+ mod = Module (name = mod_name , filepath = filepath , parent = parent )
200+ lines = source .splitlines (keepends = False )
201+ if lineno > 1 :
202+ # Insert empty lines to pad to the desired line number
203+ lines = ["" ] * (lineno - 1 ) + lines
204+ mod .lines_collection [filepath ] = lines
205+ doc = Docstring (
206+ parent = mod ,
207+ value = inspect .cleandoc (literal_eval (source )),
208+ lineno = lineno ,
209+ endlineno = len (lines )
210+ )
211+ return doc
212+
213+ def test_doc_value_offset_to_location () -> None :
214+ """Unit test for _doc_value_offset_to_location."""
215+ doc1 = make_docstring_from_source (
216+ dedent (
217+ '''
218+ """first
219+ second
220+ third
221+ """
222+ '''
223+ ).lstrip ("\n " ),
224+ )
225+
226+ assert doc_value_offset_to_location (doc1 , 0 ) == (1 , 3 )
227+ assert doc_value_offset_to_location (doc1 , 3 ) == (1 , 6 )
228+ assert doc_value_offset_to_location (doc1 , 7 ) == (2 , 1 )
229+ assert doc_value_offset_to_location (doc1 , 15 ) == (3 , 2 )
230+
231+ doc2 = make_docstring_from_source (
232+ dedent (
233+ '''
234+ """ first
235+ second
236+ third
237+ """ # a comment
238+ # another comment
239+ '''
240+ ).lstrip ("\n " ),
241+ lineno = 3 ,
242+ )
243+
244+ assert doc_value_offset_to_location (doc2 , 0 ) == (3 , 9 )
245+ assert doc_value_offset_to_location (doc2 , 6 ) == (4 , 6 )
246+ assert doc_value_offset_to_location (doc2 , 15 ) == (5 , 8 )
247+
248+ # Remove parent so that source is not available
249+ doc2 .parent = None
250+ assert doc_value_offset_to_location (doc2 , 0 ) == (3 , - 1 )
251+
252+ doc3 = make_docstring_from_source (
253+ dedent (
254+ """
255+ '''
256+ first
257+ second
258+ '''
259+ """
260+ ).lstrip ("\n " ),
261+ )
262+
263+ assert doc_value_offset_to_location (doc3 , 0 ) == (2 , 4 )
264+ assert doc_value_offset_to_location (doc3 , 6 ) == (3 , 2 )
0 commit comments