diff options
-rw-r--r-- | docs/change_log/release-3.3.md | 1 | ||||
-rw-r--r-- | markdown/blockprocessors.py | 2 | ||||
-rw-r--r-- | markdown/test_tools.py | 29 | ||||
-rw-r--r-- | markdown/util.py | 18 | ||||
-rw-r--r-- | tests/test_syntax/blocks/test_blockquotes.py | 51 |
5 files changed, 99 insertions, 2 deletions
diff --git a/docs/change_log/release-3.3.md b/docs/change_log/release-3.3.md index 752564d..339c98c 100644 --- a/docs/change_log/release-3.3.md +++ b/docs/change_log/release-3.3.md @@ -53,6 +53,7 @@ The following new features have been included in the 3.3 release: The following bug fixes are included in the 3.3 release: +* Avoid a `RecursionError` from deeply nested blockquotes (#799). * Fix issues with complex emphasis (#979). * Limitations of `attr_list` extension are Documented (#965). diff --git a/markdown/blockprocessors.py b/markdown/blockprocessors.py index 60baca1..88ebb62 100644 --- a/markdown/blockprocessors.py +++ b/markdown/blockprocessors.py @@ -276,7 +276,7 @@ class BlockQuoteProcessor(BlockProcessor): RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)') def test(self, parent, block): - return bool(self.RE.search(block)) + return bool(self.RE.search(block)) and not util.nearing_recursion_limit() def run(self, parent, blocks): block = blocks.pop(0) diff --git a/markdown/test_tools.py b/markdown/test_tools.py index be7bbf1..fb407bb 100644 --- a/markdown/test_tools.py +++ b/markdown/test_tools.py @@ -20,9 +20,10 @@ License: BSD (see LICENSE.md for details). """ import os +import sys import unittest import textwrap -from . import markdown +from . import markdown, util try: import tidylib @@ -73,6 +74,32 @@ class TestCase(unittest.TestCase): return textwrap.dedent(text).strip() +class recursionlimit: + """ + A context manager which temporarily modifies the Python recursion limit. + + The testing framework, coverage, etc. may add an arbitrary number of levels to the depth. To maintain consistency + in the tests, the current stack depth is determined when called, then added to the provided limit. + + Example usage: + + with recursionlimit(20): + # test code here + + See https://stackoverflow.com/a/50120316/866026 + """ + + def __init__(self, limit): + self.limit = util._get_stack_depth() + limit + self.old_limit = sys.getrecursionlimit() + + def __enter__(self): + sys.setrecursionlimit(self.limit) + + def __exit__(self, type, value, tb): + sys.setrecursionlimit(self.old_limit) + + ######################### # Legacy Test Framework # ######################### diff --git a/markdown/util.py b/markdown/util.py index a8db7bd..a49486b 100644 --- a/markdown/util.py +++ b/markdown/util.py @@ -26,6 +26,7 @@ from functools import wraps import warnings import xml.etree.ElementTree from .pep562 import Pep562 +from itertools import count try: from importlib import metadata @@ -156,6 +157,23 @@ def code_escape(text): return text +def _get_stack_depth(size=2): + """Get stack size for caller's frame. + See https://stackoverflow.com/a/47956089/866026 + """ + frame = sys._getframe(size) + + for size in count(size): + frame = frame.f_back + if not frame: + return size + + +def nearing_recursion_limit(): + """Return true if current stack depth is withing 100 of maximum limit.""" + return sys.getrecursionlimit() - _get_stack_depth() < 100 + + """ MISC AUXILIARY CLASSES ============================================================================= diff --git a/tests/test_syntax/blocks/test_blockquotes.py b/tests/test_syntax/blocks/test_blockquotes.py new file mode 100644 index 0000000..42eea33 --- /dev/null +++ b/tests/test_syntax/blocks/test_blockquotes.py @@ -0,0 +1,51 @@ +""" +Python Markdown + +A Python implementation of John Gruber's Markdown. + +Documentation: https://python-markdown.github.io/ +GitHub: https://github.com/Python-Markdown/markdown/ +PyPI: https://pypi.org/project/Markdown/ + +Started by Manfred Stienstra (http://www.dwerg.net/). +Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org). +Currently maintained by Waylan Limberg (https://github.com/waylan), +Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser). + +Copyright 2007-2020 The Python Markdown Project (v. 1.7 and later) +Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b) +Copyright 2004 Manfred Stienstra (the original version) + +License: BSD (see LICENSE.md for details). +""" + +from markdown.test_tools import TestCase, recursionlimit + + +class TestBlockquoteBlocks(TestCase): + + # TODO: Move legacy tests here + + def test_nesting_limit(self): + # Test that the nesting limit is within 100 levels of recursion limit. Future code changes could cause the + # recursion limit to need adjusted here. We need to acocunt for all of Markdown's internal calls. Finally, we + # need to account for the 100 level cushion which we are testing. + with recursionlimit(120): + self.assertMarkdownRenders( + '>>>>>>>>>>', + self.dedent( + """ + <blockquote> + <blockquote> + <blockquote> + <blockquote> + <blockquote> + <p>>>>>></p> + </blockquote> + </blockquote> + </blockquote> + </blockquote> + </blockquote> + """ + ) + ) |