blob: 21ae1a7de03c22cb0abc4aca1ca38133bdee596a [file] [log] [blame]
Yu-Ping Wu6a8f3a22021-11-24 00:45:03 +00001"""
2Python Markdown
3
4A Python implementation of John Gruber's Markdown.
5
6Documentation: https://python-markdown.github.io/
7GitHub: https://github.com/Python-Markdown/markdown/
8PyPI: https://pypi.org/project/Markdown/
9
10Started by Manfred Stienstra (http://www.dwerg.net/).
11Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
12Currently maintained by Waylan Limberg (https://github.com/waylan),
13Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
14
15Copyright 2007-2018 The Python Markdown Project (v. 1.7 and later)
16Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
17Copyright 2004 Manfred Stienstra (the original version)
18
19License: BSD (see LICENSE.md for details).
20"""
21
22import os
23import sys
24import unittest
25import textwrap
26from . import markdown, Markdown, util
27
28try:
29 import tidylib
30except ImportError:
31 tidylib = None
32
33__all__ = ['TestCase', 'LegacyTestCase', 'Kwargs']
34
35
36class TestCase(unittest.TestCase):
37 """
38 A unittest.TestCase subclass with helpers for testing Markdown output.
39
40 Define `default_kwargs` as a dict of keywords to pass to Markdown for each
41 test. The defaults can be overridden on individual tests.
42
43 The `assertMarkdownRenders` method accepts the source text, the expected
44 output, and any keywords to pass to Markdown. The `default_kwargs` are used
45 except where overridden by `kwargs`. The ouput and expected ouput are passed
46 to `TestCase.assertMultiLineEqual`. An AssertionError is raised with a diff
47 if the actual output does not equal the expected output.
48
49 The `dedent` method is available to dedent triple-quoted strings if
50 necessary.
51
52 In all other respects, behaves as unittest.TestCase.
53 """
54
55 default_kwargs = {}
56
57 def assertMarkdownRenders(self, source, expected, expected_attrs=None, **kwargs):
58 """
59 Test that source Markdown text renders to expected output with given keywords.
60
61 `expected_attrs` accepts a dict. Each key should be the name of an attribute
62 on the `Markdown` instance and the value should be the expected value after
63 the source text is parsed by Markdown. After the expected output is tested,
64 the expected value for each attribute is compared against the actual
65 attribute of the `Markdown` instance using `TestCase.assertEqual`.
66 """
67
68 expected_attrs = expected_attrs or {}
69 kws = self.default_kwargs.copy()
70 kws.update(kwargs)
71 md = Markdown(**kws)
72 output = md.convert(source)
73 self.assertMultiLineEqual(output, expected)
74 for key, value in expected_attrs.items():
75 self.assertEqual(getattr(md, key), value)
76
77 def dedent(self, text):
78 """
79 Dedent text.
80 """
81
82 # TODO: If/when actual output ends with a newline, then use:
83 # return textwrap.dedent(text.strip('/n'))
84 return textwrap.dedent(text).strip()
85
86
87class recursionlimit:
88 """
89 A context manager which temporarily modifies the Python recursion limit.
90
91 The testing framework, coverage, etc. may add an arbitrary number of levels to the depth. To maintain consistency
92 in the tests, the current stack depth is determined when called, then added to the provided limit.
93
94 Example usage:
95
96 with recursionlimit(20):
97 # test code here
98
99 See https://stackoverflow.com/a/50120316/866026
100 """
101
102 def __init__(self, limit):
103 self.limit = util._get_stack_depth() + limit
104 self.old_limit = sys.getrecursionlimit()
105
106 def __enter__(self):
107 sys.setrecursionlimit(self.limit)
108
109 def __exit__(self, type, value, tb):
110 sys.setrecursionlimit(self.old_limit)
111
112
113#########################
114# Legacy Test Framework #
115#########################
116
117
118class Kwargs(dict):
119 """ A dict like class for holding keyword arguments. """
120 pass
121
122
123def _normalize_whitespace(text):
124 """ Normalize whitespace for a string of html using tidylib. """
125 output, errors = tidylib.tidy_fragment(text, options={
126 'drop_empty_paras': 0,
127 'fix_backslash': 0,
128 'fix_bad_comments': 0,
129 'fix_uri': 0,
130 'join_styles': 0,
131 'lower_literals': 0,
132 'merge_divs': 0,
133 'output_xhtml': 1,
134 'quote_ampersand': 0,
135 'newline': 'LF'
136 })
137 return output
138
139
140class LegacyTestMeta(type):
141 def __new__(cls, name, bases, dct):
142
143 def generate_test(infile, outfile, normalize, kwargs):
144 def test(self):
145 with open(infile, encoding="utf-8") as f:
146 input = f.read()
147 with open(outfile, encoding="utf-8") as f:
148 # Normalize line endings
149 # (on Windows, git may have altered line endings).
150 expected = f.read().replace("\r\n", "\n")
151 output = markdown(input, **kwargs)
152 if tidylib and normalize:
153 try:
154 expected = _normalize_whitespace(expected)
155 output = _normalize_whitespace(output)
156 except OSError:
157 self.skipTest("Tidylib's c library not available.")
158 elif normalize:
159 self.skipTest('Tidylib not available.')
160 self.assertMultiLineEqual(output, expected)
161 return test
162
163 location = dct.get('location', '')
164 exclude = dct.get('exclude', [])
165 normalize = dct.get('normalize', False)
166 input_ext = dct.get('input_ext', '.txt')
167 output_ext = dct.get('output_ext', '.html')
168 kwargs = dct.get('default_kwargs', Kwargs())
169
170 if os.path.isdir(location):
171 for file in os.listdir(location):
172 infile = os.path.join(location, file)
173 if os.path.isfile(infile):
174 tname, ext = os.path.splitext(file)
175 if ext == input_ext:
176 outfile = os.path.join(location, tname + output_ext)
177 tname = tname.replace(' ', '_').replace('-', '_')
178 kws = kwargs.copy()
179 if tname in dct:
180 kws.update(dct[tname])
181 test_name = 'test_%s' % tname
182 if tname not in exclude:
183 dct[test_name] = generate_test(infile, outfile, normalize, kws)
184 else:
185 dct[test_name] = unittest.skip('Excluded')(lambda: None)
186
187 return type.__new__(cls, name, bases, dct)
188
189
190class LegacyTestCase(unittest.TestCase, metaclass=LegacyTestMeta):
191 """
192 A `unittest.TestCase` subclass for running Markdown's legacy file-based tests.
193
194 A subclass should define various properties which point to a directory of
195 text-based test files and define various behaviors/defaults for those tests.
196 The following properties are supported:
197
198 location: A path to the directory fo test files. An absolute path is preferred.
199 exclude: A list of tests to exclude. Each test name should comprise the filename
200 without an extension.
201 normalize: A boolean value indicating if the HTML should be normalized.
202 Default: `False`.
203 input_ext: A string containing the file extension of input files. Default: `.txt`.
204 ouput_ext: A string containing the file extension of expected output files.
205 Default: `html`.
206 default_kwargs: A `Kwargs` instance which stores the default set of keyword
207 arguments for all test files in the directory.
208
209 In addition, properties can be defined for each individual set of test files within
210 the directory. The property should be given the name of the file without the file
211 extension. Any spaces and dashes in the filename should be replaced with
212 underscores. The value of the property should be a `Kwargs` instance which
213 contains the keyword arguments that should be passed to `Markdown` for that
214 test file. The keyword arguments will "update" the `default_kwargs`.
215
216 When the class instance is created, it will walk the given directory and create
217 a separate unitttest for each set of test files using the naming scheme:
218 `test_filename`. One unittest will be run for each set of input and output files.
219 """
220 pass