Ejemplo n.º 1
0
    def test_intersects_request(self):
        """Test intersects with request object"""
        # request.get
        request = RequirementsBinding([Requirement("foo.bar-1")])
        bar_on = intersects(request.get("foo.bar", "0"), "1")
        self.assertTrue(bar_on)

        request = RequirementsBinding([])
        bar_on = intersects(request.get("foo.bar", "0"), "1")
        self.assertTrue(bar_on)  # should be False, but for backward compat

        request = RequirementsBinding([])
        bar_on = intersects(request.get("foo.bar", "foo.bar-0"), "1")
        self.assertFalse(bar_on)  # workaround, see PR nerdvegas/rez#1030

        # request.get_range
        request = RequirementsBinding([Requirement("foo.bar-1")])
        bar_on = intersects(request.get_range("foo.bar", "0"), "1")
        self.assertTrue(bar_on)

        request = RequirementsBinding([])
        bar_on = intersects(request.get_range("foo.bar", "0"), "1")
        self.assertFalse(bar_on)

        request = RequirementsBinding([])
        foo = intersects(request.get_range("foo", "==1.2.3"), "1.2")
        self.assertTrue(foo)

        request = RequirementsBinding([])
        foo = intersects(request.get_range("foo", "==1.2.3"), "1.4")
        self.assertFalse(foo)

        request = RequirementsBinding([Requirement("foo-1.4.5")])
        foo = intersects(request.get_range("foo", "==1.2.3"), "1.4")
        self.assertTrue(foo)
Ejemplo n.º 2
0
Archivo: test.py Proyecto: gst/rez
        def _confl(reqs, a, b):
            _print("requirements(%s) == %s <--!--> %s" % (' '.join(reqs), a, b))
            reqs_ = [Requirement(x) for x in reqs]
            reqlist = RequirementList(reqs_)
            _print("result: %s" % str(reqlist))

            a_req = Requirement(a)
            b_req = Requirement(b)
            self.assertTrue(reqlist.conflict == (a_req, b_req))
Ejemplo n.º 3
0
        def _eq(reqs, expected_reqs):
            _print("requirements(%s) == requirements(%s)" %
                   (' '.join(reqs), ' '.join(expected_reqs)))
            reqs_ = [Requirement(x) for x in reqs]
            reqlist = RequirementList(reqs_)
            _print("result: %s" % str(reqlist))

            exp_reqs_ = [Requirement(x) for x in expected_reqs]
            self.assertTrue(reqlist.requirements == exp_reqs_)
Ejemplo n.º 4
0
    def test_intersects_ephemerals(self):
        """Test intersects with ephemerals object"""
        # ephemerals.get
        ephemerals = EphemeralsBinding([Requirement(".foo.bar-1")])
        bar_on = intersects(ephemerals.get("foo.bar", "0"), "1")
        self.assertTrue(bar_on)

        ephemerals = EphemeralsBinding([])
        bar_on = intersects(ephemerals.get("foo.bar", "0"), "1")
        self.assertTrue(bar_on)  # should be False, but for backward compat

        ephemerals = EphemeralsBinding([])
        bar_on = intersects(ephemerals.get("foo.bar", "foo.bar-0"), "1")
        self.assertFalse(
            bar_on
        )  # workaround, see https://github.com/nerdvegas/rez/pull/1030

        ephemerals = EphemeralsBinding([])
        self.assertRaises(
            RuntimeError,  # no default
            intersects,
            ephemerals.get("foo.bar"),
            "0")

        # ephemerals.get_range
        ephemerals = EphemeralsBinding([Requirement(".foo.bar-1")])
        bar_on = intersects(ephemerals.get_range("foo.bar", "0"), "1")
        self.assertTrue(bar_on)

        ephemerals = EphemeralsBinding([])
        bar_on = intersects(ephemerals.get_range("foo.bar", "0"), "1")
        self.assertFalse(bar_on)

        ephemerals = EphemeralsBinding([])
        foo = intersects(ephemerals.get_range("foo", "==1.2.3"), "1.2")
        self.assertTrue(foo)

        ephemerals = EphemeralsBinding([])
        foo = intersects(ephemerals.get_range("foo", "==1.2.3"), "1.4")
        self.assertFalse(foo)

        ephemerals = EphemeralsBinding([Requirement(".foo-1.4.5")])
        foo = intersects(ephemerals.get_range("foo", "==1.2.3"), "1.4")
        self.assertTrue(foo)

        ephemerals = EphemeralsBinding([])
        self.assertRaises(
            RuntimeError,  # no default
            intersects,
            ephemerals.get_range("foo.bar"),
            "0")
