Skip to content

Commit ab24c23

Browse files
facelessuserwaylan
authored andcommitted
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.
1 parent aa6667e commit ab24c23

File tree

6 files changed

+271
-61
lines changed

6 files changed

+271
-61
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ omit=
33
*site-packages*
44
tests/*
55
markdown/test_tools.py
6+
markdown/pep562.py

markdown/__init__.py

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
from __future__ import absolute_import
2424
from __future__ import unicode_literals
2525
from .core import Markdown, markdown, markdownFromFile
26-
from .util import ModuleWrap, deprecated
26+
from .util import PY37
27+
from .pep562 import Pep562
2728
from pkg_resources.extern import packaging
29+
import warnings
2830

2931
# For backward compatibility as some extensions expect it...
3032
from .extensions import Extension # noqa
@@ -64,28 +66,25 @@ def _get_version(): # pragma: no cover
6466

6567
__version__ = _get_version()
6668

69+
__deprecated__ = {
70+
"version": ("__version__", __version__),
71+
"version_info": ("__version_info__", __version_info__)
72+
}
6773

68-
class _ModuleWrap(ModuleWrap):
69-
"""
70-
Wrap module so that we can control `__getattribute__` and `__dir__` logic.
7174

72-
Treat `version` and `version_info` as deprecated properties.
73-
Provides backward-compatabillity with <3.0 versions.
74-
"""
75+
def __getattr__(name):
76+
"""Get attribute."""
7577

76-
@property
77-
@deprecated("Use '__version__' instead.", stacklevel=3)
78-
def version(self):
79-
"""Get deprecated version."""
78+
deprecated = __deprecated__.get(name)
79+
if deprecated:
80+
warnings.warn(
81+
"'{}' is deprecated. Use '{}' instead.".format(name, deprecated[0]),
82+
category=DeprecationWarning,
83+
stacklevel=(3 if PY37 else 4)
84+
)
85+
return deprecated[1]
86+
raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))
8087

81-
return __version__
8288

83-
@property
84-
@deprecated("Use '__version_info__' instead.", stacklevel=3)
85-
def version_info(self):
86-
"""Get deprecated version info."""
87-
88-
return __version_info__
89-
90-
91-
_ModuleWrap(__name__)
89+
if not PY37:
90+
Pep562(__name__)

markdown/pep562.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
"""
2+
Backport of PEP 562.
3+
4+
https://pypi.org/search/?q=pep562
5+
6+
Licensed under MIT
7+
Copyright (c) 2018 Isaac Muse <[email protected]>
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
10+
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
11+
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
12+
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in all copies or substantial portions
15+
of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
18+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
19+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
20+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
21+
IN THE SOFTWARE.
22+
"""
23+
from __future__ import unicode_literals
24+
import sys
25+
from collections import namedtuple
26+
import re
27+
28+
__all__ = ('Pep562',)
29+
30+
RE_VER = re.compile(
31+
r'''(?x)
32+
(?P<major>\d+)(?:\.(?P<minor>\d+))?(?:\.(?P<micro>\d+))?
33+
(?:(?P<type>a|b|rc)(?P<pre>\d+))?
34+
(?:\.post(?P<post>\d+))?
35+
(?:\.dev(?P<dev>\d+))?
36+
'''
37+
)
38+
39+
REL_MAP = {
40+
".dev": "",
41+
".dev-alpha": "a",
42+
".dev-beta": "b",
43+
".dev-candidate": "rc",
44+
"alpha": "a",
45+
"beta": "b",
46+
"candidate": "rc",
47+
"final": ""
48+
}
49+
50+
DEV_STATUS = {
51+
".dev": "2 - Pre-Alpha",
52+
".dev-alpha": "2 - Pre-Alpha",
53+
".dev-beta": "2 - Pre-Alpha",
54+
".dev-candidate": "2 - Pre-Alpha",
55+
"alpha": "3 - Alpha",
56+
"beta": "4 - Beta",
57+
"candidate": "4 - Beta",
58+
"final": "5 - Production/Stable"
59+
}
60+
61+
PRE_REL_MAP = {"a": 'alpha', "b": 'beta', "rc": 'candidate'}
62+
63+
64+
class Version(namedtuple("Version", ["major", "minor", "micro", "release", "pre", "post", "dev"])):
65+
"""
66+
Get the version (PEP 440).
67+
68+
A biased approach to the PEP 440 semantic version.
69+
70+
Provides a tuple structure which is sorted for comparisons `v1 > v2` etc.
71+
(major, minor, micro, release type, pre-release build, post-release build, development release build)
72+
Release types are named in is such a way they are comparable with ease.
73+
Accessors to check if a development, pre-release, or post-release build. Also provides accessor to get
74+
development status for setup files.
75+
76+
How it works (currently):
77+
78+
- You must specify a release type as either `final`, `alpha`, `beta`, or `candidate`.
79+
- To define a development release, you can use either `.dev`, `.dev-alpha`, `.dev-beta`, or `.dev-candidate`.
80+
The dot is used to ensure all development specifiers are sorted before `alpha`.
81+
You can specify a `dev` number for development builds, but do not have to as implicit development releases
82+
are allowed.
83+
- You must specify a `pre` value greater than zero if using a prerelease as this project (not PEP 440) does not
84+
allow implicit prereleases.
85+
- You can optionally set `post` to a value greater than zero to make the build a post release. While post releases
86+
are technically allowed in prereleases, it is strongly discouraged, so we are rejecting them. It should be
87+
noted that we do not allow `post0` even though PEP 440 does not restrict this. This project specifically
88+
does not allow implicit post releases.
89+
- It should be noted that we do not support epochs `1!` or local versions `+some-custom.version-1`.
90+
91+
Acceptable version releases:
92+
93+
```
94+
Version(1, 0, 0, "final") 1.0
95+
Version(1, 2, 0, "final") 1.2
96+
Version(1, 2, 3, "final") 1.2.3
97+
Version(1, 2, 0, ".dev-alpha", pre=4) 1.2a4
98+
Version(1, 2, 0, ".dev-beta", pre=4) 1.2b4
99+
Version(1, 2, 0, ".dev-candidate", pre=4) 1.2rc4
100+
Version(1, 2, 0, "final", post=1) 1.2.post1
101+
Version(1, 2, 3, ".dev") 1.2.3.dev0
102+
Version(1, 2, 3, ".dev", dev=1) 1.2.3.dev1
103+
```
104+
105+
"""
106+
107+
def __new__(cls, major, minor, micro, release="final", pre=0, post=0, dev=0):
108+
"""Validate version info."""
109+
110+
# Ensure all parts are positive integers.
111+
for value in (major, minor, micro, pre, post):
112+
if not (isinstance(value, int) and value >= 0):
113+
raise ValueError("All version parts except 'release' should be integers.")
114+
115+
if release not in REL_MAP:
116+
raise ValueError("'{}' is not a valid release type.".format(release))
117+
118+
# Ensure valid pre-release (we do not allow implicit pre-releases).
119+
if ".dev-candidate" < release < "final":
120+
if pre == 0:
121+
raise ValueError("Implicit pre-releases not allowed.")
122+
elif dev:
123+
raise ValueError("Version is not a development release.")
124+
elif post:
125+
raise ValueError("Post-releases are not allowed with pre-releases.")
126+
127+
# Ensure valid development or development/pre release
128+
elif release < "alpha":
129+
if release > ".dev" and pre == 0:
130+
raise ValueError("Implicit pre-release not allowed.")
131+
elif post:
132+
raise ValueError("Post-releases are not allowed with pre-releases.")
133+
134+
# Ensure a valid normal release
135+
else:
136+
if pre:
137+
raise ValueError("Version is not a pre-release.")
138+
elif dev:
139+
raise ValueError("Version is not a development release.")
140+
141+
return super(Version, cls).__new__(cls, major, minor, micro, release, pre, post, dev)
142+
143+
def _is_pre(self):
144+
"""Is prerelease."""
145+
146+
return self.pre > 0
147+
148+
def _is_dev(self):
149+
"""Is development."""
150+
151+
return bool(self.release < "alpha")
152+
153+
def _is_post(self):
154+
"""Is post."""
155+
156+
return self.post > 0
157+
158+
def _get_dev_status(self): # pragma: no cover
159+
"""Get development status string."""
160+
161+
return DEV_STATUS[self.release]
162+
163+
def _get_canonical(self):
164+
"""Get the canonical output string."""
165+
166+
# Assemble major, minor, micro version and append `pre`, `post`, or `dev` if needed..
167+
if self.micro == 0:
168+
ver = "{}.{}".format(self.major, self.minor)
169+
else:
170+
ver = "{}.{}.{}".format(self.major, self.minor, self.micro)
171+
if self._is_pre():
172+
ver += '{}{}'.format(REL_MAP[self.release], self.pre)
173+
if self._is_post():
174+
ver += ".post{}".format(self.post)
175+
if self._is_dev():
176+
ver += ".dev{}".format(self.dev)
177+
178+
return ver
179+
180+
181+
def parse_version(ver, pre=False):
182+
"""Parse version into a comparable Version tuple."""
183+
184+
m = RE_VER.match(ver)
185+
186+
# Handle major, minor, micro
187+
major = int(m.group('major'))
188+
minor = int(m.group('minor')) if m.group('minor') else 0
189+
micro = int(m.group('micro')) if m.group('micro') else 0
190+
191+
# Handle pre releases
192+
if m.group('type'):
193+
release = PRE_REL_MAP[m.group('type')]
194+
pre = int(m.group('pre'))
195+
else:
196+
release = "final"
197+
pre = 0
198+
199+
# Handle development releases
200+
dev = m.group('dev') if m.group('dev') else 0
201+
if m.group('dev'):
202+
dev = int(m.group('dev'))
203+
release = '.dev-' + release if pre else '.dev'
204+
else:
205+
dev = 0
206+
207+
# Handle post
208+
post = int(m.group('post')) if m.group('post') else 0
209+
210+
return Version(major, minor, micro, release, pre, post, dev)
211+
212+
213+
class Pep562(object):
214+
"""
215+
Backport of PEP 562 <https://pypi.org/search/?q=pep562>.
216+
217+
Wraps the module in a class that exposes the mechanics to override `__dir__` and `__getattr__`.
218+
The given module will be searched for overrides of `__dir__` and `__getattr__` and use them when needed.
219+
"""
220+
221+
def __init__(self, name):
222+
"""Acquire `__getattr__` and `__dir__`, but only replace module for versions less than Python 3.7."""
223+
224+
self._module = sys.modules[name]
225+
self._get_attr = getattr(self._module, '__getattr__', None)
226+
self._get_dir = getattr(self._module, '__dir__', None)
227+
sys.modules[name] = self
228+
229+
def __dir__(self):
230+
"""Return the overridden `dir` if one was provided, else apply `dir` to the module."""
231+
232+
return self._get_dir() if self._get_dir else dir(self._module)
233+
234+
def __getattr__(self, name):
235+
"""Attempt to retrieve the attribute from the module, and if missing, use the overridden function if present."""
236+
237+
try:
238+
return getattr(self._module, name)
239+
except AttributeError:
240+
if self._get_attr:
241+
return self._get_attr(name)
242+
raise
243+
244+
245+
__version_info__ = Version(1, 0, 0, "final")
246+
__version__ = __version_info__._get_canonical()

markdown/util.py

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
=============================================================================
3434
"""
3535
PY3 = sys.version_info[0] == 3
36+
PY37 = (3, 7) <= sys.version_info
3637

3738
if PY3: # pragma: no cover
3839
string_type = str
@@ -177,44 +178,6 @@ def code_escape(text):
177178
"""
178179

179180

180-
class ModuleWrap(object):
181-
"""
182-
Provided so that we can deprecate old version methodology.
183-
184-
See comments from Guido: <https://mail.python.org/pipermail/python-ideas/2012-May/014969.html>
185-
and see PEP 562 which this is essentially a backport of: <https://www.python.org/dev/peps/pep-0562/>.
186-
"""
187-
188-
def __init__(self, module):
189-
"""Initialize."""
190-
191-
self._module = sys.modules[module]
192-
sys.modules[module] = self
193-
194-
def __dir__(self):
195-
"""
196-
Implement the `dir` command.
197-
198-
Return module's results for the `dir` command along with any
199-
attributes that have been added to the class.
200-
"""
201-
202-
attr = (
203-
set(dir(super(ModuleWrap, self).__getattribute__('_module'))) |
204-
(set(self.__class__.__dict__.keys()) - set(ModuleWrap.__dict__.keys()))
205-
)
206-
207-
return sorted(list(attr))
208-
209-
def __getattribute__(self, name):
210-
"""Get the class attribute first and fallback to the module if not available."""
211-
212-
try:
213-
return super(ModuleWrap, self).__getattribute__(name)
214-
except AttributeError:
215-
return getattr(super(ModuleWrap, self).__getattribute__('_module'), name)
216-
217-
218181
class AtomicString(text_type):
219182
"""A string which should not be further processed."""
220183
pass

tests/test_apis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,7 +1039,7 @@ def test_deprecation_wrapper_dir(self):
10391039
"""Tests the `__dir__` attribute of the class as it replaces the module's."""
10401040

10411041
dir_attr = dir(markdown)
1042-
self.assertTrue('version' in dir_attr)
1042+
self.assertFalse('version' in dir_attr)
10431043
self.assertTrue('__version__' in dir_attr)
1044-
self.assertTrue('version_info' in dir_attr)
1044+
self.assertFalse('version_info' in dir_attr)
10451045
self.assertTrue('__version_info__' in dir_attr)

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ commands = {toxinidir}/checkspelling.sh
2121

2222
[flake8]
2323
max-line-length = 119
24+
exclude=markdown/pep562.py

0 commit comments

Comments
 (0)