88import posixpath
99import sys
1010import traceback
11- from collections import ChainMap
1211from collections .abc import Iterator , Mapping , MutableMapping
12+ from copy import deepcopy
1313from pathlib import Path
1414from subprocess import PIPE , Popen
1515from typing import Any , BinaryIO , ClassVar , Optional
1616
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
2220
2321from mkdocstrings_handlers .python .rendering import (
2422 do_brief_xref ,
3432
3533
3634class PythonHandler (BaseHandler ):
37- """The Python handler class.
35+ """The Python handler class."""
3836
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."""
4743
48- fallback_theme = "material"
44+ fallback_theme : ClassVar [str ] = "material"
45+ """The fallback theme to use when the user-selected theme is not supported."""
4946 fallback_config : ClassVar [dict ] = {"docstring_style" : "markdown" , "filters" : ["!.*" ]}
5047 """The configuration used when falling back to re-collecting an object to get its anchor.
5148
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].
5350
5451 When trying to fix (optional) cross-references, the autorefs plugin will try to collect
5552 an object with every configured handler until one succeeds. It will then try to get
@@ -118,14 +115,7 @@ class PythonHandler(BaseHandler):
118115 - `show_source` (`bool`): Show the source code of this object. Default: `True`.
119116 """
120117
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 :
129119 """Initialize the handler.
130120
131121 When instantiating a Python handler, we open a `pytkdocs` subprocess in the background with `subprocess.Popen`.
@@ -134,24 +124,27 @@ def __init__(
134124 too resource intensive, and would slow down `mkdocstrings` a lot.
135125
136126 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.
142130 """
131+ super ().__init__ (** kwargs )
132+
133+ self .base_dir = base_dir
134+ self .config = config
135+ self .global_options = config .get ("options" , {})
136+
143137 logger .debug ("Opening 'pytkdocs' subprocess" )
144138 env = os .environ .copy ()
145139 env ["PYTHONUNBUFFERED" ] = "1"
146140
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 )
151144 search_paths = []
152145 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
155148 if path not in search_paths :
156149 search_paths .append (path )
157150 self ._paths = search_paths
@@ -161,7 +154,7 @@ def __init__(
161154 if search_paths :
162155 commands .extend ([f"sys.path.insert(0, { path !r} )" for path in reversed (search_paths )])
163156
164- if setup_commands :
157+ if setup_commands := config . get ( "setup_commands" ) :
165158 # prevent the Python interpreter or the setup commands
166159 # from writing to stdout as it would break pytkdocs output
167160 commands .extend (
@@ -193,7 +186,13 @@ def __init__(
193186 bufsize = - 1 ,
194187 env = env ,
195188 )
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+ ]
197196
198197 @classmethod
199198 def load_inventory (
@@ -222,7 +221,20 @@ def load_inventory(
222221 for item in Inventory .parse_sphinx (in_file , domain_filter = ("py" ,)).values ():
223222 yield item .name , posixpath .join (base_url , item .uri )
224223
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 :
226238 """Collect the documentation tree given an identifier and selection options.
227239
228240 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
244256
245257 Arguments:
246258 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`.
248260
249261 Raises:
250262 CollectionError: When there was a problem collecting the object documentation.
251263
252264 Returns:
253265 The collected object-tree.
254266 """
255- final_config = {}
267+ pytkdocs_options = {}
256268 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 ]
261271
262272 logger .debug ("Preparing input" )
263- json_input = json .dumps ({"objects" : [{"path" : identifier , ** final_config }]})
273+ json_input = json .dumps ({"objects" : [{"path" : identifier , ** pytkdocs_options }]})
264274
265275 logger .debug ("Writing to process' stdin" )
266276 self .process .stdin .write (json_input + "\n " ) # type: ignore[union-attr]
@@ -302,17 +312,16 @@ def teardown(self) -> None:
302312 logger .debug ("Tearing process down" )
303313 self .process .terminate ()
304314
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."""
308317 template = self .env .get_template (f"{ data ['category' ]} .html" )
309318
310319 # Heading level is a "state" variable, that will change at each step
311320 # of the rendering recursion. Therefore, it's easier to use it as a plain value
312321 # 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" ]
315323
324+ members_order = options ["members_order" ]
316325 if members_order == "alphabetical" :
317326 sort_function = sort_key_alphabetical
318327 elif members_order == "source" :
@@ -323,49 +332,37 @@ def render(self, data: CollectorItem, config: Mapping[str, Any]) -> str: # noqa
323332 sort_object (data , sort_function = sort_function )
324333
325334 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 },
327336 )
328337
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."""
330340 try :
341+ data = self .collect (identifier , self .fallback_config )
331342 return (data ["path" ],)
332- except KeyError :
343+ except ( CollectionError , KeyError ) :
333344 return ()
334345
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
337347 self .env .trim_blocks = True
338348 self .env .lstrip_blocks = True
339349 self .env .keep_trailing_newline = False
340350 self .env .filters ["brief_xref" ] = do_brief_xref
341351
342352
343353def 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 ,
350357) -> PythonHandler :
351358 """Simply return an instance of `PythonHandler`.
352359
353360 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.
360363
361364 Returns:
362365 An instance of `PythonHandler`.
363366 """
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