11import collections
2+ import dataclasses
3+ import functools
24import json
35import logging
6+ import os
47import re
58import shlex
69import subprocess
7- from typing import Any , Callable , Iterable , Iterator , Mapping , Sequence , TypeVar , Union
10+ from typing import Any , Callable , Iterable , Iterator , List , Mapping , Sequence , TypeVar , Union
811
912import mkdocs .exceptions
1013import mkdocs .utils
1114from cached_property import cached_property
1215from mkdocstrings .handlers .base import BaseCollector , CollectionError
1316
14- from .items import DocConstant , DocItem , DocMapping , DocMethod , DocModule , DocType
17+ from .items import DocConstant , DocItem , DocLocation , DocMapping , DocMethod , DocModule , DocType
1518
1619try :
1720 from mkdocs .exceptions import PluginError
2528
2629
2730class CrystalCollector (BaseCollector ):
28- def __init__ (self , crystal_docs_flags : Sequence [str ] = ()):
31+ def __init__ (
32+ self , crystal_docs_flags : Sequence [str ] = (), source_locations : Mapping [str , str ] = ()
33+ ):
2934 """Create a "collector", reading docs from `crystal doc` in the current directory.
3035
3136 Normally this should not be instantiated.
@@ -40,23 +45,35 @@ def __init__(self, crystal_docs_flags: Sequence[str] = ()):
4045 "--format=json" ,
4146 "--project-name=" ,
4247 "--project-version=" ,
43- "--source-refname=master" ,
44- * crystal_docs_flags ,
4548 ]
49+ if source_locations :
50+ command .append ("--source-refname=master" )
51+ command += crystal_docs_flags
4652 log .debug ("Running `%s`" , " " .join (shlex .quote (arg ) for arg in command ))
4753
4854 self ._proc = subprocess .Popen (command , stdout = subprocess .PIPE )
4955 self ._collected = collections .Counter ()
5056
57+ # For unambiguous prefix match: add trailing slash, sort by longest path first.
58+ self ._source_locations = sorted (
59+ (
60+ _SourceDestination (os .path .relpath (k ) + os .sep , source_locations [k ])
61+ for k in source_locations
62+ ),
63+ key = lambda d : - d .src_path .count ("/" ),
64+ )
65+
5166 # pytype: disable=bad-return-type
5267 @cached_property
53- def root (self ) -> DocModule :
68+ def root (self ) -> "DocRoot" :
5469 """The top-level namespace, represented as a fake module."""
5570 try :
5671 with self ._proc :
5772 data = json .load (self ._proc .stdout )
5873 data ["program" ]["full_name" ] = ""
59- return DocModule (data ["program" ], None , None )
74+ root = DocRoot (data ["program" ], None , None )
75+ root .source_locations = self ._source_locations
76+ return root
6077 finally :
6178 if self ._proc .returncode :
6279 cmd = " " .join (shlex .quote (arg ) for arg in self ._proc .args )
@@ -104,6 +121,56 @@ def teardown(self):
104121 log .debug (f"These types were never put into the documentation: { not_collected } " )
105122
106123
124+ @dataclasses .dataclass
125+ class _SourceDestination :
126+ src_path : str
127+ dest_url : str
128+
129+ def substitute (self , location : DocLocation ) -> str :
130+ data = {"file" : location .filename [len (self .src_path ) :], "line" : location .line }
131+ try :
132+ return self .dest_url .format_map (collections .ChainMap (data , self )) # type: ignore
133+ except KeyError as e :
134+ raise PluginError (
135+ f"The source_locations template { self .dest_url !r} did not resolve correctly: { e } "
136+ )
137+
138+ def __getitem__ (self , key ):
139+ try :
140+ return getattr (self , key )
141+ except AttributeError as e :
142+ raise KeyError (str (e ))
143+
144+ @property
145+ def shard_version (self ):
146+ return self ._shard_version (os .path .dirname (self .src_path ))
147+
148+ @classmethod
149+ @functools .lru_cache (maxsize = None )
150+ def _shard_version (cls , path : str ):
151+ file_path = os .path .join (path , "shard.yml" )
152+ if os .path .isfile (file_path ):
153+ with open (file_path , "rb" ) as f :
154+ m = re .search (rb"^version: *([\S+]+)" , f .read (), flags = re .MULTILINE )
155+ if not m :
156+ raise PluginError (f"`version:` not found in { file_path !r} " )
157+ return m [1 ].decode ()
158+ if not path :
159+ raise PluginError (f"'shard.yml' not found anywhere above { path !r} " )
160+ return cls ._shard_version (os .path .dirname (path ))
161+
162+
163+ class DocRoot (DocModule ):
164+ source_locations : List [_SourceDestination ]
165+
166+ def update_url (self , location : DocLocation ) -> DocLocation :
167+ for dest in self .source_locations :
168+ if (location .filename or "" ).startswith (dest .src_path ):
169+ location .url = dest .substitute (location )
170+ break
171+ return location
172+
173+
107174class DocView :
108175 def __init__ (self , item : DocItem , config : Mapping [str , Any ]):
109176 self .item = item
0 commit comments