Skip to content
Open
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
4 changes: 4 additions & 0 deletions lib/markdown2.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,9 @@ def __init__(
self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)
self.cli = cli

self._reset_tables()

def _reset_tables(self):
self._escape_table = g_escape_table.copy()
self._code_table = {}
if "smarty-pants" in self.extras:
Expand All @@ -395,6 +398,7 @@ def reset(self):
self.html_spans = {}
self.list_level = 0
self.extras = self._instance_extras.copy()
self._reset_tables()
self._setup_extras()
self._toc = []

Expand Down
63 changes: 63 additions & 0 deletions test/test_reuse_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Tests for reused Markdown instance state isolation (issue #763)."""

import sys
import time
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent / "lib"))
import markdown2 # noqa: E402


class TestReuseInstanceIsolation(unittest.TestCase):
"""Each convert() call on a reused instance must behave like a fresh one."""

def test_escape_table_reset_between_calls(self):
"""Backslash-escaped text must not be affected by a previous convert()."""
md = markdown2.Markdown()
md.convert("[a link](https://example.com/page)")
result_reused = md.convert(r"Type \https://example.com/page to see it.")
result_fresh = markdown2.Markdown().convert(r"Type \https://example.com/page to see it.")
self.assertEqual(result_reused, result_fresh)

def test_code_table_reset_between_calls(self):
"""Code spans from earlier documents must not leak into later ones."""
md = markdown2.Markdown()
md.convert("`first_code_span`")
# The hash for first_code_span must not persist and corrupt the new doc
result_reused = md.convert("plain `second_code_span` text")
result_fresh = markdown2.Markdown().convert("plain `second_code_span` text")
self.assertEqual(result_reused, result_fresh)

def test_performance_does_not_degrade(self):
"""Processing time per document must not grow with the number of documents."""
md = markdown2.Markdown()
doc = "[link{}](https://example.com) `code{}`\n"

# warm up
for i in range(5):
md.convert(doc.format(i, i))

t_start_early = time.monotonic()
for i in range(5, 10):
md.convert(doc.format(i, i))
t_early = time.monotonic() - t_start_early

# process 50 more documents to accumulate stale table entries
for i in range(10, 60):
md.convert(doc.format(i, i))

t_start_late = time.monotonic()
for i in range(60, 65):
md.convert(doc.format(i, i))
t_late = time.monotonic() - t_start_late

# With the fix, late processing is not significantly slower than early.
# Use max(..., 1e-5) so the comparison is valid when both times round
# to 0.0 on fast CI runners (Windows).
self.assertLess(t_late, max(t_early, 1e-5) * 10,
f"Performance degraded: early={t_early*1000:.1f}ms late={t_late*1000:.1f}ms")


if __name__ == "__main__":
unittest.main()
Loading