Ejemplo n.º 5
0
 def test_packaging_req_to_rez_req(self):
     """
     """
     self.assertEqual(
         rez.utils.pip.packaging_req_to_rez_req(
             packaging_Requirement("package>1")),
         Requirement("package-1.1+"))
     self.assertEqual(
         rez.utils.pip.packaging_req_to_rez_req(
             packaging_Requirement("package")), Requirement("package"))
     self.assertEqual(
         rez.utils.pip.packaging_req_to_rez_req(
             packaging_Requirement("package[extra]")),
         Requirement("package"))
Ejemplo n.º 6
0
        def _eq(reqs, expected_reqs):
            _print("requirements(%s) == requirements(%s)" %
                   (' '.join(reqs), ' '.join(expected_reqs)))
            reqs_ = [Requirement(x) for x in reqs]
            reqlist = RequirementList(reqs_)
            _print("result: %s" % str(reqlist))

            exp_reqs_ = [Requirement(x) for x in expected_reqs]
            self.assertTrue(reqlist.requirements == exp_reqs_)

            exp_names = set(x.name for x in exp_reqs_ if not x.conflict)
            self.assertTrue(reqlist.names == exp_names)

            exp_confl_names = set(x.name for x in exp_reqs_ if x.conflict)
            self.assertTrue(reqlist.conflict_names == exp_confl_names)
Ejemplo n.º 7
0
    def _update_status(self):
        def _ok():
            self._set_style()
            self.setToolTip("")

        def _err(msg, color="red"):
            self._set_style("QLineEdit { border : 2px solid %s;}" % color)
            self.setToolTip(msg)

        txt = str(self.text())
        if not txt:
            _ok()
            return

        try:
            req = Requirement(str(txt))
        except Exception as e:
            _err(str(e))
            return

        _ok()
        if not req.conflict:
            try:
                it = iter_packages(name=req.name,
                                   range_=req.range,
                                   paths=self._paths)
                pkg = sorted(it, key=lambda x: x.version)[-1]
            except Exception:
                _err("cannot find package: %r" % txt, "orange")
                return

            if pkg.description:
                self.setToolTip(pkg.description)
Ejemplo n.º 8
0
    def _solve(self, packages, expected_resolve):
        print()
        reqs = [Requirement(x) for x in packages]
        s1, s2, s_perms = self._create_solvers(reqs)

        s1.solve()
        self.assertEqual(s1.status, SolverStatus.solved)

        # ephemeral order doesn't matter, hence the sort
        resolve = ([str(x) for x in s1.resolved_packages] +
                   sorted(str(x) for x in s1.resolved_ephemerals))

        print()
        print("request: %s" % ' '.join(packages))
        print("expecting: %s" % ' '.join(expected_resolve))
        print("result: %s" % ' '.join(str(x) for x in resolve))
        self.assertEqual(resolve, expected_resolve)

        print("checking that unoptimised solve matches optimised...")
        s2.solve()
        self.assertEqual(s2.status, SolverStatus.solved)
        resolve2 = ([str(x) for x in s2.resolved_packages] +
                    sorted(str(x) for x in s2.resolved_ephemerals))
        self.assertEqual(resolve2, resolve)

        print("checking that permutations also succeed...")
        for s in s_perms:
            s.solve()
            self.assertEqual(s.status, SolverStatus.solved)

        return s1
Ejemplo n.º 9
0
    def get_package(self):
        """Get the target package.

        Returns:
            `Package`: Package to run tests on.
        """
        if self.package is not None:
            return self.package

        if self.use_current_env:
            # get package from current context, or return None
            current_context = ResolvedContext.get_current()
            if current_context is None:
                return None

            req = Requirement(self.package_request)
            variant = current_context.get_resolved_package(req.name)
            if variant is None:
                return None

            package = variant.parent

            if not req.range.contains_version(package.version):
                return None

        else:
            # find latest package within request
            package = get_latest_package_from_string(str(self.package_request),
                                                     self.package_paths)
            if package is None:
                raise PackageNotFoundError("Could not find package to test: %s"
                                           % str(self.package_request))

        self.package = package
        return self.package
