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