18609 support for a human-readable version string
authorDanek Duvall <danek.duvall@oracle.com>
Thu, 04 Aug 2011 13:17:33 -0700
changeset 2493 5db1603f0c3f
parent 2492 41794fa860e3
child 2494 106731ab1e98
18609 support for a human-readable version string
doc/client_api_versions.txt
doc/guide-metadata-conventions.rst
doc/tags-and-attributes.txt
src/client.py
src/gui/modules/misc_non_gui.py
src/modules/api_common.py
src/modules/client/api.py
src/modules/lint/engine.py
src/modules/publish/dependencies.py
src/packagemanager.py
src/pkgdep.py
src/sysrepo.py
src/tests/api/t_api_list.py
src/tests/api/t_linked_image.py
src/tests/cli/t_pkg_info.py
src/tests/pkg5unittest.py
src/util/distro-import/importer.py
--- a/doc/client_api_versions.txt	Wed Aug 03 17:49:35 2011 -0700
+++ b/doc/client_api_versions.txt	Thu Aug 04 13:17:33 2011 -0700
@@ -1,3 +1,14 @@
+Version 65:
+Incompatible with clients using versions 0-64.
+
+    pkg.client.api.ImageInterface has changed as follows:
+        * get_pkg_list() yields five values instead of four; the fifth is a
+          dictionary of package attributes.
+
+    pkg.client.api.PackageInfo has changed as follows:
+        * added get_attr_values() to return the values of named package
+          attributes.
+
 Version 64:
 Incompatible with clients using versions 0-63.
 
--- a/doc/guide-metadata-conventions.rst	Wed Aug 03 17:49:35 2011 -0700
+++ b/doc/guide-metadata-conventions.rst	Thu Aug 04 13:17:33 2011 -0700
@@ -119,6 +119,14 @@
        A value of "true" indicates the package is obsolete and should be
        removed on upgrade.
 
