Skip to content

Commit

Permalink
implement static function support
Browse files Browse the repository at this point in the history
* add directive 'staticfunction'
* directive sets prefix 'static' in front of a function
* write some tests
* extend template function.rst
  • Loading branch information
mariusschenzle committed Apr 30, 2021
1 parent 91d35b8 commit eede308
Show file tree
Hide file tree
Showing 10 changed files with 194 additions and 4 deletions.
8 changes: 6 additions & 2 deletions sphinx_js/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

from .directives import (auto_class_directive_bound_to_app,
auto_function_directive_bound_to_app,
auto_attribute_directive_bound_to_app)
auto_attribute_directive_bound_to_app,
JSStaticFunction)
from .jsdoc import Analyzer as JsAnalyzer
from .typedoc import Analyzer as TsAnalyzer


def setup(app):
# I believe this is the best place to run jsdoc. I was tempted to use
# app.add_source_parser(), but I think the kind of source it's referring to
Expand All @@ -17,6 +17,9 @@ def setup(app):

app.connect('env-before-read-docs', read_all_docs)

app.add_directive_to_domain('js',
'staticfunction',
JSStaticFunction)
app.add_directive_to_domain('js',
'autofunction',
auto_function_directive_bound_to_app(app))
Expand All @@ -31,6 +34,7 @@ def setup(app):
app.add_config_value('js_language', 'javascript', 'env')
app.add_config_value('js_source_path', '../', 'env')
app.add_config_value('jsdoc_config_path', None, 'env')
app.add_config_value('jsdoc_cache', None, 'env')

# We could use a callable as the "default" param here, but then we would
# have had to duplicate or build framework around the logic that promotes
Expand Down
11 changes: 10 additions & 1 deletion sphinx_js/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
from docutils.parsers.rst import Directive
from docutils.parsers.rst.directives import flag

from .renderers import AutoFunctionRenderer, AutoClassRenderer, AutoAttributeRenderer
from sphinx.domains.javascript import JSCallable

from .renderers import (AutoFunctionRenderer,
AutoClassRenderer,
AutoAttributeRenderer)


class JsDirective(Directive):
Expand Down Expand Up @@ -83,3 +87,8 @@ def _members_to_exclude(arg):
"""
return set(a.strip() for a in (arg or '').split(','))


class JSStaticFunction(JSCallable):
"""Like a callable but with a different prefix."""
display_prefix = 'static '
6 changes: 5 additions & 1 deletion sphinx_js/jsdoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def _doclet_as_function(doclet, full_path):
exported_from=None,
is_abstract=False,
is_optional=False,
is_static=False,
is_static=is_static(doclet),
is_private=is_private(doclet),
exceptions=exceptions_to_ir(doclet.get('exceptions', [])),
returns=returns_to_ir(doclet.get('returns', [])),
Expand All @@ -156,6 +156,10 @@ def is_private(doclet):
return doclet.get('access') == 'private'


def is_static(obj):
return obj.get('scope', '') == 'static'


def full_path_segments(d, base_dir, longname_field='longname'):
"""Return the full, unambiguous list of path segments that points to an
entity described by a doclet.
Expand Down
1 change: 1 addition & 0 deletions sphinx_js/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def _template_vars(self, name, obj):
examples=obj.examples,
deprecated=obj.deprecated,
is_optional=obj.is_optional,
is_static=obj.is_static,
see_also=obj.see_alsos,
content='\n'.join(self._content))

Expand Down
4 changes: 4 additions & 0 deletions sphinx_js/templates/function.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{% import 'common.rst' as common %}

{% if is_static %}
.. js:staticfunction:: {{ name }}{{ '?' if is_optional else '' }}{{ params }}
{% else %}
.. js:function:: {{ name }}{{ '?' if is_optional else '' }}{{ params }}
{% endif %}

{{ common.deprecated(deprecated)|indent(3) }}

Expand Down
12 changes: 12 additions & 0 deletions tests/test_build_js/source/code.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable */
/**
* Return the ratio of the inline text length of the links in an element to
* the inline text length of the entire element.
Expand Down Expand Up @@ -234,3 +235,14 @@ function union(fnodeA) {
*/
function longDescriptions(a, b) {
}

/**
* Class doc.
*/
class SimpleClass {

/**
* Static.
*/
static noUseOfThis() {}
}
2 changes: 2 additions & 0 deletions tests/test_build_js/source/docs/autofunction_static.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.. js:autoclass:: SimpleClass
:members:
1 change: 1 addition & 0 deletions tests/test_build_js/source/docs/staticfunction.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.. js:staticfunction:: staticFunction
136 changes: 136 additions & 0 deletions tests/test_build_js/source/docs/testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
from io import open
from os.path import dirname, join
from shutil import rmtree
from unittest import TestCase
import sys

from sphinx.cmd.build import main as sphinx_main

