Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/change_log/release-3.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
2 changes: 1 addition & 1 deletion markdown/blockprocessors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 28 additions & 1 deletion markdown/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
"""

import os
import sys
import unittest
import textwrap
from . import markdown
from . import markdown, util

try:
import tidylib
Expand Down Expand Up @@ -73,6 +74,32 @@ def dedent(self, text):
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 #
#########################
Expand Down
18 changes: 18 additions & 0 deletions markdown/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import warnings
import xml.etree.ElementTree
from .pep562 import Pep562
from itertools import count

try:
from importlib import metadata
Expand Down Expand Up @@ -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
=============================================================================
Expand Down
51 changes: 51 additions & 0 deletions tests/test_syntax/blocks/test_blockquotes.py
Original file line number Diff line number Diff line change
@@ -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>&gt;&gt;&gt;&gt;&gt;</p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
"""
)
)