+    pkg.human-version
+       For components whose upstream version isn't a dot-separated sequence
+       of nonnegative integers (OpenSSL's 0.9.8r, for example), this
+       attribute can be set to that string, and will be displayed when
+       appropriate.  It cannot be used in an FMRI to install a particular
+       version; package authors must still convert the version into a
+       sequence of integers.
+
     variant.*
        See facets.txt
 
--- a/doc/tags-and-attributes.txt	Wed Aug 03 17:49:35 2011 -0700
+++ b/doc/tags-and-attributes.txt	Thu Aug 04 13:17:33 2011 -0700
@@ -90,6 +90,14 @@
        A value of "true" indicates the package is obsolete and should be
        removed on upgrade.
 
+    pkg.human-version
+       For components whose upstream version isn't a dot-separated sequence
+       of nonnegative integers (OpenSSL's 0.9.8r, for example), this
+       attribute can be set to that string, and will be displayed when
+       appropriate.  It cannot be used in an FMRI to install a particular
+       version; package authors must still convert the version into a
+       sequence of integers.
+
     variant.*
        See facets.txt
 
--- a/src/client.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/client.py	Thu Aug 04 13:17:33 2011 -0700
@@ -90,7 +90,7 @@
         import sys
         sys.exit(1)
 
-CLIENT_API_VERSION = 64
+CLIENT_API_VERSION = 65
 PKG_CLIENT_NAME = "pkg"
 
 JUST_UNKNOWN = 0
@@ -470,7 +470,7 @@
         try:
                 res = api_inst.get_pkg_list(pkg_list, patterns=pargs,
                     raise_unmatched=True, repos=origins, variants=variants)
-                for pt, summ, cats, states in res:
+                for pt, summ, cats, states, attrs in res:
                         found = True
                         if not omit_headers:
                                 if verbose:
@@ -3017,7 +3017,12 @@
 
                 # XXX even more info on the publisher would be nice?
                 msg(_("     Publisher:"), pi.publisher)
-                msg(_("       Version:"), pi.version)
+                hum_ver = pi.get_attr_values("pkg.human-version")
+                if hum_ver and hum_ver[0] != pi.version:
+                        msg(_("       Version:"), "%s (%s)" %
+                            (pi.version, hum_ver[0]))
+                else:
+                        msg(_("       Version:"), pi.version)
                 msg(_(" Build Release:"), pi.build_release)
                 msg(_("        Branch:"), pi.branch)
                 msg(_("Packaging Date:"), pi.packaging_date)
@@ -3423,7 +3428,7 @@
                     repos=origins)
                 manifests = []
 
-                for pfmri, summ, cats, states in res:
+                for pfmri, summ, cats, states, pattrs in res:
                         manifests.append(api_inst.get_manifest(pfmri,
                             all_variants=display_raw, repos=origins))
         except api_errors.ImageFormatUpdateNeeded, e:
--- a/src/gui/modules/misc_non_gui.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/gui/modules/misc_non_gui.py	Thu Aug 04 13:17:33 2011 -0700
@@ -41,7 +41,7 @@
 
 # The current version of the Client API the PM, UM and
 # WebInstall GUIs have been tested against and are known to work with.
-CLIENT_API_VERSION = 64
+CLIENT_API_VERSION = 65
 LOG_DIR = "/var/tmp"
 LOG_ERROR_EXT = "_error.log"
 LOG_INFO_EXT = "_info.log"
--- a/src/modules/api_common.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/modules/api_common.py	Thu Aug 04 13:17:33 2011 -0700
@@ -122,10 +122,10 @@
         UNSUPPORTED = image.Image.PKG_STATE_UNSUPPORTED
         FROZEN = image.Image.PKG_STATE_FROZEN
 
-        __NUM_PROPS = 12
+        __NUM_PROPS = 13
         IDENTITY, SUMMARY, CATEGORIES, STATE, SIZE, LICENSES, LINKS, \
-            HARDLINKS, FILES, DIRS, DEPENDENCIES, DESCRIPTION = \
-            range(__NUM_PROPS)
+            HARDLINKS, FILES, DIRS, DEPENDENCIES, DESCRIPTION, \
+            ALL_ATTRIBUTES = range(__NUM_PROPS)
         ALL_OPTIONS = frozenset(range(__NUM_PROPS))
         ACTION_OPTIONS = frozenset([LINKS, HARDLINKS, FILES, DIRS,
             DEPENDENCIES])
@@ -134,7 +134,7 @@
             category_info_list=None, states=None, publisher=None,
             version=None, build_release=None, branch=None, packaging_date=None,
             size=None, licenses=None, links=None, hardlinks=None, files=None,
-            dirs=None, dependencies=None, description=None):
+            dirs=None, dependencies=None, description=None, attrs=None):
                 self.pkg_stem = pkg_stem
 
                 self.summary = summary
@@ -156,6 +156,7 @@
                 self.dirs = dirs
                 self.dependencies = dependencies
                 self.description = description
+                self.attrs = attrs or {}
 
         def __str__(self):
                 return self.fmri
@@ -172,6 +173,28 @@
                     packaging_date=version.get_timestamp().strftime("%c"),
                     pfmri=f)
 
+        def get_attr_values(self, name, modifiers=()):
+                """Returns a list of the values of the package attribute 'name'.
+
+                The 'modifiers' parameter, if present, is a dict containing
+                key/value pairs, all of which must be present on an action in
+                order for the values to be returned.
+
+                Returns an empty list if there are no values.
+                """
+
+                # XXX should the modifiers parameter be allowed to be a subset
+                # of an action's modifiers?
+                if isinstance(modifiers, dict):
+                        modifiers = tuple(
+                            (k, isinstance(modifiers[k], basestring) and
+                                tuple([sorted(modifiers[k])]) or
+                                tuple(sorted(modifiers[k])))
+                            for k in sorted(modifiers.iterkeys())
+                        )
+                return self.attrs.get(name, {modifiers: []}).get(
+                    modifiers, [])
+
 
 def _get_pkg_cat_data(cat, info_needed, actions=None,
     excludes=misc.EmptyI, pfmri=None):
