File: balrog.py

package info (click to toggle)
firefox 147.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 4,683,324 kB
  • sloc: cpp: 7,607,156; javascript: 6,532,492; ansic: 3,775,158; python: 1,415,368; xml: 634,556; asm: 438,949; java: 186,241; sh: 62,751; makefile: 18,079; objc: 13,092; perl: 12,808; yacc: 4,583; cs: 3,846; pascal: 3,448; lex: 1,720; ruby: 1,003; php: 436; lisp: 258; awk: 247; sql: 66; sed: 54; csh: 10; exp: 6
file content (155 lines) | stat: -rw-r--r-- 5,072 bytes parent folder | download | duplicates (16)
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
"""Defines characteristics of a Balrog release name.

Balrog is the server that delivers Firefox and Thunderbird updates. Release names follow
the pattern "{product}-{version}-build{build_number}"

Examples:
    .. code-block:: python

        from mozilla_version.balrog import BalrogReleaseName

        balrog_release = BalrogReleaseName.parse('firefox-60.0.1-build1')

        balrog_release.product                 # firefox
        balrog_release.version.major_number    # 60
        str(balrog_release)                    # 'firefox-60.0.1-build1'

        previous_release = BalrogReleaseName.parse('firefox-60.0-build2')
        previous_release < balrog_release      # True

        BalrogReleaseName.parse('60.0.1')           # raises PatternNotMatchedError
        BalrogReleaseName.parse('firefox-60.0.1')   # raises PatternNotMatchedError

        # Releases can be built thanks to version classes like FirefoxVersion
        BalrogReleaseName('firefox', FirefoxVersion(60, 0, 1))  # 'firefox-60.0-build1'

"""

import re

import attr

from mozilla_version.errors import PatternNotMatchedError
from mozilla_version.gecko import (
    DeveditionVersion,
    FennecVersion,
    FirefoxVersion,
    GeckoVersion,
    ThunderbirdVersion,
)
from mozilla_version.parser import get_value_matched_by_regex

_VALID_ENOUGH_BALROG_RELEASE_PATTERN = re.compile(
    r"^(?P<product>[a-z]+)-(?P<version>.+)$", re.IGNORECASE
)


_SUPPORTED_PRODUCTS = {
    "firefox": FirefoxVersion,
    "devedition": DeveditionVersion,
    "fennec": FennecVersion,
    "thunderbird": ThunderbirdVersion,
}


def _supported_product(string):
    product = string.lower()
    if product not in _SUPPORTED_PRODUCTS:
        raise PatternNotMatchedError(string, patterns=("unknown product",))
    return product


def _products_must_be_identical(method):
    def checker(this, other):
        if this.product != other.product:
            raise ValueError(f'Cannot compare "{this.product}" and "{other.product}"')
        return method(this, other)

    return checker


@attr.s(frozen=True, eq=False, hash=True)
class BalrogReleaseName:
    """Class that validates and handles Balrog release names.

    Raises:
        PatternNotMatchedError: if a parsed string doesn't match the pattern of a valid
            release
        MissingFieldError: if a mandatory field is missing in the string. Mandatory
            fields are
            `product`, `major_number`, `minor_number`, and `build_number`
        ValueError: if an integer can't be cast or is not (strictly) positive
        TooManyTypesError: if the string matches more than 1 `VersionType`
        NoVersionTypeError: if the string matches none.

    """

    product = attr.ib(type=str, converter=_supported_product)
    version = attr.ib(type=GeckoVersion)

    def __attrs_post_init__(self):
        """Ensure attributes are sane all together."""
        if self.version.build_number is None:
            raise PatternNotMatchedError(self, patterns=("build_number must exist",))

    @classmethod
    def parse(cls, release_string):
        """Construct an object representing a valid Firefox version number."""
        regex_matches = _VALID_ENOUGH_BALROG_RELEASE_PATTERN.match(release_string)
        if regex_matches is None:
            raise PatternNotMatchedError(
                release_string, (_VALID_ENOUGH_BALROG_RELEASE_PATTERN,)
            )

        product = get_value_matched_by_regex("product", regex_matches, release_string)
        try:
            version_class = _SUPPORTED_PRODUCTS[product.lower()]
        except KeyError:
            raise PatternNotMatchedError(
                release_string, patterns=("unknown product",)
            ) from None

        version_string = get_value_matched_by_regex(
            "version", regex_matches, release_string
        )
        version = version_class.parse(version_string)

        return cls(product, version)

    def __str__(self):
        """Implement string representation.

        Computes a new string based on the given attributes.
        """
        version_string = str(self.version).replace("build", "-build")
        return f"{self.product}-{version_string}"

    @_products_must_be_identical
    def __eq__(self, other):
        """Implement `==` operator."""
        return self.version == other.version

    @_products_must_be_identical
    def __ne__(self, other):
        """Implement `!=` operator."""
        return self.version != other.version

    @_products_must_be_identical
    def __lt__(self, other):
        """Implement `<` operator."""
        return self.version < other.version

    @_products_must_be_identical
    def __le__(self, other):
        """Implement `<=` operator."""
        return self.version <= other.version

    @_products_must_be_identical
    def __gt__(self, other):
        """Implement `>` operator."""
        return self.version > other.version

    @_products_must_be_identical
    def __ge__(self, other):
        """Implement `>=` operator."""
        return self.version >= other.version