• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""
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 output and expected output 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 of 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
221