-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Expand file tree
/
Copy path__init__.py
More file actions
382 lines (326 loc) · 13.2 KB
/
__init__.py
File metadata and controls
382 lines (326 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
import os as _os
import re as _re
import sys as _sys
import warnings as _warnings
from math import inf as _inf
from typing import (
ClassVar as _ClassVar,
Dict as _Dict,
List as _List,
Optional as _Optional,
Pattern as _Pattern,
Tuple as _Tuple,
Union as _Union,
)
__all__ = (
"__version__",
"version_info",
"VersionInfo",
)
MIN_PYTHON_VERSION = (3, 8, 1)
if _sys.version_info < MIN_PYTHON_VERSION:
print(
f"Python {'.'.join(map(str, MIN_PYTHON_VERSION))} is required to run Red, but you have "
f"{_sys.version}! Please update Python."
)
_sys.exit(78)
class VersionInfo:
ALPHA = "alpha"
BETA = "beta"
RELEASE_CANDIDATE = "release candidate"
FINAL = "final"
_VERSION_STR_PATTERN: _ClassVar[_Pattern[str]] = _re.compile(
r"^"
r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<micro>0|[1-9]\d*)"
r"(?:(?P<releaselevel>a|b|rc)(?P<serial>0|[1-9]\d*))?"
r"(?:\.post(?P<post_release>0|[1-9]\d*))?"
r"(?:\.dev(?P<dev_release>0|[1-9]\d*))?"
r"(?:\+(?P<local_version>g[a-z0-9]+(?:\.dirty)?))?"
r"$",
flags=_re.IGNORECASE,
)
_RELEASE_LEVELS: _ClassVar[_List[str]] = [ALPHA, BETA, RELEASE_CANDIDATE, FINAL]
_SHORT_RELEASE_LEVELS: _ClassVar[_Dict[str, str]] = {
"a": ALPHA,
"b": BETA,
"rc": RELEASE_CANDIDATE,
}
def __init__(
self,
major: int,
minor: int,
micro: int,
releaselevel: str,
serial: _Optional[int] = None,
post_release: _Optional[int] = None,
dev_release: _Optional[int] = None,
local_version: _Optional[str] = None,
) -> None:
self.major: int = major
self.minor: int = minor
self.micro: int = micro
if releaselevel not in self._RELEASE_LEVELS:
raise TypeError(f"'releaselevel' must be one of: {', '.join(self._RELEASE_LEVELS)}")
self.releaselevel: str = releaselevel
self.serial: _Optional[int] = serial
self.post_release: _Optional[int] = post_release
self.dev_release: _Optional[int] = dev_release
self.local_version: _Optional[str] = local_version
@property
def short_commit_hash(self) -> _Optional[str]:
if self.local_version is None:
return None
return self.local_version[1:].split(".", 1)[0]
@property
def dirty(self) -> bool:
return self.local_version is not None and self.local_version.endswith(".dirty")
@classmethod
def from_str(cls, version_str: str) -> "VersionInfo":
"""Parse a string into a VersionInfo object.
Raises
------
ValueError
If the version info string is invalid.
"""
match = cls._VERSION_STR_PATTERN.match(version_str)
if not match:
raise ValueError(f"Invalid version string: {version_str}")
kwargs: _Dict[str, _Union[str, int]] = {}
for key in ("major", "minor", "micro"):
kwargs[key] = int(match[key])
releaselevel = match["releaselevel"]
if releaselevel is not None:
kwargs["releaselevel"] = cls._SHORT_RELEASE_LEVELS[releaselevel]
else:
kwargs["releaselevel"] = cls.FINAL
for key in ("serial", "post_release", "dev_release"):
if match[key] is not None:
kwargs[key] = int(match[key])
kwargs["local_version"] = match["local_version"]
return cls(**kwargs)
@classmethod
def from_json(
cls, data: _Union[_Dict[str, _Union[int, str]], _List[_Union[int, str]]]
) -> "VersionInfo":
if isinstance(data, _List):
# For old versions, data was stored as a list:
# [MAJOR, MINOR, MICRO, RELEASELEVEL, SERIAL]
return cls(*data)
else:
return cls(**data)
def to_json(self) -> _Dict[str, _Union[int, str]]:
return {
"major": self.major,
"minor": self.minor,
"micro": self.micro,
"releaselevel": self.releaselevel,
"serial": self.serial,
"post_release": self.post_release,
"dev_release": self.dev_release,
"local_version": self.local_version,
}
def _generate_comparison_tuples(
self, other: "VersionInfo"
) -> _List[
_Tuple[int, int, int, int, _Union[int, float], _Union[int, float], _Union[int, float], int]
]:
tups: _List[
_Tuple[
int, int, int, int, _Union[int, float], _Union[int, float], _Union[int, float], int
]
] = []
for obj in (self, other):
if (
obj.releaselevel == obj.FINAL
and obj.post_release is None
and obj.dev_release is not None
):
releaselevel = -1
else:
releaselevel = obj._RELEASE_LEVELS.index(obj.releaselevel)
tups.append(
(
obj.major,
obj.minor,
obj.micro,
releaselevel,
obj.serial if obj.serial is not None else _inf,
obj.post_release if obj.post_release is not None else -_inf,
obj.dev_release if obj.dev_release is not None else _inf,
int(obj.dirty),
)
)
return tups
def __lt__(self, other: "VersionInfo") -> bool:
tups = self._generate_comparison_tuples(other)
return tups[0] < tups[1]
def __eq__(self, other: "VersionInfo") -> bool:
tups = self._generate_comparison_tuples(other)
return tups[0] == tups[1]
def __le__(self, other: "VersionInfo") -> bool:
tups = self._generate_comparison_tuples(other)
return tups[0] <= tups[1]
def __str__(self) -> str:
ret = f"{self.major}.{self.minor}.{self.micro}"
if self.releaselevel != self.FINAL:
short = next(
k for k, v in self._SHORT_RELEASE_LEVELS.items() if v == self.releaselevel
)
ret += f"{short}{self.serial}"
if self.post_release is not None:
ret += f".post{self.post_release}"
if self.dev_release is not None:
ret += f".dev{self.dev_release}"
if self.local_version is not None:
ret += f"+{self.local_version}"
return ret
def __repr__(self) -> str:
return (
"VersionInfo(major={major}, minor={minor}, micro={micro}, "
"releaselevel={releaselevel}, serial={serial}, post={post_release}, "
"dev={dev_release}, local={local_version})"
).format(**self.to_json())
@classmethod
def _get_version(cls, *, ignore_installed: bool = False) -> _Tuple[str, "VersionInfo"]:
if not _VERSION.endswith(".dev1"):
return _VERSION, cls.from_str(_VERSION)
project_root = _os.path.abspath(_os.path.dirname(_os.path.dirname(__file__)))
methods = [
cls._get_version_from_git_repo,
]
# `ignore_installed` is `True` when building with setuptools.
if ignore_installed:
methods.append(cls._get_version_from_sdist_pkg_info)
methods.append(cls._get_version_from_git_archive)
else:
methods.append(cls._get_version_from_package_metadata)
exceptions = []
for method in methods:
try:
version = method(project_root)
except Exception as exc:
exceptions.append(exc)
else:
break
else:
import traceback
for exc in exceptions:
traceback.print_exception(None, exc, exc.__traceback__)
exc.__traceback__ = None
version = _VERSION
return version, cls.from_str(version)
@classmethod
def _get_version_from_git_repo(cls, project_root: str) -> str:
# we only want to do this for editable installs
if not _os.path.exists(_os.path.join(project_root, ".git")):
raise RuntimeError("not a git repository")
import subprocess
output = subprocess.check_output(
("git", "describe", "--tags", "--long", "--dirty"),
stderr=subprocess.DEVNULL,
cwd=project_root,
)
_, count, commit, *dirty = output.decode("utf-8").strip().split("-", 3)
dirty_suffix = f".{dirty[0]}" if dirty else ""
return f"{_VERSION[:-1]}{count}+{commit}{dirty_suffix}"
@classmethod
def _get_version_from_git_archive(cls, project_root: str) -> str:
with open(_os.path.join(project_root, ".git_archive_info.txt"), encoding="utf-8") as fp:
commit, describe_name = fp.read().splitlines()
if not describe_name:
raise RuntimeError("git archive's describe didn't output anything")
if "%(describe" in describe_name:
# either git-archive was generated with Git < 2.35 or this is not a git-archive
raise RuntimeError("git archive did not support describe output")
_, _, suffix = describe_name.partition("-")
if suffix:
count, _, _ = suffix.partition("-")
else:
count = "0"
return f"{_VERSION[:-1]}{count}+g{commit}"
@classmethod
def _get_version_from_sdist_pkg_info(cls, project_root: str) -> str:
pkg_info_path = _os.path.join(project_root, "PKG-INFO")
if not _os.path.exists(pkg_info_path):
raise RuntimeError("not an sdist")
import email
with open(pkg_info_path, encoding="utf-8") as fp:
return email.message_from_file(fp)["Version"]
@classmethod
def _get_version_from_package_metadata(cls, project_root: str) -> str:
from importlib.metadata import version
return version("Red-DiscordBot")
def _update_event_loop_policy():
if _sys.implementation.name == "cpython":
# Let's not force this dependency, uvloop is much faster on cpython
try:
import uvloop
except ImportError:
pass
else:
import asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
def _ensure_no_colorama():
# a hacky way to ensure that nothing initialises colorama
# if we're not running with legacy Windows command line mode
from rich.console import detect_legacy_windows
if not detect_legacy_windows():
try:
import colorama
import colorama.initialise
except ModuleNotFoundError:
# colorama is not Red's primary dependency so it might not be present
return
colorama.deinit()
def _colorama_wrap_stream(stream, *args, **kwargs):
return stream
colorama.wrap_stream = _colorama_wrap_stream
colorama.initialise.wrap_stream = _colorama_wrap_stream
def _update_logger_class():
from red_commons.logging import maybe_update_logger_class
maybe_update_logger_class()
def _early_init():
# This function replaces logger so we preferably (though not necessarily) want that to happen
# before importing anything that calls `logging.getLogger()`, i.e. `asyncio`.
_update_logger_class()
_update_event_loop_policy()
_ensure_no_colorama()
# This is bumped automatically by release workflow (`.github/workflows/scripts/bump_version.py`)
_VERSION = "3.5.20"
__version__, version_info = VersionInfo._get_version()
# Show DeprecationWarning
_warnings.filterwarnings("default", category=DeprecationWarning)
# TODO: Rearrange cli flags here and use the value instead of this monkeypatch
if not any(_re.match("^-(-debug|d+|-verbose|v+)$", i) for i in _sys.argv):
# DEP-WARN
# Individual warnings - tracked in https://github.com/Cog-Creators/Red-DiscordBot/issues/3529
# DeprecationWarning: an integer is required (got type float). Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python.
_warnings.filterwarnings("ignore", category=DeprecationWarning, module="importlib", lineno=219)
# DeprecationWarning: The loop argument is deprecated since Python 3.8, and scheduled for removal in Python 3.10
# stdin, stdout, stderr = await tasks.gather(stdin, stdout, stderr,
# this is a bug in CPython
_warnings.filterwarnings(
"ignore",
category=DeprecationWarning,
module="asyncio",
message="The loop argument is deprecated since Python 3.8",
)
# DEP-WARN - d.py currently uses audioop module, Danny is aware of the deprecation
#
# DeprecationWarning: 'audioop' is deprecated and slated for removal in Python 3.13
# import audioop
_warnings.filterwarnings(
"ignore",
category=DeprecationWarning,
module="discord",
message="'audioop' is deprecated and slated for removal",
)
# DEP-WARN - will need a fix before Python 3.12 support
#
# DeprecationWarning: the load_module() method is deprecated and slated for removal in Python 3.12; use exec_module() instead
_warnings.filterwarnings(
"ignore",
category=DeprecationWarning,
module="importlib",
message=r"the load_module\(\) method is deprecated and slated for removal",
)