From d1b61b7ac1ff83c612948137c833d6f8d4470e85 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Thu, 21 Apr 2022 15:23:53 -0700 Subject: [PATCH] Track dependencies; do not read all documents. Fix #194. This does a few things: - stops marking all docs as to-be-read - tracks the absolute path(s) to the relevant JS file(s) in each IR object - exposes the dependency path(s) via each renderer, since the renderer manages the IR object - records the dependency path(s), each relative to `app.srcdir`, for each directive --- sphinx_js/__init__.py | 13 ----- sphinx_js/directives.py | 25 ++++++++-- sphinx_js/ir.py | 3 ++ sphinx_js/jsdoc.py | 16 +++---- sphinx_js/renderers.py | 64 ++++++++++++++++--------- sphinx_js/typedoc.py | 20 ++++++++ tests/test_jsdoc_analysis/test_jsdoc.py | 1 + 7 files changed, 96 insertions(+), 46 deletions(-) diff --git a/sphinx_js/__init__.py b/sphinx_js/__init__.py index 242e7adb..d6b4f395 100644 --- a/sphinx_js/__init__.py +++ b/sphinx_js/__init__.py @@ -15,8 +15,6 @@ def setup(app): # is RSTs. app.connect('builder-inited', analyze) - app.connect('env-before-read-docs', read_all_docs) - app.add_directive_to_domain('js', 'staticfunction', JSStaticFunction) @@ -82,14 +80,3 @@ def root_or_fallback(root_for_relative_paths, abs_source_paths): raise SphinxError('Since more than one js_source_path is specified in conf.py, root_for_relative_js_paths must also be specified. This allows paths beginning with ./ or ../ to be unambiguous.') else: return abs_source_paths[0] - - -def read_all_docs(app, env, doc_names): - """Add all found docs to the to-be-read list, because we have no way of - telling which ones reference JS code that might have changed. - - Otherwise, builds go stale until you touch the stale RSTs or do a ``make - clean``. - - """ - doc_names[:] = env.found_docs diff --git a/sphinx_js/directives.py b/sphinx_js/directives.py index 2545798f..9eaf8e31 100644 --- a/sphinx_js/directives.py +++ b/sphinx_js/directives.py @@ -7,6 +7,8 @@ can access each other and collaborate. """ +from os.path import join, relpath + from docutils.parsers.rst import Directive from docutils.parsers.rst.directives import flag from sphinx.domains.javascript import JSCallable @@ -29,6 +31,17 @@ class JsDirective(Directive): } +def note_dependencies(app, dependencies): + """Note dependencies of current document.""" + for fn in dependencies: + # Dependencies in the IR are relative to `root_for_relative_paths`, itself + # relative to the configuration directory. + abs = join(app._sphinxjs_analyzer._base_dir, fn) + # Sphinx dependencies are relative to the source directory. + rel = relpath(abs, app.srcdir) + app.env.note_dependency(rel) + + def auto_function_directive_bound_to_app(app): class AutoFunctionDirective(JsDirective): """js:autofunction directive, which spits out a js:function directive @@ -38,7 +51,9 @@ class AutoFunctionDirective(JsDirective): """ def run(self): - return AutoFunctionRenderer.from_directive(self, app).rst_nodes() + renderer = AutoFunctionRenderer.from_directive(self, app) + note_dependencies(app, renderer.dependencies()) + return renderer.rst_nodes() return AutoFunctionDirective @@ -60,7 +75,9 @@ class AutoClassDirective(JsDirective): 'private-members': flag}) def run(self): - return AutoClassRenderer.from_directive(self, app).rst_nodes() + renderer = AutoClassRenderer.from_directive(self, app) + note_dependencies(app, renderer.dependencies()) + return renderer.rst_nodes() return AutoClassDirective @@ -73,7 +90,9 @@ class AutoAttributeDirective(JsDirective): """ def run(self): - return AutoAttributeRenderer.from_directive(self, app).rst_nodes() + renderer = AutoAttributeRenderer.from_directive(self, app) + note_dependencies(app, renderer.dependencies()) + return renderer.rst_nodes() return AutoAttributeDirective diff --git a/sphinx_js/ir.py b/sphinx_js/ir.py index 7f0e4bf9..09ae288c 100644 --- a/sphinx_js/ir.py +++ b/sphinx_js/ir.py @@ -160,6 +160,9 @@ class TopLevel: path: Pathname #: The basename of the file the object is from, e.g. "foo.js" filename: str + #: The path to the dependency, i.e., the file the object is from. + #: Either absolute or relative to the root_for_relative_js_paths. + deppath: Optional[str] #: The human-readable description of the entity or '' if absent description: ReStructuredText #: Line number where the object (exluding any prefixing comment) begins diff --git a/sphinx_js/jsdoc.py b/sphinx_js/jsdoc.py index 5f71efad..ced026b5 100644 --- a/sphinx_js/jsdoc.py +++ b/sphinx_js/jsdoc.py @@ -122,10 +122,9 @@ def _doclet_as_class(self, doclet, full_path): # the fields are about the default constructor: constructor=self._doclet_as_function(doclet, full_path), members=members, - **top_level_properties(doclet, full_path)) + **top_level_properties(doclet, full_path, self._base_dir)) - @staticmethod - def _doclet_as_function(doclet, full_path): + def _doclet_as_function(self, doclet, full_path): return Function( description=description(doclet), exported_from=None, @@ -136,10 +135,9 @@ def _doclet_as_function(doclet, full_path): exceptions=exceptions_to_ir(doclet.get('exceptions', [])), returns=returns_to_ir(doclet.get('returns', [])), params=params_to_ir(doclet), - **top_level_properties(doclet, full_path)) + **top_level_properties(doclet, full_path, self._base_dir)) - @staticmethod - def _doclet_as_attribute(doclet, full_path): + def _doclet_as_attribute(self, doclet, full_path): return Attribute( description=description(doclet), exported_from=None, @@ -148,7 +146,7 @@ def _doclet_as_attribute(doclet, full_path): is_static=False, is_private=is_private(doclet), type=get_type(doclet), - **top_level_properties(doclet, full_path) + **top_level_properties(doclet, full_path, self._base_dir) ) @@ -264,7 +262,7 @@ def get_type(props): return '|'.join(names) if names else None -def top_level_properties(doclet, full_path): +def top_level_properties(doclet, full_path, base_dir): """Extract information common to complex entities, and return it as a dict. Specifically, pull out the information needed to parametrize TopLevel's @@ -275,6 +273,7 @@ def top_level_properties(doclet, full_path): name=doclet['name'], path=Pathname(full_path), filename=doclet['meta']['filename'], + deppath=relpath(join(doclet['meta']['path'], doclet['meta']['filename']), base_dir), # description's source varies depending on whether the doclet is a # class, so it gets filled out elsewhere. line=doclet['meta']['lineno'], @@ -292,6 +291,7 @@ def properties_to_ir(properties): # because we never use them for anything: path=Pathname([]), filename='', + deppath='', description=description(p), line=0, deprecated=False, diff --git a/sphinx_js/renderers.py b/sphinx_js/renderers.py index 6b099b5f..8fc38e4f 100644 --- a/sphinx_js/renderers.py +++ b/sphinx_js/renderers.py @@ -57,39 +57,58 @@ def from_directive(cls, directive, app): content=directive.content, options=directive.options) - def rst_nodes(self): - """Render into RST nodes a thing shaped like a function, having a name - and arguments. - - Fill in args, docstrings, and info fields from stored JSDoc output. + def get_object(self): + """Return the IR object rendered by this renderer. """ try: obj = self._app._sphinxjs_analyzer.get_object( self._partial_path, self._renderer_type) + return obj except SuffixNotFound as exc: raise SphinxError('No documentation was found for object "%s" or any path ending with that.' % ''.join(exc.segments)) except SuffixAmbiguous as exc: raise SphinxError('More than one object matches the path suffix "%s". Candidate paths have these segments in front: %s' % (''.join(exc.segments), exc.next_possible_keys)) - else: - rst = self.rst(self._partial_path, - obj, - use_short_name='short-name' in self._options) - - # Parse the RST into docutils nodes with a fresh doc, and return - # them. - # - # Not sure if passing the settings from the "real" doc is the right - # thing to do here: - doc = new_document('%s:%s(%s)' % (obj.filename, - obj.path, - obj.line), - settings=self._directive.state.document.settings) - RstParser().parse(rst, doc) - return doc.children - return [] + + def dependencies(self): + """Return a set of path(s) to the file(s) that the IR object + rendered by this renderer is from. Each path is absolute or + relative to `root_for_relative_js_paths`. + + """ + try: + obj = self.get_object() + if obj.deppath: + return set([obj.deppath]) + except SphinxError: + pass + return set([]) + + def rst_nodes(self): + """Render into RST nodes a thing shaped like a function, having a name + and arguments. + + Fill in args, docstrings, and info fields from stored JSDoc output. + + """ + obj = self.get_object() + rst = self.rst(self._partial_path, + obj, + use_short_name='short-name' in self._options) + + # Parse the RST into docutils nodes with a fresh doc, and return + # them. + # + # Not sure if passing the settings from the "real" doc is the right + # thing to do here: + doc = new_document('%s:%s(%s)' % (obj.filename, + obj.path, + obj.line), + settings=self._directive.state.document.settings) + RstParser().parse(rst, doc) + return doc.children def rst(self, partial_path, obj, use_short_name=False): """Return rendered RST about an entity with the given name and IR @@ -199,6 +218,7 @@ def _template_vars(self, name, obj): name='', path=Pathname([]), filename='', + deppath=None, description='', line=0, deprecated=False, diff --git a/sphinx_js/typedoc.py b/sphinx_js/typedoc.py index a6315d1e..bdd2c487 100644 --- a/sphinx_js/typedoc.py +++ b/sphinx_js/typedoc.py @@ -68,6 +68,25 @@ def _containing_module(self, node) -> Optional[Pathname]: # Found one! return Pathname(make_path_segments(node, self._base_dir)) + def _containing_deppath(self, node) -> Optional[Pathname]: + """Return the path pointing to the module containing the given node. + The path is absolute or relative to `root_for_relative_js_paths`. + Raises ValueError if one isn't found. + + """ + while True: + node = node.get('__parent') + if not node or node['id'] == 0: + # We went all the way up but didn't find a containing module. + raise ValueError('Could not find deppath') + elif node.get('kindString') == 'External module': + # Found one! + deppath = node.get('originalName') + if deppath: + return relpath(deppath, self._base_dir) + else: + raise ValueError('Could not find deppath') + def _top_level_properties(self, node): source = node.get('sources')[0] if node.get('flags', {}).get('isExported', False): @@ -78,6 +97,7 @@ def _top_level_properties(self, node): name=short_name(node), path=Pathname(make_path_segments(node, self._base_dir)), filename=basename(source['fileName']), + deppath=self._containing_deppath(node), description=make_description(node.get('comment', {})), line=source['line'], diff --git a/tests/test_jsdoc_analysis/test_jsdoc.py b/tests/test_jsdoc_analysis/test_jsdoc.py index 0331ceb1..f5eb12b3 100644 --- a/tests/test_jsdoc_analysis/test_jsdoc.py +++ b/tests/test_jsdoc_analysis/test_jsdoc.py @@ -36,6 +36,7 @@ def test_top_level_and_function(self): name='foo', path=Pathname(['./', 'function.', 'foo']), filename='function.js', + deppath='function.js', # Line breaks and indentation should be preserved: description=( 'Determine any of type, note, score, and element using a callback. This\n'