Ejemplo n.º 10
0
    def _solve(self, packages, expected_resolve):
        print
        reqs = [Requirement(x) for x in packages]
        s1, s2, s_perms = self._create_solvers(reqs)

        s1.solve()
        self.assertEqual(s1.status, SolverStatus.solved)
        resolve = [str(x) for x in s1.resolved_packages]

        print
        print "request: %s" % ' '.join(packages)
        print "expecting: %s" % ' '.join(expected_resolve)
        print "result: %s" % ' '.join(str(x) for x in resolve)
        self.assertEqual(resolve, expected_resolve)

        print "checking that unoptimised solve matches optimised..."
        s2.solve()
        self.assertEqual(s2.status, SolverStatus.solved)
        resolve2 = [str(x) for x in s2.resolved_packages]
        self.assertEqual(resolve2, resolve)

        print "checking that permutations also succeed..."
        for s in s_perms:
            s.solve()
            self.assertEqual(s.status, SolverStatus.solved)

        return s1
Ejemplo n.º 11
0
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)
Ejemplo n.º 12
0
 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
Ejemplo n.º 13
0
    def _apply_resolve(self,
                       context_model,
                       column,
                       reference_column,
                       hide_locks=False,
                       read_only=False,
                       reference_column_is_variants=False):
        context = context_model.context()
        resolved = context.resolved_packages[:]
        consumed_rows = set()

        # match variants up with matching request/variant in source column
        for row, widget in self._iter_column_widgets(
                reference_column, (PackageSelectWidget, VariantCellWidget)):
            request_str = str(widget.text())
            if not request_str:
                continue

            package_name = Requirement(request_str).name
            matches = [x for x in resolved if x.name == package_name]
            if matches:
                variant = matches[0]
                resolved = [x for x in resolved if x.name != package_name]
                reference_variant = None
                if reference_column_is_variants and isinstance(
                        widget, VariantCellWidget):
                    reference_variant = widget.variant
                self._set_variant_cell(row,
                                       column,
                                       context_model,
                                       variant,
                                       reference_variant=reference_variant,
                                       hide_locks=hide_locks,
                                       read_only=read_only)
            consumed_rows.add(row)

        # append variants that don't match reference requests/variants
        if reference_column_is_variants:
            hide_locks = True
        row = 0

        while resolved:
            variant = resolved[0]
            resolved = resolved[1:]
            while row in consumed_rows:
                row += 1
            self._set_variant_cell(row,
                                   column,
                                   context_model,
                                   variant,
                                   hide_locks=hide_locks,
                                   read_only=read_only)
            row += 1
Ejemplo n.º 14
0
    def _parse_request(cls, resources_request):
        name_pattern = resources_request or '*'
        version_range = None

        try:
            req = Requirement(name_pattern)
            name_pattern = req.name
            if not req.range.is_any():
                version_range = req.range
        except:
            pass

        return name_pattern, version_range
Ejemplo n.º 15
0
    def test_range_filter(self):
        """Test the range filter.
        """
        fltr = PackageFilter()
        fltr.add_exclusion(RangeRule(Requirement("timestamped-1.1+")))

        self._test(
            fltr,
            "timestamped",
            [
                "1.0.5",
                "1.0.6"
            ]
        )
Ejemplo n.º 16
0
    def set_package_text(self, txt):
        try:
            req = Requirement(str(txt))
            package_name = req.name
            version_range = req.range
        except:
            package_name = str(txt)
            version_range = None

        self.edit.setText(package_name)
        self._set_package_name(package_name)

        if version_range is not None:
            self.versions_table.select_version(version_range)
Ejemplo n.º 17
0
    def _fail(self, *packages):
        reqs = [Requirement(x) for x in packages]
        s1, s2, s_perms = self._create_solvers(reqs)

        s1.solve()
        self.assertEqual(s1.status, SolverStatus.failed)

        s2.solve()
        self.assertEqual(s2.status, SolverStatus.failed)
        self.assertEqual(s1.failure_reason(), s2.failure_reason())

        for s in s_perms:
            s.solve()
            self.assertEqual(s.status, SolverStatus.failed)

        return s1
