Repository: Python-Markdown/markdown
Pull request #1260 (2022-05-24 19:04:05)

Remove previously deprecated objects.
This completely removes all objects which were deprecated back on version 3.0. We are now working on 3.4 so I think perhaps we can move forward with this. The one hesitation I have is that it is possible some older (unmaintained) extensions could stop working. However, given the time that has passed, and the fact that those extensions probably don't even support the new minimum Python version, I expect this to mostly be a non-issue.

So far, I have only made changes to the code. I still need to add something to the release notes.

waylan: After having made these changes it occurs to me that we could perhaps have raised some sort of DeprecationError for a version before completely removing these objects. Any thoughts?
mitya57: > After having made these changes it occurs to me that we could perhaps have raised some sort of DeprecationError for a version before completely removing these objects. Any thoughts?

You mean one intermediate step between DeprecationWarning and complete removal? I don't see any sense, warnings are quite visible and I don't remember any other projects doing this. Some projects have distinct PendingDeprecationWarning and DeprecationWarning, though.

Patch:
diff --git a/docs/change_log/release-3.4.md b/docs/change_log/release-3.4.md
index eb36b40..ade0ff8 100644
--- a/docs/change_log/release-3.4.md
+++ b/docs/change_log/release-3.4.md
@@ -30,6 +30,34 @@ markdown.markdown(src, extensions=[TableExtension(use_align_attribute=True)])
 
 In addition, tests were moved to the modern test environment.
 
+### Previously deprecated objects have been removed
+
+Various objects were deprecated in version 3.0 and began raising deprecation
+warnings (see the [version 3.0 release notes] for details). Any of those object
+which remained in version 3.3 have been removed from the codebase in version 3.4
+and will now raise errors. A summary of the objects are provided below.
+
+[version 3.0 release notes]: release-3.0.md
+
+| Deprecated Object                      | Replacement Object                  |
+|----------------------------------------|-------------------------------------|
+| `markdown.version`                     | `markdown.__version__`              |
+| `markdown.version_info`                | `markdown.__version_info__`         |
+| `markdown.util.etree`                  | `xml.etree.ElementTree`             |
+| `markdown.util.string_type`            | `str`                               |
+| `markdown.util.text_type`              | `str`                               |
+| `markdown.util.int2str`                | `chr`                               |
+| `markdown.util.iterrange`              | `range`                             |
+| `markdown.util.isBlockLevel`           | `markdown.Markdown.is_block_level`  |
+| `markdown.util.Processor().markdown`   | `markdown.util.Processor().md`      |
+| `markdown.util.Registry().__setitem__` | `markdown.util.Registry().register` |
+| `markdown.util.Registry().__delitem__` |`markdown.util.Registry().deregister`|
+| `markdown.util.Registry().add`         | `markdown.util.Registry().register` |
+
+In addition, the `md_globals` parameter of
+`Markdown.extensions.Extension.extendMarkdown()` is no longer recognized as a
+valid parameter and will raise an error if provided.
+
 ## New features
 
 The following new features have been included in the 3.4 release:
