File: version_utils.py

package info (click to toggle)
python-b2sdk 2.10.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,228 kB
  • sloc: python: 32,094; sh: 13; makefile: 8
file content (198 lines) | stat: -rw-r--r-- 6,945 bytes parent folder | download | duplicates (2)
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
######################################################################
#
# File: b2sdk/_internal/version_utils.py
#
# Copyright 2019 Backblaze Inc. All Rights Reserved.
#
# License https://www.backblaze.com/using_b2_code.html
#
######################################################################
from __future__ import annotations

import inspect
import re
import warnings
from abc import ABCMeta, abstractmethod
from functools import total_ordering, wraps

from b2sdk.version import VERSION


@total_ordering
class _Version:
    """
    Rudimentary semver version parser.

    It uses VERY naive parsing which is only supposed to produce a tuple, able to
    compare major.minor.patch versions.
    It does not support PEP 440 epoch, pre-releases, post-releases, local versions, etc.
    """

    def __init__(self, version: str):
        self._raw = version
        self._parsed = self._parse_version(version)

    def __str__(self):
        return self._raw

    def __eq__(self, other):
        return self._parsed == other._parsed

    def __lt__(self, other):
        return self._parsed < other._parsed

    @classmethod
    def _parse_version(cls, version: str) -> tuple[int, ...]:
        if '!' in version:  # strip PEP 440 epoch
            version = version.split('!', 1)[1]
        return tuple(map(int, re.findall(r'\d+', version)))


class AbstractVersionDecorator(metaclass=ABCMeta):
    WHAT = NotImplemented  # 'function', 'method', 'class' etc

    def __init__(self, changed_version, cutoff_version=None, reason='', current_version=None):
        """
        Changed_version, cutoff_version and current_version are version strings.
        """
        if current_version is None:  # this is for tests only
            current_version = VERSION  # TODO autodetect by going up the qualname tree and trying getattr(part, '__version__')
        self.current_version = _Version(current_version)  #: current version
        self.reason = reason

        self.changed_version = self._parse_if_not_none(
            changed_version
        )  #: version in which the decorator was added
        self.cutoff_version = self._parse_if_not_none(
            cutoff_version
        )  #: version in which the decorator (and something?) shall be removed

    @classmethod
    def _parse_if_not_none(cls, version):
        if version is None:
            return None
        return _Version(version)

    @abstractmethod
    def __call__(self, func):
        """
        The actual implementation of decorator. Needs self.source to be set before it's called.
        """
        if self.cutoff_version and self.changed_version:
            assert (
                self.changed_version < self.cutoff_version
            ), f'{self.__class__.__name__} decorator is set to start renaming {self.WHAT} {self.source!r} starting at version {self.changed_version} and finishing in {self.cutoff_version}. It needs to start at a lower version and finish at a higher version.'


class AbstractDeprecator(AbstractVersionDecorator):
    ALTERNATIVE_DECORATOR = NotImplemented

    def __init__(self, target, *args, **kwargs):
        self.target = target
        super().__init__(*args, **kwargs)


class rename_argument(AbstractDeprecator):
    """
    Change the argument name to new one if old one is used, warns about deprecation in docs and through a warning.

    >>> @rename_argument('aaa', 'bbb', '0.1.0', '0.2.0')
    >>> def easy(bbb):
    >>>     return bbb

    >>> easy(aaa=5)
    'aaa' is a deprecated argument for 'easy' function/method - it was renamed to 'bbb' in version 0.1.0. Support for the old name is going to be dropped in 0.2.0.
    5
    >>>
    """

    WHAT = 'argument'
    ALTERNATIVE_DECORATOR = 'discourage_argument'

    def __init__(self, source, *args, **kwargs):
        self.source = source
        super().__init__(*args, **kwargs)

    def __call__(self, func):
        super().__call__(func)
        signature = inspect.signature(func)
        has_target_arg = self.target in signature.parameters or any(
            p.kind == p.VAR_KEYWORD for p in signature.parameters.values()
        )
        assert has_target_arg, f'{self.target!r} is not an argument of the decorated function so it cannot be remapped to from a deprecated parameter name'

        @wraps(func)
        def wrapper(*args, **kwargs):
            if self.source in kwargs:
                assert (
                    self.target not in kwargs
                ), f'both argument names were provided: {self.source!r} (deprecated) and {self.target!r} (new)'
                kwargs[self.target] = kwargs[self.source]
                del kwargs[self.source]
                info = f'{self.source!r} is a deprecated argument for {func.__name__!r} function/method - it was renamed to {self.target!r}'
                if self.changed_version:
                    info += f' in version {self.changed_version}'
                if self.cutoff_version:
                    info += f'. Support for the old name is going to be dropped in {self.cutoff_version}'

                warnings.warn(
                    f'{info}.',
                    DeprecationWarning,
                )
            return func(*args, **kwargs)

        return wrapper


class rename_function(AbstractDeprecator):
    """
    Warn about deprecation in docs and through a DeprecationWarning when used.  Use it to decorate a proxy function, like this:

    >>> def new(foobar):
    >>>     return foobar ** 2
    >>> @rename_function(new, '0.1.0', '0.2.0')
    >>> def old(foo, bar):
    >>>     return new(foo + bar)
    >>> old()
    'old' is deprecated since version 0.1.0 - it was moved to 'new', please switch to use that. The proxy for the old name is going to be removed in 0.2.0.
    123
    >>>

    """

    WHAT = 'function'
    ALTERNATIVE_DECORATOR = 'discourage_function'

    def __init__(self, target, *args, **kwargs):
        if callable(target):
            target = target.__name__
        super().__init__(target, *args, **kwargs)

    def __call__(self, func):
        self.source = func.__name__
        super().__call__(func)

        @wraps(func)
        def wrapper(*args, **kwargs):
            warnings.warn(
                f'{func.__name__!r} is deprecated since version {self.changed_version} - it was moved to {self.target!r}, please switch to use that. The proxy for the old name is going to be removed in {self.cutoff_version}.',
                DeprecationWarning,
            )
            return func(*args, **kwargs)

        return wrapper


class rename_method(rename_function):
    WHAT = 'method'
    ALTERNATIVE_DECORATOR = 'discourage_method'


class FeaturePreviewWarning(FutureWarning):
    """
    Feature Preview Warning

    Marks a feature, that is in "Feature Preview" state.
    Such features are not yet fully stable and are subject to change or even outright removal.
    Do not rely on them in production code.
    """