diff --git a/README.rst b/README.rst index 5e15ea9d..c1ad8895 100644 --- a/README.rst +++ b/README.rst @@ -307,6 +307,11 @@ Configuration Reference ``jsdoc_cache`` Path to a file where jsdoc output will be cached. If omitted, jsdoc will be run every time Sphinx is. If you have a large number of source files, it may be beneficial to configure this value. But be careful: the cache is not automatically flushed if your source code changes; you must delete it manually. +``sphinx_js_lax`` + If `True`, doclet conflicts and parsing errors will result in warnings instead of exceptions. + Useful when starting to use sphinx-js on an existing project where you do not want to fix + all errors upfront. + Example ======= diff --git a/requirements_dev.txt b/requirements_dev.txt index d38ab234..e456328c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -5,3 +5,4 @@ recommonmark==0.4.0 # with regard to how it emits class names and em dashes from # time to time: Sphinx==1.7.2 +mock==3.0.5 diff --git a/sphinx_js/doclets.py b/sphinx_js/doclets.py index 63dc7e70..7a8ad74c 100644 --- a/sphinx_js/doclets.py +++ b/sphinx_js/doclets.py @@ -8,13 +8,17 @@ import subprocess from tempfile import TemporaryFile, NamedTemporaryFile +from parsimonious.exceptions import ParseError from six import string_types from sphinx.errors import SphinxError +from sphinx.util.logging import getLogger from .parsers import path_and_formal_params, PathVisitor from .suffix_tree import PathTaken, SuffixTree from .typedoc import parse_typedoc +logger = getLogger(__name__) + def gather_doclets(app): """Run JSDoc or another analysis tool across a whole codebase, and squirrel @@ -46,8 +50,17 @@ def gather_doclets(app): d) except PathTaken as conflict: conflicts.append(conflict.segments) + except ParseError as e: + if not app.config.sphinx_js_lax: + raise + else: + logger.warning('Could not parse path correctly %s' % e.text) if conflicts: - raise PathsTaken(conflicts) + exception = PathsTaken(conflicts) + if not app.config.sphinx_js_lax: + raise exception + else: + logger.warning('%s' % exception) # Build lookup table for autoclass's :members: option. This will also # pick up members of functions (inner variables), but it will instantly diff --git a/tests/test_doclets.py b/tests/test_doclets.py index b161aacd..f4a9fd5c 100644 --- a/tests/test_doclets.py +++ b/tests/test_doclets.py @@ -1,10 +1,15 @@ # -*- coding: utf-8 -*- from os.path import abspath +try: + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch + import pytest from sphinx.errors import SphinxError -from sphinx_js.doclets import doclet_full_path, root_or_fallback +from sphinx_js.doclets import doclet_full_path, root_or_fallback, gather_doclets, PathsTaken def test_doclet_full_path(): @@ -34,3 +39,120 @@ def test_relative_path_root(): with pytest.raises(SphinxError): root_or_fallback(None, ['a', 'b']) assert root_or_fallback('smoo', ['a']) == abspath('smoo') + + +CONFLICTED_DOCLETS = [ + { + 'comment': True, + 'undocumented': False, + 'longname': 'best#thing~yeah', + 'meta': { + 'filename': 'utils.jsm', + 'path': '/boogie/smoo/Checkouts/fathom' + } + }, + { + 'comment': True, + 'undocumented': False, + 'longname': 'best#thing~yeah', + 'meta': { + 'filename': 'utils.jsm', + 'path': '/boogie/smoo/Checkouts/fathom' + } + }, + { + 'comment': True, + 'undocumented': False, + 'longname': 'best#thing~woot', + 'meta': { + 'filename': 'utils.jsm', + 'path': '/boogie/smoo/Checkouts/fathom' + } + }, + { + 'comment': True, + 'undocumented': False, + 'longname': 'best#thing~woot', + 'meta': { + 'filename': 'utils.jsm', + 'path': '/boogie/smoo/Checkouts/fathom' + } + } +] + + +def mock_analyze_jsdoc(doclets): + def analyze(source_paths, app): + return doclets + return analyze + + +@patch( + 'sphinx_js.doclets.ANALYZERS', { + 'javascript': mock_analyze_jsdoc(CONFLICTED_DOCLETS) + } +) +def test_gather_doclets_conflicted_lax(): + app = Mock() + app.config.js_language = 'javascript' + app.config.js_source_path = 'source-path' + app.config.root_for_relative_js_paths = '/boogie/smoo/Checkouts/fathom' + + with patch('sphinx_js.doclets.logger', Mock()) as logger_mock: + gather_doclets(app) + + message, = logger_mock.warning.call_args_list[0][0] + assert './utils.best#thing~yeah' in message + assert './utils.best#thing~woot' in message + + +@patch( + 'sphinx_js.doclets.ANALYZERS', { + 'javascript': mock_analyze_jsdoc(CONFLICTED_DOCLETS) + } +) +def test_gather_doclets_conflicted(): + app = Mock() + app.config.js_language = 'javascript' + app.config.js_source_path = 'source-path' + app.config.root_for_relative_js_paths = '/boogie/smoo/Checkouts/fathom' + app.config.sphinx_js_lax = False + + with pytest.raises(PathsTaken) as e: + gather_doclets(app) + message = str(e.value) + assert './utils.best#thing~yeah' in message + assert './utils.best#thing~woot' in message + + +def test_gather_doclets_parse_error(): + app = Mock() + app.config.js_language = 'javascript' + app.config.js_source_path = 'source-path' + app.config.root_for_relative_js_paths = '/boogie/smoo/Checkouts/fathom' + app.config.sphinx_js_lax = True + + parse_error_doclet = { + 'comment': True, + 'undocumented': False, + 'longname': 'best#thing~yeah', + 'meta': { + 'filename': 'utils.jsm', + 'path': '../boogie/smoo/Checkouts/fathom' + } + } + + def patch_logger(): + return patch('sphinx_js.doclets.logger', Mock()) + + def patch_analyze(): + return patch('sphinx_js.doclets.ANALYZERS', { + 'javascript': mock_analyze_jsdoc([parse_error_doclet]) + }) + + with patch_logger() as logger_mock, patch_analyze(): + gather_doclets(app) + + message, = logger_mock.warning.call_args_list[0][0] + assert 'Could not parse path correctly' in message + assert 'boogie/smoo/Checkouts/fathom/utils.best#thing~yeah' in message