summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsaac Muse <faceless.shop@gmail.com>2018-12-22 13:50:04 -0700
committerWaylan Limberg <waylan.limberg@icloud.com>2018-12-22 15:50:04 -0500
commitab24c2357dfd98540d5196470164bf49b5e6d9f3 (patch)
treef785639a9045479d1ae24ce25482597395784834
parentaa6667e0c58cd01caddc3a5aa47ee3f395ef5b8e (diff)
downloadpython-markdown-ab24c2357dfd98540d5196470164bf49b5e6d9f3.tar.gz
Use a PEP562 implementation for deprecating attributes (#757)
Use a vendored Pep562 backport to simulate Python 3.7's new PEP 562 feature. For Python3.7 and later, default to the official implementation.
-rw-r--r--.coveragerc1
-rw-r--r--markdown/__init__.py41
-rw-r--r--markdown/pep562.py246
-rw-r--r--markdown/util.py39
-rw-r--r--tests/test_apis.py4
-rw-r--r--tox.ini1
6 files changed, 271 insertions, 61 deletions
diff --git a/.coveragerc b/.coveragerc
index c785d90..c28a02a 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -3,3 +3,4 @@ omit=
*site-packages*
tests/*
markdown/test_tools.py
+ markdown/pep562.py
diff --git a/markdown/__init__.py b/markdown/__init__.py
index ea2bbf4..67b76f1 100644
--- a/markdown/__init__.py
+++ b/markdown/__init__.py
@@ -23,8 +23,10 @@ License: BSD (see LICENSE.md for details).
from __future__ import absolute_import
from __future__ import unicode_literals
from .core import Markdown, markdown, markdownFromFile
-from .util import ModuleWrap, deprecated
+from .util import PY37
+from .pep562 import Pep562
from pkg_resources.extern import packaging
+import warnings
# For backward compatibility as some extensions expect it...
from .extensions import Extension # noqa
@@ -64,28 +66,25 @@ def _get_version(): # pragma: no cover
__version__ = _get_version()
+__deprecated__ = {
+ "version": ("__version__", __version__),
+ "version_info": ("__version_info__", __version_info__)
+}
-class _ModuleWrap(ModuleWrap):
- """
- Wrap module so that we can control `__getattribute__` and `__dir__` logic.
- Treat `version` and `version_info` as deprecated properties.
- Provides backward-compatabillity with <3.0 versions.
- """
+def __getattr__(name):
+ """Get attribute."""
- @property
- @deprecated("Use '__version__' instead.", stacklevel=3)
- def version(self):
- """Get deprecated version."""
+ deprecated = __deprecated__.get(name)
+ if deprecated:
+ warnings.warn(
+ "'{}' is deprecated. Use '{}' instead.".format(name, deprecated[0]),
+ category=DeprecationWarning,
+ stacklevel=(3 if PY37 else 4)
+ )
+ return deprecated[1]
+ raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))
- return __version__
- @property
- @deprecated("Use '__version_info__' instead.", stacklevel=3)
- def version_info(self):
- """Get deprecated version info."""
-
- return __version_info__
-
-
-_ModuleWrap(__name__)
+if not PY37:
+ Pep562(__name__)
diff --git a/markdown/pep562.py b/markdown/pep562.py
new file mode 100644
index 0000000..8add471
--- /dev/null
+++ b/markdown/pep562.py
@@ -0,0 +1,246 @@
+"""
+Backport of PEP 562.
+
+https://pypi.org/search/?q=pep562
+
+Licensed under MIT
+Copyright (c) 2018 Isaac Muse <isaacmuse@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
+and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
+"""
+from __future__ import unicode_literals
+import sys
+from collections import namedtuple
+import re
+
+__all__ = ('Pep562',)
+
+RE_VER = re.compile(
+ r'''(?x)
+ (?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<micro>\d+))?
+ (?:(?P<type>a|b|rc)(?P<pre>\d+))?
+ (?:\.post(?P<post>\d+))?
+ (?:\.dev(?P<dev>\d+))?
+ '''
+)
+
+REL_MAP = {
+ ".dev": "",
+ ".dev-alpha": "a",
+ ".dev-beta": "b",
+ ".dev-candidate": "rc",
+ "alpha": "a",
+ "beta": "b",
+ "candidate": "rc",
+ "final": ""
+}
+
+DEV_STATUS = {
+ ".dev": "2 - Pre-Alpha",
+ ".dev-alpha": "2 - Pre-Alpha",
+ ".dev-beta": "2 - Pre-Alpha",
+ ".dev-candidate": "2 - Pre-Alpha",
+ "alpha": "3 - Alpha",
+ "beta": "4 - Beta",
+ "candidate": "4 - Beta",
+ "final": "5 - Production/Stable"
+}
+
+PRE_REL_MAP = {"a": 'alpha', "b": 'beta', "rc": 'candidate'}
+
+
+class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre", "post", "dev"])):
+ """
+ Get the version (PEP 440).
+
+ A biased approach to the PEP 440 semantic version.
+
+ Provides a tuple structure which is sorted for comparisons `v1 > v2` etc.
+ (major, minor, micro, release type, pre-release build, post-release build, development release build)
+ Release types are named in is such a way they are comparable with ease.
+ Accessors to check if a development, pre-release, or post-release build. Also provides accessor to get
+ development status for setup files.
+
+ How it works (currently):
+
+ - You must specify a release type as either `final`, `alpha`, `beta`, or `candidate`.
+ - To define a development release, you can use either `.dev`, `.dev-alpha`, `.dev-beta`, or `.dev-candidate`.
+ The dot is used to ensure all development specifiers are sorted before `alpha`.
+ You can specify a `dev` number for development builds, but do not have to as implicit development releases
+ are allowed.
+ - You must specify a `pre` value greater than zero if using a prerelease as this project (not PEP 440) does not
+ allow implicit prereleases.
+ - You can optionally set `post` to a value greater than zero to make the build a post release. While post releases
+ are technically allowed in prereleases, it is strongly discouraged, so we are rejecting them. It should be
+ noted that we do not allow `post0` even though PEP 440 does not restrict this. This project specifically
+ does not allow implicit post releases.
+ - It should be noted that we do not support epochs `1!` or local versions `+some-custom.version-1`.
+
+ Acceptable version releases:
+
+ ```
+ Version(1, 0, 0, "final") 1.0
+ Version(1, 2, 0, "final") 1.2
+ Version(1, 2, 3, "final") 1.2.3
+ Version(1, 2, 0, ".dev-alpha", pre=4) 1.2a4
+ Version(1, 2, 0, ".dev-beta", pre=4) 1.2b4
+ Version(1, 2, 0, ".dev-candidate", pre=4) 1.2rc4
+ Version(1, 2, 0, "final", post=1) 1.2.post1
+ Version(1, 2, 3, ".dev") 1.2.3.dev0
+ Version(1, 2, 3, ".dev", dev=1) 1.2.3.dev1
+ ```
+
+ """
+
+ def __new__(cls, major, minor, micro, release="final", pre=0, post=0, dev=0):
+ """Validate version info."""
+
+ # Ensure all parts are positive integers.
+ for value in (major, minor, micro, pre, post):
+ if not (isinstance(value, int) and value >= 0):
+ raise ValueError("All version parts except 'release' should be integers.")
+
+ if release not in REL_MAP:
+ raise ValueError("'{}' is not a valid release type.".format(release))
+
+ # Ensure valid pre-release (we do not allow implicit pre-releases).
+ if ".dev-candidate" < release < "final":
+ if pre == 0:
+ raise ValueError("Implicit pre-releases not allowed.")
+ elif dev:
+ raise ValueError("Version is not a development release.")
+ elif post:
+ raise ValueError("Post-releases are not allowed with pre-releases.")
+
+ # Ensure valid development or development/pre release
+ elif release < "alpha":
+ if release > ".dev" and pre == 0:
+ raise ValueError("Implicit pre-release not allowed.")
+ elif post:
+ raise ValueError("Post-releases are not allowed with pre-releases.")
+
+ # Ensure a valid normal release
+ else:
+ if pre:
+ raise ValueError("Version is not a pre-release.")
+ elif dev:
+ raise ValueError("Version is not a development release.")
+
+ return super(Version, cls).__new__(cls, major, minor, micro, release, pre, post, dev)
+
+ def _is_pre(self):
+ """Is prerelease."""
+
+ return self.pre > 0
+
+ def _is_dev(self):
+ """Is development."""
+
+ return bool(self.release < "alpha")
+
+ def _is_post(self):
+ """Is post."""
+
+ return self.post > 0
+
+ def _get_dev_status(self): # pragma: no cover
+ """Get development status string."""
+
+ return DEV_STATUS[self.release]
+
+ def _get_canonical(self):
+ """Get the canonical output string."""
+
+ # Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed..
+ if self.micro == 0:
+ ver = "{}.{}".format(self.major, self.minor)
+ else:
+ ver = "{}.{}.{}".format(self.major, self.minor, self.micro)
+ if self._is_pre():
+ ver += '{}{}'.format(REL_MAP[self.release], self.pre)
+ if self._is_post():
+ ver += ".post{}".format(self.post)
+ if self._is_dev():
+ ver += ".dev{}".format(self.dev)
+
+ return ver
+
+
+def parse_version(ver, pre=False):
+ """Parse version into a comparable Version tuple."""
+
+ m = RE_VER.match(ver)
+
+ # Handle major, minor, micro
+ major = int(m.group('major'))
+ minor = int(m.group('minor')) if m.group('minor') else 0
+ micro = int(m.group('micro')) if m.group('micro') else 0
+
+ # Handle pre releases
+ if m.group('type'):
+ release = PRE_REL_MAP[m.group('type')]
+ pre = int(m.group('pre'))
+ else:
+ release = "final"
+ pre = 0
+
+ # Handle development releases
+ dev = m.group('dev') if m.group('dev') else 0
+ if m.group('dev'):
+ dev = int(m.group('dev'))
+ release = '.dev-' + release if pre else '.dev'
+ else:
+ dev = 0
+
+ # Handle post
+ post = int(m.group('post')) if m.group('post') else 0
+
+ return Version(major, minor, micro, release, pre, post, dev)
+
+
+class Pep562(object):
+ """
+ Backport of PEP 562 <https://pypi.org/search/?q=pep562>.
+
+ Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`.
+ The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed.
+ """
+
+ def __init__(self, name):
+ """Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7."""
+
+ self._module = sys.modules[name]
+ self._get_attr = getattr(self._module, '__getattr__', None)
+ self._get_dir = getattr(self._module, '__dir__', None)
+ sys.modules[name] = self
+
+ def __dir__(self):
+ """Return the overridden `dir` if one was provided, else apply `dir` to the module."""
+
+ return self._get_dir() if self._get_dir else dir(self._module)
+
+ def __getattr__(self, name):
+ """Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present."""
+
+ try:
+ return getattr(self._module, name)
+ except AttributeError:
+ if self._get_attr:
+ return self._get_attr(name)
+ raise
+
+
+__version_info__ = Version(1, 0, 0, "final")
+__version__ = __version_info__._get_canonical()
diff --git a/markdown/util.py b/markdown/util.py
index 371c4e0..18d4a71 100644
--- a/markdown/util.py
+++ b/markdown/util.py
@@ -33,6 +33,7 @@ Python 3 Stuff
=============================================================================
"""
PY3 = sys.version_info[0] == 3
+PY37 = (3, 7) <= sys.version_info
if PY3: # pragma: no cover
string_type = str
@@ -177,44 +178,6 @@ MISC AUXILIARY CLASSES
"""
-class ModuleWrap(object):
- """
- Provided so that we can deprecate old version methodology.
-
- See comments from Guido: <https://mail.python.org/pipermail/python-ideas/2012-May/014969.html>
- and see PEP 562 which this is essentially a backport of: <https://www.python.org/dev/peps/pep-0562/>.
- """
-
- def __init__(self, module):
- """Initialize."""
-
- self._module = sys.modules[module]
- sys.modules[module] = self
-
- def __dir__(self):
- """
- Implement the `dir` command.
-
- Return module's results for the `dir` command along with any
- attributes that have been added to the class.
- """
-
- attr = (
- set(dir(super(ModuleWrap, self).__getattribute__('_module'))) |
- (set(self.__class__.__dict__.keys()) - set(ModuleWrap.__dict__.keys()))
- )
-
- return sorted(list(attr))
-
- def __getattribute__(self, name):
- """Get the class attribute first and fallback to the module if not available."""
-
- try:
- return super(ModuleWrap, self).__getattribute__(name)
- except AttributeError:
- return getattr(super(ModuleWrap, self).__getattribute__('_module'), name)
-
-
class AtomicString(text_type):
"""A string which should not be further processed."""
pass
diff --git a/tests/test_apis.py b/tests/test_apis.py
index ddeed7e..38c567a 100644
--- a/tests/test_apis.py
+++ b/tests/test_apis.py
@@ -1039,7 +1039,7 @@ class TestGeneralDeprecations(unittest.TestCase):
"""Tests the `__dir__` attribute of the class as it replaces the module's."""
dir_attr = dir(markdown)
- self.assertTrue('version' in dir_attr)
+ self.assertFalse('version' in dir_attr)
self.assertTrue('__version__' in dir_attr)
- self.assertTrue('version_info' in dir_attr)
+ self.assertFalse('version_info' in dir_attr)
self.assertTrue('__version_info__' in dir_attr)
diff --git a/tox.ini b/tox.ini
index ee8f50f..799c2eb 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,3 +21,4 @@ commands = {toxinidir}/checkspelling.sh
[flake8]
max-line-length = 119
+exclude=markdown/pep562.py