from sphinx_js.jsdoc import Analyzer as JsAnalyzer, jsdoc_output
from sphinx_js.typedoc import Analyzer as TsAnalyzer, index_by_id, typedoc_output


class ThisDirTestCase(TestCase):
"""A TestCase that knows how to find the directory the subclass is defined
in"""

@classmethod
def this_dir(cls):
"""Return the path to the dir containing the testcase class."""
# nose does some amazing magic that makes this work even if there are
# multiple test modules with the same name:
return dirname(sys.modules[cls.__module__].__file__)


class SphinxBuildTestCase(ThisDirTestCase):
"""Base class for tests which require a Sphinx tree to be built and then
deleted afterward
"""
builder = 'text'

@classmethod
def setup_class(cls):
"""Run Sphinx against the dir adjacent to the testcase."""
cls.docs_dir = join(cls.this_dir(), 'source', 'docs')
# -v for better tracebacks:
if sphinx_main([cls.docs_dir, '-b', cls.builder, '-v', '-E', join(cls.docs_dir, '_build')]):
raise RuntimeError('Sphinx build exploded.')

@classmethod
def teardown_class(cls):
rmtree(join(cls.docs_dir, '_build'))

def _file_contents(self, filename):
extension = 'txt' if self.builder == 'text' else 'html'
with open(join(self.docs_dir, '_build', '%s.%s' % (filename, extension)),
encoding='utf8') as file:
return file.read()

def _file_contents_eq(self, filename, contents):
assert self._file_contents(filename) == contents


class JsDocTestCase(ThisDirTestCase):
"""Base class for tests which analyze a file using JSDoc"""

@classmethod
def setup_class(cls):
"""Run the JS analyzer over the JSDoc output."""
source_dir = join(cls.this_dir(), 'source')
output = jsdoc_output(None,
[join(source_dir, cls.file)],
source_dir,
source_dir)
cls.analyzer = JsAnalyzer(output, source_dir)


class TypeDocTestCase(ThisDirTestCase):
"""Base class for tests which imbibe TypeDoc's output"""

@classmethod
def setup_class(cls):
"""Run the TS analyzer over the TypeDoc output."""
cls._source_dir = join(cls.this_dir(), 'source')
cls.json = typedoc_output([join(cls._source_dir, file)
for file in cls.files],
cls._source_dir,
'tsconfig.json')
index_by_id({}, cls.json)


class TypeDocAnalyzerTestCase(TypeDocTestCase):
"""Base class for tests which analyze a file using TypeDoc"""

@classmethod
def setup_class(cls):
"""Run the TS analyzer over the TypeDoc output."""
super().setup_class()
cls.analyzer = TsAnalyzer(cls.json, cls._source_dir)


NO_MATCH = object()
def dict_where(json, already_seen=None, **kwargs):
"""Return the first object in the given data structure with properties
equal to the ones given by ``kwargs``.
For example::
>>> dict_where({'hi': 'there', {'mister': 'zangler', 'and': 'friends'}},
mister=zangler)
{'mister': 'zangler', 'and': 'friends'}
So far, only dicts and lists are supported. Other data structures won't be
recursed into. Cycles are avoided.
"""
def object_if_matches_properties(json, **kwargs):
"""Return the given JSON object iff all the properties and values given
by ``kwargs`` are in it. Else, return NO_MATCH."""
for k, v in kwargs.items():
if json.get(k, NO_MATCH) != v:
return NO_MATCH
return json

if already_seen is None:
already_seen = set()
already_seen.add(id(json))
if isinstance(json, list):
for list_item in json:
if id(list_item) not in already_seen:
match = dict_where(list_item, already_seen, **kwargs)
if match is not NO_MATCH:
return match
elif isinstance(json, dict):
match = object_if_matches_properties(json, **kwargs)
if match is not NO_MATCH:
return match
for k, v in json.items():
if id(v) not in already_seen:
match = dict_where(v, already_seen, **kwargs)
if match is not NO_MATCH:
return match
else:
# We don't know how to match leaf values yet.
pass
return NO_MATCH
17 changes: 17 additions & 0 deletions tests/test_build_js/test_build_js.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ def test_autofunction_see(self):
' * "deprecatedFunction"\n\n'
' * "DeprecatedAttribute"\n')

def test_staticfunction(self):
"""Make sure the staticfunction directive works."""
self._file_contents_eq(
'staticfunction',
'static staticFunction()\n')

def test_autofunction_static(self):
"""Make sure the static function gets its prefix ``static``."""
self._file_contents_eq(
'autofunction_static',
'class SimpleClass()\n\n'
' Class doc.\n'
'\n'
' static SimpleClass.noUseOfThis()\n'
'\n'
' Static.\n')

def test_autoclass(self):
"""Make sure classes show their class comment and constructor
comment."""
Expand Down

0 comments on commit eede308

Please sign in to comment.