Ejemplo n.º 18
0
    def _solve(self, packages, expected_resolve):
        reqs = [Requirement(x) for x in packages]
        s1, s2, s_perms = self._create_solvers(reqs)

        s1.solve()
        self.assertEqual(s1.status, SolverStatus.solved)
        resolve = [str(x) for x in s1.resolved_packages]
        self.assertEqual(resolve, expected_resolve)

        s2.solve()
        self.assertEqual(s2.status, SolverStatus.solved)
        resolve2 = [str(x) for x in s2.resolved_packages]
        self.assertEqual(resolve2, resolve)

        for s in s_perms:
            s.solve()
            self.assertEqual(s.status, SolverStatus.solved)

        return s1
Ejemplo n.º 19
0
    def _set_result(self, solver_dict):
        self.status_ = solver_dict.get("status")
        self.graph_ = solver_dict.get("graph")
        self.solve_time = solver_dict.get("solve_time")
        self.load_time = solver_dict.get("load_time")
        self.failure_description = solver_dict.get("failure_description")

        self.resolved_packages_ = None
        self.resolved_ephemerals_ = None

        if self.status_ == ResolverStatus.solved:
            # convert solver.Variants to packages.Variants
            self.resolved_packages_ = []
            for variant_handle in solver_dict.get("variant_handles", []):
                variant = self._get_variant(variant_handle)
                self.resolved_packages_.append(variant)

            self.resolved_ephemerals_ = []
            for req_str in solver_dict.get("ephemerals", []):
                req = Requirement(req_str)
                self.resolved_ephemerals_.append(req)
Ejemplo n.º 20
0
    def resolve(self):
        # validate the request before opening dialog
        for req_str in self.context_model.request:
            try:
                Requirement(req_str)
            except Exception as e:
                title = "Invalid package request - %r" % req_str
                QtGui.QMessageBox.critical(self, title, str(e))
                return

        self._reset()
        if not self.advanced:
            self._start_resolve()

        self.exec_()
        if self.started:
            self.resolver.stop()
            self.thread.quit()
            self.thread.wait()
            return self.resolver.success()
        return False
Ejemplo n.º 21
0
def expand_requirement(request):
    """Expands a requirement string like 'python-2.*'

    Only trailing wildcards are supported; they will be replaced with the
    latest package version found within the range. If none are found, the
    wildcards will just be stripped.

    Example:

        >>> print expand_requirement('python-2.*')
        python-2.7

    Args:
        request (str): Request to expand, eg 'python-2.*'

    Returns:
        str: Expanded request string.
    """
    if '*' not in request:
        return request

    from rez.vendor.version.requirement import VersionedObject, Requirement
    from rez.packages_ import get_latest_package

    txt = request.replace('*', '_')
    obj = VersionedObject(txt)
    rank = len(obj.version)

    request_ = request
    while request_.endswith('*'):
        request_ = request_[:-2]  # strip sep + *

    req = Requirement(request_)
    package = get_latest_package(name=req.name, range_=req.range_)

    if package is None:
        return request_

    obj.version_ = package.version.trim(rank)
    return str(obj)
Ejemplo n.º 22
0
def packaging_req_to_rez_req(packaging_req):
    """Convert packaging requirement object to equivalent rez requirement.

    Note that environment markers are ignored.

    Args:
        packaging_req (`packaging.requirements.Requirement`): Packaging requirement.

    Returns:
        `Requirement`: Equivalent rez requirement object.
    """
    if packaging_req.extras:
        print_warning("Ignoring extras requested on %r - "
                      "this is not yet supported" % str(packaging_req))

    rez_req_str = pip_to_rez_package_name(packaging_req.name)

    if packaging_req.specifier:
        range_ = pip_specifier_to_rez_requirement(packaging_req.specifier)
        rez_req_str += '-' + str(range_)

    return Requirement(rez_req_str)
Ejemplo n.º 23
0
    def _fail(self, *packages):
        print
        reqs = [Requirement(x) for x in packages]
        s1, s2, s_perms = self._create_solvers(reqs)

        s1.solve()
        print
        print "request: %s" % ' '.join(packages)
        print "expecting failure"
        self.assertEqual(s1.status, SolverStatus.failed)
        print "result: %s" % str(s1.failure_reason())

        print "checking that unoptimised solve fail matches optimised..."
        s2.solve()
        self.assertEqual(s2.status, SolverStatus.failed)
        self.assertEqual(s1.failure_reason(), s2.failure_reason())

        print "checking that permutations also fail..."
        for s in s_perms:
            s.solve()
            self.assertEqual(s.status, SolverStatus.failed)

        return s1
