diff --git a/CHANGES.md b/CHANGES.md
index 6fb80db0208..6949e37e9bd 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -21,6 +21,17 @@ pages**.
- cylc-8 (master branch, Python 3 - not yet released) does not bundle Jinja2,
and uses the fixed version 2.10.1.
+-------------------------------------------------------------------------------
+## __cylc-7.8.5 (2019-Q4?)__
+
+
+### Enhancements
+
+[#3349](https://github.com/cylc/cylc-flow/pull/3349) - new command `cylc
+ref-graph` to generate text-format "reference graphs" without PyGTK (back-port
+from Python 3 master for Cylc 8).
+
+
-------------------------------------------------------------------------------
## __cylc-7.8.4 (2019-09-04)__
diff --git a/bin/cylc b/bin/cylc
index d756fc18d4f..42344fdb60e 100755
--- a/bin/cylc
+++ b/bin/cylc
@@ -75,6 +75,10 @@ help_util() {
local COMMAND="${CYLC_HOME_BIN}/cylc-graph"
exec "${COMMAND}" "--help"
fi
+ if [[ "$@" == "ref-graph" ]]; then
+ local COMMAND="${CYLC_HOME_BIN}/cylc-ref-graph"
+ exec "${COMMAND}" "--help"
+ fi
# For help command/option with no args
if [[ $# == 0 && "${HELP_OPTS[*]} " == *"$UTIL"* ]]; then
exec "${CYLC_HOME_BIN}/cylc-help"
diff --git a/bin/cylc-graph b/bin/cylc-graph
index 4ed0d9ae415..0550c5ed4ce 100755
--- a/bin/cylc-graph
+++ b/bin/cylc-graph
@@ -26,7 +26,11 @@
Plot SUITE dependencies to a file FILE with a extension-derived format.
If FILE endswith ".png", output in PNG format, etc.
-Plot suite dependency graphs in an interactive graph viewer.
+Plot suite dependency graphs in an interactive graph viewer, or (with
+"--output-file") directly to image file.
+
+See also "cylc ref-graph" to generate the plain text "reference" graph format
+without the need for PyGTK to be installed.
If START is given it overrides "[visualization] initial cycle point" to
determine the start point of the graph, which defaults to the suite initial
diff --git a/bin/cylc-help b/bin/cylc-help
index 04eac4e1b35..3415d17f23a 100755
--- a/bin/cylc-help
+++ b/bin/cylc-help
@@ -311,6 +311,7 @@ preparation_commands['5to6'] = ['5to6']
preparation_commands['list'] = ['list', 'ls']
preparation_commands['search'] = ['search', 'grep']
preparation_commands['graph'] = ['graph']
+preparation_commands['ref-graph'] = ['ref-graph']
preparation_commands['graph-diff'] = ['graph-diff']
preparation_commands['diff'] = ['diff', 'compare']
preparation_commands['jobscript'] = ['jobscript']
@@ -380,6 +381,7 @@ comsum['validate'] = 'Parse and validate suite definitions'
comsum['5to6'] = 'Improve the cylc 6 compatibility of a cylc 5 suite file'
comsum['search'] = 'Search in suite definitions'
comsum['graph'] = 'Plot suite dependency graphs and runtime hierarchies'
+comsum['ref-graph'] = 'Print text-format "reference graphs" without GTK'
comsum['graph-diff'] = 'Compare two suite dependencies or runtime hierarchies'
comsum['diff'] = 'Compare two suite definitions and print differences'
# information
diff --git a/bin/cylc-ref-graph b/bin/cylc-ref-graph
new file mode 100755
index 00000000000..8dc72535786
--- /dev/null
+++ b/bin/cylc-ref-graph
@@ -0,0 +1,238 @@
+#!/usr/bin/env python2
+#
+# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
+# Copyright (C) 2008-2019 NIWA & British Crown (Met Office) & Contributors.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+"""Usage:
+ cylc ref-graph SUITE [START] [STOP]
+
+Implement the old ``cylc graph --reference command`` for producing a
+text-format representation of a suite graph.
+
+The `--reference` flag is optional here.
+
+THIS IS A BACK-PORT OF the Python 3 bin/cylc-graph FOR CYLC 8, TO GENERATE
+TEXT-FORMAT "REFERENCE GRAPHS" WITHOUT THE NEED FOR PYGTK TO BE INSTALLED."""
+
+from cylc.config import SuiteConfig
+from cylc.cycling.loader import get_point
+from cylc.option_parsers import CylcOptionParser as COP
+from cylc.suite_srv_files_mgr import (
+ SuiteSrvFilesManager, SuiteServiceFileError)
+from cylc.templatevars import load_template_vars
+
+
+def sort_integer_node(item):
+ """Return sort tokens for nodes with cyclepoints in integer format.
+
+ Example:
+ >>> sort_integer_node('foo.11')
+ ('foo', 11)
+
+ """
+ name, point = item.split('.')
+ return (name, int(point))
+
+
+def sort_integer_edge(item):
+ """Return sort tokens for edges with cyclepoints in integer format.
+
+ Example:
+ >>> sort_integer_edge(('foo.11', 'foo.12', None))
+ (('foo', 11), ('foo', 12))
+ >>> sort_integer_edge(('foo.11', None , None))
+ (('foo', 11), ('', 0))
+
+ """
+
+ return (
+ sort_integer_node(item[0]),
+ sort_integer_node(item[1]) if item[1] else ('', 0)
+ )
+
+
+def sort_datetime_edge(item):
+ """Return sort tokens for edges with cyclepoints in ISO8601 format.
+
+ Example:
+ >>> sort_datetime_edge(('a', None, None))
+ ('a', '')
+
+ """
+ return (item[0], item[1] or '')
+
+
+def get_cycling_bounds(config, start_point=None, stop_point=None):
+ """Determine the start and stop points for graphing a suite."""
+ # default start and stop points to values in the visualization section
+ if not start_point:
+ start_point = config.cfg['visualization']['initial cycle point']
+ if not stop_point:
+ viz_stop_point = config.cfg['visualization']['final cycle point']
+ if viz_stop_point:
+ stop_point = viz_stop_point
+
+ # don't allow stop_point before start_point
+ if stop_point is not None:
+ if get_point(stop_point) < get_point(start_point):
+ # NOTE: we need to cast with get_point for this comparison due to
+ # ISO8061 extended datetime formats
+ stop_point = start_point
+
+ return start_point, stop_point
+
+
+def graph_workflow(config, start_point=None, stop_point=None, ungrouped=False,
+ show_suicide=False):
+ """Implement ``cylc-graph --reference``."""
+ # set sort keys based on cycling mode
+ if config.cfg['scheduling']['cycling mode'] == 'integer':
+ # integer sorting
+ node_sort = sort_integer_node
+ edge_sort = sort_integer_edge
+ else:
+ # datetime sorting
+ node_sort = None # lexicographically sortable
+ edge_sort = sort_datetime_edge
+
+ # get graph
+ start_point, stop_point = get_cycling_bounds(
+ config, start_point, stop_point)
+ graph = config.get_graph_raw(
+ start_point, stop_point, ungroup_all=ungrouped)
+ if not graph:
+ return
+
+ edges = (
+ (left, right)
+ for left, right, _, suicide, _ in graph
+ if right
+ if show_suicide or not suicide
+ )
+ for left, right in sorted(set(edges), key=edge_sort):
+ print('edge "%s" "%s"' % (left, right))
+
+ print('graph')
+
+ # print nodes
+ nodes = (
+ node
+ for left, right, _, suicide, _ in graph
+ for node in (left, right)
+ if node
+ if show_suicide or not suicide
+ )
+ for node in sorted(set(nodes), key=node_sort):
+ print('node "%s" "%s"' % (node, node.replace('.', r'\n')))
+
+ print('stop')
+
+
+def graph_inheritance(config):
+ """Implement ``cylc-graph --reference --namespaces``."""
+ edges = set()
+ nodes = set()
+ for namespace, tasks in config.get_parent_lists().items():
+ for task in tasks:
+ edges.add((task, namespace))
+ nodes.add(task)
+
+ for namespace in config.get_parent_lists():
+ nodes.add(namespace)
+
+ for edge in sorted(edges):
+ print('edge %s %s' % edge)
+
+ print('graph')
+
+ for node in sorted(nodes):
+ print('node %s %s' % (node, node))
+
+ print('stop')
+
+
+def get_config(suite, opts, template_vars=None):
+ """Return a SuiteConfig object for the provided reg / path."""
+ try:
+ suiterc = SuiteSrvFilesManager().get_suite_rc(suite)
+ except SuiteServiceFileError:
+ # could not find suite, assume we have been given a path instead
+ suiterc = suite
+ suite = 'test'
+ return SuiteConfig(suite, suiterc, opts, template_vars=template_vars)
+
+
+def get_option_parser():
+ """CLI."""
+ parser = COP(
+ __doc__, jset=True, prep=True,
+ argdoc=[
+ ('[SUITE]', 'Suite name or path'),
+ ('[START]', 'Initial cycle point '
+ '(default: suite initial point)'),
+ ('[STOP]', 'Final cycle point '
+ '(default: initial + 3 points)')])
+
+ parser.add_option(
+ '-u', '--ungrouped',
+ help='Start with task families ungrouped (the default is grouped).',
+ action='store_true', default=False, dest='ungrouped')
+
+ parser.add_option(
+ '-n', '--namespaces',
+ help='Plot the suite namespace inheritance hierarchy '
+ '(task run time properties).',
+ action='store_true', default=False, dest='namespaces')
+
+ parser.add_option(
+ '-r', '--reference',
+ help='Output in a sorted plain text format for comparison purposes.',
+ action='store_true', default=True, dest='reference')
+
+ parser.add_option(
+ '--show-suicide',
+ help='Show suicide triggers. They are not shown by default, unless '
+ 'toggled on with the tool bar button.',
+ action='store_true', default=False, dest='show_suicide')
+
+ parser.add_option(
+ '--icp', action='store', default=None, metavar='CYCLE_POINT', help=(
+ 'Set initial cycle point. Required if not defined in suite.rc.'))
+
+ return parser
+
+
+def main(opts, suite=None, start=None, stop=None):
+ """Implement ``cylc graph``."""
+ if opts.ungrouped and opts.namespaces:
+ raise Exception('Cannot combine --ungrouped and --namespaces.')
+ if not opts.reference:
+ raise Exception('Only the --reference use cases are supported')
+
+ template_vars = load_template_vars(
+ opts.templatevars, opts.templatevars_file)
+
+ config = get_config(suite, opts, template_vars=template_vars)
+ if opts.namespaces:
+ graph_inheritance(config)
+ else:
+ graph_workflow(config, start, stop, ungrouped=opts.ungrouped,
+ show_suicide=opts.show_suicide)
+
+
+if __name__ == '__main__':
+ options, args = get_option_parser().parse_args()
+ suite, _ = SuiteSrvFilesManager().parse_suite_arg(options, args[0])
+ main(options, suite)
diff --git a/doc/src/installation.rst b/doc/src/installation.rst
index 3474aa311a9..8c1438cdf21 100644
--- a/doc/src/installation.rst
+++ b/doc/src/installation.rst
@@ -25,12 +25,21 @@ The following packages are highly recommended, but are technically optional as
you can construct and run suites without dependency graph visualisation or
the Cylc GUIs:
-- `PyGTK `_ - GUI toolkit.
+- `PyGTK `_ - Python bindings for the GTK+ GUI toolkit.
.. note::
- PyGTK typically comes with your system Python. It is allegedly quite
- difficult to install if you need to do so for another Python version.
+ PyGTK typically comes with your system Python 2 version. It is allegedly
+ quite difficult to install if you need to do so for another Python
+ version. At time of writing, for instance, there are no functional PyGTK
+ conda packages available.
+
+ Note that **we need to do ``import gtk`` in Python, not ``import pygtk``**.
+
+ In Centos 7.6, for example, the Cylc GUIs run "out of the box" with the
+ system-installed Python 2.7.5. Under the hood, the Python “gtk” package is
+ provided by the “pygtk2” yum package. (The “pygtk” Python module, which we
+ don't need, is supplied by the “pygobject2” yum package).
- `Graphviz `_ - graph layout engine (tested 2.36.0)
- `Pygraphviz `_ - Python Graphviz interface
@@ -39,6 +48,12 @@ the Cylc GUIs:
- python-devel
- graphviz-devel
+ .. note::
+
+ The ``cylc graph`` command for static workflow visualization requires
+ PyGTK, but we provide a separate ``cylc ref-graph`` command to print
+ out a simple text-format "reference graph" without PyGTK.
+
The Cylc Review service does not need any additional packages.
The following packages are necessary for running all the tests in Cylc:
diff --git a/tests/cylc-graph-diff/00-simple.t b/tests/cylc-graph-diff/00-simple.t
index 1ca3401aded..fa6f9bef46f 100644
--- a/tests/cylc-graph-diff/00-simple.t
+++ b/tests/cylc-graph-diff/00-simple.t
@@ -18,7 +18,7 @@
# Test cylc graph-diff for two suites.
. $(dirname $0)/test_header
#-------------------------------------------------------------------------------
-set_test_number 24
+set_test_number 27
#-------------------------------------------------------------------------------
install_suite $TEST_NAME_BASE-control $TEST_NAME_BASE-control
CONTROL_SUITE_NAME=$SUITE_NAME
@@ -160,6 +160,18 @@ node "foo.20140810T0000Z" "foo\n20140810T0000Z"
stop
__OUT__
cmp_ok "$TEST_NAME.stderr"