def _inv(a, b): a_range = VersionRange(a) b_range = VersionRange(b) self.assertTrue(~a_range == b_range) self.assertTrue(~b_range == a_range) self.assertTrue(a_range | b_range == VersionRange()) self.assertTrue(a_range & b_range is None)
def intersects(obj, range_): """Test if an object intersects with the given version range. Examples: # in package.py def commands(): # test a request if intersects(request.maya, '2019+'): info('requested maya allows >=2019.*') # tests if a resolved version intersects with given range if intersects(resolve.maya, '2019+') ... # same as above if intersects(resolve.maya.version, '2019+') ... # disable my cli tools if .foo.cli-0 was specified def commands(): if intersects(ephemerals.get('foo.cli', '1'), '1'): env.PATH.append('{root}/bin') Args: obj (VariantBinding or str): Object to test, either a variant, or requirement string (eg 'foo-1.2.3+'). range_ (str): Version range, eg '1.2+<2' Returns: bool: True if the object intersects the given range. """ range1 = VersionRange(range_) # eg 'if intersects(request.maya, ...)' if isinstance(obj, basestring): req = Requirement(obj) if req.conflict: return False range2 = req.range # eg 'if intersects(ephemerals.get_range('foo.cli', '1'), ...)' elif isinstance(obj, VersionRange): range2 = obj # eg 'if intersects(resolve.maya, ...)' elif isinstance(obj, VariantBinding): range2 = VersionRange(str(obj.version)) # eg 'if intersects(resolve.maya.version, ...)' elif isinstance(obj, VersionBinding): range2 = VersionRange(str(obj)) else: raise RuntimeError( "Invalid type %s passed as first arg to 'intersects'" % type(obj) ) return range1.intersects(range2)
def _and(a, b, c): _print("'%s' & '%s' == '%s'" % (a, b, c)) a_range = VersionRange(a) b_range = VersionRange(b) c_range = None if c is None else VersionRange(c) self.assertTrue(a_range & b_range == c_range) self.assertTrue(b_range & a_range == c_range) a_or_b = a_range | b_range a_and_b = a_range & b_range a_sub_b = a_range - b_range b_sub_a = b_range - a_range ranges = [a_and_b, a_sub_b, b_sub_a] ranges = [x for x in ranges if x] self.assertTrue(ranges[0].union(ranges[1:]) == a_or_b)
def _get_dest_pkg(self, name, version): return get_latest_package( name, range_=VersionRange("==" + version), paths=[self.dest_install_root], error=True )
def expand_version(version): rank = len(version) wildcard_found = False while version and str(version[-1]) in wildcard_map: token = wildcard_map[str(version[-1])] version = version.trim(len(version) - 1) if token == "**": if wildcard_found: # catches bad syntax '**.*' return None else: wildcard_found = True rank = 0 break wildcard_found = True if not wildcard_found: return None range_ = VersionRange(str(version)) package = get_latest_package(name=req.name, range_=range_, paths=paths) if package is None: return version if rank: return package.version.trim(rank) else: return package.version
def iter_packages(name, range_=None, paths=None): """Iterate over `Package` instances, in no particular order. Packages of the same name and version earlier in the search path take precedence - equivalent packages later in the paths are ignored. Packages are not returned in any specific order. Args: name (str): Name of the package, eg 'maya'. range_ (VersionRange or str): If provided, limits the versions returned to those in `range_`. paths (list of str, optional): paths to search for packages, defaults to `config.packages_path`. Returns: `Package` iterator. """ entries = _get_families(name, paths) seen = set() for repo, family_resource in entries: for package_resource in repo.iter_packages(family_resource): key = (package_resource.name, package_resource.version) if key in seen: continue seen.add(key) if range_: if isinstance(range_, basestring): range_ = VersionRange(range_) if package_resource.version not in range_: continue yield Package(package_resource)
def get_range(self, name, default=None): """Returns ephemeral version range object""" req_str = self._data.get(name) if req_str: return Requirement(req_str).range elif default is not None: return VersionRange(default) else: return None
def construct(cls, name, range=None): """Create a requirement directly from an object name and VersionRange. Args: name: Object name string. range: VersionRange object. If None, an unversioned requirement is created. """ other = Requirement(None) other.name_ = name other.range_ = VersionRange() if range is None else range return other
def __init__(self, s, invalid_bound_error=True): self.name_ = None self.range_ = None self.negate_ = False self.conflict_ = False self._str = None self.sep_ = '-' if s is None: return self.conflict_ = s.startswith('!') if self.conflict_: s = s[1:] elif s.startswith('~'): s = s[1:] self.negate_ = True self.conflict_ = True m = self.sep_regex.search(s) if m: i = m.start() self.name_ = s[:i] req_str = s[i:] if req_str[0] in ('-', '@', '#'): self.sep_ = req_str[0] req_str = req_str[1:] self.range_ = VersionRange(req_str, invalid_bound_error=invalid_bound_error) if self.negate_: self.range_ = ~self.range_ elif self.negate_: self.name_ = s # rare case - '~foo' equates to no effect self.range_ = None else: self.name_ = s self.range_ = VersionRange()
def __str__(self): pre_str = '~' if self.negate_ else ('!' if self.conflict_ else '') range_str = '' sep_str = '' range = self.range_ if self.negate_: range = ~range if range else VersionRange() if not range.is_any(): range_str = str(range) if range_str[0] not in ('=', '<', '>'): sep_str = self.sep_ return pre_str + self.name_ + sep_str + range_str
def __str__(self): if self._str is None: pre_str = '~' if self.negate_ else ('!' if self.conflict_ else '') range_str = '' sep_str = '' range_ = self.range_ if self.negate_: range_ = ~range_ if range_ else VersionRange() if not range_.is_any(): range_str = str(range_) if range_str[0] not in ('=', '<', '>'): sep_str = self.sep_ self._str = pre_str + self.name_ + sep_str + range_str return self._str
def _eq(a, b): _print("'%s' == '%s'" % (a, b)) a_range = VersionRange(a) b_range = VersionRange(b) self.assertTrue(a_range == b_range) self.assertTrue(a_range.issuperset(a_range)) self.assertTrue(a_range.issuperset(b_range)) self.assertTrue(VersionRange(str(a_range)) == a_range) self.assertTrue(VersionRange(str(b_range)) == a_range) self.assertTrue(hash(a_range) == hash(b_range)) a_ = a.replace('.', '-') a_ = a_.replace("--", "..") a_range_ = VersionRange(a_) self.assertTrue(a_range_ == a_range) self.assertTrue(hash(a_range_) == hash(a_range)) range_strs = a.split('|') ranges = [VersionRange(x) for x in range_strs] ranges_ = ranges[0].union(ranges[1:]) self.assertTrue(ranges_ == a_range) self.assertTrue(a_range | b_range == a_range) self.assertTrue(a_range - b_range is None) self.assertTrue(b_range - a_range is None) self.assertTrue(VersionRange() & a_range == a_range) self.assertTrue(b_range.span() & a_range == a_range) a_inv = a_range.inverse() self.assertTrue(a_inv == ~b_range) if a_inv: self.assertTrue(~a_inv == a_range) self.assertTrue(a_range | a_inv == VersionRange()) self.assertTrue(a_range & a_inv is None) a_ranges = a_range.split() a_range_ = a_ranges[0].union(a_ranges[1:]) self.assertTrue(a_range_ == b_range)
def get_package(name, version, paths=None): """Get an exact version of a package. Args: name (str): Name of the package, eg 'maya'. version (Version or str): Version of the package, eg '1.0.0' paths (list of str, optional): paths to search for package, defaults to `config.packages_path`. Returns: `Package` object, or None if the package was not found. """ if isinstance(version, basestring): range_ = VersionRange("==%s" % version) else: range_ = VersionRange.from_version(version, "==") it = iter_packages(name, range_, paths) try: return it.next() except StopIteration: return None
def assertPipRezEquivalent(pip_spec_str, rez_req_str): pip_spec = SpecifierSet(pip_spec_str) self.assertEqual( rez.utils.pip.pip_specifier_to_rez_requirement(pip_spec), VersionRange(rez_req_str))
def test_version_range(self): def _eq(a, b): _print("'%s' == '%s'" % (a, b)) a_range = VersionRange(a) b_range = VersionRange(b) self.assertTrue(a_range == b_range) self.assertTrue(a_range.issuperset(a_range)) self.assertTrue(a_range.issuperset(b_range)) self.assertTrue(VersionRange(str(a_range)) == a_range) self.assertTrue(VersionRange(str(b_range)) == a_range) self.assertTrue(hash(a_range) == hash(b_range)) a_ = a.replace('.', '-') a_ = a_.replace("--", "..") a_range_ = VersionRange(a_) self.assertTrue(a_range_ == a_range) self.assertTrue(hash(a_range_) == hash(a_range)) range_strs = a.split('|') ranges = [VersionRange(x) for x in range_strs] ranges_ = ranges[0].union(ranges[1:]) self.assertTrue(ranges_ == a_range) self.assertTrue(a_range | b_range == a_range) self.assertTrue(a_range - b_range is None) self.assertTrue(b_range - a_range is None) self.assertTrue(VersionRange() & a_range == a_range) self.assertTrue(b_range.span() & a_range == a_range) a_inv = a_range.inverse() self.assertTrue(a_inv == ~b_range) if a_inv: self.assertTrue(~a_inv == a_range) self.assertTrue(a_range | a_inv == VersionRange()) self.assertTrue(a_range & a_inv is None) a_ranges = a_range.split() a_range_ = a_ranges[0].union(a_ranges[1:]) self.assertTrue(a_range_ == b_range) def _and(a, b, c): _print("'%s' & '%s' == '%s'" % (a, b, c)) a_range = VersionRange(a) b_range = VersionRange(b) c_range = None if c is None else VersionRange(c) self.assertTrue(a_range & b_range == c_range) self.assertTrue(b_range & a_range == c_range) a_or_b = a_range | b_range a_and_b = a_range & b_range a_sub_b = a_range - b_range b_sub_a = b_range - a_range ranges = [a_and_b, a_sub_b, b_sub_a] ranges = [x for x in ranges if x] self.assertTrue(ranges[0].union(ranges[1:]) == a_or_b) def _inv(a, b): a_range = VersionRange(a) b_range = VersionRange(b) self.assertTrue(~a_range == b_range) self.assertTrue(~b_range == a_range) self.assertTrue(a_range | b_range == VersionRange()) self.assertTrue(a_range & b_range is None) # simple cases _print() _eq("", "") _eq("1", "1") _eq("1.0.0", "1.0.0") _eq("3+<3_", "3") _eq("_+<__", "_") _eq("1.2+<=2.0", "1.2..2.0") _eq("10+,<20", "10+<20") _eq("1+<1.0", "1+<1.0") _eq(">=2", "2+") # optimised cases _eq("3|3", "3") _eq("3|1", "1|3") _eq("5|3|1", "1|3|5") _eq("1|1_", "1+<1__") _eq("1|1_|1__", "1+,<1___") _eq("|", "") _eq("||", "||||||||") _eq("1|1_+", "1+") _eq("<1|1", "<1_") _eq("1+<3|3+<5", "1+<5") _eq(">4<6|1+<3", "1+<3|>4,<6") _eq("4+<6|1+<3|", "") _eq("4|2+", "2+") _eq("3|<5", "<5") _eq("<3|>3", ">3|<3") _eq("3+|<3", "") _eq("3+|<4", "") _eq("2+<=6|3+<5", "2..6") _eq("3+,<5|2+<=6", "2+<=6") _eq("2|2+", "2+") _eq("2|2.1+", "2+") _eq("2|<2.1", "<2_") _eq("3..3", "==3") _eq(">=3,<=3", "==3") # AND'ing _and("3", "3", "3") _and("1", "==1", "==1") _and("", "==1", "==1") _and("3", "4", None) _and("<3", "5+", None) _and("4+<6", "6+<8", None) _and("2+", "<=4", "2..4") _and("1", "1.0", "1.0") _and("4..6", "6+<8", "==6") # inverse _inv("3+", "<3") _inv("<=3", ">3") _inv("3.5", "<3.5|3.5_+") self.assertTrue(~VersionRange() is None) # odd (but valid) cases _eq(">", ">") # greater than the empty version _eq("+", "") # greater or equal to empty version (is all vers) _eq(">=", "") # equivalent to above _eq("<=", "==") # less or equal to empty version (is only empty) _eq("..", "==") # from empty version to empty version _eq("+<=", "==") # equivalent to above invalid_range = [ "4+<2", # lower bound greater than upper ">3<3", # both greater and less than same version ">3<=3", # greater and less or equal to same version "3+<3" # greater and equal to, and less than, same version ] for s in invalid_range: self.assertRaises(VersionError, VersionRange, s) invalid_syntax = [ "<", # less than the empty version "><", # both greater and less than empty version ">3>4", # both are lower bounds "<3<4", # both are upper bounds "<4>3", # upper bound before lower ",<4", # leading comma "4+,", # trailing comma "1>=", # pre-lower-op in post "+1", # post-lower-op in pre "4<", # pre-upper-op in post "1+<2<3" # more than two bounds ] for s in invalid_syntax: self.assertRaises(VersionError, VersionRange, s) # test simple logic self.assertTrue(VersionRange("").is_any()) self.assertTrue(VersionRange("2+<4").bounded()) self.assertTrue(VersionRange("2+").lower_bounded()) self.assertTrue(not VersionRange("2+").upper_bounded()) self.assertTrue(not VersionRange("2+").bounded()) self.assertTrue(VersionRange("<2").upper_bounded()) self.assertTrue(not VersionRange("<2").lower_bounded()) self.assertTrue(not VersionRange("<2").bounded()) # test range from version(s) v = Version("3") self.assertTrue( VersionRange.from_version(v, "eq") == VersionRange("==3")) self.assertTrue( VersionRange.from_version(v, "gt") == VersionRange(">3")) self.assertTrue( VersionRange.from_version(v, "gte") == VersionRange("3+")) self.assertTrue( VersionRange.from_version(v, "lt") == VersionRange("<3")) self.assertTrue( VersionRange.from_version(v, "lte") == VersionRange("<=3")) range1 = VersionRange.from_version(Version("2"), "gte") range2 = VersionRange.from_version(Version("4"), "lte") _eq(str(range1 & range2), "2..4") v2 = Version("6.0") v3 = Version("4") self.assertTrue( VersionRange.from_versions([v, v2, v3]) == VersionRange( "==3|==4|==6.0")) # test behaviour in sets def _eq2(a, b): _print("'%s' == '%s'" % (a, b)) self.assertTrue(a == b) a = VersionRange("1+<=2.5") b = VersionRange("1..2.5") c = VersionRange(">=5") d = VersionRange(">6.1.0") e = VersionRange("3.2") _eq2(set([a]) - set([a]), set()) _eq2(set([a]) - set([b]), set()) _eq2(set([a, a]) - set([a]), set()) _eq2(set([b, c, d, e]) - set([a]), set([c, d, e])) _eq2(set([b, c, e]) | set([c, d]), set([b, c, d, e])) _eq2(set([b, c]) & set([c, d]), set([c]))
def pip_specifier_to_rez_requirement(specifier): """Convert PEP440 version specifier to rez equivalent. See https://www.python.org/dev/peps/pep-0440/#version-specifiers Note that version numbers in the specifier are converted to rez equivalents at the same time. Thus a specifier like '<1.ALPHA2' becomes '<1.a2'. Note that the conversion is not necessarily exact - there are cases in PEP440 that have no equivalent in rez versioning. Most of these are specifiers that involve pre/post releases, which don't exist in rez (or rather, they do exist in the sense that '1.0.post1' is a valid rez version number, but it has no special meaning). Note also that the specifier is being converted into rez format, but in a way that still expresses how _pip_ interprets the specifier. For example, '==1' is a valid version range in rez, but '==1' has a different meaning to pip than it does to rez ('1.0' matches '==1' in pip, but not in rez). This is why '==1' is converted to '1+<1.1' in rez, rather than '==1'. Example conversions: | PEP440 | rez | |-------------|-------------| | ==1 | 1+<1.1 | | ==1.* | 1 | | >1 | 1.1+ | | <1 | <1 | | >=1 | 1+ | | <=1 | <1.1 | | ~=1.2 | 1.2+<2 | | ~=1.2.3 | 1.2.3+<1.3 | | !=1 | <1|1.1+ | | !=1.2 | <1.2|1.2.1+ | | !=1.* | <1|2+ | | !=1.2.* | <1.2|1.3+ | Args: specifier (`package.SpecifierSet`): Pip specifier. Returns: `VersionRange`: Equivalent rez version range. """ def is_release(rez_ver): parts = rez_ver.split('.') try: _ = int(parts[-1]) # noqa return True except: return False # 1 --> 2; 1.2 --> 1.3; 1.a2 -> 1.0 def next_ver(rez_ver): parts = rez_ver.split('.') if is_release(rez_ver): parts = parts[:-1] + [str(int(parts[-1]) + 1)] else: parts = parts[:-1] + ["0"] return '.'.join(parts) # 1 --> 1.1; 1.2 --> 1.2.1; 1.a2 --> 1.0 def adjacent_ver(rez_ver): if is_release(rez_ver): return rez_ver + ".1" else: parts = rez_ver.split('.') parts = parts[:-1] + ["0"] return '.'.join(parts) def convert_spec(spec): def parsed_rez_ver(): v = spec.version.replace(".*", "") return pip_to_rez_version(v) def fmt(txt): v = parsed_rez_ver() vnext = next_ver(v) vadj = adjacent_ver(v) return txt.format(V=v, VNEXT=vnext, VADJ=vadj) # ==1.* --> 1 if spec.operator == "==" and spec.version.endswith(".*"): return fmt("{V}") # ==1 --> 1+<1.1 if spec.operator == "==": return fmt("{V}+<{VADJ}") # >=1 --> 1+ if spec.operator == ">=": return fmt("{V}+") # >1 --> 1.1+ if spec.operator == ">": return fmt("{VADJ}+") # <= 1 --> <1.1 if spec.operator == "<=": return fmt("<{VADJ}") # <1 --> <1 if spec.operator == "<": return fmt("<{V}") # ~=1.2 --> 1.2+<2; ~=1.2.3 --> 1.2.3+<1.3 if spec.operator == "~=": v = Version(parsed_rez_ver()) v = v.trim(len(v) - 1) v_next = next_ver(str(v)) return fmt("{V}+<" + v_next) # !=1.* --> <1|2+; !=1.2.* --> <1.2|1.3+ if spec.operator == "!=" and spec.version.endswith(".*"): return fmt("<{V}|{VNEXT}+") # !=1 --> <1|1.1+; !=1.2 --> <1.2|1.2.1+ if spec.operator == "!=": return fmt("<{V}|{VADJ}+") raise PackageRequestError( "Don't know how to convert PEP440 specifier %r " "into rez equivalent" % specifier) # convert each spec into rez equivalent ranges = list(map(convert_spec, specifier)) # AND together ranges total_range = VersionRange(ranges[0]) for range_ in ranges[1:]: range_ = VersionRange(range_) total_range = total_range.intersection(range_) if total_range is None: raise PackageRequestError( "PEP440 specifier %r converts to a non-intersecting rez " "version range" % specifier) return total_range
def test_containment(self): # basic containment self.assertTrue(Version("3") in VersionRange("3+")) self.assertTrue(Version("5") in VersionRange("3..5")) self.assertTrue(Version("5_") not in VersionRange("3..5")) self.assertTrue(Version("3.0.0") in VersionRange("3+")) self.assertTrue(Version("3.0.0") not in VersionRange("3.1+")) self.assertTrue(Version("3") in VersionRange("<1|5|6|8|7|3|60+")) self.assertTrue(Version("3") in VersionRange("<1|5|6|8|7|==3|60+")) self.assertTrue(VersionRange("2.1+<4") in VersionRange("<4")) self.assertTrue(VersionRange("2.1..4") not in VersionRange("<4")) self.assertTrue(VersionRange("3") in VersionRange("3")) self.assertTrue(VersionRange("==3") in VersionRange("3")) self.assertTrue(VersionRange("3.5+<3_") in VersionRange("3")) self.assertTrue(VersionRange("3") not in VersionRange("4+<6")) self.assertTrue(VersionRange("3+<10") not in VersionRange("4+<6")) # iterating over sorted version list numbers = [2, 3, 5, 10, 11, 13, 14] versions = [Version(str(x)) for x in numbers] rev_versions = list(reversed(versions)) composite_range = VersionRange.from_versions(versions) entries = [(VersionRange(""), 7), (VersionRange("0+"), 7), (VersionRange("5+"), 5), (VersionRange("6+"), 4), (VersionRange("50+"), 0), (VersionRange(">5"), 4), (VersionRange("5"), 1), (VersionRange("6"), 0), (VersionRange("<5"), 2), (VersionRange("<6"), 3), (VersionRange("<50"), 7), (VersionRange("<=5"), 3), (VersionRange("<1"), 0), (VersionRange("2|9+"), 5), (VersionRange("3+<6|12+<13.5"), 3), (VersionRange("<1|20+"), 0), (VersionRange(">0<20"), 7)] for range_, count in entries: # brute-force containment tests matches = set(x for x in versions if x in range_) self.assertEqual(len(matches), count) # more optimal containment tests def _test_it(it): matches_ = set(version for contains, version in it if contains) self.assertEqual(matches_, matches) _test_it(range_.iter_intersect_test(versions)) _test_it(range_.iter_intersect_test(rev_versions, descending=True)) # throw in an intersection test self.assertEqual(composite_range.intersects(range_), (count != 0)) int_range = composite_range & range_ versions_ = [] if int_range is None else int_range.to_versions() self.assertEqual(set(versions_), matches) # throw in a superset test as well self.assertEqual(range_.issuperset(composite_range), (count == 7)) if count: self.assertTrue(composite_range.issuperset(int_range))