Ejemplo n.º 24
0
 def _parse(cls, txt):
     _, txt = Rule._parse_label(txt)
     return cls(Requirement(txt))
Ejemplo n.º 25
0
def command(opts, parser, extra_arg_groups=None):
    from rez.config import config
    from rez.exceptions import RezError
    from rez.utils.formatting import get_epoch_time_from_str, expand_abbreviations
    from rez.utils.logging_ import print_error
    from rez.packages_ import iter_package_families, iter_packages
    from rez.vendor.version.requirement import Requirement
    import os.path
    import fnmatch
    import sys

    error_class = None if opts.debug else RezError

    before_time = 0
    after_time = 0
    if opts.before:
        before_time = get_epoch_time_from_str(opts.before)
    if opts.after:
        after_time = get_epoch_time_from_str(opts.after)
    if after_time and before_time and (after_time >= before_time):
        parser.error("non-overlapping --before and --after")

    if opts.paths is None:
        pkg_paths = config.nonlocal_packages_path if opts.no_local else None
    else:
        pkg_paths = (opts.paths or "").split(os.pathsep)
        pkg_paths = [os.path.expanduser(x) for x in pkg_paths if x]

    name_pattern = opts.PKG or '*'
    version_range = None
    if opts.PKG:
        try:
            req = Requirement(opts.PKG)
            name_pattern = req.name
            if not req.range.is_any():
                version_range = req.range
        except:
            pass

    type_ = opts.type
    if opts.errors or (type_ == "auto" and version_range):
        type_ = "package"
        # turn some of the nastier rez-1 warnings into errors
        config.override("error_package_name_mismatch", True)
        config.override("error_version_mismatch", True)
        config.override("error_nonstring_version", True)

    if opts.no_warnings:
        config.override("warn_none", True)

    # families
    found = False
    family_names = []
    families = iter_package_families(paths=pkg_paths)
    if opts.sort:
        families = sorted(families, key=lambda x: x.name)
    for family in families:
        if family.name not in family_names and \
                fnmatch.fnmatch(family.name, name_pattern):
            family_names.append(family.name)
            if type_ == "auto":
                type_ = "package" if family.name == name_pattern else "family"
            if type_ == "family":
                print family.name
                found = True

    def _handle(e):
        print_error(str(e))

    def _print_resource(r):
        if opts.validate:
            try:
                r.validate_data()
            except error_class as e:
                _handle(e)
                return

        if opts.format:
            txt = expand_abbreviations(opts.format, fields)
            lines = txt.split("\\n")
            for line in lines:
                try:
                    line_ = r.format(line)
                except error_class as e:
                    _handle(e)
                    break
                if opts.no_newlines:
                    line_ = line_.replace('\n', "\\n")

                print line_
        else:
            print r.qualified_name

    # packages/variants
    if type_ in ("package", "variant"):
        for name in family_names:
            packages = iter_packages(name, version_range, paths=pkg_paths)
            if opts.sort or opts.latest:
                packages = sorted(packages, key=lambda x: x.version)
                if opts.latest and packages:
                    packages = [packages[-1]]

            for package in packages:
                if ((before_time or after_time) and package.timestamp and
                    (before_time and package.timestamp >= before_time
                     or after_time and package.timestamp <= after_time)):
                    continue

                if opts.errors:
                    try:
                        package.validate_data()
                    except error_class as e:
                        _handle(e)
                        found = True
                elif type_ == "package":
                    _print_resource(package)
                    found = True
                elif type_ == "variant":
                    try:
                        package.validate_data()
                    except error_class as e:
                        _handle(e)
                        continue

                    try:
                        for variant in package.iter_variants():
                            _print_resource(variant)
                            found = True
                    except error_class as e:
                        _handle(e)
                        continue

    if not found:
        if opts.errors:
            print "no erroneous packages found"
        else:
            print "no matches found"
            sys.exit(1)