--- a/src/modules/client/api.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/modules/client/api.py	Thu Aug 04 13:17:33 2011 -0700
@@ -73,7 +73,7 @@
 from pkg.client.pkgdefs import *
 from pkg.smf import NonzeroExitException
 
-CURRENT_API_VERSION = 64
+CURRENT_API_VERSION = 65
 CURRENT_P5I_VERSION = 1
 
 # Image type constants.
@@ -818,20 +818,20 @@
                     patterns=["release/name"], return_fmris=True)
                 results = [e for e in results]
                 if results:
-                        pfmri, summary, categories, states = \
-                            results[0]
+                        pfmri, summary, categories, states, attrs = results[0]
                         mfst = self._img.get_manifest(pfmri)
                         osname = mfst.get("pkg.release.osname", None)
                         if osname == "sunos":
                                 return True
 
                 # Otherwise, see if we can find package/pkg (or SUNWipkg) and
-                # SUNWcs.
+                # system/core-os (or SUNWcs).
                 results = self.__get_pkg_list(self.LIST_INSTALLED,
-                    patterns=["pkg:/package/pkg", "SUNWipkg", "SUNWcs"])
+                    patterns=["/package/pkg", "SUNWipkg", "/system/core-os",
+                        "SUNWcs"])
                 installed = set(e[0][1] for e in results)
-                if "SUNWcs" in installed and ("SUNWipkg" in installed or
-                    "package/pkg" in installed):
+                if ("SUNWcs" in installed or "system/core-os" in installed) and \
+                    ("SUNWipkg" in installed or "package/pkg" in installed):
                         return True
 
                 return False
@@ -2654,9 +2654,9 @@
                         self._img.cleanup_downloads()
 
         @_LockedGenerator()
-        def get_pkg_list(self, pkg_list, cats=None, patterns=misc.EmptyI,
-            pubs=misc.EmptyI, raise_unmatched=False, repos=None,
-            return_fmris=False, variants=False):
+        def get_pkg_list(self, pkg_list, cats=None, collect_attrs=False,
+            patterns=misc.EmptyI, pubs=misc.EmptyI, raise_unmatched=False,
+            repos=None, return_fmris=False, variants=False):
                 """A generator function that produces tuples of the form:
 
                     (
@@ -2667,7 +2667,8 @@
                         ),
                         summary,    - (string) the package summary
                         categories, - (list) string tuples of (scheme, category)
-                        states      - (list) PackageInfo states
+                        states,     - (list) PackageInfo states
+                        attributes  - (dict) package attributes
                     )
 
                 Results are always sorted by stem, publisher, and then in
@@ -2704,6 +2705,11 @@
                 to any package category.  A value of None indicates that no
                 package category filtering should be applied.
 
+                'collect_attrs' is an optional boolean that indicates whether
+                all package attributes should be collected and returned in the
+                fifth element of the return tuple.  If False, that element will
+                be an empty dictionary.
+
                 'patterns' is an optional list of FMRI wildcard strings to
                 filter results by.
 
@@ -2731,14 +2737,14 @@
                 to retrieve the requested package information."""
 
                 return self.__get_pkg_list(pkg_list, cats=cats,
-                    patterns=patterns, pubs=pubs,
+                    collect_attrs=collect_attrs, patterns=patterns, pubs=pubs,
                     raise_unmatched=raise_unmatched, repos=repos,
                     return_fmris=return_fmris, variants=variants)
 
