11import collections
22import copy
3+ import fnmatch
34import operator
45import os
56import re
67
8+ from jinja2 import Environment , FileSystemLoader
9+ import sphinx
710import sphinx .environment
811from sphinx .errors import ExtensionError
912import sphinx .util
13+ import sphinx .util .logging
1014from sphinx .util .console import colorize
1115from sphinx .util .display import status_iterator
1216import sphinx .util .docstrings
13- import sphinx .util .logging
17+ from sphinx .util .osutil import ensuredir
1418
15- from ..base import SphinxMapperBase
16- from .parser import Parser
17- from .objects import (
19+ from ._parser import Parser
20+ from ._objects import (
1821 PythonClass ,
1922 PythonFunction ,
2023 PythonModule ,
2629 PythonException ,
2730 TopLevelPythonPythonMapper ,
2831)
32+ from .settings import OWN_PAGE_LEVELS , TEMPLATE_DIR
2933
3034LOGGER = sphinx .util .logging .getLogger (__name__ )
3135
@@ -219,13 +223,11 @@ def _link_objs(value):
219223 return result [:- 2 ]
220224
221225
222- class PythonSphinxMapper (SphinxMapperBase ):
223- """AutoAPI domain handler for Python
224-
225- Parses directly from Python files.
226+ class Mapper :
227+ """Base class for mapping `PythonMapperBase` objects to Sphinx.
226228
227229 Args:
228- app: Sphinx application passed in as part of the extension
230+ app: Sphinx application instance
229231 """
230232
231233 _OBJ_MAP = {
@@ -244,13 +246,142 @@ class PythonSphinxMapper(SphinxMapperBase):
244246 }
245247
246248 def __init__ (self , app , template_dir = None , dir_root = None , url_root = None ):
247- super ().__init__ (app , template_dir , dir_root , url_root )
249+ self .app = app
250+
251+ template_paths = [TEMPLATE_DIR ]
252+
253+ if template_dir :
254+ # Put at the front so it's loaded first
255+ template_paths .insert (0 , template_dir )
256+
257+ self .jinja_env = Environment (
258+ loader = FileSystemLoader (template_paths ),
259+ trim_blocks = True ,
260+ lstrip_blocks = True ,
261+ )
262+
263+ def _wrapped_prepare (value ):
264+ return value
265+
266+ self .jinja_env .filters ["prepare_docstring" ] = _wrapped_prepare
267+ if self .app .config .autoapi_prepare_jinja_env :
268+ self .app .config .autoapi_prepare_jinja_env (self .jinja_env )
269+
270+ own_page_level = self .app .config .autoapi_own_page_level
271+ desired_page_level = OWN_PAGE_LEVELS .index (own_page_level )
272+ self .own_page_types = set (OWN_PAGE_LEVELS [: desired_page_level + 1 ])
273+
274+ self .dir_root = dir_root
275+ self .url_root = url_root
276+
277+ # Mapping of {filepath -> raw data}
278+ self .paths = collections .OrderedDict ()
279+ # Mapping of {object id -> Python Object}
280+ self .objects_to_render = collections .OrderedDict ()
281+ # Mapping of {object id -> Python Object}
282+ self .all_objects = collections .OrderedDict ()
283+ # Mapping of {namespace id -> Python Object}
284+ self .namespaces = collections .OrderedDict ()
248285
249286 self .jinja_env .filters ["link_objs" ] = _link_objs
250287 self ._use_implicit_namespace = (
251288 self .app .config .autoapi_python_use_implicit_namespaces
252289 )
253290
291+ @staticmethod
292+ def find_files (patterns , dirs , ignore ):
293+ if not ignore :
294+ ignore = []
295+
296+ pattern_regexes = []
297+ for pattern in patterns :
298+ regex = re .compile (fnmatch .translate (pattern ).replace (".*" , "(.*)" ))
299+ pattern_regexes .append ((pattern , regex ))
300+
301+ for _dir in dirs :
302+ for root , _ , filenames in os .walk (_dir ):
303+ seen = set ()
304+ for pattern , pattern_re in pattern_regexes :
305+ for filename in fnmatch .filter (filenames , pattern ):
306+ skip = False
307+
308+ match = re .match (pattern_re , filename )
309+ norm_name = match .groups ()
310+ if norm_name in seen :
311+ continue
312+
313+ # Skip ignored files
314+ for ignore_pattern in ignore :
315+ if fnmatch .fnmatch (
316+ os .path .join (root , filename ), ignore_pattern
317+ ):
318+ LOGGER .info (
319+ colorize ("bold" , "[AutoAPI] " )
320+ + colorize (
321+ "darkgreen" , f"Ignoring { root } /{ filename } "
322+ )
323+ )
324+ skip = True
325+
326+ if skip :
327+ continue
328+
329+ # Make sure the path is full
330+ if not os .path .isabs (filename ):
331+ filename = os .path .join (root , filename )
332+
333+ yield filename
334+ seen .add (norm_name )
335+
336+ def add_object (self , obj ):
337+ """Add object to local and app environment storage
338+
339+ Args:
340+ obj: Instance of a AutoAPI object
341+ """
342+ display = obj .display
343+ if display and obj .type in self .own_page_types :
344+ self .objects_to_render [obj .id ] = obj
345+
346+ self .all_objects [obj .id ] = obj
347+ child_stack = list (obj .children )
348+ while child_stack :
349+ child = child_stack .pop ()
350+ self .all_objects [child .id ] = child
351+ if display and child .type in self .own_page_types :
352+ self .objects_to_render [child .id ] = child
353+ child_stack .extend (getattr (child , "children" , ()))
354+
355+ def output_rst (self , source_suffix ):
356+ for _ , obj in status_iterator (
357+ self .objects_to_render .items (),
358+ colorize ("bold" , "[AutoAPI] " ) + "Rendering Data... " ,
359+ length = len (self .objects_to_render ),
360+ verbosity = 1 ,
361+ stringify_func = (lambda x : x [0 ]),
362+ ):
363+ rst = obj .render (is_own_page = True )
364+ if not rst :
365+ continue
366+
367+ output_dir = obj .output_dir (self .dir_root )
368+ ensuredir (output_dir )
369+ output_path = output_dir / obj .output_filename ()
370+ path = f"{ output_path } { source_suffix } "
371+ with open (path , "wb+" ) as detail_file :
372+ detail_file .write (rst .encode ("utf-8" ))
373+
374+ if self .app .config .autoapi_add_toctree_entry :
375+ self ._output_top_rst ()
376+
377+ def _output_top_rst (self ):
378+ # Render Top Index
379+ top_level_index = os .path .join (self .dir_root , "index.rst" )
380+ pages = [obj for obj in self .objects_to_render .values () if obj .display ]
381+ with open (top_level_index , "wb" ) as top_level_file :
382+ content = self .jinja_env .get_template ("index.rst" )
383+ top_level_file .write (content .render (pages = pages ).encode ("utf-8" ))
384+
254385 def _need_to_load (self , files ):
255386 last_files = getattr (self .app .env , "autoapi_source_files" , [])
256387 self .app .env .autoapi_source_files = files
@@ -361,7 +492,14 @@ def map(self, options=None):
361492 self ._hide_yo_kids ()
362493 self .app .env .autoapi_annotations = {}
363494
364- super ().map (options )
495+ for _ , data in status_iterator (
496+ self .paths .items (),
497+ colorize ("bold" , "[AutoAPI] " ) + "Mapping Data... " ,
498+ length = len (self .paths ),
499+ stringify_func = (lambda x : x [0 ]),
500+ ):
501+ for obj in self .create_class (data , options = options ):
502+ self .add_object (obj )
365503
366504 top_level_objects = {
367505 obj .id : obj
@@ -383,7 +521,7 @@ def map(self, options=None):
383521 self .app .env .autoapi_objects = self .objects_to_render
384522 self .app .env .autoapi_all_objects = self .all_objects
385523
386- def create_class (self , data , options = None , ** kwargs ):
524+ def create_class (self , data , options = None ):
387525 """Create a class from the passed in data
388526
389527 Args:
@@ -402,13 +540,10 @@ def create_class(self, data, options=None, **kwargs):
402540 jinja_env = self .jinja_env ,
403541 app = self .app ,
404542 url_root = self .url_root ,
405- ** kwargs ,
406543 )
407544
408545 for child_data in data .get ("children" , []):
409- for child_obj in self .create_class (
410- child_data , options = options , ** kwargs
411- ):
546+ for child_obj in self .create_class (child_data , options = options ):
412547 obj .children .append (child_obj )
413548
414549 # Some objects require children to establish their docstring
0 commit comments