|
From: <mi...@us...> - 2021-12-23 14:55:15
|
Revision: 8916
http://sourceforge.net/p/docutils/code/8916
Author: milde
Date: 2021-12-23 14:55:12 +0000 (Thu, 23 Dec 2021)
Log Message:
-----------
More detailled system-message for include directive with missing parser.
The system-message issued when an "include" directive sets
the "parser" option to "markdown", "commonmark" or "recommonmark"
but the upstream parser module is not available
has source/line info now.
Modified Paths:
--------------
trunk/docutils/HISTORY.txt
trunk/docutils/docutils/parsers/__init__.py
trunk/docutils/docutils/parsers/recommonmark_wrapper.py
trunk/docutils/docutils/parsers/rst/__init__.py
trunk/docutils/docutils/parsers/rst/directives/__init__.py
trunk/docutils/test/DocutilsTestSupport.py
trunk/docutils/test/test_parsers/test_recommonmark/test_misc.py
trunk/docutils/test/test_parsers/test_rst/test_directives/test__init__.py
trunk/docutils/test/test_parsers/test_rst/test_directives/test_include.py
Modified: trunk/docutils/HISTORY.txt
===================================================================
--- trunk/docutils/HISTORY.txt 2021-12-23 14:55:01 UTC (rev 8915)
+++ trunk/docutils/HISTORY.txt 2021-12-23 14:55:12 UTC (rev 8916)
@@ -14,9 +14,30 @@
Changes Since 0.18.1
====================
+* docutils/parsers/__init__.py
+
+ - Alias for the "myst" parser (https://pypi.org/project/myst-docutils).
+ - Use absolute module names in ``_parser_aliases`` instead of two
+ import attempts. (Keeps details if the `recommonmark_wrapper.py` module
+ raises an ImportError.)
+ - Prepend parser name to ImportError if importing a parser class fails.
+
+* docutils/parsers/recommonmark_wrapper.py
+
+ - Raise ImportError, if import of the upstream parser module fails.
+ If the parser was called from an `"include" directive`_,
+ the system-message now has source/line info.
+
+ .. _"include" directive: docs/ref/rst/directives.html#include
+
+* docutils/parsers/rst/directives/__init__.py
+
+ - parser_name() keeps details if converting ImportError to ValueError.
+
* test/DocutilsTestSupport.py
- - Function exception_data returns None if no exception was raised.
+ - exception_data() returns None if no exception was raised.
+ - recommonmark_wrapper only imported if upstream parser is present.
* test/test_parsers/test_rst/test_directives/test_tables.py
Modified: trunk/docutils/docutils/parsers/__init__.py
===================================================================
--- trunk/docutils/docutils/parsers/__init__.py 2021-12-23 14:55:01 UTC (rev 8915)
+++ trunk/docutils/docutils/parsers/__init__.py 2021-12-23 14:55:12 UTC (rev 8916)
@@ -64,22 +64,28 @@
self.document.note_parse_message)
-_parser_aliases = {
- 'restructuredtext': 'rst',
- 'rest': 'rst',
- 'restx': 'rst',
- 'rtxt': 'rst',
- 'recommonmark': 'recommonmark_wrapper',
- 'commonmark': 'recommonmark_wrapper',
- 'markdown': 'recommonmark_wrapper'}
+_parser_aliases = {# short names for known parsers
+ 'null': 'docutils.parsers.null',
+ # reStructuredText
+ 'rst': 'docutils.parsers.rst',
+ 'restructuredtext': 'docutils.parsers.rst',
+ 'rest': 'docutils.parsers.rst',
+ 'restx': 'docutils.parsers.rst',
+ 'rtxt': 'docutils.parsers.rst',
+ # 3rd-party Markdown parsers
+ 'recommonmark': 'docutils.parsers.recommonmark_wrapper',
+ 'myst': 'myst_parser.docutils_',
+ # 'myst': 'docutils.parsers.myst_wrapper',
+ # TODO: the following two could be either of the above
+ 'commonmark': 'docutils.parsers.recommonmark_wrapper',
+ 'markdown': 'docutils.parsers.recommonmark_wrapper',
+ }
def get_parser_class(parser_name):
"""Return the Parser class from the `parser_name` module."""
parser_name = parser_name.lower()
- if parser_name in _parser_aliases:
- parser_name = _parser_aliases[parser_name]
try:
- module = import_module('docutils.parsers.'+parser_name)
- except ImportError:
- module = import_module(parser_name)
+ module = import_module(_parser_aliases.get(parser_name, parser_name))
+ except ImportError as err:
+ raise ImportError('Parser "%s" missing. %s' % (parser_name, err))
return module.Parser
Modified: trunk/docutils/docutils/parsers/recommonmark_wrapper.py
===================================================================
--- trunk/docutils/docutils/parsers/recommonmark_wrapper.py 2021-12-23 14:55:01 UTC (rev 8915)
+++ trunk/docutils/docutils/parsers/recommonmark_wrapper.py 2021-12-23 14:55:12 UTC (rev 8916)
@@ -13,7 +13,7 @@
# Revision: $Revision$
# Date: $Date$
"""
-A parser for CommonMark MarkDown text using `recommonmark`__.
+A parser for CommonMark Markdown text using `recommonmark`__.
__ https://pypi.org/project/recommonmark/
@@ -27,15 +27,11 @@
try:
from recommonmark.parser import CommonMarkParser
except ImportError as err:
- CommonMarkParser = None
- class Parser(docutils.parsers.Parser):
- def parse(self, inputstring, document):
- error = document.reporter.warning(
- 'Missing dependency: MarkDown input is processed by a 3rd '
- 'party parser but Python did not find the required module '
- '"recommonmark" (https://pypi.org/project/recommonmark/).')
- document.append(error)
+ missing_dependency_msg = '%s.\nParsing "recommonmark" Markdown flavour ' \
+ 'requires the package https://pypi.org/project/recommonmark' % err
+ raise ImportError(missing_dependency_msg)
+
# recommonmark 0.5.0 introduced a hard dependency on Sphinx
# https://github.com/readthedocs/recommonmark/issues/202
# There is a PR to change this to an optional dependency
@@ -47,117 +43,116 @@
class addnodes(nodes.pending): pass
-if CommonMarkParser:
- class Parser(CommonMarkParser):
- """MarkDown parser based on recommonmark.
-
- This parser is provisional:
- the API is not settled and may change with any minor Docutils version.
+class Parser(CommonMarkParser):
+ """MarkDown parser based on recommonmark.
+
+ This parser is provisional:
+ the API is not settled and may change with any minor Docutils version.
+ """
+ supported = ('recommonmark', 'commonmark', 'markdown', 'md')
+ config_section = 'recommonmark parser'
+ config_section_dependencies = ('parsers',)
+
+ def get_transforms(self):
+ return Component.get_transforms(self) # + [AutoStructify]
+
+ def parse(self, inputstring, document):
+ """Use the upstream parser and clean up afterwards.
"""
- supported = ('recommonmark', 'commonmark', 'markdown', 'md')
- config_section = 'recommonmark parser'
- config_section_dependencies = ('parsers',)
+ # check for exorbitantly long lines
+ for i, line in enumerate(inputstring.split('\n')):
+ if len(line) > document.settings.line_length_limit:
+ error = document.reporter.error(
+ 'Line %d exceeds the line-length-limit.'%(i+1))
+ document.append(error)
+ return
- def get_transforms(self):
- return Component.get_transforms(self) # + [AutoStructify]
+ # pass to upstream parser
+ try:
+ CommonMarkParser.parse(self, inputstring, document)
+ except Exception as err:
+ error = document.reporter.error('Parsing with "recommonmark" '
+ 'returned the error:\n%s'%err)
+ document.append(error)
- def parse(self, inputstring, document):
- """Use the upstream parser and clean up afterwards.
- """
- # check for exorbitantly long lines
- for i, line in enumerate(inputstring.split('\n')):
- if len(line) > document.settings.line_length_limit:
- error = document.reporter.error(
- 'Line %d exceeds the line-length-limit.'%(i+1))
- document.append(error)
- return
+ # Post-Processing
+ # ---------------
- # pass to upstream parser
- try:
- CommonMarkParser.parse(self, inputstring, document)
- except Exception as err:
- error = document.reporter.error('Parsing with "recommonmark" '
- 'returned the error:\n%s'%err)
- document.append(error)
+ # merge adjoining Text nodes:
+ for node in document.findall(nodes.TextElement):
+ children = node.children
+ i = 0
+ while i+1 < len(children):
+ if (isinstance(children[i], nodes.Text)
+ and isinstance(children[i+1], nodes.Text)):
+ children[i] = nodes.Text(children[i]+children.pop(i+1))
+ children[i].parent = node
+ else:
+ i += 1
- # Post-Processing
- # ---------------
+ # add "code" class argument to literal elements (inline and block)
+ for node in document.findall(lambda n: isinstance(n,
+ (nodes.literal, nodes.literal_block))):
+ node['classes'].append('code')
+ # move "language" argument to classes
+ for node in document.findall(nodes.literal_block):
+ if 'language' in node.attributes:
+ node['classes'].append(node['language'])
+ del node['language']
- # merge adjoining Text nodes:
- for node in document.findall(nodes.TextElement):
- children = node.children
- i = 0
- while i+1 < len(children):
- if (isinstance(children[i], nodes.Text)
- and isinstance(children[i+1], nodes.Text)):
- children[i] = nodes.Text(children[i]+children.pop(i+1))
- children[i].parent = node
- else:
- i += 1
+ # remove empty target nodes
+ for node in list(document.findall(nodes.target)):
+ # remove empty name
+ node['names'] = [v for v in node['names'] if v]
+ if node.children or [v for v in node.attributes.values() if v]:
+ continue
+ node.parent.remove(node)
- # add "code" class argument to literal elements (inline and block)
- for node in document.findall(lambda n: isinstance(n,
- (nodes.literal, nodes.literal_block))):
- node['classes'].append('code')
- # move "language" argument to classes
- for node in document.findall(nodes.literal_block):
- if 'language' in node.attributes:
- node['classes'].append(node['language'])
- del node['language']
+ # replace raw nodes if raw is not allowed
+ if not document.settings.raw_enabled:
+ for node in document.findall(nodes.raw):
+ warning = document.reporter.warning('Raw content disabled.')
+ node.parent.replace(node, warning)
- # remove empty target nodes
- for node in list(document.findall(nodes.target)):
- # remove empty name
- node['names'] = [v for v in node['names'] if v]
- if node.children or [v for v in node.attributes.values() if v]:
- continue
- node.parent.remove(node)
+ # fix section nodes
+ for node in document.findall(nodes.section):
+ # remove spurious IDs (first may be from duplicate name)
+ if len(node['ids']) > 1:
+ node['ids'].pop()
+ # fix section levels (recommonmark 0.4.0
+ # later versions silently ignore incompatible levels)
+ if 'level' in node:
+ section_level = self.get_section_level(node)
+ if node['level'] != section_level:
+ warning = document.reporter.warning(
+ 'Title level inconsistent. Changing from %d to %d.'
+ %(node['level'], section_level),
+ nodes.literal_block('', node[0].astext()))
+ node.insert(1, warning)
+ # remove non-standard attribute "level"
+ del node['level']
- # replace raw nodes if raw is not allowed
- if not document.settings.raw_enabled:
- for node in document.findall(nodes.raw):
- warning = document.reporter.warning('Raw content disabled.')
- node.parent.replace(node, warning)
+ # drop pending_xref (Sphinx cross reference extension)
+ for node in document.findall(addnodes.pending_xref):
+ reference = node.children[0]
+ if 'name' not in reference:
+ reference['name'] = nodes.fully_normalize_name(
+ reference.astext())
+ node.parent.replace(node, reference)
- # fix section nodes
- for node in document.findall(nodes.section):
- # remove spurious IDs (first may be from duplicate name)
- if len(node['ids']) > 1:
- node['ids'].pop()
- # fix section levels (recommonmark 0.4.0
- # later versions silently ignore incompatible levels)
- if 'level' in node:
- section_level = self.get_section_level(node)
- if node['level'] != section_level:
- warning = document.reporter.warning(
- 'Title level inconsistent. Changing from %d to %d.'
- %(node['level'], section_level),
- nodes.literal_block('', node[0].astext()))
- node.insert(1, warning)
- # remove non-standard attribute "level"
- del node['level']
-
- # drop pending_xref (Sphinx cross reference extension)
- for node in document.findall(addnodes.pending_xref):
- reference = node.children[0]
- if 'name' not in reference:
- reference['name'] = nodes.fully_normalize_name(
- reference.astext())
- node.parent.replace(node, reference)
+ def get_section_level(self, node):
+ """Auxiliary function for post-processing in self.parse()"""
+ level = 1
+ while True:
+ node = node.parent
+ if isinstance(node, nodes.document):
+ return level
+ if isinstance(node, nodes.section):
+ level += 1
- def get_section_level(self, node):
- """Auxiliary function for post-processing in self.parse()"""
- level = 1
- while True:
- node = node.parent
- if isinstance(node, nodes.document):
- return level
- if isinstance(node, nodes.section):
- level += 1
+ def visit_document(self, node):
+ """Dummy function to prevent spurious warnings.
- def visit_document(self, node):
- """Dummy function to prevent spurious warnings.
-
- cf. https://github.com/readthedocs/recommonmark/issues/177
- """
- pass
+ cf. https://github.com/readthedocs/recommonmark/issues/177
+ """
+ pass
Modified: trunk/docutils/docutils/parsers/rst/__init__.py
===================================================================
--- trunk/docutils/docutils/parsers/rst/__init__.py 2021-12-23 14:55:01 UTC (rev 8915)
+++ trunk/docutils/docutils/parsers/rst/__init__.py 2021-12-23 14:55:12 UTC (rev 8916)
@@ -325,7 +325,7 @@
self.state_machine = state_machine
def run(self):
- raise NotImplementedError('Must override run() is subclass.')
+ raise NotImplementedError('Must override run() in subclass.')
# Directive errors:
Modified: trunk/docutils/docutils/parsers/rst/directives/__init__.py
===================================================================
--- trunk/docutils/docutils/parsers/rst/directives/__init__.py 2021-12-23 14:55:01 UTC (rev 8915)
+++ trunk/docutils/docutils/parsers/rst/directives/__init__.py 2021-12-23 14:55:12 UTC (rev 8916)
@@ -429,10 +429,11 @@
(Directive option conversion function.)
Return `None`, if the argument evaluates to `False`.
+ Raise `ValueError` if importing the parser module fails.
"""
if not argument:
return None
try:
return parsers.get_parser_class(argument)
- except ImportError:
- raise ValueError('Unknown parser name "%s".'%argument)
+ except ImportError as err:
+ raise ValueError(str(err))
Modified: trunk/docutils/test/DocutilsTestSupport.py
===================================================================
--- trunk/docutils/test/DocutilsTestSupport.py 2021-12-23 14:55:01 UTC (rev 8915)
+++ trunk/docutils/test/DocutilsTestSupport.py 2021-12-23 14:55:12 UTC (rev 8916)
@@ -64,7 +64,7 @@
from docutils import frontend, nodes, statemachine, utils
from docutils.utils import urischemes
from docutils.transforms import universal
- from docutils.parsers import rst, recommonmark_wrapper
+ from docutils.parsers import rst
from docutils.parsers.rst import states, tableparser, roles, languages
from docutils.readers import standalone, pep
from docutils.statemachine import StringList, string2lines
@@ -522,11 +522,15 @@
"""Recommonmark-specific parser test case."""
- parser = recommonmark_wrapper.Parser()
+ try:
+ parser_class = docutils.parsers.get_parser_class('recommonmark')
+ parser = parser_class()
+ except ImportError:
+ parser_class = None
+ # recommonmark_wrapper.Parser
"""Parser shared by all RecommonmarkParserTestCases."""
- option_parser = frontend.OptionParser(
- components=(recommonmark_wrapper.Parser,))
+ option_parser = frontend.OptionParser(components=(parser_class,))
settings = option_parser.get_default_values()
settings.report_level = 5
settings.halt_level = 5
@@ -540,7 +544,7 @@
test_case_class = RecommonmarkParserTestCase
def generateTests(self, dict, dictname='totest'):
- if 'recommonmark' not in recommonmark_wrapper.Parser.supported:
+ if not RecommonmarkParserTestCase.parser_class:
return
# TODO: currently the tests are too version-specific
import recommonmark
Modified: trunk/docutils/test/test_parsers/test_recommonmark/test_misc.py
===================================================================
--- trunk/docutils/test/test_parsers/test_recommonmark/test_misc.py 2021-12-23 14:55:01 UTC (rev 8915)
+++ trunk/docutils/test/test_parsers/test_recommonmark/test_misc.py 2021-12-23 14:55:12 UTC (rev 8916)
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
# -*- coding: utf8 -*-
# :Copyright: © 2020 Günter Milde.
# :License: Released under the terms of the `2-Clause BSD license`_, in short:
@@ -23,7 +23,6 @@
from test_parsers import DocutilsTestSupport # must be imported before docutils
from docutils import core, utils
from docutils.core import publish_string
-from docutils.parsers import recommonmark_wrapper
sample_with_html = """\
A paragraph:
@@ -38,11 +37,12 @@
Final paragraph.
"""
+parser_class = DocutilsTestSupport.RecommonmarkParserTestCase.parser_class
skip_msg = 'optional module "recommonmark" not found'
class reCommonMarkParserTests(unittest.TestCase):
- @unittest.skipUnless(recommonmark_wrapper.CommonMarkParser, skip_msg)
+ @unittest.skipUnless(parser_class, skip_msg)
def test_raw_disabled(self):
output = publish_string(sample_with_html, parser_name='recommonmark',
settings_overrides={'warning_stream': '',
@@ -51,7 +51,7 @@
self.assertIn(b'<system_message', output)
self.assertIn(b'Raw content disabled.', output)
- @unittest.skipUnless(recommonmark_wrapper.CommonMarkParser, skip_msg)
+ @unittest.skipUnless(parser_class, skip_msg)
def test_raw_disabled_inline(self):
output = publish_string('foo <a href="uri">', parser_name='recommonmark',
settings_overrides={'warning_stream': '',
@@ -62,7 +62,7 @@
self.assertIn(b'Raw content disabled.', output)
- @unittest.skipUnless(recommonmark_wrapper.CommonMarkParser, skip_msg)
+ @unittest.skipUnless(parser_class, skip_msg)
def test_raw_disabled(self):
output = publish_string(sample_with_html, parser_name='recommonmark',
settings_overrides={'warning_stream': '',
@@ -72,13 +72,17 @@
self.assertNotIn(b'<raw>', output)
self.assertNotIn(b'<system_message', output)
- @unittest.skipIf(recommonmark_wrapper.CommonMarkParser,
+ @unittest.skipIf(parser_class,
'recommonmark_wrapper: parser found, fallback not used')
- def test_fallback_parser(self):
- output = publish_string(sample_with_html, parser_name='recommonmark',
- settings_overrides={'warning_stream': ''})
- self.assertIn(b'Python did not find the required module "recommonmark"',
- output)
+ def test_missing_parser(self):
+ try:
+ output = publish_string(sample_with_html,
+ parser_name='recommonmark')
+ except ImportError as err:
+ pass
+ self.assertIn(
+ b'requires the package https://pypi.org/project/recommonmark',
+ str(err))
if __name__ == '__main__':
unittest.main()
Modified: trunk/docutils/test/test_parsers/test_rst/test_directives/test__init__.py
===================================================================
--- trunk/docutils/test/test_parsers/test_rst/test_directives/test__init__.py 2021-12-23 14:55:01 UTC (rev 8915)
+++ trunk/docutils/test/test_parsers/test_rst/test_directives/test__init__.py 2021-12-23 14:55:12 UTC (rev 8916)
@@ -15,7 +15,9 @@
"""
from __future__ import absolute_import
+import unittest
+
if __name__ == '__main__':
import __init__
from test_parsers import DocutilsTestSupport
@@ -51,11 +53,16 @@
directives.parser_name('null'))
self.assertEqual(docutils.parsers.rst.Parser,
directives.parser_name('rst'))
- self.assertEqual(docutils.parsers.recommonmark_wrapper.Parser,
- directives.parser_name('markdown'))
self.assertRaises(ValueError, directives.parser_name, 'fantasy')
+ parser_class = DocutilsTestSupport.RecommonmarkParserTestCase.parser_class
+ skip_msg = 'optional module "recommonmark" not found'
+ @unittest.skipUnless(parser_class, skip_msg)
+ def test_external_parser_name(self):
+ self.assertEqual(docutils.parsers.recommonmark_wrapper.Parser,
+ directives.parser_name('recommonmark'))
+
if __name__ == '__main__':
import unittest
unittest.main()
Modified: trunk/docutils/test/test_parsers/test_rst/test_directives/test_include.py
===================================================================
--- trunk/docutils/test/test_parsers/test_rst/test_directives/test_include.py 2021-12-23 14:55:01 UTC (rev 8915)
+++ trunk/docutils/test/test_parsers/test_rst/test_directives/test_include.py 2021-12-23 14:55:12 UTC (rev 8916)
@@ -15,7 +15,6 @@
import __init__
from test_parsers import DocutilsTestSupport
from docutils.parsers.rst import states
-from docutils.parsers import recommonmark_wrapper
from docutils.utils.code_analyzer import with_pygments
if sys.version_info >= (3, 0):
@@ -76,7 +75,7 @@
# Parsing with Markdown (recommonmark) is an optional feature depending
# on 3rd-party modules:
-if recommonmark_wrapper.CommonMarkParser:
+if DocutilsTestSupport.RecommonmarkParserTestCase.parser_class:
markdown_parsing_result = """\
<section ids="title-1" names="title\\ 1">
<title>
@@ -94,12 +93,16 @@
."""
else:
markdown_parsing_result = """\
- <system_message level="2" source="test_parsers/test_rst/test_directives/include.md" type="WARNING">
+ <system_message level="3" line="3" source="test data" type="ERROR">
<paragraph>
- Missing dependency: MarkDown input is processed by a 3rd party parser but Python did not find the required module "recommonmark" (https://pypi.org/project/recommonmark/).\
-"""
+ Error in "include" directive:
+ invalid option value: (option: "parser"; value: \'markdown\')
+ Parser "markdown" missing. No module named recommonmark.parser.
+ Parsing "recommonmark" Markdown flavour requires the package https://pypi.org/project/recommonmark.
+ <literal_block xml:space="preserve">
+ .. include:: test_parsers/test_rst/test_directives/include.md
+ :parser: markdown"""
-
totest = {}
totest['include'] = [
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|