-        def __get_pkg_list(self, pkg_list, cats=None, inst_cat=None,
-            known_cat=None, patterns=misc.EmptyI, pubs=misc.EmptyI,
-            raise_unmatched=False, repos=None, return_fmris=False,
-            variants=False):
+        def __get_pkg_list(self, pkg_list, cats=None, collect_attrs=False,
+            inst_cat=None, known_cat=None, patterns=misc.EmptyI,
+            pubs=misc.EmptyI, raise_unmatched=False, repos=None,
+            return_fmris=False, variants=False):
                 """This is the implementation of get_pkg_list.  The other
                 function is a wrapper that uses locking.  The separation was
                 necessary because of API functions that already perform locking
@@ -3054,6 +3060,8 @@
                         omit_var = False
                         states = entry["metadata"]["states"]
                         pkgi = self._img.PKG_STATE_INSTALLED in states
+                        ddm = lambda: collections.defaultdict(list)
+                        attrs = collections.defaultdict(ddm)
                         try:
                                 for a in actions:
                                         if a.name == "depend" and \
@@ -3065,6 +3073,18 @@
 
                                         atname = a.attrs["name"]
                                         atvalue = a.attrs["value"]
+                                        if collect_attrs:
+                                                atvlist = a.attrlist("value")
+
+                                                # XXX Need to describe this data
+                                                # structure sanely somewhere.
+                                                mods = tuple(
+                                                    (k, tuple(sorted(a.attrlist(k))))
+                                                    for k in sorted(a.attrs.iterkeys())
+                                                    if k not in ("name", "value")
+                                                )
+                                                attrs[atname][mods].extend(atvlist)
+
                                         if atname == "pkg.summary":
                                                 summ = atvalue
                                                 continue
@@ -3074,6 +3094,10 @@
                                                         # Historical summary
                                                         # field.
                                                         summ = atvalue
+                                                        collect_attrs and \
+                                                            attrs["pkg.summary"] \
+                                                            [mods]. \
+                                                            extend(atvlist)
                                                 continue
 
                                         if atname == "info.classification":
@@ -3174,9 +3198,9 @@
                         if return_fmris:
                                 pfmri = fmri.PkgFmri("%s@%s" % (stem, ver),
                                     build_release=brelease, publisher=pub)
-                                yield (pfmri, summ, pcats, states)
+                                yield (pfmri, summ, pcats, states, attrs)
                         else:
-                                yield (t, summ, pcats, states)
+                                yield (t, summ, pcats, states, attrs)
 
                 if raise_unmatched:
                         # Caller has requested that non-matching patterns or
@@ -3249,6 +3273,8 @@
                 act_opts = PackageInfo.ACTION_OPTIONS - \
                     frozenset([PackageInfo.DEPENDENCIES])
 
+                collect_attrs = PackageInfo.ALL_ATTRIBUTES in info_needed
+
                 pis = []
                 rval = {
                     self.INFO_FOUND: pis,
@@ -3257,8 +3283,9 @@
                 }
 
                 try:
-                        for pfmri, summary, cats, states in self.__get_pkg_list(
-                            ilist, inst_cat=inst_cat, known_cat=known_cat,
+                        for pfmri, summary, cats, states, attrs in self.__get_pkg_list(
+                            ilist, collect_attrs=collect_attrs,
+                            inst_cat=inst_cat, known_cat=known_cat,
                             patterns=fmri_strings, raise_unmatched=True,
                             return_fmris=True, variants=True):
                                 release = build_release = branch = \
@@ -3385,7 +3412,7 @@
                                     pfmri=pfmri, licenses=licenses,
                                     links=links, hardlinks=hardlinks, files=files,
                                     dirs=dirs, dependencies=dependencies,
-                                    description=description))
+                                    description=description, attrs=attrs))
                 except apx.InventoryException, e:
                         if e.illegal:
                                 self.log_operation_end(
--- a/src/modules/lint/engine.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/modules/lint/engine.py	Thu Aug 04 13:17:33 2011 -0700
@@ -39,7 +39,7 @@
 import sys
 
 PKG_CLIENT_NAME = "pkglint"
-CLIENT_API_VERSION = 64
+CLIENT_API_VERSION = 65
 pkg.client.global_settings.client_name = PKG_CLIENT_NAME
 
 class LintEngineException(Exception):
--- a/src/modules/publish/dependencies.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/modules/publish/dependencies.py	Thu Aug 04 13:17:33 2011 -0700
@@ -1331,11 +1331,11 @@
         pkg_list = None
         if use_system:
                 # We make this a list because we want to reuse the list of
-                # packages again, and despite it's name, get_pkg_list is a
+                # packages again, and despite its name, get_pkg_list is a
                 # generator.
                 pkg_list = list(api_inst.get_pkg_list(
                     api.ImageInterface.LIST_INSTALLED))
-                for (pub, stem, ver), summ, cats, states in pkg_list:
+                for (pub, stem, ver), summ, cats, states, attrs in pkg_list:
                         pfmri = fmri.PkgFmri("pkg:/%s@%s" % (stem, ver), "5.11")
                         if pfmri.pkg_name in resolving_pkgs:
                                 continue
@@ -1355,7 +1355,7 @@
         # Build a list of all files delivered in the packages installed on
         # the system.
         if use_system:
-                for (pub, stem, ver), summ, cats, states in pkg_list:
+                for (pub, stem, ver), summ, cats, states, attrs in pkg_list:
                         pfmri = fmri.PkgFmri("pkg:/%s@%s" % (stem, ver), "5.11")
                         if pfmri.pkg_name in resolving_pkgs:
                                 continue
--- a/src/packagemanager.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/packagemanager.py	Thu Aug 04 13:17:33 2011 -0700
@@ -4060,7 +4060,7 @@
                 self.special_package_names = []
                 pub_names = {}
                 for entry in pkgs_from_api:
-                        (pkg_pub, pkg_name, ver), summ, cats, states = entry
+                        (pkg_pub, pkg_name, ver), summ, cats, states, attrs = entry
                         if debug:
                                 print entry, ver
                         pkg_stem  = "pkg://" + pkg_pub + "/"  + pkg_name
--- a/src/pkgdep.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/pkgdep.py	Thu Aug 04 13:17:33 2011 -0700
@@ -42,7 +42,7 @@
 import pkg.publish.dependencies as dependencies
 from pkg.misc import msg, emsg, PipeError
 
-CLIENT_API_VERSION = 64
+CLIENT_API_VERSION = 65
 PKG_CLIENT_NAME = "pkgdepend"
 
 DEFAULT_SUFFIX = ".res"
--- a/src/sysrepo.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/sysrepo.py	Thu Aug 04 13:17:33 2011 -0700
@@ -53,7 +53,7 @@
 orig_cwd = None
 
 PKG_CLIENT_NAME = "pkg.sysrepo"
-CLIENT_API_VERSION = 64
+CLIENT_API_VERSION = 65
 pkg.client.global_settings.client_name = PKG_CLIENT_NAME
 
 # exit codes
--- a/src/tests/api/t_api_list.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/tests/api/t_api_list.py	Thu Aug 04 13:17:33 2011 -0700
@@ -384,7 +384,7 @@
                 returned = []
                 for entry in api_obj.get_pkg_list(pkg_list, cats=cats,
                     patterns=patterns, pubs=pubs, variants=variants):
-                        (pub, stem, ver), summ, pcats, raw_states = entry
+                        (pub, stem, ver), summ, pcats, raw_states, attrs = entry
 
                         sver = ver.split(":", 1)[0]
 
--- a/src/tests/api/t_linked_image.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/tests/api/t_linked_image.py	Thu Aug 04 13:17:33 2011 -0700
@@ -309,14 +309,14 @@
                 pkg_list = apio.get_pkg_list(api.ImageInterface.LIST_INSTALLED)
                 return set(sorted([
                         "pkg://%s/%s@%s" % (pfmri[0], pfmri[1], pfmri[2])
-                        for pfmri, summ, cats, states in pkg_list
+                        for pfmri, summ, cats, states, attrs in pkg_list
                 ]))
 
         def _list_all_packages(self, apio):
                 pkg_list = apio.get_pkg_list(api.ImageInterface.LIST_ALL)
                 return set(sorted([
                         "pkg://%s/%s@%s" % (pfmri[0], pfmri[1], pfmri[2])
-                        for pfmri, summ, cats, states in pkg_list
+                        for pfmri, summ, cats, states, attrs in pkg_list
                 ]))
 
         # utility functions for use by test cases
@@ -984,7 +984,7 @@
                 pkg_list = list(api_objs[1].get_pkg_list(
                     api.ImageInterface.LIST_INSTALLED))
                 self.assertEqual(len(pkg_list), 1)
-                pfmri, summ, cats, states = pkg_list[0]
+                pfmri, summ, cats, states, attrs = pkg_list[0]
                 pkg_installed = "%s@%s" % (pfmri[1], pfmri[2])
                 self.assertEqual(pkg_installed, self.p_foo1_name[2])
 
@@ -999,7 +999,7 @@
                 pkg_list = list(api_objs[1].get_pkg_list(
                     api.ImageInterface.LIST_INSTALLED))
                 self.assertEqual(len(pkg_list), 1)
-                pfmri, summ, cats, states = pkg_list[0]
+                pfmri, summ, cats, states, attrs = pkg_list[0]
                 pkg_installed = "%s@%s" % (pfmri[1], pfmri[2])
                 self.assertEqual(pkg_installed, self.p_foo1_name[0])
 
--- a/src/tests/cli/t_pkg_info.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/tests/cli/t_pkg_info.py	Thu Aug 04 13:17:33 2011 -0700
@@ -62,6 +62,12 @@
             close
         """
 
