66
77import operator
88from functools import total_ordering
9- import attr
109
10+ import attr
1111from univers .utils import remove_spaces
1212from univers .versions import Version
1313
@@ -125,8 +125,14 @@ def from_string(cls, string, version_class):
125125 a ``version_class`` Version class.
126126 """
127127 constraint_string = remove_spaces (string )
128- comparator , version = cls .split (constraint_string )
129128
129+ # A version range specifier contains only printable ASCII letters, digits and
130+ # punctuation.
131+ is_ascii = len (constraint_string ) + 2 == len (ascii (constraint_string ))
132+ if not is_ascii :
133+ raise ValueError (f"Invalid non ASCII characters: { string !r} " )
134+
135+ comparator , version = cls .split (constraint_string )
130136 if comparator not in COMPARATORS :
131137 raise ValueError (f"Unknown comparator: { comparator !r} " )
132138
@@ -230,10 +236,18 @@ def validate(cls, constraints):
230236 if not all (isinstance (c , VersionConstraint ) for c in constraints ):
231237 raise ValueError (f"{ constraints !r} can contain only VersionConstraint" )
232238
239+ # Versions are unique. Each ``version`` must be unique in a range and can
240+ # occur only once in any ``<version-constraint>`` of a range specifier,
241+ # irrespective of its comparators. Tools must report an error for duplicated
242+ # versions.
233243 if len (set (c .version for c in constraints )) != len (constraints ):
234244 raise ValueError (f"{ constraints !r} cannot contain duplicated Version" )
235245
246+ # Constraints are sorted by version**. The canonical ordering is the versions
247+ # order. The ordering of ``<version-constraint>`` is not significant otherwise
248+ # but this sort order is needed when check if a version is contained in a range.
236249 constraints .sort ()
250+
237251 return validate_comparators (constraints )
238252
239253 @classmethod
@@ -282,17 +296,28 @@ def validate_comparators(constraints):
282296 - ">" and ">=" can only be followed by one of "<", "<="
283297 """
284298
299+ # Starting from a de-duplicated and sorted list of constraints, these extra rules
300+ # apply to the comparators of any two contiguous constraints to be valid:
301+
302+ # There is only one star: "*" must only occur once and alone in a range,
303+ # without any other constraint or version.
285304 if any (c .comparator == "*" for c in constraints ):
286305 if len (constraints ) != 1 :
287306 raise ValueError (f"Invalid { constraints !r} : can contain only one star '*'" )
288307 return True
289308
290- # discard != that can occur anywhere
309+ # "!=" constraint can be followed by a constraint using any comparator, i.e.,
310+ # any of "=", "!=", ">", ">=", "<", "<=" as comparator (or no constraint).
311+
312+ # Ignoring all constraints with "!=" comparators:
313+ # --> discard != that can occur anywhere
291314 constraints = [c for c in constraints if c .comparator != "!=" ]
292315 if not constraints :
293316 return True
294317
295- # check that equals is followed only by "=", ">", ">="
318+ # A "=" constraint must be followed only by a constraint with one of "=", ">",
319+ # ">=" as comparator (or no constraint).
320+ # --> check that equals is followed only by "=", ">", ">="
296321 invalid_equal = [
297322 (cur , nxt )
298323 for cur , nxt in pairwise (constraints )
@@ -305,16 +330,22 @@ def validate_comparators(constraints):
305330 f"Invalid { c !r} : where { i !r} " "cannot contain an equal = followed by either < or <="
306331 )
307332
308- # discard = that have been validated above
333+ # And ignoring all constraints with "=" or "!=" comparators:
334+ # --> discard = that have been validated above
309335 constraints = [c for c in constraints if c .comparator != "=" ]
310336 if not constraints :
311337 return True
312338
313- # from now on this must be an alternation of greater/lesser
339+ # the sequence of constraint comparators must be an alternation of greater
340+ # and lesser comparators:
341+ # --> from now on this must be an alternation of greater/lesser
314342 for cur_constraint , nxt_constraint in pairwise (constraints ):
315343 cur_comp = cur_constraint .comparator
316344 nxt_comp = nxt_constraint .comparator
317345
346+ # "<" and "<=" must be followed by one of ">", ">=" (or no constraint).
347+ # ">" and ">=" must be followed by one of "<", "<=" (or no constraint).
348+ # Tools must report an error for such invalid ranges.
318349 if (cur_comp in ("<" , "<=" ) and nxt_comp not in (">" , ">=" )) or (
319350 cur_comp in (">" , ">=" ) and nxt_comp not in ("<" , "<=" )
320351 ):
@@ -373,11 +404,11 @@ def simplify_constraints(constraints):
373404 # discard current constraint
374405 constraints .pop (i )
375406 # Previous constraint becomes current if if exists.
376- if i :
407+ if i > 0 :
377408 i -= 1
378409
379410 # If there is a previous constraint:
380- if i :
411+ if i > 0 :
381412
382413 prv = constraints [i - 1 ]
383414 prv_comp = prv .comparator
0 commit comments