forked from canonical/cloud-init
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlifecycle.py
More file actions
242 lines (198 loc) · 7.75 KB
/
Copy pathlifecycle.py
File metadata and controls
242 lines (198 loc) · 7.75 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
# This file is part of cloud-init. See LICENSE file for license information.
import collections
import functools
import logging
from typing import NamedTuple, Optional
from cloudinit import features, log
LOG = logging.getLogger(__name__)
class DeprecationLog(NamedTuple):
log_level: int
message: str
@functools.total_ordering
class Version(
collections.namedtuple("Version", ["major", "minor", "patch", "rev"])
):
"""A class for comparing versions.
Implemented as a named tuple with all ordering methods. Comparisons
between X.Y.N and X.Y always treats the more specific number as larger.
:param major: the most significant number in a version
:param minor: next greatest significant number after major
:param patch: next greatest significant number after minor
:param rev: the least significant number in a version
:raises TypeError: If invalid arguments are given.
:raises ValueError: If invalid arguments are given.
Examples:
>>> Version(2, 9) == Version.from_str("2.9")
True
>>> Version(2, 9, 1) > Version.from_str("2.9.1")
False
>>> Version(3, 10) > Version.from_str("3.9.9.9")
True
>>> Version(3, 7) >= Version.from_str("3.7")
True
"""
def __new__(
cls, major: int = -1, minor: int = -1, patch: int = -1, rev: int = -1
) -> "Version":
"""Default of -1 allows us to tiebreak in favor of the most specific
number"""
return super(Version, cls).__new__(cls, major, minor, patch, rev)
@classmethod
def from_str(cls, version: str) -> "Version":
"""Create a Version object from a string.
:param version: A period-delimited version string, max 4 segments.
:raises TypeError: Raised if invalid arguments are given.
:raises ValueError: Raised if invalid arguments are given.
:return: A Version object.
"""
return cls(*(list(map(int, version.split(".")))))
def __gt__(self, other):
return 1 == self._compare_version(other)
def __eq__(self, other):
return (
self.major == other.major
and self.minor == other.minor
and self.patch == other.patch
and self.rev == other.rev
)
def __iter__(self):
"""Iterate over the version (drop sentinels)"""
for n in (self.major, self.minor, self.patch, self.rev):
if n != -1:
yield str(n)
else:
break
def __str__(self):
return ".".join(self)
def __hash__(self):
return hash(str(self))
def _compare_version(self, other: "Version") -> int:
"""Compare this Version to another.
:param other: A Version object.
:return: -1 if self > other, 1 if self < other, else 0
"""
if self == other:
return 0
if self.major > other.major:
return 1
if self.minor > other.minor:
return 1
if self.patch > other.patch:
return 1
if self.rev > other.rev:
return 1
return -1
def should_log_deprecation(version: str, boundary_version: str) -> bool:
"""Determine if a deprecation message should be logged.
:param version: The version in which the thing was deprecated.
:param boundary_version: The version at which deprecation level is logged.
:return: True if the message should be logged, else False.
"""
return boundary_version == "devel" or Version.from_str(
version
) <= Version.from_str(boundary_version)
def log_with_downgradable_level(
*,
logger: logging.Logger,
version: str,
requested_level: int,
msg: str,
args,
):
"""Log a message at the requested level, if that is acceptable.
If the log level is too high due to the version boundary, log at DEBUG
level. Useful to add new warnings to previously unguarded code without
disrupting stable downstreams.
:param logger: Logger object to log with
:param version: Version string of the version that this log was introduced
:param level: Preferred level at which this message should be logged
:param msg: Message, as passed to the logger.
:param args: Message formatting args, ass passed to the logger
:return: True if the message should be logged, else False.
"""
if should_log_deprecation(version, features.DEPRECATION_INFO_BOUNDARY):
logger.log(requested_level, msg, args)
else:
logger.debug(msg, args)
def deprecate(
*,
deprecated: str,
deprecated_version: str,
extra_message: Optional[str] = None,
schedule: int = 5,
skip_log: bool = False,
) -> DeprecationLog:
"""Mark a "thing" as deprecated. Deduplicated deprecations are
logged.
:param deprecated: Noun to be deprecated. Write this as the start
of a sentence, with no period. Version and extra message will
be appended.
:param deprecated_version: The version in which the thing was
deprecated
:param extra_message: A remedy for the user's problem. A good
message will be actionable and specific (i.e., don't use a
generic "Use updated key." if the user used a deprecated key).
End the string with a period.
:param schedule: Manually set the deprecation schedule. Defaults to
5 years. Leave a comment explaining your reason for deviation if
setting this value.
:param skip_log: Return log text rather than logging it. Useful for
running prior to logging setup.
:return: NamedTuple containing log level and log message
DeprecationLog(level: int, message: str)
Note: uses keyword-only arguments to improve legibility
"""
if not hasattr(deprecate, "log"):
setattr(deprecate, "log", set())
message = extra_message or ""
dedup = hash(deprecated + message + deprecated_version + str(schedule))
version = Version.from_str(deprecated_version)
version_removed = Version(version.major + schedule, version.minor)
deprecate_msg = (
f"{deprecated} is deprecated in "
f"{deprecated_version} and scheduled to be removed in "
f"{version_removed}. {message}"
).rstrip()
if not should_log_deprecation(
deprecated_version, features.DEPRECATION_INFO_BOUNDARY
):
level = logging.INFO
elif hasattr(LOG, "deprecated"):
level = log.DEPRECATED
else:
level = logging.WARN
log_cache = getattr(deprecate, "log")
if not skip_log and dedup not in log_cache:
log_cache.add(dedup)
LOG.log(level, deprecate_msg)
return DeprecationLog(level, deprecate_msg)
def deprecate_call(
*, deprecated_version: str, extra_message: str, schedule: int = 5
):
"""Mark a "thing" as deprecated. Deduplicated deprecations are
logged.
:param deprecated_version: The version in which the thing was
deprecated
:param extra_message: A remedy for the user's problem. A good
message will be actionable and specific (i.e., don't use a
generic "Use updated key." if the user used a deprecated key).
End the string with a period.
:param schedule: Manually set the deprecation schedule. Defaults to
5 years. Leave a comment explaining your reason for deviation if
setting this value.
Note: uses keyword-only arguments to improve legibility
"""
def wrapper(func):
@functools.wraps(func)
def decorator(*args, **kwargs):
# don't log message multiple times
out = func(*args, **kwargs)
deprecate(
deprecated_version=deprecated_version,
deprecated=func.__name__,
extra_message=extra_message,
schedule=schedule,
)
return out
return decorator
return wrapper