From eede30851040ad0e4ef624e22e9d5fb8fc8e38fa Mon Sep 17 00:00:00 2001 From: mariusschenzle Date: Fri, 30 Apr 2021 16:34:26 +0200 Subject: [PATCH 1/9] implement static function support * add directive 'staticfunction' * directive sets prefix 'static' in front of a function * write some tests * extend template function.rst --- sphinx_js/__init__.py | 8 +- sphinx_js/directives.py | 11 +- sphinx_js/jsdoc.py | 6 +- sphinx_js/renderers.py | 1 + sphinx_js/templates/function.rst | 4 + tests/test_build_js/source/code.js | 12 ++ .../source/docs/autofunction_static.rst | 2 + .../source/docs/staticfunction.rst | 1 + tests/test_build_js/source/docs/testing.py | 136 ++++++++++++++++++ tests/test_build_js/test_build_js.py | 17 +++ 10 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 tests/test_build_js/source/docs/autofunction_static.rst create mode 100644 tests/test_build_js/source/docs/staticfunction.rst create mode 100644 tests/test_build_js/source/docs/testing.py diff --git a/sphinx_js/__init__.py b/sphinx_js/__init__.py index 8749fc11..04fe066a 100644 --- a/sphinx_js/__init__.py +++ b/sphinx_js/__init__.py @@ -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 @@ -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)) @@ -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 diff --git a/sphinx_js/directives.py b/sphinx_js/directives.py index 2d4e4793..9244240c 100644 --- a/sphinx_js/directives.py +++ b/sphinx_js/directives.py @@ -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): @@ -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 ' diff --git a/sphinx_js/jsdoc.py b/sphinx_js/jsdoc.py index 9fa33cfe..03f65065 100644 --- a/sphinx_js/jsdoc.py +++ b/sphinx_js/jsdoc.py @@ -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', [])), @@ -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. diff --git a/sphinx_js/renderers.py b/sphinx_js/renderers.py index f2e48526..6b099b5f 100644 --- a/sphinx_js/renderers.py +++ b/sphinx_js/renderers.py @@ -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)) diff --git a/sphinx_js/templates/function.rst b/sphinx_js/templates/function.rst index 105538dc..4a136c76 100644 --- a/sphinx_js/templates/function.rst +++ b/sphinx_js/templates/function.rst @@ -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) }} diff --git a/tests/test_build_js/source/code.js b/tests/test_build_js/source/code.js index 508d0acb..42b07d25 100644 --- a/tests/test_build_js/source/code.js +++ b/tests/test_build_js/source/code.js @@ -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. @@ -234,3 +235,14 @@ function union(fnodeA) { */ function longDescriptions(a, b) { } + +/** + * Class doc. + */ +class SimpleClass { + + /** + * Static. + */ + static noUseOfThis() {} +} diff --git a/tests/test_build_js/source/docs/autofunction_static.rst b/tests/test_build_js/source/docs/autofunction_static.rst new file mode 100644 index 00000000..ed11b695 --- /dev/null +++ b/tests/test_build_js/source/docs/autofunction_static.rst @@ -0,0 +1,2 @@ +.. js:autoclass:: SimpleClass + :members: diff --git a/tests/test_build_js/source/docs/staticfunction.rst b/tests/test_build_js/source/docs/staticfunction.rst new file mode 100644 index 00000000..ef69c579 --- /dev/null +++ b/tests/test_build_js/source/docs/staticfunction.rst @@ -0,0 +1 @@ +.. js:staticfunction:: staticFunction diff --git a/tests/test_build_js/source/docs/testing.py b/tests/test_build_js/source/docs/testing.py new file mode 100644 index 00000000..e193861c --- /dev/null +++ b/tests/test_build_js/source/docs/testing.py @@ -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 diff --git a/tests/test_build_js/test_build_js.py b/tests/test_build_js/test_build_js.py index 3aca151c..c5b08363 100644 --- a/tests/test_build_js/test_build_js.py +++ b/tests/test_build_js/test_build_js.py @@ -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.""" From 353af7bef82e10593ae33576d8e21b3da6ace169 Mon Sep 17 00:00:00 2001 From: mariusschenzle Date: Tue, 4 May 2021 13:15:59 +0200 Subject: [PATCH 2/9] remove wrongly added config value 'jsdoc_cache' --- sphinx_js/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx_js/__init__.py b/sphinx_js/__init__.py index 04fe066a..6f0f8732 100644 --- a/sphinx_js/__init__.py +++ b/sphinx_js/__init__.py @@ -34,7 +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') + # 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 From e3051e9a7d2147a53b143143c0918f0d74563476 Mon Sep 17 00:00:00 2001 From: mariusschenzle Date: Tue, 4 May 2021 13:18:07 +0200 Subject: [PATCH 3/9] fix code style issue --- sphinx_js/directives.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sphinx_js/directives.py b/sphinx_js/directives.py index 9244240c..2545798f 100644 --- a/sphinx_js/directives.py +++ b/sphinx_js/directives.py @@ -9,7 +9,6 @@ """ from docutils.parsers.rst import Directive from docutils.parsers.rst.directives import flag - from sphinx.domains.javascript import JSCallable from .renderers import (AutoFunctionRenderer, From 00655c7019a9f3a25ece0bbce4191a5cc3cf82d0 Mon Sep 17 00:00:00 2001 From: mariusschenzle Date: Tue, 4 May 2021 13:20:16 +0200 Subject: [PATCH 4/9] remove eslint-disable comment --- tests/test_build_js/source/code.js | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_build_js/source/code.js b/tests/test_build_js/source/code.js index 42b07d25..1c39f9f7 100644 --- a/tests/test_build_js/source/code.js +++ b/tests/test_build_js/source/code.js @@ -1,4 +1,3 @@ -/* 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. From 492b5411df07f4644f6c15d1774fefb8821f5b02 Mon Sep 17 00:00:00 2001 From: mariusschenzle Date: Tue, 4 May 2021 13:22:16 +0200 Subject: [PATCH 5/9] drop wrongly stored testing.py --- tests/test_build_js/source/docs/testing.py | 136 --------------------- 1 file changed, 136 deletions(-) delete mode 100644 tests/test_build_js/source/docs/testing.py diff --git a/tests/test_build_js/source/docs/testing.py b/tests/test_build_js/source/docs/testing.py deleted file mode 100644 index e193861c..00000000 --- a/tests/test_build_js/source/docs/testing.py +++ /dev/null @@ -1,136 +0,0 @@ -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 From 8475dbe00cebcc2e7a1d21952c19f0933376ea74 Mon Sep 17 00:00:00 2001 From: mariusschenzle Date: Wed, 5 May 2021 14:47:53 +0200 Subject: [PATCH 6/9] clean-up code * remove unused config value * change analyzers is_static method to be consistente with is_private method --- sphinx_js/__init__.py | 1 - sphinx_js/jsdoc.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sphinx_js/__init__.py b/sphinx_js/__init__.py index 6f0f8732..242e7adb 100644 --- a/sphinx_js/__init__.py +++ b/sphinx_js/__init__.py @@ -34,7 +34,6 @@ 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 diff --git a/sphinx_js/jsdoc.py b/sphinx_js/jsdoc.py index 03f65065..47765d91 100644 --- a/sphinx_js/jsdoc.py +++ b/sphinx_js/jsdoc.py @@ -157,7 +157,7 @@ def is_private(doclet): def is_static(obj): - return obj.get('scope', '') == 'static' + return obj.get('scope') == 'static' def full_path_segments(d, base_dir, longname_field='longname'): From f9e9edd4dfdf4d289c4c7d381564f9c992c4aaa4 Mon Sep 17 00:00:00 2001 From: mariusschenzle Date: Wed, 5 May 2021 15:13:40 +0200 Subject: [PATCH 7/9] clean-up code --- sphinx_js/jsdoc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx_js/jsdoc.py b/sphinx_js/jsdoc.py index 47765d91..5f71efad 100644 --- a/sphinx_js/jsdoc.py +++ b/sphinx_js/jsdoc.py @@ -156,8 +156,8 @@ def is_private(doclet): return doclet.get('access') == 'private' -def is_static(obj): - return obj.get('scope') == 'static' +def is_static(doclet): + return doclet.get('scope') == 'static' def full_path_segments(d, base_dir, longname_field='longname'): From 1af50139bec444860b59950e82e529ef2b025757 Mon Sep 17 00:00:00 2001 From: mariusschenzle Date: Thu, 6 May 2021 09:52:03 +0200 Subject: [PATCH 8/9] drop single staticfunction test --- tests/test_build_js/test_build_js.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_build_js/test_build_js.py b/tests/test_build_js/test_build_js.py index c5b08363..4a081950 100644 --- a/tests/test_build_js/test_build_js.py +++ b/tests/test_build_js/test_build_js.py @@ -119,12 +119,6 @@ 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( From ca2eed5720521388d57dde2cd50ced5c63a77b67 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Thu, 6 May 2021 12:41:51 -0400 Subject: [PATCH 9/9] Remove staticfunction.rst that went with a deleted test. --- tests/test_build_js/source/docs/staticfunction.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tests/test_build_js/source/docs/staticfunction.rst diff --git a/tests/test_build_js/source/docs/staticfunction.rst b/tests/test_build_js/source/docs/staticfunction.rst deleted file mode 100644 index ef69c579..00000000 --- a/tests/test_build_js/source/docs/staticfunction.rst +++ /dev/null @@ -1 +0,0 @@ -.. js:staticfunction:: staticFunction