Skip to content

Commit

Permalink
Track dependencies; do not read all documents. Fix #194.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ncalexan authored and lonnen committed May 13, 2022
1 parent abb38d2 commit d1b61b7
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 46 deletions.
13 changes: 0 additions & 13 deletions sphinx_js/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
25 changes: 22 additions & 3 deletions sphinx_js/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down
3 changes: 3 additions & 0 deletions sphinx_js/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions sphinx_js/jsdoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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)
)


Expand Down Expand Up @@ -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
Expand All @@ -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'],
Expand All @@ -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,
Expand Down
64 changes: 42 additions & 22 deletions sphinx_js/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -199,6 +218,7 @@ def _template_vars(self, name, obj):
name='',
path=Pathname([]),
filename='',
deppath=None,
description='',
line=0,
deprecated=False,
Expand Down
20 changes: 20 additions & 0 deletions sphinx_js/typedoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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'],

Expand Down
1 change: 1 addition & 0 deletions tests/test_jsdoc_analysis/test_jsdoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit d1b61b7

Please sign in to comment.