diff options
author | Isaac Muse <faceless.shop@gmail.com> | 2018-12-22 13:50:04 -0700 |
---|---|---|
committer | Waylan Limberg <waylan.limberg@icloud.com> | 2018-12-22 15:50:04 -0500 |
commit | ab24c2357dfd98540d5196470164bf49b5e6d9f3 (patch) | |
tree | f785639a9045479d1ae24ce25482597395784834 | |
parent | aa6667e0c58cd01caddc3a5aa47ee3f395ef5b8e (diff) | |
download | python-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-- | .coveragerc | 1 | ||||
-rw-r--r-- | markdown/__init__.py | 41 | ||||
-rw-r--r-- | markdown/pep562.py | 246 | ||||
-rw-r--r-- | markdown/util.py | 39 | ||||
-rw-r--r-- | tests/test_apis.py | 4 | ||||
-rw-r--r-- | tox.ini | 1 |
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) @@ -21,3 +21,4 @@ commands = {toxinidir}/checkspelling.sh [flake8] max-line-length = 119 +exclude=markdown/pep562.py |