11type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final' ;
2- type ComparisonOperator = '~=' | '==' | '!=' | '>=' | '<=' | '>' | '<' ;
3- type ParsedClause =
4- | { readonly operator : '===' ; readonly expected : string }
5- | { readonly operator : '==' | '!=' ; readonly wildcard : readonly number [ ] }
6- | { readonly operator : ComparisonOperator ; readonly expected : PythonVersion } ;
72
83export class PythonVersion {
94 private static readonly VERSION_PATTERN =
10- / ^ ( \d + ) (?: \. ( \d + ) ) ? (?: \. ( \d + ) ) ? (?: (?: \. ( a l p h a | b e t a | c a n d i d a t e | f i n a l ) \. ( \d + ) ) | (?: ( a | b | r c ) ( \d + ) ) ) ? $ / i;
5+ / ^ (?< major > \d + ) (?: \. (?< minor > \d + ) ) ? (?: \. (?< patch > \d + ) ) ? (?: (?: \. (?< longLevel > a l p h a | b e t a | c a n d i d a t e | f i n a l ) \. (?< longSerial > \d + ) ) | (?: (?< shortLevel > a | b | r c ) (?< shortSerial > \d + ) ) ) ? $ / i;
116
127 private static readonly WILDCARD_PATTERN = / ^ ( \d + ) (?: \. ( \d + ) ) ? (?: \. ( \d + ) ) ? \. \* $ / ;
138
@@ -47,13 +42,14 @@ export class PythonVersion {
4742 throw new TypeError ( `Invalid Python version: ${ version } ` ) ;
4843 }
4944
45+ const groups = match . groups ! ;
5046 this . original = normalizedVersion ;
51- this . releaseComponentCount = match [ 3 ] !== undefined ? 3 : match [ 2 ] !== undefined ? 2 : 1 ;
52- this . major = parseNumericComponent ( match [ 1 ] , version ) ;
53- this . minor = parseNumericComponent ( match [ 2 ] , version ) ;
54- this . patch = parseNumericComponent ( match [ 3 ] , version ) ;
55- this . releaseLevel = PythonVersion . normalizeReleaseLevel ( match [ 4 ] ?? match [ 6 ] ) ;
56- this . releaseSerial = parseNumericComponent ( match [ 5 ] ?? match [ 7 ] , version ) ;
47+ this . releaseComponentCount = groups . patch !== undefined ? 3 : groups . minor !== undefined ? 2 : 1 ;
48+ this . major = parseNumericComponent ( groups . major , version ) ;
49+ this . minor = parseNumericComponent ( groups . minor , version ) ;
50+ this . patch = parseNumericComponent ( groups . patch , version ) ;
51+ this . releaseLevel = PythonVersion . normalizeReleaseLevel ( groups . longLevel ?? groups . shortLevel ) ;
52+ this . releaseSerial = parseNumericComponent ( groups . longSerial ?? groups . shortSerial , version ) ;
5753 if ( this . releaseLevel === 'final' && this . releaseSerial !== 0 ) {
5854 throw new TypeError ( `Invalid Python version: ${ version } ` ) ;
5955 }
@@ -103,17 +99,6 @@ export class PythonVersion {
10399 ) ;
104100 }
105101
106- /**
107- * Tests whether this version matches a release-prefix wildcard.
108- *
109- * @param wildcard A terminal wildcard such as `3.*`, `3.14.*`, or `3.14.0.*`.
110- * @returns `true` when all components before the wildcard match, otherwise `false`.
111- */
112- satisfiesWildcard ( wildcard : unknown ) : boolean {
113- const expected = PythonVersion . parseWildcard ( wildcard ) ;
114- return expected !== undefined && this . matchesReleaseComponents ( expected ) ;
115- }
116-
117102 /**
118103 * Tests whether this version satisfies a Python version specifier.
119104 *
@@ -123,21 +108,27 @@ export class PythonVersion {
123108 * comparisons, matching the inline-script interpreter behavior.
124109 *
125110 * @param specifier A version specifier such as `>=3.11,<3.14` or `==3.12.*`.
126- * @returns `true` when every clause matches; otherwise `false` .
111+ * @returns Whether every clause matches, or `undefined` when the specifier is invalid .
127112 */
128- satisfies ( specifier : unknown ) : boolean {
129- return this . matchSpecifier ( specifier ) ?? false ;
130- }
113+ satisfies ( specifier : unknown ) : boolean | undefined {
114+ if ( typeof specifier !== 'string' ) {
115+ return undefined ;
116+ }
131117
132- /**
133- * Evaluates a Python version specifier while distinguishing invalid syntax.
134- *
135- * @param specifier The specifier to evaluate.
136- * @returns Whether this version matches, or `undefined` for an invalid specifier.
137- */
138- matchSpecifier ( specifier : unknown ) : boolean | undefined {
139- const clauses = PythonVersion . parseSpecifier ( specifier ) ;
140- return clauses ?. every ( ( clause ) => this . satisfiesClause ( clause ) ) ;
118+ const clauses = specifier . split ( ',' ) . map ( ( clause ) => clause . trim ( ) ) ;
119+ if ( clauses . some ( ( clause ) => clause . length === 0 ) ) {
120+ return undefined ;
121+ }
122+
123+ let satisfiesAll = true ;
124+ for ( const clause of clauses ) {
125+ const result = this . matchClause ( clause ) ;
126+ if ( result === undefined ) {
127+ return undefined ;
128+ }
129+ satisfiesAll &&= result ;
130+ }
131+ return satisfiesAll ;
141132 }
142133
143134 /** Returns the normalized Python version representation. */
@@ -159,24 +150,7 @@ export class PythonVersion {
159150 return value ? ( PythonVersion . RELEASE_LEVEL_ALIASES [ value . toLowerCase ( ) ] ?? 'final' ) : 'final' ;
160151 }
161152
162- private static parseSpecifier ( specifier : unknown ) : readonly ParsedClause [ ] | undefined {
163- if ( typeof specifier !== 'string' ) {
164- return undefined ;
165- }
166-
167- const clauses = specifier
168- . split ( ',' )
169- . map ( ( clause ) => clause . trim ( ) )
170- . filter ( ( clause ) => clause . length > 0 ) ;
171- if ( clauses . length === 0 ) {
172- return undefined ;
173- }
174-
175- const parsed = clauses . map ( ( clause ) => PythonVersion . parseClause ( clause ) ) ;
176- return parsed . every ( ( clause ) : clause is ParsedClause => clause !== undefined ) ? parsed : undefined ;
177- }
178-
179- private static parseClause ( clause : string ) : ParsedClause | undefined {
153+ private matchClause ( clause : string ) : boolean | undefined {
180154 const match = PythonVersion . SPECIFIER_PATTERN . exec ( clause ) ;
181155 if ( ! match ) {
182156 return undefined ;
@@ -185,31 +159,24 @@ export class PythonVersion {
185159 const operator = match [ 1 ] ;
186160 const expected = match [ 2 ] . trim ( ) ;
187161 if ( operator === '===' ) {
188- return expected . length > 0 ? { operator , expected } : undefined ;
162+ return expected ? this . original . replace ( / ^ v / i , '' ) === expected . replace ( / ^ v / i , '' ) : undefined ;
189163 }
190164 if ( expected . endsWith ( '.*' ) ) {
191165 const wildcard = PythonVersion . parseWildcard ( expected ) ;
192- return ( operator === '==' || operator === '!=' ) && wildcard ? { operator, wildcard } : undefined ;
166+ if ( ( operator !== '==' && operator !== '!=' ) || ! wildcard ) {
167+ return undefined ;
168+ }
169+ const matches = this . matchesReleaseComponents ( wildcard ) ;
170+ return operator === '==' ? matches : ! matches ;
193171 }
194172
195173 const expectedVersion = PythonVersion . tryParse ( expected ) ;
196174 if ( ! expectedVersion || ( operator === '~=' && expectedVersion . releaseComponentCount < 2 ) ) {
197175 return undefined ;
198176 }
199- return { operator : operator as ComparisonOperator , expected : expectedVersion } ;
200- }
201-
202- private satisfiesClause ( clause : ParsedClause ) : boolean {
203- if ( clause . operator === '===' ) {
204- return this . original . replace ( / ^ v / i, '' ) === clause . expected . replace ( / ^ v / i, '' ) ;
205- }
206- if ( 'wildcard' in clause ) {
207- const matches = this . matchesReleaseComponents ( clause . wildcard ) ;
208- return clause . operator === '==' ? matches : ! matches ;
209- }
210177
211- const comparison = this . compareReleaseTo ( clause . expected ) ;
212- switch ( clause . operator ) {
178+ const comparison = this . compareReleaseTo ( expectedVersion ) ;
179+ switch ( operator ) {
213180 case '==' :
214181 return comparison === 0 ;
215182 case '!=' :
@@ -226,7 +193,7 @@ export class PythonVersion {
226193 return (
227194 comparison >= 0 &&
228195 this . matchesReleaseComponents (
229- clause . expected . releaseComponents . slice ( 0 , clause . expected . releaseComponentCount - 1 ) ,
196+ expectedVersion . releasePrefix ( expectedVersion . releaseComponentCount - 1 ) ,
230197 )
231198 ) ;
232199 default :
@@ -235,21 +202,23 @@ export class PythonVersion {
235202 }
236203
237204 private compareReleaseTo ( other : PythonVersion ) : number {
238- for ( let index = 0 ; index < this . releaseComponents . length ; index ++ ) {
239- const comparison = compareNumbers ( this . releaseComponents [ index ] , other . releaseComponents [ index ] ) ;
240- if ( comparison !== 0 ) {
241- return comparison ;
242- }
243- }
244- return 0 ;
205+ return (
206+ compareNumbers ( this . major , other . major ) ||
207+ compareNumbers ( this . minor , other . minor ) ||
208+ compareNumbers ( this . patch , other . patch )
209+ ) ;
245210 }
246211
247- private get releaseComponents ( ) : readonly number [ ] {
248- return [ this . major , this . minor , this . patch ] ;
212+ private releasePrefix ( length : number ) : readonly number [ ] {
213+ return [ this . major , this . minor , this . patch ] . slice ( 0 , length ) ;
249214 }
250215
251216 private matchesReleaseComponents ( expected : readonly number [ ] ) : boolean {
252- return expected . every ( ( component , index ) => component === this . releaseComponents [ index ] ) ;
217+ return (
218+ expected [ 0 ] === this . major &&
219+ ( expected . length < 2 || expected [ 1 ] === this . minor ) &&
220+ ( expected . length < 3 || expected [ 2 ] === this . patch )
221+ ) ;
253222 }
254223
255224 private static parseWildcard ( wildcard : unknown ) : number [ ] | undefined {
0 commit comments