Repository: sagemath/sage

Issue:
sage-package: Add commands "dependencies", "trees"
<div id="comment:0"></div>

to simplify bootstrapping and configure code


Component: **refactoring**

_Issue created by migration from https://trac.sagemath.org/ticket/33860_




Discussion:

Resolving patch:
diff --git a/build/sage_bootstrap/app.py b/build/sage_bootstrap/app.py
--- a/build/sage_bootstrap/app.py
+++ b/build/sage_bootstrap/app.py
@@ -94,8 +94,9 @@ def properties(self, *package_classes, **kwds):
         pc = PackageClass(*package_classes)
         for package_name in pc.names:
             package = Package(package_name)
-            if format == 'plain':
-                print("{0}:".format(package_name))
+            if len(pc.names) > 1:
+                if format == 'plain':
+                    print("{0}:".format(package_name))
             for p in props:
                 value = getattr(package, p)
                 if value is None:
@@ -108,6 +109,79 @@ def properties(self, *package_classes, **kwds):
                 else:
                     print("{0}_{1}='{2}'".format(p, package_name, value))
 
+    def dependencies(self, *package_classes, **kwds):
+        """
+        Find the dependencies given package names
+
+        $ sage --package dependencies maxima --runtime --order-only --format=shell
+        order_only_deps_maxima='info'
+        runtime_deps_maxima='ecl'
+        """
+        types = kwds.pop('types', None)
+        format = kwds.pop('format', 'plain')
+        log.debug('Looking up dependencies')
+        pc = PackageClass(*package_classes)
+        if format in ['plain', 'rst']:
+            if types is None:
+                typesets = [['order_only', 'runtime']]
+            else:
+                typesets = [[t] for t in types]
+        elif format == 'shell':
+            if types is None:
+                types = ['order_only', 'optional', 'runtime', 'check']
+            typesets = [[t] for t in types]
+        else:
+            raise ValueError('format must be one of "plain", "rst", and "shell"')
+
+        for package_name in pc.names:
+            package = Package(package_name)
+            if len(pc.names) > 1:
+                if format == 'plain':
+                    print("{0}:".format(package_name))
+                    indent1 = "        "
+                elif format == 'rst':
+                    print("\n{0}\n{1}\n".format(package_name, "~" * len(package_name)))
+                    indent1 = ""
+            else:
+                indent1 = ""
+
+            for typeset in typesets:
+                if len(typesets) > 1:
+                    if format == 'plain':
+                        print(indent1 + "{0}: ".format('/'.join(typeset)))
+                        indent2 = indent1 + "        "
+                    elif format == 'rst':
+                        print("\n" + indent1 + ".. tab:: {0}\n".format('/'.join(typeset)))
+                        indent2 = indent1 + "    "
+                else:
+                    indent2 = indent1
+
+                deps = []
+                for t in typeset:
+                    deps.extend(getattr(package, 'dependencies_' + t))
+                deps = sorted(set(deps))
+
+                if format in ['plain', 'rst']:
+                    for dep in deps:
+                        if '/' in dep:
+                            # Suppress dependencies on source files, e.g. of the form $(SAGE_ROOT)/..., $(SAGE_SRC)/...
+                            continue
+                        if dep == 'FORCE':
+                            # Suppress FORCE
+                            continue
+                        if dep.startswith('$('):
+                            # Dependencies like $(BLAS)
+                            print(indent2 + "- {0}".format(dep))
+                        elif format == 'rst' and Package(dep).has_file('SPKG.rst'):
+                            # This RST label is set in src/doc/bootstrap
+                            print(indent2 + "- :ref:`spkg_{0}`".format(dep))
+                        else:
+                            print(indent2 + "- {0}".format(dep))
+                elif format == 'shell':
+                    # We single-quote the values because dependencies
+                    # may contain Makefile variable substitutions
+                    print("{0}_deps_{1}='{2}'".format(t, package_name, ' '.join(deps)))
+
     def name(self, tarball_filename):
         """
         Find the package name given a tarball filename
diff --git a/build/sage_bootstrap/cmdline.py b/build/sage_bootstrap/cmdline.py
--- a/build/sage_bootstrap/cmdline.py
+++ b/build/sage_bootstrap/cmdline.py
@@ -99,6 +99,36 @@
 """
 
 
