8
8
import posixpath
9
9
import sys
10
10
import traceback
11
- from collections import ChainMap
12
11
from collections .abc import Iterator , Mapping , MutableMapping
12
+ from copy import deepcopy
13
13
from pathlib import Path
14
14
from subprocess import PIPE , Popen
15
15
from typing import Any , BinaryIO , ClassVar , Optional
16
16
17
- from markdown import Markdown
18
- from mkdocstrings .extension import PluginError
19
- from mkdocstrings .handlers .base import BaseHandler , CollectionError , CollectorItem
20
- from mkdocstrings .inventory import Inventory
21
- from mkdocstrings .loggers import get_logger
17
+ from mkdocs .config .defaults import MkDocsConfig
18
+ from mkdocs .exceptions import PluginError
19
+ from mkdocstrings import BaseHandler , CollectionError , CollectorItem , Inventory , get_logger
22
20
23
21
from mkdocstrings_handlers .python .rendering import (
24
22
do_brief_xref ,
34
32
35
33
36
34
class PythonHandler (BaseHandler ):
37
- """The Python handler class.
35
+ """The Python handler class."""
38
36
39
- Attributes:
40
- domain: The cross-documentation domain/language for this handler.
41
- enable_inventory: Whether this handler is interested in enabling the creation
42
- of the `objects.inv` Sphinx inventory file.
43
- """
44
-
45
- domain : str = "py" # to match Sphinx's default domain
46
- enable_inventory : bool = True
37
+ name : ClassVar [str ] = "python"
38
+ """The handler name."""
39
+ domain : ClassVar [str ] = "py" # to match Sphinx's default domain
40
+ """The domain of the handler."""
41
+ enable_inventory : ClassVar [bool ] = True
42
+ """Whether the handler supports inventory files."""
47
43
48
- fallback_theme = "material"
44
+ fallback_theme : ClassVar [str ] = "material"
45
+ """The fallback theme to use when the user-selected theme is not supported."""
49
46
fallback_config : ClassVar [dict ] = {"docstring_style" : "markdown" , "filters" : ["!.*" ]}
50
47
"""The configuration used when falling back to re-collecting an object to get its anchor.
51
48
52
- This configuration is used in [`Handlers.get_anchors`][mkdocstrings.handlers.base. Handlers.get_anchors].
49
+ This configuration is used in [`Handlers.get_anchors`][mkdocstrings.Handlers.get_anchors].
53
50
54
51
When trying to fix (optional) cross-references, the autorefs plugin will try to collect
55
52
an object with every configured handler until one succeeds. It will then try to get
@@ -118,14 +115,7 @@ class PythonHandler(BaseHandler):
118
115
- `show_source` (`bool`): Show the source code of this object. Default: `True`.
119
116
"""
120
117
121
- def __init__ (
122
- self ,
123
- * args : Any ,
124
- setup_commands : Optional [List [str ]] = None ,
125
- config_file_path : Optional [str ] = None ,
126
- paths : Optional [List [str ]] = None ,
127
- ** kwargs : Any ,
128
- ) -> None :
118
+ def __init__ (self , config : dict [str , Any ], base_dir : Path , ** kwargs : Any ) -> None :
129
119
"""Initialize the handler.
130
120
131
121
When instantiating a Python handler, we open a `pytkdocs` subprocess in the background with `subprocess.Popen`.
@@ -134,24 +124,27 @@ def __init__(
134
124
too resource intensive, and would slow down `mkdocstrings` a lot.
135
125
136
126
Parameters:
137
- *args: Handler name, theme and custom templates.
138
- setup_commands: A list of python commands as strings to be executed in the subprocess before `pytkdocs`.
139
- config_file_path: The MkDocs configuration file path.
140
- paths: A list of paths to use as search paths.
141
- **kwargs: Same thing, but with keyword arguments.
127
+ config: The handler configuration.
128
+ base_dir: The base directory of the project.
129
+ **kwargs: Arguments passed to the parent constructor.
142
130
"""
131
+ super ().__init__ (** kwargs )
132
+
133
+ self .base_dir = base_dir
134
+ self .config = config
135
+ self .global_options = config .get ("options" , {})
136
+
143
137
logger .debug ("Opening 'pytkdocs' subprocess" )
144
138
env = os .environ .copy ()
145
139
env ["PYTHONUNBUFFERED" ] = "1"
146
140
147
- self ._config_file_path = config_file_path
148
- paths = paths or []
149
- if not paths and config_file_path :
150
- paths .append (os .path .dirname (config_file_path ))
141
+ paths = config .get ("paths" ) or []
142
+ if not paths and self .base_dir :
143
+ paths .append (self .base_dir )
151
144
search_paths = []
152
145
for path in paths :
153
- if not os .path .isabs (path ) and config_file_path :
154
- path = os .path .abspath (os .path .join (os . path . dirname ( config_file_path ) , path )) # noqa: PLW2901
146
+ if not os .path .isabs (path ) and self . base_dir :
147
+ path = os .path .abspath (os .path .join (self . base_dir , path )) # noqa: PLW2901
155
148
if path not in search_paths :
156
149
search_paths .append (path )
157
150
self ._paths = search_paths
@@ -161,7 +154,7 @@ def __init__(
161
154
if search_paths :
162
155
commands .extend ([f"sys.path.insert(0, { path !r} )" for path in reversed (search_paths )])
163
156
164
- if setup_commands :
157
+ if setup_commands := config . get ( "setup_commands" ) :
165
158
# prevent the Python interpreter or the setup commands
166
159
# from writing to stdout as it would break pytkdocs output
167
160
commands .extend (
@@ -193,7 +186,13 @@ def __init__(
193
186
bufsize = - 1 ,
194
187
env = env ,
195
188
)
196
- super ().__init__ (* args , ** kwargs )
189
+
190
+ def get_inventory_urls (self ) -> list [tuple [str , dict [str , Any ]]]:
191
+ """Return the URLs of the inventory files to download."""
192
+ return [
193
+ (inv .pop ("url" ), inv ) if isinstance (inv , dict ) else (inv , {})
194
+ for inv in deepcopy (self .config .get ("import" , []))
195
+ ]
197
196
198
197
@classmethod
199
198
def load_inventory (
@@ -222,7 +221,20 @@ def load_inventory(
222
221
for item in Inventory .parse_sphinx (in_file , domain_filter = ("py" ,)).values ():
223
222
yield item .name , posixpath .join (base_url , item .uri )
224
223
225
- def collect (self , identifier : str , config : MutableMapping [str , Any ]) -> CollectorItem :
224
+ def get_options (self , local_options : Mapping [str , Any ]) -> MutableMapping [str , Any ]:
225
+ """Return the options to use to collect an object.
226
+
227
+ We merge the global options with the options specific to the object being collected.
228
+
229
+ Arguments:
230
+ local_options: The selection options.
231
+
232
+ Returns:
233
+ The options to use to collect an object.
234
+ """
235
+ return {** self .default_config , ** self .global_options , ** local_options }
236
+
237
+ def collect (self , identifier : str , options : MutableMapping [str , Any ]) -> CollectorItem :
226
238
"""Collect the documentation tree given an identifier and selection options.
227
239
228
240
In this method, we feed one line of JSON to the standard input of the subprocess that was opened
@@ -244,23 +256,21 @@ def collect(self, identifier: str, config: MutableMapping[str, Any]) -> Collecto
244
256
245
257
Arguments:
246
258
identifier: The dotted-path of a Python object available in the Python path.
247
- config : Selection options, used to alter the data collection done by `pytkdocs`.
259
+ options : Selection options, used to alter the data collection done by `pytkdocs`.
248
260
249
261
Raises:
250
262
CollectionError: When there was a problem collecting the object documentation.
251
263
252
264
Returns:
253
265
The collected object-tree.
254
266
"""
255
- final_config = {}
267
+ pytkdocs_options = {}
256
268
for option in ("filters" , "members" , "docstring_style" , "docstring_options" ):
257
- if option in config :
258
- final_config [option ] = config [option ]
259
- elif option in self .default_config :
260
- final_config [option ] = self .default_config [option ]
269
+ if option in options :
270
+ pytkdocs_options [option ] = options [option ]
261
271
262
272
logger .debug ("Preparing input" )
263
- json_input = json .dumps ({"objects" : [{"path" : identifier , ** final_config }]})
273
+ json_input = json .dumps ({"objects" : [{"path" : identifier , ** pytkdocs_options }]})
264
274
265
275
logger .debug ("Writing to process' stdin" )
266
276
self .process .stdin .write (json_input + "\n " ) # type: ignore[union-attr]
@@ -302,17 +312,16 @@ def teardown(self) -> None:
302
312
logger .debug ("Tearing process down" )
303
313
self .process .terminate ()
304
314
305
- def render (self , data : CollectorItem , config : Mapping [str , Any ]) -> str : # noqa: D102 (ignore missing docstring)
306
- final_config = ChainMap (config , self .default_config ) # type: ignore[arg-type]
307
-
315
+ def render (self , data : CollectorItem , options : MutableMapping [str , Any ]) -> str :
316
+ """Render the collected data into HTML."""
308
317
template = self .env .get_template (f"{ data ['category' ]} .html" )
309
318
310
319
# Heading level is a "state" variable, that will change at each step
311
320
# of the rendering recursion. Therefore, it's easier to use it as a plain value
312
321
# than as an item in a dictionary.
313
- heading_level = final_config ["heading_level" ]
314
- members_order = final_config ["members_order" ]
322
+ heading_level = options ["heading_level" ]
315
323
324
+ members_order = options ["members_order" ]
316
325
if members_order == "alphabetical" :
317
326
sort_function = sort_key_alphabetical
318
327
elif members_order == "source" :
@@ -323,49 +332,37 @@ def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: # noqa
323
332
sort_object (data , sort_function = sort_function )
324
333
325
334
return template .render (
326
- ** {"config" : final_config , data ["category" ]: data , "heading_level" : heading_level , "root" : True },
335
+ ** {"config" : options , data ["category" ]: data , "heading_level" : heading_level , "root" : True },
327
336
)
328
337
329
- def get_anchors (self , data : CollectorItem ) -> tuple [str , ...]: # noqa: D102 (ignore missing docstring)
338
+ def get_aliases (self , identifier : str ) -> tuple [str , ...]:
339
+ """Return the aliases of an identifier."""
330
340
try :
341
+ data = self .collect (identifier , self .fallback_config )
331
342
return (data ["path" ],)
332
- except KeyError :
343
+ except ( CollectionError , KeyError ) :
333
344
return ()
334
345
335
- def update_env (self , md : Markdown , config : dict ) -> None : # noqa: D102 (ignore missing docstring)
336
- super ().update_env (md , config )
346
+ def update_env (self , config : dict ) -> None : # noqa: ARG002,D102
337
347
self .env .trim_blocks = True
338
348
self .env .lstrip_blocks = True
339
349
self .env .keep_trailing_newline = False
340
350
self .env .filters ["brief_xref" ] = do_brief_xref
341
351
342
352
343
353
def get_handler (
344
- theme : str ,
345
- custom_templates : Optional [str ] = None ,
346
- setup_commands : Optional [List [str ]] = None ,
347
- config_file_path : Optional [str ] = None ,
348
- paths : Optional [List [str ]] = None ,
349
- ** config : Any , # noqa: ARG001
354
+ handler_config : MutableMapping [str , Any ],
355
+ tool_config : MkDocsConfig ,
356
+ ** kwargs : Any ,
350
357
) -> PythonHandler :
351
358
"""Simply return an instance of `PythonHandler`.
352
359
353
360
Arguments:
354
- theme: The theme to use when rendering contents.
355
- custom_templates: Directory containing custom templates.
356
- setup_commands: A list of commands as strings to be executed in the subprocess before `pytkdocs`.
357
- config_file_path: The MkDocs configuration file path.
358
- paths: A list of paths to use as search paths.
359
- config: Configuration passed to the handler.
361
+ handler_config: The handler configuration.
362
+ tool_config: The tool (SSG) configuration.
360
363
361
364
Returns:
362
365
An instance of `PythonHandler`.
363
366
"""
364
- return PythonHandler (
365
- handler = "python" ,
366
- theme = theme ,
367
- custom_templates = custom_templates ,
368
- setup_commands = setup_commands ,
369
- config_file_path = config_file_path ,
370
- paths = paths ,
371
- )
367
+ base_dir = Path (tool_config .config_file_path or "./mkdocs.yml" ).parent
368
+ return PythonHandler (config = dict (handler_config ), base_dir = base_dir , ** kwargs )
0 commit comments