+        human = """
+            open [email protected],5.11-0
+            add set name=pkg.human-version value=0.9.8r
+            close
+        """
+
         misc_files = [ "tmp/bronzeA1",  "tmp/bronzeA2", "tmp/bronze1",
             "tmp/bronze2", "tmp/copyright1", "tmp/sh", "tmp/baz"]
 
@@ -69,7 +75,7 @@
                 pkg5unittest.SingleDepotTestCase.setUp(self)
                 self.make_misc_files(self.misc_files)
                 self.plist = self.pkgsend_bulk(self.rurl, (self.badfile10,
-                    self.baddir10, self.bronze10))
+                    self.baddir10, self.bronze10, self.human))
 
         def test_pkg_info_bad_fmri(self):
                 """Test bad frmi's with pkg info."""
@@ -257,6 +263,13 @@
                                 self.write_img_manifest(pfmri, bad_mdata)
                                 self.pkg("info -r %s" % pfmri.pkg_name, exit=0)
 
+        def test_human_version(self):
+                """Verify that info returns the expected output for packages
+                with a human-readable version defined."""
+
+                self.image_create(self.rurl)
+                self.pkg("info -r human | grep 'Version: 0.9.8.18 (0.9.8r)'")
+
         def test_renamed_packages(self):
                 """Verify that info returns the expected output for renamed
                 packages."""
--- a/src/tests/pkg5unittest.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/tests/pkg5unittest.py	Thu Aug 04 13:17:33 2011 -0700
@@ -127,7 +127,7 @@
 
 # Version test suite is known to work with.
 PKG_CLIENT_NAME = "pkg"
-CLIENT_API_VERSION = 64
+CLIENT_API_VERSION = 65
 
 ELIDABLE_ERRORS = [ TestSkippedException, depotcontroller.DepotStateException ]
 
--- a/src/util/distro-import/importer.py	Wed Aug 03 17:49:35 2011 -0700
+++ b/src/util/distro-import/importer.py	Thu Aug 04 13:17:33 2011 -0700
@@ -56,7 +56,7 @@
 from pkg.misc import emsg
 from pkg.portable import PD_LOCAL_PATH, PD_PROTO_DIR, PD_PROTO_DIR_LIST
 
-CLIENT_API_VERSION = 64
+CLIENT_API_VERSION = 65
 PKG_CLIENT_NAME = "importer.py"
 pkg.client.global_settings.client_name = PKG_CLIENT_NAME