Repository: nerdvegas/rez
Pull request #887 (2020-05-09 05:28:23)

Cli variant uri
The rez-cp tool now has a --variant-uri option, so you can do things like this:

```
> ]$ rez-context --resolve --su | xargs -i rez-cp --variant-uri {} --dest-path ...
```

Ie, copy all the variants in a context, into their own package repo.

PR to merge first: https://github.com/nerdvegas/rez/pull/886



Patch:
diff --git a/src/rez/cli/_util.py b/src/rez/cli/_util.py
index 5a68d0ff..8a43a2ea 100644
--- a/src/rez/cli/_util.py
+++ b/src/rez/cli/_util.py
@@ -127,6 +127,19 @@ def _env_var_true(name):
     return (os.getenv(name, "").lower() in ("1", "true", "on", "yes"))
 
 
+def print_items(items, stream=sys.stdout):
+    try:
+        item_per_line = (not stream.isatty())
+    except:
+        item_per_line = True
+
+    if item_per_line:
+        for item in items:
+            print(item)
+    else:
+        print(' '.join(map(str, items)))
+
+
 def sigbase_handler(signum, frame):
     # show cursor - progress lib may have hidden it
     SHOW_CURSOR = '\x1b[?25h'
diff --git a/src/rez/cli/context.py b/src/rez/cli/context.py
index d2de50c3..f7857569 100644
--- a/src/rez/cli/context.py
+++ b/src/rez/cli/context.py
@@ -34,7 +34,7 @@ def setup_parser(parser, completions=False):
     parser.add_argument(
         "--res", "--print-resolve", dest="print_resolve",
         action="store_true",
-        help="print only the resolve list")
+        help="print only the resolve list. Use with --su to print package URIs")
     parser.add_argument(
         "--so", "--source-order", dest="source_order", action="store_true",
         help="print resolved packages in order they are sorted, rather than "
@@ -96,6 +96,7 @@ def setup_parser(parser, completions=False):
 
 
 def command(opts, parser, extra_arg_groups=None):
+    from rez.cli._util import print_items
     from rez.status import status
     from rez.utils.formatting import columnise, PackageRequest
     from rez.resolved_context import ResolvedContext
@@ -123,9 +124,12 @@ def command(opts, parser, extra_arg_groups=None):
 
     if not opts.interpret:
         if opts.print_request:
-            print(" ".join(str(x) for x in rc.requested_packages(False)))
+            print_items(rc.requested_packages(False))
         elif opts.print_resolve:
-            print(' '.join(x.qualified_package_name for x in rc.resolved_packages))
+            if opts.show_uris:
+                print_items(x.uri for x in rc.resolved_packages)
+            else:
+                print_items(x.qualified_package_name for x in rc.resolved_packages)
         elif opts.tools:
             rc.print_tools()
         elif opts.diff:
diff --git a/src/rez/cli/cp.py b/src/rez/cli/cp.py
index f0d82b82..7eaaeec9 100644
--- a/src/rez/cli/cp.py
+++ b/src/rez/cli/cp.py
@@ -49,8 +49,12 @@ def setup_parser(parser, completions=False):
     parser.add_argument(
         "--variants", nargs='+', type=int, metavar="INDEX",
         help="select variants to copy (zero-indexed).")
+    parser.add_argument(
+        "--variant-uri", metavar="URI",
+        help="copy variant with the given URI. Ignores --variants."
+        )
     pkg_action = parser.add_argument(
-        "PKG",
+        "PKG", nargs='?',
         help="package to copy")
 
     if completions:
@@ -66,7 +70,13 @@ def command(opts, parser, extra_arg_groups=None):
     from rez.package_repository import package_repository_manager
     from rez.package_copy import copy_package
     from rez.utils.formatting import PackageRequest
-    from rez.packages import iter_packages
+    from rez.packages import iter_packages, get_variant_from_uri
+
+    if opts.variant_uri:
+        if opts.PKG:
+            parser.error("Supply PKG or --variant-uri, not both.")
+    elif not opts.PKG:
+        parser.error("Expected PKG.")
 
     if (not opts.dest_path) and not (opts.rename or opts.reversion):
         parser.error("--dest-path must be specified unless --rename or "
@@ -75,34 +85,45 @@ def command(opts, parser, extra_arg_groups=None):
     # Load the source package.
     #
 
-    if opts.paths:
-        paths = opts.paths.split(os.pathsep)
-        paths = [x for x in paths if x]
-    elif opts.no_local:
-        paths = config.nonlocal_packages_path
+    if opts.variant_uri:
+        variant = get_variant_from_uri(opts.variant_uri)
+        if variant is None:
+            print("Unknown variant: %s" % opts.variant_uri, file=sys.stderr)
+            sys.exit(1)
+
+        src_pkg = variant.parent
+        variant_indexes = [variant.index]
+
     else:
-        paths = None
+        if opts.paths:
+            paths = opts.paths.split(os.pathsep)
+            paths = [x for x in paths if x]
+        elif opts.no_local:
+            paths = config.nonlocal_packages_path
+        else:
+            paths = None
 
-    req = PackageRequest(opts.PKG)
+        req = PackageRequest(opts.PKG)
 
-    it = iter_packages(
-        name=req.name,
-        range_=req.range_,
-        paths=paths
-    )
+        it = iter_packages(
+            name=req.name,
+            range_=req.range_,
+            paths=paths
+        )
 
-    src_pkgs = list(it)
-    if not src_pkgs:
-        print("No matching packages found.", file=sys.stderr)
-        sys.exit(1)
+        src_pkgs = list(it)
+        if not src_pkgs:
+            print("No matching packages found.", file=sys.stderr)
+            sys.exit(1)
 
-    if len(src_pkgs) > 1:
-        print("More than one package matches, please choose:", file=sys.stderr)
-        for pkg in sorted(src_pkgs, key=lambda x: x.version):
-            print(pkg.qualified_name, file=sys.stderr)
-        sys.exit(1)
+        if len(src_pkgs) > 1:
+            print("More than one package matches, please choose:", file=sys.stderr)
+            for pkg in sorted(src_pkgs, key=lambda x: x.version):
+                print(pkg.qualified_name, file=sys.stderr)
+            sys.exit(1)
 
-    src_pkg = src_pkgs[0]
+        src_pkg = src_pkgs[0]
+        variant_indexes = opts.variants or None
 
     # Determine repo and perform checks.
     #
@@ -129,14 +150,12 @@ def command(opts, parser, extra_arg_groups=None):
     # Perform the copy.
     #
 
-    variants = opts.variants or None
-
     result = copy_package(
         package=src_pkg,
         dest_repository=dest_pkg_repo,
         dest_name=opts.rename,
         dest_version=opts.reversion,
-        variants=variants,
+        variants=variant_indexes,
         overwrite=opts.overwrite,
         shallow=opts.shallow,
         follow_symlinks=opts.follow_symlinks,
diff --git a/src/rez/package_repository.py b/src/rez/package_repository.py
index b4a59526..19b0cee8 100644
--- a/src/rez/package_repository.py
+++ b/src/rez/package_repository.py
@@ -170,6 +170,18 @@ class PackageRepository(object):
         """
         raise NotImplementedError
 
+    def get_variant_from_uri(self, uri):
+        """Get a variant given its URI.
+
+        Args:
+            uri (str): Variant URI
+
+        Returns:
+            `VariantResource`, or None if the variant is not present in this
+            package repository.
+        """
+        return None
+
     def pre_variant_install(self, variant_resource):
         """Called before a variant is installed.
 
diff --git a/src/rez/packages.py b/src/rez/packages.py
index 8a34ac42..1ff1b9ab 100644
--- a/src/rez/packages.py
+++ b/src/rez/packages.py
@@ -637,12 +637,38 @@ def get_variant(variant_handle, context=None):
     return variant
 
 
+def get_variant_from_uri(uri, paths=None):
+    """Get a variant given its URI.
+
+    Args:
+        uri (str): Variant URI
+        paths (list of str): paths to search for variants, defaults to
+            `config.packages_path`.
+
+    Returns:
+        `VariantResource`, or None if the variant is not present in this
+        package repository.
+    """
+    for path in (paths or config.packages_path):
+        repo = package_repository_manager.get_repository(path)
+        variant_resource = repo.get_variant_from_uri(uri)
+        if variant_resource is not None:
+            return Variant(variant_resource)
+
+    return None
+
+
 def get_last_release_time(name, paths=None):
     """Returns the most recent time this package was released.
 
     Note that releasing a variant into an already-released package is also
     considered a package release.
 
+    Args:
+        name (str): Package family name.
+        paths (list of str): paths to search for packages, defaults to
+            `config.packages_path`.
+
     Returns:
         int: Epoch time of last package release, or zero if this cannot be
         determined.
diff --git a/src/rez/utils/_version.py b/src/rez/utils/_version.py
index 6525d50d..96115da4 100644
--- a/src/rez/utils/_version.py
+++ b/src/rez/utils/_version.py
@@ -1,7 +1,7 @@
 
 
 # Update this value to version up Rez. Do not place anything else in this file.
-_rez_version = "2.59.0"
+_rez_version = "2.60.0"
 
 
 # Copyright 2013-2016 Allan Johns.
diff --git a/src/rezplugins/package_repository/filesystem.py b/src/rezplugins/package_repository/filesystem.py
index 8edc4906..9f2d9e1f 100644
--- a/src/rezplugins/package_repository/filesystem.py
+++ b/src/rezplugins/package_repository/filesystem.py
@@ -534,6 +534,64 @@ class FileSystemPackageRepository(PackageRepository):
     def get_last_release_time(self, package_family_resource):
         return package_family_resource.get_last_release_time()
 
+    def get_variant_from_uri(self, uri):
+        """
+        Example URIs:
+        - /svr/packages/mypkg/1.0.0/package.py[1]
+        - /svr/packages/mypkg/1.0.0/package.py[]  ("null" variant)
+        - /svr/packages/mypkg/package.py[1]  (unversioned package - rare)
+        """
+        i = uri.rfind('[')
+        if i == -1:
+            return None
+
+        prefix = self.location + os.path.sep
+        if not uri.startswith(prefix):
+            return None
+
+        part1 = uri[len(prefix):i]  # 'mypkg/1.0.0/package.py'
+        part2 = uri[i:][1:-1]  # the '1' in '[1]'
+
+        # find package
+        parts = part1.split(os.path.sep)
+        if len(parts) == 3:  # versioned package
+            pkg_name, pkg_ver_str = parts[0], parts[1]
+        elif len(parts) == 2:  # unversioned package
+            pkg_name, pkg_ver_str = parts[0], ''
+        else:
+            return None
+
+        fam = self.get_package_family(pkg_name)
+        if fam is None:
+            return None
+
+        ver = Version(pkg_ver_str)
+        pkg = None
+
+        for package in fam.iter_packages():
+            if package.version == ver:
+                pkg = package
+                break
+
+        if pkg is None:
+            return None
+
+        # find variant in package
+        if part2 == '':
+            variant_index = None
+        else:
+            try:
+                variant_index = int(part2)
+            except:
+                # future proof - we may move to hash-based indices for hashed variants
+                variant_index = part2
+
+        for variant in pkg.iter_variants():
+            if variant.index == variant_index:
+                return variant
+
+        return None
+
     @cached_property
     def file_lock_dir(self):
         dirname = _settings.file_lock_dir


Test patch:
diff --git a/src/rez/tests/test_packages.py b/src/rez/tests/test_packages.py
index 3405fdec..576572d8 100644
--- a/src/rez/tests/test_packages.py
+++ b/src/rez/tests/test_packages.py
@@ -2,7 +2,7 @@
 test package iteration, serialization etc
 """
 from rez.packages import iter_package_families, iter_packages, get_package, \
-    create_package, get_developer_package
+    create_package, get_developer_package, get_variant_from_uri
 from rez.package_py_utils import expand_requirement
 from rez.package_resources import package_release_keys
 from rez.tests.util import TestBase, TempdirMixin
@@ -422,6 +422,13 @@ class TestPackages(TestBase, TempdirMixin):
         for req in bad_tests:
             self.assertRaises(VersionError, expand_requirement, req)
 
+    def test_variant_from_uri(self):
+        """Test getting a variant from its uri."""
+        package = get_package("variants_py", "2.0")
+        for variant in package.iter_variants():
+            variant2 = get_variant_from_uri(variant.uri)
+            self.assertEqual(variant, variant2)
+
 
 class TestMemoryPackages(TestBase):
     def test_1_memory_variant_parent(self):