+epilog_dependencies = \
+"""
+Print the list of packages that are dependencies of given package.
+By default, list a summary of the build, order-only, and runtime
+dependencies.
+
+EXAMPLE:
+
+    $ sage --package dependencies maxima openblas
+    maxima:
+            - ecl
+            - info
+    openblas:
+            - gfortran
+    $ sage --package dependencies maxima --runtime
+    - ecl
+
+    $ sage --package dependencies maxima openblas --runtime --order-only
+    maxima:
+            order_only:
+                    - info
+            runtime:
+                    - ecl
+    openblas:
+            order_only:
+            runtime:
+                    - gfortran
+"""
+
+
 epilog_name = \
 """
 Find the package name given a tarball filename
@@ -286,6 +316,31 @@ def make_parser():
         '--format', type=str, default='plain',
         help='output format (one of plain and shell; default: plain)')
 
+    parser_dependencies = subparsers.add_parser(
+        'dependencies', epilog=epilog_dependencies,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        help='Print the list of packages that are dependencies of given packages')
+    parser_dependencies.add_argument(
+        'package_class', metavar='[package_name|:package_type:]',
+        type=str, nargs='+',
+        help=('package name or designator for all packages of a given type '
+              '(one of :all:, :standard:, :optional:, and :experimental:)'))
+    parser_dependencies.add_argument(
+        '--order-only', action='store_true',
+        help='list the order-only build dependencies')
+    parser_dependencies.add_argument(
+        '--optional', action='store_true',
+        help='list the optional build dependencies')
+    parser_dependencies.add_argument(
+        '--runtime', action='store_true',
+        help='list the runtime dependencies')
+    parser_dependencies.add_argument(
+        '--check', action='store_true',
+        help='list the check dependencies')
+    parser_dependencies.add_argument(
+        '--format', type=str, default='plain',
+        help='output format (one of plain, rst, and shell; default: plain)')
+
     parser_name = subparsers.add_parser(
         'name', epilog=epilog_name,
         formatter_class=argparse.RawDescriptionHelpFormatter,
@@ -435,6 +490,19 @@ def run():
                      exclude_dependencies=args.exclude_dependencies)
     elif args.subcommand == 'properties':
         app.properties(*args.package_class, format=args.format)
+    elif args.subcommand == 'dependencies':
+        types = []
+        if args.order_only:
+            types.append('order_only')
+        if args.optional:
+            types.append('optional')
+        if args.runtime:
+            types.append('runtime')
+        if args.check:
+            types.append('check')
+        if not types:
+            types = None
+        app.dependencies(*args.package_class, types=types, format=args.format)
     elif args.subcommand == 'name':
         app.name(args.tarball_filename)
     elif args.subcommand == 'tarball':
diff --git a/build/sage_bootstrap/package.py b/build/sage_bootstrap/package.py
--- a/build/sage_bootstrap/package.py
+++ b/build/sage_bootstrap/package.py
@@ -380,6 +380,23 @@ def dependencies_order_only(self):
         """
         return self.__dependencies.partition('|')[2].strip().split() + self.__dependencies_order_only.strip().split()
 
+    @property
+    def dependencies_optional(self):
+        """
+        Return a list of strings, the package names of the optional build dependencies
+        """
+        return self.__dependencies_optional.strip().split()
+
+    @property
+    def dependencies_runtime(self):
+        """
+        Return a list of strings, the package names of the runtime dependencies
+        """
+        # after a '|', we have order-only build dependencies
+        return self.__dependencies.partition('|')[0].strip().split()
+
+    dependencies = dependencies_runtime
+
     @property
     def dependencies_check(self):
         """
@@ -503,17 +520,22 @@ def _init_install_requires(self):
     def _init_dependencies(self):
         try:
             with open(os.path.join(self.path, 'dependencies')) as f:
-                self.__dependencies = f.readline().strip()
+                self.__dependencies = f.readline().partition('#')[0].strip()
         except IOError:
             self.__dependencies = ''
         try:
             with open(os.path.join(self.path, 'dependencies_check')) as f:
-                self.__dependencies_check = f.readline().strip()
+                self.__dependencies_check = f.readline().partition('#')[0].strip()
         except IOError:
             self.__dependencies_check = ''
+        try:
+            with open(os.path.join(self.path, 'dependencies_optional')) as f:
+                self.__dependencies_optional = f.readline().partition('#')[0].strip()
+        except IOError:
+            self.__dependencies_optional = ''
         try:
             with open(os.path.join(self.path, 'dependencies_order_only')) as f:
-                self.__dependencies_order_only = f.readline()
+                self.__dependencies_order_only = f.readline().partition('#')[0].strip()
         except IOError:
             self.__dependencies_order_only = ''
 


Test patch: