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 set_packages(self, packages): package_paths = self.context_model.packages_path self.help_1 = PackageHelp(self.variant.name, paths=package_paths) self.tab.setTabEnabled(0, self.help_1.success) if self.help_1.success: self._apply_help(self.help_1, 0) label = "latest help (%s)" % self.help_1.package.qualified_name self.tab.setTabText(0, label) exact_range = VersionRange.from_version(self.variant.version, "==") self.help_2 = PackageHelp(self.variant.name, exact_range, paths=package_paths) self.tab.setTabEnabled(1, self.help_2.success) label = None if self.help_2.success: self._apply_help(self.help_2, 1) label = "help for %s" if label: self.tab.setTabText(1, label % self.help_2.package.qualified_name) if self.help_1.success or self.help_2.success: self.tab.show() else: self.no_help_label.show()
def set_variant(self, variant): self.tab.setCurrentIndex(0) self.stop_loading_packages() self.clear() self.variant = variant if self.variant is None: return package_paths = self.context_model.packages_path if self.variant.wrapped.location not in package_paths: txt = "not on the package search path" self.label.setText(txt) return self.setEnabled(True) range_ = None if self.reference_variant and self.reference_variant.name == variant.name: versions = sorted( [variant.version, self.reference_variant.version]) range_ = VersionRange.as_span(*versions) self.load_packages(package_paths=package_paths, package_name=variant.name, range_=range_, package_attributes=("timestamp", ))
def set_variant(self, variant): self.tab.setCurrentIndex(0) self.stop_loading_packages() self.clear() self.variant = variant if self.variant is None: return package_paths = self.context_model.packages_path if self.variant.wrapped.location not in package_paths: txt = "not on the package search path" self.label.setText(txt) return self.setEnabled(True) range_ = None if self.reference_variant and self.reference_variant.name == variant.name: versions = sorted([variant.version, self.reference_variant.version]) range_ = VersionRange.as_span(*versions) self.load_packages(package_paths=package_paths, package_name=variant.name, range_=range_, package_attributes=("timestamp",))
def _get_dest_pkg(self, name, version): return get_latest_package( name, range_=VersionRange("==" + version), paths=[self.dest_install_root], error=True )
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 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 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 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 __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 __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 __init__(self, s): self.name_ = None self.range_ = None self.negate_ = False self.conflict_ = False 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) 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 _set_variant(self, variant, preloaded_packages=None): self.clear() hh = self.horizontalHeader() self.setHorizontalHeaderLabels(["path", "released"]) hh.setResizeMode(0, QtGui.QHeaderView.Interactive) hh.setStretchLastSection(True) hh.setVisible(True) package_paths = self.context_model.packages_path package_filter = PackageFilterList.from_pod(self.context_model.package_filter) if variant and variant.wrapped.location in package_paths: self.version_index = -1 self.reference_version_index = -1 reference_version = None range_ = None if self.reference_variant and self.reference_variant.name == variant.name: reference_version = self.reference_variant.version versions = sorted([reference_version, variant.version]) range_ = VersionRange.as_span(*versions) timestamp = self.context().timestamp if preloaded_packages is not None: if range_ is None: packages = preloaded_packages else: packages = [x for x in preloaded_packages if x.version in range_] else: it = iter_packages(name=variant.name, paths=package_paths, range_=range_) packages = sorted(it, key=lambda x: x.version, reverse=True) self.setRowCount(len(packages)) brush = self.palette().brush(QtGui.QPalette.Active, QtGui.QPalette.Base) for row, package in enumerate(packages): version_str = str(package.version) + ' ' item = QtGui.QTableWidgetItem(version_str) item.setTextAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.setVerticalHeaderItem(row, item) if package.version == variant.version: self.version_index = row update_font(item, bold=True) if reference_version is not None \ and package.version == reference_version: self.reference_version_index = row update_font(item, bold=True, italic=True) def _item(): item_ = QtGui.QTableWidgetItem() item_.setBackground(brush) # get rid of mouse-hover coloring return item_ if package.timestamp: release_str = get_timestamp_str(package.timestamp) in_future = (package.timestamp > timestamp) else: release_str = '-' in_future = False item = _item() txt = package.uri + " " icons = [] if in_future: icon = get_icon_widget( "clock_warning", "package did not exist at time of resolve") icons.append(icon) rule = package_filter.excludes(package) if rule: icon = get_icon_widget( "excluded", "package was excluded by rule %s" % str(rule)) icons.append(icon) if icons: label = QtGui.QLabel(txt) pane = create_pane(icons + [label, None], True, compact=True) self.setCellWidget(row, 0, pane) else: item.setText(txt) self.setItem(row, 0, item) item = _item() item.setText(release_str) self.setItem(row, 1, item) vh = self.verticalHeader() vh.setVisible(True) self.resizeColumnsToContents() hh.setStretchLastSection(True) self.update() self.allow_selection = True self.selectRow(self.version_index) self.allow_selection = False self.variant = variant
def _contextChanged(self, flags=0): self._set_stale(self.context_model.is_stale()) if flags & (ContextModel.PACKAGES_PATH_CHANGED | ContextModel.CONTEXT_CHANGED): # update icons new_icons = [] if self.variant.index is not None: package = self.variant.parent if package.num_variants > 1: txt = "1 of %d variants" % package.num_variants new_icons.append(("variant", txt)) if self.variant.is_local: new_icons.append(("local", "package is local")) package_paths = PackageSearchPath(self.context_model.packages_path) package_filter = PackageFilterList.from_pod(self.context_model.package_filter) # TODO: move this all into a thread, it's blocking up the GUI during context load if self.variant in package_paths: # find all >= version packages, so we can determine tick type ge_range = VersionRange.from_version(self.variant.version, ">=") packages = None try: it = package_paths.iter_packages(name=self.variant.name, range_=ge_range) packages = sorted(it, key=lambda x: x.version) except: pass # apply a tick icon if appropriate ticked = False if packages: # test if variant is latest package latest_pkg = packages[-1] if self.variant.version == latest_pkg.version: new_icons.append(("green_tick", "package is latest")) ticked = True range_ = None packages_ = None # test if variant is in request, and is latest possible if not ticked: range_ = None try: request = self.context().requested_packages(True) reqlist = RequirementList(request) if self.variant.name in reqlist.names: variant_ = reqlist.get(self.variant.name) if not variant_.conflict: range_ = variant_.range except: pass if range_ is not None: packages_ = [x for x in packages if x.version in range_] if packages_: latest_pkg = packages_[-1] if self.variant.version == latest_pkg.version: new_icons.append(("yellow_tick", "package is latest possible")) ticked = True packages2 = [x for x in (packages_ or packages) if x.version > self.variant.version] # test if variant is latest within package filter if (not ticked and packages2 and package_filter): if all(package_filter.excludes(x) for x in packages2): new_icons.append(("yellow_tick", "package is latest possible")) ticked = True # test if variant was latest package at time of resolve if not ticked and self.variant.timestamp: untimestamped_packages = [x for x in packages if not x.timestamp] if not untimestamped_packages: resolve_time = self.context().timestamp old_packages = [x for x in packages if x.timestamp <= resolve_time] if old_packages: latest_pkg = old_packages[-1] if self.variant.version == latest_pkg.version: new_icons.append( ("green_white_tick", "package was latest at time of resolve")) ticked = True # test if variant is in request, and was latest possible at # the time of resolve if (not ticked and self.variant.timestamp and range_ is not None and packages_ is not None): untimestamped_packages = any(x for x in packages_ if not x.timestamp) if not untimestamped_packages: resolve_time = self.context().timestamp old_packages = [x for x in packages_ if x.timestamp <= resolve_time] if old_packages: latest_pkg = old_packages[-1] if self.variant.version == latest_pkg.version: new_icons.append( ("yellow_white_tick", "package was latest possible at time of resolve")) ticked = True # test if variant is within package filter, and was latest # possible at the time of resolve if (not ticked and packages2 and package_filter and self.variant.timestamp): untimestamped_packages = any(x for x in (packages_ or packages) if not x.timestamp) if not untimestamped_packages: newer_package = any(x for x in packages2 if not package_filter.excludes(x) and x.timestamp <= resolve_time) if not newer_package: new_icons.append( ("yellow_white_tick", "package was latest possible at time of resolve")) ticked = True # bring in the old man if not ticked: new_icons.append( ("old_man", "newer packages are/were available")) else: new_icons.append(("error", "package is not in the search path")) self._set_icons(new_icons) if (not self.hide_locks) and (flags & (ContextModel.LOCKS_CHANGED | ContextModel.CONTEXT_CHANGED)): # update lock icon lock = self.context_model.get_patch_lock(self.variant.name) if lock is None: lock = self.context_model.default_patch_lock icon_name = "%s_faint" % lock.name else: icon_name = lock.name # update lock tooltip if lock == PatchLock.no_lock: desc = lock.description else: req = self._get_lock_requirement(lock) if req: if lock == PatchLock.lock: desc = "Exact version (%s)" % str(req) else: unit = lock.description.split()[0] desc = ("%s version updates only (%s.*)" % (unit.capitalize(), str(req))) else: desc = lock.description self._set_lock_icon(icon_name, desc.lower())
def __init__(self, context_model, variant_left=None, variant_right=None, parent=None): super(CompareCell, self).__init__(parent) self.context_model = context_model self.left_variant = variant_left self.right_variant = variant_right self.color = None self.side = None self.mode = None self.comparable = False package_paths = self.context_model.packages_path widget = None if self.left_variant and self.right_variant: self.side = "both" equal_versions = ( self.left_variant.version == self.right_variant.version) right_variant_visible = (self.right_variant.wrapped.location in package_paths) self.comparable = right_variant_visible and not equal_versions if self.comparable: # determine how far apart the variant versions are versions = sorted( [self.left_variant.version, self.right_variant.version]) range_ = VersionRange.as_span(*versions) it = iter_packages(name=self.left_variant.name, paths=package_paths, range_=range_) diff_num = sum(1 for x in it) - 1 unit = "version" if diff_num == 1 else "versions" icon_suffixes = {1: "_1", 2: "_2", 3: "_3"} icon_suffix = icon_suffixes.get(diff_num, "") if self.left_variant == self.right_variant: self.mode = "equal_to" self._set_color(0.7, 0.7, 0.7) widget = IconButton("equal_to", "packages are equal") elif self.left_variant.version == self.right_variant.version: # same version, but package is different. This can happen when # a local package is released which then hides a central package # of the same version self.mode = "equalish" self._set_color(1, 1, 0) widget = IconButton( "equalish", "packages versions are equal, but package is different") elif self.left_variant.version > self.right_variant.version: self.mode = "greater_than" self._set_color(0, 1, 0) if self.comparable: desc = "package is %d %s ahead" % (diff_num, unit) widget = IconButton("greater_than" + icon_suffix, desc) else: widget = IconButton("greater_than", "package is newer") else: self.mode = "less_than" self._set_color(1, 0, 0) if self.comparable: desc = "package is %d %s behind" % (diff_num, unit) widget = IconButton("less_than" + icon_suffix, desc) else: widget = IconButton("less_than", "package is older") elif self.right_variant: self.side = "right" self.mode = "missing" self._set_color(1, 0, 0) widget = IconButton("missing", "package is missing") elif self.left_variant: self.side = "left" self.mode = "new" self._set_color(0, 1, 0) widget = IconButton("new", "package is new") if widget: create_pane([None, widget, None], True, compact=True, parent_widget=self) widget.clicked.connect(self._clicked)
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))
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 _set_variant(self, variant, preloaded_packages=None): self.clear() hh = self.horizontalHeader() self.setHorizontalHeaderLabels(["path", "released"]) hh.setResizeMode(0, QtGui.QHeaderView.Interactive) hh.setStretchLastSection(True) hh.setVisible(True) package_paths = self.context_model.packages_path package_filter = PackageFilterList.from_pod( self.context_model.package_filter) if variant and variant.wrapped.location in package_paths: self.version_index = -1 self.reference_version_index = -1 reference_version = None range_ = None if self.reference_variant and self.reference_variant.name == variant.name: reference_version = self.reference_variant.version versions = sorted([reference_version, variant.version]) range_ = VersionRange.as_span(*versions) timestamp = self.context().timestamp if preloaded_packages is not None: if range_ is None: packages = preloaded_packages else: packages = [ x for x in preloaded_packages if x.version in range_ ] else: it = iter_packages(name=variant.name, paths=package_paths, range_=range_) packages = sorted(it, key=lambda x: x.version, reverse=True) self.setRowCount(len(packages)) brush = self.palette().brush(QtGui.QPalette.Active, QtGui.QPalette.Base) for row, package in enumerate(packages): version_str = str(package.version) + ' ' item = QtGui.QTableWidgetItem(version_str) item.setTextAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) self.setVerticalHeaderItem(row, item) if package.version == variant.version: self.version_index = row update_font(item, bold=True) if reference_version is not None \ and package.version == reference_version: self.reference_version_index = row update_font(item, bold=True, italic=True) def _item(): item_ = QtGui.QTableWidgetItem() item_.setBackground( brush) # get rid of mouse-hover coloring return item_ if package.timestamp: release_str = get_timestamp_str(package.timestamp) in_future = (package.timestamp > timestamp) else: release_str = '-' in_future = False item = _item() txt = package.uri + " " icons = [] if in_future: icon = get_icon_widget( "clock_warning", "package did not exist at time of resolve") icons.append(icon) rule = package_filter.excludes(package) if rule: icon = get_icon_widget( "excluded", "package was excluded by rule %s" % str(rule)) icons.append(icon) if icons: label = QtGui.QLabel(txt) pane = create_pane(icons + [label, None], True, compact=True) self.setCellWidget(row, 0, pane) else: item.setText(txt) self.setItem(row, 0, item) item = _item() item.setText(release_str) self.setItem(row, 1, item) vh = self.verticalHeader() vh.setVisible(True) self.resizeColumnsToContents() hh.setStretchLastSection(True) self.update() self.allow_selection = True self.selectRow(self.version_index) self.allow_selection = False self.variant = variant
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 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))
class Requirement(_Common): """Requirement for a versioned object. Examples of valid requirement strings: foo-1.0 [email protected] foo#1.0 foo-1+ foo-1+<4.3 foo<3 foo==1.0.1 Defines a requirement for an object. For example, "foo-5+" means that you require any version of "foo", version 5 or greater. An unversioned requirement can also be used ("foo"), this means you require any version of foo. You can drop the hyphen between object name and version range if the version range starts with a non-alphanumeric character - eg "foo<2". There are two different prefixes that can be applied to a requirement: - "!": The conflict requirement. This means that you require this version range of an object NOT to be present. To conflict with all versions of an object, use "!foo". - "~": This is known as a "weak reference", and means, "I do not require this object, but if present, it must be within this range." It is equivalent to the *conflict of the inverse* of the given version range. There is one subtle case to be aware of. "~foo" is a requirement that has no effect - ie, it means "I do not require foo, but if foo is present, it can be any version." This statement is still valid, but will produce a Requirement object with a None range. """ sep_regex = re.compile(r'[-@#=<>]') def __init__(self, s): 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) 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() @classmethod 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 @property def name(self): """Name of the required object.""" return self.name_ @property def range(self): """VersionRange of the requirement.""" return self.range_ @property def conflict(self): """True if the requirement is a conflict requirement, eg "!foo", "~foo-1". """ return self.conflict_ @property def weak(self): """True if the requirement is weak, eg "~foo". Note that weak requirements are also conflict requirements, but not necessarily the other way around. """ return self.negate_ def safe_str(self): """Return a string representation that is safe for the current filesystem, and guarantees that no two different Requirement objects will encode to the same value.""" return str(self) def conflicts_with(self, other): """Returns True if this requirement conflicts with another `Requirement` or `VersionedObject`.""" if isinstance(other, Requirement): if (self.name_ != other.name_) or (self.range is None) \ or (other.range is None): return False elif self.conflict: return False if other.conflict \ else self.range_.issuperset(other.range_) elif other.conflict: return other.range_.issuperset(self.range_) else: return not self.range_.intersects(other.range_) else: # VersionedObject if (self.name_ != other.name_) or (self.range is None): return False if self.conflict: return (other.version_ in self.range_) else: return (other.version_ not in self.range_) def merged(self, other): """Returns the merged result of two requirements. Two requirements can be in conflict and if so, this function returns None. For example, requests for "foo-4" and "foo-6" are in conflict, since both cannot be satisfied with a single version of foo. Some example successful requirements merges are: - "foo-3+" and "!foo-5+" == "foo-3+<5" - "foo-1" and "foo-1.5" == "foo-1.5" - "!foo-2" and "!foo-5" == "!foo-2|5" """ if self.name_ != other.name_: return None # cannot merge across object names def _r(r_): r = Requirement(None) r.name_ = r_.name_ r.negate_ = r_.negate_ r.conflict_ = r_.conflict_ r.sep_ = r_.sep_ return r if self.range is None: return other elif other.range is None: return self elif self.conflict: if other.conflict: r = _r(self) r.range_ = self.range_ | other.range_ r.negate_ = (self.negate_ and other.negate_ and not r.range_.is_any()) return r else: range_ = other.range - self.range if range_ is None: return None else: r = _r(other) r.range_ = range_ return r elif other.conflict: range_ = self.range_ - other.range_ if range_ is None: return None else: r = _r(self) r.range_ = range_ return r else: range_ = self.range_ & other.range_ if range_ is None: return None else: r = _r(self) r.range_ = range_ return r def __eq__(self, other): return (isinstance(other, Requirement) and (self.name_ == other.name_) and (self.range_ == other.range_) and (self.conflict_ == other.conflict_)) def __hash__(self): return hash(str(self)) 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 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 __init__(self, context_model, variant_left=None, variant_right=None, parent=None): super(CompareCell, self).__init__(parent) self.context_model = context_model self.left_variant = variant_left self.right_variant = variant_right self.color = None self.side = None self.mode = None self.comparable = False package_paths = self.context_model.packages_path widget = None if self.left_variant and self.right_variant: self.side = "both" equal_versions = (self.left_variant.version == self.right_variant.version) right_variant_visible = (self.right_variant.wrapped.location in package_paths) self.comparable = right_variant_visible and not equal_versions if self.comparable: # determine how far apart the variant versions are versions = sorted([self.left_variant.version, self.right_variant.version]) range_ = VersionRange.as_span(*versions) it = iter_packages(name=self.left_variant.name, paths=package_paths, range_=range_) diff_num = sum(1 for x in it) - 1 unit = "version" if diff_num == 1 else "versions" icon_suffixes = {1: "_1", 2: "_2", 3: "_3"} icon_suffix = icon_suffixes.get(diff_num, "") if self.left_variant == self.right_variant: self.mode = "equal_to" self._set_color(0.7, 0.7, 0.7) widget = IconButton("equal_to", "packages are equal") elif self.left_variant.version == self.right_variant.version: # same version, but package is different. This can happen when # a local package is released which then hides a central package # of the same version self.mode = "equalish" self._set_color(1, 1, 0) widget = IconButton( "equalish", "packages versions are equal, but package is different") elif self.left_variant.version > self.right_variant.version: self.mode = "greater_than" self._set_color(0, 1, 0) if self.comparable: desc = "package is %d %s ahead" % (diff_num, unit) widget = IconButton("greater_than" + icon_suffix, desc) else: widget = IconButton("greater_than", "package is newer") else: self.mode = "less_than" self._set_color(1, 0, 0) if self.comparable: desc = "package is %d %s behind" % (diff_num, unit) widget = IconButton("less_than" + icon_suffix, desc) else: widget = IconButton("less_than", "package is older") elif self.right_variant: self.side = "right" self.mode = "missing" self._set_color(1, 0, 0) widget = IconButton("missing", "package is missing") elif self.left_variant: self.side = "left" self.mode = "new" self._set_color(0, 1, 0) widget = IconButton("new", "package is new") if widget: create_pane([None, widget, None], True, compact=True, parent_widget=self) widget.clicked.connect(self._clicked)
class Requirement(_Common): """Requirement for a versioned object. Examples of valid requirement strings: foo-1.0 [email protected] foo#1.0 foo-1+ foo-1+<4.3 foo<3 foo==1.0.1 Defines a requirement for an object. For example, "foo-5+" means that you require any version of "foo", version 5 or greater. An unversioned requirement can also be used ("foo"), this means you require any version of foo. You can drop the hyphen between object name and version range if the version range starts with a non-alphanumeric character - eg "foo<2". There are two different prefixes that can be applied to a requirement: - "!": The conflict requirement. This means that you require this version range of an object NOT to be present. To conflict with all versions of an object, use "!foo". - "~": This is known as a "weak reference", and means, "I do not require this object, but if present, it must be within this range." It is equivalent to the *conflict of the inverse* of the given version range. There is one subtle case to be aware of. "~foo" is a requirement that has no effect - ie, it means "I do not require foo, but if foo is present, it can be any version." This statement is still valid, but will produce a Requirement object with a None range. """ sep_regex = re.compile(r'[-@#=<>]') 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() @classmethod 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 @property def name(self): """Name of the required object.""" return self.name_ @property def range(self): """VersionRange of the requirement.""" return self.range_ @property def conflict(self): """True if the requirement is a conflict requirement, eg "!foo", "~foo-1". """ return self.conflict_ @property def weak(self): """True if the requirement is weak, eg "~foo". Note that weak requirements are also conflict requirements, but not necessarily the other way around. """ return self.negate_ def safe_str(self): """Return a string representation that is safe for the current filesystem, and guarantees that no two different Requirement objects will encode to the same value.""" return str(self) def conflicts_with(self, other): """Returns True if this requirement conflicts with another `Requirement` or `VersionedObject`.""" if isinstance(other, Requirement): if (self.name_ != other.name_) or (self.range is None) \ or (other.range is None): return False elif self.conflict: return False if other.conflict \ else self.range_.issuperset(other.range_) elif other.conflict: return other.range_.issuperset(self.range_) else: return not self.range_.intersects(other.range_) else: # VersionedObject if (self.name_ != other.name_) or (self.range is None): return False if self.conflict: return (other.version_ in self.range_) else: return (other.version_ not in self.range_) def merged(self, other): """Returns the merged result of two requirements. Two requirements can be in conflict and if so, this function returns None. For example, requests for "foo-4" and "foo-6" are in conflict, since both cannot be satisfied with a single version of foo. Some example successful requirements merges are: - "foo-3+" and "!foo-5+" == "foo-3+<5" - "foo-1" and "foo-1.5" == "foo-1.5" - "!foo-2" and "!foo-5" == "!foo-2|5" """ if self.name_ != other.name_: return None # cannot merge across object names def _r(r_): r = Requirement(None) r.name_ = r_.name_ r.negate_ = r_.negate_ r.conflict_ = r_.conflict_ r.sep_ = r_.sep_ return r if self.range is None: return other elif other.range is None: return self elif self.conflict: if other.conflict: r = _r(self) r.range_ = self.range_ | other.range_ r.negate_ = (self.negate_ and other.negate_ and not r.range_.is_any()) return r else: range_ = other.range - self.range if range_ is None: return None else: r = _r(other) r.range_ = range_ return r elif other.conflict: range_ = self.range_ - other.range_ if range_ is None: return None else: r = _r(self) r.range_ = range_ return r else: range_ = self.range_ & other.range_ if range_ is None: return None else: r = _r(self) r.range_ = range_ return r def __eq__(self, other): return (isinstance(other, Requirement) and (self.name_ == other.name_) and (self.range_ == other.range_) and (self.conflict_ == other.conflict_)) def __hash__(self): return hash(str(self)) 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