11from __future__ import annotations
22
3- from dataclasses import dataclass
43from functools import total_ordering
5- import re
6- from typing import List , Optional , Protocol , runtime_checkable , Union
7-
8-
9- _MODDED_PATTERN = "[0-9a-f]+-modded"
4+ from typing import Union
105
116
127@total_ordering
13- @dataclass
148class NodeVersion :
159 """NodeVersion
1610
@@ -29,196 +23,56 @@ class NodeVersion:
2923 See `strict_equal` if an exact match is required
3024 - `v23.11` < `v24.02`
3125 The oldest version is the smallest
26+ - `vd6fa78c`
27+ This is an untagged version, such as in CI. This is assumed to be the latest, greater than
28+ any test.
3229 """
33-
34- version : str
35-
36- def to_parts (self ) -> List [_NodeVersionPart ]:
37- parts = self .version [1 :].split ("." )
38- # If the first part contains a v we will ignore it
39- if not parts [0 ][0 ].isdigit ():
40- parts [0 ] = parts [1 :]
41-
42- return [_NodeVersionPart .parse (p ) for p in parts ]
43-
44- def strict_equal (self , other : NodeVersion ) -> bool :
45- if not isinstance (other , NodeVersion ):
46- raise TypeError (
47- "`other` is expected to be of type `NodeVersion` but is `{type(other)}`"
48- )
30+ def __init__ (self , version : str ):
31+ # e.g. v24.11-225-gda793e66b9
32+ if version .startswith ('v' ):
33+ version = version [1 :]
34+ version = version .split ('-' )[0 ]
35+ parts = version .split ('.' )
36+ # rc is considered "close enough"
37+ if 'rc' in parts [- 1 ]:
38+ parts [- 1 ] = parts [- 1 ].split ('rc' )[0 ]
39+
40+ self .parts : int = []
41+
42+ # Single part? It's a git version, so treat it as the future.
43+ if len (parts ) == 1 :
44+ self .parts .append (100 )
4945 else :
50- return self .version == other .version
46+ for p in parts :
47+ self .parts .append (int (p ))
5148
5249 def __eq__ (self , other : Union [NodeVersion , str ]) -> bool :
5350 if isinstance (other , str ):
5451 other = NodeVersion (other )
5552 if not isinstance (other , NodeVersion ):
5653 return False
5754
58- if self .strict_equal (other ):
59- return True
60- elif re .match (_MODDED_PATTERN , self .version ):
55+ if len (self .parts ) != len (other .parts ):
6156 return False
62- else :
63- self_parts = [p .num for p in self .to_parts ()]
64- other_parts = [p .num for p in other .to_parts ()]
65-
66- if len (self_parts ) != len (other_parts ):
57+ for a , b in zip (self .parts , other .parts ):
58+ if a != b :
6759 return False
68-
69- for ps , po in zip (self_parts , other_parts ):
70- if ps != po :
71- return False
72- return True
60+ return True
7361
7462 def __lt__ (self , other : Union [NodeVersion , str ]) -> bool :
7563 if isinstance (other , str ):
7664 other = NodeVersion (other )
7765 if not isinstance (other , NodeVersion ):
7866 return NotImplemented
7967
80- # If we are in CI the version will by a hex ending on modded
81- # We will assume it is the latest version
82- if re .match (_MODDED_PATTERN , self .version ):
83- return False
84- elif re .match (_MODDED_PATTERN , other .version ):
85- return True
86- else :
87- self_parts = [p .num for p in self .to_parts ()]
88- other_parts = [p .num for p in other .to_parts ()]
89-
90- # zip truncates to shortes length
91- for sp , op in zip (self_parts , other_parts ):
92- if sp < op :
93- return True
94- if sp > op :
95- return False
96-
97- # If the initial parts are all equal the longest version is the biggest
98- #
99- # self = 'v24.02'
100- # other = 'v24.02.1'
101- return len (self_parts ) < len (other_parts )
102-
103- def matches (self , version_spec : VersionSpecLike ) -> bool :
104- """Returns True if the version matches the spec
105-
106- The `version_spec` can be represented as a string and has 8 operators
107- which are `=`, `===`, `!=`, `!===`, `<`, `<=`, `>`, `>=`.
108-
109- The `=` is the equality operator. The verson_spec `=v24.02` matches
110- all versions that equal `v24.02` including release candidates such as `v24.02rc1`.
111- You can use the strict-equality operator `===` if strict equality is required.
112-
113- Specifiers can be combined by separating the with a comma ','. The `version_spec`
114- `>=v23.11, <v24.02" includes any version which is greater than or equal to `v23.11`
115- and smaller than `v24.02`.
116- """
117- spec = VersionSpec .parse (version_spec )
118- return spec .matches (self )
119-
120-
121- @dataclass
122- class _NodeVersionPart :
123- num : int
124- text : Optional [str ] = None
125-
126- @classmethod
127- def parse (cls , part : str ) -> _NodeVersionPart :
128- # We assume all parts start with a number and are followed by a text
129- # E.g: v24.01rc2 has two parts
130- # - "24" -> num = 24, text = None
131- # - "01rc" -> num = 01, text = "rc"
132-
133- number = re .search (r"\d+" , part ).group ()
134- text = part [len (number ):]
135- text_opt = text if text != "" else None
136- return _NodeVersionPart (int (number ), text_opt )
137-
138-
139- @runtime_checkable
140- class VersionSpec (Protocol ):
141- def matches (self , other : NodeVersionLike ) -> bool :
142- ...
143-
144- @classmethod
145- def parse (cls , spec : VersionSpecLike ) -> VersionSpec :
146- if isinstance (spec , VersionSpec ):
147- return spec
148- else :
149- parts = [p .strip () for p in spec .split ("," )]
150- subspecs = [_CompareSpec .parse (p ) for p in parts ]
151- return _AndVersionSpecifier (subspecs )
152-
153-
154- @dataclass
155- class _AndVersionSpecifier (VersionSpec ):
156- specs : List [VersionSpec ]
157-
158- def matches (self , other : NodeVersionLike ) -> bool :
159- for spec in self .specs :
160- if not spec .matches (other ):
68+ # We want a zero-padded zip. Pad both to make one.
69+ totlen = max (len (self .parts ), len (other .parts ))
70+ for a , b in zip (self .parts + [0 ] * totlen , other .parts + [0 ] * totlen ):
71+ if a < b :
72+ return True
73+ if a > b :
16174 return False
162- return True
163-
164-
165- _OPERATORS = [
166- "===" , # Strictly equal
167- "!===" , # not strictly equal
168- "=" , # Equal
169- ">=" , # Greater or equal
170- "<=" , # Less or equal
171- "<" , # less
172- ">" , # greater than
173- "!=" , # not equal
174- ]
175-
176-
177- @dataclass
178- class _CompareSpec (VersionSpec ):
179- operator : str
180- version : NodeVersion
181-
182- def __post_init__ (self ):
183- if self .operator not in _OPERATORS :
184- raise ValueError (f"Invalid operator '{ self .operator } '" )
185-
186- def matches (self , other : NodeVersionLike ):
187- if isinstance (other , str ):
188- other = NodeVersion (other )
189- if self .operator == "===" :
190- return other .strict_equal (self .version )
191- if self .operator == "!===" :
192- return not other .strict_equal (self .version )
193- if self .operator == "=" :
194- return other == self .version
195- if self .operator == ">=" :
196- return other >= self .version
197- if self .operator == "<=" :
198- return other <= self .version
199- if self .operator == "<" :
200- return other < self .version
201- if self .operator == ">" :
202- return other > self .version
203- if self .operator == "!=" :
204- return other != self .version
205- else :
206- ValueError ("Unknown operator" )
207-
208- @classmethod
209- def parse (cls , spec_string : str ) -> _CompareSpec :
210- spec_string = spec_string .strip ()
211-
212- for op in _OPERATORS :
213- if spec_string .startswith (op ):
214- version = spec_string [len (op ):]
215- version = version .strip ()
216- return _CompareSpec (op , NodeVersion (version ))
217-
218- raise ValueError (f"Failed to parse '{ spec_string } '" )
219-
75+ return False
22076
221- NodeVersionLike = Union [NodeVersion , str ]
222- VersionSpecLike = Union [VersionSpec , str ]
22377
224- __all__ = [NodeVersion , NodeVersionLike , VersionSpec , VersionSpecLike ]
78+ __all__ = [NodeVersion ]
0 commit comments