@@ -68,3 +96,4 @@ The following new features have been included in the 3.4 release:
 The following bug fixes are included in the 3.4 release:
 
 * Extension entry-points are only loaded if needed (#1216).
+* Added additional checks to the `<pre><code>` handling of `PrettifyTreeprocessor` (#1261, #1263).
diff --git a/markdown/__init__.py b/markdown/__init__.py
index 7760a0c..d88b1e9 100644
--- a/markdown/__init__.py
+++ b/markdown/__init__.py
@@ -19,37 +19,10 @@ Copyright 2004 Manfred Stienstra (the original version)
 License: BSD (see LICENSE.md for details).
 """
 
-import sys
-
-# TODO: Remove this check at some point in the future.
-# (also remove flake8's 'ignore E402' comments below)
-if sys.version_info[0] < 3:  # pragma: no cover
-    raise ImportError('A recent version of Python 3 is required.')
-
-from .core import Markdown, markdown, markdownFromFile  # noqa: E402
-from .__meta__ import __version__, __version_info__     # noqa: E402
-import warnings                                         # noqa: E402
+from .core import Markdown, markdown, markdownFromFile
+from .__meta__ import __version__, __version_info__  # noqa
 
 # For backward compatibility as some extensions expect it...
 from .extensions import Extension  # noqa
 
 __all__ = ['Markdown', 'markdown', 'markdownFromFile']
-
-__deprecated__ = {
-    "version": ("__version__", __version__),
-    "version_info": ("__version_info__", __version_info__)
-}
-
-
-def __getattr__(name):
-    """Get attribute."""
-
-    deprecated = __deprecated__.get(name)
-    if deprecated:
-        warnings.warn(
-            "'{}' is deprecated. Use '{}' instead.".format(name, deprecated[0]),
-            category=DeprecationWarning,
-            stacklevel=(3 if (3, 7) <= sys.version_info else 4)
-        )
-        return deprecated[1]
-    raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))
diff --git a/markdown/blockparser.py b/markdown/blockparser.py
index 39219fd..b0ca4b1 100644
--- a/markdown/blockparser.py
+++ b/markdown/blockparser.py
@@ -69,12 +69,6 @@ class BlockParser:
         self.state = State()
         self.md = md
 
-    @property
-    @util.deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
     def parseDocument(self, lines):
         """ Parse a markdown document into an ElementTree.
 
diff --git a/markdown/core.py b/markdown/core.py
index 3ed674a..f6a171c 100644
--- a/markdown/core.py
+++ b/markdown/core.py
@@ -122,7 +122,7 @@ class Markdown:
             if isinstance(ext, str):
                 ext = self.build_extension(ext, configs.get(ext, {}))
             if isinstance(ext, Extension):
-                ext._extendMarkdown(self)
+                ext.extendMarkdown(self)
                 logger.debug(
                     'Successfully loaded extension "%s.%s".'
                     % (ext.__class__.__module__, ext.__class__.__name__)
diff --git a/markdown/extensions/__init__.py b/markdown/extensions/__init__.py
index 4712e25..2d8d72a 100644
--- a/markdown/extensions/__init__.py
+++ b/markdown/extensions/__init__.py
@@ -19,7 +19,6 @@ Copyright 2004 Manfred Stienstra (the original version)
 License: BSD (see LICENSE.md for details).
 """
 
-import warnings
 from ..util import parseBoolValue
 
 
@@ -70,24 +69,6 @@ class Extension:
         for key, value in items:
             self.setConfig(key, value)
 
-    def _extendMarkdown(self, *args):
-        """ Private wrapper around extendMarkdown. """
-        md = args[0]
-        try:
-            self.extendMarkdown(md)
-        except TypeError as e:
-            if "missing 1 required positional argument" in str(e):
-                # Must be a 2.x extension. Pass in a dumby md_globals.
-                self.extendMarkdown(md, {})
-                warnings.warn(
-                    "The 'md_globals' parameter of '{}.{}.extendMarkdown' is "
-                    "deprecated.".format(self.__class__.__module__, self.__class__.__name__),
-                    category=DeprecationWarning,
-                    stacklevel=2
-                )
-            else:
-                raise
-
     def extendMarkdown(self, md):
         """
         Add the various processors and patterns to the Markdown Instance.
@@ -98,8 +79,6 @@ class Extension:
 
         * md: The Markdown instance.
 
-        * md_globals: Global variables in the markdown module namespace.
-
         """
         raise NotImplementedError(
             'Extension "%s.%s" must define an "extendMarkdown"'
diff --git a/markdown/extensions/smarty.py b/markdown/extensions/smarty.py
index 894805f..c4bfd58 100644
--- a/markdown/extensions/smarty.py
+++ b/markdown/extensions/smarty.py
@@ -83,7 +83,7 @@ smartypants.py license:
 from . import Extension
 from ..inlinepatterns import HtmlInlineProcessor, HTML_RE
 from ..treeprocessors import InlineProcessor
-from ..util import Registry, deprecated
+from ..util import Registry
 
 
 # Constants for quote education.
@@ -155,12 +155,6 @@ class SubstituteTextPattern(HtmlInlineProcessor):
         self.replace = replace
         self.md = md
 
-    @property
-    @deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
     def handleMatch(self, m, data):
         result = ''
         for part in self.replace:
diff --git a/markdown/inlinepatterns.py b/markdown/inlinepatterns.py
index eb74f49..eb313bd 100644
--- a/markdown/inlinepatterns.py
+++ b/markdown/inlinepatterns.py
@@ -211,12 +211,6 @@ class Pattern:  # pragma: no cover
 
         self.md = md
 
-    @property
-    @util.deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
     def getCompiledRegExp(self):
         """ Return a compiled regular expression. """
         return self.compiled_re
diff --git a/markdown/treeprocessors.py b/markdown/treeprocessors.py
index a475fde..7767347 100644
--- a/markdown/treeprocessors.py
+++ b/markdown/treeprocessors.py
@@ -75,12 +75,6 @@ class InlineProcessor(Treeprocessor):
         self.inlinePatterns = md.inlinePatterns
         self.ancestors = []
 
-    @property
-    @util.deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
     def __makePlaceholder(self, type):
         """ Generate a placeholder """
         id = "%04d" % len(self.stashed_nodes)
@@ -433,4 +427,7 @@ class PrettifyTreeprocessor(Treeprocessor):
         pres = root.iter('pre')
         for pre in pres:
             if len(pre) and pre[0].tag == 'code':
-                pre[0].text = util.AtomicString(pre[0].text.rstrip() + '\n')
+                code = pre[0]
+                # Only prettify code containing text only
+                if not len(code) and code.text is not None:
+                    code.text = util.AtomicString(code.text.rstrip() + '\n')
diff --git a/markdown/util.py b/markdown/util.py
index 6d3a195..e6b08e5 100644
--- a/markdown/util.py
+++ b/markdown/util.py
@@ -22,22 +22,11 @@ License: BSD (see LICENSE.md for details).
 import re
 import sys
 import warnings
-import xml.etree.ElementTree
 from collections import namedtuple
 from functools import wraps, lru_cache
 from itertools import count
 
 
-# TODO: Remove deprecated variables in a future release.
-__deprecated__ = {
-    'etree': ('xml.etree.ElementTree', xml.etree.ElementTree),
-    'string_type': ('str', str),
-    'text_type': ('str', str),
-    'int2str': ('chr', chr),
-    'iterrange': ('range', range)
-}
-
-
 """
 Constants you might want to modify
 -----------------------------------------------------------------------------
@@ -121,15 +110,6 @@ def deprecated(message, stacklevel=2):
     return wrapper
 
 
-@deprecated("Use 'Markdown.is_block_level' instead.")
-def isBlockLevel(tag):
-    """Check if the tag is a block level HTML tag."""
-    if isinstance(tag, str):
-        return tag.lower().rstrip('/') in BLOCK_LEVEL_ELEMENTS
-    # Some ElementTree tags are not strings, so return False.
-    return False
-
-
 def parseBoolValue(value, fail_on_errors=True, preserve_none=False):
     """Parses a string representing bool value. If parsing was successful,
        returns True or False. If preserve_none=True, returns True, False,
@@ -191,12 +171,6 @@ class Processor:
     def __init__(self, md=None):
         self.md = md
 
-    @property
-    @deprecated("Use 'md' instead.")
-    def markdown(self):
-        # TODO: remove this later
-        return self.md
-
 
 class HtmlStash:
     """
@@ -382,83 +356,3 @@ class Registry:
         if not self._is_sorted:
             self._priority.sort(key=lambda item: item.priority, reverse=True)
             self._is_sorted = True
-
-    # Deprecated Methods which provide a smooth transition from OrderedDict
-
-    @deprecated('Use the `register` method instead.')
-    def __setitem__(self, key, value):
-        """ Register item with priority 5 less than lowest existing priority. """
-        if isinstance(key, str):
-            if key in self:
-                # Key already exists, replace without altering priority
-                self._data[key] = value
-                return
-            if len(self) == 0:
-                # This is the first item. Set priority to 50.
-                priority = 50
-            else:
-                self._sort()
-                priority = self._priority[-1].priority - 5
-            self.register(value, key, priority)
-        else:
-            raise TypeError
-
-    @deprecated('Use the `deregister` method instead.')
-    def __delitem__(self, key):
-        """ Deregister an item by name. """
-        if key in self:
-            self.deregister(key)
-        else:
-            raise KeyError('Cannot delete key {}, not registered.'.format(key))
-
-    @deprecated('Use the `register` method instead.')
-    def add(self, key, value, location):
-        """ Register a key by location. """
-        if len(self) == 0:
-            # This is the first item. Set priority to 50.
-            priority = 50
-        elif location == '_begin':
-            self._sort()
-            # Set priority 5 greater than highest existing priority
-            priority = self._priority[0].priority + 5
-        elif location == '_end':
-            self._sort()
-            # Set priority 5 less than lowest existing priority
-            priority = self._priority[-1].priority - 5
-        elif location.startswith('<') or location.startswith('>'):
-            # Set priority halfway between existing priorities.
-            i = self.get_index_for_name(location[1:])
-            if location.startswith('<'):
-                after = self._priority[i].priority
-                if i > 0:
-                    before = self._priority[i-1].priority
-                else:
-                    # Location is first item`
-                    before = after + 10
-            else:
-                # location.startswith('>')
-                before = self._priority[i].priority
-                if i < len(self) - 1:
-                    after = self._priority[i+1].priority
-                else:
-                    # location is last item
-                    after = before - 10
-            priority = before - ((before - after) / 2)
-        else:
-            raise ValueError('Not a valid location: "%s". Location key '
-                             'must start with a ">" or "<".' % location)
-        self.register(value, key, priority)
-
-
-def __getattr__(name):
-    """Get attribute."""
-
-    deprecated = __deprecated__.get(name)
-    if deprecated:
-        warnings.warn(
-            "'{}' is deprecated. Use '{}' instead.".format(name, deprecated[0]),
-            category=DeprecationWarning,
-            stacklevel=(3 if (3, 7) <= sys.version_info else 4)
-        )
-        return deprecated[1]
-    raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))


Test patch:
diff --git a/tests/test_apis.py b/tests/test_apis.py
index 7a3c735..1a38be6 100644
--- a/tests/test_apis.py
+++ b/tests/test_apis.py
@@ -316,50 +316,16 @@ class RegistryTests(unittest.TestCase):
         r = markdown.util.Registry()
         with self.assertRaises(TypeError):
             r[0] = 'a'
-        # TODO: restore this when deprecated __setitem__ is removed.
-        # with self.assertRaises(TypeError):
-        #     r['a'] = 'a'
-        # TODO: remove this when deprecated __setitem__ is removed.
-        with warnings.catch_warnings(record=True) as w:
-            warnings.simplefilter("always")
-
-            r['a'] = Item('a')
-            self.assertEqual(list(r), ['a'])
-            r['b'] = Item('b')
-            self.assertEqual(list(r), ['a', 'b'])
-            r['a'] = Item('a1')
-            self.assertEqual(list(r), ['a1', 'b'])
-
-            # Check the warnings
-            self.assertEqual(len(w), 3)
-            self.assertTrue(all(issubclass(x.category, DeprecationWarning) for x in w))
+        with self.assertRaises(TypeError):
+            r['a'] = 'a'
 
     def testRegistryDelItem(self):
         r = markdown.util.Registry()
         r.register(Item('a'), 'a', 20)
-        with self.assertRaises(KeyError):
+        with self.assertRaises(TypeError):
             del r[0]
-        # TODO: restore this when deprecated __del__ is removed.
-        # with self.assertRaises(TypeError):
-        #     del r['a']
-        # TODO: remove this when deprecated __del__ is removed.
-        with warnings.catch_warnings(record=True) as w:
-            warnings.simplefilter("always")
-
-            r.register(Item('b'), 'b', 15)
-            r.register(Item('c'), 'c', 10)
-            del r['b']
-            self.assertEqual(list(r), ['a', 'c'])
+        with self.assertRaises(TypeError):
             del r['a']
-            self.assertEqual(list(r), ['c'])
-            with self.assertRaises(KeyError):
-                del r['badname']
-            del r['c']
-            self.assertEqual(list(r), [])
-
-            # Check the warnings
-            self.assertEqual(len(w), 4)
-            self.assertTrue(all(issubclass(x.category, DeprecationWarning) for x in w))
 
     def testRegistrySlice(self):
         r = markdown.util.Registry()
@@ -390,39 +356,6 @@ class RegistryTests(unittest.TestCase):
         self.assertEqual(len(r), 2)
         self.assertEqual(list(r), ['b2', 'a'])
 
-    def testRegistryDeprecatedAdd(self):
-        with warnings.catch_warnings(record=True) as w:
-            warnings.simplefilter("always")
-
-            r = markdown.util.Registry()
-            # Add first item
-            r.add('c', Item('c'), '_begin')
-            self.assertEqual(list(r), ['c'])
-            # Added to beginning
-            r.add('b', Item('b'), '_begin')
-            self.assertEqual(list(r), ['b', 'c'])
-            # Add before first item
-            r.add('a', Item('a'), '<b')
-            self.assertEqual(list(r), ['a', 'b', 'c'])
-            # Add before non-first item
-            r.add('a1', Item('a1'), '<b')
-            self.assertEqual(list(r), ['a', 'a1', 'b', 'c'])
-            # Add after non-last item
-            r.add('b1', Item('b1'), '>b')
-            self.assertEqual(list(r), ['a', 'a1', 'b', 'b1', 'c'])
-            # Add after last item
-            r.add('d', Item('d'), '>c')
-            self.assertEqual(list(r), ['a', 'a1', 'b', 'b1', 'c', 'd'])
-            # Add to end
-            r.add('e', Item('e'), '_end')
-            self.assertEqual(list(r), ['a', 'a1', 'b', 'b1', 'c', 'd', 'e'])
-            with self.assertRaises(ValueError):
-                r.add('f', Item('f'), 'badlocation')
-
-            # Check the warnings
-            self.assertEqual(len(w), 8)
-            self.assertTrue(all(issubclass(x.category, DeprecationWarning) for x in w))
-
 
 class TestErrors(unittest.TestCase):
     """ Test Error Reporting. """
@@ -525,6 +458,43 @@ class testElementTailTests(unittest.TestCase):
         self.assertEqual(br.tail, "\n")
 
 
+class testElementPreCodeTests(unittest.TestCase):
+    """ Element PreCode Tests """
+    def setUp(self):
+        md = markdown.Markdown()
+        self.pretty = markdown.treeprocessors.PrettifyTreeprocessor(md)
+
+    def prettify(self, xml):
+        root = etree.fromstring(xml)
+        self.pretty.run(root)
+        return etree.tostring(root, encoding="unicode", short_empty_elements=False)
+
+    def testPreCodeEmpty(self):
+        xml = "<pre><code></code></pre>"
+        expected = "<pre><code></code></pre>\n"
+        self.assertEqual(expected, self.prettify(xml))
+
+    def testPreCodeWithChildren(self):
+        xml = "<pre><code> <span /></code></pre>"
+        expected = "<pre><code> <span></span></code></pre>\n"
+        self.assertEqual(expected, self.prettify(xml))
+
+    def testPreCodeWithSpaceOnly(self):
+        xml = "<pre><code> </code></pre>"
+        expected = "<pre><code>\n</code></pre>\n"
+        self.assertEqual(expected, self.prettify(xml))
+
+    def testPreCodeWithText(self):
+        xml = "<pre><code> hello</code></pre>"
+        expected = "<pre><code> hello\n</code></pre>\n"
+        self.assertEqual(expected, self.prettify(xml))
+
+    def testPreCodeWithTrailingSpace(self):
+        xml = "<pre><code> hello </code></pre>"
+        expected = "<pre><code> hello\n</code></pre>\n"
+        self.assertEqual(expected, self.prettify(xml))
+
+
 class testSerializers(unittest.TestCase):
     """ Test the html and xhtml serializers. """
 
@@ -985,42 +955,3 @@ Some +test+ and a [+link+](http://test.com)
 
         self.md.reset()
         self.assertEqual(self.md.convert(test), result)
-
-
-class TestGeneralDeprecations(unittest.TestCase):
-    """Test general deprecations."""
-
-    def test_version_deprecation(self):
-        """Test that version is deprecated."""
-
-        with warnings.catch_warnings(record=True) as w:
-            # Cause all warnings to always be triggered.
-            warnings.simplefilter("always")
-            # Trigger a warning.
-            version = markdown.version
-            # Verify some things
-            self.assertEqual(len(w), 1)
-            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
-            self.assertEqual(version, markdown.__version__)
-
-    def test_version_info_deprecation(self):
-        """Test that version info is deprecated."""
-
-        with warnings.catch_warnings(record=True) as w:
-            # Cause all warnings to always be triggered.
-            warnings.simplefilter("always")
-            # Trigger a warning.
-            version_info = markdown.version_info
-            # Verify some things
-            self.assertEqual(len(w), 1)
-            self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
-            self.assertEqual(version_info, markdown.__version_info__)
-
-    def test_deprecation_wrapper_dir(self):
-        """Tests the `__dir__` attribute of the class as it replaces the module's."""
-
-        dir_attr = dir(markdown)
-        self.assertNotIn('version', dir_attr)
-        self.assertIn('__version__', dir_attr)
-        self.assertNotIn('version_info', dir_attr)
-        self.assertIn('__version_info__', dir_attr)