diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000..9079951da7
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,33 @@
+/**
+ * Copyright (C) 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 .
+ */
+
+module.exports = {
+ root: true,
+ env: {
+ browser: true
+ },
+ extends: [
+ 'standard',
+ 'eslint:recommended'
+ ],
+ rules: {
+ 'operator-linebreak': ['error', 'before']
+ },
+ globals: {
+ '$': 'readonly'
+ }
+}
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index ac5ee369f7..0000000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "extends": "eslint:recommended",
- "parserOptions": {
- "ecmaVersion": 6
- },
- "env": {
- "browser": true,
- "es6": true,
- "jquery": true
- }
-}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000000..7c415317c4
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,227 @@
+name: test
+
+on:
+ pull_request:
+ workflow_dispatch:
+ inputs:
+ rose_ref:
+ description: The Rose branch to test against
+ required: true
+ fcm_ref:
+ description: The FCM branch to test against
+ required: false
+ fcm_repo:
+ description: The FCM repo to test against
+ required: false
+ cylc_ref:
+ description: The Cylc branch to test against
+ required: false
+ cylc_repo:
+ description: The Cylc repo to test against
+ required: false
+
+defaults:
+ run:
+ shell: bash #Â macos default shell is zsh
+
+jobs:
+ test:
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 45
+ strategy:
+ fail-fast: false
+ matrix:
+ os: ['ubuntu-latest']
+ python-version: ['3.7', '3.8', '3.9']
+ # TODO: re-enable macos testing
+ # currently (in the absence of a wrapper script) rose cannot be run
+ # from within cylc jobs in situations where the output of the following
+ # commands differ:
+ # bash -c 'which python'
+ # bash -l -c 'which python'
+ # include:
+ # - os: 'macos-latest'
+ # python-version: '3.7'
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.event.inputs.rose_ref || github.sha }}
+ path: rose
+
+ - name: Configure Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install Cylc
+ env:
+ cylc_repo: ${{ github.event.inputs.cylc_repo || 'cylc/cylc-flow' }}
+ cylc_branch: ${{ github.event.inputs.cylc_ref || 'master' }}
+ run: |
+ pip install "git+https://github.com/${cylc_repo}@${cylc_branch}"
+
+ - name: Brew Install
+ if: startsWith(matrix.os, 'macos')
+ run: |
+ # install system deps
+ brew install bash coreutils gnu-sed shellcheck sqlite3 subversion
+
+ # add GNU coreutils and sed to the user PATH (for actions steps)
+ # (see instructions in brew install output)
+ echo \
+ "$(brew --prefix)/opt/coreutils/libexec/gnubin" \
+ >> "${GITHUB_PATH}"
+ echo \
+ "/usr/local/opt/gnu-sed/libexec/gnubin" \
+ >> "${GITHUB_PATH}"
+
+ # add GNU coreutils and sed to the user PATH (for Cylc jobs)
+ cat >> "$HOME/.bashrc" <<__HERE__
+ PATH="/usr/local/opt/coreutils/libexec/gnubin:\$PATH"
+ PATH="/usr/local/opt/gnu-sed/libexec/gnubin:\$PATH"
+ PATH="$pythonLocation:\$PATH"
+ export PATH
+ # see NOTE in t/rosie-lookup/00-basic.t
+ export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
+ __HERE__
+ cat "$HOME/.bashrc"
+
+ - name: Apt-Get Install
+ if: startsWith(matrix.os, 'ubuntu')
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y shellcheck sqlite3 at
+
+ - name: MacOS DNS Patch
+ if: startsWith(matrix.os, 'macos')
+ run: |
+ # apply DNS patch
+ hostuserutil="$(python3 -c '
+ import cylc.flow.hostuserutil
+ print(cylc.flow.hostuserutil.__file__)
+ ')"
+ patch "${hostuserutil}" < rose/etc/conf/macos-patch
+
+ - name: Install Rose
+ working-directory: rose
+ run: |
+ pip install ."[all]"
+ pip install --no-deps git+https://github.com/cylc/cylc-rose.git
+ yarn install
+
+ - name: Checkout FCM
+ if: startsWith(matrix.os, 'ubuntu')
+ uses: actions/checkout@v2
+ with:
+ repository: ${{ github.event.inputs.fcm_repo || 'metomi/fcm' }}
+ ref: ${{ github.event.inputs.fcm_ref || 'master' }}
+ path: 'fcm'
+
+ - name: Install FCM
+ if: startsWith(matrix.os, 'ubuntu')
+ run: |
+ # install FCM deps
+ sudo apt-get install -y \
+ subversion \
+ build-essential \
+ gfortran \
+ libxml-parser-perl \
+ libconfig-inifiles-perl \
+ libdbi-perl \
+ libdbd-sqlite3-perl
+
+ # install wandisco
+ sudo sh -c 'echo "deb http://opensource.wandisco.com/ubuntu \
+ `lsb_release -cs` svn19" \
+ >> /etc/apt/sources.list.d/subversion19.list'
+ sudo wget -q http://opensource.wandisco.com/wandisco-debian.gpg -O- \
+ | sudo apt-key add -
+
+ # prepend FCM bin to $PATH
+ FCM_PATH="$GITHUB_WORKSPACE/fcm/bin"
+ # the github actions way (needed for cylc jobs)
+ echo "$FCM_PATH" >> "${GITHUB_PATH}"
+ # the bashrc wat (needed for subsequent gh action steps)
+ echo "export PATH=\"$FCM_PATH:\$PATH\"" >> "$HOME/.bashrc"
+
+ - name: Style
+ working-directory: rose
+ run: |
+ flake8
+ etc/bin/shellchecker
+ yarn run lint
+
+ - name: Unit Tests
+ working-directory: rose
+ run: |
+ pytest
+
+ - name: Functional Tests
+ timeout-minutes: 30
+ id: functest
+ working-directory: rose
+ env:
+ OBJC_DISABLE_INITIALIZE_FORK_SAFETY: YES
+ run: |
+ # rose tests should pass first time around
+ etc/bin/rose-test-battery -j 4 --state=save
+
+ - name: Re-Run Fails
+ if: failure() && steps.functest.outcome == 'failure'
+ working-directory: rose
+ env:
+ OBJC_DISABLE_INITIALIZE_FORK_SAFETY: YES
+ run: |
+ # so we only re-run for debug purposes
+ cylc scan --state=all --color=never
+ etc/bin/rose-test-battery -j 1 -v --state=save,failed
+
+ - name: Upload
+ if: failure() && steps.functest.outcome == 'failure'
+ uses: actions/upload-artifact@v2
+ with:
+ name: cylc-run ${{ matrix.os }}
+ path: ~/cylc-run/
+
+ docs:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ with:
+ ref: ${{ github.event.inputs.rose_ref || github.sha }}
+
+ - name: Configure Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.7
+
+ - name: install graphviz
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y graphviz pkg-config libgraphviz-dev
+ pip install pygraphviz
+
+ - name: install
+ run: |
+ pip install -e .[docs]
+
+ - name: build (html)
+ run: |
+ make -C sphinx/ html SPHINXOPTS='-Wn'
+
+ - name: build (slides)
+ run: |
+ make -C sphinx/ slides SPHINXOPTS='-Wn'
+
+ - name: build (linkcheck)
+ run: |
+ make -C sphinx/ linkcheck SPHINXOPTS='-Wn'
+
+ - name: debug
+ if: failure()
+ run: |
+ cat /tmp/sphinx-err* >&2 || true
diff --git a/.gitignore b/.gitignore
index 901998cab0..1794ebeac4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,11 +2,11 @@
*.swp
etc/rose.conf
etc/opt
-lib/bash/rose_init_site
doc
venv
metomi_rose.egg-info
dist
+node_modules
# coverage
.coverage
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f7c9d572cd..0000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-# Configuration for running Rose test battery on Travis CI
-# See https://travis-ci.org/ for more info.
-
----
-language: python
-python:
- - 3.7
-dist: xenial
-
-before_install:
- - export PATH="$PWD/.travis:$PATH"
-
-after_success:
- - now report coverage
-
-after_failure:
- - now report error
-
-jobs:
- include:
- - name: "Unit Tests"
- install:
- - now install coverage linters pytest cylc rose
- script:
- - now test style
- - now test units
-
- - name: "Test Battery"
- before_install:
- - export PATH="$PWD/.travis:$PATH"
- - now install coverage fcm tut_suite
- install:
- - now install cylc rose
- script:
- - now test battery
-
- - name: "Documentation"
- install:
- - now install coverage rose sphinx tut_suite
- script:
- - now test docs
diff --git a/.travis/cover.py b/.travis/cover.py
deleted file mode 100644
index c88e58b372..0000000000
--- a/.travis/cover.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-
-import sys
-
-from subprocess import call
-
-
-def main():
- command = ['etc/bin/rose-test-battery', '-j', '5']
- if call(command + ['--state=save']):
- # Non-zero return code
- sys.stderr.write('\n\nRerunning Failed Tests...\n\n')
- # Exit with final return code
- sys.exit(call(command + ['--state=save,failed', '-v']))
-
-
-if __name__ == '__main__':
- main()
diff --git a/.travis/now b/.travis/now
deleted file mode 100755
index c32b4f1026..0000000000
--- a/.travis/now
+++ /dev/null
@@ -1,253 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-
-# Bash script for use with Travis-CI builds.
-#
-# usage: now command [args...]
-#
-# commands:
-# build
-# install
-# report_coverage
-# test
-
-# source the .bashrc because for some reason this doesn't get done for us
-# do this before the set -eu to avoid bailing on the build for hardcoded
-# bashrc issues
-if [[ -f "${HOME}/.bashrc" ]]; then
- source "${HOME}/.bashrc"
-fi
-
-
-set -eu
-
-APT=()
-NPM=()
-PIP=()
-GIT=()
-PY_PATH=()
-RC_PATH=("${HOME}")
-RI_PATH=()
-WANDISCO=false
-
-
-_build_docs () {
- etc/bin/rose-make-docs --strict clean html slides latexpdf
-}
-
-_gh_extract () { # extract project from GitHub to $HOME
- IFS='|' read -ra SPEC <<< "$1"
- local USR="${SPEC[0]}"
- local REPO="${SPEC[1]}"
- local BRANCH="${SPEC[2]}"
- local URL="https://github.com/${USR}/${REPO}/archive/${BRANCH}.tar.gz"
- local DEST="${HOME}/${REPO}-${BRANCH}"
-
- if [[ -d "${DEST}" ]]; then
- # already installed
- return
- fi
-
- # download + unpack
- wget "${URL}" -O - | tar -xz -C "${HOME}"
-
- # in-place installation
- if [[ -f "${DEST}/setup.py" ]]; then
- pip install -e "${DEST}"
- fi
-}
-
-_install_coverage () {
- PIP+=(coverage pytest-cov)
- PY_PATH+=("./.travis")
-}
-
-_install_rose () {
- pip install -e .
- PY_PATH+=("./metomi")
- RC_PATH+=("./bin")
-}
-
-_install_cylc () {
- APT+=(at)
-}
-
-_install_fcm () {
- APT+=(subversion build-essential gfortran libxml-parser-perl \
- libconfig-inifiles-perl libdbi-perl libdbd-sqlite3-perl)
- GIT+=('metomi|fcm|master')
- RC_PATH+=("${HOME}/fcm-master/bin")
- WANDISCO=true
-}
-
-_install_pytest () {
- # pytest and its extensions
- PIP+=(pytest)
-}
-
-_install_rosie () {
- PIP+=(requests tornado sqlalchemy)
- RI_PATH+=("$(_path_for python)" "$(_path_for rose)")
-}
-
-_install_sphinx () {
- # sphinx documentation and its extensions
- APT+=(latexmk texlive texlive-generic-extra texlive-latex-extra \
- texlive-fonts-recommended graphviz)
- pip install -e .[docs]
-}
-
-_install_tut_suite () {
- # cylc tutorial suite
- PIP+=(pillow)
-}
-
-_join () {
- local IFS="$1";
- shift;
- echo "$*";
-}
-
-_install_linters () {
- APT+=(shellcheck)
- PIP+=(pycodestyle)
- NPM+=(eslint@6)
-}
-
-_path_for () {
- COMMAND="$1"
- dirname "$(command -v "${COMMAND}")"
-}
-
-_test_units () {
- pytest --cov-append metomi/rose/tests/*
-}
-
-_test_style () {
- pycodestyle
- eslint .
- "$(dirname "$0")/shellchecker"
-}
-
-_test_battery () {
- cp "./.travis/sitecustomize.py" ./lib/python
- coverage run .travis/cover.py
-}
-
-_test_docs () {
- etc/bin/rose-make-docs --strict clean linkcheck doctest
-}
-
-_wandisco_configure () { # extract Wandisco stuff
- # shellcheck disable=SC1004,SC2016
- sudo sh -c 'echo "deb http://opensource.wandisco.com/ubuntu \
- `lsb_release -cs` svn19" >> /etc/apt/sources.list.d/subversion19.list'
- sudo wget -q http://opensource.wandisco.com/wandisco-debian.gpg -O- | \
- sudo apt-key add -
-}
-
-build () {
- for arg in "$@"; do
- "_build_${arg}"
- done
-}
-
- # shellcheck disable=SC2032
-install () {
- for arg in "$@"; do
- "_install_${arg}"
- done
-
- if ${WANDISCO}; then
- _wandisco_configure
- fi
-
- if [[ ${#PIP[@]} -gt 0 ]]; then
- pip install "${PIP[@]}" &
- fi
-
- if [[ ${#NPM[@]} -gt 0 ]]; then
- npm install -g "${NPM[@]}" &
- fi
-
- if [[ ${#APT[@]} -gt 0 ]]; then
- sudo apt-get update
- # shellcheck disable=SC155,2033
- sudo apt-get install -y "${APT[@]}"
- fi
-
- if [[ ${#GIT[@]} -gt 0 ]]; then
- # wrapping the for loop to avoid unbound variable "GIT[@]" error
- for gh_project in "${GIT[@]}"; do
- _gh_extract "${gh_project}"
- done
- fi
-
- wait
-
- # .bashrc
- cat >"${HOME}/.bashrc" \
- <<<"export PATH=\"$(_join ':' "${RC_PATH[@]}"):\$PATH\";"
- cat >>"${HOME}/.bashrc" \
- <<<"export PYTHONPATH=\"$(_join ':' "${PY_PATH[@]}"):\$PYTHONPATH\";"
-
- # rose_init_site
- cat >"./lib/bash/rose_init_site" \
- <<<"
- if [[ -z \${HOME+x} ]]; then
- export PATH=\"$(_join ':' "${RI_PATH[@]}"):\${PATH:-}\";
- fi
- "
-}
-
-_report_coverage () {
- coverage combine --append
- coverage xml --ignore-errors
- bash <(curl -s https://codecov.io/bash)
-}
-
-_report_error() {
- # don't bail out on error
- set +eu
-
- printenv PATH PYTHONPATH
- rose check-software
- cat /tmp/sphinx-err* >&2 # sphinx traceback
-}
-
-report () {
- for arg in "$@"; do
- "_report_${arg}"
- done
-}
-
-test () {
- PS1='$' . "${HOME}/.bashrc"
- export COVERAGE_PROCESS_START="./.coveragerc"
- for arg in "$@"; do
- "_test_${arg}"
- done
-}
-
-# do this here so we only trace the commands we are interested in
-set -o xtrace
-
-# run the specified function
-"$@"
diff --git a/ACKNOWLEDGEMENT.md b/ACKNOWLEDGEMENT.md
index bc7458284b..824c733bbf 100644
--- a/ACKNOWLEDGEMENT.md
+++ b/ACKNOWLEDGEMENT.md
@@ -3,69 +3,36 @@
Licences for non-Rose works included in this distribution can be
found in the licences/ directory.
-doc/rose-icon.png,
etc/images/rose-icon.png,
etc/images/rose-icon.svg,
etc/images/rose-icon-trim.png,
etc/images/rose-icon-trim.svg,
etc/images/rose-logo.png,
-etc/images/rose-splash-logo.png,
etc/images/rosie-icon.png,
etc/images/rosie-icon.svg,
etc/images/rosie-icon-trim.png,
etc/images/rosie-icon-trim.svg,
-etc/metadata/all/etc/images/icon.png:
+metomi/rose/etc/rose-all/etc/images/icon.png,
+metomi/rose/etc/rose-meta/rose-all/etc/images/icon.png
* These icons are all derived from the public domain image at
.
-etc/images/rose-config-edit/gnome_add.png,
-etc/images/rose-config-edit/gnome_package_system.png:
-* These icons are part of the GNOME icon theme 2.28.0, licensed under
- GPLv2. This theme was developed by:
- * Ulisse Perusin
- * Riccardo Buzzotta
- * Josef VybÃral
- * Hylke Bons
- * Ricardo González
- * Lapo Calamandrei
- * Rodney Dawes
- * Luca Ferretti
- * Tuomas Kuosmanen
- * Andreas Nilsson
- * Jakub Steiner
-
-etc/images/rose-config-edit/gnome_add_???,
-etc/images/rose-config-edit/gnome_package_system_???:
-* These icons are derivative works produced from
- * etc/images/rose-config-edit/gnome_add.png or
- * etc/images/rose-config-edit/gnome_package_system.png, and are
- distributed in their preferred PNG form.
-
-lib/html/static/css/bootstrap-???,
-lib/html/static/images/glyphicons-halflings-???,
-lib/html/static/js/bootstrap.min.js:
+metomi/rosie/lib/html/static/css/bootstrap.min.css
+metomi/rosie/lib/html/static/js/bootstrap.min.js
+metomi/rosie/lib/html/static/fonts/glyphicons-halflings*,
* Unmodified external software library copyright 2013 Twitter Inc
released under the Apache 2.0 license.
See .
-lib/html/static/css/rose-bush.css
-* Contains modified code from Twitter Bootstrap v2.3.1 released
- under the Apache 2.0 licence.
- See .
-
-lib/html/static/css/jquery.dataTables.???:
-lib/html/static/images/sort_???:
-lib/html/static/js/jquery.dataTables.???:
+metomi/rosie/lib/html/static/css/jquery.dataTables.css
+metomi/rosie/lib/html/static/images/sort_*
* Unmodified external software library released under GPLv2 and BSD.
See .
-lib/html/static/js/livestamp.min.js:
+metomi/rosie/lib/html/static/js/livestamp.min.js
* Unmodified external software library released under the MIT license.
See .
-lib/html/static/js/moment.min.js:
+metomi/rosie/lib/html/static/js/moment.min.js
* Unmodified external software library released under the MIT license.
See
-
-lib/python/rose/tests/test_ancils/unicode.txt:
-* Markus Kuhn - 2015-08-28 - CC BY 4.0
diff --git a/CHANGES.md b/CHANGES.md
index f5a0e0fd2d..5b02a0532f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -22,6 +22,8 @@ It is able to run most existing Rose Suites and has been ported to Python3.
**This is the first Python3 version of Rose**
+[#2446](https://github.com/metomi/rose/pull/2446): Host select: change implementation to psutil for portability.
+
[#2288](https://github.com/metomi/rose/pull/2288):
Rosie & Rosa: migrate to Python 3(.6-.7) & Tornado
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000000..294cd4596f
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+recursive-include metomi/rose/etc/ *
+recursive-include metomi/rosie/lib/ *
diff --git a/bin/rose b/bin/rose
index 7c9b1bd028..6b766356eb 100755
--- a/bin/rose
+++ b/bin/rose
@@ -28,6 +28,7 @@
# rose help # Print help and list available utilities
# rose help UTIL ... # Print help for UTIL ...
# rose version # Print version information
+# rose version --long # Print more version information
#
# DESCRIPTION
# Simple launcher for utilities in the "rose", "rosie" or "rosa"
@@ -37,10 +38,55 @@
if ${ROSE_DEBUG:-false}; then
set -x
fi
-# shellcheck source=lib/bash/rose_init
-# shellcheck source=lib/bash/rose_usage
-. rose_init
-rose_init
+set -eu
+
+ROSE_NS=$(basename "$0")
+ROSE_HOME_BIN="$(dirname "$0")"
+ROSE_VERSION="$(python3 -c "import metomi.rose; print(metomi.rose.__version__)")"
+export ROSE_NS ROSE_HOME_BIN ROSE_VERSION
+
+# NOTE: cannot use associative array due to bash version requirement
+DEAD_ENDS=(
+ rose-config-edit
+ rose-edit
+ rose-suite-clean
+ rose-suite-cmp-vc
+ rose-suite-gcontrol
+ rose-sgc
+ rose-suite-hook
+ rose-task-hook
+ rose-suite-log-view
+ rose-suite-log
+ rose-slv
+ rose-suite-restart
+ rose-suite-run
+ rose-suite-init
+ rose-suite-scan
+ rose-suite-shutdown
+ rose-suite-stop
+)
+
+# NOTE: indicies must match DEAD_ENDS array
+# (be very careful not to jumble them)
+DEAD_END_MSGS=(
+ 'The Rose configuration editor has been removed, use the Cylc GUI.'
+ 'The Rose configuration editor has been removed, use the Cylc GUI.'
+ 'This command has been replaced by: "cylc clean".'
+ 'This command is awaiting re-implementation in Cylc8'
+ 'This command has been removed: use the Cylc GUI.'
+ 'This command has been removed: use the Cylc GUI.'
+ 'Command obsolete, use Cylc event handlers'
+ 'Command obsolete, use Cylc event handlers'
+ 'This command has been removed: use the Cylc GUI.'
+ 'This command has been removed: use the Cylc GUI'
+ 'This command has been removed: use the Cylc GUI'
+ 'This command has been replaced by: "cylc restart".'
+ 'This command has been replaced by: "cylc install".'
+ 'This command has been replaced by: "cylc install".'
+ 'This command has been replaced by: "cylc scan".'
+ 'This command has been replaced by: "cylc stop".'
+ 'This command has been replaced by: "cylc stop".'
+)
# Print actual command of a command alias
get_alias() {
@@ -49,6 +95,7 @@ get_alias() {
"$ROSE_HOME_BIN/$ROSE_NS-$NAME"
}
+
# Print help for a given utility
help_util() {
local NAME=$1
@@ -102,7 +149,13 @@ path_lead() {
# Print Rose version
function print_version() {
- echo "Rose $ROSE_VERSION ($ROSE_HOME_BIN)"
+ for arg in "$@"; do
+ if [[ "$arg" == '--long' ]]; then
+ echo "Rose $ROSE_VERSION ($ROSE_HOME_BIN)"
+ return
+ fi
+ done
+ echo "$ROSE_VERSION"
}
#-------------------------------------------------------------------------------
@@ -118,6 +171,8 @@ help|h|?|--help|-h)
if (($# == 0)); then
{
print_version
+ # shellcheck source=metomi/rose/etc/lib/bash/rose_usage
+ . "$(rose resource lib/bash/rose_usage)"
rose_usage
echo
echo "$ROSE_NS provides the following utilities:"
@@ -129,8 +184,16 @@ help|h|?|--help|-h)
echo " (=$ALIAS)"
else
echo " $NAME"
- sed '1,/^# DESCRIPTION$/d;{s/^# / /;q;}' \
- "$ROSE_HOME_BIN/$U"
+ if [[ $U == 'rose-date' ]]; then
+ # hack an exception for this lone Python command
+ # shellcheck disable=SC2026
+ echo ' '\
+ 'Parse and print 1. a date time point '\
+ 'or 2. a duration.'
+ else
+ sed '1,/^# DESCRIPTION$/d;{s/^# / /;q;}' \
+ "$ROSE_HOME_BIN/$U"
+ fi
fi
done
} | ${PAGER:-less}
@@ -146,7 +209,7 @@ help|h|?|--help|-h)
exit $RC
:;;
version|--version|-V)
- print_version
+ print_version "$@"
exit
:;;
doc)
@@ -176,6 +239,14 @@ doc)
:;;
esac
+CMD="${ROSE_NS}-${UTIL}"
+for i in $(seq 0 $(( ${#DEAD_ENDS[@]} - 1 ))); do
+ if [[ "${DEAD_ENDS[$i]}" == "$CMD" ]]; then
+ echo "${DEAD_END_MSGS[$i]}" >&2
+ exit 42
+ fi
+done
+
COMMAND="$(dirname "$0")/$ROSE_NS-$UTIL"
if [[ ! -f $COMMAND || ! -x $COMMAND ]]; then
echo "$ROSE_NS: $UTIL: unknown utility. Abort." >&2
@@ -188,4 +259,5 @@ if (($# > 0)) && [[ $1 == '--help' || $1 == '-h' ]]; then
fi
ROSE_UTIL=$UTIL
export ROSE_UTIL
+
exec "$COMMAND" "$@"
diff --git a/bin/rose-config-edit b/bin/rose-config-edit
deleted file mode 100755
index 8ffe233cca..0000000000
--- a/bin/rose-config-edit
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env bash
-#-----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-----------------------------------------------------------------------------
-# NAME
-# rose config-edit
-#
-# SYNOPSIS
-# rose config-edit [OPTIONS]... [PAGE_PATH]...
-#
-# DESCRIPTION
-# Launch the GTK+ GUI to edit a suite or application configuration.
-#
-# If a suite contains more than 10 applications then they will only be
-# loaded after startup, on demand.
-#
-# OPTIONS
-# --config=DIR, -C DIR
-# A path to either:
-#
-# 1. a directory containing a suite with a file named
-# `suite.rc` and a directory called `app` containing
-# subdirectories with files named `rose-app.conf`,
-# in the format specified in the Rose pages.
-# 2. a directory containing a single 'application' - a file named
-# `rose-app.conf` and an optional subdirectory called `file`
-# with other application files.
-#
-# --load-all-apps
-# Force loading of all applications on startup.
-# --load-no-apps
-# Load applications in the suite on demand.
-# --meta-path=PATH, -M PATH
-# Prepend `PATH` to the search path for metadata (look here first).
-# This option can be used repeatedly to load multiple paths.
-# --new
-# Launch, ignoring any configuration.
-# --no-metadata
-# Launch with metadata switched off.
-# --no-warn=WARNING-TYPE
-# Suppress warnings of the provided type. `WARNING-TYPE` may be:
-#
-# `version`
-# Suppress "Could not find metadata for app/version, using app/HEAD"
-# warnings.
-#
-# ARGUMENTS
-# PAGE_PATH
-# One or more paths to open on load, pages may be full or partial
-# namespaces e.g. `foo/bar/env` or `env`.
-#
-# NOTE: Opens the shortest namespace that matches the provided string.
-#
-# ENVIRONMENT VARIABLES
-# optional ROSE_META_PATH
-# Prepend `$ROSE_META_PATH` to the metadata search path.
-#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.config_editor.main "$@"
diff --git a/bin/rose-date b/bin/rose-date
index 83f75ab438..616f5ef394 100755
--- a/bin/rose-date
+++ b/bin/rose-date
@@ -182,6 +182,11 @@ from metomi.isodatetime.main import main as iso_main
def main():
"""Implement rose date."""
+ print(
+ 'WARNING: "rose date" is depreacted, use the "isodatetime" command.',
+ file=sys.stderr
+ )
+
# Handle Legacy Rose-date -c functionality
if '-c' in sys.argv or '--use-task-cycle-time' in sys.argv:
if os.getenv('ROSE_TASK_CYCLE_TIME'):
diff --git a/bin/rose-edit b/bin/rose-edit
deleted file mode 100755
index 80ec62fb6b..0000000000
--- a/bin/rose-edit
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# Alias of "rose config-edit".
-#-------------------------------------------------------------------------------
-exec "$(dirname "$0")/rose-config-edit" "$@"
diff --git a/bin/rose-host-select b/bin/rose-host-select
index e3b7b6db56..d3a342d5cc 100755
--- a/bin/rose-host-select
+++ b/bin/rose-host-select
@@ -27,9 +27,6 @@
# Select a host from a set of groups or names by load, by free memory
# or by random.
#
-# Use settings in `$ROSE_HOME/etc/rose.conf` and `$HOME/.metomi/rose.conf`
-# to determine the ranking method.
-#
# Print the selected host name.
#
# OPTIONS
@@ -75,8 +72,9 @@
#
# CONFIGURATION
# The command reads its settings from the `[rose-host-select]` section in
-# `$ROSE_HOME/etc/rose.conf` and `$HOME/.metomi/rose.conf`. All settings
-# are optional. Type `rose config rose-host-select` to print settings.
+# the Rose configuration. All settings are optional. Type
+# `rose config rose-host-select` to print settings.
+#
# Valid settings are:
#
# default = GROUP/HOST ...
diff --git a/bin/rose-host-select-client b/bin/rose-host-select-client
new file mode 100644
index 0000000000..c5f97eade0
--- /dev/null
+++ b/bin/rose-host-select-client
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+#-------------------------------------------------------------------------------
+# Copyright (C) 2012-2019 British Crown (Met Office) & Contributors.
+#
+# This file is part of Rose, a framework for meteorological suites.
+#
+# Rose 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.
+#
+# Rose 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 Rose. If not, see .
+#-------------------------------------------------------------------------------
+# NAME
+# rose host-select-client
+#
+# SYNOPSIS
+# rose host-select-client
+#
+# DESCRIPTION
+# Internal command for obtaining information about hosts using the
+# Python psutil module.
+#
+# The names of psutil methods are passed to stdin along with any arguments
+# as a JSON list.
+#
+# Examples:
+# # return the virtual memory and cpu usage
+# [["virual_memory"], ["cpu_percent"]]
+#
+# # return disk usage for the filesystem mounted at "/"
+# [["disk_usage", "/"]]
+#
+# The input json should be preceded by \n**start**\n
+# and followed by \n**end**\n to avoid issues with user profile scripts
+# and stdin deadlock.
+#
+# $ rose host-select-client <<'__HERE__'
+# > **start**
+# > [["virtual_memory"]]
+# > **end**
+# > __HERE__
+# [{"total": 17179869184, "available": 6276612096, "percent": 63.5, ...}]
+#
+#-------------------------------------------------------------------------------
+exec python3 -m metomi.rose.host_select_client "$@"
diff --git a/bin/rose-metadata-graph b/bin/rose-metadata-graph
index 2d4bd83421..bb703f8cfc 100755
--- a/bin/rose-metadata-graph
+++ b/bin/rose-metadata-graph
@@ -50,4 +50,5 @@
# optional ROSE_META_PATH
# Prepend `$ROSE_META_PATH` to the metadata search path.
#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.metadata_graph "$@"
+echo 'This command has been removed pending re-implementation' >&2
+exit 1
diff --git a/bin/rose-mpi-launch b/bin/rose-mpi-launch
index ad534df38b..d035d29dfa 100755
--- a/bin/rose-mpi-launch
+++ b/bin/rose-mpi-launch
@@ -54,7 +54,8 @@
#
# CONFIGURATION
# The command reads from the `[rose-mpi-launch]` section in
-# `$ROSE_HOME/etc/rose.conf` and `$HOME/.metomi/rose.conf`.
+# the Rose configuration.
+#
# Valid settings are:
#
# launcher-list=LIST
@@ -111,9 +112,13 @@
# DIAGNOSTICS
# Return 0 on success, 1 or exit code of the launcher program on failure.
#-------------------------------------------------------------------------------
-# shellcheck source=lib/bash/rose_init
-. rose_init
-rose_init rose_log
+set -eu
+# shellcheck source=metomi/rose/etc/lib/bash/rose_log
+. "$(rose resource lib/bash/rose_log)"
+# shellcheck source=metomi/rose/etc/lib/bash/rose_usage
+. "$(rose resource lib/bash/rose_usage)"
+
+ROSE_CMD="${ROSE_NS}-${ROSE_UTIL}"
# ------------------------------------------------------------------------------
ROSE_COMMAND=
@@ -200,7 +205,7 @@ else
fi
ROSE_LAUNCHER_LIST=${ROSE_LAUNCHER_LIST:-$( \
- rose config --default= "$ROSE_NS" launcher-list)}
+ rose config --default= "$ROSE_CMD" launcher-list)}
export NPROC=${NPROC:-1}
#-------------------------------------------------------------------------------
@@ -244,7 +249,7 @@ if [[ -n $ROSE_COMMAND_FILE ]]; then
fi
ROSE_LAUNCHER_FILEOPTS=${ROSE_LAUNCHER_FILEOPTS:-$( \
rose config --default= \
- "$ROSE_NS" "launcher-fileopts.$ROSE_LAUNCHER_BASE")}
+ "$ROSE_CMD" "launcher-fileopts.$ROSE_LAUNCHER_BASE")}
eval "info 2 exec $ROSE_LAUNCHER $ROSE_LAUNCHER_FILEOPTS $*"
eval "exec $ROSE_LAUNCHER $ROSE_LAUNCHER_FILEOPTS $*"
else
@@ -255,9 +260,9 @@ else
fi
# Options
ROSE_LAUNCHER_PREOPTS=${ROSE_LAUNCHER_PREOPTS:-$(rose config -E \
- --default= "$ROSE_NS" "launcher-preopts.$ROSE_LAUNCHER_BASE")}
+ --default= "$ROSE_CMD" "launcher-preopts.$ROSE_LAUNCHER_BASE")}
ROSE_LAUNCHER_POSTOPTS=${ROSE_LAUNCHER_POSTOPTS:-$(rose config -E \
- --default= "$ROSE_NS" "launcher-postopts.$ROSE_LAUNCHER_BASE")}
+ --default= "$ROSE_CMD" "launcher-postopts.$ROSE_LAUNCHER_BASE")}
else
ROSE_LAUNCH_INNER=
ROSE_LAUNCHER_PREOPTS=
diff --git a/bin/rose-suite-scan b/bin/rose-resource
similarity index 67%
rename from bin/rose-suite-scan
rename to bin/rose-resource
index fdaba4d0e4..ac1be09419 100755
--- a/bin/rose-suite-scan
+++ b/bin/rose-resource
@@ -18,12 +18,23 @@
# along with Rose. If not, see .
#-------------------------------------------------------------------------------
# NAME
-# rose suite-scan
+# rose resource
#
# SYNOPSIS
-# rose suite-scan [...]
+# rose resource RESOURCE_PATH
#
# DESCRIPTION
-# Run `cylc scan [...]`.
+# Display the path of resources in the Rose Python installation.
+#
+# * If the requested resource exists and is a file its path is printed.
+# * If the requested resource exists and is a directory it is listed.
+#
+# Provide no arguments to see a list of top-level resources.
+#
+# OPTIONS
+# --quiet, -q
+# Decrement verbosity.
+# --verbose, -v
+# Increment verbosity.
#-------------------------------------------------------------------------------
-exec cylc scan "$@"
+exec python3 -m metomi.rose.resource "$@"
diff --git a/bin/rose-slv b/bin/rose-slv
deleted file mode 100755
index 847b85b0a9..0000000000
--- a/bin/rose-slv
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# Alias of "rose suite-log".
-#-------------------------------------------------------------------------------
-exec "$(dirname "$0")/rose-suite-log" "$@"
diff --git a/bin/rose-stem b/bin/rose-stem
index c88d5a76cb..1059dbdf85 100755
--- a/bin/rose-stem
+++ b/bin/rose-stem
@@ -82,4 +82,5 @@
# is intended to specify the revision of `fcm-make` config files.
#
#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.stem "$@"
+echo 'Awaiting re-implementation in cylc-rose.' >&2
+exit 1
diff --git a/bin/rose-suite-clean b/bin/rose-suite-clean
deleted file mode 100755
index d310e066e9..0000000000
--- a/bin/rose-suite-clean
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# NAME
-# rose suite-clean
-#
-# SYNOPSIS
-# rose suite-clean [OPTIONS] [SUITE-NAME [...]]
-#
-# DESCRIPTION
-# Remove items created by "rose suite-run".
-#
-# If no argument is specified, use the base-name of `$PWD` as suite name.
-#
-# Correctly remove runtime directories created by `rose suite-run`
-# including those on remote job hosts.
-#
-# OPTIONS
-# --name=NAME, -n NAME
-# Append `NAME` to the argument list.
-# --non-interactive, --yes, -y
-# Switch off interactive prompting (=answer yes to everything)
-# --only=ITEM
-# If one or more `--only=ITEM` option is specified, only files and/or
-# directories matching an `ITEM` will be removed. An `ITEM` should be a
-# glob pattern (bash extglob) for matching a relative path in the run
-# directory of the suite(s).
-# --quiet, -q
-# Decrement verbosity.
-# --verbose, -v
-# Increment verbosity.
-#
-# DIAGNOSTICS
-# Return the difference between the number of arguments and number of
-# successfully cleaned suites, i.e. 0 if all successful.
-#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.suite_clean "$@"
diff --git a/bin/rose-suite-cmp-vc b/bin/rose-suite-cmp-vc
deleted file mode 100755
index 277ac9cc87..0000000000
--- a/bin/rose-suite-cmp-vc
+++ /dev/null
@@ -1,53 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# NAME
-# rose suite-cmp-vc
-#
-# SYNOPSIS
-# rose suite-cmp-vc NAME
-# rose suite-cmp-vc --name=NAME
-#
-# # If CYLC_SUITE_NAME is exported, compare source info of the current
-# # suite from within a suite task if no name is specified.
-# rose suite-cmp-vc
-#
-# DESCRIPTION
-# Compare VCS information of a suite source between installation and now.
-#
-# Version control system information of a suite is installed under
-# log/rose-suite-run.version file. This command attempts to regenerate the
-# information from the recorded source, and compare the original file with
-# the latest information.
-#
-# Return 0 if no difference. Print unified diff and return 1 if difference
-# found. Return 2 on other errors.
-#
-# ARGUMENTS
-# Specify the suite NAME.
-#
-# OPTIONS
-# --name=NAME, -n NAME
-# Specify the suite NAME.
-# --quiet, -q
-# Decrement verbosity.
-# --verbose, -v
-# Increment verbosity.
-#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.cmp_source_vc "$@"
diff --git a/bin/rose-suite-hook b/bin/rose-suite-hook
deleted file mode 100755
index cb1e5b8c74..0000000000
--- a/bin/rose-suite-hook
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# NAME
-# rose suite-hook
-#
-# SYNOPSIS
-# # Cylc interface
-# rose suite-hook [OPTIONS] EVENT SUITE MSG
-# rose task-hook [OPTIONS] EVENT SUITE TASK_ID MSG
-#
-# DESCRIPTION
-# Deprecated. Use cylc built-in event handlers instead.
-#
-# Provide a common event hook for cylc suites and tasks.
-#
-# * (Task event only) Retrieve remote task logs back to server host.
-# * Email user if `--mail` specified.
-# * Shutdown suite if `--shutdown` specified.
-#
-# OPTIONS
-# --debug
-# Switch on debug mode.
-# --mail-cc=LIST
-# Only useful if the `--mail` option is specified. Specify a
-# comma separated list of additional addresses to email.
-# --mail
-# Trigger an email notification to the user.
-# --retrieve-job-logs
-# Retrieve remote task job logs.
-# --shutdown
-# Trigger a shutdown of the suite.
-# --quiet, -q
-# Decrement verbosity.
-# --verbose, -v
-# Increment verbosity.
-#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.suite_hook "$@"
diff --git a/bin/rose-suite-init b/bin/rose-suite-init
deleted file mode 100755
index 0ee6bc98d0..0000000000
--- a/bin/rose-suite-init
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# Deprecated.
-# Alias of "rose suite-run".
-#-------------------------------------------------------------------------------
-exec "$(dirname "$0")/rose-suite-run" "$@"
diff --git a/bin/rose-suite-log b/bin/rose-suite-log
deleted file mode 100755
index f27e50b44e..0000000000
--- a/bin/rose-suite-log
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# NAME
-# rose suite-log
-#
-# SYNOPSIS
-# 1. rose suite-log [--view]
-# 2. rose suite-log --update [ITEM ...]
-# rose suite-log --update '*' # all task jobs
-# 3. rose suite-log --archive CYCLE ...
-# rose suite-log --archive '*' # all cycles
-#
-# DESCRIPTION
-# View or update suite log.
-#
-# 1. Launch web browser to view suite log. If "rose bush" is not
-# configured, the command will offer to start it.
-# 2. Pull back task job logs from any remote hosts for specified cycle
-# times or task names or IDs.
-# 3. Archive (tar-gzip) job logs at the specified cycle time.
-#
-# If `--name=SUITE-NAME` is not specified, the name will be determined by
-# locating a `rose-suite.conf` file in `$PWD` or its nearest parent
-# directories. In a normal suite, the basename of the (nearest parent)
-# directory containing the `rose-suite.conf` file is assumed to be the
-# suite name. In a project containing a rose stem suite, the basename of
-# the (nearest parent) directory containing the `rose-stem/rose-suite.conf`
-# file is assumed to be the suite name.
-#
-# OPTIONS
-# --archive
-# Archive (tar-gzip) job logs at specified cycle times. Implies
-# `--update`.
-# --force, -f
-# Same as `rose suite-log --update '*'`.
-# --name=SUITE-NAME, -n SUITE-NAME
-# Specify the suite name, instead of using basename of `$PWD`.
-# --prune-remote
-# If specified, remove job logs from remote hosts after pulling them to
-# suite host.
-# --tidy-remote
-# Deprecated. Use `--prune-remote` instead.
-# --update, -U
-# Update job logs for items specified in arguments.
-# --user=USER-NAME, -u USER-NAME
-# View mode only. View logs of a suite of a different user.
-# --view
-# Launch web browser to view suite log.
-#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.suite_log "$@"
diff --git a/bin/rose-suite-log-view b/bin/rose-suite-log-view
deleted file mode 100755
index 847b85b0a9..0000000000
--- a/bin/rose-suite-log-view
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# Alias of "rose suite-log".
-#-------------------------------------------------------------------------------
-exec "$(dirname "$0")/rose-suite-log" "$@"
diff --git a/bin/rose-suite-restart b/bin/rose-suite-restart
deleted file mode 100755
index b05d670570..0000000000
--- a/bin/rose-suite-restart
+++ /dev/null
@@ -1,58 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# NAME
-# rose suite-restart
-#
-# SYNOPSIS
-# rose suite-restart [OPTIONS] [[--] CYLC-RESTART-ARGS]
-#
-# # Restart cylc suite with name equal to basename of $PWD.
-# rose suite-restart
-#
-# # Restart cylc suite NAME
-# rose suite-restart --name=NAME
-#
-# # Restart cylc suite NAME with a given state dump
-# rose suite-restart --name=NAME -- state.20141118T161121.195326Z
-#
-# # Restart cylc suite NAME on given host
-# rose suite-restart --name=NAME --host=my-suite-host
-#
-# DESCRIPTION
-# Restart a shutdown suite from its last known state without reinstalling
-# it.
-#
-# If re-installation is required, use `rose suite-run --restart`.
-#
-# ARGUMENTS
-# Arguments (and options after `--`) are passed to `cylc restart`.
-#
-# OPTIONS
-# --host=HOST
-# Specify a host for restarting the suite.
-# --name=NAME, -n NAME
-# Specify the suite `NAME` in cylc, instead of using the basename of
-# the current working directory.
-# --quiet, -q
-# Decrement verbosity.
-# --verbose, -v
-# Increment verbosity.
-#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.suite_restart "$@"
diff --git a/bin/rose-suite-run b/bin/rose-suite-run
deleted file mode 100755
index 47b5175904..0000000000
--- a/bin/rose-suite-run
+++ /dev/null
@@ -1,132 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# NAME
-# rose suite-run
-#
-# SYNOPSIS
-# rose suite-run [OPTIONS] [[--] CYLC-RUN-ARGS]
-#
-# # Install and run a Cylc suite in $PWD.
-# rose suite-run
-#
-# # As above, but start the suite in simulation mode.
-# rose suite-run -- --mode=simulation
-#
-# # Install and run the suite in $PWD, and register it as "my.suite".
-# rose suite-run -n my.suite
-#
-# # Install and run suite in "/dir/to/my.suite".
-# # Equivalent to (cd /dir/to/my.suite && rose suite-run).
-# rose suite-run -C /dir/to/my.suite
-#
-# DESCRIPTION
-# Install and run a Cylc suite.
-#
-# Install a suite (in `$PWD`), register it in Cylc using the basename of
-# the configuration directory or the option specified in `--name=NAME`.
-# Invoke `cylc run` on it. Arguments (and options after `--`) are passed
-# to `cylc run`.
-#
-# OPTIONS
-# --config=DIR, -C DIR
-# Specify the configuration directory of the suite. (default=`$PWD`)
-# --define=[SECTION]KEY=VALUE, -D [SECTION]KEY=VALUE
-# Each of these overrides the `[SECTION]KEY` setting with a given
-# `VALUE`. Can be used to disable a setting using the syntax
-# `--define=[SECTION]!KEY` or even `--define=[!SECTION]`.
-# See also `--define-suite`.
-# --define-suite=KEY=VALUE, -S KEY=VALUE
-# As `--define`, but with an implicit `[SECTION]` for suite variables.
-# --host=HOST
-# Specify a host for running the suite.
-# --install-only, -i
-# Install the suite. Do not run it.
-# --local-install-only, -l
-# Install the suite locally. Do not install to job hosts. Do not run
-# it.
-# --validate-suite-only
-# Install the suite in a temporary location. Do not setup any directories.
-# Do not run any jobs. Overrides --install-only and
-# --local-install-only.
-# --log-keep=DAYS
-# Specify the number of days to keep log directories/archives.
-# Do not housekeep if not specified. Named log directories (created by
-# `--log-name=NAME` in previous runs) will not be housekept.
-# --log-name=NAME
-# Specify a name for the log directory of the current run. If
-# specified, it will create a symbolic link `log.NAME` to point to the
-# log directory of the current run. Named log directories will not be
-# automatically archived or housekept. Only works with `--run=run`.
-# --name=NAME, -n NAME
-# Specify the suite `NAME` in Cylc, instead of using the basename of
-# the configuration directory.
-# --new, -N
-# (Re-)create suite runtime locations. This option is equivalent to
-# running `rose suite-clean -y && rose suite-run` and will remove
-# previous runs. Users may want to take extra care when using this
-# option.
-# --no-log-archive
-# Do not archive (tar-gzip) old log directories.
-# --no-overwrite
-# Do not overwrite existing files.
-# --no-strict
-# Do not validate (suite engine configuration) in strict mode.
-# --opt-conf-key=KEY, -O KEY, --opt-conf-key='(KEY)', -O '(KEY)'
-# Each of these switches on an optional configuration identified by
-# `KEY`. The configurations are applied first-to-last. The `(KEY)`
-# syntax denotes an optional configuration that can be missing.
-# Otherwise, the optional configuration must exist.
-# --quiet, -q
-# Decrement verbosity.
-# --reload
-# Shorthand for `--run=reload`.
-# --restart
-# Shorthand for `--run=restart`.
-# --run=reload|restart|run
-# Invoke `cylc reload|restart|run` according to this option.
-# (default=`run`)
-# --verbose, -v
-# Increment verbosity.
-#
-# ENVIRONMENT VARIABLES
-# `rose suite-run` provides the following environment variables to the suite.
-#
-# ROSE_ORIG_HOST
-# The name of the host where the `rose suite-run` command was invoked.
-# optional ROSE_SUITE_OPT_CONF_KEYS
-# Each `KEY` in this space delimited list switches on an optional
-# configuration. The configurations are applied first-to-last.
-#
-# SUITE VARIABLES
-# `rose suite-run` provides the following variables to the suite as
-# template variables (i.e. Jinja2 or EmPy).
-#
-# ROSE_ORIG_HOST
-# The name of the host where the `rose suite-run` command was invoked.
-# ROSE_SUITE_VARIABLES
-# Dictionary containing all suite variables inserted via
-# `[jinja2:suite.rc]` or `[empy:suite.rc]` section.
-#
-# SEE ALSO
-# * `cylc help gui`
-# * `cylc help register`
-# * `cylc help run`
-#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.suite_run "$@"
diff --git a/bin/rose-suite-shutdown b/bin/rose-suite-shutdown
deleted file mode 100755
index 4cfd3ca1a1..0000000000
--- a/bin/rose-suite-shutdown
+++ /dev/null
@@ -1,57 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# NAME
-# rose suite-shutdown
-#
-# SYNOPSIS
-# rose suite-shutdown [OPTIONS] [--name=SUITE-NAME] [[--] EXTRA-ARGS ...]
-#
-# DESCRIPTION
-# Shutdown a running suite.
-#
-# If `--name=SUITE-NAME` is not specified, the name will be determined by
-# locating a `rose-suite.conf` file in `$PWD` or its nearest parent
-# directories. In a normal suite, the basename of the (nearest parent)
-# directory containing the `rose-suite.conf` file is assumed to be the
-# suite name. In a project containing a rose stem suite, the basename of
-# the (nearest parent) directory containing the `rose-stem/rose-suite.conf`
-# file is assumed to be the suite name.
-#
-# This wrapper also deals with the use case where a suite may be running on
-# dedicated servers at a site. The wrapper will make an attempt to detect
-# where the suite is running or last run.
-#
-# OPTIONS
-# --all
-# Shutdown all running suites. You will be prompted to confirm shutting
-# down each affected suite.
-# --name=SUITE-NAME
-# Specify the suite name.
-# --non-interactive, --yes, -y
-# Switch off interactive prompting (=answer yes to everything)
-# --quiet, -q
-# Decrement verbosity.
-# --verbose, -v
-# Increment verbosity.
-# -- --EXTRA-ARGS
-# See the cylc documentation, cylc shutdown for options and details on
-# `EXTRA-ARGS`.
-#-------------------------------------------------------------------------------
-exec python3 -m metomi.rose.suite_control shutdown "$@"
diff --git a/bin/rose-suite-stop b/bin/rose-suite-stop
deleted file mode 100755
index 30a76f43ff..0000000000
--- a/bin/rose-suite-stop
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# Alias of "rose suite-shutdown".
-#-------------------------------------------------------------------------------
-exec "$(dirname "$0")/rose-suite-shutdown" "$@"
diff --git a/bin/rose-task-env b/bin/rose-task-env
index 0f6fa472ec..ad53feebfe 100755
--- a/bin/rose-task-env
+++ b/bin/rose-task-env
@@ -136,7 +136,7 @@
#
# USAGE IN SUITES
# rose `task-env` can be used to make environment variables available to a
-# suite by defining its `suite.rc` `env-script` option as
+# suite by defining its `flow.cylc` `env-script` option as
# `env-script = eval $(rose task-env)`.
#
#-------------------------------------------------------------------------------
diff --git a/bin/rose-task-hook b/bin/rose-task-hook
deleted file mode 100755
index 34934cee3e..0000000000
--- a/bin/rose-task-hook
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# Alias of "rose suite-hook".
-#-------------------------------------------------------------------------------
-exec "$(dirname "$0")/rose-suite-hook" "$@"
diff --git a/bin/rose-tutorial b/bin/rose-tutorial
index 7a41346187..fdb8b4b795 100755
--- a/bin/rose-tutorial
+++ b/bin/rose-tutorial
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
#-------------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
#
@@ -30,8 +30,7 @@
# Make a copy of one of the tutorial SUITE in the cylc-run directory.
#
#-------------------------------------------------------------------------------
-# shellcheck source=lib/bash/rose_init
-. rose_init
+set -eu
mkdir -p "$HOME/cylc-run"
usage () {
@@ -86,7 +85,7 @@ fi
# Copy files.
echo -n "Copying tutorial files to ${DIRECTORY} ... "
-rose_init 'rose_log'
+
mkdir -p "$(dirname "${DIRECTORY}")"
if run rsync -rLptgoD "${ROSE_HOME}/etc/tutorial/$1/" "${DIRECTORY}" \
--exclude='.validate'; then
diff --git a/bin/rosie-id b/bin/rosie-id
index d6db8e61fd..3ce8f23827 100755
--- a/bin/rosie-id
+++ b/bin/rosie-id
@@ -29,9 +29,6 @@
# # Print the local location of a given suite ID
# rosie id --to-local-copy mo1-abc45
#
-# # Print the output directory of a given suite ID
-# rosie id --to-output mo1-abc45
-#
# # Print the web URL of a given suite ID
# rosie id --to-web mo1-abc45
#
@@ -63,8 +60,6 @@
# Print the local location of a given suite ID
# --to-origin
# Print the repository URL of a given suite ID
-# --to-output
-# Print the output directory of a given suite ID
# --to-web
# Print the web URL of a given suite ID
# --next
diff --git a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/badenvswitch.py b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/badenvswitch.py
index 59380b8893..b0d2599c38 100644
--- a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/badenvswitch.py
+++ b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/badenvswitch.py
@@ -1,13 +1,11 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
-import rose.macro
+import metomi.rose.macro
-class InvalidValueTransformer(rose.macro.MacroBase):
+class InvalidValueTransformer(metomi.rose.macro.MacroBase):
"""Test class to return an invalid value."""
diff --git a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/envswitch.py b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/envswitch.py
index c144c819a3..3e5bd8605d 100644
--- a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/envswitch.py
+++ b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/envswitch.py
@@ -1,13 +1,11 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
-import rose.macro
+import metomi.rose.macro
-class LogicalTransformer(rose.macro.MacroBase):
+class LogicalTransformer(metomi.rose.macro.MacroBase):
"""Test class to change the value of a boolean environment variable."""
@@ -17,10 +15,10 @@ def transform(self, config, meta_config=None):
"""Perform the transform operation on the env switch."""
if config.get(["env", "TRANSFORM_SWITCH"]) is not None:
value = config.get(["env", "TRANSFORM_SWITCH"]).value
- if value == rose.TYPE_BOOLEAN_VALUE_FALSE:
- new_value = rose.TYPE_BOOLEAN_VALUE_TRUE
+ if value == metomi.rose.TYPE_BOOLEAN_VALUE_FALSE:
+ new_value = metomi.rose.TYPE_BOOLEAN_VALUE_TRUE
else:
- new_value = rose.TYPE_BOOLEAN_VALUE_FALSE
+ new_value = metomi.rose.TYPE_BOOLEAN_VALUE_FALSE
config.set(["env", "TRANSFORM_SWITCH"], new_value)
info = self.WARNING_CHANGED_VALUE.format(value, new_value)
self.add_report("env", "TRANSFORM_SWITCH", value, info)
diff --git a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/nl_add_remove.py b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/nl_add_remove.py
index c62773d19f..3210f17aad 100644
--- a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/nl_add_remove.py
+++ b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/nl_add_remove.py
@@ -1,13 +1,11 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
-import rose.macro
+import metomi.rose.macro
-class NamelistAdderRemover(rose.macro.MacroBase):
+class NamelistAdderRemover(metomi.rose.macro.MacroBase):
"""Test class to add and remove a section."""
diff --git a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/nl_ignore.py b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/nl_ignore.py
index cbc427c900..fe0c075266 100644
--- a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/nl_ignore.py
+++ b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/nl_ignore.py
@@ -1,13 +1,11 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
-import rose.macro
+import metomi.rose.macro
-class NamelistIgnorer(rose.macro.MacroBase):
+class NamelistIgnorer(metomi.rose.macro.MacroBase):
"""Test class to ignore and enable a section."""
@@ -16,15 +14,14 @@ class NamelistIgnorer(rose.macro.MacroBase):
def transform(self, config, meta_config=None):
"""Perform the transform operation on the section."""
- change_list = []
section = "namelist:ignore_nl"
node = config.get([section])
if node is not None:
if node.state:
- node.state = rose.config.ConfigNode.STATE_NORMAL
+ node.state = metomi.rose.config.ConfigNode.STATE_NORMAL
info = self.WARNING_ENABLED.format(section)
else:
- node.state = rose.config.ConfigNode.STATE_USER_IGNORED
+ node.state = metomi.rose.config.ConfigNode.STATE_USER_IGNORED
info = self.WARNING_IGNORED.format(section)
self.add_report(section, None, None, info)
return config, self.reports
diff --git a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/null.py b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/null.py
index fad4c9baf0..7809e61bb2 100644
--- a/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/null.py
+++ b/demo/rose-config-edit/demo_meta/app/04-transform/meta/lib/python/macros/null.py
@@ -1,14 +1,11 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
-import rose.macro
-import rose.variable
+import metomi.rose.macro
-class NullTransformer(rose.macro.MacroBase):
+class NullTransformer(metomi.rose.macro.MacroBase):
"""Class to report changes for missing or null settings."""
diff --git a/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/fib.py b/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/fib.py
index 3482750e15..774525bf37 100644
--- a/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/fib.py
+++ b/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/fib.py
@@ -1,14 +1,12 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
-import rose.macro
-import rose.variable
+import metomi.rose.macro
+import metomi.rose.variable
-class FibonacciChecker(rose.macro.MacroBase):
+class FibonacciChecker(metomi.rose.macro.MacroBase):
"""Class to check if an array matches a Fibonacci sequence."""
@@ -21,14 +19,13 @@ def validate(self, config, meta_config, starts_at_zero=False):
seq = [0, 1]
else:
seq = [1, 1]
- problem_list = []
section = "env"
option = "INVALID_SEQUENCE"
node = config.get([section, option])
if node is None:
return []
value = node.value
- elems = rose.variable.array_split(value)
+ elems = metomi.rose.variable.array_split(value)
if all([w.isdigit() for w in elems]) and len(elems) > 1:
int_elems = [int(w) for w in elems]
if len(int_elems) >= 2 and int_elems[:2] != seq:
diff --git a/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/null.py b/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/null.py
index cc33318b15..d63a78e0fa 100644
--- a/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/null.py
+++ b/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/null.py
@@ -1,14 +1,11 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
-import rose.macro
-import rose.variable
+import metomi.rose.macro
-class NullChecker(rose.macro.MacroBase):
+class NullChecker(metomi.rose.macro.MacroBase):
"""Class to report errors for missing or null settings."""
diff --git a/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/url.py b/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/url.py
index 887c59d9b5..c96349863c 100644
--- a/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/url.py
+++ b/demo/rose-config-edit/demo_meta/app/05-validate/meta/lib/python/macros/url.py
@@ -1,15 +1,13 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
import http.client
-import rose.macro
+import metomi.rose.macro
-class URLChecker(rose.macro.MacroBase):
+class URLChecker(metomi.rose.macro.MacroBase):
"""Class to check if a URL is valid."""
@@ -18,8 +16,6 @@ class URLChecker(rose.macro.MacroBase):
def validate(self, config, meta_config):
"""Validate a string containing a URL."""
self.reports = []
- seq = [1, 1]
- problem_list = []
for section in config.value.keys():
node = config.get([section])
if not isinstance(node.value, dict):
diff --git a/demo/rose-config-edit/demo_meta/suite.rc b/demo/rose-config-edit/demo_meta/flow.cylc
similarity index 100%
rename from demo/rose-config-edit/demo_meta/suite.rc
rename to demo/rose-config-edit/demo_meta/flow.cylc
diff --git a/etc/bin/rose-make-docs b/etc/bin/rose-make-docs
deleted file mode 100755
index 1594ebbacd..0000000000
--- a/etc/bin/rose-make-docs
+++ /dev/null
@@ -1,290 +0,0 @@
-#!/usr/bin/env bash
-#-----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-----------------------------------------------------------------------------
-# NAME
-# rose make-docs
-#
-# SYNOPSIS
-# rose make-docs [OPTIONS] [BUILD...]
-#
-# DESCRIPTION
-# Build the rose documentation in the requested `BUILD` format(s).
-#
-# OPTIONS
-# --venv
-# Build virtualenv for temporarilly installing python dependencies
-# if necessary.
-# --dev
-# Development mode, don't remove virtualenv after build.
-# --strict
-# Disable cache forcing a complete re-build and turn warnings into
-# errors.
-# --debug
-# Run `make` with the --debug option.
-# --default-version=VERSION
-# By default the current version is symlinked as the default version,
-# provide an alternative version to override this.
-#
-# BUILD
-# The format(s) to build the documentation in - default html.
-# Avaliable formats are listed in the sphinx documentation
-# (http://www.sphinx-doc.org/en/stable/builders.html).
-# The most commonly used formats are:
-#
-# `html`
-# For building standalone HTML files.
-# `singlehtml`
-# For building a single page HTML document.
-# `latexpdf`
-# For building PDF documentation.
-# `clean`
-# Removes all built documentation for the current rose version.
-# (use `rm -rf doc` to remove all documentation).
-#
-# DEVELOPMENT BUILDS
-# For development purposes use the following BUILDs:
-#
-# `doctest`
-# Runs any doctest examples present in documented python modules.
-# `linkcheck`
-# Checks external links.
-#
-# SOFTWARE ENVIRONMENT
-# The software dependencies can be found by running the command::
-#
-# $ rose check-software --docs
-#
-# Rose provides two ways of installing the Python dependencies for the Rose
-# documentation builder:
-#
-# Virtual Environment:
-# Rose will automatically build a Python virtual environment
-# when the --venv flag is used with this command. The virtual
-# environment will be created in rose/venv and will be destroyed
-# after use. Use the --dev flag to prevent the environment being
-# destroyed for future use. Example::
-#
-# $ rose make-docs --venv --dev html # create and keep a virtualenv
-#
-# Conda
-# An environment file for creating a conda env can be found in
-# etc/rose-docs-env.yaml. Example usage::
-#
-# $ conda env create -f etc/rose-docs-env.yaml # create conda env
-# $ source activate rose-docs # activate conda env
-# $ rose make-docs html # make docs as normal
-#-----------------------------------------------------------------------------
-set -e
-set -o pipefail
-shopt -s extglob
-
-# Move into rose directory
-cd "$(dirname "$0")/../.."
-ROSE_HOME=$PWD
-# Path for virtualenv.
-VENV_PATH='venv' # Note: update above docs when changing this value
-# Set to `true` when the virtualenv is being used.
-USING_VENV=false
-# Path to the sphinx directory.
-SPHINX_PATH=sphinx
-# pick up official rose version
-ROSE_VERSION="$(python3 -c "import metomi.rose; print(metomi.rose.__version__)")"
-# documentation root output directory
-DOCS_DIR="${ROSE_HOME}/doc"
-
-# is the virtualenv command available
-if command -v virtualenv >/dev/null 2>&1; then
- VENV_COMPLIANT=true
-else
- VENV_COMPLIANT=false
-fi
-
-# Parse command line args.
-VENV_MODE=false
-FORCE=false
-DEV_MODE=false
-DEBUG=''
-BUILDS=()
-SPHINX_OPTS=()
-DEFAULT_ALIAS='doc'
-DEFAULT_VERSION=
-while [[ $# -gt 0 ]]; do
- case $1 in
- --venv)
- VENV_MODE=true
- shift
- ;;
- --dev)
- DEV_MODE=true
- shift
- ;;
- --force)
- FORCE=true
- shift
- ;;
- --strict)
- SPHINX_OPTS+=('SPHINXOPTS=-aEW')
- shift
- ;;
- --debug)
- DEBUG='--debug'
- shift
- ;;
- --default-version)
- DEFAULT_VERSION="$2"
- shift
- shift
- ;;
- *)
- BUILDS+=("$1")
- shift
- ;;
- esac
-done
-if [[ "${#BUILDS}" == 0 ]]; then
- BUILDS=('html')
-fi
-
-
-venv-activate () {
- USING_VENV=true
- . "${VENV_PATH}/bin/activate"
-}
-
-venv-install () {
- venv-destroy
- virtualenv --python=python3.7 "${VENV_PATH}"
- venv-activate
- pip install 'sphinx'
- pip install 'sphinx_rtd_theme'
- pip install 'sphinxcontrib-httpdomain'
- pip install 'hieroglyph'
-}
-
-venv-deactivate () {
- deactivate >/dev/null 2>&1 || true
-}
-
-venv-destroy () {
- venv-deactivate
- rm -rf "${VENV_PATH}"
-}
-
-version_file () {
- # output the dictionary {"version": ["build", ...]} in JSON format
- DOCS_DIR="$1"
- ret='{'
-
- # scrape filesystem for list of rose versions which have built docs
- for version_dir in "${DOCS_DIR}/"*.*.*; do
- version=$(basename "$version_dir")
- # scrape filesystem to get list of formats this version is available in
- ret+="\n \"$version\": ["
- for format_dir in "${version_dir}/"!(doctrees|*.html); do
- format=$(basename "$format_dir")
- ret+="\"$format\", "
- done
- ret="${ret:0:-2}],"
- done
-
- ret="${ret:0:-1}\n}"
- echo -e "$ret"
-}
-
-html_redirect () {
- # write an html file to $2 which auto-redirects to the relative path $1
- SRC="$1"
- DEST="$2"
-
- cat >"$DEST" << __HTML__
-
-
-
- Rose Documentation
-
-
-
-
-
-
-__HTML__
-}
-
-
-# Use virtualenv if present and requested or if we are in development mode.
-if "${DEV_MODE}" || "${VENV_MODE}"; then
- if ! "${VENV_COMPLIANT}"; then
- echo 'The virtualenv command is required for the --venv option.'
- exit 1
- fi
- if [[ -d "${VENV_PATH}" ]]; then
- venv-activate
- fi
-fi
-
-# Check core (sphinx) builder.
-if ! rose-check-software --doc >/dev/null; then
- if "${VENV_MODE}"; then
- venv-install
- elif ! "${FORCE}"; then
- echo 'Software required by the rose documentation builder is not
-present (run "rose check-software --doc" for details).
-
-For information on building a Python environment for the Rose documentation
-builder see the "Software Dependencies" section of the help page for this
-command.' >&2
- exit 1
- fi
-fi
-
-# makefile argument to set the output directory for this build
-SPHINX_OPTS+=("BUILDDIR=${DOCS_DIR}/${ROSE_VERSION}")
-
-# run sphinx-build
-if make ${DEBUG} -C "${SPHINX_PATH}" "${BUILDS[@]}" "${SPHINX_OPTS[@]}"; then
- RET=0
- # output file containing details of all versions and formats the
- # documentation has been built in (locally) for the version / format
- # switching pane
- version_file "${DOCS_DIR}" > "${DOCS_DIR}/versions.json"
- # symlink this version as the default
- (
- cd "${DOCS_DIR}"
- rm "${DEFAULT_ALIAS}" 2>/dev/null || true
- ln -s "${DEFAULT_VERSION:-$ROSE_VERSION}" "${DEFAULT_ALIAS}"
- )
- # symlink landing pages
- html_redirect "${DEFAULT_ALIAS}/html/index.html" 'doc/index.html'
- html_redirect "html/index.html" "doc/${ROSE_VERSION}/index.html"
- # support legacy doc/rose.html url
- mkdir 'doc/doc' 2>/dev/null || true
- html_redirect "html/index.html" "doc/${ROSE_VERSION}/rose.html"
-else
- RET=1
-fi
-
-# Remove virtualenv if used and if we are not in development mode.
-if "${USING_VENV}"; then
- if ! "${DEV_MODE}"; then
- venv-destroy
- fi
-fi
-
-exit ${RET}
diff --git a/etc/bin/rose-test-battery b/etc/bin/rose-test-battery
index 79b09d20b7..a2aebb2b67 100755
--- a/etc/bin/rose-test-battery
+++ b/etc/bin/rose-test-battery
@@ -48,19 +48,18 @@
# SEE ALSO
# * `prove(1)`
#-------------------------------------------------------------------------------
-# shellcheck source=lib/bash/rose_init
-# shellcheck source=lib/bash/rose_log
-. rose_init
-. rose_log
-
set -eu
-
-rose_init
+# shellcheck source=metomi/rose/etc/lib/bash/rose_log
+. "$(rose resource lib/bash/rose_log)"
# Move to folder in which prove command should be run.
TESTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd "$TESTDIR/../../"
mkdir -p ~/.metomi
+mkdir -p "${HOME}/cylc-run"
+
+ROSE_TEST_TIME_INIT="$(date -u +'%Y%m%dT%H%M%SZ')"
+export ROSE_TEST_TIME_INIT
# Recompile *.pyc files to ensure we are running the current code.
# @TODO Consider if this is appropriate in new version
diff --git a/.travis/shellchecker b/etc/bin/shellchecker
similarity index 95%
rename from .travis/shellchecker
rename to etc/bin/shellchecker
index f6857d954b..96e3b55da1 100755
--- a/.travis/shellchecker
+++ b/etc/bin/shellchecker
@@ -21,7 +21,7 @@
# Wrapper for running `shellcheck` over projects.
set -e
-cd "$(dirname "$0")/../"
+cd "$(dirname "$0")/../../"
# find shell files under the specified directory
find_files () {
@@ -77,9 +77,10 @@ main () {
default () {
# run a strict check on all "functional" scripts
main . \
- --exclude etc/rose-bash-completion \
--exclude t \
- -- -e SC1090 -e SC2119 -e SC2001
+ --exclude node_modules \
+ --exclude .git \
+ -- -e SC1090 -e SC2119 -e SC2001 \
# run a lenient check on all test scripts
main t -- -S error -e SC1090
diff --git a/etc/conf/macos-patch b/etc/conf/macos-patch
new file mode 100644
index 0000000000..51ef158d55
--- /dev/null
+++ b/etc/conf/macos-patch
@@ -0,0 +1,13 @@
+diff --git a/cylc/flow/hostuserutil.py b/cylc/flow/hostuserutil.py
+index 21e51735e..17917b8fc 100644
+--- a/cylc/flow/hostuserutil.py
++++ b/cylc/flow/hostuserutil.py
+@@ -113,7 +113,7 @@ class HostUtil:
+ """Return the extended info of the current host."""
+ if target not in self._host_exs:
+ if target is None:
+- target = socket.getfqdn()
++ target = socket.gethostname()
+ try:
+ self._host_exs[target] = socket.gethostbyname_ex(target)
+ except IOError as exc:
diff --git a/etc/deploy-docs b/etc/deploy-docs
index 26deecfabf..1392c083c9 100755
--- a/etc/deploy-docs
+++ b/etc/deploy-docs
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
#-----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
#
diff --git a/etc/images/rose-config-edit/change_icon.xpm b/etc/images/rose-config-edit/change_icon.xpm
deleted file mode 100644
index 8e731df3ba..0000000000
--- a/etc/images/rose-config-edit/change_icon.xpm
+++ /dev/null
@@ -1,78 +0,0 @@
-/* XPM */
-static char * change_icon_xpm[] = {
-"12 19 56 1",
-" c None",
-". c #0E2171",
-"+ c #1C3282",
-"@ c #445CA3",
-"# c #5268AC",
-"$ c #435AA2",
-"% c #1D3486",
-"& c #1D3284",
-"* c #647CBA",
-"= c #97ABD4",
-"- c #9EB1D8",
-"; c #96AAD4",
-"> c #637AB9",
-", c #253E90",
-"' c #122778",
-") c #344E9E",
-"! c #88A0D0",
-"~ c #95ABD5",
-"{ c #93A9D4",
-"] c #859ECF",
-"^ c #354E9F",
-"/ c #485FA7",
-"( c #14297A",
-"_ c #334DA0",
-": c #6985C3",
-"< c #6E89C5",
-"[ c #6D89C5",
-"} c #6A87C4",
-"| c #6481C1",
-"1 c #304A9D",
-"2 c #1B3285",
-"3 c #132879",
-"4 c #274198",
-"5 c #3B56A8",
-"6 c #415DAD",
-"7 c #3F5BAC",
-"8 c #3752A5",
-"9 c #253E95",
-"0 c #1B3185",
-"a c #1D358A",
-"b c #253F96",
-"c c #29439A",
-"d c #29449B",
-"e c #223B92",
-"f c #1D368B",
-"g c #162D81",
-"h c #1C348B",
-"i c #1F388F",
-"j c #1E368D",
-"k c #1A3187",
-"l c #1E3589",
-"m c #273E8E",
-"n c #20388C",
-"o c #182E83",
-"p c #1C3389",
-"q c #22398A",
-" ",
-" ",
-" ",
-" ",
-" ",
-" . ",
-" +@#$% ",
-" &*=-;>, ",
-" ')!~~{]^/ ",
-" (_:<[}|12 ",
-" 345667890 ",
-" abcd4ef ",
-" ghiijkl ",
-" mnopq ",
-" ",
-" ",
-" ",
-" ",
-" "};
diff --git a/etc/images/rose-config-edit/error_icon.xpm b/etc/images/rose-config-edit/error_icon.xpm
deleted file mode 100644
index 993ed1bde2..0000000000
--- a/etc/images/rose-config-edit/error_icon.xpm
+++ /dev/null
@@ -1,96 +0,0 @@
-/* XPM */
-static char * error_icon_xpm[] = {
-"14 20 73 1",
-" c None",
-". c #9D7A77",
-"+ c #9D7773",
-"@ c #B0A8A6",
-"# c #F66662",
-"$ c #F52121",
-"% c #B4A4A2",
-"& c #BB847D",
-"* c #FF2127",
-"= c #FF1D23",
-"- c #BB3D3D",
-"; c #AA9794",
-"> c #F85E5A",
-", c #B31919",
-"' c #B22020",
-") c #F73D3D",
-"! c #AA8988",
-"~ c #D1736B",
-"{ c #FD2D2D",
-"] c #1A0606",
-"^ c #1B0909",
-"/ c #FD5955",
-"( c #CF504D",
-"_ c #AA7F7C",
-": c #FF4747",
-"< c #FF3939",
-"[ c #2A0F0E",
-"} c #2B1817",
-"| c #FE716A",
-"1 c #FE6966",
-"2 c #A67370",
-"3 c #E65E5A",
-"4 c #FF3131",
-"5 c #FF4545",
-"6 c #3D1917",
-"7 c #3F2B27",
-"8 c #FE807A",
-"9 c #E6726C",
-"0 c #AD6461",
-"a c #FF3333",
-"b c #FF524D",
-"c c #A24A46",
-"d c #A4655E",
-"e c #FF8E85",
-"f c #FF978F",
-"g c #AD6A67",
-"h c #F64040",
-"i c #FF2929",
-"j c #FF3D3D",
-"k c #FF5551",
-"l c #49201F",
-"m c #4A3732",
-"n c #FF9C91",
-"o c #FEACA2",
-"p c #F68F88",
-"q c #AD4D4B",
-"r c #FF191F",
-"s c #FF5753",
-"t c #F8716A",
-"u c #F78F87",
-"v c #FEAEA4",
-"w c #FEB0A6",
-"x c #FF9E93",
-"y c #AC6764",
-"z c #BF7171",
-"A c #BF7979",
-"B c #BF7F7F",
-"C c #BF8785",
-"D c #BF8F8D",
-"E c #BF9693",
-"F c #BF9D99",
-"G c #BFA19D",
-"H c #BFA39F",
-" ",
-" ",
-" ",
-" ",
-" ",
-" .+ ",
-" @#$% ",
-" &*=- ",
-" ;>,')! ",
-" ~{]^/( ",
-" _:<[}|12 ",
-" 34567889 ",
-" 0a.
-#-------------------------------------------------------------------------------
-# NAME
-# rose-bash-completion
-#
-# SYNOPSIS
-# . $ROSE_HOME/etc/rose-bash-completion
-#
-# DESCRIPTION
-# Set Bash auto-completion for rose/rosie commands.
-#
-# Users should source this file in their ~/.bashrc, using something
-# like this:
-# if [[ $- =~ i && -f /path/to/rose-bash-completion ]]; then
-# . /path/to/rose-bash-completion
-# fi
-# where /path/to/rose-bash-completion is replaced by the path to
-# this file.
-#
-# Administrators may want to place this file in the
-# /etc/bash_completion.d/ (or equivalent) directory.
-#-------------------------------------------------------------------------------
-
-_rose() {
- local current possible_subcommand rose_command ROSE_DIR ok_subcommands
- local subcommand current_option_line current_option_arg_index
- local current_option current_option_arg current_index suboption_lines
- local suboption_sep_lines suboptions sub_args custom_function_name metavar
- local formal_current_option
- COMPREPLY=()
- current="${COMP_WORDS[COMP_CWORD]}"
- possible_subcommand="${COMP_WORDS[1]:-}"
- rose_command="${COMP_WORDS[0]}"
- rose --version 1>/dev/null 2>&1 || return 1
- if grep -q "bin/ros[^/]*$" <<<"$rose_command"; then
- ROSE_DIR=$(dirname "$rose_command")
- eval ROSE_DIR="$ROSE_DIR"
- ROSE_DIR=$(dirname $(cd $ROSE_DIR && pwd -P))
- rose_command=$(basename "$rose_command")
- else
- ROSE_DIR=$(__rose_get_rose_dir)
- fi
- ok_subcommands=$(cd $ROSE_DIR/ && ls $rose_command-* | \
- sed "s/^$rose_command-//g")
-
- # Determine the subcommand (e.g. app-run for rose app-run)
- subcommand=
- if [[ -n $possible_subcommand ]]; then
- for ok_subcommand in $ok_subcommands; do
- if [[ $ok_subcommand == $possible_subcommand ]]; then
- subcommand=$possible_subcommand
- break
- fi
- done
- fi
- if [[ -n $subcommand && -L $ROSE_DIR/bin/rose-$subcommand ]]; then
- subcommand=$(readlink $subcommand)
- fi
-
- # Determine option or option argument completion.
- current_option_line=$(printf "%s\n" "${COMP_WORDS[@]}" | grep -n "^-" | \
- tail -1)
- if [[ -n $current_option_line ]]; then
- current_option_arg_index=$( \
- grep -o "^[0-9][0-9]*" <<<"$current_option_line")
- current_option=$(grep -o '\-.*$' <<<"$current_option_line")
- current_option_arg="${COMP_WORDS[$current_option_arg_index]:-}"
- if [[ $current_option_arg == "=" ]]; then
- current_option_arg_index=$(( current_option_arg_index + 1 ))
- current_option_arg="${COMP_WORDS[$current_option_arg_index]:-}"
- fi
- else
- current_option_arg_index=
- current_option_arg=
- current_option=
- fi
- current_index=${#COMP_WORDS[@]}
- current_index=$(( current_index - 1 ))
-
- # Supply any completion information if we can.
- if [[ -n $subcommand ]]; then
- suboption_lines=$(__${rose_command}_help $subcommand | \
- sed -n '/^ -/p')
- suboption_sep_lines=$( \
- sed 's/^ *//g; s/, \(-\)/\n\1/g' <<<"$suboption_lines")
- suboptions=$(sed 's/=/ /g' <<<"$suboption_sep_lines")
- if grep -q '^'$current_option'$' <<<"$suboptions"; then
- # The current option does not take an argument - forget about it.
- current_option=
- fi
- if [[ -z $current_option ]] || \
- [[ $current_index -gt $current_option_arg_index && \
- -n $current_option_arg ]]; then
- # No relevant current option - supply options and arguments.
- custom_function_name="_${rose_command}_"${subcommand//-/_}_ARGS
- sub_args=
- if type -t $custom_function_name 1>/dev/null 2>&1; then
- sub_args=$($custom_function_name "$current")
- fi
- suboptions=$( \
- sed 's/=.*$/=/g; s/ *.*$//g; s/ *$//g;' \
- <<<"$suboption_sep_lines")
- COMPREPLY=( $(compgen -W "$suboptions $sub_args" -- "$current") )
- return 0
- fi
- if grep -q "^$current_option\>" <<<"$suboptions" && \
- [[ $current != $current_option ]]; then
- # The current string must be an argument to the option.
- current_option_args=
- if [[ $current != "=" ]]; then
- current_option_args=$current
- fi
- metavar=$(sed -n "s/^$current_option \([^ ]*\)/\1/p" \
- <<<"$suboptions")
- if [[ $metavar == "DIR" || $metavar == "PATH" || \
- $metavar = "FILE" ]]; then
- return 0
- fi
- formal_current_option=$( \
- sed -n "s/^ *\(-[^ =]*\).*, $current_option\b.*/\1/p" \
- <<<"$suboption_lines")
- if [[ -n $formal_current_option ]]; then
- current_option=$formal_current_option
- fi
- custom_function_name="_${rose_command}_"${subcommand//-/_}
- custom_function_name+=${current_option//-/_}
- if type -t $custom_function_name 1>/dev/null 2>&1; then
- $custom_function_name "$current_option_args"
- return $?
- fi
- COMPREPLY=
- return 1
- fi
- suboptions=$(sed 's/=.*$/=/g; s/ *.*$//g; s/ *$//g;' \
- <<<"$suboption_sep_lines")
- COMPREPLY=( $(compgen -W "$suboptions" -- "$current") )
- return 0
- fi
- # There is an incomplete subcommand or none at all - supply a list of them.
- COMPREPLY=( $(compgen -W "$ok_subcommands" -- "$current") )
- return 0
-}
-
-_rose_app_run__app_mode() {
- COMPREPLY=( $(compgen -W "fcm_make rose_ana rose_arch rose_prune" -- $1) )
-}
-
-_rose_app_run__command_key() {
- local config_dir command_keys
- config_dir=$(__rose_get_config_dir)
- if [[ -d $config_dir ]]; then
- command_keys=$(rose config --keys --file=$config_dir/rose-app.conf command 2>/dev/null)
- COMPREPLY=( $(compgen -W "$command_keys" -- $1) )
- return 0
- fi
- return 1
-}
-
-_rose_app_run__opt_conf_key() {
- local config_dir opt_conf_keys
- config_dir=$(__rose_get_config_dir)
- if [[ -d $config_dir/opt ]]; then
- opt_conf_keys=$(cd $config_dir/opt/ && ls rose-app-*.conf | \
- sed "s/rose-app-\(.*\).conf/\1/g")
- COMPREPLY=( $(compgen -W "$opt_conf_keys" -- $1) )
- return 0
- fi
- return 1
-}
-
-_rose_app_upgrade_ARGS() {
- local config_dir meta_path meta_path_option all_versions_option
- config_dir=$(__rose_get_config_dir)
- meta_path=$(__rose_get_meta_path)
- all_versions_option=
- meta_path_option="--meta-path=$meta_path"
- if [[ -z $meta_path ]]; then
- meta_path_option=
- fi
- if printf "%s " "${COMP_WORDS[@]}" | \
- grep -q " --all-versions\| -a "; then
- all_versions_option="--all-versions"
- fi
- rose app-upgrade $all_versions_option $meta_path_option \
- --config=$config_dir 2>/dev/null | sed 's/^. \(.*\)$/\1/g'
-}
-
-_rose_host_select__rank_method() {
- COMPREPLY=( $(compgen -W "load fs mem random" $1) )
-}
-
-_rose_macro_ARGS() {
- local config_dir meta_path meta_path_option
- config_dir=$(__rose_get_config_dir)
- meta_path=$(__rose_get_meta_path)
- meta_path_option="--meta-path=$meta_path"
- if [[ -z $meta_path ]]; then
- meta_path_option=
- fi
- rose macro -q $meta_path_option --config=$config_dir 2>/dev/null | \
- sed "s/^\[.*\] //g"
-}
-
-_rose_metadata_graph_ARGS() {
- local config_dir meta_path
- config_dir=$(__rose_get_config_dir)
- meta_path=$(__rose_get_meta_path)
- if [[ -f $config_dir/rose-app.conf ]]; then
- PATH=$PATH:$meta_path rose config --meta --keys \
- --file=$config_dir/rose-app.conf 2>/dev/null | \
- sed '/=/d'
- else
- rose config --keys --file=$config_dir/rose-meta.conf 2>/dev/null | \
- sed '/=/d'
- fi
-}
-
-_rose_metadata_graph__property() {
- COMPREPLY=( $(compgen -W "trigger" -- $1) )
- return 0
-}
-
-_rose_stem__group() {
- local config_dir groups
- config_dir=$(__rose_stem_get_config_dir)
- if [[ ! -e $config_dir/meta/rose-meta.conf ]]; then
- return 1
- fi
- groups=$(rose config --file=$config_dir/meta/rose-meta.conf \
- jinja2:suite.rc=RUN_NAMES widget[rose-config-edit] | \
- grep -o "\-\-choices.[^ ]*" | sed "s/--choices.//g;" | \
- tr '\n' ',' | tr ',' ' ')
- tasks=$(rose config --file=$config_dir/meta/rose-meta.conf \
- jinja2:suite.rc=RUN_NAMES widget[rose-config-edit] | \
- tr ' ' '\n' | sed -n '/rose.config_editor.*/d; /^\([^-].*\)/p')
- COMPREPLY=( $(compgen -W "$groups $tasks" -- $1) )
- return 0
-}
-
-# Should we separate this from _rose_stem__group?
-_rose_stem__task() {
- _rose_stem__group "$@"
-}
-
-_rose_suite_hook__mail_cc() {
- local users
- prev_users=$1
- user_tail=${prev_users##*,}
- user_head=${prev_users%,*}
- users=$(__rose_get_users)
- if [[ -n $user_tail ]] && grep -q "\<$user_tail\>" <<<"$users"; then
- # A complete user name, now need a comma.
- users=$(sed "s/^/$prev_users,/; s/ / $prev_users,/g" <<<"$users")
- COMPREPLY=( $(compgen -W "$users" -- $prev_users) )
- return
- fi
- if [[ -n $user_head && $user_head != $prev_users ]]; then
- users=$(sed "s/^/$user_head,/; s/ / $user_head,/g" <<<"$users")
- fi
- COMPREPLY=( $(compgen -W "$users" -- $prev_users) )
-}
-
-_rose_suite_log__name() {
- local names
- names=$(cylc print -xy 2>/dev/null)
- COMPREPLY=( $(compgen -W "$names" -- $1) )
-}
-
-_rose_suite_log__user() {
- local users
- users=$(__rose_get_users)
- COMPREPLY=( $(compgen -W "$users" -- $1) )
-}
-
-_rose_suite_run__host() {
- local hosts
- hosts=$(rose config rose-suite-run hosts 2>/dev/null)
- hosts=$(__rose_get_expanded_hosts "$hosts")
- COMPREPLY=( $(compgen -W "$hosts" -- $1) )
-}
-
-_rose_suite_run__opt_conf_key() {
- local config_dir opt_conf_keys
- config_dir=$(__rose_get_config_dir)
- if [[ -d $config_dir/opt ]]; then
- opt_conf_keys=$(cd $config_dir/opt/ && ls rose-suite-*.conf | \
- sed "s/rose-suite-\(.*\).conf/\1/g")
- COMPREPLY=( $(compgen -W "$opt_conf_keys" -- $1) )
- return 0
- fi
- return 1
-}
-
-_rose_suite_run__run() {
- COMPREPLY=( $(compgen -W "reload restart run" -- $1) )
-}
-
-_rose_suite_shutdown__name() {
- local names
- names=$(cylc print -xy 2>/dev/null)
- COMPREPLY=( $(compgen -W "$names" -- $1) )
-}
-
-_rose_suite_stop__name() {
- _rose_suite_shutdown__name "$@"
- return $?
-}
-
-_rose_task_run__app_mode() {
- _rose_app_run__app_mode "$@"
- return $?
-}
-
-_rose_task_run__command_key() {
- _rose_app_run__command_key "$@"
- return $?
-}
-
-_rose_task_run__opt_conf_key() {
- _rose_app_run__opt_conf_key "$@"
- return $?
-}
-
-_rose_test_battery_ARGS() {
- cd $ROSE_DIR/t && ls -d ros*
-}
-
-_rosie_create__prefix() {
- __rosie_get_prefixes "$@"
- return $?
-}
-
-_rosie_go__prefix() {
- __rosie_get_prefixes "$@"
- return $?
-}
-
-_rosie_lookup__prefix() {
- __rosie_get_prefixes "$@"
- return $?
-}
-
-_rosie_ls__prefix() {
- __rosie_get_prefixes "$@"
- return $?
-}
-
-__rose_get_config_dir() {
- local config_dir
- config_dir=$( \
- printf "%s " "${COMP_WORDS[@]}" | \
- sed -n "s/.*--config = \([^ ]*\).*/\1/p; s/.*-C *\([^ ]*\).*/\1/p")
- if [[ -z $config_dir ]]; then
- config_dir=$PWD
- fi
- eval config_dir="$config_dir"
- cd $config_dir 1>/dev/null 2>&1 && pwd -P
-}
-
-__rose_stem_get_config_dir() {
- local config_dir
- config_dir=$(__rose_get_config_dir)
- if [[ ! -e $config_dir/rose-stem/suite.rc ]]; then
- while [[ ! -d $config_dir/rose-stem ]] && \
- svn info $config_dir >/dev/null 2>&1; do
- new_config_dir=$(cd $config_dir/.. && pwd -P)
- if [[ $new_config_dir == $config_dir ]]; then
- return 1
- fi
- config_dir=$new_config_dir
- done
- if [[ ! -e $config_dir/rose-stem/suite.rc ]]; then
- return 1
- fi
- fi
- config_dir=$config_dir/rose-stem
- cd $config_dir 1>/dev/null 2>&1 && pwd -P
-}
-
-__rose_get_expanded_hosts() {
- for host in "$@"; do
- rose config rose-host-select group{$host} || echo $host
- done
-}
-
-__rose_get_meta_path() {
- local meta_path
- meta_path=$( \
- printf "%s " "${COMP_WORDS[@]}" | \
- sed -n "s/.*--meta-path = \([^ ]*\).*/\1/p; s/.*-M *\([^ ]*\).*/\1/p")
- if [[ -n $meta_path ]]; then
- eval meta_path="$meta_path"
- cd $meta_path 1>/dev/null 2>&1 && pwd -P
- fi
-}
-
-__rose_get_rose_dir() {
- rose --version | sed "s/.*(\(.*\))/\1/"
-}
-
-__rose_get_users() {
- getent passwd | cut -d: -f1 | LANG=C sort -u
-}
-
-__rose_help() {
- local subcommand
- subcommand=$1
- if type -t _rose_help__${subcommand//-/_}; then
- _rose_help__${subcommand//-/_} | sed -n '/^OPTIONS$/,/^[^ ]$/p;'
- return
- fi
- rose help $subcommand 2>/dev/null
-}
-
-__rosie_get_prefixes() {
- local prefixes
- prefixes=$(rose config --keys rosie-id | \
- sed -n "s/^prefix-location\.\(.*\)/\1/p")
- COMPREPLY=( $(compgen -W "$prefixes" -- $1) )
-}
-
-__rosie_help() {
- local subcommand
- subcommand=$1
- if type -t _rosie_help__${subcommand//-/_}; then
- _rosie_help__${subcommand//-/_}
- return
- fi
- rosie help $subcommand
-}
-
-_rose_help__stem() {
- rose help stem
- rose help suite-run
-}
-
-_rose_help__task_run() {
- rose help task-run
- rose help app-run
- rose help task-env
-}
-
-complete -o bashdefault -o default -o nospace -F _rose rose rosie
diff --git a/etc/rose-config-edit/.gtkrc-2.0 b/etc/rose-config-edit/.gtkrc-2.0
deleted file mode 100644
index f8bd295494..0000000000
--- a/etc/rose-config-edit/.gtkrc-2.0
+++ /dev/null
@@ -1 +0,0 @@
-gtk-icon-sizes = "gtk-large-toolbar=20,20:gtk-small-toolbar=20,20:panel-menu=16,16:gtk-button=16,16"
diff --git a/etc/rose.conf.example b/etc/rose.conf.example
index 3d0e279649..983362ab7f 100644
--- a/etc/rose.conf.example
+++ b/etc/rose.conf.example
@@ -1,7 +1,8 @@
# The :rose:file:`rose.conf` file can be installed in:
#
-# * ``$ROSE_HOME/etc/`` for *site* configuration.
-# * ``$HOME/.metomi/rose/`` for *user* configuration.
+# * ``/etc/rose.conf``
+# * ``$ROSE_SITE_CONF_PATH/rose.conf``
+# * ``$HOME/.metomi/rose.conf``
#
#.. This file documents the settings and their default values.
# Values of settings in this file are mostly placeholders,
@@ -111,42 +112,6 @@ notification-from=EMAIL-ADDRESS
user-tool=ldap|passwd
-# Configuration for Rose Bush server.
-[rose-bush]
-# :default: 100
-#
-# Cycles list view: default number of cycles per page.
-cycles-per-page=NUMBER
-# :default: The server's host name.
-#
-# An alternative host name.
-host=NAME
-# :default: 15
-#
-# Job list view: default number of jobs per page.
-jobs-per-page=NUMBER
-# :default: 300
-#
-# Job list view: maximum number of jobs per page.
-jobs-per-page-max=NUMBER
-# Image logo attributes, can be any HTML ```` tag attributes
-# e.g: ``logo=src="http://server/my-rose-bush-logo.png" alt="My Rose Bush
-# Logo"``.
-logo=HTML-IMG-ATTRIBUTE ...
-# :default: 100
-#
-# Suites list view: default number of suites per page.
-suites-per-page=NUMBER
-# :default: "Rose Bush"
-#
-# An alternative service title.
-title=TITLE
-# :default: 10485760
-#
-# File view: maximum viewable file size in bytes.
-view-size-max=BYTES
-
-
# Configuration specific to :ref:`command-rose-config-diff`.
[rose-config-diff]
# :default: "title,ns,description,help"
@@ -158,20 +123,6 @@ properties=title,ns,description,help
ignore{foo}=REGEX
-
-# Configuration specific to :ref:`command-rose-config-edit`.
-[rose-config-edit]
-# :default: /opt/cylc/images/icon.svg
-#
-# Path to an image containing the suite engine icon.
-# See :py:mod:`rose.config_editor` for detail.
-icon-path-scheduler=PATH
-# :default: https://github.com/metomi/rose/
-#
-# Hyperlink to the Rose project page.
-project-url=URL
-
-
# Configuration related to :ref:`command-rose-host-select`.
[rose-host-select]
# The default arguments to use for this command e.g. ``default=hpc``.
@@ -206,17 +157,11 @@ timeout=FLOAT
[rose-stem]
# Automatic options. These are added as if the user added them with
# ``--define-suite`` on the command line and can be accessed as Jinja2
-# variables in the ``suite.rc`` file. E.g ``automatic-options=TEA=earl_grey``
+# variables in the ``flow.cylc`` file. E.g ``automatic-options=TEA=earl_grey``
# would set the Jinja2 variable ``TEA`` to be ``earl_grey``.
automatic-options=VARIABLE=VALUE
-# Configuration related to :ref:`command-rose-suite-log`.
-[rose-suite-log]
-# URL to the site's Rose Bush web service.
-rose-bush=URL
-
-
# Configuration related to :ref:`command-rose-mpi-launch`.
[rose-mpi-launch]
# Specify a list of launcher commands e.g::
@@ -243,77 +188,6 @@ launcher-preopts.LAUNCHER=OPTIONS-TEMPLATE
launcher-postopts.LAUNCHER=OPTIONS-TEMPLATE
-# Configuration related to :ref:`command-rose-suite-run`.
-[rose-suite-run]
-# Hosts in the :rose:conf:`[rose-host-select]` section that can be used to
-# run a suite e.g::
-#
-# hosts=rose-vm
-hosts=HOST-GROUP|HOST ...
-# Hosts in the :rose:conf:`[rose-host-select]` section that can be scanned
-# by rose utilities e.g::
-#
-# scan-hosts=localhost rose-vm
-scan-hosts=HOST-GROUP|HOST ...
-# :default: false
-#
-# Don't use login shell to invoke ``rose suite-run --remote``
-# where ``HOST-GLOB`` is a glob for matching host names e.g::
-#
-# remote-no-login-shell=myhpc*=true
-# mycluster*=true
-remote-no-login-shell=HOST-GLOB=true|false
-# :default: rose
-#
-# Path to ``rose`` executable on remote hosts where:
-#
-# * ``HOST-GLOB`` is a glob for matching host names.
-# * ``ROSE-BIN-PATH`` is the path to the ``rose`` executable.
-#
-# E.g::
-#
-# remote-rose-bin=myhpc*=/opt/rose/bin/rose
-# mycluster*=/usr/local/bin/rose
-remote-rose-bin=HOST-GLOB=ROSE-BIN-PATH
-# :default: $HOME
-#
-# Root location of a suite run directory where:
-#
-# * ``HOST-GLOB`` is a glob for matching host names.
-# * ``HOST-DIR`` is the value of the root location for matching hosts.
-#
-# E.g::
-#
-# root-dir=hpc*=$DATADIR
-# =*=$HOME
-root-dir=HOST-GLOB=$HOST-DIR
-# :default: $HOME
-#
-# Root location of a suite run's ``share/`` directory. Syntax is the same
-# as :rose:conf:`root-dir`.
-# Multiple pairs can be specified by a new-line separated list.
-#
-# .. warning::
-#
-# If a suite has previously been run changes to any of the ``root-dir``
-# settings will take effect on the next clean re-installation i.e:
-#
-# $ rose suite-run --new
-root-dir{share}=HOST-GLOB=$HOST-DIR
-# :default: $HOME
-#
-# Root location of a suite run's ``share/cycle/`` directory. Syntax is
-# the same as :rose:conf:`root-dir`.
-# Multiple pairs can be specified by a new-line separated list.
-root-dir{share/cycle}=HOST-GLOB=$HOST-DIR
-# :default: $HOME
-#
-# Root location of a suite run's ``work/`` directory. Syntax is the same
-# as :rose:conf:`root-dir`.
-# Multiple pairs can be specified by a new-line separated list.
-root-dir{work}=HOST-GLOB=$HOST-DIR
-
-
# Configuration related to :ref:`command-rose-task-run`.
[rose-task-run]
# Items to prepend to the ``PATH`` environment variable.
@@ -333,9 +207,9 @@ method-path=/path/1 /path2
# `.
kgo-database=.true.
# Limits the number of lines printed when using the
-# :py:mod:`rose.apps.ana_builtin.grepper` analysis class.
+# :py:mod:`metomi.rose.apps.ana_builtin.grepper` analysis class.
grepper-report-limit=42
-# Causes the :py:mod:`rose.apps.ana_builtin.grepper` class to pass
+# Causes the :py:mod:`metomi.rose.apps.ana_builtin.grepper` class to pass
# if all files to be compared are missing.
skip-if-all-files-missing=.true.
@@ -367,7 +241,6 @@ title=TITLE
# Configuration related to the :ref:`command-rosie-go` GUI.
-# See :py:mod:`rosie.browser` for detail.
[rosie-go]
# :default: /opt/cylc/images/icon.svg
#
@@ -437,6 +310,8 @@ access-list-default=USER-ID ...
# Tool to compare two files when there are differences.
difftool=COMMAND ...
# List of selectable host groups.
+#
+# These must be defined in :rose:conf:`[rose-host-select]group{NAME}`.
host-groups=GROUP ...
# A remote host that does not share its ``HOME`` directory with ``localhost``.
job-host=HOST
diff --git a/etc/tutorial/consolidation-tutorial/suite.rc b/etc/tutorial/consolidation-tutorial/flow.cylc
similarity index 100%
rename from etc/tutorial/consolidation-tutorial/suite.rc
rename to etc/tutorial/consolidation-tutorial/flow.cylc
diff --git a/etc/tutorial/cylc-forecasting-suite/.validate b/etc/tutorial/cylc-forecasting-suite/.validate
index 9d13a3b35d..3d0b183322 100644
--- a/etc/tutorial/cylc-forecasting-suite/.validate
+++ b/etc/tutorial/cylc-forecasting-suite/.validate
@@ -1,3 +1,3 @@
rose tutorial "$(basename $TUT_DIR)" "${CYLC_RUN_DIR}/${REG}"
-sed -i '1s;^;[cylc]\n abort if any task fails = True\n;' "${CYLC_RUN_DIR}/${REG}/suite.rc"
-cylc run --no-detach "${REG}" 2>&1
+sed -i '1s;^;[cylc]\n abort if any task fails = True\n;' "${CYLC_RUN_DIR}/${REG}/flow.cylc"
+cylc play --no-detach "${REG}" 2>&1
diff --git a/etc/tutorial/cylc-forecasting-suite/bin/get-rainfall b/etc/tutorial/cylc-forecasting-suite/bin/get-rainfall
index 4dd421cc44..12c7bddcb9 100755
--- a/etc/tutorial/cylc-forecasting-suite/bin/get-rainfall
+++ b/etc/tutorial/cylc-forecasting-suite/bin/get-rainfall
@@ -34,7 +34,7 @@ URL = ('http://datapoint.metoffice.gov.uk/public/data/layer/wxobs/'
'RADAR_UK_Composite_Highres/png?TIME={time}&key={api_key}')
-class Rainfall(object):
+class Rainfall:
"""Class for holding rainfall data.
Args:
diff --git a/etc/tutorial/cylc-forecasting-suite/suite.rc b/etc/tutorial/cylc-forecasting-suite/flow.cylc
similarity index 100%
rename from etc/tutorial/cylc-forecasting-suite/suite.rc
rename to etc/tutorial/cylc-forecasting-suite/flow.cylc
diff --git a/etc/tutorial/cylc-forecasting-suite/lib/python/mecator.py b/etc/tutorial/cylc-forecasting-suite/lib/python/mecator.py
index 35e6a14365..2812e737dc 100644
--- a/etc/tutorial/cylc-forecasting-suite/lib/python/mecator.py
+++ b/etc/tutorial/cylc-forecasting-suite/lib/python/mecator.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors - GNU V3+.
# This is illustrative code developed for tutorial purposes, it is not
# intended for scientific use and is not guarantied to be accurate or correct.
diff --git a/etc/tutorial/cylc-forecasting-suite/lib/python/util.py b/etc/tutorial/cylc-forecasting-suite/lib/python/util.py
index 4bf2c83618..eb9f7fa3a2 100644
--- a/etc/tutorial/cylc-forecasting-suite/lib/python/util.py
+++ b/etc/tutorial/cylc-forecasting-suite/lib/python/util.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors - GNU V3+.
# This is illustrative code developed for tutorial purposes, it is not
# intended for scientific use and is not guarantied to be accurate or correct.
@@ -213,7 +211,7 @@ def get_grid_coordinates(lng, lat, domain, resolution):
length_y - int((abs(lat - domain['lat1'])) // resolution))
-class SurfaceFitter(object):
+class SurfaceFitter:
"""A 2D interpolation for random points.
A standin for scipy.interpolate.interp2d
diff --git a/etc/tutorial/inheritance-tutorial/suite.rc b/etc/tutorial/inheritance-tutorial/flow.cylc
similarity index 100%
rename from etc/tutorial/inheritance-tutorial/suite.rc
rename to etc/tutorial/inheritance-tutorial/flow.cylc
diff --git a/etc/tutorial/queues-tutorial/suite.rc b/etc/tutorial/queues-tutorial/flow.cylc
similarity index 100%
rename from etc/tutorial/queues-tutorial/suite.rc
rename to etc/tutorial/queues-tutorial/flow.cylc
diff --git a/etc/tutorial/retries-tutorial/suite.rc b/etc/tutorial/retries-tutorial/flow.cylc
similarity index 100%
rename from etc/tutorial/retries-tutorial/suite.rc
rename to etc/tutorial/retries-tutorial/flow.cylc
diff --git a/etc/tutorial/rose-stem/.validate b/etc/tutorial/rose-stem/.validate
index 736af32a69..adf657b18f 100644
--- a/etc/tutorial/rose-stem/.validate
+++ b/etc/tutorial/rose-stem/.validate
@@ -1,4 +1,4 @@
mkdir "${CYLC_RUN_DIR}/${REG}"
-echo -e '#!Jinja2\n{% set RUN_NAMES=["command_spaceship"] %}' > "${CYLC_RUN_DIR}/${REG}/suite.rc"
-cat "$TUT_DIR/rose-stem/suite.rc" >> "${CYLC_RUN_DIR}/${REG}/suite.rc"
+echo -e '#!Jinja2\n{% set RUN_NAMES=["command_spaceship"] %}' > "${CYLC_RUN_DIR}/${REG}/flow.cylc"
+cat "$TUT_DIR/rose-stem/flow.cylc" >> "${CYLC_RUN_DIR}/${REG}/flow.cylc"
cylc validate "${CYLC_RUN_DIR}/${REG}" -s "SOURCE_SPACESHIP=foo"
diff --git a/etc/tutorial/rose-stem/rose-stem/suite.rc b/etc/tutorial/rose-stem/rose-stem/flow.cylc
similarity index 100%
rename from etc/tutorial/rose-stem/rose-stem/suite.rc
rename to etc/tutorial/rose-stem/rose-stem/flow.cylc
diff --git a/etc/tutorial/rose-suite-tutorial/suite.rc b/etc/tutorial/rose-suite-tutorial/flow.cylc
similarity index 100%
rename from etc/tutorial/rose-suite-tutorial/suite.rc
rename to etc/tutorial/rose-suite-tutorial/flow.cylc
diff --git a/etc/tutorial/rose-weather-forecasting-suite/suite.rc b/etc/tutorial/rose-weather-forecasting-suite/flow.cylc
similarity index 100%
rename from etc/tutorial/rose-weather-forecasting-suite/suite.rc
rename to etc/tutorial/rose-weather-forecasting-suite/flow.cylc
diff --git a/etc/tutorial/runtime-introduction/suite.rc b/etc/tutorial/runtime-introduction/flow.cylc
similarity index 100%
rename from etc/tutorial/runtime-introduction/suite.rc
rename to etc/tutorial/runtime-introduction/flow.cylc
diff --git a/etc/tutorial/runtime-tutorial/suite.rc b/etc/tutorial/runtime-tutorial/flow.cylc
similarity index 100%
rename from etc/tutorial/runtime-tutorial/suite.rc
rename to etc/tutorial/runtime-tutorial/flow.cylc
diff --git a/etc/tutorial/widget/meta/lib/python/widget/username.py b/etc/tutorial/widget/meta/lib/python/widget/username.py
index b2062c3cc3..d381db4397 100644
--- a/etc/tutorial/widget/meta/lib/python/widget/username.py
+++ b/etc/tutorial/widget/meta/lib/python/widget/username.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
"""
This module contains value widgets for helping enter usernames.
@@ -10,9 +9,9 @@
from functools import partial
-import gobject
import pygtk
pygtk.require('2.0')
+# flake8: noqa: E402
import gtk
diff --git a/lib/bash/rose_init b/lib/bash/rose_init
deleted file mode 100755
index 0c219bd4e5..0000000000
--- a/lib/bash/rose_init
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/bin/bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# NAME
-# rose_init
-#
-# SYNOPSIS
-# . $ROSE_HOME/lib/bash/rose_init
-# rose_init [FUNC ...]
-#
-# DESCRIPTION
-# Initialise a bash script with the following:
-# * "set -eu".
-# * load the "rose_usage" function.
-# * load any FUNC specified in the argument list.
-#-------------------------------------------------------------------------------
-# shellcheck source=lib/bash/rose_usage
-. rose_usage
-
-function rose_init() {
- set -eu
- ROSE_NS=$(basename "$0")
- ROSE_LIB="$(dirname "$(python -c "import metomi.rose; print(metomi.rose.__file__)")")"
- ROSE_HOME_BIN=$(dirname "$(command -v rose)")
- ROSE_VERSION="$(python3 -c "import metomi.rose; print(metomi.rose.__version__)")"
- export ROSE_LIB ROSE_HOME_BIN ROSE_NS ROSE_VERSION
- local LIB=$ROSE_LIB
- if [[ -f $LIB/${FUNCNAME[0]}_site ]]; then
- . "$LIB/${FUNCNAME[0]}_site"
- fi
-
- local ITEM=
- for ITEM in rose_usage "$@"; do
- local FILE=
- for FILE in $ITEM; do
- . "$FILE"
- done
- done
-}
diff --git a/lib/bash/rose_init_site.example b/lib/bash/rose_init_site.example
deleted file mode 100644
index a569cb26b6..0000000000
--- a/lib/bash/rose_init_site.example
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-#-------------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-#-------------------------------------------------------------------------------
-# NAME
-# rose_init_site
-#
-# SYNOPSIS
-# . $ROSE_HOME/lib/bash/rose_init_site
-#
-# DESCRIPTION
-# Site specified PATH, PYTHONPATH, etc.
-#-------------------------------------------------------------------------------
-#PYTHONPATH=/path/to/site/specific/python/lib:$PYTHONPATH
-#PATH=/path/to/site/specific/bin:$PATH
diff --git a/metomi/rose/__init__.py b/metomi/rose/__init__.py
index 70bebfb1ed..a944af6af5 100644
--- a/metomi/rose/__init__.py
+++ b/metomi/rose/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -56,15 +53,6 @@
"=type"]
SUB_CONFIG_DEFAULT_META_IDS = ["=file-install-root", "=meta", "=mode",
"=opts", "command", "file:", "poll"]
-TOP_CONFIG_DEFAULT_META_IDS = [
- "file:",
- "jinja2:suite.rc",
- "=meta",
- "=opts",
- "=root-dir",
- "=root-dir{share}",
- "=root-dir{share/cycle}",
- "=root-dir{work}"]
CONFIG_SETTING_INDEX_DEFAULT = "1"
diff --git a/metomi/rose/app_run.py b/metomi/rose/app_run.py
index c9ed262a42..3195f5f7fb 100644
--- a/metomi/rose/app_run.py
+++ b/metomi/rose/app_run.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -115,7 +112,7 @@ def __str__(self):
ok_str, strftime("%Y-%m-%dT%H:%M:%S", localtime(sec)), test)
-class Poller(object):
+class Poller:
"""Handle the [poll] functionality for AppRunner."""
@@ -287,7 +284,7 @@ def _poll_file(self, file_, poll_file_test):
return is_done
-class BuiltinApp(object):
+class BuiltinApp:
"""An abstract base class for a builtin application.
diff --git a/metomi/rose/apps/__init__.py b/metomi/rose/apps/__init__.py
index 8460776f05..f7b71f8284 100644
--- a/metomi/rose/apps/__init__.py
+++ b/metomi/rose/apps/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/apps/ana_builtin/grepper.py b/metomi/rose/apps/ana_builtin/grepper.py
index 5504ab5929..a7efb9ae7f 100644
--- a/metomi/rose/apps/ana_builtin/grepper.py
+++ b/metomi/rose/apps/ana_builtin/grepper.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/apps/comparisons/cumf.py b/metomi/rose/apps/comparisons/cumf.py
index 718b98b7ed..93195dc0c1 100644
--- a/metomi/rose/apps/comparisons/cumf.py
+++ b/metomi/rose/apps/comparisons/cumf.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -30,7 +27,7 @@
HEADER = "differ, however the data fields are identical"
-class Cumf(object):
+class Cumf:
"""Analyse the output from the UM small exec cumf"""
@@ -54,7 +51,7 @@ def run(self, task):
return task
-class CumfWarnHeader(object):
+class CumfWarnHeader:
"""As cumf, but issue a warning if only the header has changed"""
@@ -74,7 +71,7 @@ def run(self, task):
return task
-class CumfComparisonFailure(object):
+class CumfComparisonFailure:
"""Class used if a cumf comparison fails."""
@@ -97,7 +94,7 @@ def __repr__(self):
__str__ = __repr__
-class CumfComparisonSuccess(object):
+class CumfComparisonSuccess:
"""Class used if a cumf comparison succeeds"""
@@ -114,7 +111,7 @@ def __repr__(self):
__str__ = __repr__
-class CumfComparisonHeaderWarning(object):
+class CumfComparisonHeaderWarning:
"""Class used if cumf reports just the header of a file is different"""
@@ -131,7 +128,7 @@ def __repr__(self):
__str__ = __repr__
-class CumfSummaryNotFoundFailure(object):
+class CumfSummaryNotFoundFailure:
"""Class used if there is a problem finding a cumf summary file"""
@@ -148,7 +145,7 @@ def __repr__(self):
__str__ = __repr__
-class CumfDiffNotFoundFailure(object):
+class CumfDiffNotFoundFailure:
"""Class used if there is a problem finding a cumf diff file"""
diff --git a/metomi/rose/apps/comparisons/exact.py b/metomi/rose/apps/comparisons/exact.py
index 2ab46fb3f9..d1df06cefb 100644
--- a/metomi/rose/apps/comparisons/exact.py
+++ b/metomi/rose/apps/comparisons/exact.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -26,10 +23,9 @@
FAIL = "!="
-class Exact(object):
+class Exact:
def run(self, task):
"""Perform an exact comparison between the result and the KGO data"""
- failures = 0
if len(task.resultdata) != len(task.kgo1data):
raise DataLengthError(task)
location = 0
@@ -46,7 +42,7 @@ def run(self, task):
return task
-class ExactComparisonFailure(object):
+class ExactComparisonFailure:
"""Class used if results do not match the KGO"""
@@ -79,7 +75,7 @@ def __repr__(self):
__str__ = __repr__
-class ExactComparisonSuccess(object):
+class ExactComparisonSuccess:
"""Class used if results match the KGO"""
diff --git a/metomi/rose/apps/comparisons/mandatory.py b/metomi/rose/apps/comparisons/mandatory.py
index aabc159914..f3616f38ee 100644
--- a/metomi/rose/apps/comparisons/mandatory.py
+++ b/metomi/rose/apps/comparisons/mandatory.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -24,10 +21,9 @@
FAIL = "!~"
-class Mandatory(object):
+class Mandatory:
def run(self, task):
"""Perform an exact comparison between the result and the KGO data"""
- failures = 0
if len(task.resultdata) == 0:
task.set_failure(MandatoryStringResult(task, FAIL))
else:
@@ -35,7 +31,7 @@ def run(self, task):
return task
-class MandatoryStringResult(object):
+class MandatoryStringResult:
"""Result of mandatory text examination."""
diff --git a/metomi/rose/apps/comparisons/output_grepper.py b/metomi/rose/apps/comparisons/output_grepper.py
index 5c42a97c08..7a0ccb247f 100644
--- a/metomi/rose/apps/comparisons/output_grepper.py
+++ b/metomi/rose/apps/comparisons/output_grepper.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -28,7 +25,7 @@
}
-class OutputGrepper(object):
+class OutputGrepper:
def run(self, task, variable):
"""Return a list of values matching a regular expression."""
filevar = variable + "file"
diff --git a/metomi/rose/apps/comparisons/prohibited.py b/metomi/rose/apps/comparisons/prohibited.py
index 7b45f731e8..82a796dc66 100644
--- a/metomi/rose/apps/comparisons/prohibited.py
+++ b/metomi/rose/apps/comparisons/prohibited.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -24,10 +21,9 @@
FAIL = "~~"
-class Prohibited(object):
+class Prohibited:
def run(self, task):
"""Perform an exact comparison between the result and the KGO data"""
- failures = 0
if len(task.resultdata) == 0:
task.set_pass(ProhibitedStringResult(task, PASS))
else:
@@ -35,7 +31,7 @@ def run(self, task):
return task
-class ProhibitedStringResult(object):
+class ProhibitedStringResult:
"""Result of prohibited text examination."""
diff --git a/metomi/rose/apps/comparisons/within.py b/metomi/rose/apps/comparisons/within.py
index d88f484124..c95412ba50 100644
--- a/metomi/rose/apps/comparisons/within.py
+++ b/metomi/rose/apps/comparisons/within.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -34,10 +31,9 @@
FAIL = ">"
-class Within(object):
+class Within:
def run(self, task):
"""Check that the results are within a specified tolerance."""
- failures = 0
if len(task.resultdata) != len(task.kgo1data):
raise DataLengthError(task)
val_num = 0
@@ -63,7 +59,7 @@ def run(self, task):
return task
-class WithinComparisonFailure(object):
+class WithinComparisonFailure:
"""Class used if results are not within a certain amount of the KGO"""
@@ -108,7 +104,7 @@ def __repr__(self):
__str__ = __repr__
-class WithinComparisonSuccess(object):
+class WithinComparisonSuccess:
"""Class used if results are within a certain amount of the KGO"""
diff --git a/metomi/rose/apps/fcm_make.py b/metomi/rose/apps/fcm_make.py
index f6098152ef..06502be45c 100644
--- a/metomi/rose/apps/fcm_make.py
+++ b/metomi/rose/apps/fcm_make.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# ----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -127,7 +124,13 @@ def _invoke_fcm_make(self, app_runner, conf_tree, opts, args, uuid, task,
if fast_root:
# N.B. Name in "little endian", like cycle task ID
prefix = ".".join([
- task.task_name, task.task_cycle_time, task.suite_name])
+ task.task_name,
+ task.task_cycle_time,
+ # suite_name may be a hierarchical registration which
+ # isn't a safe prefix
+ task.suite_name.replace(os.sep, '_')
+ ])
+ os.makedirs(fast_root, exist_ok=True)
dest = mkdtemp(prefix=prefix, dir=fast_root)
# N.B. Don't use app_runner.popen.get_cmd("rsync") as we are using
# "rsync" for a local copy.
@@ -173,6 +176,7 @@ def _run_orig(self, app_runner, conf_tree, opts, args, uuid, task,
# Determine the name of the continuation task
task_name_cont = task.task_name.replace(
orig_cont_map[ORIG], orig_cont_map[CONT])
+ # TODO: get_task_auth currently does nothing
auth = app_runner.suite_engine_proc.get_task_auth(
task.suite_name, task_name_cont)
if auth is not None:
diff --git a/metomi/rose/apps/rose_ana.py b/metomi/rose/apps/rose_ana.py
index 0aae1cae0d..07eafb5757 100644
--- a/metomi/rose/apps/rose_ana.py
+++ b/metomi/rose/apps/rose_ana.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -50,7 +47,7 @@ def timestamp():
return time.strftime("%H:%M:%S")
-class KGODatabase(object):
+class KGODatabase:
"""
KGO Database object, stores comparison information for metomi.rose_ana
apps.
@@ -173,14 +170,14 @@ class AnalysisTask(object, metaclass=abc.ABCMeta):
self.config:
A dictionary containing any Rose Ana configuration options.
self.reporter:
- A reference to the :py:class:`rose.reporter.Reporter` instance used
- by the parent app (for printing to stderr/stdout).
+ A reference to the :py:class:`metomi.rose.reporter.Reporter`
+ instance used by the parent app (for printing to stderr/stdout).
self.kgo_db:
A reference to the KGO database object created by the parent app
(for adding entries to the database).
self.popen:
- A reference to the :py:class:`rose.popen.RosePopener` instance
- used by the parent app (for spawning subprocesses).
+ A reference to the :py:class:`metomi.rose.popen.RosePopener`
+ instance used by the parent app (for spawning subprocesses).
"""
diff --git a/metomi/rose/apps/rose_ana_v1.py b/metomi/rose/apps/rose_ana_v1.py
index f55fe3e16e..6859c87fa3 100644
--- a/metomi/rose/apps/rose_ana_v1.py
+++ b/metomi/rose/apps/rose_ana_v1.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -49,7 +46,7 @@
USRCOMPARISON_EXT = ".py"
-class KGODatabase(object):
+class KGODatabase:
"""
KGO Database object, stores comparison information for rose_ana apps.
"""
@@ -239,7 +236,7 @@ def __repr__(self):
__str__ = __repr__
-class Analyse(object):
+class Analyse:
"""A comparison engine for Rose."""
@@ -388,7 +385,7 @@ def load_tasks(self):
tasks = []
for task in self.config.value.keys():
- if task is "env":
+ if task == "env":
continue
if task.startswith("file:"):
continue
@@ -497,7 +494,7 @@ def write_config(self, filename, tasks):
metomi.rose.config.dump(config, filename)
-class AnalysisTask(object):
+class AnalysisTask:
"""Class to completely describe an analysis task.
diff --git a/metomi/rose/apps/rose_arch.py b/metomi/rose/apps/rose_arch.py
index c31553b2b6..66edab7426 100644
--- a/metomi/rose/apps/rose_arch.py
+++ b/metomi/rose/apps/rose_arch.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# ----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -404,7 +401,7 @@ def _get_conf(self, r_node, t_node, key, compulsory=False, default=None):
return value
-class RoseArchTarget(object):
+class RoseArchTarget:
"""An archive target."""
@@ -435,7 +432,7 @@ def __ne__(self, other):
return not self.__eq__(other)
-class RoseArchSource(object):
+class RoseArchSource:
"""An archive source."""
@@ -457,7 +454,7 @@ def __ne__(self, other):
return not self.__eq__(other)
-class RoseArchDAO(object):
+class RoseArchDAO:
"""Data access object for incremental mode."""
diff --git a/metomi/rose/apps/rose_arch_compressions/__init__.py b/metomi/rose/apps/rose_arch_compressions/__init__.py
index 8460776f05..f7b71f8284 100644
--- a/metomi/rose/apps/rose_arch_compressions/__init__.py
+++ b/metomi/rose/apps/rose_arch_compressions/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/apps/rose_arch_compressions/rose_arch_gzip.py b/metomi/rose/apps/rose_arch_compressions/rose_arch_gzip.py
index 7826021181..c79bd9eac4 100644
--- a/metomi/rose/apps/rose_arch_compressions/rose_arch_gzip.py
+++ b/metomi/rose/apps/rose_arch_compressions/rose_arch_gzip.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -23,7 +20,7 @@
import os
-class RoseArchGzip(object):
+class RoseArchGzip:
"""Compress archive sources in gzip."""
diff --git a/metomi/rose/apps/rose_arch_compressions/rose_arch_tar.py b/metomi/rose/apps/rose_arch_compressions/rose_arch_tar.py
index 8a3f869039..dfdfd64bdb 100644
--- a/metomi/rose/apps/rose_arch_compressions/rose_arch_tar.py
+++ b/metomi/rose/apps/rose_arch_compressions/rose_arch_tar.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -24,7 +21,7 @@
from tempfile import mkstemp
-class RoseArchTarGzip(object):
+class RoseArchTarGzip:
"""Compress archive sources in tar."""
diff --git a/metomi/rose/apps/rose_bunch.py b/metomi/rose/apps/rose_bunch.py
index a7dbc6f44e..64091b6700 100644
--- a/metomi/rose/apps/rose_bunch.py
+++ b/metomi/rose/apps/rose_bunch.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# ----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -20,7 +17,6 @@
"""Builtin application: rose_bunch: run multiple commands in parallel.
"""
-
import itertools
import os
import shlex
@@ -372,7 +368,7 @@ def run(self, app_runner, conf_tree, opts, args, uuid, work_files):
return 0
-class RoseBunchCmd(object):
+class RoseBunchCmd:
"""A command instance to run."""
OUTPUT_TEMPLATE = "bunch.%s.%s"
@@ -406,7 +402,7 @@ def get_log_prefix(self):
return self.name
-class RoseBunchDAO(object):
+class RoseBunchDAO:
"""Database object for rose_bunch"""
TABLE_COMMANDS = "commands"
@@ -441,7 +437,7 @@ def connect(self):
def create_tables(self):
"""Create tables as appropriate"""
existing = []
- first_run = os.environ.get("CYLC_TASK_SUBMIT_NUMBER") == "1"
+ first_run = os.environ.get("CYLC_TASK_TRY_NUMBER") == "1"
for row in self.conn.execute("SELECT name FROM sqlite_master " +
"WHERE type=='table'"):
@@ -543,7 +539,15 @@ def same_prev_config(self, current):
unchanged = True
current = self.flatten_config(current)
for key, value in self.conn.execute(s_stmt):
- if key in current:
+ if key == 'env_PATH':
+ # due to re-invocation the PATH may have changed in-between
+ # runs - only re-run jobs if the PATH has changed in a way
+ # that could actually make a difference
+ if simplify_path(current[key]) != simplify_path(value):
+ break
+ else:
+ current.pop(key)
+ elif key in current:
if current[key] != value:
break
else:
@@ -554,3 +558,29 @@ def same_prev_config(self, current):
if current:
unchanged = False
return unchanged
+
+
+def simplify_path(path):
+ """Removes duplication in paths whilst maintaining integrity.
+
+ If duplicate items are present in a path this keeps the first item and
+ removes any subsequent duplicates.
+
+ Examples:
+ >>> simplify_path('')
+ ''
+ >>> simplify_path('a')
+ 'a'
+ >>> simplify_path('a:a:a')
+ 'a'
+ >>> simplify_path('a:b:a')
+ 'a:b'
+ >>> simplify_path('a:b:b:a')
+ 'a:b'
+ >>> simplify_path('a:b:a:b:c:d:a:b:c:d:e')
+ 'a:b:c:d:e'
+
+ """
+ return ':'.join(
+ dict.fromkeys(path.split(':')).keys()
+ )
diff --git a/metomi/rose/apps/rose_prune.py b/metomi/rose/apps/rose_prune.py
index 7da776a488..15481f1ff1 100644
--- a/metomi/rose/apps/rose_prune.py
+++ b/metomi/rose/apps/rose_prune.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# ----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/bush_dao.py b/metomi/rose/bush_dao.py
deleted file mode 100644
index fbe45abf9f..0000000000
--- a/metomi/rose/bush_dao.py
+++ /dev/null
@@ -1,696 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Rose Bush: data access to cylc suite runtime databases."""
-
-from fnmatch import fnmatch
-from glob import glob
-import os
-import re
-import tarfile
-
-from metomi.rose.suite_engine_procs.cylc import CylcProcessor, CylcSuiteDAO
-
-
-class RoseBushDAO(object):
-
- """Rose Bush: data access to cylc suite runtime databases."""
-
- CYCLE_ORDERS = {"time_desc": " DESC", "time_asc": " ASC"}
- JOB_ORDERS = {
- "time_desc": "time DESC, submit_num DESC, name DESC, cycle DESC",
- "time_asc": "time ASC, submit_num ASC, name ASC, cycle ASC",
- "cycle_desc_name_asc": "cycle DESC, name ASC, submit_num DESC",
- "cycle_desc_name_desc": "cycle DESC, name DESC, submit_num DESC",
- "cycle_asc_name_asc": "cycle ASC, name ASC, submit_num DESC",
- "cycle_asc_name_desc": "cycle ASC, name DESC, submit_num DESC",
- "name_asc_cycle_asc": "name ASC, cycle ASC, submit_num DESC",
- "name_desc_cycle_asc": "name DESC, cycle ASC, submit_num DESC",
- "name_asc_cycle_desc": "name ASC, cycle DESC, submit_num DESC",
- "name_desc_cycle_desc": "name DESC, cycle DESC, submit_num DESC",
- "time_submit_desc": (
- "time_submit DESC, submit_num DESC, name DESC, cycle DESC"),
- "time_submit_asc": (
- "time_submit ASC, submit_num DESC, name DESC, cycle DESC"),
- "time_run_desc": (
- "time_run DESC, submit_num DESC, name DESC, cycle DESC"),
- "time_run_asc": (
- "time_run ASC, submit_num DESC, name DESC, cycle DESC"),
- "time_run_exit_desc": (
- "time_run_exit DESC, submit_num DESC, name DESC, cycle DESC"),
- "time_run_exit_asc": (
- "time_run_exit ASC, submit_num DESC, name DESC, cycle DESC"),
- "duration_queue_desc": (
- "(CAST(strftime('%s', time_run) AS NUMERIC) -" +
- " CAST(strftime('%s', time_submit) AS NUMERIC)) DESC, " +
- "submit_num DESC, name DESC, cycle DESC"),
- "duration_queue_asc": (
- "(CAST(strftime('%s', time_run) AS NUMERIC) -" +
- " CAST(strftime('%s', time_submit) AS NUMERIC)) ASC, " +
- "submit_num DESC, name DESC, cycle DESC"),
- "duration_run_desc": (
- "(CAST(strftime('%s', time_run_exit) AS NUMERIC) -" +
- " CAST(strftime('%s', time_run) AS NUMERIC)) DESC, " +
- "submit_num DESC, name DESC, cycle DESC"),
- "duration_run_asc": (
- "(CAST(strftime('%s', time_run_exit) AS NUMERIC) -" +
- " CAST(strftime('%s', time_run) AS NUMERIC)) ASC, " +
- "submit_num DESC, name DESC, cycle DESC"),
- "duration_queue_run_desc": (
- "(CAST(strftime('%s', time_run_exit) AS NUMERIC) -" +
- " CAST(strftime('%s', time_submit) AS NUMERIC)) DESC, " +
- "submit_num DESC, name DESC, cycle DESC"),
- "duration_queue_run_asc": (
- "(CAST(strftime('%s', time_run_exit) AS NUMERIC) -" +
- " CAST(strftime('%s', time_submit) AS NUMERIC)) ASC, " +
- "submit_num DESC, name DESC, cycle DESC"),
- }
- JOB_STATUS_COMBOS = {
- "all": "",
- "submitted": "submit_status == 0 AND time_run IS NULL",
- "submitted,running": "submit_status == 0 AND run_status IS NULL",
- "submission-failed": "submit_status == 1",
- "submission-failed,failed": "submit_status == 1 OR run_status == 1",
- "running": "time_run IS NOT NULL AND run_status IS NULL",
- "running,succeeded,failed": "time_run IS NOT NULL",
- "succeeded": "run_status == 0",
- "succeeded,failed": "run_status IS NOT NULL",
- "failed": "run_status == 1",
- }
- REC_CYCLE_QUERY_OP = re.compile(r"\A(before |after |[<>]=?)(.+)\Z")
- REC_SEQ_LOG = re.compile(r"\A(.+\.)([^\.]+)(\.[^\.]+)\Z")
- SUITE_CONF = CylcProcessor.SUITE_CONF
- SUITE_DIR_REL_ROOT = CylcProcessor.SUITE_DIR_REL_ROOT
- TASK_STATUS_GROUPS = {
- "active": [
- "ready", "queued", "submitting", "submitted", "submit-retrying",
- "running", "retrying"],
- "fail": ["submission failed", "failed"],
- "success": ["expired", "succeeded"]}
- TASK_STATUSES = (
- "runahead", "waiting", "held", "queued", "ready", "expired",
- "submitted", "submit-failed", "submit-retrying", "running",
- "succeeded", "failed", "retrying")
-
- def __init__(self):
- self.daos = {}
-
- def get_suite_broadcast_states(self, user_name, suite_name):
- """Return broadcast states of a suite.
-
- [[point, name, key, value], ...]
-
- """
- # Check if "broadcast_states" table is available or not
- if not self._db_has_table(user_name, suite_name, "broadcast_states"):
- return
-
- broadcast_states = []
- for row in self._db_exec(
- user_name, suite_name,
- "SELECT point,namespace,key,value FROM broadcast_states" +
- " ORDER BY point ASC, namespace ASC, key ASC"):
- point, namespace, key, value = row
- broadcast_states.append([point, namespace, key, value])
- return broadcast_states
-
- def get_suite_broadcast_events(self, user_name, suite_name):
- """Return broadcast events of a suite.
-
- [[time, change, point, name, key, value], ...]
-
- """
- # Check if "broadcast_events" table is available or not
- if not self._db_has_table(user_name, suite_name, "broadcast_events"):
- return {}
-
- broadcast_events = []
- for row in self._db_exec(
- user_name, suite_name,
- "SELECT time,change,point,namespace,key,value" +
- " FROM broadcast_events" +
- " ORDER BY time DESC, point DESC, namespace DESC, key DESC"):
- time_, change, point, namespace, key, value = row
- broadcast_events.append(
- (time_, change, point, namespace, key, value))
- return broadcast_events
-
- @staticmethod
- def get_suite_dir_rel(suite_name, *paths):
- """Return the relative path to the suite running directory.
-
- paths -- if specified, are added to the end of the path.
- """
- return CylcProcessor.get_suite_dir_rel(suite_name, *paths)
-
- def get_suite_job_entries(
- self, user_name, suite_name, cycles, tasks, task_status,
- job_status, order, limit, offset):
- """Query suite runtime database to return a listing of task jobs.
-
- user -- A string containing a valid user ID
- suite -- A string containing a valid suite ID
- cycles -- If specified, display only task jobs matching these cycles.
- A value in the list can be a cycle, the string "before|after
- CYCLE", or a glob to match cycles.
- tasks -- If specified, display only jobs with task names matching
- these names. Values can be a valid task name or a glob like
- pattern for matching valid task names.
- task_status -- If specified, it should be a list of task statuses.
- Display only jobs in the specified list. If not
- specified, display all jobs.
- job_status -- If specified, must be a string matching a key in
- RoseBushDAO.JOB_STATUS_COMBOS. Select jobs by their
- statuses.
- order -- Order search in a predetermined way. A valid value is one of
- the keys in RoseBushDAO.ORDERS.
- limit -- Limit number of returned entries
- offset -- Offset entry number
-
- Return (entries, of_n_entries) where:
- entries -- A list of matching entries
- of_n_entries -- Total number of entries matching query
-
- Each entry is a dict:
- {"cycle": cycle, "name": name, "submit_num": submit_num,
- "events": [time_submit, time_init, time_exit],
- "task_status": task_status,
- "logs": {"script": {"path": path, "path_in_tar", path_in_tar,
- "size": size, "mtime": mtime},
- "out": {...},
- "err": {...},
- ...}}
- """
- where_expr, where_args = self._get_suite_job_entries_where(
- cycles, tasks, task_status, job_status)
-
- # Get number of entries
- of_n_entries = 0
- stmt = ("SELECT COUNT(*)" +
- " FROM task_jobs JOIN task_states USING (name, cycle)" +
- where_expr)
- for row in self._db_exec(user_name, suite_name, stmt, where_args):
- of_n_entries = row[0]
- break
- else:
- self._db_close(user_name, suite_name)
- return ([], 0)
-
- # Get entries
- entries = []
- entry_of = {}
- stmt = ("SELECT" +
- " task_states.time_updated AS time," +
- " cycle, name," +
- " task_jobs.submit_num AS submit_num," +
- " task_states.submit_num AS submit_num_max," +
- " task_states.status AS task_status," +
- " time_submit, submit_status," +
- " time_run, time_run_exit, run_signal, run_status," +
- " user_at_host, batch_sys_name, batch_sys_job_id" +
- " FROM task_jobs JOIN task_states USING (cycle, name)" +
- where_expr +
- " ORDER BY " +
- self.JOB_ORDERS.get(order, self.JOB_ORDERS["time_desc"]))
- limit_args = []
- if limit:
- stmt += " LIMIT ? OFFSET ?"
- limit_args = [limit, offset]
- for row in self._db_exec(
- user_name, suite_name, stmt, where_args + limit_args):
- (
- cycle, name, submit_num, submit_num_max, task_status,
- time_submit, submit_status,
- time_run, time_run_exit, run_signal, run_status,
- user_at_host, batch_sys_name, batch_sys_job_id
- ) = row[1:]
- entry = {
- "cycle": cycle,
- "name": name,
- "submit_num": submit_num,
- "submit_num_max": submit_num_max,
- "events": [time_submit, time_run, time_run_exit],
- "task_status": task_status,
- "submit_status": submit_status,
- "run_signal": run_signal,
- "run_status": run_status,
- "host": user_at_host,
- "submit_method": batch_sys_name,
- "submit_method_id": batch_sys_job_id,
- "logs": {},
- "seq_logs_indexes": {}}
- entries.append(entry)
- entry_of[(cycle, name, submit_num)] = entry
- self._db_close(user_name, suite_name)
- if entries:
- self._get_job_logs(user_name, suite_name, entries, entry_of)
- return (entries, of_n_entries)
-
- def _get_suite_job_entries_where(
- self, cycles, tasks, task_status, job_status):
- """Helper for get_suite_job_entries.
-
- Get query's "WHERE" expression and its arguments.
- """
- where_exprs = []
- where_args = []
- if cycles:
- cycle_where_exprs = []
- for cycle in cycles:
- match = self.REC_CYCLE_QUERY_OP.match(cycle)
- if match:
- operator, operand = match.groups()
- where_args.append(operand)
- if operator == "before ":
- cycle_where_exprs.append("cycle <= ?")
- elif operator == "after ":
- cycle_where_exprs.append("cycle >= ?")
- else:
- cycle_where_exprs.append("cycle %s ?" % operator)
- else:
- where_args.append(cycle)
- cycle_where_exprs.append("cycle GLOB ?")
- where_exprs.append(" OR ".join(cycle_where_exprs))
- if tasks:
- where_exprs.append(" OR ".join(["name GLOB ?"] * len(tasks)))
- where_args += tasks
- if task_status:
- task_status_where_exprs = []
- for item in task_status:
- task_status_where_exprs.append("task_states.status == ?")
- where_args.append(item)
- where_exprs.append(" OR ".join(task_status_where_exprs))
- try:
- job_status_where = self.JOB_STATUS_COMBOS[job_status]
- except KeyError:
- pass
- else:
- if job_status_where:
- where_exprs.append(job_status_where)
- if where_exprs:
- return (" WHERE (" + ") AND (".join(where_exprs) + ")", where_args)
- else:
- return ("", where_args)
-
- def _get_job_logs(self, user_name, suite_name, entries, entry_of):
- """Helper for "get_suite_job_entries". Get job logs.
-
- Recent job logs are likely to be in the file system, so we can get a
- listing of the relevant "log/job/CYCLE/NAME/SUBMI_NUM/" directory.
- Older job logs may be archived in "log/job-CYCLE.tar.gz", we should
- only open each relevant TAR file once to obtain a listing for all
- relevant entries of that cycle.
-
- Modify each entry in entries.
- """
- prefix = "~"
- if user_name:
- prefix += user_name
- user_suite_dir = os.path.expanduser(os.path.join(
- prefix, self.get_suite_dir_rel(suite_name)))
- try:
- fs_log_cycles = os.listdir(
- os.path.join(user_suite_dir, "log", "job"))
- except OSError:
- fs_log_cycles = []
- targzip_log_cycles = []
- for name in glob(os.path.join(user_suite_dir, "log", "job-*.tar.gz")):
- targzip_log_cycles.append(os.path.basename(name)[4:-7])
-
- relevant_targzip_log_cycles = []
- for entry in entries:
- if entry["cycle"] in fs_log_cycles:
- pathd = "log/job/%(cycle)s/%(name)s/%(submit_num)02d" % entry
- try:
- filenames = os.listdir(os.path.join(user_suite_dir, pathd))
- except OSError:
- continue
- for filename in filenames:
- try:
- stat = os.stat(
- os.path.join(user_suite_dir, pathd, filename))
- except OSError:
- pass
- else:
- entry["logs"][filename] = {
- "path": "/".join([pathd, filename]),
- "path_in_tar": None,
- "mtime": int(stat.st_mtime), # int precise enough
- "size": stat.st_size,
- "exists": True,
- "seq_key": None}
- continue
- if entry["cycle"] in targzip_log_cycles:
- if entry["cycle"] not in relevant_targzip_log_cycles:
- relevant_targzip_log_cycles.append(entry["cycle"])
-
- for cycle in relevant_targzip_log_cycles:
- path = os.path.join("log", "job-%s.tar.gz" % cycle)
- tar = tarfile.open(os.path.join(user_suite_dir, path), "r:gz")
- for member in tar.getmembers():
- # member.name expected to be "job/cycle/task/submit_num/*"
- if not member.isfile():
- continue
- try:
- cycle_str, name, submit_num_str = (
- member.name.split("/", 4)[1:4])
- entry = entry_of[(cycle_str, name, int(submit_num_str))]
- except (KeyError, ValueError):
- continue
- entry["logs"][os.path.basename(member.name)] = {
- "path": path,
- "path_in_tar": member.name,
- "mtime": int(member.mtime), # too precise otherwise
- "size": member.size,
- "exists": True,
- "seq_key": None}
-
- # Sequential logs
- for entry in entries:
- for filename, filename_items in entry["logs"].items():
- seq_log_match = self.REC_SEQ_LOG.match(filename)
- if not seq_log_match:
- continue
- head, index_str, tail = seq_log_match.groups()
- seq_key = head + "*" + tail
- filename_items["seq_key"] = seq_key
- if seq_key not in entry["seq_logs_indexes"]:
- entry["seq_logs_indexes"][seq_key] = {}
- entry["seq_logs_indexes"][seq_key][index_str] = filename
- for seq_key, indexes in entry["seq_logs_indexes"].items():
- # Only one item, not a sequence
- if len(indexes) <= 1:
- entry["seq_logs_indexes"].pop(seq_key)
- # All index_str are numbers, convert key to integer so
- # the template can sort them as numbers
- try:
- int_indexes = {}
- for index_str, filename in indexes.items():
- int_indexes[int(index_str)] = filename
- entry["seq_logs_indexes"][seq_key] = int_indexes
- except ValueError:
- pass
- for filename, log_dict in entry["logs"].items():
- # Unset seq_key for singular items
- if log_dict["seq_key"] not in entry["seq_logs_indexes"]:
- log_dict["seq_key"] = None
-
- def get_suite_logs_info(self, user_name, suite_name):
- """Return the information of the suite logs.
-
- Return a tuple that looks like:
- ("cylc-run",
- {"err": {"path": "log/suite/err", "mtime": mtime, "size": size},
- "log": {"path": "log/suite/log", "mtime": mtime, "size": size},
- "out": {"path": "log/suite/out", "mtime": mtime, "size": size}})
-
- """
- logs_info = {}
- prefix = "~"
- if user_name:
- prefix += user_name
- d_rel = self.get_suite_dir_rel(suite_name)
- dir_ = os.path.expanduser(os.path.join(prefix, d_rel))
- # Get cylc files.
- cylc_files = ["cylc-suite-env", "suite.rc", "suite.rc.processed"]
- for key in cylc_files:
- f_name = os.path.join(dir_, key)
- if os.path.isfile(f_name):
- f_stat = os.stat(f_name)
- logs_info[key] = {"path": key,
- "mtime": f_stat.st_mtime,
- "size": f_stat.st_size}
- # Get cylc suite log files.
- log_files = ["log/suite/err", "log/suite/log", "log/suite/out"]
- for key in log_files:
- f_name = os.path.join(dir_, key)
- if os.path.isfile(f_name):
- try:
- link_path = os.readlink(f_name)
- except OSError:
- link_path = f_name
- old_logs = [] # Old log naming system.
- new_logs = [] # New log naming system.
- # TODO: Post migration to cylc this logic can be replaced by:
- # `from cylc.suite_logging import get_logs` (superior)
- for log in glob(f_name + '.*'):
- log_name = os.path.basename(log)
- if log_name == link_path:
- continue
- if len(log_name.split('.')[1]) > 3:
- new_logs.append(os.path.join("log", "suite", log_name))
- else:
- old_logs.append(os.path.join("log", "suite", log_name))
- new_logs.sort(reverse=True)
- old_logs.sort()
- f_stat = os.stat(f_name)
- logs_info[key] = {"path": key,
- "paths": [key] + new_logs + old_logs,
- "mtime": f_stat.st_mtime,
- "size": f_stat.st_size}
- return ("cylc", logs_info)
-
- def get_suite_cycles_summary(
- self, user_name, suite_name, order, limit, offset):
- """Return a the state summary (of each cycle) of a user's suite.
-
- user -- A string containing a valid user ID
- suite -- A string containing a valid suite ID
- limit -- Limit number of returned entries
- offset -- Offset entry number
-
- Return (entries, of_n_entries), where entries is a data structure that
- looks like:
- [ { "cycle": cycle,
- "n_states": {
- "active": N, "success": M, "fail": L, "job_fails": K,
- },
- "max_time_updated": T2,
- },
- # ...
- ]
- where:
- * cycle is a date-time cycle label
- * N, M, L, K are the numbers of tasks in given states
- * T2 is the time when last update time of (a task in) the cycle
-
- and of_n_entries is the total number of entries.
-
- """
- of_n_entries = 0
- stmt = ("SELECT COUNT(DISTINCT cycle) FROM task_states WHERE " +
- "submit_num > 0")
- for row in self._db_exec(user_name, suite_name, stmt):
- of_n_entries = row[0]
- break
- if not of_n_entries:
- return ([], 0)
-
- # Not strictly correct, if cycle is in basic date-only format,
- # but should not matter for most cases
- integer_mode = False
- stmt = "SELECT cycle FROM task_states LIMIT 1"
- for row in self._db_exec(user_name, suite_name, stmt):
- integer_mode = row[0].isdigit()
- break
-
- prefix = "~"
- if user_name:
- prefix += user_name
- user_suite_dir = os.path.expanduser(os.path.join(
- prefix, self.get_suite_dir_rel(suite_name)))
- targzip_log_cycles = []
- try:
- for item in os.listdir(os.path.join(user_suite_dir, "log")):
- if item.startswith("job-") and item.endswith(".tar.gz"):
- targzip_log_cycles.append(item[4:-7])
- except OSError:
- pass
-
- states_stmt = {}
- for key, names in self.TASK_STATUS_GROUPS.items():
- states_stmt[key] = " OR ".join(
- ["status=='%s'" % (name) for name in names])
- stmt = (
- "SELECT" +
- " cycle," +
- " max(time_updated)," +
- " sum(" + states_stmt["active"] + ") AS n_active," +
- " sum(" + states_stmt["success"] + ") AS n_success,"
- " sum(" + states_stmt["fail"] + ") AS n_fail"
- " FROM task_states" +
- " GROUP BY cycle")
- if integer_mode:
- stmt += " ORDER BY cast(cycle as number)"
- else:
- stmt += " ORDER BY cycle"
- stmt += self.CYCLE_ORDERS.get(order, self.CYCLE_ORDERS["time_desc"])
- stmt_args = []
- if limit:
- stmt += " LIMIT ? OFFSET ?"
- stmt_args += [limit, offset]
- entry_of = {}
- entries = []
- for row in self._db_exec(user_name, suite_name, stmt, stmt_args):
- cycle, max_time_updated, n_active, n_success, n_fail = row
- if n_active or n_success or n_fail:
- entry_of[cycle] = {
- "cycle": cycle,
- "has_log_job_tar_gz": cycle in targzip_log_cycles,
- "max_time_updated": max_time_updated,
- "n_states": {
- "active": n_active,
- "success": n_success,
- "fail": n_fail,
- "job_active": 0,
- "job_success": 0,
- "job_fail": 0,
- },
- }
- entries.append(entry_of[cycle])
-
- # Check if "task_jobs" table is available or not.
- # Note: A single query with a JOIN is probably a more elegant solution.
- # However, timing tests suggest that it is cheaper with 2 queries.
- # This 2nd query may return more results than is necessary, but should
- # be a very cheap query as it does not have to do a lot of work.
- if self._db_has_table(user_name, suite_name, "task_jobs"):
- stmt = (
- "SELECT cycle," +
- " sum(" + self.JOB_STATUS_COMBOS["submitted,running"] +
- ") AS n_job_active," +
- " sum(" + self.JOB_STATUS_COMBOS["succeeded"] +
- ") AS n_job_success," +
- " sum(" + self.JOB_STATUS_COMBOS["submission-failed,failed"] +
- ") AS n_job_fail" +
- " FROM task_jobs GROUP BY cycle")
- else:
- fail_events_stmt = " OR ".join(
- ["event=='%s'" % (name)
- for name in self.TASK_STATUS_GROUPS["fail"]])
- stmt = (
- "SELECT cycle," +
- " sum(" + fail_events_stmt + ") AS n_job_fail" +
- " FROM task_events GROUP BY cycle")
- for cycle, n_job_active, n_job_success, n_job_fail in self._db_exec(
- user_name, suite_name, stmt):
- try:
- entry_of[cycle]["n_states"]["job_active"] = n_job_active
- entry_of[cycle]["n_states"]["job_success"] = n_job_success
- entry_of[cycle]["n_states"]["job_fail"] = n_job_fail
- except KeyError:
- pass
- else:
- del entry_of[cycle]
- if not entry_of:
- break
- self._db_close(user_name, suite_name)
-
- return entries, of_n_entries
-
- def get_suite_state_summary(self, user_name, suite_name):
- """Return a the state summary of a user's suite.
-
- Return {"is_running": b, "is_failed": b, "server": s}
- where:
- * is_running is a boolean to indicate if the suite is running
- * is_failed: a boolean to indicate if any tasks (submit) failed
- * server: host:port of server, if available
-
- """
- ret = {
- "is_running": False,
- "is_failed": False,
- "server": None}
- dao = self._db_init(user_name, suite_name)
- if not os.access(dao.db_f_name, os.F_OK | os.R_OK):
- return ret
-
- port_file_path = os.path.expanduser(
- os.path.join(
- "~" + user_name, "cylc-run", suite_name, ".service",
- "contact"))
- try:
- host = None
- port_str = None
- for line in open(port_file_path):
- key, value = [item.strip() for item in line.split("=", 1)]
- if key == "CYLC_SUITE_HOST":
- host = value
- elif key == "CYLC_SUITE_PORT":
- port_str = value
- except (IOError, ValueError):
- pass
- else:
- if host and port_str:
- ret["is_running"] = True
- ret["server"] = host.split(".", 1)[0] + ":" + port_str
-
- stmt = "SELECT status FROM task_states WHERE status GLOB ? LIMIT 1"
- stmt_args = ["*failed"]
- for _ in self._db_exec(user_name, suite_name, stmt, stmt_args):
- ret["is_failed"] = True
- break
- self._db_close(user_name, suite_name)
-
- return ret
-
- @staticmethod
- def is_conf(path):
- """Return "cylc-suite-rc" if path is a Cylc suite.rc file."""
- if fnmatch(os.path.basename(path), "suite*.rc*"):
- return "cylc-suite-rc"
-
- @classmethod
- def parse_job_log_rel_path(cls, f_name):
- """Return (cycle, task, submit_num, ext)."""
- return CylcProcessor.parse_job_log_rel_path(f_name)
-
- def _db_close(self, user_name, suite_name):
- """Close a named database connection."""
- key = (user_name, suite_name)
- if self.daos.get(key) is not None:
- self.daos[key].close()
-
- def _db_exec(self, user_name, suite_name, stmt, stmt_args=None):
- """Execute a query on a named database connection."""
- daos = self._db_init(user_name, suite_name)
- return daos.execute(stmt, stmt_args)
-
- def _db_has_table(self, user_name, suite_name, table_name):
- """Return True if table_name exists in the suite database."""
- cursor = self._db_exec(
- user_name, suite_name,
- "SELECT name FROM sqlite_master WHERE name==?", [table_name])
- return cursor.fetchone() is not None
-
- def _db_init(self, user_name, suite_name):
- """Initialise a named database connection."""
- key = (user_name, suite_name)
- if key not in self.daos:
- prefix = "~"
- if user_name:
- prefix += user_name
- for name in [os.path.join("log", "db"), "cylc-suite.db"]:
- db_f_name = os.path.expanduser(os.path.join(
- prefix, self.get_suite_dir_rel(suite_name, name)))
- self.daos[key] = CylcSuiteDAO(db_f_name)
- if os.path.exists(db_f_name):
- break
- return self.daos[key]
diff --git a/metomi/rose/c3.py b/metomi/rose/c3.py
index d11d6ad708..bfa216d7bb 100644
--- a/metomi/rose/c3.py
+++ b/metomi/rose/c3.py
@@ -1,8 +1,5 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -105,7 +102,7 @@ def mro(target_name, get_base_names, *args, **kwargs):
return results[target_name]
-class _Test(object):
+class _Test:
"""Self tests. Print results in TAP format.
diff --git a/metomi/rose/checksum.py b/metomi/rose/checksum.py
index 264e43edc3..4d74216675 100644
--- a/metomi/rose/checksum.py
+++ b/metomi/rose/checksum.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/cmp_source_vc.py b/metomi/rose/cmp_source_vc.py
deleted file mode 100644
index e08180995c..0000000000
--- a/metomi/rose/cmp_source_vc.py
+++ /dev/null
@@ -1,113 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Tool to determine whether source of an installed suite has changed."""
-
-from difflib import unified_diff
-import os
-from io import StringIO
-import sys
-import traceback
-
-from metomi.rose.opt_parse import RoseOptionParser
-from metomi.rose.popen import RosePopener
-from metomi.rose.reporter import Reporter
-from metomi.rose.run_source_vc import write_source_vc_info
-from metomi.rose.suite_engine_proc import SuiteEngineProcessor
-
-
-class SuiteVCComparator(object):
- """Tool to determine whether source of an installed suite has changed."""
-
- def __init__(self, event_handler=None):
- self.event_handler = event_handler
- self.popen = RosePopener(self.event_handler)
- self.suite_engine_proc = SuiteEngineProcessor.get_processor(
- event_handler=self.event_handler, popen=self.popen)
-
- def cmp_source_vc_info(self, suite_name):
- """Compare source VC with installed "log/rose-suite-run.version".
-
- Return (list): Result in unified diff format or None if irrelevant.
-
- Args:
- suite_name (str): suite name.
- """
- rund = self.suite_engine_proc.get_suite_dir(suite_name)
- old_info_file_name = self.suite_engine_proc.get_suite_dir(
- suite_name, 'log', 'rose-suite-run.version')
- try:
- old_info = open(old_info_file_name).read().splitlines()
- except IOError: # Cannot find/read version file
- return None
- else:
- if len(old_info) <= 1: # No VC information
- return None
- handle = StringIO()
- write_source_vc_info(old_info[0], handle, self.popen)
- new_info = handle.getvalue().splitlines()
- return unified_diff(
- old_info, new_info,
- "installed @ %s" % rund, "latest @ %s" % old_info[0])
-
- def handle_event(self, *args, **kwargs):
- """Handle event."""
- if callable(self.event_handler):
- self.event_handler(*args, **kwargs)
-
-
-def main():
- """Launcher for the CLI."""
- opt_parser = RoseOptionParser()
- opt_parser.add_my_options('name')
- opts, args = opt_parser.parse_args(sys.argv[1:])
- event_handler = Reporter(opts.verbosity - opts.quietness)
- suite_vc_cmp = SuiteVCComparator(event_handler)
- suite_name = opts.name
- if not suite_name and args:
- suite_name = args[0]
- if not suite_name:
- suite_name = os.getenv(suite_vc_cmp.suite_engine_proc.SUITE_NAME_ENV)
- if not suite_name:
- opt_parser.print_usage(sys.stderr)
- sys.exit(2)
- try:
- lines = suite_vc_cmp.cmp_source_vc_info(suite_name=suite_name)
- except Exception as exc:
- event_handler(exc)
- traceback.print_exc()
- sys.exit(2)
- else:
- if lines is None:
- event_handler(
- '%s: rose-suite-run.version: VC info not found' % (
- suite_name),
- kind=Reporter.KIND_ERR, level=Reporter.FAIL)
- sys.exit(2)
- lines = list(line for line in lines)
- for line in lines:
- event_handler('%s\n' % line, prefix='')
- if lines:
- sys.exit(1)
- else:
- sys.exit(0)
-
-
-if __name__ == "__main__":
- main()
diff --git a/metomi/rose/config.py b/metomi/rose/config.py
index 2999deed24..8ae578af12 100644
--- a/metomi/rose/config.py
+++ b/metomi/rose/config.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -114,7 +111,7 @@
OPT_CONFIG_SETTING_COMMENT = " setting from opt config \"%s\" (%s)"
-class ConfigNode(object):
+class ConfigNode:
"""Represent a node in a configuration file.
@@ -302,10 +299,10 @@ def get(self, keys=None, no_ignore=False):
"""Return a node at the position of keys, if any.
Args:
- keys (list, optional): A list defining a hierarchy of
+ keys (list): A list defining a hierarchy of
node.value 'keys'. If an entry in keys is the null
string, it is skipped.
- no_ignore (bool, optional): If True any ignored nodes will
+ no_ignore (bool): If True any ignored nodes will
not be returned.
Returns:
@@ -384,12 +381,12 @@ def get_value(self, keys=None, default=None):
If the node does not exist or is ignored, return None.
Args:
- keys (list, optional): A list defining a hierarchy of node.value
+ keys (list): A list defining a hierarchy of node.value
'keys'. If an entry in keys is the null string, it is skipped.
- default (obj, optional): Return default if the value is not set.
+ default (object): Return default if the value is not set.
Returns:
- obj: The value of this ConfigNode at the position of keys or
+ object: The value of this ConfigNode at the position of keys or
default if not set.
Examples:
@@ -428,7 +425,7 @@ def set(self, keys=None, value=None, state=None, comments=None):
Arguments:
keys (list): A list defining a hierarchy of node.value 'keys'.
If an entry in keys is the null string, it is skipped.
- value (obj): The node.value property to set at this position.
+ value (object): The node.value property to set at this position.
state (str): The node.state property to set at this position.
If None, the node.state property is unchanged.
comments (str): The node.comments property to set at this position.
@@ -619,7 +616,8 @@ def __sub__(self, other_config_node):
"""Produce a ConfigNodeDiff from another ConfigNode.
Arguments:
- other_config_node - The ConfigNode to be applied to this ConfigNode
+ other_config_node (ConfigNode):
+ The ConfigNode to be applied to this ConfigNode
to produce the ConfigNodeDiff.
Returns:
@@ -661,7 +659,7 @@ def __setstate__(self, state):
self.comments = state["comments"]
-class ConfigNodeDiff(object):
+class ConfigNodeDiff:
"""Represent differences between two ConfigNode instances.
@@ -792,9 +790,12 @@ def set_added_setting(self, keys, data):
"""Set a config setting to be "added" in this ConfigNodeDiff.
Args:
- keys (list/tuple): The position of the setting to add.
- data (obj, str, str): A tuple (value, state, comments) for the
- setting to add.
+ keys (list, tuple):
+ The position of the setting to add.
+ data (tuple):
+ A tuple of the form
+ ``(value: object, state: string, comments: string)``
+ for the setting to add.
Examples:
>>> config_node_diff = ConfigNodeDiff()
@@ -823,11 +824,14 @@ def set_modified_setting(self, keys, old_data, data):
None then no change will be made to any pre-existing value.
Args:
- keys (list/tuple): The position of the setting to add.
- old_data (obj, str, str): A tuple (value, state, comments) for
- the "current" properties of the setting to modify.
- data (obj, str, str): A tuple (value, state, comments) for "new"
- properties to change this setting to.
+ keys (list, tuple):
+ The position of the setting to add.
+ old_data (tuple):
+ A tuple ``(value: object, state: str, comments: str)``
+ for the "current" properties of the setting to modify.
+ data (object):
+ A tuple ``(value: object, state: str, comments: str)``
+ for "new" properties to change this setting to.
Examples:
>>> # Create a ConfigNodeDiff.
@@ -853,8 +857,10 @@ def set_removed_setting(self, keys, data):
"""Set a config setting to be "removed" in this ConfigNodeDiff.
Arguments:
- keys (list): The position of the setting to add.
- data (obj, str, str): A tuple (value, state, comments) of the
+ keys (list):
+ The position of the setting to add.
+ data (tuple):
+ A tuple ``(value: object, state: str, comments: str)`` of the
properties for the setting to remove.
Example:
@@ -929,10 +935,10 @@ def get_removed(self):
set to None for sections.
Returns:
- list - A list of the form [(keys, data), ...]:
- - keys - The position of an added setting.
- - data - Tuple of the form (value, state, comments) of the
- properties of the removed setting.
+ list: A list of the form ``[(keys, data), ...]``:
+ - keys - The position of an added setting.
+ - data - Tuple of the form (value, state, comments) of the
+ properties of the removed setting.
Examples:
>>> config_node_diff = ConfigNodeDiff()
@@ -1007,7 +1013,7 @@ def delete_removed(self):
self._data[self.KEY_REMOVED] = {}
-class ConfigDumper(object):
+class ConfigDumper:
"""Dumper of a ConfigNode object in Rose INI format.
@@ -1040,17 +1046,17 @@ def dump(self, root, target=sys.stdout, sort_sections=None,
Args:
root (ConfigNode): The root config node.
- target (str/file): An open file handle or a string containing a
+ target (object): An open file handle or a string containing a
file path. If not specified, the result is written to
sys.stdout.
- sort_sections (fcn - optional): An optional argument that should be
+ sort_sections (Callable): An optional argument that should be
a function for sorting a list of section keys.
- sort_option_items (fcn - optional): An optional argument that
+ sort_option_items (Callable): An optional argument that
should be a function for sorting a list of option (key, value)
tuples in string values.
- env_escape_ok (bool - optional): An optional argument to indicate
+ env_escape_ok (bool): An optional argument to indicate
that $NAME and ${NAME} syntax in values should be escaped.
- concat_mode (bool - optional): Switch on concatenation mode. If
+ concat_mode (bool): Switch on concatenation mode. If
True, add [] before root level options.
"""
@@ -1148,7 +1154,7 @@ def _comment_format(cls, comment):
return "#%s\n" % (comment)
-class ConfigLoader(object):
+class ConfigLoader:
"""Loader of an INI format configuration into a ConfigNode object.
@@ -1210,25 +1216,25 @@ def load_with_opts(self, source, node=None, more_keys=None,
Arguments:
source (str): A file path.
- node (ConfigNode - optional): A ConfigNode object if specified,
+ node (ConfigNode): A ConfigNode object if specified,
otherwise one is created.
- more_keys (list - optional): A list of additional optional
+ more_keys (list): A list of additional optional
configuration names. If source is "rose-${TYPE}.conf", the
file of each name should be "opt/rose-${TYPE}-${NAME}.conf".
- used_keys (list - optional): If defined, it should be a list for
+ used_keys (list): If defined, it should be a list for
this method to append to. The key of each successfully loaded
optional configuration will be appended to the list (unless the
key is already in the list). Missing optional configurations
that are specified in more_keys will not raise an error.
If not defined, any missing optional configuration will
trigger an OSError.
- mark_opt_configs (bool - optional): if True, add comments above any
+ mark_opt_configs (bool): if True, add comments above any
settings which have been loaded from an optional config.
- return_config_map (bool - optional): If True, construct and return
+ return_config_map (bool): If True, construct and return
a dict (config_map) containing config names vs their uncombined
nodes. Optional configurations use their opt keys as keys, and
the main configuration uses 'None'.
- defines (list - optional): A list of [SECTION]KEY=VALUE overrides.
+ defines (list): A list of [SECTION]KEY=VALUE overrides.
Returns:
tuple: node or (node, config_map):
diff --git a/metomi/rose/config_cli.py b/metomi/rose/config_cli.py
index 4617118d50..864afb9dcd 100644
--- a/metomi/rose/config_cli.py
+++ b/metomi/rose/config_cli.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/config_diff.py b/metomi/rose/config_diff.py
index cd11f7d6d6..03d1da6693 100644
--- a/metomi/rose/config_diff.py
+++ b/metomi/rose/config_diff.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# ----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -36,7 +33,7 @@
import metomi.rose.run
-class ConfigDiffDefaults(object):
+class ConfigDiffDefaults:
"""Store default settings for the rose config-diff command."""
diff --git a/metomi/rose/config_dump.py b/metomi/rose/config_dump.py
index 48feaf0cba..3718b53794 100644
--- a/metomi/rose/config_dump.py
+++ b/metomi/rose/config_dump.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/config_processor.py b/metomi/rose/config_processor.py
index c29abe61f5..133cbbc9a8 100644
--- a/metomi/rose/config_processor.py
+++ b/metomi/rose/config_processor.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -67,7 +64,7 @@ def __str__(self):
return "%s: %s" % (setting_str, e_str)
-class ConfigProcessorBase(object):
+class ConfigProcessorBase:
"""Base class for a config processor."""
diff --git a/metomi/rose/config_processors/__init__.py b/metomi/rose/config_processors/__init__.py
index 8460776f05..f7b71f8284 100644
--- a/metomi/rose/config_processors/__init__.py
+++ b/metomi/rose/config_processors/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/config_processors/empy.py b/metomi/rose/config_processors/empy.py
deleted file mode 100644
index c14cd1a3e7..0000000000
--- a/metomi/rose/config_processors/empy.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Process a section in a metomi.rose.config.ConfigNode into a EmPy template.
-"""
-
-
-from metomi.rose.config_processors.jinja2 import ConfigProcessorForJinja2
-
-
-class ConfigProcessorForEmPy(ConfigProcessorForJinja2):
-
- """Processor for [empy:FILE] sections in a runtime configuration."""
-
- SCHEME = "empy"
- ASSIGN_TEMPL = "@{%s=%s}@\n"
- COMMENT_TEMPL = "@# %s\n"
-
-
-del ConfigProcessorForJinja2 # avoid loading it more than once
diff --git a/metomi/rose/config_processors/env.py b/metomi/rose/config_processors/env.py
index 11bcd9deb6..0e9cb4310c 100644
--- a/metomi/rose/config_processors/env.py
+++ b/metomi/rose/config_processors/env.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/config_processors/fileinstall.py b/metomi/rose/config_processors/fileinstall.py
index 5f366635d8..2e2ef2202f 100644
--- a/metomi/rose/config_processors/fileinstall.py
+++ b/metomi/rose/config_processors/fileinstall.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# ----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -20,27 +17,34 @@
"""Process "file:*" sections in node of a metomi.rose.config_tree.ConfigTree.
"""
+import aiofiles
from fnmatch import fnmatch
from glob import glob
+from io import BytesIO
import os
+import shlex
+from shutil import rmtree
+import sqlite3
+import sys
+from tempfile import mkdtemp
+from typing import Any, Optional
+from urllib.parse import urlparse
+
from metomi.rose.checksum import (
- get_checksum, get_checksum_func, guess_checksum_algorithm)
-from metomi.rose.config_processor import (ConfigProcessError,
- ConfigProcessorBase)
+ get_checksum,
+ get_checksum_func,
+ guess_checksum_algorithm
+)
+from metomi.rose.config_processor import (
+ ConfigProcessError,
+ ConfigProcessorBase,
+)
from metomi.rose.env import env_var_process, UnboundEnvironmentVariableError
from metomi.rose.fs_util import FileSystemUtil
from metomi.rose.job_runner import JobManager, JobProxy, JobRunner
from metomi.rose.popen import RosePopener
from metomi.rose.reporter import Event
from metomi.rose.scheme_handler import SchemeHandlersManager
-import shlex
-from shutil import rmtree
-import sqlite3
-from io import BytesIO
-import sys
-from tempfile import mkdtemp
-from urllib.parse import urlparse
-import aiofiles
class ConfigProcessorForFile(ConfigProcessorBase):
@@ -256,19 +260,25 @@ def _process(self, conf_tree, nodes, loc_dao, **kwargs):
# See if any sources have changed names.
if not target.is_out_of_date:
conn = loc_dao.get_conn()
- try:
- prev_dep_locs = conn.execute(
- "SELECT * FROM dep_names WHERE name=?", [target.name]
- ).fetchall()
- prev_dep_locs = [i[1] for i in prev_dep_locs]
- prev_dep_locs = [loc_dao.select(i) for i in prev_dep_locs]
- if (
- [i.name for i in prev_dep_locs] !=
- [i.name for i in target.dep_locs]
- ):
- target.is_out_of_date = True
- finally:
- conn.close()
+ prev_dep_locs = conn.execute(
+ """
+ SELECT *
+ FROM dep_names
+ WHERE name=?
+ ORDER BY ROWID
+ """,
+ [target.name]
+ ).fetchall()
+ prev_dep_locs = [i[1] for i in prev_dep_locs]
+ prev_dep_locs = [
+ loc_dao.select(i)
+ for i in prev_dep_locs
+ ]
+ if (
+ [i.name for i in prev_dep_locs] !=
+ [i.name for i in target.dep_locs]
+ ):
+ target.is_out_of_date = True
# See if any sources out of date
if not target.is_out_of_date:
for dep_loc in target.dep_locs:
@@ -486,7 +496,7 @@ def __str__(self):
return str(self.args[0])
-class Loc(object):
+class Loc:
"""Represent a location.
@@ -576,13 +586,22 @@ def __str__(self):
return "%s <= %s, expected %s, got %s" % self.args
-class LocSubPath(object):
- """Represent a sub-path in a location."""
+class LocSubPath:
+ """Represent a sub-path in a location.
+
+ Attrs:
+ name:
+ Path name.
+ checksum:
+ Computed checksum value.
+ access_mode:
+ File type and mode bits (see os.stat_result:st_mode).
+ """
def __init__(self, name, checksum=None, access_mode=None):
- self.name = name
- self.checksum = checksum
- self.access_mode = access_mode
+ self.name: str = name
+ self.checksum: Any = checksum
+ self.access_mode: Optional[int] = access_mode
def __lt__(self, other):
return (
@@ -603,7 +622,7 @@ def __str__(self):
return self.name
-class LocDAO(object):
+class LocDAO:
"""DAO for information for incremental updates."""
FILE_NAME = ".rose-config_processors-file.db"
diff --git a/metomi/rose/config_processors/jinja2.py b/metomi/rose/config_processors/jinja2.py
deleted file mode 100644
index b9a964b749..0000000000
--- a/metomi/rose/config_processors/jinja2.py
+++ /dev/null
@@ -1,128 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Process a section in a metomi.rose.config.ConfigNode into a Jinja2 template.
-"""
-
-import filecmp
-from metomi.rose.config_processor import (
- ConfigProcessError, ConfigProcessorBase)
-from metomi.rose.env import env_var_process, UnboundEnvironmentVariableError
-from metomi.rose.fs_util import FileSystemEvent
-import os
-from tempfile import NamedTemporaryFile
-
-
-class ConfigProcessorForJinja2(ConfigProcessorBase):
-
- """Processor for [jinja2:FILE] sections in a runtime configuration."""
-
- SCHEME = "jinja2"
- ASSIGN_TEMPL = "{%% set %s=%s %%}\n"
- COMMENT_TEMPL = "{# %s #}\n"
- SCHEME_TEMPL = "#!%s\n"
- MSG_DONE = "Rose Configuration Insertion: Done"
- MSG_INIT = "Rose Configuration Insertion: Init"
-
- def process(self, conf_tree, item, orig_keys=None, orig_value=None,
- **kwargs):
- """Process [jinja2:*] in "conf_tree.node".
-
- Arguments:
- conf_tree:
- The relevant metomi.rose.config_tree.ConfigTree object with the
- full configuration.
- item: The current configuration item to process.
- orig_keys:
- The keys for locating the originating setting in conf_tree in a
- recursive processing. None implies a top level call.
- orig_value: The value of orig_keys in conf_tree.
- **kwargs:
- environ (dict): suite level environment variables.
- """
- for s_key, s_node in sorted(conf_tree.node.value.items()):
- if (s_node.is_ignored() or
- not s_key.startswith(self.PREFIX) or
- not s_node.value):
- continue
- target = s_key[len(self.PREFIX):]
- source = os.path.join(conf_tree.files[target], target)
- if not os.access(source, os.F_OK | os.R_OK):
- continue
- scheme_ln = self.SCHEME_TEMPL % self.SCHEME
- msg_init_ln = self.COMMENT_TEMPL % self.MSG_INIT
- msg_done_ln = self.COMMENT_TEMPL % self.MSG_DONE
- tmp_file = NamedTemporaryFile()
- tmp_file.write(scheme_ln.encode('UTF-8'))
- tmp_file.write(msg_init_ln.encode('UTF-8'))
- suite_variables = ['{']
- for key, node in sorted(s_node.value.items()):
- if node.is_ignored():
- continue
- try:
- value = env_var_process(node.value)
- except UnboundEnvironmentVariableError as exc:
- raise ConfigProcessError([s_key, key], node.value, exc)
- tmp_file.write(
- (self.ASSIGN_TEMPL % (key, value)).encode('UTF-8'))
- suite_variables.append(" '%s': %s," % (key, key))
- suite_variables.append('}')
- suite_variables = self.ASSIGN_TEMPL % ('ROSE_SUITE_VARIABLES',
- '\n'.join(suite_variables))
- tmp_file.write(suite_variables.encode('UTF-8'))
- environ = kwargs.get("environ")
- if environ:
- tmp_file.write('[cylc]\n'.encode('UTF-8'))
- tmp_file.write(' [[environment]]\n'.encode('UTF-8'))
- for key, value in sorted(environ.items()):
- tmp_file.write(
- (' %s=%s\n' % (key, value)).encode('UTF-8'))
- tmp_file.write(msg_done_ln.encode('UTF-8'))
- line_n = 0
- is_in_old_insert = False
- for line in open(source):
- line_n += 1
- if line_n == 1 and line.strip().lower() == scheme_ln.strip():
- continue
- elif line_n == 2 and line == msg_init_ln:
- is_in_old_insert = True
- continue
- elif is_in_old_insert and line == msg_done_ln:
- is_in_old_insert = False
- continue
- elif is_in_old_insert:
- continue
- tmp_file.write(line.encode('UTF-8'))
- tmp_file.seek(0)
- if os.access(target, os.F_OK | os.R_OK):
- if filecmp.cmp(target, tmp_file.name): # identical
- tmp_file.close()
- continue
- else:
- self.manager.fs_util.delete(target)
- # Write content to target
- target_file = open(target, "w")
- for line in tmp_file:
- try:
- target_file.write(line)
- except TypeError:
- target_file.write(line.decode())
- event = FileSystemEvent(FileSystemEvent.INSTALL, target)
- self.manager.handle_event(event)
- tmp_file.close()
diff --git a/metomi/rose/config_tree.py b/metomi/rose/config_tree.py
index 725ac86e98..ca12e070f7 100644
--- a/metomi/rose/config_tree.py
+++ b/metomi/rose/config_tree.py
@@ -1,8 +1,5 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -38,7 +35,7 @@ def __str__(self):
return "Bad optional configuration key(s): " + ", ".join(self.args[0])
-class ConfigTree(object):
+class ConfigTree:
"""A run time Rose configuration with linearised inheritance.
@@ -77,7 +74,7 @@ def get_file_locs_of(self, key):
os.path.join(file_loc, key) for file_loc in self.file_locs[key]]
-class ConfigTreeLoader(object):
+class ConfigTreeLoader:
"""Load a Rose configuration with inheritance."""
@@ -186,7 +183,7 @@ def _search(cls, conf_dir, conf_dir_paths):
return os.path.abspath(os.path.join(conf_dir_paths[0], conf_dir))
-class _Test(object):
+class _Test:
"""Self tests. Print results in TAP format."""
diff --git a/metomi/rose/date.py b/metomi/rose/date.py
index df511c5789..0a24c8b0a6 100644
--- a/metomi/rose/date.py
+++ b/metomi/rose/date.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -39,7 +36,7 @@ def __str__(self):
return "%s: bad offset value" % self.args[0]
-class RoseDateTimeOperator(object):
+class RoseDateTimeOperator:
"""A class to parse and print date string with an offset."""
diff --git a/metomi/rose/env.py b/metomi/rose/env.py
index cb9c3f4068..f879017448 100644
--- a/metomi/rose/env.py
+++ b/metomi/rose/env.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# ----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/env_cat.py b/metomi/rose/env_cat.py
index 903bd7c404..dd157df461 100644
--- a/metomi/rose/env_cat.py
+++ b/metomi/rose/env_cat.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/lib/bash/rose_log b/metomi/rose/etc/lib/bash/rose_log
similarity index 98%
rename from lib/bash/rose_log
rename to metomi/rose/etc/lib/bash/rose_log
index 034d26c2f9..ebfb721990 100644
--- a/lib/bash/rose_log
+++ b/metomi/rose/etc/lib/bash/rose_log
@@ -21,7 +21,7 @@
# rose_log
#
# SYNOPSIS
-# . $ROSE_HOME/lib/bash/rose_log
+# . rose_log
# info 1 "Hello world"
# info 2 "Hello world"
# echo "Hello world" | out
diff --git a/lib/bash/rose_usage b/metomi/rose/etc/lib/bash/rose_usage
similarity index 95%
rename from lib/bash/rose_usage
rename to metomi/rose/etc/lib/bash/rose_usage
index 47e1640f4b..761a6bcf0c 100644
--- a/lib/bash/rose_usage
+++ b/metomi/rose/etc/lib/bash/rose_usage
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
#-------------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
#
@@ -21,7 +21,7 @@
# rose_usage
#
# SYNOPSIS
-# . $ROSE_HOME/lib/bash/rose_usage
+# . $rose_usage
# rose_usage [CODE]
#
# DESCRIPTION
@@ -45,7 +45,7 @@ function rose_usage() {
}
}
}
- }' "${ROSE_USAGE_FILE:-$0}" >&"$FD"
+ }' "$0" >&"$FD"
if [[ -n $CODE ]]; then
exit "$CODE"
fi
diff --git a/metomi/rose/etc/rose-demo-baked-alaska-sponge/vn1.0/lib/python/macros/desoggy.py b/metomi/rose/etc/rose-demo-baked-alaska-sponge/vn1.0/lib/python/macros/desoggy.py
index 236963af89..99f9f03be1 100644
--- a/metomi/rose/etc/rose-demo-baked-alaska-sponge/vn1.0/lib/python/macros/desoggy.py
+++ b/metomi/rose/etc/rose-demo-baked-alaska-sponge/vn1.0/lib/python/macros/desoggy.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -23,9 +20,6 @@
"""
-import re
-import subprocess
-
import metomi.rose.macro
diff --git a/metomi/rose/etc/rose-demo-upgrade-null/versions.py b/metomi/rose/etc/rose-demo-upgrade-null/versions.py
index bc94de545c..b4d4a97f7f 100644
--- a/metomi/rose/etc/rose-demo-upgrade-null/versions.py
+++ b/metomi/rose/etc/rose-demo-upgrade-null/versions.py
@@ -1,14 +1,12 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
"""Module containing test upgrade macros"""
-import rose.upgrade
+import metomi.rose.upgrade
-class UpgradeNull01(rose.upgrade.MacroUpgrade):
+class UpgradeNull01(metomi.rose.upgrade.MacroUpgrade):
"""Upgrade nothing..."""
diff --git a/metomi/rose/etc/rose-demo-upgrade/versions.py b/metomi/rose/etc/rose-demo-upgrade/versions.py
index 33d66a7887..62cecf036d 100644
--- a/metomi/rose/etc/rose-demo-upgrade/versions.py
+++ b/metomi/rose/etc/rose-demo-upgrade/versions.py
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
"""Module containing example macros for using rose app-upgrade.
@@ -9,10 +7,10 @@
"""
-import rose.upgrade
+import metomi.rose.upgrade
-class UpgradeGarden01(rose.upgrade.MacroUpgrade):
+class UpgradeGarden01(metomi.rose.upgrade.MacroUpgrade):
"""'We want... a shrubbery!'"""
@@ -28,7 +26,7 @@ def upgrade(self, config, meta_config=None):
return config, self.reports
-class UpgradeGarden02(rose.upgrade.MacroUpgrade):
+class UpgradeGarden02(metomi.rose.upgrade.MacroUpgrade):
"""'...there is one small problem...'"""
@@ -51,7 +49,7 @@ def upgrade(self, config, meta_config=None):
return config, self.reports
-class UpgradeGarden03(rose.upgrade.MacroUpgrade):
+class UpgradeGarden03(metomi.rose.upgrade.MacroUpgrade):
"""'You must find... another shrubbery!'"""
@@ -81,7 +79,7 @@ def _get_shrub_num(self, config):
return shrub_num
-class UpgradeGarden041(rose.upgrade.MacroUpgrade):
+class UpgradeGarden041(metomi.rose.upgrade.MacroUpgrade):
"""'...the two-level effect with a little path running down the middle'"""
@@ -97,7 +95,7 @@ def upgrade(self, config, meta_config=None):
return config, self.reports
-class UpgradeGarden09(rose.upgrade.MacroUpgrade):
+class UpgradeGarden09(metomi.rose.upgrade.MacroUpgrade):
"""'cut down the mightiest tree in the forest... with... a herring!'"""
diff --git a/metomi/rose/etc/rose-meta/rose-demo-baked-alaska-sponge/vn1.0/lib/python/macros/desoggy.py b/metomi/rose/etc/rose-meta/rose-demo-baked-alaska-sponge/vn1.0/lib/python/macros/desoggy.py
index 236963af89..99f9f03be1 100644
--- a/metomi/rose/etc/rose-meta/rose-demo-baked-alaska-sponge/vn1.0/lib/python/macros/desoggy.py
+++ b/metomi/rose/etc/rose-meta/rose-demo-baked-alaska-sponge/vn1.0/lib/python/macros/desoggy.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -23,9 +20,6 @@
"""
-import re
-import subprocess
-
import metomi.rose.macro
diff --git a/metomi/rose/etc/rose-meta/rose-demo-upgrade-null/versions.py b/metomi/rose/etc/rose-meta/rose-demo-upgrade-null/versions.py
index bc94de545c..b4d4a97f7f 100644
--- a/metomi/rose/etc/rose-meta/rose-demo-upgrade-null/versions.py
+++ b/metomi/rose/etc/rose-meta/rose-demo-upgrade-null/versions.py
@@ -1,14 +1,12 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
"""Module containing test upgrade macros"""
-import rose.upgrade
+import metomi.rose.upgrade
-class UpgradeNull01(rose.upgrade.MacroUpgrade):
+class UpgradeNull01(metomi.rose.upgrade.MacroUpgrade):
"""Upgrade nothing..."""
diff --git a/metomi/rose/etc/rose-meta/rose-demo-upgrade/versions.py b/metomi/rose/etc/rose-meta/rose-demo-upgrade/versions.py
index 33d66a7887..62cecf036d 100644
--- a/metomi/rose/etc/rose-meta/rose-demo-upgrade/versions.py
+++ b/metomi/rose/etc/rose-meta/rose-demo-upgrade/versions.py
@@ -1,6 +1,4 @@
#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
"""Module containing example macros for using rose app-upgrade.
@@ -9,10 +7,10 @@
"""
-import rose.upgrade
+import metomi.rose.upgrade
-class UpgradeGarden01(rose.upgrade.MacroUpgrade):
+class UpgradeGarden01(metomi.rose.upgrade.MacroUpgrade):
"""'We want... a shrubbery!'"""
@@ -28,7 +26,7 @@ def upgrade(self, config, meta_config=None):
return config, self.reports
-class UpgradeGarden02(rose.upgrade.MacroUpgrade):
+class UpgradeGarden02(metomi.rose.upgrade.MacroUpgrade):
"""'...there is one small problem...'"""
@@ -51,7 +49,7 @@ def upgrade(self, config, meta_config=None):
return config, self.reports
-class UpgradeGarden03(rose.upgrade.MacroUpgrade):
+class UpgradeGarden03(metomi.rose.upgrade.MacroUpgrade):
"""'You must find... another shrubbery!'"""
@@ -81,7 +79,7 @@ def _get_shrub_num(self, config):
return shrub_num
-class UpgradeGarden041(rose.upgrade.MacroUpgrade):
+class UpgradeGarden041(metomi.rose.upgrade.MacroUpgrade):
"""'...the two-level effect with a little path running down the middle'"""
@@ -97,7 +95,7 @@ def upgrade(self, config, meta_config=None):
return config, self.reports
-class UpgradeGarden09(rose.upgrade.MacroUpgrade):
+class UpgradeGarden09(metomi.rose.upgrade.MacroUpgrade):
"""'cut down the mightiest tree in the forest... with... a herring!'"""
diff --git a/etc/rose-conf-mode.el b/metomi/rose/etc/syntax/rose-conf-mode.el
similarity index 96%
rename from etc/rose-conf-mode.el
rename to metomi/rose/etc/syntax/rose-conf-mode.el
index b99ceb93ad..febdc4936c 100644
--- a/etc/rose-conf-mode.el
+++ b/metomi/rose/etc/syntax/rose-conf-mode.el
@@ -21,10 +21,9 @@
;; An emacs syntax highlighting mode for the various rose .conf files
;;
;; = Instructions =
-;; Place this file in a directory on your emacs load path (or symlink it)
-;; e.g.
-;; mkdir -p ~/.emacs.d/lisp
-;; ln -s $ROSE_HOME/etc/rose-conf-mode.el ~/.emacs.d/lisp/
+;; Place this file in a directory on your emacs load path. e.g:
+;;
+;; ~/.emacs.d/lisp
;;
;; and in your .emacs file:
;;
diff --git a/etc/rose-conf.lang b/metomi/rose/etc/syntax/rose-conf.lang
similarity index 100%
rename from etc/rose-conf.lang
rename to metomi/rose/etc/syntax/rose-conf.lang
diff --git a/etc/rose-conf.vim b/metomi/rose/etc/syntax/rose-conf.vim
similarity index 100%
rename from etc/rose-conf.vim
rename to metomi/rose/etc/syntax/rose-conf.vim
diff --git a/etc/rose-conf.xml b/metomi/rose/etc/syntax/rose-conf.xml
similarity index 100%
rename from etc/rose-conf.xml
rename to metomi/rose/etc/syntax/rose-conf.xml
diff --git a/metomi/rose/external.py b/metomi/rose/external.py
index f646c44995..5fc887f31d 100644
--- a/metomi/rose/external.py
+++ b/metomi/rose/external.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/formats/__init__.py b/metomi/rose/formats/__init__.py
index 81dfa29fb6..02b8f84c4f 100644
--- a/metomi/rose/formats/__init__.py
+++ b/metomi/rose/formats/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# ----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -21,4 +18,5 @@
such as namelists. To add a new format, place it in this directory and
add an import statement below.
"""
+# flake8: noqa: F401
from . import namelist
diff --git a/metomi/rose/formats/namelist.py b/metomi/rose/formats/namelist.py
index 0d267db58a..46fa2a99ef 100644
--- a/metomi/rose/formats/namelist.py
+++ b/metomi/rose/formats/namelist.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -41,11 +38,11 @@ def _rec(exp):
# Matches namelist literals for intrinsic types
RE_INTEGER = r"[\+\-]?(?:" + RE_NATURAL + r")"
REC_INTEGER = _rec(r"\A(?:" + RE_INTEGER + r")\Z")
-RE_REAL = r"(?i)[\+\-]?(?:" + RE_FLOAT + r")(?:[de][\+\-]?\d+)?"
+RE_REAL = r"[\+\-]?(?:" + RE_FLOAT + r")(?:[deDE][\+\-]?\d+)?"
REC_REAL = _rec(r"\A(?:" + RE_REAL + r")\Z")
RE_COMPLEX = r"\(\s*" + RE_REAL + r"\s*" + RE_SEP + r"\s*" + RE_REAL + r"\s*\)"
REC_COMPLEX = _rec(r"\A(?:" + RE_COMPLEX + r")\Z")
-RE_LOGICAL = r"(?i)\.(?:true|false)\."
+RE_LOGICAL = r"\.(?:[Tt][Rr][Uu][Ee]|[Ff][Aa][Ll][Ss][Ee])\."
REC_LOGICAL = _rec(r"\A(?:" + RE_LOGICAL + r")\Z")
RE_CHARACTER = r"'(?:[^']|'')*'|\"(?:[^\"]|\"\")*\""
REC_CHARACTER = _rec(r"\A(?:" + RE_CHARACTER + r")\Z")
@@ -95,7 +92,7 @@ def _rec(exp):
[_rec(r"^([+-])0+(\d)"), r"\1\2"]] # +02.0 => +2.0, -000.5 => -0.5
-class NamelistGroup(object):
+class NamelistGroup:
"""Represent a namelist group.
It has the following attributes:
@@ -120,7 +117,7 @@ def __repr__(self):
return "&%s\n%s\n/\n" % (self.name, "\n".join(object_strings))
-class NamelistObject(object):
+class NamelistObject:
"""Represent an object in a namelist group.
An object can be an assignment or a key=value pair in a
@@ -187,7 +184,7 @@ def get_rhs_as_string(self, min_repeats=5, wrapped=False, max_len=60):
return "\n".join(lines)
-class NamelistValue(object):
+class NamelistValue:
"""Represent a value in a namelist object."""
def __init__(self, value_init, quote=False):
@@ -232,7 +229,7 @@ def _tidy_real(self, value):
return value
-class _ParseContext(object):
+class _ParseContext:
"""Convenient object for storing the parser's state."""
def __init__(self):
diff --git a/metomi/rose/fs_util.py b/metomi/rose/fs_util.py
index 830abf71e2..46c2ab9616 100644
--- a/metomi/rose/fs_util.py
+++ b/metomi/rose/fs_util.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -54,7 +51,7 @@ def __str__(self):
return "%s: %s" % (self.action, target)
-class FileSystemUtil(object):
+class FileSystemUtil:
"""File system utilities with event reporting."""
diff --git a/metomi/rose/host_select.py b/metomi/rose/host_select.py
index caaaea325b..fc7c2a2f7e 100644
--- a/metomi/rose/host_select.py
+++ b/metomi/rose/host_select.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -19,20 +16,29 @@
# -----------------------------------------------------------------------------
"""Select an available host machine by load or by random."""
+from collections import namedtuple
+from functools import lru_cache
+import json
import os
from random import choice, random, shuffle
-from metomi.rose.opt_parse import RoseOptionParser
-from metomi.rose.popen import RosePopener
-from metomi.rose.reporter import Reporter, Event
-from metomi.rose.resource import ResourceLocator
import shlex
import signal
from socket import (
- getaddrinfo, gethostbyname_ex, gethostname, getfqdn, error as SocketError)
+ getaddrinfo,
+ gethostbyname_ex,
+ gethostname,
+ getfqdn,
+ error as SocketError
+)
import sys
from time import sleep, time
import traceback
+from metomi.rose.opt_parse import RoseOptionParser
+from metomi.rose.popen import RosePopener
+from metomi.rose.reporter import Reporter, Event
+from metomi.rose.resource import ResourceLocator
+
class NoHostError(Exception):
@@ -50,14 +56,23 @@ def __str__(self):
return "No hosts selected."
-class DeadHostEvent(Event):
+class HostSelectCommandFailedEvent(Event):
- """An error raised when a host is not contactable."""
+ """A remote host select command failed."""
KIND = Event.KIND_ERR
+ def __init__(self, return_code: int, host: str):
+ self.return_code = return_code
+ self.host = host
+ Event.__init__(self)
+
def __str__(self):
- return self.args[0] + ": (ssh failed)"
+ if self.return_code == 255:
+ msg = 'ssh failed'
+ else:
+ msg = f'failed {self.return_code}'
+ return f'{self.host}: ({msg})'
class HostThresholdNotMetEvent(Event):
@@ -116,7 +131,7 @@ def __str__(self):
return self.args[0] + ": (timed out)"
-class HostSelector(object):
+class HostSelector:
"""Select an available host machine by load of by random."""
@@ -342,7 +357,11 @@ def select(self, names=None, rank_method=None, thresholds=None,
proc.wait()
self.handle_event(TimedOutHostEvent(host_name))
elif proc.wait():
- self.handle_event(DeadHostEvent(host_name))
+ self.handle_event(
+ HostSelectCommandFailedEvent(
+ proc.returncode, host_name
+ )
+ )
else:
return [(host_name, 1)]
else:
@@ -351,40 +370,65 @@ def select(self, names=None, rank_method=None, thresholds=None,
# ssh to each host to return its score(s).
host_proc_dict = {}
for host_name in sorted(host_names):
+ # build host-select-client command
command = []
if not self.is_local_host(host_name):
command_args = []
command_args.append(host_name)
command = self.popen.get_cmd("ssh", *command_args)
- command.append("bash")
- stdin = rank_conf.get_command()
+ command.extend(["rose", "host-select-client"])
+
+ # build list of metrics to obtain for each host
+ metrics = rank_conf.get_command()
for threshold_conf in threshold_confs:
- stdin += threshold_conf.get_command()
- stdin += "exit\n"
- proc = self.popen.run_bg(*command, stdin=stdin,
- preexec_fn=os.setpgrp)
+ for metric in threshold_conf.get_command():
+ if metric not in metrics:
+ metrics.append(metric)
+
+ # convert metrics list to JSON stdin
+ stdin = (
+ '\n***start**\n'
+ + json.dumps(metrics)
+ + '\n**end**\n'
+ )
+
+ # fire off host-select-client processes
+ proc = self.popen.run_bg(
+ *command,
+ stdin=stdin,
+ preexec_fn=os.setpgrp
+ )
proc.stdin.write(stdin.encode('UTF-8'))
proc.stdin.flush()
- host_proc_dict[host_name] = proc
+ host_proc_dict[host_name] = (proc, metrics)
# Retrieve score for each host name
host_score_list = []
time0 = time()
while host_proc_dict:
sleep(self.SSH_CMD_POLL_DELAY)
- for host_name, proc in list(host_proc_dict.items()):
+ for host_name, (proc, metrics) in list(host_proc_dict.items()):
if proc.poll() is None:
score = None
elif proc.wait():
- self.handle_event(DeadHostEvent(host_name))
+ stdout, stderr = (f.decode() for f in proc.communicate())
+ self.handle_event(
+ HostSelectCommandFailedEvent(
+ proc.returncode, host_name
+ )
+ )
host_proc_dict.pop(host_name)
else:
- out = proc.communicate()[0]
+ out = proc.communicate()[0].decode()
+ out = _deserialise(metrics, json.loads(out.strip()))
+
host_proc_dict.pop(host_name)
for threshold_conf in threshold_confs:
try:
- is_bad = threshold_conf.check_threshold(out)
- score = threshold_conf.command_out_parser(out)
+ score = threshold_conf.command_out_parser(
+ out, metrics
+ )
+ is_bad = threshold_conf.check_threshold(score)
except ValueError:
is_bad = True
score = None
@@ -394,7 +438,7 @@ def select(self, names=None, rank_method=None, thresholds=None,
break
else:
try:
- score = rank_conf.command_out_parser(out)
+ score = rank_conf.command_out_parser(out, metrics)
host_score_list.append((host_name, score))
except ValueError:
score = None
@@ -404,7 +448,7 @@ def select(self, names=None, rank_method=None, thresholds=None,
break
# Report timed out hosts
- for host_name, proc in sorted(host_proc_dict.items()):
+ for host_name, (proc, _) in sorted(host_proc_dict.items()):
self.handle_event(TimedOutHostEvent(host_name))
os.killpg(proc.pid, signal.SIGTERM)
proc.wait()
@@ -419,7 +463,41 @@ def select(self, names=None, rank_method=None, thresholds=None,
__call__ = select
-class ScorerConf(object):
+@lru_cache()
+def _tuple_factory(name, params):
+ """Wrapper to namedtuple which caches results to prevent duplicates."""
+ return namedtuple(name, params)
+
+
+def _deserialise(metrics, data):
+ """Convert dict to named tuples.
+
+ Examples:
+ >>> _deserialise(
+ ... [
+ ... ['foo', 'bar'],
+ ... ['baz']
+ ... ],
+ ... [
+ ... {'a': 1, 'b': 2, 'c': 3},
+ ... [1, 2, 3]
+ ... ]
+ ... )
+ [foo(a=1, b=2, c=3), [1, 2, 3]]
+
+ """
+ for index, (metric, datum) in enumerate(zip(metrics, data)):
+ if isinstance(datum, dict):
+ data[index] = _tuple_factory(
+ metric[0],
+ tuple(datum.keys())
+ )(
+ *datum.values()
+ )
+ return data
+
+
+class ScorerConf:
"""Wrap a threshold/ranking scorer + extra configuration."""
@@ -433,18 +511,25 @@ def get_command(self):
"""Return a shell command to get the info for scoring a host."""
return self.scorer.get_command(self.method_arg)
- def check_threshold(self, out):
+ def check_threshold(self, score):
"""Parse command output. Return True if threshold not met."""
- score = self.command_out_parser(out)
return (float(score) * self.scorer.SIGN >
float(self.value) * self.scorer.SIGN)
- def command_out_parser(self, out):
+ def command_out_parser(self, out, metrics):
"""Parse command output to return a numeric score."""
- return self.scorer.command_out_parser(out, self.method_arg)
+ results = self.get_results(out, metrics)
+ return self.scorer.command_out_parser(results, self.method_arg)
+
+ def get_results(self, out, metrics):
+ """Return list of results for the requested metrics."""
+ return [
+ out[metrics.index(metric)]
+ for metric in self.scorer.get_command(self.method_arg)
+ ]
-class RandomScorer(object):
+class RandomScorer:
"""Base class for threshold/ranking scorer.
@@ -454,17 +539,13 @@ class RandomScorer(object):
ARG = None
KEY = "random"
- CMD = "true\n"
+ CMD = ['cpu_count'] # fetch an arbitrary metric
CMD_IS_FORMAT = False
SIGN = 1 # Positive
def get_command(self, method_arg=None):
"""Return a shell command to get the info for scoring a host."""
-
- if self.CMD_IS_FORMAT:
- return self.CMD % {"method_arg": method_arg}
- else:
- return self.CMD
+ return list(self.CMD)
@classmethod
def command_out_parser(cls, out, method_arg=None):
@@ -474,7 +555,6 @@ def command_out_parser(cls, out, method_arg=None):
returned by the command run on the remote host. Otherwise, this method
returns a random number.
"""
-
return random()
@@ -484,24 +564,13 @@ class LoadScorer(RandomScorer):
ARG = "15"
KEY = "load"
- INDEX_OF = {"1": 1, "5": 2, "15": 3}
- CMD = ("echo nproc=$((cat /proc/cpuinfo || lscfg) | grep -ic processor)\n"
- "echo uptime=$(uptime)\n")
+ VALUES = ('1', '5', '15') # 1, 5, 15 min average values
+ CMD = [["getloadavg"], ["cpu_count"]]
def command_out_parser(self, out, method_arg=None):
- if method_arg is None:
- method_arg = self.ARG
- nprocs = None
- load = None
- for line in out.splitlines():
- if line.startswith(b"nproc="):
- nprocs = line.split(b"=", 1)[1]
- elif line.startswith(b"uptime="):
- idx = self.INDEX_OF[method_arg]
- load = line.rsplit(None, 3)[idx].rstrip(b",")
- if load is None or not nprocs:
- return None
- return float(load) / float(nprocs)
+ load = out[0][self.VALUES.index(method_arg or self.ARG)]
+ cpus = out[1]
+ return load / cpus
class MemoryScorer(RandomScorer):
@@ -509,17 +578,11 @@ class MemoryScorer(RandomScorer):
"""Score host by amount of free memory"""
KEY = "mem"
- CMD = """echo mem=$(free -m | sed '3!d; s/^.* \\/')\n"""
+ CMD = [["virtual_memory"]]
SIGN = -1 # Negative
def command_out_parser(self, out, method_arg=None):
- if method_arg is None:
- method_arg = self.ARG
- mem = None
- for line in out.splitlines():
- if line.startswith(b"mem="):
- mem = line.split(b"=", 1)[1]
- return float(mem)
+ return out[0].available
class FileSystemScorer(RandomScorer):
@@ -528,15 +591,12 @@ class FileSystemScorer(RandomScorer):
ARG = "~"
KEY = "fs"
- CMD = """echo df:'%(method_arg)s'=$(df -Pk %(method_arg)s | tail -1)\n"""
- CMD_IS_FORMAT = True
+
+ def get_command(self, method_arg):
+ return [['disk_usage', method_arg or self.ARG]]
def command_out_parser(self, out, method_arg=None):
- if method_arg is None:
- method_arg = self.ARG
- for line in out.splitlines():
- if line.startswith("df:" + method_arg + "="):
- return int(line.rsplit(None, 2)[-2][0:-1])
+ return out[0].percent
def main():
diff --git a/metomi/rose/host_select_client.py b/metomi/rose/host_select_client.py
new file mode 100644
index 0000000000..c0a377499c
--- /dev/null
+++ b/metomi/rose/host_select_client.py
@@ -0,0 +1,66 @@
+# Copyright (C) British Crown (Met Office) & Contributors.
+#
+# This file is part of Rose, a framework for meteorological suites.
+#
+# Rose 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.
+#
+# Rose 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 Rose. If not, see .
+# -----------------------------------------------------------------------------
+import json
+from pathlib import Path
+import sys
+
+import psutil
+
+
+def main():
+ # read metrics from stdin
+ started = False
+ line = True
+ metrics = ''
+ while True:
+ line = sys.stdin.readline().strip()
+ if '**start**' in line:
+ started = True
+ continue
+ elif not started:
+ continue
+ elif '**end**' in line:
+ break
+ metrics += f'\n{line}'
+ metrics = json.loads(metrics)
+
+ # extract metrics using psutil
+ ret = [
+ getattr(psutil, key)(
+ *args
+ if key != 'disk_usage'
+ else [
+ # expand ~ in paths for disk usage queries
+ Path(arg).expanduser()
+ for arg in args
+ ]
+ )
+ for key, *args in metrics
+ ]
+
+ # serialise results
+ for ind, item in enumerate(ret):
+ if hasattr(item, '_asdict'):
+ ret[ind] = item._asdict()
+
+ # output results as json
+ print(json.dumps(ret))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/metomi/rose/job_runner.py b/metomi/rose/job_runner.py
index 09b5dd6dee..0f0dfb18df 100644
--- a/metomi/rose/job_runner.py
+++ b/metomi/rose/job_runner.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -35,7 +32,7 @@ def __str__(self):
return str(self.args[0])
-class JobManager(object):
+class JobManager:
"""Manage a set of JobProxy objects and their states."""
def __init__(self, jobs, names=None):
@@ -110,7 +107,7 @@ def put_job(self, job_proxy):
return job
-class JobProxy(object):
+class JobProxy:
"""Represent the state of the job."""
ST_DONE = "ST_DONE"
@@ -148,7 +145,7 @@ def update(self, other):
self.context.update(other.context)
-class JobRunner(object):
+class JobRunner:
"""Runs JobProxy objects with pool of workers."""
def __init__(self, job_processor, nproc=None):
diff --git a/metomi/rose/loc_handlers/__init__.py b/metomi/rose/loc_handlers/__init__.py
index ea8bb3646f..94490716c3 100644
--- a/metomi/rose/loc_handlers/__init__.py
+++ b/metomi/rose/loc_handlers/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/loc_handlers/fs.py b/metomi/rose/loc_handlers/fs.py
index ab9434da48..48da4ba8d9 100644
--- a/metomi/rose/loc_handlers/fs.py
+++ b/metomi/rose/loc_handlers/fs.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -24,7 +21,7 @@
import os
-class FileSystemLocHandler(object):
+class FileSystemLocHandler:
"""Handler of file system locations."""
diff --git a/metomi/rose/loc_handlers/namelist.py b/metomi/rose/loc_handlers/namelist.py
index b21f355cc4..0274bb826e 100644
--- a/metomi/rose/loc_handlers/namelist.py
+++ b/metomi/rose/loc_handlers/namelist.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -37,7 +34,7 @@ class NamelistEvent(Event):
LEVEL = Event.VV
-class NamelistLocHandler(object):
+class NamelistLocHandler:
"""Handler of namelists."""
SCHEME = "namelist"
diff --git a/metomi/rose/loc_handlers/rsync.py b/metomi/rose/loc_handlers/rsync.py
index a50988bd63..a1ffd0e666 100644
--- a/metomi/rose/loc_handlers/rsync.py
+++ b/metomi/rose/loc_handlers/rsync.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -19,8 +16,6 @@
# -----------------------------------------------------------------------------
"""A handler of locations on remote hosts."""
-from pathlib import Path
-from tempfile import TemporaryFile
from time import sleep, time
from metomi.rose.popen import RosePopenError
@@ -29,7 +24,7 @@
)
-class RsyncLocHandler(object):
+class RsyncLocHandler:
"""Handler of locations on remote hosts."""
SCHEME = "rsync"
@@ -67,10 +62,8 @@ def parse(self, loc, _):
host, path = loc.name.split(":", 1)
cmd = self.manager.popen.get_cmd(
"ssh", host, "python3", "-", path, loc.TYPE_BLOB, loc.TYPE_TREE)
- temp_file = TemporaryFile()
- temp_file.write(Path(rsync_remote_check_file).read_bytes())
- temp_file.seek(0)
- out = self.manager.popen(*cmd, stdin=temp_file)[0].decode()
+ with open(rsync_remote_check_file, 'rb') as stdin:
+ out = self.manager.popen(*cmd, stdin=stdin)[0].decode()
lines = out.splitlines()
if not lines or lines[0] not in [loc.TYPE_BLOB, loc.TYPE_TREE]:
raise ValueError(loc.name)
@@ -80,7 +73,7 @@ def parse(self, loc, _):
access_mode, mtime, size, name = line.split(None, 3)
fake_sum = "source=%s:mtime=%s:size=%s" % (
name, mtime, size)
- loc.add_path(loc.BLOB, fake_sum, int(access_mode, base=8))
+ loc.add_path(loc.BLOB, fake_sum, int(access_mode))
else: # if loc.loc_type == loc.TYPE_TREE:
for line in lines:
access_mode, mtime, size, name = line.split(None, 3)
diff --git a/metomi/rose/loc_handlers/rsync_remote_check.py b/metomi/rose/loc_handlers/rsync_remote_check.py
index 8ab7e85a83..3f6de15365 100644
--- a/metomi/rose/loc_handlers/rsync_remote_check.py
+++ b/metomi/rose/loc_handlers/rsync_remote_check.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
#
# This file is part of Rose, a framework for meteorological suites.
@@ -23,12 +21,18 @@
This is a Python file but we read it and pass it to stdin
to avoid reliance on remote platforms having rose installed.
+
+Warning:
+ This script will not necessarily be run in the Rose Python environment.
+ It should not have any dependencies outside of the stdlib and should be
+ compatible with as wide a range of Python3 versions as possible.
+
"""
import os
import sys
-def main():
+def main(path, str_blob, str_tree):
"""Check file exists and print some info:
1. Octal protection bits.
@@ -36,7 +40,6 @@ def main():
3. Filesize.
4. Path, which has been checked.
"""
- path, str_blob, str_tree = sys.argv[1:]
if os.path.isdir(path):
print(str_tree)
os.chdir(path)
@@ -46,19 +49,19 @@ def main():
if not dirname.startswith("."):
good_dirnames.append(dirname)
name = os.path.join(dirpath, dirname)
- print(("-", "-", "-", name))
+ print("-", "-", "-", name)
dirnames[:] = good_dirnames
for filename in filenames:
if filename.startswith("."):
continue
name = os.path.join(dirpath, filename)
stat = os.stat(name)
- print((oct(stat.st_mode), stat.st_mtime, stat.st_size, name))
+ print(stat.st_mode, stat.st_mtime, stat.st_size, name)
elif os.path.isfile(path):
print(str_blob)
stat = os.stat(path)
- print(oct(stat.st_mode), stat.st_mtime, stat.st_size, path)
+ print(stat.st_mode, stat.st_mtime, stat.st_size, path)
if __name__ == '__main__':
- main()
+ main(*sys.argv[1:])
diff --git a/metomi/rose/loc_handlers/svn.py b/metomi/rose/loc_handlers/svn.py
index 92502f43d6..872b865665 100644
--- a/metomi/rose/loc_handlers/svn.py
+++ b/metomi/rose/loc_handlers/svn.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -24,7 +21,7 @@
import xml.parsers.expat
-class SvnLocHandler(object):
+class SvnLocHandler:
"""Handler of Subversion locations."""
FCM = "fcm"
@@ -73,7 +70,7 @@ async def pull(self, loc, conf_tree):
"svn", "export", "-q", loc.real_name, loc.cache)
-class SvnInfoXMLParser(object):
+class SvnInfoXMLParser:
"""An XML parser tailored for a single entry of "svn info --xml"."""
def __init__(self):
diff --git a/metomi/rose/macro.py b/metomi/rose/macro.py
index 5342a0dd96..69809bba30 100644
--- a/metomi/rose/macro.py
+++ b/metomi/rose/macro.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -173,7 +170,7 @@ def __str__(self):
return ERROR_LOAD_CONF_META_NODE
-class MacroBase(object):
+class MacroBase:
"""Base class for macros for validating or transforming configurations.
@@ -341,7 +338,7 @@ def standard_format_config(self, config):
def add_report(self, *args, **kwargs):
"""Add a metomi.rose.macro.MacroReport.
- See :class:`rose.macro.MacroReport` for details of arguments.
+ See :class:`metomi.rose.macro.MacroReport` for details of arguments.
Examples:
>>> # An example validator macro which adds a report to the setting
@@ -497,7 +494,7 @@ def transform(self, config, meta_config=None):
return config, self.reports
-class MacroReport(object):
+class MacroReport:
"""Class to hold information about a macro issue.
@@ -505,7 +502,7 @@ class MacroReport(object):
section (str): The name of the section to attach this report to.
option (str): The name of the option (within the section) to
attach this report to.
- value (obj): The value of the configuration associated with this
+ value (object): The value of the configuration associated with this
report.
info (str): Text information describing the nature of the report.
is_warning (bool): If True then this report will be logged as a
@@ -554,7 +551,9 @@ def add_site_meta_paths():
for path in path.split(os.pathsep):
path = os.path.expanduser(os.path.expandvars(path))
sys.path.insert(0, os.path.abspath(path))
- sys.path.append(os.path.join(os.getenv("ROSE_LIB"), "etc/rose-meta"))
+ sys.path.append(
+ metomi.rose.resource.ResourceLocator.default().locate('rose-meta')
+ )
def add_env_meta_paths():
@@ -648,7 +647,7 @@ def load_meta_path(config=None, directory=None, is_upgrade=False,
if is_upgrade:
path = meta_key
try:
- meta_path = locator.locate(path)
+ meta_path = str(locator.locate(path))
except metomi.rose.resource.ResourceError:
continue
else:
@@ -690,7 +689,7 @@ def load_meta_config_tree(config, directory=None, config_type=None,
meta_config = metomi.rose.config.ConfigNode()
for meta_key in meta_list:
try:
- meta_path = locator.locate(meta_key)
+ meta_path = str(locator.locate(meta_key))
except metomi.rose.resource.ResourceError:
if not ignore_meta_error:
error_handler(text=ERROR_LOAD_META_PATH.format(meta_key))
@@ -1633,7 +1632,6 @@ def main():
sys.exit(1)
# Path manipulation.
- sys.path.append(os.getenv("ROSE_LIB"))
add_opt_meta_paths(opts.meta_path)
# Run macros for each config.
diff --git a/metomi/rose/macros/__init__.py b/metomi/rose/macros/__init__.py
index c6301abb3a..00975571ea 100644
--- a/metomi/rose/macros/__init__.py
+++ b/metomi/rose/macros/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/macros/compulsory.py b/metomi/rose/macros/compulsory.py
index fe09fe092a..cc82a990de 100644
--- a/metomi/rose/macros/compulsory.py
+++ b/metomi/rose/macros/compulsory.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/macros/duplicate.py b/metomi/rose/macros/duplicate.py
index a4248c530d..c6345c77c6 100644
--- a/metomi/rose/macros/duplicate.py
+++ b/metomi/rose/macros/duplicate.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/macros/format.py b/metomi/rose/macros/format.py
index f201662fd6..db860c2160 100644
--- a/metomi/rose/macros/format.py
+++ b/metomi/rose/macros/format.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
# -----------------------------------------------------------------------------
diff --git a/metomi/rose/macros/rule.py b/metomi/rose/macros/rule.py
index 3254663aae..9cc506faaa 100644
--- a/metomi/rose/macros/rule.py
+++ b/metomi/rose/macros/rule.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/macros/trigger.py b/metomi/rose/macros/trigger.py
index 38c965dcc9..d14d641bf6 100644
--- a/metomi/rose/macros/trigger.py
+++ b/metomi/rose/macros/trigger.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/macros/value.py b/metomi/rose/macros/value.py
index 5ee7038fa3..4837b3fd55 100644
--- a/metomi/rose/macros/value.py
+++ b/metomi/rose/macros/value.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/meta_type.py b/metomi/rose/meta_type.py
index 2d234206ad..12e81deedc 100644
--- a/metomi/rose/meta_type.py
+++ b/metomi/rose/meta_type.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -26,7 +23,7 @@
REC_CHARACTER = re.compile(r"'(?:[^']|'')*'$")
-class MetaType(object):
+class MetaType:
KEY = None
meta_type_classes = {}
diff --git a/metomi/rose/metadata_check.py b/metomi/rose/metadata_check.py
index 7deec314d3..a935ec3929 100644
--- a/metomi/rose/metadata_check.py
+++ b/metomi/rose/metadata_check.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/metadata_gen.py b/metomi/rose/metadata_gen.py
index 919f25c00b..70c6541320 100644
--- a/metomi/rose/metadata_gen.py
+++ b/metomi/rose/metadata_gen.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/metadata_graph.py b/metomi/rose/metadata_graph.py
index 11b556cf1d..a84b10cb01 100644
--- a/metomi/rose/metadata_graph.py
+++ b/metomi/rose/metadata_graph.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -267,8 +264,6 @@ def main():
if opts.conf_dir:
os.chdir(opts.conf_dir)
opts.conf_dir = os.getcwd()
- sys.path.append(
- metomi.rose.resource.ResourceLocator.default().get_util_home())
metomi.rose.macro.add_opt_meta_paths(opts.meta_path)
config_file_path = os.path.join(opts.conf_dir, metomi.rose.SUB_CONFIG_NAME)
diff --git a/metomi/rose/namelist_dump.py b/metomi/rose/namelist_dump.py
index 0f4b35bee4..5e11015883 100644
--- a/metomi/rose/namelist_dump.py
+++ b/metomi/rose/namelist_dump.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/opt_parse.py b/metomi/rose/opt_parse.py
index 139aa95374..dcbf878634 100644
--- a/metomi/rose/opt_parse.py
+++ b/metomi/rose/opt_parse.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -20,7 +17,7 @@
"""Common option parser for Rose command utilities."""
from optparse import OptionParser
-from metomi.rose.resource import ResourceLocator
+import metomi.rose.resource
class RoseOptionParser(OptionParser):
@@ -616,10 +613,6 @@ class RoseOptionParser(OptionParser):
["--to-origin"],
{"action": "store_true",
"help": "Convert ID to the origin URL"}],
- "to_output": [
- ["--to-output"],
- {"action": "store_true",
- "help": "Get the ID output directory"}],
"to_web": [
["--to-web"],
{"action": "store_true",
@@ -686,9 +679,15 @@ class RoseOptionParser(OptionParser):
def __init__(self, *args, **kwargs):
if hasattr(kwargs, "prog"):
namespace, util = kwargs["prog"].split(None, 1)
- resource_loc = ResourceLocator(namespace=namespace, util=util)
+ resource_loc = (
+ metomi.rose.resource.ResourceLocator(
+ namespace=namespace, util=util
+ )
+ )
else:
- resource_loc = ResourceLocator.default()
+ resource_loc = (
+ metomi.rose.resource.ResourceLocator.default()
+ )
kwargs["prog"] = resource_loc.get_util_name()
if not hasattr(kwargs, "usage"):
kwargs["usage"] = resource_loc.get_synopsis()
diff --git a/metomi/rose/popen.py b/metomi/rose/popen.py
index 63b15ce104..c4f2cb2244 100644
--- a/metomi/rose/popen.py
+++ b/metomi/rose/popen.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -19,16 +16,17 @@
# -----------------------------------------------------------------------------
"""Wraps Python's subprocess.Popen."""
-import os
import asyncio
+import os
import re
-import io
-from metomi.rose.reporter import Event
-from metomi.rose.resource import ResourceLocator
+import select
import shlex
from subprocess import Popen, PIPE
import sys
+from metomi.rose.reporter import Event
+from metomi.rose.resource import ResourceLocator
+
class RosePopenError(Exception):
@@ -69,21 +67,44 @@ def __str__(self):
ret = command
else:
ret = RosePopener.list_to_shell_str(self.command)
- if isinstance(self.stdin, str):
- ret += " <<'__STDIN__'\n" + self.stdin + "\n'__STDIN__'"
- elif isinstance(self.stdin, io.IOBase):
- try:
- # FIXME: Is this safe?
- pos = self.stdin.tell()
- ret += " <<'__STDIN__'\n" +\
- self.stdin.read().decode() + "\n'__STDIN__'"
- self.stdin.seek(pos)
- except IOError:
- pass
+
+ try:
+ # real file or real stream
+ self.stdin.fileno()
+ # ask select if it is readable (real files can hang)
+ readable = bool(select.select([self.stdin], [], [], 0.0)[0])
+ except (AttributeError, IOError):
+ # file like
+ readable = True
+
+ if self.stdin:
+ if isinstance(self.stdin, str):
+ # string
+ stdin = self.stdin
+ elif isinstance(self.stdin, bytes):
+ # byte string
+ stdin = self.stdin.decode()
+ elif readable:
+ # file like
+ try:
+ pos = self.stdin.tell()
+ stdin = self.stdin.read()
+ self.stdin.seek(pos)
+ if not isinstance(stdin, str):
+ stdin = stdin.decode()
+ except Exception:
+ # purposefully vague for safety (catch any exception)
+ stdin = ''
+ else:
+ stdin = ''
+
+ if stdin:
+ ret += f" <<'__STDIN__'\n{stdin}\n__STDIN__"
+
return ret
-class RosePopener(object):
+class RosePopener:
"""Wrap Python's subprocess.Popen."""
diff --git a/metomi/rose/reporter.py b/metomi/rose/reporter.py
index 633f893f7e..5c494e6eb5 100644
--- a/metomi/rose/reporter.py
+++ b/metomi/rose/reporter.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -24,7 +21,7 @@
import time
-class Reporter(object):
+class Reporter:
"""Report diagnostic messages.
@@ -181,7 +178,7 @@ def report(self, message, kind=None, level=None, prefix=None, clip=None):
__call__ = report
-class ReporterContext(object):
+class ReporterContext:
"""A context for the reporter object.
@@ -260,7 +257,7 @@ def _tty_colour_err(self, str_):
return str_
-class Event(object):
+class Event:
"""A base class for events suitable for feeding into a Reporter."""
diff --git a/metomi/rose/resource.py b/metomi/rose/resource.py
index 57bf4050e2..7d892f4979 100644
--- a/metomi/rose/resource.py
+++ b/metomi/rose/resource.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -22,29 +19,19 @@
"""
import os
-from metomi.rose.config import ConfigLoader, ConfigNode
+from pathlib import Path
import inspect
import string
import sys
from importlib.machinery import SourceFileLoader
+import metomi.rose
+from metomi.rose.config import ConfigLoader, ConfigNode
+import metomi.rose.opt_parse
+from metomi.rose.reporter import Reporter
-ERROR_LOCATE_OBJECT = "Could not locate {0}"
-
-
-def get_util_home(*args):
- """Return ROSE_LIB or the dirname of the dirname of sys.argv[0].
-
- If args are specified, they are added to the end of returned path.
- """
- try:
- value = os.environ["ROSE_LIB"]
- except KeyError:
- value = os.path.abspath(__file__)
- for _ in range(3): # assume __file__ under $ROSE_LIB/metomi/rose/
- value = os.path.dirname(value)
- return os.path.join(value, *args)
+ERROR_LOCATE_OBJECT = "Could not locate {0}"
class ResourceError(Exception):
@@ -55,12 +42,30 @@ def __init__(self, key):
Exception.__init__(self, "%s: resource not found." % key)
-class ResourceLocator(object):
+ROSE_CONF_PATH = 'ROSE_CONF_PATH'
+ROSE_SITE_CONF_PATH = 'ROSE_SITE_CONF_PATH'
+ROSE_INSTALL_ROOT = Path(metomi.rose.__file__).parent
+
+
+class ResourceLocator:
+ """A class for searching resource files.
+
+ Loads files in the following order:
+
+ System:
+ /etc
+ Site:
+ $ROSE_SITE_CONF_PATH
+ User:
+ ~/.metomi
- """A class for searching resource files."""
+ If $ROSE_CONF_PATH is defined these files are skipped and configuration
+ found in $ROSE_CONF_PATH is loaded instead.
- SITE_CONF_PATH = get_util_home("etc")
- USER_CONF_PATH = os.path.join(os.path.expanduser("~"), ".metomi")
+ """
+
+ SYST_CONF_PATH = Path('/etc')
+ USER_CONF_PATH = Path('~/.metomi').expanduser()
ROSE_CONF = "rose.conf"
_DEFAULT_RESOURCE_LOCATOR = None
@@ -75,35 +80,53 @@ def __init__(self, namespace=None, util=None, paths=None):
self.namespace = namespace
self.util = util
if paths:
- self.paths = list(paths)
+ self.paths = list(map(Path, paths))
else:
- home = self.get_util_home()
- name = self.get_util_name("-")
- self.paths = [os.path.join(home, "etc", name),
- os.path.join(home, "etc")]
+ self.paths = [
+ (ROSE_INSTALL_ROOT / 'etc') / self.get_util_name("-"),
+ ROSE_INSTALL_ROOT / 'etc'
+ ]
self.conf = None
def get_conf(self):
"""Return the site/user configuration root node."""
if self.conf is None:
- paths = [self.SITE_CONF_PATH, self.USER_CONF_PATH]
+ # base system conf path
+ paths = [self.SYST_CONF_PATH]
+
+ # add $ROSE_SITE_CONF_PATH if defined
+ if "ROSE_SITE_CONF_PATH" in os.environ:
+ path_str = os.environ["ROSE_SITE_CONF_PATH"].strip()
+ if path_str:
+ paths.append(Path(path_str))
+
+ # add user conf path
+ paths.append(self.USER_CONF_PATH)
+
+ # use $ROSE_CONF_PATH (and ignore all others) if defined
if "ROSE_CONF_PATH" in os.environ:
paths_str = os.getenv("ROSE_CONF_PATH").strip()
if paths_str:
- paths = paths_str.split(os.pathsep)
+ paths = [
+ Path(path)
+ for path in paths_str.split(os.pathsep)
+ ]
else:
paths = []
+
+ # load and cache config
self.conf = ConfigNode()
config_loader = ConfigLoader()
for path in paths:
- name = os.path.join(path, self.ROSE_CONF)
- if os.path.isfile(name) and os.access(name, os.R_OK):
- config_loader.load_with_opts(name, self.conf)
+ conffile = path / self.ROSE_CONF
+ if conffile.is_file() and os.access(conffile, os.R_OK):
+ config_loader.load_with_opts(str(conffile), self.conf)
+
return self.conf
def get_doc_url(self):
"""Return the URL of Rose documentation."""
- default = "file://%s/doc/" % self.get_util_home()
+ default = f"file://{ROSE_INSTALL_ROOT}/doc/"
return self.get_conf().get_value(["rose-doc"], default=default)
def get_synopsis(self):
@@ -120,15 +143,6 @@ def get_synopsis(self):
except IOError:
return None
- @classmethod
- def get_util_home(cls, *args):
- """Return ROSE_HOME or the dirname of the dirname of sys.argv[0].
-
- If args are specified, they are added to the end of returned path.
-
- """
- return get_util_home(*args)
-
def get_util_name(self, separator=" "):
"""Return the name of the Rose utility, e.g. "rose app-run".
@@ -147,42 +161,16 @@ def get_util_name(self, separator=" "):
except KeyError:
return os.path.basename(sys.argv[0])
- def get_version(self, ignore_environment=False):
- """return the current metomi.rose_version number.
-
- By default pass through the value of the ``ROSE_VERSION`` environment
- variable.
-
- Args:
- ignore_environment (bool): Return the value extracted from the
- ``rose-version`` file.
- """
- version = None
- if not ignore_environment:
- version = os.getenv("ROSE_VERSION")
- if not version:
- for line in open(self.get_util_home("rose-version")):
- if line.startswith("ROSE_VERSION="):
- value = line.replace("ROSE_VERSION=", "")
- version = value.strip(string.whitespace + "\";")
- break
- return version
-
def locate(self, key):
"""Return the location of the resource key."""
key = os.path.expanduser(key)
for path in self.paths:
- name = os.path.join(path, key)
- if os.path.exists(name):
+ name = path / key
+ if name.exists():
return name
raise ResourceError(key)
-def resource_locate(key):
- """Return the location of the resource key."""
- return ResourceLocator.default().locate(key)
-
-
def import_object(import_string, from_files, error_handler,
module_prefix=None):
"""Import a Python callable.
@@ -239,3 +227,40 @@ def import_object(import_string, from_files, error_handler,
if obj_name == class_name and inspect.isclass(obj):
return_object = obj
return return_object
+
+
+def main():
+ """Launcher for the CLI."""
+ opt_parser = metomi.rose.opt_parse.RoseOptionParser()
+ opt_parser.add_my_options()
+ opts, args = opt_parser.parse_args(sys.argv[1:])
+ reporter = Reporter(opts.verbosity - opts.quietness)
+ is_top_level = False
+ if len(args) > 1:
+ reporter.report('Only one argument accepted\n', level=Reporter.FAIL)
+ sys.exit(1)
+ if len(args) == 0:
+ key = ROSE_INSTALL_ROOT / 'etc'
+ path = ResourceLocator(paths=[ROSE_INSTALL_ROOT]).locate('etc')
+ is_top_level = True
+ else:
+ key = args[0]
+ try:
+ path = ResourceLocator().locate(key)
+ except ResourceError:
+ reporter.report('Resource not found\n', level=Reporter.FAIL)
+ sys.exit(1)
+ if path.is_file():
+ print(path)
+ elif path.is_dir():
+ print(f'{key}/')
+ for item in path.iterdir():
+ if is_top_level:
+ item = item.relative_to(path)
+ else:
+ item = item.relative_to(path.parent)
+ print(f' {item}')
+
+
+if __name__ == "__main__":
+ main()
diff --git a/metomi/rose/run.py b/metomi/rose/run.py
index d1be7e3398..0f71074b80 100644
--- a/metomi/rose/run.py
+++ b/metomi/rose/run.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -17,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with Rose. If not, see .
# -----------------------------------------------------------------------------
-"""Shared utilities for app/suite/task run."""
+"""Shared utilities for app/task run."""
import os
from metomi.rose.config_processor import ConfigProcessorsManager
@@ -83,7 +80,7 @@ def __str__(self):
return "%s=%s, --new mode not supported." % self.args
-class Dummy(object):
+class Dummy:
"""Convert a dict into an object."""
@@ -92,9 +89,9 @@ def __init__(self, **kwargs):
setattr(self, key, value)
-class Runner(object):
+class Runner:
- """Invoke a Rose application or a Rose suite."""
+ """Invoke a Rose application."""
CONF_NAME = None
NAME = None
diff --git a/metomi/rose/run_source_vc.py b/metomi/rose/run_source_vc.py
index 315fd7ca50..6eacff572f 100644
--- a/metomi/rose/run_source_vc.py
+++ b/metomi/rose/run_source_vc.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -22,7 +19,6 @@
import os
from metomi.rose.popen import RosePopener
import sys
-import _io
from metomi.rose.unicode_utils import write_safely
diff --git a/metomi/rose/scheme_handler.py b/metomi/rose/scheme_handler.py
index ab1a509561..d193164af4 100644
--- a/metomi/rose/scheme_handler.py
+++ b/metomi/rose/scheme_handler.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -26,7 +23,7 @@
import sys
-class SchemeHandlersManager(object):
+class SchemeHandlersManager:
"""Load and select from a group of related functional classes."""
CAN_HANDLE = "can_handle"
diff --git a/metomi/rose/section.py b/metomi/rose/section.py
index c2edf09194..746ffe1692 100644
--- a/metomi/rose/section.py
+++ b/metomi/rose/section.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -25,7 +22,7 @@
import copy
-class Section(object):
+class Section:
"""This class stores the data and metadata of an input section.
diff --git a/metomi/rose/stem.py b/metomi/rose/stem.py
deleted file mode 100644
index 8c777e8b1c..0000000000
--- a/metomi/rose/stem.py
+++ /dev/null
@@ -1,497 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Implementation of 'rose stem'"""
-
-import os
-import re
-import sys
-
-import metomi.rose.config
-from metomi.rose.fs_util import FileSystemUtil
-from metomi.rose.host_select import HostSelector
-from metomi.rose.opt_parse import RoseOptionParser
-from metomi.rose.popen import RosePopener, RosePopenError
-from metomi.rose.reporter import Reporter, Event
-from metomi.rose.resource import ResourceLocator
-from metomi.rose.suite_run import SuiteRunner
-
-DEFAULT_TEST_DIR = 'rose-stem'
-OPTIONS = ['group', 'source', 'task', ]
-ROSE_STEM_VERSION = 1
-SUITE_RC_PREFIX = '[jinja2:suite.rc]'
-
-
-class ConfigVariableSetEvent(Event):
-
- """Event to report a particular variable has been set."""
-
- LEVEL = Event.V
-
- def __repr__(self):
- return "Variable %s set to %s" % (self.args[0], self.args[1])
-
- __str__ = __repr__
-
-
-class ConfigSourceTreeSetEvent(Event):
-
- """Event to report a source tree for config files."""
-
- LEVEL = Event.V
-
- def __repr__(self):
- return "Using config files from source %s" % (self.args[0])
-
- __str__ = __repr__
-
-
-class NameSetEvent(Event):
-
- """Event to report a name for the suite being set."""
-
- LEVEL = Event.V
-
- def __repr__(self):
- return "Suite is named %s" % (self.args[0])
-
- __str__ = __repr__
-
-
-class ProjectNotFoundException(Exception):
-
- """Exception class when unable to determine project a source belongs to."""
-
- def __init__(self, source, error=None):
- Exception.__init__(self, source, error)
- self.source = source
- self.error = error
-
- def __repr__(self):
- if self.error is not None:
- return "Cannot ascertain project for source tree %s:\n%s" % (
- self.source, self.error)
- else:
- return "Cannot ascertain project for source tree %s" % (
- self.source)
-
- __str__ = __repr__
-
-
-class RoseStemVersionException(Exception):
-
- """Exception class when running the wrong metomi.rose-stem version."""
-
- def __init__(self, version):
- Exception.__init__(self, version)
- if version is None:
- self.suite_version = "not metomi.rose-stem compatible"
- else:
- self.suite_version = "at version %s" % (version)
-
- def __repr__(self):
- return "Running metomi.rose-stem version %s but suite is %s" % (
- ROSE_STEM_VERSION, self.suite_version)
-
- __str__ = __repr__
-
-
-class RoseSuiteConfNotFoundException(Exception):
-
- """Exception class when unable to find metomi.rose-suite.conf."""
-
- def __init__(self, location):
- Exception.__init__(self, location)
- self.location = location
-
- def __repr__(self):
- if os.path.isdir(self.location):
- return "\nCannot find a suite to run in directory %s" % (
- self.location)
- else:
- return "\nSuite directory %s is not a valid directory" % (
- self.location)
-
- __str__ = __repr__
-
-
-class SourceTreeAddedAsBranchEvent(Event):
-
- """Event to report a source tree has been added as a branch."""
-
- LEVEL = Event.DEFAULT
-
- def __repr__(self):
- return "Source tree %s added as branch" % (self.args[0])
-
- __str__ = __repr__
-
-
-class SourceTreeAddedAsTrunkEvent(Event):
-
- """Event to report a source tree has been added as a trunk."""
-
- LEVEL = Event.DEFAULT
-
- def __repr__(self):
- return "Source tree %s added as trunk" % (self.args[0])
-
- __str__ = __repr__
-
-
-class SuiteSelectionEvent(Event):
-
- """Event to report a source tree for config files."""
-
- LEVEL = Event.DEFAULT
-
- def __repr__(self):
- return "Will run suite from %s" % (self.args[0])
-
- __str__ = __repr__
-
-
-class StemRunner(object):
-
- """Set up options for running a STEM job through Rose."""
-
- def __init__(self, opts, reporter=None, popen=None, fs_util=None):
- self.opts = opts
- if reporter is None:
- self.reporter = Reporter(opts.verbosity - opts.quietness)
- else:
- self.reporter = reporter
- if popen is None:
- self.popen = RosePopener(event_handler=self.reporter)
- else:
- self.popen = popen
- if fs_util is None:
- self.fs_util = FileSystemUtil(event_handler=self.reporter)
- else:
- self.fs_util = fs_util
- self.host_selector = HostSelector(event_handler=self.reporter,
- popen=self.popen)
-
- def _add_define_option(self, var, val):
- """Add a define option passed to the SuiteRunner."""
-
- if self.opts.defines:
- self.opts.defines.append(SUITE_RC_PREFIX + var + '=' + val)
- else:
- self.opts.defines = [SUITE_RC_PREFIX + var + '=' + val]
- self.reporter(ConfigVariableSetEvent(var, val))
- return
-
- def _get_base_dir(self, item):
- """Given a source tree return the following from 'fcm loc-layout':
- * url
- * sub_tree
- * peg_rev
- * root
- * project
- """
-
- ret_code, output, stderr = self.popen.run('fcm', 'loc-layout', item)
- output = output.decode()
- if ret_code != 0:
- raise ProjectNotFoundException(item, stderr)
-
- ret = {}
- for line in output.splitlines():
- if ":" not in line:
- continue
- key, value = line.split(":", 1)
- if key:
- if value:
- ret[key] = value.strip()
-
- return ret
-
- def _get_project_from_url(self, source_dict):
- """Run 'fcm keyword-print' to work out the project name."""
-
- repo = source_dict['root']
- if source_dict['project']:
- repo += '/' + source_dict['project']
-
- kpoutput = self.popen.run('fcm', 'kp', source_dict['url'])[1]
-
- project = None
- for line in kpoutput.splitlines():
- if line.rstrip().endswith(repo.encode('UTF-8')):
- kpresult = re.search(r'^location{primary}\[(.*)\]',
- line.decode())
- if kpresult:
- project = kpresult.group(1)
- break
- return project
-
- def _deduce_mirror(self, source_dict, project):
- """Deduce the mirror location of this source tree."""
-
- # Root location for project
- proj_root = source_dict['root'] + '/' + source_dict['project']
-
- # Swap project to mirror
- project = re.sub(r'\.x$', r'.xm', project)
- mirror_repo = "fcm:" + project
-
- # Generate mirror location
- mirror = re.sub(proj_root, mirror_repo, source_dict['url'])
-
- # Remove any sub-tree
- mirror = re.sub(source_dict['sub_tree'], r'', mirror)
- mirror = re.sub(r'/@', r'@', mirror)
-
- # Add forwards slash after .xm if missing
- if '.xm/' not in mirror:
- mirror = re.sub(r'\.xm', r'.xm/', mirror)
- return mirror
-
- def _ascertain_project(self, item):
- """Set the project name and top-level from 'fcm loc-layout'.
- Returns:
- * project name
- * top-level location of the source tree with revision number
- * top-level location of the source tree without revision number
- * revision number
- """
-
- project = None
- try:
- project, item = item.split("=", 1)
- except ValueError:
- pass
-
- if re.search(r'^\.', item):
- item = os.path.abspath(os.path.join(os.getcwd(), item))
-
- if project is not None:
- print("[WARN] Forcing project for '{0}' to be '{1}'".format(
- item, project))
- return project, item, item, '', ''
-
- source_dict = self._get_base_dir(item)
- project = self._get_project_from_url(source_dict)
- if not project:
- raise ProjectNotFoundException(item)
-
- mirror = self._deduce_mirror(source_dict, project)
-
- if 'peg_rev' in source_dict and '@' in item:
- revision = '@' + source_dict['peg_rev']
- base = re.sub(r'@.*', r'', item)
- else:
- revision = ''
- base = item
-
- # Remove subtree from base and item
- if 'sub_tree' in source_dict:
- item = re.sub(
- r'(.*)%s/?$' % (source_dict['sub_tree']), r'\1', item, count=1)
- base = re.sub(
- r'(.*)%s/?$' % (source_dict['sub_tree']), r'\1', base, count=1)
-
- # Remove trailing forwards-slash
- item = re.sub(r'/$', r'', item)
- base = re.sub(r'/$', r'', base)
-
- # Remove anything after a point
- project = re.sub(r'\..*', r'', project)
- return project, item, base, revision, mirror
-
- def _generate_name(self):
- """Generate a suite name from the name of the first source tree."""
- try:
- basedir = self._ascertain_project(os.getcwd())[1]
- except ProjectNotFoundException:
- if self.opts.conf_dir:
- basedir = os.path.abspath(self.opts.conf_dir)
- else:
- basedir = os.getcwd()
- name = os.path.basename(basedir)
- self.reporter(NameSetEvent(name))
- return name
-
- def _this_suite(self):
- """Find the location of the suite in the first source tree."""
-
- # Get base of first source
- basedir = ''
- if self.opts.source:
- basedir = self.opts.source[0]
- else:
- basedir = self._ascertain_project(os.getcwd())[1]
-
- suitedir = os.path.join(basedir, DEFAULT_TEST_DIR)
- suitefile = os.path.join(suitedir, "rose-suite.conf")
-
- if not os.path.isfile(suitefile):
- raise RoseSuiteConfNotFoundException(suitedir)
-
- self._check_suite_version(suitefile)
-
- return suitedir
-
- def _read_auto_opts(self):
- """Read the site metomi.rose.conf file."""
- return ResourceLocator.default().get_conf().get_value(
- ["rose-stem", "automatic-options"])
-
- def _check_suite_version(self, fname):
- """Check the suite is compatible with this version of metomi.rose-stem.
- """
- if not os.path.isfile(fname):
- raise RoseSuiteConfNotFoundException(os.path.dirname(fname))
- config = metomi.rose.config.load(fname)
- suite_rose_stem_version = config.get(['ROSE_STEM_VERSION'])
- if suite_rose_stem_version:
- suite_rose_stem_version = int(suite_rose_stem_version.value)
- else:
- suite_rose_stem_version = None
- if not suite_rose_stem_version == ROSE_STEM_VERSION:
- raise RoseStemVersionException(suite_rose_stem_version)
-
- def _prepend_localhost(self, url):
- """Prepend the local hostname to urls which do not point to repository
- locations."""
- if ':' not in url or url.split(':', 1)[0] not in ['svn', 'fcm', 'http',
- 'https', 'svn+ssh']:
- url = self.host_selector.get_local_host() + ':' + url
- return url
-
- def process(self):
- """Process STEM options into 'rose suite-run' options."""
-
- # Generate options for source trees
- repos = {}
- repos_with_hosts = {}
- if not self.opts.source:
- self.opts.source = ['.']
- self.opts.project = list()
-
- for i, url in enumerate(self.opts.source):
- project, url, base, rev, mirror = self._ascertain_project(url)
- self.opts.source[i] = url
- self.opts.project.append(project)
-
- # Versions of variables with hostname prepended for working copies
- url_host = self._prepend_localhost(url)
- base_host = self._prepend_localhost(base)
-
- if project in repos:
- repos[project].append(url)
- repos_with_hosts[project].append(url_host)
- else:
- repos[project] = [url]
- repos_with_hosts[project] = [url_host]
- self._add_define_option('SOURCE_' + project.upper() + '_REV',
- '"' + rev + '"')
- self._add_define_option('SOURCE_' + project.upper() + '_BASE',
- '"' + base + '"')
- self._add_define_option('HOST_SOURCE_' + project.upper() +
- '_BASE', '"' + base_host + '"')
- self._add_define_option('SOURCE_' + project.upper() +
- '_MIRROR', '"' + mirror + '"')
- self.reporter(SourceTreeAddedAsBranchEvent(url))
- for project, branches in repos.items():
- var = 'SOURCE_' + project.upper()
- branchstring = RosePopener.list_to_shell_str(branches)
- self._add_define_option(var, '"' + branchstring + '"')
- for project, branches in repos_with_hosts.items():
- var_host = 'HOST_SOURCE_' + project.upper()
- branchstring = RosePopener.list_to_shell_str(branches)
- self._add_define_option(var_host, '"' + branchstring + '"')
-
- # Generate the variable containing tasks to run
- if self.opts.group:
- if not self.opts.defines:
- self.opts.defines = []
- expanded_groups = []
- for i in self.opts.group:
- expanded_groups.extend(i.split(','))
- self.opts.defines.append(SUITE_RC_PREFIX + 'RUN_NAMES=' +
- str(expanded_groups))
-
- # Load the config file and return any automatic-options
- auto_opts = self._read_auto_opts()
- if auto_opts:
- automatic_options = auto_opts.split()
- for option in automatic_options:
- elements = option.split("=")
- if len(elements) == 2:
- self._add_define_option(
- elements[0], '"' + elements[1] + '"')
-
- # Change into the suite directory
- if self.opts.conf_dir:
- self.reporter(SuiteSelectionEvent(self.opts.conf_dir))
- self._check_suite_version(
- os.path.join(self.opts.conf_dir, 'rose-suite.conf'))
- else:
- thissuite = self._this_suite()
- self.fs_util.chdir(thissuite)
- self.reporter(SuiteSelectionEvent(thissuite))
-
- # Create a default name for the suite; allow override by user
- if not self.opts.name:
- self.opts.name = self._generate_name()
-
- return self.opts
-
-
-def main():
- """Launcher for command line invokation of metomi.rose stem."""
-
- # Process options
- opt_parser = RoseOptionParser()
-
- option_keys = SuiteRunner.OPTIONS + OPTIONS
- opt_parser.add_my_options(*option_keys)
- opts, args = opt_parser.parse_args()
-
- # Set up a runner instance and process the options
- stem = StemRunner(opts)
- if opts.debug_mode:
- opts = stem.process()
- else:
- try:
- opts = stem.process()
- except Exception as exc:
- stem.reporter(exc)
- sys.exit(1)
-
- # Get the suiterunner object and execute
- runner = SuiteRunner(event_handler=stem.reporter,
- popen=stem.popen,
- fs_util=stem.fs_util)
- if opts.debug_mode:
- sys.exit(runner(opts, args))
- try:
- sys.exit(runner(opts, args))
- except Exception as exc:
- runner.handle_event(exc)
- if isinstance(exc, RosePopenError):
- sys.exit(exc.ret_code)
- else:
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/metomi/rose/suite_clean.py b/metomi/rose/suite_clean.py
deleted file mode 100644
index 491895d495..0000000000
--- a/metomi/rose/suite_clean.py
+++ /dev/null
@@ -1,206 +0,0 @@
-# -*- coding: utf-8 -*-
-# ----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# ----------------------------------------------------------------------------
-"""Implement the "rose suite-clean" command."""
-
-import os
-from pipes import quote
-from metomi.rose.config import ConfigLoader, ConfigNode, ConfigSyntaxError
-from metomi.rose.fs_util import FileSystemEvent
-from metomi.rose.host_select import HostSelector
-from metomi.rose.opt_parse import RoseOptionParser
-from metomi.rose.popen import RosePopenError
-from metomi.rose.reporter import Reporter
-from metomi.rose.suite_engine_proc import (
- SuiteEngineProcessor, SuiteStillRunningError)
-import sys
-import traceback
-from uuid import uuid4
-from functools import cmp_to_key
-
-
-class SuiteRunCleaner(object):
-
- """Logic to remove items created by the previous runs of suites."""
-
- CLEANABLE_PATHS = ["share", "share/cycle", "work"]
-
- def __init__(self, event_handler=None, host_selector=None,
- suite_engine_proc=None):
- if event_handler is None:
- event_handler = Reporter()
- self.event_handler = event_handler
- if host_selector is None:
- host_selector = HostSelector(event_handler=event_handler)
- self.host_selector = host_selector
- if suite_engine_proc is None:
- suite_engine_proc = SuiteEngineProcessor.get_processor(
- event_handler=event_handler)
- self.suite_engine_proc = suite_engine_proc
-
- def clean(self, suite_name, only_items=None):
- """Remove items created by the previous run of a suite.
-
- Change to user's $HOME for safety.
-
- """
- os.chdir(os.path.expanduser('~'))
- self.suite_engine_proc.check_suite_not_running(suite_name)
- self._clean(suite_name, only_items)
-
- def _clean(self, suite_name, only_items=None):
- """Perform the cleaning operations."""
- engine = self.suite_engine_proc
- suite_dir_rel = engine.get_suite_dir_rel(suite_name)
- locs_file_path = engine.get_suite_dir(
- suite_name, "log", "rose-suite-run.locs")
- locs_conf = ConfigNode().set(["localhost"], {})
- try:
- ConfigLoader().load(locs_file_path, locs_conf)
- except IOError:
- pass
- items = self.CLEANABLE_PATHS + [""]
- if only_items:
- items = only_items
- items.sort()
- uuid_str = str(uuid4())
- for auth, node in sorted(locs_conf.value.items(),
- key=cmp_to_key(self._auth_node_cmp)):
- locs = []
- roots = set([""])
- for item in items:
- if item:
- locs.append(os.path.join(suite_dir_rel, item))
- else:
- locs.append(suite_dir_rel)
- if item and os.path.normpath(item) in self.CLEANABLE_PATHS:
- item_root = node.get_value(["root-dir{" + item + "}"])
- if item_root is None: # backward compat
- item_root = node.get_value(["root-dir-" + item])
- elif item == "":
- item_root = node.get_value(["root-dir"])
- else:
- continue
- if item_root:
- loc_rel = suite_dir_rel
- if item:
- loc_rel = os.path.join(suite_dir_rel, item)
- locs.append(os.path.join(item_root, loc_rel))
- roots.add(item_root)
- locs.reverse()
- # Invoke bash as a login shell. The root location of a path may be
- # in $DIR syntax, which can only be expanded correctly in a login
- # shell. However, profile scripts invoked on login shell may print
- # lots of junks. Hence we use a UUID here as a delimiter. Only
- # output after the UUID lines are desirable lines.
- command = ["bash", "-l", "-O", "extglob", "-c"]
- sh_command = "cd; echo '%s'" % (uuid_str,)
- if not self.host_selector.is_local_host(auth):
- command = engine.popen.get_cmd("ssh", auth) + command
- sh_command += "; ls -d -r %(locs)s; rm -fr %(locs)s" % {
- "locs": engine.popen.list_to_shell_str(locs)}
- if not only_items:
- # Clean empty directories
- # Change directory to root level to avoid cleaning them as
- # well For cylc suites, e.g. it can clean up to an empty
- # "cylc-run/" directory.
- for root in roots:
- names = []
- # Reverse sort to ensure that e.g. "share/cycle/" is
- # cleaned before "share/"
- for name in sorted(self.CLEANABLE_PATHS, reverse=True):
- names.append(os.path.join(suite_dir_rel, name))
- if os.sep in suite_dir_rel:
- names.append(os.path.dirname(suite_dir_rel))
- sh_command += (
- "; " +
- "(cd %(root)s; " +
- "rmdir -p %(names)s 2>/dev/null || true)"
- ) % {
- "root": root,
- "names": engine.popen.list_to_shell_str(names),
- }
- if self.host_selector.is_local_host(auth):
- command.append(sh_command)
- else:
- command.append(quote(sh_command))
- is_after_uuid_str = False
- for line in engine.popen(*command)[0].splitlines():
- line = line.decode()
- if is_after_uuid_str:
- engine.handle_event(FileSystemEvent(
- FileSystemEvent.DELETE, auth + ":" + line.strip()))
- elif line == uuid_str:
- is_after_uuid_str = True
-
- __call__ = clean
-
- @staticmethod
- def _auth_node_cmp(item1, item2):
- """Compare (auth1, node1) and (auth2, node2)."""
- # This logic replicates output of the deprecated Python2 `cmp` builtin
- ret = (item1 > item2) - (item1 < item2)
- if ret:
- if item1[0] == "localhost":
- return -1
- elif item2[0] == "localhost":
- return 1
- return ret
-
-
-def main():
- """Implement the "rose suite-clean" command."""
- opt_parser = RoseOptionParser()
- opt_parser.add_my_options("name", "non_interactive", "only_items")
- opts, args = opt_parser.parse_args()
- report = Reporter(opts.verbosity - opts.quietness)
- cleaner = SuiteRunCleaner(event_handler=report)
- if opts.name:
- args.append(opts.name)
- if not args:
- args = [os.path.basename(os.getcwd())]
- os.chdir(os.path.expanduser('~'))
- n_done = 0
- for arg in args:
- if not opts.non_interactive:
- try:
- answer = input("Clean %s? y/n (default n) " % arg)
- except EOFError:
- sys.exit(1)
- if answer not in ["Y", "y"]:
- continue
- try:
- cleaner.clean(arg, opts.only_items)
- except (
- OSError,
- IOError,
- ConfigSyntaxError,
- RosePopenError,
- SuiteStillRunningError,
- ) as exc:
- report(exc)
- if opts.debug_mode:
- traceback.print_exc()
- else:
- n_done += 1
- sys.exit(len(args) - n_done) # Return 0 if everything done
-
-
-if __name__ == "__main__":
- main()
diff --git a/metomi/rose/suite_control.py b/metomi/rose/suite_control.py
deleted file mode 100644
index 7945fc2183..0000000000
--- a/metomi/rose/suite_control.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Launch suite engine's control commands."""
-
-import os
-from metomi.rose.fs_util import FileSystemUtil
-from metomi.rose.opt_parse import RoseOptionParser
-from metomi.rose.reporter import Reporter
-from metomi.rose.suite_engine_proc import SuiteEngineProcessor
-import sys
-
-
-YES = "y"
-PROMPT = "Really %s %s? [" + YES + " or n (default)] "
-
-
-class SuiteControl(object):
- """Launch suite engine's control commands from the correct suite host."""
-
- def __init__(self, event_handler=None):
- self.event_handler = event_handler
- self.suite_engine_proc = SuiteEngineProcessor.get_processor(
- event_handler=event_handler)
-
- def shutdown(self, suite_name, confirm=None, stderr=None,
- stdout=None, *args):
- """Shutdown the suite.
-
- suite_name: the name of the suite.
- confirm: If specified, must be a callable with the interface
- b = confirm("shutdown", suite_name, host). This method will
- only issue the shutdown command to suite_name at host if b is
- True.
- stderr: A file handle for stderr, if relevant for suite engine.
- stdout: A file handle for stdout, if relevant for suite engine.
- args: extra arguments for the suite engine's shutdown command.
-
- """
- if confirm is None or confirm("shutdown", suite_name):
- self.suite_engine_proc.shutdown(suite_name, args, stderr, stdout)
-
-
-class SuiteNotFoundError(Exception):
-
- """An exception raised when a suite can't be found at or below cwd."""
- def __str__(self):
- return ("%s - no suite found for this path." % self.args[0])
-
-
-def get_suite_name(event_handler=None):
- """Find the top level of a suite directory structure"""
- fs_util = FileSystemUtil(event_handler)
- conf_dir = os.getcwd()
- while True:
- if os.path.basename(conf_dir) != "rose-stem":
- for tail in [
- "rose-suite.conf",
- "log/rose-suite-run.conf",
- "rose-stem/rose-suite.conf"]:
- conf = os.path.join(conf_dir, tail)
- if os.path.exists(conf):
- return os.path.basename(conf_dir)
- up_dir = fs_util.dirname(conf_dir)
- if up_dir == conf_dir:
- raise SuiteNotFoundError(os.getcwd())
- conf_dir = up_dir
-
-
-def prompt(action, suite_name, host):
- """Prompt user to confirm action for suite_name at host."""
- if not host:
- host = "localhost"
- return input(PROMPT % (action, suite_name, host)).strip() in [YES]
-
-
-def main():
- """Implement "rose suite-shutdown"."""
- argv = sys.argv[1:]
- method_name = argv.pop(0)
- opt_parser = RoseOptionParser()
- opt_parser.add_my_options("name", "non_interactive")
- opts, args = opt_parser.parse_args(argv)
- event_handler = Reporter(opts.verbosity - opts.quietness)
- suite_control = SuiteControl(event_handler=event_handler)
- method = getattr(suite_control, method_name)
- confirm = None
- suite_names = []
- if not opts.non_interactive:
- confirm = prompt
- else:
- if opts.name:
- suite_names.append(opts.name)
- else:
- try:
- suite_name = get_suite_name(event_handler)
- suite_names.append(suite_name)
- except SuiteNotFoundError as exc:
- event_handler(exc)
- sys.exit(1)
-
- if opts.debug_mode:
- for sname in suite_names:
- method(sname, confirm, sys.stderr, sys.stdout, *args)
- else:
- for sname in suite_names:
- try:
- method(sname, confirm, sys.stderr, sys.stdout, *args)
- except Exception as exc:
- event_handler(exc)
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/metomi/rose/suite_engine_proc.py b/metomi/rose/suite_engine_proc.py
index 26993f27bf..454aa95636 100644
--- a/metomi/rose/suite_engine_proc.py
+++ b/metomi/rose/suite_engine_proc.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -19,21 +16,18 @@
# -----------------------------------------------------------------------------
"""Suite engine processor management."""
-from metomi.isodatetime.data import Duration
-from metomi.isodatetime.parsers import DurationParser, ISO8601SyntaxError
-from glob import glob
import os
-import pwd
import re
+import sys
+
+from metomi.isodatetime.data import Duration
+from metomi.isodatetime.parsers import DurationParser, ISO8601SyntaxError
from metomi.rose.date import RoseDateTimeOperator, OffsetValueError
from metomi.rose.fs_util import FileSystemUtil
from metomi.rose.host_select import HostSelector
from metomi.rose.popen import RosePopener
from metomi.rose.reporter import Event
-from metomi.rose.resource import ResourceLocator
from metomi.rose.scheme_handler import SchemeHandlersManager
-import sys
-import webbrowser
class NoSuiteLogError(Exception):
@@ -62,7 +56,7 @@ def __str__(self):
return "%s %s" % self.args
-class BaseCycleOffset(object):
+class BaseCycleOffset:
"""Represent a cycle time offset."""
@@ -163,35 +157,6 @@ def to_duration(self):
return self.duration
-class SuiteEngineGlobalConfCompatError(Exception):
-
- """An exception raised on incompatible global configuration."""
-
- def __str__(self):
- engine, key, value = self.args
- return ("%s global configuration incompatible to Rose: %s=%s" %
- (engine, key, value))
-
-
-class SuiteNotRunningError(Exception):
-
- """An exception raised when a suite is not running."""
-
- def __str__(self):
- return "%s: does not appear to be running" % (self.args)
-
-
-class SuiteStillRunningError(Exception):
-
- """An exception raised when a suite is still running."""
-
- FMT_HEAD = "Suite \"%(suite_name)s\" appears to be running:\n"
-
- def __str__(self):
- suite_name, extras = self.args
- return self.FMT_HEAD % {"suite_name": suite_name} + "".join(extras)
-
-
class CycleOffsetError(ValueError):
"""Unrecognised cycle time offset format."""
@@ -213,7 +178,7 @@ def __str__(self):
return self.args[0] + ": unrecognised cycling mode."
-class TaskProps(object):
+class TaskProps:
"""Task properties.
@@ -291,7 +256,7 @@ def __str__(self):
return ret
-class SuiteEngineProcessor(object):
+class SuiteEngineProcessor:
"""An abstract suite engine processor."""
TASK_NAME_DELIM = {"prefix": "_", "suffix": "_"}
@@ -331,47 +296,6 @@ def __init__(self, event_handler=None, popen=None, fs_util=None,
self.host_selector = host_selector
self.date_time_oper = RoseDateTimeOperator()
- def check_global_conf_compat(self):
- """Raise exception on suite engine specific incompatibity.
-
- Should raise SuiteEngineGlobalConfCompatError.
-
- """
- raise NotImplementedError()
-
- def check_suite_not_running(self, suite_name):
- """Check that suite is not running.
-
- This method is not implemented. Sub-class should override.
-
- Arguments:
- suite_name: name of suite to check.
-
- Raise:
- SuiteStillRunningError:
- Should raise SuiteStillRunningError if suite is still running.
- """
- raise NotImplementedError()
-
- def cmp_suite_conf(
- self, suite_name, run_mode, strict_mode=False, debug_mode=False):
- """Compare current suite configuration with that in the previous run.
-
- An implementation of this method should:
- * Raise an exception on failure.
- * Return True if suite configuration is unmodified c.f. previous run.
- * Return False otherwise.
-
- """
- raise NotImplementedError()
-
- def get_suite_contact(self, suite_name):
- """Return suite contact information for a user suite.
-
- Return (dict): suite contact information.
- """
- raise NotImplementedError()
-
def get_suite_dir(self, suite_name, *paths):
"""Return the path to the suite running directory.
@@ -389,48 +313,10 @@ def get_suite_dir_rel(self, suite_name, *paths):
"""
raise NotImplementedError()
- def get_suite_log_url(self, user_name, suite_name):
- """Return the "rose bush" URL for a user's suite."""
- prefix = "~"
- if user_name:
- prefix += user_name
- suite_d = os.path.join(prefix, self.get_suite_dir_rel(suite_name))
- suite_d = os.path.expanduser(suite_d)
- if not os.path.isdir(suite_d):
- raise NoSuiteLogError(user_name, suite_name)
- rose_bush_url = None
- for f_name in glob(os.path.expanduser("~/.metomi/rose-bush*.status")):
- status = {}
- for line in open(f_name):
- key, value = line.strip().split("=", 1)
- status[key] = value
- if status.get("host"):
- rose_bush_url = "http://" + status["host"]
- if status.get("port"):
- rose_bush_url += ":" + status["port"]
- rose_bush_url += "/"
- break
- if not rose_bush_url:
- conf = ResourceLocator.default().get_conf()
- rose_bush_url = conf.get_value(
- ["rose-suite-log", "rose-bush"])
- if not rose_bush_url:
- return "file://" + suite_d
- if not rose_bush_url.endswith("/"):
- rose_bush_url += "/"
- if not user_name:
- user_name = pwd.getpwuid(os.getuid()).pw_name
- return rose_bush_url + "/".join(
- ["taskjobs", user_name, suite_name])
-
def get_task_auth(self, suite_name, task_name):
"""Return [user@]host for a remote task in a suite."""
raise NotImplementedError()
- def get_tasks_auths(self, suite_name):
- """Return a list of [user@]host for remote tasks in a suite."""
- raise NotImplementedError()
-
def get_task_props(self, *args, **kwargs):
"""Return a TaskProps object containing suite task's attributes."""
calendar_mode = self.date_time_oper.get_calendar_mode()
@@ -531,23 +417,11 @@ def get_task_props_from_env(self):
"""
raise NotImplementedError()
- def get_version(self):
- """Return the version string of the suite engine."""
- raise NotImplementedError()
-
- def get_version_env_name(self):
- """Return the name of the suite engine version environment variable."""
- return self.SCHEME.upper() + "_VERSION"
-
def handle_event(self, *args, **kwargs):
"""Call self.event_handler if it is callable."""
if callable(self.event_handler):
return self.event_handler(*args, **kwargs)
- def is_suite_registered(self, suite_name):
- """Return whether or not a suite is registered."""
- raise NotImplementedError()
-
def job_logs_archive(self, suite_name, items):
"""Archive cycle job logs.
@@ -578,30 +452,10 @@ def job_logs_remove_on_server(self, suite_name, items):
"""
raise NotImplementedError()
- def launch_suite_log_browser(self, user_name, suite_name):
- """Launch web browser to view suite log.
-
- Return URL of suite log on success, None otherwise.
-
- """
- url = self.get_suite_log_url(user_name, suite_name)
- browser = webbrowser.get()
- browser.open(url, new=True, autoraise=True)
- self.handle_event(WebBrowserEvent(browser.name, url))
- return url
-
def parse_job_log_rel_path(self, f_name):
"""Return (cycle, task, submit_num, ext) for a job log rel path."""
raise NotImplementedError()
- def run(self, suite_name, host=None, run_mode=None, args=None):
- """Start a suite (in a specified host)."""
- raise NotImplementedError()
-
- def shutdown(self, suite_name, args=None, stderr=None, stdout=None):
- """Shut down the suite."""
- raise NotImplementedError()
-
def _get_offset_cycle_time(self, cycle, cycle_offset):
"""Return the actual date time of an BaseCycleOffset against cycle.
diff --git a/metomi/rose/suite_engine_procs/__init__.py b/metomi/rose/suite_engine_procs/__init__.py
index 8460776f05..f7b71f8284 100644
--- a/metomi/rose/suite_engine_procs/__init__.py
+++ b/metomi/rose/suite_engine_procs/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/suite_engine_procs/cylc.py b/metomi/rose/suite_engine_procs/cylc.py
index 0ec4fb84e8..0475d9c363 100644
--- a/metomi/rose/suite_engine_procs/cylc.py
+++ b/metomi/rose/suite_engine_procs/cylc.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -19,7 +16,6 @@
# -----------------------------------------------------------------------------
"""Logic specific to the Cylc suite engine."""
-import filecmp
from glob import glob
import os
import pwd
@@ -28,33 +24,26 @@
import socket
import sqlite3
import tarfile
-from tempfile import mkstemp
from time import sleep
from uuid import uuid4
from metomi.rose.fs_util import FileSystemEvent
from metomi.rose.popen import RosePopenError
-from metomi.rose.reporter import Event, Reporter
+from metomi.rose.reporter import Reporter
from metomi.rose.suite_engine_proc import (
- SuiteEngineProcessor, SuiteEngineGlobalConfCompatError,
- SuiteNotRunningError, SuiteStillRunningError, TaskProps)
-
-
-_PORT_SCAN = "port-scan"
+ SuiteEngineProcessor,
+ TaskProps
+)
class CylcProcessor(SuiteEngineProcessor):
"""Logic specific to the cylc suite engine."""
- CONTACT_KEYS = (
- "CYLC_SUITE_HOST", "CYLC_SUITE_OWNER", "CYLC_SUITE_PORT",
- "CYLC_SUITE_PROCESS")
- PGREP_CYLC_RUN = r"python.*/bin/cylc-(run|restart)( | .+ )%s( |$)"
REC_CYCLE_TIME = re.compile(
r"\A[\+\-]?\d+(?:W\d+)?(?:T\d+(?:Z|[+-]\d+)?)?\Z") # Good enough?
SCHEME = "cylc"
- SUITE_CONF = "suite.rc"
+ SUITE_CONF = "flow.cylc"
SUITE_NAME_ENV = "CYLC_SUITE_NAME"
SUITE_DIR_REL_ROOT = "cylc-run"
TASK_ID_DELIM = "."
@@ -67,108 +56,6 @@ def __init__(self, *args, **kwargs):
self.host = None
self.user = None
- def check_global_conf_compat(self):
- """Raise exception on incompatible Cylc global configuration."""
- expected = os.path.join("~", self.SUITE_DIR_REL_ROOT)
- expected = os.path.expanduser(expected)
- for key in ["[hosts][localhost]run directory",
- "[hosts][localhost]work directory"]:
- out = self.popen("cylc", "get-global-config", "-i", key)[0]
- lines = out.splitlines()
- try:
- lines[0] = lines[0].decode()
- except AttributeError:
- pass
- if lines and lines[0] != expected:
- raise SuiteEngineGlobalConfCompatError(
- self.SCHEME, key, lines[0])
-
- def check_suite_not_running(self, suite_name):
- """Check if a suite is still running.
-
- Args:
- suite_name (str): the name of the suite as known by Cylc.
- Raise:
- SuiteStillRunningError: if suite is still running.
- """
- try:
- contact_info = self.get_suite_contact(suite_name)
- except SuiteNotRunningError:
- return # No contact file, suite not running
- else:
- fname = self.get_suite_dir(suite_name, ".service", "contact")
- extras = ["Contact info from: \"%s\"\n" % fname]
- for key, value in sorted(contact_info.items()):
- if key in self.CONTACT_KEYS:
- extras.append(" %s=%s\n" % (key, value))
- extras.append("Try \"cylc stop '%s'\" first?" % suite_name)
- raise SuiteStillRunningError(suite_name, extras)
-
- def cmp_suite_conf(
- self, suite_name, run_mode, strict_mode=False, debug_mode=False):
- """Parse and compare current "suite.rc" with that in the previous run.
-
- (Re-)register and validate the "suite.rc" file.
- Raise RosePopenError on failure.
- Return True if "suite.rc.processed" is unmodified c.f. previous run.
- Return False otherwise.
-
- """
- suite_dir = self.get_suite_dir(suite_name)
- if run_mode == "run":
- self.popen.run_simple("cylc", "register", suite_name, suite_dir)
- f_desc, new_suite_rc_processed = mkstemp()
- os.close(f_desc)
- command = ["cylc", "validate", "-o", new_suite_rc_processed]
- if debug_mode:
- command.append("--debug")
- if strict_mode:
- command.append("--strict")
- command.append(suite_name)
- old_suite_rc_processed = os.path.join(suite_dir, "suite.rc.processed")
- try:
- self.popen.run_simple(*command, stdout_level=Event.V)
- return (
- os.path.exists(old_suite_rc_processed) and
- filecmp.cmp(old_suite_rc_processed, new_suite_rc_processed))
- finally:
- os.unlink(new_suite_rc_processed)
-
- def get_running_suites(self):
- """Return a list containing the names of running suites."""
- rootd = os.path.join(os.path.expanduser('~'), self.SUITE_DIR_REL_ROOT)
- sub_names = ['.service', 'log', 'share', 'work', self.SUITE_CONF]
- items = []
- for dirpath, dnames, fnames in os.walk(rootd, followlinks=True):
- if dirpath != rootd and any(
- name in dnames + fnames for name in sub_names):
- dnames[:] = []
- else:
- continue
- if os.path.exists(os.path.join(dirpath, '.service', 'contact')):
- items.append(os.path.relpath(dirpath, rootd))
- return items
-
- def get_suite_contact(self, suite_name):
- """Return suite contact information for a user suite.
-
- Return (dict): suite contact information.
- """
- try:
- # Note: low level directory open ensures that the file system
- # containing the contact file is synchronised, e.g. in an NFS
- # environment.
- os.close(os.open(
- self.get_suite_dir(suite_name, ".service"), os.O_DIRECTORY))
- ret = {}
- for line in open(
- self.get_suite_dir(suite_name, ".service", "contact")):
- key, value = [item.strip() for item in line.split("=", 1)]
- ret[key] = value
- return ret
- except (IOError, OSError, ValueError):
- raise SuiteNotRunningError(suite_name)
-
@classmethod
def get_suite_dir_rel(cls, suite_name, *paths):
"""Return the relative path to the suite running directory.
@@ -180,81 +67,33 @@ def get_suite_dir_rel(cls, suite_name, *paths):
def get_suite_jobs_auths(self, suite_name, cycle_name_tuples=None):
"""Return remote ["[user@]host", ...] for submitted jobs."""
- auths = []
- stmt = "SELECT DISTINCT user_at_host FROM task_jobs"
- stmt_where_list = []
- stmt_args = []
- if cycle_name_tuples:
- for cycle, name in cycle_name_tuples:
- stmt_fragments = []
- if cycle is not None:
- stmt_fragments.append("cycle==?")
- stmt_args.append(cycle)
- if name is not None:
- stmt_fragments.append("name==?")
- stmt_args.append(name)
- stmt_where_list.append(" AND ".join(stmt_fragments))
- if stmt_where_list:
- stmt += " WHERE (" + ") OR (".join(stmt_where_list) + ")"
- for row in self._db_exec(suite_name, stmt, stmt_args):
- if row and row[0]:
- auth = self._parse_user_host(auth=row[0])
- if auth:
- auths.append(auth)
- self._db_close(suite_name)
- return auths
+ # TODO: reimplement for Cylc8?
+ # https://github.com/metomi/rose/issues/2445
+ self.handle_event(
+ Exception(
+ 'WARNING: Rose cannot currently inspect the platform a Cylc'
+ ' task has or will run on.\n'
+ ' https://github.com/metomi/rose/issues/2445'
+ )
+ )
+ return []
def get_task_auth(self, suite_name, task_name):
- """
- Return [user@]host for a remote task in a suite.
+ """Return [user@]host for a remote task in a suite.
Or None if task does not run remotely.
"""
- try:
- out = self.popen(
- "cylc", "get-config", "-o",
- "-i", "[runtime][%s][remote]owner" % task_name,
- "-i", "[runtime][%s][remote]host" % task_name,
- suite_name)[0]
- except RosePopenError:
- return
- user, host = (None, None)
- items = out.strip().split(None, 1)
- if items:
- user = items.pop(0).replace(b"*", b" ")
- if items:
- host = items.pop(0).replace(b"*", b" ")
- return self._parse_user_host(user=user, host=host)
-
- def get_tasks_auths(self, suite_name):
- """Return a list of unique [user@]host for remote tasks in a suite."""
- actual_hosts = {}
- auths = []
- out = self.popen("cylc", "get-config", "-ao",
- "-i", "[remote]owner",
- "-i", "[remote]host",
- suite_name)[0]
- for line in out.splitlines():
- items = line.split(None, 2)
- user, host = (None, None)
- items.pop(0)
- if items:
- user = items.pop(0).decode().replace("*", " ")
- if items:
- host = items.pop(0).decode().replace("*", " ")
- if host in actual_hosts:
- host = str(actual_hosts[host])
- auth = self._parse_user_host(user=user, host=host)
- else:
- auth = self._parse_user_host(user=user, host=host)
- if auth and "@" in auth:
- actual_hosts[host] = auth.split("@", 1)[1]
- else:
- actual_hosts[host] = auth
- if auth and auth not in auths:
- auths.append(auth)
- return auths
+ # TODO: reimplement for Cylc8?
+ # https://github.com/metomi/rose/issues/2445
+ self.handle_event(
+ Exception(
+ 'WARNING: Rose cannot currently inspect the platform a Cylc'
+ ' task has or will run on.\n'
+ ' https://github.com/metomi/rose/issues/2445'
+ )
+ )
+ return None
def get_task_props_from_env(self):
"""Get attributes of a suite task from environment variables.
@@ -288,17 +127,6 @@ def get_task_props_from_env(self):
task_is_cold_start=task_is_cold_start,
cycling_mode=cycling_mode)
- def get_version(self):
- """Return Cylc's version."""
- return self.popen("cylc", "--version")[0].strip()
-
- def is_suite_registered(self, suite_name):
- """See if a suite is registered
- Return True directory for a suite if it is registered
- Return False otherwise
- """
- return self.popen.run("cylc", "get-directory", suite_name)[0] == 0
-
def job_logs_archive(self, suite_name, items):
"""Archive cycle job logs.
@@ -503,62 +331,6 @@ def parse_job_log_rel_path(cls, f_name):
"""Return (cycle, task, submit_num, ext)."""
return f_name.replace("log/job/", "").split("/", 3)
- @staticmethod
- def process_suite_hook_args(*args, **_):
- """Rearrange args for TaskHook.run."""
- task = None
- if len(args) == 3:
- hook_event, suite, hook_message = args
- else:
- hook_event, suite, task, hook_message = args
- return [suite, task, hook_event, hook_message]
-
- def run(self, suite_name, host=None, run_mode=None, args=None):
- """Invoke "cylc run" (in a specified host).
-
- The current working directory is assumed to be the suite log directory.
-
- suite_name: the name of the suite.
- host: the host to run the suite. "localhost" if None.
- run_mode: call "cylc restart|reload" instead of "cylc run".
- args: arguments to pass to "cylc run".
-
- """
- if run_mode not in ['reload', 'restart', 'run']:
- run_mode = 'run'
- cmd = ['cylc', run_mode]
- if run_mode == 'reload':
- cmd.append('--force')
- if host and not self.host_selector.is_local_host(host):
- cmd.append('--host=%s' % host)
- cmd.append(suite_name)
- cmd += args
- out, err = self.popen(*cmd)
- if err:
- self.handle_event(err, kind=Event.KIND_ERR)
- if out:
- self.handle_event(out)
-
- def shutdown(self, suite_name, args=None, stderr=None, stdout=None):
- """Shut down the suite.
-
- suite_name -- the name of the suite.
- stderr -- A file handle for stderr, if relevant for suite engine.
- stdout -- A file handle for stdout, if relevant for suite engine.
- args -- extra arguments for "cylc shutdown".
-
- """
- contact_info = self.get_suite_contact(suite_name)
- if not contact_info:
- raise SuiteNotRunningError(suite_name)
- environ = dict(os.environ)
- environ.update({'CYLC_VERSION': contact_info['CYLC_VERSION']})
- command = ["cylc", "shutdown", suite_name, "--force"]
- if args:
- command += args
- self.popen.run_simple(
- *command, env=environ, stderr=stderr, stdout=stdout)
-
def _db_close(self, suite_name):
"""Close a named database connection."""
if self.daos.get(suite_name) is not None:
@@ -621,7 +393,7 @@ def _parse_user_host(self, auth=None, user=None, host=None):
return auth
-class CylcSuiteDAO(object):
+class CylcSuiteDAO:
"""Generic SQLite Data Access Object."""
diff --git a/metomi/rose/suite_hook.py b/metomi/rose/suite_hook.py
deleted file mode 100644
index 306db4482b..0000000000
--- a/metomi/rose/suite_hook.py
+++ /dev/null
@@ -1,149 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-
-"""Hook functionalities for a suite."""
-
-from email.mime.text import MIMEText
-import os
-import pwd
-from metomi.rose.opt_parse import RoseOptionParser
-from metomi.rose.popen import RosePopener
-from metomi.rose.reporter import Reporter
-from metomi.rose.resource import ResourceLocator
-from metomi.rose.suite_engine_proc import SuiteEngineProcessor
-from smtplib import SMTP, SMTPException
-import socket
-
-
-class RoseSuiteHook(object):
-
- """Hook functionalities for a suite."""
-
- def __init__(self, event_handler=None, popen=None, suite_engine_proc=None):
- self.event_handler = event_handler
- if popen is None:
- popen = RosePopener(event_handler)
- self.popen = popen
- if suite_engine_proc is None:
- suite_engine_proc = SuiteEngineProcessor.get_processor(
- event_handler=event_handler, popen=popen)
- self.suite_engine_proc = suite_engine_proc
-
- def handle_event(self, *args, **kwargs):
- """Call self.event_handler if it is callabale."""
- if callable(self.event_handler):
- return self.event_handler(*args, **kwargs)
-
- def run(self, suite_name, task_id, hook_event, hook_message=None,
- should_mail=False, mail_cc_list=None, should_shutdown=False,
- should_retrieve_job_logs=False):
- """
- Invoke the hook for a suite.
-
- 1. For a task hook, if the task runs remotely, retrieve its log from
- the remote host.
- 2. If "should_mail", send an email notification to the current user,
- and those in the "mail_cc_list".
- 3. If "should_shutdown", shut down the suite.
-
- """
- # Retrieve log and populate job logs database
- task_ids = []
- if task_id and should_retrieve_job_logs:
- task_ids = [task_id]
- self.suite_engine_proc.job_logs_pull_remote(suite_name, task_ids)
-
- # Send email notification if required
- email_exc = None
- if should_mail:
- text = ""
- if task_id:
- text += "Task: %s\n" % task_id
- if hook_message:
- text += "Message: %s\n" % hook_message
- url = self.suite_engine_proc.get_suite_log_url(None, suite_name)
- text += "See: %s\n" % (url)
- user = pwd.getpwuid(os.getuid()).pw_name
- conf = ResourceLocator.default().get_conf()
- host = conf.get_value(["rose-suite-hook", "email-host"],
- default="localhost")
- msg = MIMEText(text)
- msg["From"] = user + "@" + host
- msg["To"] = msg["From"]
- if mail_cc_list:
- mail_cc_addresses = []
- for mail_cc_address in mail_cc_list:
- if "@" not in mail_cc_address:
- mail_cc_address += "@" + host
- mail_cc_addresses.append(mail_cc_address)
- msg["Cc"] = ", ".join(mail_cc_addresses)
- mail_cc_list = mail_cc_addresses
- else:
- mail_cc_list = []
- msg["Subject"] = "[%s] %s" % (hook_event, suite_name)
- smtp_host = conf.get_value(["rose-suite-hook", "smtp-host"],
- default="localhost")
- try:
- smtp = SMTP(smtp_host)
- smtp.sendmail(
- msg["From"], [msg["To"]] + mail_cc_list, msg.as_string())
- smtp.quit()
- except (socket.error, SMTPException) as email_exc:
- pass
-
- # Shut down if required
- if should_shutdown:
- self.suite_engine_proc.shutdown(suite_name, args=["--kill"])
-
- if email_exc is not None:
- raise
-
- __call__ = run
-
-
-def main():
- """Implement "rose suite-hook" command."""
- opt_parser = RoseOptionParser()
- opt_parser.add_my_options(
- "mail_cc", "mail", "retrieve_job_logs", "shutdown")
- opts, args = opt_parser.parse_args()
- for key in ["mail_cc"]:
- values = []
- if getattr(opts, key):
- for value in getattr(opts, key):
- values.extend(value.split(","))
- setattr(opts, key, values)
- report = Reporter(opts.verbosity - opts.quietness - 1) # Reduced default
- popen = RosePopener(event_handler=report)
- suite_engine_proc = SuiteEngineProcessor.get_processor(
- event_handler=report, popen=popen)
- args = suite_engine_proc.process_suite_hook_args(*args, **vars(opts))
- hook = RoseSuiteHook(event_handler=report,
- popen=popen,
- suite_engine_proc=suite_engine_proc)
- hook(*args,
- should_mail=opts.mail,
- mail_cc_list=opts.mail_cc,
- should_shutdown=opts.shutdown,
- should_retrieve_job_logs=opts.retrieve_job_logs)
-
-
-if __name__ == "__main__":
- main()
diff --git a/metomi/rose/suite_log.py b/metomi/rose/suite_log.py
deleted file mode 100644
index 150eeed738..0000000000
--- a/metomi/rose/suite_log.py
+++ /dev/null
@@ -1,105 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Implement "rose suite-log" CLI."""
-
-import os
-import pwd
-from metomi.rose.opt_parse import RoseOptionParser
-from metomi.rose.reporter import Event, Reporter
-from metomi.rose.suite_engine_proc import SuiteEngineProcessor
-from metomi.rose.suite_control import get_suite_name
-import sys
-from time import sleep
-import traceback
-
-
-class RoseBushStartEvent(Event):
-
- """Event raised on "rose bush start"."""
-
- def __str__(self):
- return ("""Rose bush started:\n%s""" % self.args[0] +
- """Run "rose bush stop" when no longer required.""")
-
-
-def main():
- """Implement "rose suite-log" CLI."""
- opt_parser = RoseOptionParser()
- opt_parser.add_my_options("archive_mode", "force_mode", "name",
- "non_interactive", "prune_remote_mode",
- "update_mode", "user", "view_mode")
- opts, args = opt_parser.parse_args()
- report = Reporter(opts.verbosity - opts.quietness)
-
- try:
- suite_log_view(opts, args, report)
- except Exception as exc:
- report(exc)
- if opts.debug_mode:
- traceback.print_exc()
- sys.exit(1)
-
-
-def suite_log_view(opts, args, event_handler=None):
- """Implement "rose suite-log" CLI functionality."""
- suite_engine_proc = SuiteEngineProcessor.get_processor(
- event_handler=event_handler)
- opts.update_mode = (
- opts.update_mode or opts.archive_mode or opts.force_mode)
- if opts.force_mode:
- args = ["*"]
- if not opts.name:
- opts.name = get_suite_name(event_handler)
- if not opts.update_mode and not opts.user:
- opts.user = pwd.getpwuid(os.stat(".").st_uid).pw_name
- if opts.archive_mode:
- suite_engine_proc.job_logs_archive(opts.name, args)
- elif opts.update_mode:
- suite_engine_proc.job_logs_pull_remote(
- opts.name, args, opts.prune_remote_mode, opts.force_mode)
- if opts.view_mode or not opts.update_mode:
- n_tries_left = 1
- is_rose_bush_started = False
- url = suite_engine_proc.get_suite_log_url(opts.user, opts.name)
- if url.startswith("file://"):
- if (opts.non_interactive or
- input(
- "Start rose bush? [y/n] (default=n) ") == "y"):
- suite_engine_proc.popen.run_bg(
- "rose", "bush", "start", preexec_fn=os.setpgrp)
- is_rose_bush_started = True
- n_tries_left = 5 # Give the server a chance to start
- while n_tries_left:
- n_tries_left -= 1
- if n_tries_left:
- url = suite_engine_proc.get_suite_log_url(opts.user, opts.name)
- if url.startswith("file://"):
- sleep(1)
- continue
- suite_engine_proc.launch_suite_log_browser(opts.user, opts.name)
- break
- if is_rose_bush_started:
- status = suite_engine_proc.popen("rose", "bush")[0]
- event_handler(RoseBushStartEvent(status))
- return
-
-
-if __name__ == "__main__":
- main()
diff --git a/metomi/rose/suite_restart.py b/metomi/rose/suite_restart.py
deleted file mode 100644
index e5292721a0..0000000000
--- a/metomi/rose/suite_restart.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Implement "rose suite-restart-only"."""
-
-import os
-import sys
-import traceback
-
-from metomi.rose.opt_parse import RoseOptionParser
-from metomi.rose.popen import RosePopenError
-from metomi.rose.reporter import Reporter
-from metomi.rose.suite_control import get_suite_name, SuiteNotFoundError
-from metomi.rose.suite_engine_proc import SuiteEngineProcessor
-
-
-class SuiteRestarter(object):
-
- """Wrap "cylc restart"."""
-
- def __init__(self, event_handler=None):
- self.event_handler = event_handler
- self.suite_engine_proc = SuiteEngineProcessor.get_processor(
- event_handler=self.event_handler)
-
- def handle_event(self, *args, **kwargs):
- """Handle event."""
- if callable(self.event_handler):
- self.event_handler(*args, **kwargs)
-
- def restart(self, suite_name=None, host=None, args=None):
- """Restart a "cylc" suite."""
- # Check suite engine specific compatibility
- self.suite_engine_proc.check_global_conf_compat()
-
- if not suite_name:
- suite_name = get_suite_name(self.event_handler)
-
- suite_dir = self.suite_engine_proc.get_suite_dir(suite_name)
- if not os.path.exists(suite_dir):
- raise SuiteNotFoundError(suite_dir)
-
- # Ensure suite is not running
- self.suite_engine_proc.check_suite_not_running(suite_name)
-
- # Restart the suite
- self.suite_engine_proc.run(suite_name, host, "restart", args)
-
- return
-
-
-def main():
- """Launcher for the CLI."""
- opt_parser = RoseOptionParser()
- opt_parser.add_my_options("host", "name")
- opts, args = opt_parser.parse_args(sys.argv[1:])
- event_handler = Reporter(opts.verbosity - opts.quietness)
- suite_restarter = SuiteRestarter(event_handler)
- try:
- sys.exit(suite_restarter.restart(
- suite_name=opts.name,
- host=opts.host,
- args=args))
- except Exception as exc:
- event_handler(exc)
- if opts.debug_mode:
- traceback.print_exc()
- if isinstance(exc, RosePopenError):
- sys.exit(exc.ret_code)
- else:
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/metomi/rose/suite_run.py b/metomi/rose/suite_run.py
deleted file mode 100644
index dff53bebaa..0000000000
--- a/metomi/rose/suite_run.py
+++ /dev/null
@@ -1,694 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Implement "rose suite-run"."""
-
-import ast
-from datetime import datetime
-from fnmatch import fnmatchcase
-from glob import glob
-import os
-import pipes
-from metomi.rose.config import ConfigDumper, ConfigLoader, ConfigNode
-from metomi.rose.env import env_var_process
-from metomi.rose.host_select import HostSelector
-from metomi.rose.opt_parse import RoseOptionParser
-from metomi.rose.popen import RosePopenError
-from metomi.rose.reporter import Event, Reporter, ReporterContext
-from metomi.rose.resource import ResourceLocator
-from metomi.rose.run import ConfigValueError, NewModeError, Runner
-from metomi.rose.run_source_vc import write_source_vc_info
-from metomi.rose.suite_clean import SuiteRunCleaner
-import shutil
-import sys
-from tempfile import TemporaryFile, mkdtemp
-from time import sleep, strftime, time
-import traceback
-
-
-class VersionMismatchError(Exception):
-
- """An exception raised when there is a version mismatch."""
-
- def __str__(self):
- return "Version expected=%s, actual=%s" % self.args
-
-
-class SkipReloadEvent(Event):
-
- """An event raised to report that suite configuration reload is skipped."""
-
- def __str__(self):
- return "%s: reload complete. \"%s\" unchanged" % self.args
-
-
-class SuiteLogArchiveEvent(Event):
-
- """An event raised to report the archiving of a suite log directory."""
-
- def __str__(self):
- return "%s <= %s" % self.args
-
-
-class SuiteRunner(Runner):
-
- """Invoke a Rose suite."""
-
- SLEEP_PIPE = 0.05
- NAME = "suite"
- OPTIONS = [
- "conf_dir",
- "defines",
- "defines_suite",
- "host",
- "install_only_mode",
- "local_install_only_mode",
- "log_archive_mode",
- "log_keep",
- "log_name",
- "name",
- "new_mode",
- "no_overwrite_mode",
- "opt_conf_keys",
- "reload_mode",
- "remote",
- "restart_mode",
- "run_mode",
- "strict_mode",
- "validate_suite_only"]
-
- # Lists of rsync (always) exclude globs
- SYNC_EXCLUDES = (
- "/.*",
- "/cylc-suite.db",
- "/log",
- "/log.*",
- "/state",
- "/share",
- "/work",
- )
-
- def __init__(self, *args, **kwargs):
- Runner.__init__(self, *args, **kwargs)
- self.host_selector = HostSelector(self.event_handler, self.popen)
- self.suite_run_cleaner = SuiteRunCleaner(
- event_handler=self.event_handler,
- host_selector=self.host_selector,
- suite_engine_proc=self.suite_engine_proc)
-
- def run_impl(self, opts, args, uuid, work_files):
- # Log file, temporary
- if hasattr(self.event_handler, "contexts"):
- t_file = TemporaryFile()
- log_context = ReporterContext(None, self.event_handler.VV, t_file)
- self.event_handler.contexts[uuid] = log_context
-
- # Check suite engine specific compatibility
- self.suite_engine_proc.check_global_conf_compat()
-
- # Suite name from the current working directory
- if opts.conf_dir:
- self.fs_util.chdir(opts.conf_dir)
- opts.conf_dir = os.getcwd()
-
- # --remote=KEY=VALUE,...
- if opts.remote:
- # opts.name always set for remote.
- return self._run_remote(opts, opts.name)
-
- conf_tree = self.config_load(opts)
- self.fs_util.chdir(conf_tree.conf_dirs[0])
-
- suite_name = opts.name
- if not opts.name:
- suite_name = os.path.basename(os.getcwd())
-
- # Check suite.rc #! line for template scheme
- templ_scheme = "jinja2"
- if self.suite_engine_proc.SUITE_CONF in conf_tree.files:
- suiterc_path = os.path.join(
- conf_tree.files[self.suite_engine_proc.SUITE_CONF],
- self.suite_engine_proc.SUITE_CONF)
- with open(suiterc_path) as fh:
- line = fh.readline()
- if line.startswith("#!"):
- templ_scheme = line[2:].strip().lower()
- suite_section = (templ_scheme + ':' +
- self.suite_engine_proc.SUITE_CONF)
-
- extra_defines = []
- if opts.defines_suite:
- for define in opts.defines_suite:
- extra_defines.append("[" + suite_section + "]" + define)
-
- # Automatic Rose constants
- # ROSE_ORIG_HOST: originating host
- # ROSE_VERSION: Rose version (not retained in run_mode=="reload")
- # Suite engine version
- my_rose_version = ResourceLocator.default().get_version()
- suite_engine_key = self.suite_engine_proc.get_version_env_name()
- if opts.run_mode in ["reload", "restart"]:
- prev_config_path = self.suite_engine_proc.get_suite_dir(
- suite_name, "log", "rose-suite-run.conf")
- prev_config = ConfigLoader()(prev_config_path)
- suite_engine_version = prev_config.get_value(
- ["env", suite_engine_key])
- else:
- suite_engine_version =\
- self.suite_engine_proc.get_version().decode()
- resloc = ResourceLocator.default()
- auto_items = [
- (suite_engine_key, suite_engine_version),
- ("ROSE_ORIG_HOST", self.host_selector.get_local_host()),
- ("ROSE_SITE", resloc.get_conf().get_value(['site'], '')),
- ("ROSE_VERSION", resloc.get_version())]
- for key, val in auto_items:
- requested_value = conf_tree.node.get_value(["env", key])
- if requested_value:
- if key == "ROSE_VERSION" and val != requested_value:
- exc = VersionMismatchError(requested_value, val)
- raise ConfigValueError(["env", key], requested_value, exc)
- val = requested_value
- else:
- conf_tree.node.set(["env", key], val,
- state=conf_tree.node.STATE_NORMAL)
- extra_defines.append('[%s]%s="%s"' % (suite_section, key, val))
-
- # Pass automatic Rose constants as suite defines
- self.conf_tree_loader.node_loader.load(extra_defines, conf_tree.node)
-
- # See if suite is running or not
- if opts.run_mode == "reload":
- # Check suite is running
- self.suite_engine_proc.get_suite_contact(suite_name)
- else:
- self.suite_engine_proc.check_suite_not_running(suite_name)
-
- # Install the suite to its run location
- suite_dir_rel = self._suite_dir_rel(suite_name)
-
- # Unfortunately a large try/finally block to ensure a temporary folder
- # created in validate only mode is cleaned up. Exceptions are not
- # caught here
- try:
- # Process Environment Variables
- environ = self.config_pm(conf_tree, "env")
-
- if opts.validate_suite_only_mode:
- temp_dir = mkdtemp()
- suite_dir = os.path.join(temp_dir, suite_dir_rel)
- os.makedirs(suite_dir, 0o0700)
- else:
- suite_dir = os.path.join(
- os.path.expanduser("~"), suite_dir_rel)
-
- suite_conf_dir = os.getcwd()
- locs_conf = ConfigNode()
- if opts.new_mode:
- if os.getcwd() == suite_dir:
- raise NewModeError("PWD", os.getcwd())
- elif opts.run_mode in ["reload", "restart"]:
- raise NewModeError("--run", opts.run_mode)
- self.suite_run_cleaner.clean(suite_name)
- if os.getcwd() != suite_dir:
- if opts.run_mode == "run":
- self._run_init_dir(opts, suite_name, conf_tree,
- locs_conf=locs_conf)
- os.chdir(suite_dir)
-
- # Housekeep log files
- now_str = None
- if not opts.install_only_mode and not opts.local_install_only_mode:
- now_str = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
- self._run_init_dir_log(opts, now_str)
- self.fs_util.makedirs("log/suite")
-
- # Rose configuration and version logs
- self.fs_util.makedirs("log/rose-conf")
- run_mode = opts.run_mode
- if run_mode not in ["reload", "restart", "run"]:
- run_mode = "run"
- mode = run_mode
- if opts.validate_suite_only_mode:
- mode = "validate-suite-only"
- elif opts.install_only_mode:
- mode = "install-only"
- elif opts.local_install_only_mode:
- mode = "local-install-only"
- prefix = "rose-conf/%s-%s" % (strftime("%Y%m%dT%H%M%S"), mode)
-
- # Dump the actual configuration as rose-suite-run.conf
- ConfigDumper()(conf_tree.node, "log/" + prefix + ".conf")
-
- # Install version information file
- write_source_vc_info(
- suite_conf_dir, "log/" + prefix + ".version", self.popen)
-
- # If run through rose-stem, install version information
- # files for each source tree if they're a working copy
- if hasattr(opts, 'source') and hasattr(opts, 'project'):
- for i, url in enumerate(opts.source):
- if os.path.isdir(url):
- write_source_vc_info(
- url, "log/" + opts.project[i] + "-" + str(i) +
- ".version", self.popen)
-
- for ext in [".conf", ".version"]:
- self.fs_util.symlink(prefix + ext, "log/rose-suite-run" + ext)
-
- # Move temporary log to permanent log
- if hasattr(self.event_handler, "contexts"):
- log_file_path = os.path.abspath(
- os.path.join("log", "rose-suite-run.log"))
- log_file = open(log_file_path, "ab")
- temp_log_file = self.event_handler.contexts[uuid].handle
- temp_log_file.seek(0)
- log_file.write(temp_log_file.read())
- self.event_handler.contexts[uuid].handle = log_file
- temp_log_file.close()
-
- # Process Files
- cwd = os.getcwd()
- for rel_path, conf_dir in conf_tree.files.items():
- if (conf_dir == cwd or
- any(fnmatchcase(os.sep + rel_path, exclude)
- for exclude in self.SYNC_EXCLUDES) or
- conf_tree.node.get(
- [templ_scheme + ":" + rel_path]) is not None):
- continue
- # No sub-directories, very slow otherwise
- if os.sep in rel_path:
- rel_path = rel_path.split(os.sep, 1)[0]
- target_key = self.config_pm.get_handler(
- "file").PREFIX + rel_path
- target_node = conf_tree.node.get([target_key])
- if target_node is None:
- conf_tree.node.set([target_key])
- target_node = conf_tree.node.get([target_key])
- elif target_node.is_ignored():
- continue
- source_node = target_node.get("source")
- if source_node is None:
- target_node.set(
- ["source"], os.path.join(
- conf_dir, rel_path))
- elif source_node.is_ignored():
- continue
- self.config_pm(conf_tree, "file",
- no_overwrite_mode=opts.no_overwrite_mode)
-
- # Process suite configuration template header
- # (e.g. Jinja2:suite.rc, EmPy:suite.rc)
- self.config_pm(conf_tree, templ_scheme, environ=environ)
-
- # Ask suite engine to parse suite configuration
- # and determine if it is up to date (unchanged)
- if opts.validate_suite_only_mode:
- suite_conf_unchanged = self.suite_engine_proc.cmp_suite_conf(
- suite_dir, None, opts.strict_mode,
- debug_mode=True)
- else:
- suite_conf_unchanged = self.suite_engine_proc.cmp_suite_conf(
- suite_name, opts.run_mode, opts.strict_mode,
- opts.debug_mode)
- finally:
- # Ensure the temporary directory created is cleaned up regardless
- # of success or failure
- if opts.validate_suite_only_mode and os.path.exists(temp_dir):
- shutil.rmtree(temp_dir)
-
- # Only validating so finish now
- if opts.validate_suite_only_mode:
- return
-
- # Install share/work directories (local)
- for name in ["share", "share/cycle", "work"]:
- self._run_init_dir_work(
- opts, suite_name, name, conf_tree, locs_conf=locs_conf)
-
- if opts.local_install_only_mode:
- return
-
- # Install suite files to each remote [user@]host
- for name in ["", "log/", "share/", "share/cycle/", "work/"]:
- uuid_file = os.path.abspath(name + uuid)
- open(uuid_file, "w").close()
- work_files.append(uuid_file)
-
- # Install items to user@host
- auths = self.suite_engine_proc.get_tasks_auths(suite_name)
- proc_queue = [] # [[proc, command, "ssh"|"rsync", auth], ...]
- for auth in sorted(auths):
- host = auth
- if "@" in auth:
- host = auth.split("@", 1)[1]
- # Remote shell
- command = self.popen.get_cmd("ssh", "-n", auth)
- # Provide ROSE_VERSION and CYLC_VERSION in the environment
- shcommand = "env ROSE_VERSION=%s %s=%s" % (
- my_rose_version, suite_engine_key, suite_engine_version)
- # Use login shell?
- no_login_shell = self._run_conf(
- "remote-no-login-shell", host=host, conf_tree=conf_tree)
- if not no_login_shell or no_login_shell.lower() != "true":
- shcommand += r""" bash -l -c '"$0" "$@"'"""
- # Path to "rose" command, if applicable
- rose_bin = self._run_conf(
- "remote-rose-bin", host=host, conf_tree=conf_tree,
- default="rose")
- # Build remote "rose suite-run" command
- shcommand += " %s suite-run -vv -n %s" % (
- rose_bin, suite_name)
- for key in ["new", "debug", "install-only"]:
- attr = key.replace("-", "_") + "_mode"
- if getattr(opts, attr, None) is not None:
- shcommand += " --%s" % key
- if opts.log_keep:
- shcommand += " --log-keep=%s" % opts.log_keep
- if opts.log_name:
- shcommand += " --log-name=%s" % opts.log_name
- if not opts.log_archive_mode:
- shcommand += " --no-log-archive"
- shcommand += " --run=%s" % opts.run_mode
- # Build --remote= option
- shcommand += " --remote=uuid=%s" % uuid
- if now_str is not None:
- shcommand += ",now-str=%s" % now_str
- host_confs = [
- "root-dir",
- "root-dir{share}",
- "root-dir{share/cycle}",
- "root-dir{work}"]
- locs_conf.set([auth])
- for key in host_confs:
- value = self._run_conf(key, host=host, conf_tree=conf_tree)
- if value is not None:
- val = self.popen.list_to_shell_str([str(value)])
- shcommand += ",%s=%s" % (key, pipes.quote(val))
- locs_conf.set([auth, key], value)
- command.append(shcommand)
- proc = self.popen.run_bg(*command)
- proc_queue.append([proc, command, "ssh", auth])
-
- while proc_queue:
- sleep(self.SLEEP_PIPE)
- proc, command, command_name, auth = proc_queue.pop(0)
- if proc.poll() is None: # put it back in proc_queue
- proc_queue.append([proc, command, command_name, auth])
- continue
- ret_code = proc.wait()
- out, err = proc.communicate()
- ret_code, out, err = [
- i.decode() if isinstance(i, bytes) else i for i in [
- ret_code, out, err]]
- if ret_code:
- raise RosePopenError(command, ret_code, out, err)
- if command_name == "rsync":
- self.handle_event(out, level=Event.VV)
- continue
- else:
- self.handle_event(out, level=Event.VV, prefix="[%s] " % auth)
- for line in out.split("\n"):
- if "/" + uuid == line.strip():
- locs_conf.unset([auth])
- break
- else:
- filters = {"excludes": [], "includes": []}
- for name in ["", "log/", "share/", "share/cycle/", "work/"]:
- filters["excludes"].append(name + uuid)
- target = auth + ":" + suite_dir_rel
- cmd = self._get_cmd_rsync(target, **filters)
- proc_queue.append(
- [self.popen.run_bg(*cmd), cmd, "rsync", auth])
-
- # Install ends
- ConfigDumper()(locs_conf, os.path.join("log", "rose-suite-run.locs"))
- if opts.install_only_mode:
- return
- elif opts.run_mode == "reload" and suite_conf_unchanged:
- conf_name = self.suite_engine_proc.SUITE_CONF
- self.handle_event(SkipReloadEvent(suite_name, conf_name))
- return
-
- # Start the suite
- self.fs_util.chdir("log")
- self.suite_engine_proc.run(suite_name, opts.host, opts.run_mode, args)
-
- # Disconnect log file handle, so monitoring tool command will no longer
- # be associated with the log file.
- self.event_handler.contexts[uuid].handle.close()
- self.event_handler.contexts.pop(uuid)
-
- return 0
-
- @classmethod
- def _run_conf(
- cls, key, default=None, host=None, conf_tree=None, r_opts=None):
- """Return the value of a setting given by a key for a given host. If
- r_opts is defined, we are already in a remote host, so there is no need
- to do a host match. Otherwise, the setting may be found in the run time
- configuration, or the default (i.e. site/user configuration). The value
- of each setting in the configuration would be in a line delimited list
- of PATTERN=VALUE pairs.
- """
- if r_opts is not None:
- return r_opts.get(key, default)
- if host is None:
- host = "localhost"
- for conf, keys in [
- (conf_tree.node, []),
- (ResourceLocator.default().get_conf(), ["rose-suite-run"])]:
- if conf is None:
- continue
- node_value = conf.get_value(keys + [key])
- if node_value is None:
- continue
- for line in node_value.strip().splitlines():
- pattern, value = line.strip().split("=", 1)
- if (pattern.startswith("jinja2:") or
- pattern.startswith("empy:")):
- section, name = pattern.rsplit(":", 1)
- p_node = conf.get([section, name], no_ignore=True)
- # Values in "jinja2:*" and "empy:*" sections are quoted.
- pattern = ast.literal_eval(p_node.value)
- if fnmatchcase(host, pattern):
- return value.strip()
- return default
-
- def _run_init_dir(self, opts, suite_name, conf_tree=None, r_opts=None,
- locs_conf=None):
- """Create the suite's directory."""
- suite_dir_rel = self._suite_dir_rel(suite_name)
- home = os.path.expanduser("~")
- suite_dir_root = self._run_conf("root-dir", conf_tree=conf_tree,
- r_opts=r_opts)
- if suite_dir_root:
- if locs_conf is not None:
- locs_conf.set(["localhost", "root-dir"], suite_dir_root)
- suite_dir_root = env_var_process(suite_dir_root)
- suite_dir_home = os.path.join(home, suite_dir_rel)
- if (suite_dir_root and
- os.path.realpath(home) != os.path.realpath(suite_dir_root)):
- suite_dir_real = os.path.join(suite_dir_root, suite_dir_rel)
- self.fs_util.makedirs(suite_dir_real)
- self.fs_util.symlink(suite_dir_real, suite_dir_home,
- opts.no_overwrite_mode)
- else:
- self.fs_util.makedirs(suite_dir_home)
-
- def _run_init_dir_log(self, opts, now_str=None):
- """Create the suite's log/ directory. Housekeep, archive old ones."""
- # Do nothing in log append mode if log directory already exists
- if opts.run_mode in ["reload", "restart"] and os.path.isdir("log"):
- return
-
- # Log directory of this run
- if now_str is None:
- now_str = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
- now_log = "log." + now_str
- self.fs_util.makedirs(now_log)
- self.fs_util.symlink(now_log, "log")
- now_log_name = getattr(opts, "log_name", None)
- if now_log_name:
- self.fs_util.symlink(now_log, "log." + now_log_name)
-
- # Keep log for this run and named logs
- logs = set(glob("log.*") + ["log"])
- for log in list(logs):
- if os.path.islink(log):
- logs.remove(log)
- log_link = os.readlink(log)
- if log_link in logs:
- logs.remove(log_link)
-
- # Housekeep old logs, if necessary
- log_keep = getattr(opts, "log_keep", None)
- if log_keep:
- t_threshold = time() - abs(float(log_keep)) * 86400.0
- for log in list(logs):
- if os.path.isfile(log):
- if t_threshold > os.stat(log).st_mtime:
- self.fs_util.delete(log)
- logs.remove(log)
- else:
- for root, _, files in os.walk(log):
- keep = False
- for file_ in files:
- path = os.path.join(root, file_)
- if (os.path.exists(path) and
- os.stat(path).st_mtime >= t_threshold):
- keep = True
- break
- if keep:
- break
- else:
- self.fs_util.delete(log)
- logs.remove(log)
-
- # Archive old logs, if necessary
- if getattr(opts, "log_archive_mode", True):
- for log in list(logs):
- if os.path.isfile(log):
- continue
- log_tar_gz = log + ".tar.gz"
- try:
- self.popen.run_simple("tar", "-czf", log_tar_gz, log)
- except RosePopenError:
- try:
- self.fs_util.delete(log_tar_gz)
- except OSError:
- pass
- raise
- else:
- self.handle_event(SuiteLogArchiveEvent(log_tar_gz, log))
- self.fs_util.delete(log)
-
- def _run_init_dir_work(self, opts, suite_name, name, conf_tree=None,
- r_opts=None, locs_conf=None):
- """Create a named suite's directory."""
- item_path = os.path.realpath(name)
- item_path_source = item_path
- key = "root-dir{" + name + "}"
- item_root = self._run_conf(key, conf_tree=conf_tree, r_opts=r_opts)
- if item_root is None: # backward compat
- item_root = self._run_conf(
- "root-dir-" + name, conf_tree=conf_tree, r_opts=r_opts)
- if item_root:
- if locs_conf is not None:
- locs_conf.set(["localhost", key], item_root)
- item_root = env_var_process(item_root)
- suite_dir_rel = self._suite_dir_rel(suite_name)
- if os.path.isabs(item_root):
- item_path_source = os.path.join(item_root, suite_dir_rel, name)
- else:
- item_path_source = item_root
- item_path_source = os.path.realpath(item_path_source)
- if item_path == item_path_source:
- if opts.new_mode:
- self.fs_util.delete(name)
- self.fs_util.makedirs(name)
- else:
- if opts.new_mode:
- self.fs_util.delete(item_path_source)
- self.fs_util.makedirs(item_path_source)
- if os.sep in name:
- dirname_of_name = os.path.dirname(name)
- self.fs_util.makedirs(dirname_of_name)
- item_path_source_rel = os.path.relpath(
- item_path_source, os.path.realpath(dirname_of_name))
- else:
- item_path_source_rel = os.path.relpath(item_path_source)
- if len(item_path_source_rel) < len(item_path_source):
- self.fs_util.symlink(
- item_path_source_rel, name, opts.no_overwrite_mode)
- else:
- self.fs_util.symlink(
- item_path_source, name, opts.no_overwrite_mode)
-
- def _run_remote(self, opts, suite_name):
- """rose suite-run --remote=KEY=VALUE,..."""
- suite_dir_rel = self._suite_dir_rel(suite_name)
- r_opts = {}
- for item in opts.remote.split(","):
- key, val = item.split("=", 1)
- r_opts[key] = val
- uuid_file = os.path.join(suite_dir_rel, r_opts["uuid"])
- if os.path.exists(uuid_file):
- self.handle_event("/" + r_opts["uuid"] + "\n", level=0)
- elif opts.new_mode:
- self.fs_util.delete(suite_dir_rel)
- if opts.run_mode == "run" or not os.path.exists(suite_dir_rel):
- self._run_init_dir(opts, suite_name, r_opts=r_opts)
- os.chdir(suite_dir_rel)
- for name in ["share", "share/cycle", "work"]:
- uuid_file = os.path.join(name, r_opts["uuid"])
- if os.path.exists(uuid_file):
- self.handle_event(name + "/" + r_opts["uuid"] + "\n", level=0)
- else:
- self._run_init_dir_work(opts, suite_name, name, r_opts=r_opts)
- if not opts.install_only_mode:
- uuid_file = os.path.join("log", r_opts["uuid"])
- if os.path.exists(uuid_file):
- self.handle_event("log/" + r_opts["uuid"] + "\n", level=0)
- else:
- self._run_init_dir_log(opts, r_opts.get("now-str"))
- self.fs_util.makedirs("log/suite")
-
- def _get_cmd_rsync(self, target, excludes=None, includes=None):
- """rsync relevant suite items to target."""
- if excludes is None:
- excludes = []
- if includes is None:
- includes = []
- cmd = self.popen.get_cmd("rsync")
- for exclude in excludes + list(self.SYNC_EXCLUDES):
- cmd.append("--exclude=" + exclude)
- for include in includes:
- cmd.append("--include=" + include)
- cmd.append("./")
- cmd.append(target)
- return cmd
-
- def _suite_dir_rel(self, suite_name):
- """Return the relative path to the suite running directory."""
- return self.suite_engine_proc.get_suite_dir_rel(suite_name)
-
-
-def main():
- """Launcher for the CLI."""
- opt_parser = RoseOptionParser()
- option_keys = SuiteRunner.OPTIONS
- opt_parser.add_my_options(*option_keys)
- opts, args = opt_parser.parse_args(sys.argv[1:])
- event_handler = Reporter(opts.verbosity - opts.quietness)
- runner = SuiteRunner(event_handler)
- try:
- sys.exit(runner(opts, args))
- except Exception as exc:
- runner.handle_event(exc)
- if opts.debug_mode:
- traceback.print_exc()
- if isinstance(exc, RosePopenError):
- sys.exit(exc.ret_code)
- else:
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
diff --git a/metomi/rose/task_env.py b/metomi/rose/task_env.py
index 1157f605c5..1de0ee93e9 100644
--- a/metomi/rose/task_env.py
+++ b/metomi/rose/task_env.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/task_run.py b/metomi/rose/task_run.py
index cfe16f0a10..988df9406a 100644
--- a/metomi/rose/task_run.py
+++ b/metomi/rose/task_run.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/.travis/sitecustomize.py b/metomi/rose/tests/conftest.py
similarity index 58%
rename from .travis/sitecustomize.py
rename to metomi/rose/tests/conftest.py
index 57efa4a0a7..db9f033850 100644
--- a/.travis/sitecustomize.py
+++ b/metomi/rose/tests/conftest.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -16,15 +13,27 @@
#
# You should have received a copy of the GNU General Public License
# along with Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""This file is used by Travis-CI to start the coverage process.
+from pathlib import Path
+from shutil import rmtree
+from tempfile import TemporaryDirectory
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
-In order to make python aware of it, we export PYTHONPATH when running
-the tests.
-"""
+@pytest.fixture(scope='module')
+def mod_monkeypatch():
+ """A monkeypatch fixture with module-level scope."""
+ patch = MonkeyPatch()
+ yield patch
+ patch.undo()
-import coverage
-coverage.process_startup()
+@pytest.fixture(scope='module')
+def mod_tmp_path():
+ """A tmp_path fixture with module-level scope."""
+ path = Path(TemporaryDirectory().name)
+ path.mkdir()
+ yield path
+ rmtree(path)
diff --git a/metomi/rose/tests/config.py b/metomi/rose/tests/test_config.py
similarity index 98%
rename from metomi/rose/tests/config.py
rename to metomi/rose/tests/test_config.py
index 8ea4bd5914..6ac4ad9db9 100644
--- a/metomi/rose/tests/config.py
+++ b/metomi/rose/tests/test_config.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/tests/env.py b/metomi/rose/tests/test_env.py
similarity index 95%
rename from metomi/rose/tests/env.py
rename to metomi/rose/tests/test_env.py
index 39e8c3956a..8ca4f3d1c5 100644
--- a/metomi/rose/tests/env.py
+++ b/metomi/rose/tests/test_env.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/tests/test_host_select_client.py b/metomi/rose/tests/test_host_select_client.py
new file mode 100644
index 0000000000..4881875ff6
--- /dev/null
+++ b/metomi/rose/tests/test_host_select_client.py
@@ -0,0 +1,80 @@
+# Copyright (C) British Crown (Met Office) & Contributors.
+#
+# This file is part of Rose, a framework for meteorological suites.
+#
+# Rose 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.
+#
+# Rose 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 Rose. If not, see .
+import json
+from io import StringIO
+from textwrap import dedent
+
+from metomi.rose.host_select_client import main as host_select
+
+
+def test_empty(monkeypatch, capsys):
+ """It should not return any results for an empty request."""
+ monkeypatch.setattr(
+ 'sys.stdin',
+ StringIO(dedent('''
+ **start**
+ []
+ **end**
+ '''))
+ )
+ host_select()
+ captured = capsys.readouterr()
+ assert captured.out == '[]\n'
+ assert captured.err == ''
+
+
+def test_stdin_pollution(monkeypatch, capsys):
+ """Any junk before or after the start/end markers should be ignored
+
+ Note this can come from shell profile scripts.
+ """
+ monkeypatch.setattr(
+ 'sys.stdin',
+ StringIO(dedent('''
+ hello
+ *&^%$**start**
+ []
+ **end***&^%$E
+ world
+ '''))
+ )
+ host_select()
+ captured = capsys.readouterr()
+ assert captured.out == '[]\n'
+ assert captured.err == ''
+
+
+def test_request(monkeypatch, capsys):
+ """Test a simple request."""
+ monkeypatch.setattr(
+ 'sys.stdin',
+ StringIO(dedent('''
+ **start**
+ [["virtual_memory"]]
+ **end**
+ '''))
+ )
+ host_select()
+ captured = capsys.readouterr()
+ assert captured.out
+ assert captured.err == ''
+
+ results = json.loads(captured.out)
+ assert len(results) == 1
+ result = results[0]
+ for key in ('active', 'available', 'free'):
+ assert key in result
diff --git a/metomi/rose/tests/test_loc_handlers_rsync_remote_check.py b/metomi/rose/tests/test_loc_handlers_rsync_remote_check.py
index 79cfc408cb..a71efe8509 100644
--- a/metomi/rose/tests/test_loc_handlers_rsync_remote_check.py
+++ b/metomi/rose/tests/test_loc_handlers_rsync_remote_check.py
@@ -18,48 +18,48 @@
# along with Rose. If not, see .
# -----------------------------------------------------------------------------
-import sys
-
-from ast import literal_eval
+from pathlib import Path
from metomi.rose.loc_handlers.rsync_remote_check import main
-def test_check_file(monkeypatch, capsys, tmp_path):
+def test_check_file(capsys, tmp_path):
content = 'blah'
- permission_level = '0o100644'
+ permission_level = 33188
filepath = tmp_path / 'stuff'
filepath.write_text(content)
- filepath.chmod(int(permission_level, base=8))
- monkeypatch.setattr(
- sys, 'argv', ['ignored', str(filepath), 'blob', 'tree']
- )
- main()
+ filepath.chmod(permission_level)
+ main(str(filepath), 'blob', 'tree')
captured = capsys.readouterr()
assert captured.out.splitlines()[0] == 'blob'
mode, mtime, size, path = captured.out.splitlines()[1].split()
assert path == str(filepath)
- assert mode == permission_level
+ assert int(mode) == permission_level
assert size == str(filepath.stat().st_size)
-def test_check_folder(
- monkeypatch, capsys, tmp_path
-):
- folder_permission_level = '0o100700'
- dirpath = tmp_path / 'stuff'
- dirpath.mkdir()
- (dirpath / 'more.stuff').write_text('Hi')
- (dirpath / 'more.stuff').chmod(int('0o100633', base=8))
- (dirpath / 'even.more.stuff').write_text('Hi')
- dirpath.chmod(int(folder_permission_level, base=8))
- monkeypatch.setattr(
- sys, 'argv', ['ignored', str(dirpath), 'blob', 'tree']
- )
- main()
- captured = capsys.readouterr()
- assert captured.out.splitlines()[0] == 'tree'
- mode, _, size, path = literal_eval(captured.out.splitlines()[1])
- assert path == str(dirpath / 'more.stuff')
- assert mode == '0o100633'
- assert size == 2
+def test_check_folder(capsys, tmp_path):
+ # create a file and chmod it
+ (tmp_path / 'more.stuff').write_text('Hi')
+ (tmp_path / 'more.stuff').chmod(int('0o100633', base=8))
+
+ # create another file
+ (tmp_path / 'even.more.stuff').write_text('Hi')
+
+ # run the remote check on the dir
+ main(str(tmp_path), 'blob', 'tree')
+ lines = capsys.readouterr().out.splitlines()
+
+ # the first line should be the dir
+ assert lines[0] == 'tree'
+
+ # the following lines should be the files
+ files = {
+ # filename: [mode, mod_time, size]
+ str(Path(line.split()[-1]).relative_to(tmp_path)): line.split()[:-1]
+ for line in lines[1:]
+ }
+ assert list(sorted(files)) == ['even.more.stuff', 'more.stuff']
+ mode, _, size = files['more.stuff']
+ assert mode == '33179'
+ assert size == '2'
diff --git a/metomi/rose/tests/popen.py b/metomi/rose/tests/test_popen.py
similarity index 94%
rename from metomi/rose/tests/popen.py
rename to metomi/rose/tests/test_popen.py
index f3a36e62bc..8bf0a61413 100644
--- a/metomi/rose/tests/popen.py
+++ b/metomi/rose/tests/test_popen.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/tests/test_resource_locator.py b/metomi/rose/tests/test_resource_locator.py
new file mode 100644
index 0000000000..2b09d72bfe
--- /dev/null
+++ b/metomi/rose/tests/test_resource_locator.py
@@ -0,0 +1,149 @@
+# Copyright (C) British Crown (Met Office) & Contributors.
+# This file is part of Rose, a framework for meteorological suites.
+#
+# Rose 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.
+#
+# Rose 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 Rose. If not, see .
+
+from textwrap import dedent
+
+import pytest
+
+from metomi.rose.resource import (
+ ROSE_CONF_PATH,
+ ROSE_SITE_CONF_PATH,
+ ResourceLocator
+)
+
+
+@pytest.fixture(scope='module')
+def sys_site_user_config(mod_monkeypatch, mod_tmp_path):
+ """Creates system, site and user configurations.
+
+ * Patches ResourceLocator to use the system and user configs.
+ * Unsets ROSE_*CONF_PATH envvars to prevent them effecting tests.
+
+ """
+ # unset ROSE_CONF_PATH env vars
+ mod_monkeypatch.delenv(ROSE_CONF_PATH, raising=False)
+ mod_monkeypatch.delenv(ROSE_SITE_CONF_PATH, raising=False)
+
+ # create system, site and user configurations
+ syst = mod_tmp_path / 'syst'
+ site = mod_tmp_path / 'site'
+ user = mod_tmp_path / 'user'
+ syst.mkdir()
+ site.mkdir()
+ user.mkdir()
+ with open(syst / 'rose.conf', 'w+') as conf:
+ conf.write(dedent('''
+ all=syst
+ syst=syst
+ '''))
+ with open(site / 'rose.conf', 'w+') as conf:
+ conf.write(dedent('''
+ all=site
+ site=site
+ '''))
+ with open(user / 'rose.conf', 'w+') as conf:
+ conf.write(dedent('''
+ all=user
+ user=user
+ '''))
+
+ # patch the ResourceLocator
+ mod_monkeypatch.setattr(
+ 'metomi.rose.resource.ResourceLocator.SYST_CONF_PATH', syst
+ )
+ mod_monkeypatch.setattr(
+ 'metomi.rose.resource.ResourceLocator.USER_CONF_PATH', user
+ )
+
+ return tuple(map(str, (syst, site, user)))
+
+
+@pytest.fixture
+def resource_locator():
+ """Return a ResourceLocator instance for testing.
+
+ Wipes the cached instance after each use.
+ """
+ resource_locator = ResourceLocator()
+ yield resource_locator
+ # prevent this instance being reused
+ del resource_locator
+ ResourceLocator._DEFAULT_RESOURCE_LOCATOR = None
+
+
+def test_default(sys_site_user_config, resource_locator):
+ """It should pick up the system and user config by default."""
+ conf = resource_locator.get_conf()
+ # both the syst and user config should have been loaded
+ assert conf.get(['syst']).value == 'syst'
+ assert conf.get(['user']).value == 'user'
+ # the syst config should have been loaded before the user config
+ assert conf.get(['all']).value == 'user'
+ assert set(conf.value) == {'syst', 'user', 'all'}
+
+
+def test_skip_no_read(sys_site_user_config, resource_locator, monkeypatch):
+ """It should skip config files it can't read."""
+ # make it look like all files are not readable.
+ monkeypatch.setattr(
+ 'os.access',
+ lambda x, y: False
+ )
+ conf = resource_locator.get_conf()
+ assert conf.value == {}
+
+
+def test_rose_conf_path_blank(
+ sys_site_user_config,
+ resource_locator,
+ monkeypatch
+):
+ """Setting ROSE_CONF_PATH= should prevent any conf files being loaded."""
+ monkeypatch.setenv(ROSE_CONF_PATH, '')
+ conf = resource_locator.get_conf()
+ assert conf.value == {}
+
+
+def test_rose_conf_path(
+ sys_site_user_config,
+ resource_locator,
+ monkeypatch
+):
+ """If ROSE_CONF_PATH is defined no other files should be loaded."""
+ # set ROSE_CONF_PATH to point at the system config
+ syst, site, *_ = sys_site_user_config
+ monkeypatch.setenv(ROSE_CONF_PATH, syst)
+ # set the ROSE_SITE_CONF_PATH just to make sure it is ignored
+ monkeypatch.setenv(ROSE_SITE_CONF_PATH, site)
+ conf = resource_locator.get_conf()
+ assert conf.get(['syst']).value == 'syst'
+ assert set(conf.value) == {'syst', 'all'}
+
+
+def test_rose_site_conf_path(
+ sys_site_user_config,
+ resource_locator,
+ monkeypatch
+):
+ """If ROSE_SITE_CONF_PATH is defined it should be loaded."""
+ # set ROSE_SITE_CONF_PATH to point at the system config
+ _, site, *_ = sys_site_user_config
+ monkeypatch.setenv(ROSE_SITE_CONF_PATH, site)
+ conf = resource_locator.get_conf()
+ assert conf.get(['site']).value == 'site'
+ assert conf.get(['user']).value == 'user'
+ assert conf.get(['all']).value == 'user'
+ assert set(conf.value) == {'syst', 'site', 'user', 'all'}
diff --git a/metomi/rose/tests/trigger_file.py b/metomi/rose/tests/test_trigger_file.py
similarity index 93%
rename from metomi/rose/tests/trigger_file.py
rename to metomi/rose/tests/test_trigger_file.py
index 5bceeecb4e..e39899aee3 100644
--- a/metomi/rose/tests/trigger_file.py
+++ b/metomi/rose/tests/test_trigger_file.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/tests/unicode_utils.py b/metomi/rose/tests/test_unicode_utils.py
similarity index 94%
rename from metomi/rose/tests/unicode_utils.py
rename to metomi/rose/tests/test_unicode_utils.py
index cbe0105fec..371e222a73 100644
--- a/metomi/rose/tests/unicode_utils.py
+++ b/metomi/rose/tests/test_unicode_utils.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/unicode_utils.py b/metomi/rose/unicode_utils.py
index 7e1b6236b7..0572597014 100644
--- a/metomi/rose/unicode_utils.py
+++ b/metomi/rose/unicode_utils.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rose/upgrade.py b/metomi/rose/upgrade.py
index a5315646b2..d1bcefb78f 100644
--- a/metomi/rose/upgrade.py
+++ b/metomi/rose/upgrade.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -165,22 +162,24 @@ def add_setting(self, config, keys, value=None, forced=False,
configuration.
keys (list): A list defining a hierarchy of node.value 'keys'.
A section will be a list of one keys, an option will have two.
- value (string - optional): String denoting the new setting value.
+ value (str): String denoting the new setting value.
Required for options but not for settings.
- forced (bool - optional)
+ forced (bool)
If True override value if the setting already exists.
- state (str - optional):
+ state (str):
The state of the new setting - should be one of the
- ``rose.config.ConfigNode`` states e.g.
- ``rose.config.ConfigNode.STATE_USER_IGNORED``. Defaults to
- ``rose.config.ConfigNode.STATE_NORMAL``.
- comments (list - optional): List of comment lines (strings) for
+ :py:class:`metomi.rose.config.ConfigNode` states e.g.
+ :py:data:`metomi.rose.config.ConfigNode.STATE_USER_IGNORED`.
+ Defaults to
+ :py:data:`metomi.rose.config.ConfigNode.STATE_NORMAL`.
+ comments (list): List of comment lines (strings) for
the new setting or ``None``.
- info (string - optional): A short string containing no new lines,
+ info (str): A short string containing no new lines,
describing the addition of the setting.
Returns:
None
+
"""
section, option = self._get_section_option_from_keys(keys)
id_ = self._get_id_from_section_option(section, option)
@@ -269,12 +268,12 @@ def change_setting_value(self, config, keys, value, forced=False,
configuration.
keys (list): A list defining a hierarchy of node.value 'keys'.
A section will be a list of one keys, an option will have two.
- value (string): The new value. Required for options, can be
+ value (str): The new value. Required for options, can be
``None`` for sections.
forced (bool): Create the setting if it is not present in config.
- comments (list - optional): List of comment lines (strings) for
+ comments (list): List of comment lines (strings) for
the new setting or ``None``.
- info (string - optional): A short string containing no new lines,
+ info (str): A short string containing no new lines,
describing the addition of the setting.
Returns:
@@ -312,11 +311,12 @@ def get_setting_value(self, config, keys, no_ignore=False):
configuration.
keys (list): A list defining a hierarchy of node.value 'keys'.
A section will be a list of one keys, an option will have two.
- no_ignore (bool - optional): If ``True`` return ``None`` if the
+ no_ignore (bool): If ``True`` return ``None`` if the
setting is ignored (else return the value).
Returns:
object - The setting value or ``None`` if not defined.
+
"""
section, option = self._get_section_option_from_keys(keys)
if config.get([section, option], no_ignore=no_ignore) is None:
@@ -331,11 +331,12 @@ def remove_setting(self, config, keys, info=None):
configuration.
keys (list): A list defining a hierarchy of node.value 'keys'.
A section will be a list of one keys, an option will have two.
- info (string - optional): A short string containing no new lines,
+ info (str): A short string containing no new lines,
describing the addition of the setting.
Returns:
None
+
"""
section, option = self._get_section_option_from_keys(keys)
if option is None:
@@ -356,11 +357,12 @@ def rename_setting(self, config, keys, new_keys, info=None):
keys (list): A list defining a hierarchy of node.value 'keys'.
A section will be a list of one keys, an option will have two.
new_keys (list): The new hierarchy of node.value 'keys'.
- info (string - optional): A short string containing no new lines,
+ info (str): A short string containing no new lines,
describing the addition of the setting.
Returns:
None
+
"""
section, option = self._get_section_option_from_keys(keys)
new_section, new_option = self._get_section_option_from_keys(new_keys)
@@ -406,11 +408,12 @@ def enable_setting(self, config, keys, info=None):
configuration.
keys (list): A list defining a hierarchy of node.value 'keys'.
A section will be a list of one keys, an option will have two.
- info (string - optional): A short string containing no new lines,
+ info (str): A short string containing no new lines,
describing the addition of the setting.
Returns:
False - if the setting's state is not changed else ``None``.
+
"""
return self._ignore_setting(
config,
@@ -423,17 +426,22 @@ def ignore_setting(self, config, keys, info=None,
"""User-ignore a setting in the configuration.
Args:
- config (metomi.rose.config.ConfigNode): The application
- configuration.
- keys (list): A list defining a hierarchy of node.value 'keys'.
+ config (metomi.rose.config.ConfigNode):
+ The application configuration.
+ keys (list):
+ A list defining a hierarchy of node.value 'keys'.
A section will be a list of one keys, an option will have two.
- info (string - optional): A short string containing no new lines,
- describing the addition of the setting.
- state (string - optional): A ``rose.config.ConfigNode`` state.
- ``STATE_USER_IGNORED`` by default.
+ info (str):
+ A short string containing no new lines, describing the addition
+ of the setting.
+ state (str):
+ A :py:class:`metomi.rose.config.ConfigNode` state.
+ :py:data:`metomi.rose.config.ConfigNode.STATE_USER_IGNORED` by
+ default.
Returns:
False - if the setting's state is not changed else ``None``.
+
"""
return self._ignore_setting(config, list(keys),
info=info, state=state)
@@ -474,7 +482,7 @@ def _get_section_option_from_keys(self, keys):
return (keys + [None])[:2]
-class MacroUpgradeManager(object):
+class MacroUpgradeManager:
"""Manage the upgrades."""
@@ -712,7 +720,6 @@ def parse_upgrade_args(argv=None):
opts.conf_dir = os.path.abspath(opts.conf_dir)
if opts.output_dir is not None:
opts.output_dir = os.path.abspath(opts.output_dir)
- sys.path.append(os.getenv("ROSE_LIB"))
metomi.rose.macro.add_opt_meta_paths(opts.meta_path)
config_name = os.path.basename(opts.conf_dir)
config_file_path = os.path.join(opts.conf_dir,
diff --git a/metomi/rose/variable.py b/metomi/rose/variable.py
index d8107ccffa..4f8fd20a48 100644
--- a/metomi/rose/variable.py
+++ b/metomi/rose/variable.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -50,7 +47,7 @@
IGNORED_BY_USER = 'User ignored'
-class Variable(object):
+class Variable:
"""This class stores the data and metadata of an input variable.
@@ -276,7 +273,7 @@ def get_value_from_metadata(meta_data):
return var_value
-class RangeSubFunction(object):
+class RangeSubFunction:
"""Holds a checking function."""
@@ -304,7 +301,7 @@ def __repr__(self):
self.operator, self.values)
-class CombinedRangeSubFunction(object):
+class CombinedRangeSubFunction:
def __init__(self, *range_insts):
self.range_insts = range_insts
diff --git a/metomi/rosie/__init__.py b/metomi/rosie/__init__.py
index f1b60f1a5d..e7f96eaf6b 100644
--- a/metomi/rosie/__init__.py
+++ b/metomi/rosie/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rosie/db.py b/metomi/rosie/db.py
index 9ff56b4e39..cc7640cb82 100644
--- a/metomi/rosie/db.py
+++ b/metomi/rosie/db.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -59,7 +56,7 @@ def __str__(self):
return "Failed to connect to DB '%s'." % self.bad_db_url
-class DAO(object):
+class DAO:
"""Retrieves data from the suite database.
diff --git a/metomi/rosie/db_create.py b/metomi/rosie/db_create.py
index 97bb7ea089..f710a214e9 100644
--- a/metomi/rosie/db_create.py
+++ b/metomi/rosie/db_create.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -84,7 +81,7 @@ def __str__(self):
return "%s: DB not loaded: %s" % self.args
-class RosieDatabaseInitiator(object):
+class RosieDatabaseInitiator:
"""Initiate a database file from the repository information."""
diff --git a/metomi/rosie/graph.py b/metomi/rosie/graph.py
index 94aef55216..0912085393 100644
--- a/metomi/rosie/graph.py
+++ b/metomi/rosie/graph.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rosie/suite_id.py b/metomi/rosie/suite_id.py
index c406b0e245..b98ed3ca57 100644
--- a/metomi/rosie/suite_id.py
+++ b/metomi/rosie/suite_id.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -29,7 +26,14 @@
"""
import os
+from pathlib import Path
import re
+import shlex
+import string
+import sys
+import traceback
+import xml.parsers.expat
+
import metomi.rose.env
from metomi.rose.loc_handlers.svn import SvnInfoXMLParser
from metomi.rose.opt_parse import RoseOptionParser
@@ -37,11 +41,6 @@
from metomi.rose.reporter import Reporter
from metomi.rose.resource import ResourceLocator
from metomi.rose.suite_engine_proc import SuiteEngineProcessor, NoSuiteLogError
-import shlex
-import string
-import sys
-import traceback
-import xml.parsers.expat
class SvnCaller(RosePopener):
@@ -108,7 +107,7 @@ def __str__(self):
return "%s: invalid suite ID" % (self.args[0])
-class SuiteId(object):
+class SuiteId:
"""Represent a suite ID."""
@@ -365,9 +364,32 @@ def _from_id_text(self, id_text):
def _from_location(self, location):
"""Return the ID of a location (origin URL or local copy path)."""
- # Is location a "~/cylc-run/$SUITE/" directory?
- # Use a hacky way to read the "log/rose-suite-run.version" file
suite_engine_proc = SuiteEngineProcessor.get_processor()
+ suite_dir_rel_root = getattr(
+ suite_engine_proc, "SUITE_DIR_REL_ROOT", None)
+
+ # Cylc8 run directory
+ # TODO: extract version control information
+ loc = Path(location)
+ sdrr = Path('~', suite_dir_rel_root).expanduser().resolve()
+ try:
+ loc.relative_to(sdrr)
+ except ValueError:
+ # Not an installed Cylc8 workflow run directory
+ pass
+ else:
+ if (loc / 'rose-suite.info').is_file():
+ # This is an installed workflow with a rose-suite.info file
+ # (most likely a Cylc8 run directory)
+
+ # TODO: extract version control information written by
+ # Cylc install, see:
+ # https://github.com/metomi/rose/issues/2432
+ # https://github.com/cylc/cylc-flow/issues/3849
+ raise SuiteIdLocationError(location)
+
+ # Cylc7 run directory
+ # Use a hacky way to read the "log/rose-suite-run.version" file
suite_dir_rel_root = getattr(
suite_engine_proc, "SUITE_DIR_REL_ROOT", None)
if suite_dir_rel_root and "/" + suite_dir_rel_root + "/" in location:
@@ -536,17 +558,12 @@ def to_web(self):
revision = self.REV_HEAD
return url + self.FORMAT_VERSION % (branch, revision)
- def to_output(self):
- """Return the output directory for this suite."""
- suite_engine_proc = SuiteEngineProcessor.get_processor()
- return suite_engine_proc.get_suite_log_url(None, str(self))
-
def main():
"""Implement the "rose suite-id" command."""
opt_parser = RoseOptionParser()
opt_parser.add_my_options("latest", "next", "to_local_copy", "to_origin",
- "to_output", "to_web")
+ "to_web")
opts, args = opt_parser.parse_args()
report = Reporter(opts.verbosity - opts.quietness)
SuiteId.svn.event_handler = report # FIXME: ugly?
@@ -562,10 +579,6 @@ def main():
for arg in args:
report(
str(SuiteId(id_text=arg).to_local_copy()) + "\n", level=0)
- elif opts.to_output:
- for arg in args:
- url = SuiteId(id_text=arg).to_output()
- report(str(url) + "\n", level=0)
elif opts.to_web:
for arg in args:
report(str(SuiteId(id_text=arg).to_web()) + "\n", level=0)
diff --git a/metomi/rosie/svn_post_commit.py b/metomi/rosie/svn_post_commit.py
index b0569c9eb5..87c1449782 100644
--- a/metomi/rosie/svn_post_commit.py
+++ b/metomi/rosie/svn_post_commit.py
@@ -48,7 +48,7 @@
from metomi.rosie.svn_hook import RosieSvnHook, InfoFileError
-class RosieWriteDAO(object):
+class RosieWriteDAO:
"""Data Access Object for writing to the Rosie web service database."""
diff --git a/metomi/rosie/usertools/__init__.py b/metomi/rosie/usertools/__init__.py
index f9c7376f07..b229a3d772 100644
--- a/metomi/rosie/usertools/__init__.py
+++ b/metomi/rosie/usertools/__init__.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/metomi/rosie/usertools/ldaptool.py b/metomi/rosie/usertools/ldaptool.py
index c6426d17d4..2452e81ce8 100644
--- a/metomi/rosie/usertools/ldaptool.py
+++ b/metomi/rosie/usertools/ldaptool.py
@@ -27,7 +27,7 @@
from metomi.rose.resource import ResourceLocator
-class LDAPUserTool(object):
+class LDAPUserTool:
"""User information tool via LDAP."""
diff --git a/metomi/rosie/usertools/passwdtool.py b/metomi/rosie/usertools/passwdtool.py
index 91cf262218..251c092d1e 100644
--- a/metomi/rosie/usertools/passwdtool.py
+++ b/metomi/rosie/usertools/passwdtool.py
@@ -22,7 +22,7 @@
from pwd import getpwnam
-class PasswdUserTool(object):
+class PasswdUserTool:
"""User information tool via Unix password info."""
diff --git a/metomi/rosie/vc.py b/metomi/rosie/vc.py
index 5e67a3dcc2..bfccbb0edf 100644
--- a/metomi/rosie/vc.py
+++ b/metomi/rosie/vc.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -163,7 +160,7 @@ def __str__(self):
return "delete: %s" % id_.to_origin()
-class RosieVCClient(object):
+class RosieVCClient:
"""Client for version control functionalities."""
diff --git a/metomi/rosie/ws.py b/metomi/rosie/ws.py
index 8e6bc9eaee..c09af0da4f 100644
--- a/metomi/rosie/ws.py
+++ b/metomi/rosie/ws.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -38,15 +35,18 @@
import json
import logging
import os
+from pathlib import Path
+import pkg_resources
import pwd
import signal
-import pkg_resources
from time import sleep
from tornado.ioloop import IOLoop, PeriodicCallback
import tornado.log
import tornado.web
+import wsgiref
from metomi.isodatetime.data import get_timepoint_from_seconds_since_unix_epoch
+from metomi.rose import __version__ as ROSE_VERSION
from metomi.rose.host_select import HostSelector
from metomi.rose.opt_parse import RoseOptionParser
from metomi.rose.resource import ResourceLocator
@@ -82,7 +82,7 @@ def __init__(self, service_root_mode=False, *args, **kwargs):
if self.props["host_name"] and "." in self.props["host_name"]:
self.props["host_name"] = (
self.props["host_name"].split(".", 1)[0])
- self.props["rose_version"] = ResourceLocator.default().get_version()
+ self.props["rose_version"] = ROSE_VERSION
# Get location of HTML files from package
rosie_lib = os.path.join(
@@ -139,8 +139,9 @@ def __init__(self, service_root_mode=False, *args, **kwargs):
handlers = [root_handler] + prefix_handlers
settings = dict(
autoreload=True,
- static_path=ResourceLocator.default().get_util_home(
- "lib", "html", "static"),
+ static_path=str(
+ Path(metomi.rosie.__file__).parent / 'lib/html/static'
+ )
)
super(
RosieDiscoServiceApplication, self).__init__(handlers, **settings)
diff --git a/metomi/rosie/ws_client.py b/metomi/rosie/ws_client.py
index 649cb6ff16..3726aa44de 100644
--- a/metomi/rosie/ws_client.py
+++ b/metomi/rosie/ws_client.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -64,7 +61,7 @@ def __str__(self):
return "Query syntax error: " + " ".join(self.args[0])
-class RosieWSClient(object):
+class RosieWSClient:
"""A client for the Rosie web service.
@@ -75,8 +72,8 @@ class RosieWSClient(object):
credentials. Takes and returns the arguments username and password.
popen (rose.popen.RosePopener): Use initiated RosePopener instance
create a new one if ``None``.
- event_handler (object): A callable object for reporting popen output,
- see :py:class:`rose.reporter.Reporter`.
+ event_handler : A callable object for reporting popen output,
+ see :py:class:`metomi.rose.reporter.Reporter`.
"""
MAX_LOCAL_QUERIES = 64
diff --git a/metomi/rosie/ws_client_auth.py b/metomi/rosie/ws_client_auth.py
index 2ede0ec575..3c5996fdd5 100644
--- a/metomi/rosie/ws_client_auth.py
+++ b/metomi/rosie/ws_client_auth.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -18,15 +15,16 @@
# along with Rose. If not, see .
# -----------------------------------------------------------------------------
"""The authentication manager for the Rosie web service client."""
-GI_FLAG = False
import ast
from getpass import getpass
import os
import re
+import socket
import shlex
import sys
from urllib.parse import urlparse
+import warnings
import metomi.rose.config
from metomi.rose.env import env_var_process
@@ -34,7 +32,23 @@
from metomi.rose.reporter import Reporter
from metomi.rose.resource import ResourceLocator
-import socket
+
+try:
+ from gi import require_version, pygtkcompat
+ require_version('Gtk', '3.0') # For GTK+ >=v3 use PyGObject; v2 use PyGTK
+ require_version('Secret', '1')
+ from gi.repository import Secret
+ del pygtkcompat
+ GI_FLAG = True
+except (ImportError, ValueError):
+ GI_FLAG = False
+try:
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore')
+ import gtk
+ import gnomekeyring
+except ImportError:
+ pass
class UndefinedRosiePrefixWS(Exception):
@@ -63,7 +77,7 @@ def __str__(self):
return message
-class GnomekeyringStore(object):
+class GnomekeyringStore:
"""Password management with gnomekeyring."""
@@ -121,7 +135,7 @@ def store_password(self, scheme, host, username, password):
self.item_ids[(scheme, host, username)] = (None, item_id)
-class GPGAgentStore(object):
+class GPGAgentStore:
"""Password management with gpg-agent."""
@@ -232,7 +246,7 @@ def store_password(self, scheme, host, username, password):
pass
-class LibsecretStore(object):
+class LibsecretStore:
"""Password management with libsecret."""
@@ -275,7 +289,7 @@ def store_password(self, scheme, host, username, password):
pass
-class RosieWSClientAuthManager(object):
+class RosieWSClientAuthManager:
"""Manage authentication info for a Rosie web service client."""
diff --git a/metomi/rosie/ws_client_cli.py b/metomi/rosie/ws_client_cli.py
index 878afe64a4..ab4d97afdd 100644
--- a/metomi/rosie/ws_client_cli.py
+++ b/metomi/rosie/ws_client_cli.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..f8a8f7fc2d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "metomi-rose",
+ "private": true,
+ "scripts": {
+ "lint": "eslint -c .eslintrc.js sphinx/"
+ },
+ "dependencies": {
+ "eslint-plugin-import": "^2.22.1",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-promise": "^4.2.1",
+ "eslint-plugin-standard": "^5.0.0"
+ },
+ "devDependencies": {
+ "eslint": "^7.14.0",
+ "eslint-config-standard": "^14.1.1"
+ },
+ "bugs": {
+ "url": "https://github.com/metomi/rose/issues"
+ }
+}
diff --git a/pytest.ini b/pytest.ini
index 520187d2d8..94c8001f4c 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,12 +1,15 @@
[pytest]
-addopts = --verbose
+addopts =
+ --verbose
--doctest-modules
--ignore=metomi/rosie
--ignore=metomi/rose/ws.py
--ignore=metomi/rose/metadata_graph.py
+ # these cause import issues
+ --ignore=metomi/rose/etc/
# these tests do IO, don't run them under sphinx-build rather than pytest:
--ignore=metomi/rose/config.py
--ignore=metomi/rose/macro.py
testpaths =
- metomi/rose/tests/*
+ metomi/
sphinx/
diff --git a/sbin/rosa-rpmbuild b/sbin/rosa-rpmbuild
index 4e46c34a8b..87b77a2ac9 100755
--- a/sbin/rosa-rpmbuild
+++ b/sbin/rosa-rpmbuild
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
#-------------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
#
@@ -78,8 +78,6 @@ Rose: a framework for managing and running meteorological suites
%install
rm -fr %{buildroot}
-mkdir -p %{buildroot}/etc/bash_completion.d %{buildroot}/opt %{buildroot}/usr/bin
-cp -p %_sourcedir/$NAME-$REV_BASE_DOT/etc/rose-bash-completion %{buildroot}/etc/bash_completion.d
cp -pr %_sourcedir/$NAME-$REV_BASE_DOT %{buildroot}/opt/$NAME
python3 -m compileall %{buildroot}/opt/$NAME/lib
cp -p %_sourcedir/$NAME-$REV_BASE_DOT/usr/bin/rose %{buildroot}/usr/bin/rose
@@ -89,8 +87,6 @@ cp -p %_sourcedir/$NAME-$REV_BASE_DOT/usr/bin/rose %{buildroot}/usr/bin/rosie
rm -fr %{buildroot}
%files
-/etc/bash_completion.d
-/etc/bash_completion.d/rose-bash-completion
/opt/$NAME
/usr/bin/rose
/usr/bin/rosie
diff --git a/setup.py b/setup.py
index 34b1175aa6..69f7b3486f 100644
--- a/setup.py
+++ b/setup.py
@@ -49,13 +49,14 @@ def find_version(*file_paths):
INSTALL_REQUIRES = [
- "jinja2>=2.10.1",
"aiofiles",
- "tornado",
- "sqlalchemy",
+ "jinja2>=2.10.1",
+ "ldap3",
"metomi-isodatetime",
"requests",
- "ldap3",
+ "sqlalchemy",
+ "tornado",
+ 'psutil>=5.6.0',
]
EXTRAS_REQUIRE = {
'docs': [
@@ -67,7 +68,13 @@ def find_version(*file_paths):
'cylc-sphinx-extensions[all]>=1.2.0'
]
}
-EXTRAS_REQUIRE['all'] = list({y for x in EXTRAS_REQUIRE.values() for y in x})
+TESTS_REQUIRE = [
+ 'pytest',
+ 'flake8'
+]
+EXTRAS_REQUIRE['all'] = list(set(
+ [y for x in EXTRAS_REQUIRE.values() for y in x] + TESTS_REQUIRE
+))
setup(
@@ -77,14 +84,13 @@ def find_version(*file_paths):
version=find_version("metomi", "rose", "__init__.py"),
# Options
- scripts=glob(join("bin", "*"))
- + glob(join("sbin", "*"))
- + glob(join("lib", "bash", "*")),
+ scripts=(
+ glob(join("bin", "*"))
+ + glob(join("sbin", "*"))
+ ),
install_requires=INSTALL_REQUIRES,
extras_require=EXTRAS_REQUIRE,
- package_data={
- "metomi.rose": ["etc/.*"],
- "metomi.rosie": ["lib/*"]
- },
+ tests_require=TESTS_REQUIRE,
+ include_package_data=True,
packages=find_namespace_packages(include=["metomi.*"]),
)
diff --git a/sphinx/Makefile b/sphinx/Makefile
old mode 100755
new mode 100644
index 614210ecb5..198bbe5019
--- a/sphinx/Makefile
+++ b/sphinx/Makefile
@@ -1,186 +1,25 @@
-# Makefile for Sphinx documentation
-#
+# Minimal makefile for Sphinx documentation
-# You can set these variables from the command line.
+# You can set these variables from the command line:
+ROSE_VERSION = $(shell rose version)
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
-BUILDDIR = ../doc
-
-# User-friendly check for sphinx-build
-ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
-$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
-endif
-
-# Internal variables.
-PAPEROPT_a4 = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-# the i18n builder cannot share the environment and doctrees with the others
-I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext pdf slides
+BUILDDIR = ../doc/$(ROSE_VERSION)
+SOURCEDIR = .
+# Put it first so that "make" without argument is like "make help".
help:
- @echo "Please use \`make ' where is one of"
- @echo " html to make standalone HTML files"
- @echo " dirhtml to make HTML files named index.html in directories"
- @echo " singlehtml to make a single large HTML file"
- @echo " pickle to make pickle files"
- @echo " json to make JSON files"
- @echo " htmlhelp to make HTML files and a HTML help project"
- @echo " qthelp to make HTML files and a qthelp project"
- @echo " devhelp to make HTML files and a Devhelp project"
- @echo " epub to make an epub"
- @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
- @echo " latexpdf to make LaTeX files and run them through pdflatex"
- @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
- @echo " text to make text files"
- @echo " man to make manual pages"
- @echo " texinfo to make Texinfo files"
- @echo " info to make Texinfo files and run them through makeinfo"
- @echo " gettext to make PO message catalogs"
- @echo " changes to make an overview of all changed/added/deprecated items"
- @echo " xml to make Docutils-native XML files"
- @echo " pseudoxml to make pseudoxml-XML files for display purposes"
- @echo " linkcheck to check all external links for integrity"
- @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
clean:
- rm -rf $(BUILDDIR)/*
-
-html:
- $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-slides:
- $(SPHINXBUILD) -b slides $(ALLSPHINXOPTS) $(BUILDDIR)/slides
- @echo
- @echo "Build finished. The slide-html files are in $(BUILDDIR)/slides."
-
-pdf: latexpdf
- python2 extract-pdf-documents.py "$(BUILDDIR)"
- @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/pdf."
-
-dirhtml:
- $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
- $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
- @echo
- @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
- $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
- @echo
- @echo "Build finished; now you can process the pickle files."
-
-json:
- $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
- @echo
- @echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
- $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
- @echo
- @echo "Build finished; now you can run HTML Help Workshop with the" \
- ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
- $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
- @echo
- @echo "Build finished; now you can run "qcollectiongenerator" with the" \
- ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
- @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/rose-api-doc.qhcp"
- @echo "To view the help file:"
- @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/rose-api-doc.qhc"
-
-devhelp:
- $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
- @echo
- @echo "Build finished."
- @echo "To view the help file:"
- @echo "# mkdir -p $$HOME/.local/share/devhelp/rose-api-doc"
- @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/rose-api-doc"
- @echo "# devhelp"
-
-epub:
- $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
- @echo
- @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo
- @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
- @echo "Run \`make' in that directory to run these through (pdf)latex" \
- "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo "Running LaTeX files through pdflatex..."
- $(MAKE) -C $(BUILDDIR)/latex all-pdf
- @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-latexpdfja:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo "Running LaTeX files through platex and dvipdfmx..."
- $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
- @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
- $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
- @echo
- @echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
- $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
- @echo
- @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-texinfo:
- $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
- @echo
- @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
- @echo "Run \`make' in that directory to run these through makeinfo" \
- "(use \`make info' here to do that automatically)."
-
-info:
- $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
- @echo "Running Texinfo files through makeinfo..."
- make -C $(BUILDDIR)/texinfo info
- @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
-
-gettext:
- $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
- @echo
- @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
-
-changes:
- $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
- @echo
- @echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
- $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
- @echo
- @echo "Link check complete; look for any errors in the above output " \
- "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
- $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
- @echo "Testing of doctests in the sources finished, look at the " \
- "results in $(BUILDDIR)/doctest/output.txt."
+ @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-xml:
- $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
- @echo
- @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+.PHONY: help clean Makefile .EXPORT_ALL_VARIABLES M
-pseudoxml:
- $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
- @echo
- @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+# NOTE: EXPORT_ALL_VARIABLES exports make vars as env vars
+%: Makefile .EXPORT_ALL_VARIABLES
+ # build documentation
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/sphinx/_static/js/versioning.js b/sphinx/_static/js/versioning.js
index 12b2b61d77..672d58fa49 100644
--- a/sphinx/_static/js/versioning.js
+++ b/sphinx/_static/js/versioning.js
@@ -1,52 +1,59 @@
/* global root_dir current_builder current_page_name current_version */
+/* eslint camelcase: "off" */ // global vars not in camel case
-$(document).ready(function() {
- $.ajax({
- 'async': false,
- 'type': 'GET',
- 'url': root_dir + 'versions.json',
- dataType: 'json',
- success: function (versions) {
- // the DOM element to append version and format selectors to
- var ele = $('#version-selector');
+$(document).ready(function () {
+ $.ajax({
+ async: false,
+ type: 'GET',
+ url: root_dir + 'versions.json',
+ dataType: 'json',
+ success: function (versions) {
+ // the DOM element to append version and format selectors to
+ var ele = $('#version-selector')
- // construct version selector
- var ver = ele.append($('
'));
- $(ver).append($('').append('Versions'));
- for (let version of Object.keys(versions).sort().reverse()) {
- $(ver).append($('')
- .append($('')
- .attr({'href': root_dir + version + '/' +
- current_builder + '/' +
- current_page_name + '.html'})
- .append(version)
- )
- );
- }
+ // construct version selector
+ var ver = ele.append($('
'))
+ $(ver).append($('').append('Versions'))
+ for (const version of Object.keys(versions).sort().reverse()) {
+ $(ver).append($('')
+ .append($('')
+ .attr({
+ href: root_dir
+ + version + '/'
+ + current_builder
+ + '/'
+ + current_page_name + '.html'
+ })
+ .append(version)
+ )
+ )
+ }
- // construct format selector
- var bui = ele.append($('
'));
- $(bui).append($('').append('Formats'));
- var href;
- for (let builder_for_version of versions[current_version].sort()) {
- href = root_dir + current_version + '/' + builder_for_version +
- '/';
- if (['html', 'slides'].indexOf(builder_for_version) >= 0) {
- // format has compatible document structure
- href += current_page_name + '.html';
- } else {
- // structure different, link to the index.html page
- href += 'index.html';
- }
-
-
- $(bui).append($('')
- .append($('')
- .attr({'href': href})
- .append(builder_for_version)
- )
- );
- }
+ // construct format selector
+ var bui = ele.append($('
'))
+ $(bui).append($('').append('Formats'))
+ var href
+ for (const builderForVersion of versions[current_version].sort()) {
+ href = root_dir
+ + current_version
+ + '/'
+ + builderForVersion
+ + '/'
+ if (['html', 'slides'].indexOf(builderForVersion) >= 0) {
+ // format has compatible document structure
+ href += current_page_name + '.html'
+ } else {
+ // structure different, link to the index.html page
+ href += 'index.html'
}
- });
-});
+
+ $(bui).append($('')
+ .append($('')
+ .attr({ href: href })
+ .append(builderForVersion)
+ )
+ )
+ }
+ }
+ })
+})
diff --git a/sphinx/api/built-in/rose_ana.rst b/sphinx/api/built-in/rose_ana.rst
index d372e99c79..86b8a1d6cc 100644
--- a/sphinx/api/built-in/rose_ana.rst
+++ b/sphinx/api/built-in/rose_ana.rst
@@ -83,12 +83,12 @@ details.
.. rose:conf:: grepper-report-limit
Limits the number of lines printed when using the
- :py:mod:`rose.apps.ana_builtin.grepper` analysis class.
+ :py:mod:`metomi.rose.apps.ana_builtin.grepper` analysis class.
.. rose:conf:: skip-if-all-files-missing
- Causes the :py:mod:`rose.apps.ana_builtin.grepper` class to pass
- if all files to be compared are missing.
+ Causes the :py:mod:`metomi.rose.apps.ana_builtin.grepper` class
+ to pass if all files to be compared are missing.
.. rose:conf:: kgo-database
@@ -110,15 +110,5 @@ Analysis Classes
There is one built-in module of analysis classes called ``grepper``.
-.. To document everything:
-
- .. automodule:: metomi.rose.apps.ana_builtin.grepper
- :members:
-
-.. autoclass:: metomi.rose.apps.ana_builtin.grepper.FileCommandPattern
-
-.. autoclass:: metomi.rose.apps.ana_builtin.grepper.FilePattern
-
-.. autoclass:: metomi.rose.apps.ana_builtin.grepper.SingleCommandPattern
-
-.. autoclass:: metomi.rose.apps.ana_builtin.grepper.SingleCommandStatus
+.. automodule:: metomi.rose.apps.ana_builtin.grepper
+ :members: FileCommandPattern, FilePattern, SingleCommandPattern, SingleCommandStatus
diff --git a/sphinx/api/built-in/rose_arch.rst b/sphinx/api/built-in/rose_arch.rst
index 1e8e1a3f93..d82bba7d23 100644
--- a/sphinx/api/built-in/rose_arch.rst
+++ b/sphinx/api/built-in/rose_arch.rst
@@ -42,7 +42,7 @@ In automatic selection mode, this built-in application will be invoked
automatically if a task has a name that starts with ``rose_arch*``.
This means that you can use Rose Arch with something like the example below
-in your ``suite.rc``:
+in your ``flow.cylc``:
.. code-block:: cylc
diff --git a/sphinx/api/command-reference.rst b/sphinx/api/command-reference.rst
index 7ba8f85c0d..70ba7a5ab9 100644
--- a/sphinx/api/command-reference.rst
+++ b/sphinx/api/command-reference.rst
@@ -11,6 +11,29 @@ Rose Commands
----
+.. _command-rose-config-edit:
+
+rose config-edit
+^^^^^^^^^^^^^^^^
+
+TODO: This is here to allow the documentation tests to pass
+
+.. _command-rose-suite-run:
+
+rose suite-run
+^^^^^^^^^^^^^^
+
+TODO: This is here to allow the documentation tests to pass
+
+.. _command-rose-suite-restart:
+
+rose suite-restart
+^^^^^^^^^^^^^^^^^^
+
+TODO: This is here to allow the documentation tests to pass
+
+----
+
.. _command-rose-test-battery:
etc/bin/rose-test-battery
diff --git a/sphinx/api/configuration/api.rst b/sphinx/api/configuration/api.rst
index 1206e66cc7..cda35b76e8 100644
--- a/sphinx/api/configuration/api.rst
+++ b/sphinx/api/configuration/api.rst
@@ -28,8 +28,8 @@ Python
------
Rose provides a Python API for loading, processing, editing and dumping Rose
-configurations via the :py:mod:`rose.config` module located within the Rose
-Python library.
+configurations via the :py:mod:`metomi.rose.config` module located within the
+Rose Python library.
.. automodule:: metomi.rose.config
:members:
diff --git a/sphinx/api/configuration/metadata.rst b/sphinx/api/configuration/metadata.rst
index 38eb860c5f..07f3792ed1 100644
--- a/sphinx/api/configuration/metadata.rst
+++ b/sphinx/api/configuration/metadata.rst
@@ -39,7 +39,7 @@ of precedence:
See :ref:`app-meta-loc` for more details.
The configuration metadata that controls default behaviour will be located in
-``$ROSE_HOME/etc/rose-meta/``.
+``etc/rose-meta/`` within the ``metomi.rose`` Python installation.
Configuration Metadata File
@@ -779,9 +779,9 @@ The metadata options for a configuration fall into four categories:
Another useful Rose built-in widget to use is the array element
aligning page widget,
- ``rose.config_editor.pagewidget.table.PageArrayTable``. You can set
- this for a section or namespace to make sure each *n*-th variable value
- element lines up horizontally. For example:
+ ``metomi.rose.config_editor.pagewidget.table.PageArrayTable``. You can
+ set this for a section or namespace to make sure each *n*-th variable
+ value element lines up horizontally. For example:
.. code-block:: rose
diff --git a/sphinx/api/configuration/site-and-user.rst b/sphinx/api/configuration/site-and-user.rst
index 22451d075a..fe6715c1af 100644
--- a/sphinx/api/configuration/site-and-user.rst
+++ b/sphinx/api/configuration/site-and-user.rst
@@ -8,11 +8,7 @@ site configuration file and per user via the user configuration file. Any
configuration in the site configuration overrides the default, and any
configuration in the user configuration overrides the site configuration and
the default. Rose expects these files to be in the modified INI format
-described in :ref:`Rose Configuration Format`. Rose utilities search for its
-site configuration at ``$ROSE_HOME/etc/rose.conf`` where
-``$ROSE_HOME/bin/rose`` is the location of the ``rose`` command, and they
-search for the user configuration at ``$HOME/.metomi/rose.conf`` where
-``$HOME`` is the home directory of the current user.
+described in :ref:`Rose Configuration Format`.
You can also override many internal constants of
:ref:`command-rose-config-edit` and
diff --git a/sphinx/api/configuration/suite.rst b/sphinx/api/configuration/suite.rst
index ee7952b130..f2d9a3a153 100644
--- a/sphinx/api/configuration/suite.rst
+++ b/sphinx/api/configuration/suite.rst
@@ -8,7 +8,7 @@ Suite Configuration
The configuration and functionality of a suite will usually be covered by
the use of `Cylc`_. In which case, most of the suite configuration will live
-in the Cylc ``suite.rc`` file. Otherwise, a suite is just a directory of
+in the Cylc ``flow.cylc`` file. Otherwise, a suite is just a directory of
files.
A suite directory may contain the following:
@@ -76,20 +76,20 @@ A suite directory may contain the following:
.. rose:conf:: KEY=VALUE
Define a `Jinja2`_ variable ``KEY`` with the value ``VALUE`` for use
- in the ``suite.rc`` file.
+ in the ``flow.cylc`` file.
The assignment will be inserted after the ``#!jinja2`` line of the
- installed ``suite.rc`` file.
+ installed ``flow.cylc`` file.
.. rose:conf:: empy:suite.rc
.. rose:conf:: KEY=VALUE
Define a `EmPy`_ variable ``KEY`` with the value ``VALUE`` for use
- in the ``suite.rc`` file.
+ in the ``flow.cylc`` file.
The assignment will be inserted after the ``#!empy`` line of the
- installed ``suite.rc`` file.
+ installed ``flow.cylc`` file.
.. rose:conf:: [file:NAME]
@@ -107,62 +107,6 @@ A suite directory may contain the following:
used by various Rose utilities, such as the config editor GUI. It can be
used to specify the suite type.
- .. rose:conf:: root-dir=LIST
-
- A new line delimited list of ``PATTERN=DIR`` pairs. The ``PATTERN``
- should be a glob-like pattern for matching a host name. The ``DIR``
- should be the root directory to install a suite run directory. E.g.:
-
- .. code-block:: rose
-
- root-dir=hpc*=$WORKDIR
- =*=$DATADIR
-
- In this example, :ref:`command-rose-suite-run` of a suite with name
- ``$NAME`` will create ``~/cylc-run/$NAME`` as a symbolic link to
- ``$DATADIR/cylc-run/$NAME/`` on any machine, except those with their
- hostnames matching ``hpc*``. In which case, it will create
- ``~/cylc-run/$NAME`` as a symbolic link to ``$WORKDIR/cylc-run/$NAME/``.
-
- .. warning::
-
- If a suite has previously been run changes to any of the ``root-dir``
- settings will take effect on the next clean re-installation i.e::
-
- $ rose suite-run --new
-
- .. rose:conf:: root-dir{share}=LIST
-
- A new line delimited list of ``PATTERN=DIR`` pairs. The ``PATTERN`` should
- be a glob-like pattern for matching a host name. The ``DIR`` should be the
- root directory where the suite's ``share/`` directory should be created.
-
- .. rose:conf:: root-dir{share/cycle}=LIST
-
- A new line delimited list of ``PATTERN=DIR`` pairs. The ``PATTERN`` should
- be a glob-like pattern for matching a host name. The ``DIR`` should be the
- root directory where the suite's ``share/cycle/`` directory should be
- be created.
-
- .. rose:conf:: root-dir{work}=LIST
-
- A new line delimited list of ``PATTERN=DIR`` pairs. The ``PATTERN`` should
- be a glob-like pattern for matching a host name. The ``DIR`` should be the
- root directory where the suite's ``work/`` directory for tasks should be
- created.
-
- .. rose:conf:: root-dir-share=LIST
-
- .. deprecated:: 2015.04
-
- Equivalent to :rose:conf:`root-dir{share}=LIST`.
-
- .. rose:conf:: root-dir-work=LIST
-
- .. deprecated:: 2015.04
-
- Equivalent to :rose:conf:`root-dir{work}=LIST`.
-
.. rose:file:: rose-suite.info
The suite information file :rose:file:`rose-suite.info` should contain the
diff --git a/sphinx/api/environment-variables.rst b/sphinx/api/environment-variables.rst
index 6b53bd0d3e..19b7f7bc31 100644
--- a/sphinx/api/environment-variables.rst
+++ b/sphinx/api/environment-variables.rst
@@ -56,12 +56,26 @@ Rose Environment Variables
.. envvar:: ROSE_CONF_PATH
Description
- Specify a colon (``:``) separated list of paths for searching and loading
- site/user configuration. If this environment variable is not defined, the
- normal behaviour is to search for and load :rose:file:`rose.conf` from
- ``$ROSE_HOME/etc`` and then ``$HOME/.metomi``.
+ If defined this will override the default configuration search path.
+
+ Provide a colon (``:``) separated list of paths to search for
+ ``rose.conf`` files.
+
+ If set to an empty string no config files will be loaded.
Used By
* :ref:`command-rose-test-battery`
+ See also
+ * :envvar:`ROSE_SITE_CONF_PATH`
+ * :rose:file:`rose.conf`
+
+.. envvar:: ROSE_SITE_CONF_PATH
+
+ Description
+ Defines the location of the "site" configuration. Configurations defined
+ here can be overridden by the "user" configuration.
+ See also
+ * :envvar:`ROSE_CONF_PATH`
+ * :rose:file:`rose.conf`
.. envvar:: ROSE_CYCLING_MODE
@@ -124,21 +138,6 @@ Rose Environment Variables
* :ref:`command-rose-app-run`
* :ref:`command-rose-task-run`
-.. envvar:: ROSE_HOME
-
- Description
- Specifies the path to the Rose home directory.
- Used and Provided By
- * ``rose``
-
-.. envvar:: ROSE_HOME_BIN
-
- Description
- Specifies the path to the ``bin/`` or ``sbin/`` directory of the current
- Rose utility.
- Used and Provided By
- * ``rose``
-
.. envvar:: ROSE_LAUNCHER
Description
diff --git a/sphinx/api/other.rst b/sphinx/api/other.rst
new file mode 100644
index 0000000000..5df6cf5b11
--- /dev/null
+++ b/sphinx/api/other.rst
@@ -0,0 +1,11 @@
+Other Python API Docs
+=====================
+
+
+.. automodule:: metomi.rose.reporter
+ :members: Reporter
+
+.. automodule:: metomi.rose.popen
+ :members: RosePopener
+
+.. automodule:: metomi.rose.macros
diff --git a/sphinx/api/rose-bash-library.rst b/sphinx/api/rose-bash-library.rst
index 9f806e2550..17869cbb31 100644
--- a/sphinx/api/rose-bash-library.rst
+++ b/sphinx/api/rose-bash-library.rst
@@ -1,16 +1,16 @@
Rose Bash Library
=================
-The Rose bash library lives in ``lib/bash/``. To import a module, load the file
-into your script. E.g. To load ``rose_usage``, you would do::
+Rose includes some bash modules, they can be located by running::
- . $ROSE_HOME/lib/bash/rose_usage
+ $ rose resource lib/bash
+
+These modules can be invoked like so::
+
+ . "$(rose resource lib/bash/rose_log)"
The modules are:
-``rose_init``
- Called by ``rose`` on initialisation. This is not meant to be for general
- use.
``rose_log``
Provide functions to print log messages.
``rose_usage``
diff --git a/sphinx/api/rose-macro.rst b/sphinx/api/rose-macro.rst
index 24c0b780cf..54a0468e3c 100644
--- a/sphinx/api/rose-macro.rst
+++ b/sphinx/api/rose-macro.rst
@@ -46,7 +46,7 @@ A module containing macros should be stored under a directory
should be a Python package.
When developing macros for Rose internals, macros should be placed in the
-:py:mod:`rose.macros` package in the Rose Python library. They should be
+:py:mod:`metomi.rose.macros` package in the Rose Python library. They should be
referenced by the ``lib/python/rose/macros/__init__.py`` classes and a call to
them can be added in the ``lib/python/rose/config_editor/main.py`` module if
they need to be run implicitly by the config editor.
@@ -60,14 +60,15 @@ Writing Macros
For basic usage see the :ref:`macro tutorial `.
Validator, transformer and reporter macros are Python classes which subclass
-from :py:class:`rose.macro.MacroBase` (:ref:`API `).
+from :py:class:`metomi.rose.macro.MacroBase`
+(:ref:`API `).
These macros implement their behaviours by providing a ``validate``,
``transform`` or ``report`` method. A macro can contain any combination of
these methods so, for example, a macro might be both a validator and a
transformer.
-These methods should accept two :py:class:`rose.config.ConfigNode`
+These methods should accept two :py:class:`metomi.rose.config.ConfigNode`
instances as arguments - one is the configuration, and one is the metadata
configuration that provides information about the configuration items.
@@ -92,11 +93,11 @@ A validator macro should look like:
# Some check on config appends to self.reports using self.add_report
return self.reports
-The returned list should be a list of :py:class:`rose.macro.MacroReport` objects
-containing the section, option, value, and warning strings (info) for each
-setting that is in error. These are initialised behind the scenes by calling the
-inherited method :py:meth:`rose.macro.MacroBase.add_report` via
-:py:meth:`self.add_report`. This has the form:
+The returned list should be a list of :py:class:`metomi.rose.macro.MacroReport`
+objects containing the section, option, value, and warning strings (info) for
+each setting that is in error. These are initialised behind the scenes by
+calling the inherited method :py:meth:`metomi.rose.macro.MacroBase.add_report`.
+This has the form:
.. code-block:: python
@@ -123,7 +124,7 @@ Validator macros have the option to give warnings, which do not count as
formal errors in the Rose config editor GUI. These should be used when
something *may* be wrong, such as warning when using an
advanced-developer-only option. They are invoked by passing a 5th argument
-to :py:meth:`self.add_report`, ``is_warning``, like so:
+to :py:meth:`metomi.rose.macro.MacroBase.add_report`, ``is_warning``, like so:
.. code-block:: python
diff --git a/sphinx/api/rose-upgrader-macros.rst b/sphinx/api/rose-upgrader-macros.rst
index 638ef99b0d..766cc5f36c 100644
--- a/sphinx/api/rose-upgrader-macros.rst
+++ b/sphinx/api/rose-upgrader-macros.rst
@@ -10,7 +10,7 @@ metadata versions. They are classes, very similar to
* An ``upgrade`` method instead of a ``transform`` method
* An optional ``downgrade`` method, identical in API to the ``upgrade``
method, but intended for performing the reverse operation
-* A more helpful API via :py:class:`rose.upgrade.MacroUpgrade` methods
+* A more helpful API via :py:class:`metomi.rose.upgrade.MacroUpgrade` methods
* ``BEFORE_TAG`` and ``AFTER_TAG`` attributes - the version of metadata they
apply to (``BEFORE_TAG``) and the version they upgrade to (``AFTER_TAG``)
@@ -52,8 +52,9 @@ file - ``rose-meta/CATEGORY/versions.py``.
these very modules carefully or use absolute or package level imports like
this: ``from .versionXX_YY import FooBar``.
-Upgrade macros are subclasses of :py:class:`rose.upgrade.MacroUpgrade`. They
-have all the functionality of the :ref:`transformer macros `.
+Upgrade macros are subclasses of :py:class:`metomi.rose.upgrade.MacroUpgrade`.
+They have all the functionality of the
+:ref:`transformer macros `.
.. autoclass:: metomi.rose.upgrade.MacroUpgrade
:members:
diff --git a/sphinx/api/rosie-web.rst b/sphinx/api/rosie-web.rst
index 49afc86fb1..8df2e002e2 100644
--- a/sphinx/api/rosie-web.rst
+++ b/sphinx/api/rosie-web.rst
@@ -29,13 +29,13 @@ supported format is JSON) and use a url that looks like:
REST API
--------
-.. http:get:: (str:prefix)/get_known_keys
+.. http:get:: (prefix)/get_known_keys
Return the main property names stored for suites (e.g. ``idx``, ``branch``,
``owner``) plus any additional names specified in the site config.
- :arg str prefix: Repository prefix.
- :param string format: Desired return format (``json`` or ``None``).
+ :arg prefix: Repository prefix.
+ :param format: Desired return format (``json`` or ``None``).
Example Request
.. code-block:: http
@@ -48,13 +48,13 @@ REST API
["access-list", "idx", "branch", "owner", "project", "revision", "status", "title"]
-.. http:get:: (str:prefix)/get_optional_keys
+.. http:get:: (prefix)/get_optional_keys
Return all unique optional or user-defined property names given in suite
discovery information.
- :arg str prefix: Repository prefix.
- :param string format: Desired return format (``json`` or ``None``).
+ :arg prefix: Repository prefix.
+ :param format: Desired return format (``json`` or ``None``).
Example Request
.. code-block:: http
@@ -67,13 +67,13 @@ REST API
["access-list", "description", "endgame_status", "operational_flag", "tag-list"]
-.. http:get:: (str:prefix)/get_query_operators
+.. http:get:: (prefix)/get_query_operators
Returns all the SQL-like operators used to compare column values that you
- may use in :http:get:`(str:prefix)/query` (e.g. ``eq``, ``ne``, ``contains``, ``like``).
+ may use in :http:get:`(prefix)/query` (e.g. ``eq``, ``ne``, ``contains``, ``like``).
- :arg str prefix: Repository prefix.
- :param string format: Desired return format (``json`` or ``None``).
+ :arg prefix: Repository prefix.
+ :param format: Desired return format (``json`` or ``None``).
Example Request
.. code-block:: http
@@ -86,23 +86,21 @@ REST API
["eq", "ge", "gt", "le", "lt", "ne", "contains", "endswith", "ilike", "like", "match", "startswith"]
-.. http:get:: (str:prefix)/query
+.. http:get:: (prefix)/query
Return a list of suites matching all search terms.
- :arg str prefix: Repository prefix.
- :param list q: List of queries.
- :param string format: Desired return format (``json`` or ``None``).
- :param flag all_revs: Switch on searching older revisions of current suites
- and deleted suites.
+ :arg prefix: Repository prefix.
+ :param q: List of queries.
+ :param format: Desired return format (``json`` or ``None``).
+ :param all_revs: Switch on searching older revisions of current suites and deleted suites.
- :queryparameter str CONJUNCTION: ``and`` or ``or``\ .
- :queryparameter str OPEN_GROUP: optional, one or more ``(``\ .
- :queryparameter str FIELD: e.g. ``idx`` or ``description``\ .
- :queryparameter str OPERATOR: e.g. ``contains`` or ``between``, one of
- the operators returned by :http:get:`(str:prefix)/get_query_operators`.
- :queryparameter str VALUE: e.g. ``euro4m`` or ``200``\ .
- :queryparameter str CLOSE_GROUP: optional, one or more ``)``\ .
+ :queryparameter CONJUNCTION: ``and`` or ``or``\ .
+ :queryparameter OPEN_GROUP: optional, one or more ``(``\ .
+ :queryparameter FIELD: e.g. ``idx`` or ``description``\ .
+ :queryparameter OPERATOR: e.g. ``contains`` or ``between``, one of the operators returned by :http:get:`(prefix)/get_query_operators`.
+ :queryparameter VALUE: e.g. ``euro4m`` or ``200``\ .
+ :queryparameter CLOSE_GROUP: optional, one or more ``)``\ .
Query Format
.. code-block:: none
@@ -136,16 +134,14 @@ REST API
"access-list": ["*"], "operational": "Y"}]
-.. http:get:: (str:prefix)/search
+.. http:get:: (prefix)/search
Return a list of suites matching one or more search terms.
- :arg str prefix: Repository prefix.
- :param list s: List of queries in the same format as
- :http:get:`(str:prefix)/query`
- :param string format: Desired return format (``json`` or ``None``).
- :param flag all_revs: Switch on searching older revisions of current suites
- and deleted suites.
+ :arg prefix: Repository prefix.
+ :param s: List of queries in the same format as :http:get:`(prefix)/query`
+ :param format: Desired return format (``json`` or ``None``).
+ :param ll_revs: Switch on searching older revisions of current suites and deleted suites.
Example Request
Return suites which match ``var``, ``bob`` or ``nowcast``.
diff --git a/sphinx/cheat-sheet.rst b/sphinx/cheat-sheet.rst
index 73a122ddba..9b041d61b5 100644
--- a/sphinx/cheat-sheet.rst
+++ b/sphinx/cheat-sheet.rst
@@ -28,7 +28,7 @@ Starting Suites
* - ::
cylc validate
- cylc run
+ cylc play
- ::
# run the suite in the current directory:
@@ -173,8 +173,8 @@ Visualise A Suite's :term:`graph`
cylc graph
-View A Suite's ``suite.rc`` Configuration
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+View A Suite's ``flow.cylc`` Configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. list-table::
:class: grid-table
diff --git a/sphinx/conf.py b/sphinx/conf.py
index 35389e5c4f..1660a8430d 100644
--- a/sphinx/conf.py
+++ b/sphinx/conf.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -37,6 +34,7 @@
'sphinx.ext.autosummary',
'sphinx.ext.doctest',
'sphinx.ext.graphviz',
+ 'sphinx.ext.intersphinx',
'sphinx.ext.napoleon',
'sphinx.ext.viewcode',
@@ -79,6 +77,18 @@
# vector graphics will be converted to bitmaps in all documents
extensions.append('sphinx.ext.imgconverter')
+# mapping to other Sphinx projects
+# (allows us to reference objects from other projects)
+cylc_version = '8.0a2'
+intersphinx_mapping = {
+ 'cylc': (
+ f'https://cylc.github.io/cylc-doc/{cylc_version}/html/', None
+ ),
+ 'python': (
+ 'https://docs.python.org/', None
+ )
+}
+
# Slide (hieroglyph) settings.
slide_theme = 'single-level'
slide_link_to_html = True
diff --git a/sphinx/developing/autodoc.rst b/sphinx/developing/autodoc.rst
index fa93ed0a4f..09940e2948 100644
--- a/sphinx/developing/autodoc.rst
+++ b/sphinx/developing/autodoc.rst
@@ -25,7 +25,7 @@ Some quick examples:
.. code-block:: python
- def Some_Class(object):
+ def Some_Class:
"""Some summary.
Note __init__ methods are not autodocumented, specify constructor
diff --git a/sphinx/developing/building.rst b/sphinx/developing/building.rst
index 9548f9d21a..ad1b6368e6 100644
--- a/sphinx/developing/building.rst
+++ b/sphinx/developing/building.rst
@@ -22,7 +22,8 @@ The following builders are useful for development:
``linkcheck``
Check external links (internal links are checked by a regular build).
``doctest``
- Run any doctests contained within documented code (e.g. see ``rose.config``).
+ Run any doctests contained within documented code
+ (e.g. see :py:mod:`metomi.rose.config`).
Additionally, if you are not using an editor with a spellchecker you may
wish to use aspell/ispell/hunspell to check any changed docs:
diff --git a/sphinx/ext/auto_cli_doc.py b/sphinx/ext/auto_cli_doc.py
index 88c18e9831..7d805ef98c 100644
--- a/sphinx/ext/auto_cli_doc.py
+++ b/sphinx/ext/auto_cli_doc.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -21,7 +18,7 @@
from collections import OrderedDict
import re
-from subprocess import PIPE, check_output, CalledProcessError
+from subprocess import check_output, CalledProcessError
import sys
from docutils import nodes
@@ -128,7 +125,12 @@ def line_strip(lines):
... ''
... ])
['a']
+ >>> line_strip([''])
+ []
+
"""
+ if all(not line for line in lines):
+ return []
lines = list(lines)
kill = []
for itt in range(0, len(lines)):
diff --git a/sphinx/ext/rose_domain.py b/sphinx/ext/rose_domain.py
index 048b76b8d2..fbac87b6cf 100644
--- a/sphinx/ext/rose_domain.py
+++ b/sphinx/ext/rose_domain.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -62,7 +59,7 @@
Documentation can be auto-built from RST formatted comments in Rose
configuration files using the ``autoconfig`` directive.
- Note that due to the implementation of :py:mod:`rose.config` the
+ Note that due to the implementation of :py:mod:`metomi.rose.config` the
autodocumenter will represent empty sections as top level configuration
nodes.
@@ -76,7 +73,7 @@
.. rose:conf:: jinja2:suite.rc
A section for specifying Jinja2 settings for use in the
- ``suite.rc`` file.
+ ``flow.cylc`` file.
Note that one ommits the square brackets for config sections. If
:rose:conf: contains other :rose:conf:'s then it is implicitly a
@@ -925,7 +922,7 @@ class RoseAutoDirective(Directive):
"""Directive for autodocumenting Rose configuration files.
Uses RST formatted comments in Rose configuration files using
- :py:mod:`rose.config`.
+ :py:mod:`metomi.rose.config`.
Note the directive only documents config objects not the file itself.
diff --git a/sphinx/ext/rose_lang.py b/sphinx/ext/rose_lang.py
index 4e85e460c5..8b975d99a5 100644
--- a/sphinx/ext/rose_lang.py
+++ b/sphinx/ext/rose_lang.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
@@ -157,4 +154,4 @@ class RoseLexer(RegexLexer):
def setup(app):
"""Sphinx plugin setup function."""
- app.add_lexer('rose', RoseLexer())
+ app.add_lexer('rose', RoseLexer)
diff --git a/sphinx/ext/script_include.py b/sphinx/ext/script_include.py
index 067330fdc7..989869c5be 100644
--- a/sphinx/ext/script_include.py
+++ b/sphinx/ext/script_include.py
@@ -1,7 +1,4 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
# Copyright (C) British Crown (Met Office) & Contributors.
-#
# This file is part of Rose, a framework for meteorological suites.
#
# Rose is free software: you can redistribute it and/or modify
diff --git a/sphinx/extract-pdf-documents.py b/sphinx/extract-pdf-documents.py
deleted file mode 100644
index 1e97d81a5d..0000000000
--- a/sphinx/extract-pdf-documents.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# -*- coding: utf-8 -*-
-# -----------------------------------------------------------------------------
-# Copyright (C) British Crown (Met Office) & Contributors.
-#
-# This file is part of Rose, a framework for meteorological suites.
-#
-# Rose 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.
-#
-# Rose 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 Rose. If not, see .
-# -----------------------------------------------------------------------------
-"""Script for extracting PDF documents from a Sphinx LaTeX build.
-
-* Copies PDF documents from the ``latex`` build directory to a ``pdf`` folder.
-* Creates an HTML index of these documents.
-
-"""
-
-import errno
-import os
-import shutil
-import sys
-
-import conf
-
-
-def main():
- try:
- build_dir = sys.argv[1]
- except IndexError:
- sys.exit('usage: extract-pdf-documents build_dir')
- latex_dir = os.path.join(build_dir, 'latex')
- pdf_dir = os.path.join(build_dir, 'pdf')
- os.makedirs(pdf_dir, exist_ok=True)
-
- # the index html file
- html = (
- ''
- ''
- ''
- 'Rose Documentation - PDF Documents'
- ''
- ''
- '
Rose Documentation - PDF Documents
'
- '
'
- )
-
- # loop over PDF documents defined in the Sphinx configuration file
- for file_name, document_name in (x[1:3] for x in conf.latex_documents):
- # move PDF document into the pdf directory
- file_name = file_name.replace('.tex', '.pdf')
- os.rename(
- os.path.join(latex_dir, file_name),
- os.path.join(pdf_dir, file_name)
- )
- # add an index entry for this document
- html += (
- '