Ejemplo n.º 26
0
def get_patched_request(requires, patchlist):
    """Apply patch args to a request.

    For example, consider:

        >>> print get_patched_request(["foo-5", "bah-8.1"], ["foo-6"])
        ["foo-6", "bah-8.1"]
        >>> print get_patched_request(["foo-5", "bah-8.1"], ["^bah"])
        ["foo-5"]

    The following rules apply wrt how normal/conflict/weak patches override
    (note though that the new request is always added, even if it doesn't
    override an existing request):

    PATCH  OVERRIDES: foo  !foo  ~foo
    -----  ---------- ---  ----  -----
    foo               Y    Y     Y
    !foo              N    N     N
    ~foo              N    N     Y
    ^foo              Y    Y     Y

    Args:
        requires (list of str or `version.Requirement`): Request.
        patchlist (list of str): List of patch requests.

    Returns:
        List of `version.Requirement`: Patched request.
    """

    # rules from table in docstring above
    rules = {
        '': (True, True, True),
        '!': (False, False, False),
        '~': (False, False, True),
        '^': (True, True, True)
    }

    requires = [
        Requirement(x) if not isinstance(x, Requirement) else x
        for x in requires
    ]
    appended = []

    for patch in patchlist:
        if patch and patch[0] in ('!', '~', '^'):
            ch = patch[0]
            name = Requirement(patch[1:]).name
        else:
            ch = ''
            name = Requirement(patch).name

        rule = rules[ch]
        replaced = (ch == '^')

        for i, req in enumerate(requires):
            if req is None or req.name != name:
                continue

            if not req.conflict:
                replace = rule[0]  # foo
            elif not req.weak:
                replace = rule[1]  # !foo
            else:
                replace = rule[2]  # ~foo

            if replace:
                if replaced:
                    requires[i] = None
                else:
                    requires[i] = Requirement(patch)
                    replaced = True

        if not replaced:
            appended.append(Requirement(patch))

    result = [x for x in requires if x is not None] + appended
    return result
Ejemplo n.º 27
0
def expand_requirement(request, paths=None):
    """Expands a requirement string like 'python-2.*', 'foo-2.*+<*', etc.

    Wildcards are expanded to the latest version that matches. There is also a
    special wildcard '**' that will expand to the full version, but it cannot
    be used in combination with '*'.

    Wildcards MUST placehold a whole version token, not partial - while 'foo-2.*'
    is valid, 'foo-2.v*' is not.

    Wildcards MUST appear at the end of version numbers - while 'foo-1.*.*' is
    valid, 'foo-1.*.0' is not.

    It is possible that an expansion will result in an invalid request string
    (such as 'foo-2+<2'). The appropriate exception will be raised if this
    happens.

    Examples:

        >>> print(expand_requirement('python-2.*'))
        python-2.7
        >>> print(expand_requirement('python==2.**'))
        python==2.7.12
        >>> print(expand_requirement('python<**'))
        python<3.0.5

    Args:
        request (str): Request to expand, eg 'python-2.*'
        paths (list of str, optional): paths to search for package families,
            defaults to `config.packages_path`.

    Returns:
        str: Expanded request string.
    """
    if '*' not in request:
        return request

    from rez.vendor.version.version import VersionRange
    from rez.vendor.version.requirement import Requirement
    from rez.packages_ import get_latest_package
    from uuid import uuid4

    wildcard_map = {}
    expanded_versions = {}
    request_ = request

    # replace wildcards with valid version tokens that can be replaced again
    # afterwards. This produces a horrendous, but both valid and temporary,
    # version string.
    #
    while "**" in request_:
        uid = "_%s_" % uuid4().hex
        request_ = request_.replace("**", uid, 1)
        wildcard_map[uid] = "**"

    while '*' in request_:
        uid = "_%s_" % uuid4().hex
        request_ = request_.replace('*', uid, 1)
        wildcard_map[uid] = '*'

    # create the requirement, then expand wildcards
    #
    req = Requirement(request_, invalid_bound_error=False)

    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 visit_version(version):
        # requirements like 'foo-1' are actually represented internally as
        # 'foo-1+<1_' - '1_' is the next possible version after '1'. So we have
        # to detect this case and remap the uid-ified wildcard back here too.
        #
        for v, expanded_v in expanded_versions.items():
            if version == next(v):
                return next(expanded_v)

        version_ = expand_version(version)
        if version_ is None:
            return None

        expanded_versions[version] = version_
        return version_

    if req.range_ is not None:
        req.range_.visit_versions(visit_version)

    result = str(req)

    # do some cleanup so that long uids aren't left in invalid wildcarded strings
    for uid, token in wildcard_map.items():
        result = result.replace(uid, token)

    # cast back to a Requirement again, then back to a string. This will catch
    # bad verison ranges, but will also put OR'd version ranges into the correct
    # order
    expanded_req = Requirement(result)

    return str(expanded_req)