diff --git a/bin/cylc-broadcast b/bin/cylc-broadcast
index 31d3728434e..ed6d6f02bee 100755
--- a/bin/cylc-broadcast
+++ b/bin/cylc-broadcast
@@ -86,7 +86,7 @@ from cylc.terminal import cli_function
from cylc.broadcast_report import (
get_broadcast_change_report, get_broadcast_bad_options_report)
from cylc.cfgspec.suite import SPEC, upg
-from cylc.cfgvalidate import cylc_config_validate
+from cylc.parsec.validate import cylc_config_validate
from cylc.network.client import SuiteRuntimeClient
from cylc.option_parsers import CylcOptionParser as COP
from cylc.print_tree import print_tree
diff --git a/lib/cylc/cfgspec/globalcfg.py b/lib/cylc/cfgspec/globalcfg.py
index 37489120fe4..0df8f96fdbe 100644
--- a/lib/cylc/cfgspec/globalcfg.py
+++ b/lib/cylc/cfgspec/globalcfg.py
@@ -23,17 +23,16 @@
import shutil
from tempfile import mkdtemp
-from cylc.parsec.config import ParsecConfig
-from cylc.parsec.exceptions import ParsecError
-from cylc.parsec.upgrade import upgrader
-
from cylc import LOG
-from cylc.cfgvalidate import (
- cylc_config_validate, CylcConfigValidator as VDR, DurationFloat)
+from cylc import __version__ as CYLC_VERSION
from cylc.exceptions import GlobalConfigError
from cylc.hostuserutil import get_user_home, is_remote_user
from cylc.network import Priv
-from cylc import __version__ as CYLC_VERSION
+from cylc.parsec.config import ParsecConfig
+from cylc.parsec.exceptions import ParsecError
+from cylc.parsec.upgrade import upgrader
+from cylc.parsec.validate import (
+ DurationFloat, CylcConfigValidator as VDR, cylc_config_validate)
# Nested dict of spec items.
# Spec value is [value_type, default, allowed_2, allowed_3, ...]
diff --git a/lib/cylc/cfgspec/suite.py b/lib/cylc/cfgspec/suite.py
index ea84951ed9e..0715c346981 100644
--- a/lib/cylc/cfgspec/suite.py
+++ b/lib/cylc/cfgspec/suite.py
@@ -19,13 +19,11 @@
from isodatetime.data import Calendar
-from cylc.parsec.upgrade import upgrader
-from cylc.parsec.config import ParsecConfig
-
-from cylc.cfgvalidate import (
- cylc_config_validate, CylcConfigValidator as VDR, DurationFloat)
from cylc.network import Priv
-
+from cylc.parsec.config import ParsecConfig
+from cylc.parsec.upgrade import upgrader
+from cylc.parsec.validate import (
+ DurationFloat, CylcConfigValidator as VDR, cylc_config_validate)
# Nested dict of spec items.
# Spec value is [value_type, default, allowed_2, allowed_3, ...]
diff --git a/lib/cylc/cfgvalidate.py b/lib/cylc/cfgvalidate.py
deleted file mode 100644
index f60f383a561..00000000000
--- a/lib/cylc/cfgvalidate.py
+++ /dev/null
@@ -1,292 +0,0 @@
-#!/usr/bin/env python3
-
-# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
-# Copyright (C) 2008-2019 NIWA & British Crown (Met Office) & Contributors.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-"""Extend parsec.validate for Cylc configuration.
-
-Coerce more value type from string (to time point, duration, xtriggers, etc.).
-"""
-
-import re
-
-from isodatetime.data import Calendar, Duration, TimePoint
-from isodatetime.dumpers import TimePointDumper
-from isodatetime.parsers import DurationParser, TimePointParser
-from cylc.parsec.validate import ParsecValidator, IllegalValueError
-
-from cylc.subprocctx import SubFuncContext
-
-
-class DurationFloat(float):
- """Duration in floating point seconds, but stringify as ISO8601 format."""
-
- def __str__(self):
- return str(Duration(seconds=self, standardize=True))
-
-
-class CylcConfigValidator(ParsecValidator):
- """Type validator and coercer for Cylc configurations.
-
- Attributes:
- .coercers (dict):
- Map value type keys with coerce methods.
- """
- # Parameterized names containing at least one comma.
- _REC_NAME_SUFFIX = re.compile(r'\A[\w\-+%@]+\Z')
- _REC_TRIG_FUNC = re.compile(r'(\w+)\((.*)\)(?::(\w+))?')
-
- # Value type constants
- V_CYCLE_POINT = 'V_CYCLE_POINT'
- V_CYCLE_POINT_FORMAT = 'V_CYCLE_POINT_FORMAT'
- V_CYCLE_POINT_TIME_ZONE = 'V_CYCLE_POINT_TIME_ZONE'
- V_INTERVAL = 'V_INTERVAL'
- V_INTERVAL_LIST = 'V_INTERVAL_LIST'
- V_PARAMETER_LIST = 'V_PARAMETER_LIST'
- V_XTRIGGER = 'V_XTRIGGER'
-
- def __init__(self):
- ParsecValidator.__init__(self)
- self.coercers.update({
- self.V_CYCLE_POINT: self.coerce_cycle_point,
- self.V_CYCLE_POINT_FORMAT: self.coerce_cycle_point_format,
- self.V_CYCLE_POINT_TIME_ZONE: self.coerce_cycle_point_time_zone,
- self.V_INTERVAL: self.coerce_interval,
- self.V_INTERVAL_LIST: self.coerce_interval_list,
- self.V_PARAMETER_LIST: self.coerce_parameter_list,
- self.V_XTRIGGER: self.coerce_xtrigger,
- })
-
- @classmethod
- def coerce_cycle_point(cls, value, keys):
- """Coerce value to a cycle point."""
- if not value:
- return None
- value = cls.strip_and_unquote(keys, value)
- if value == 'now':
- # Handle this later in config.py when the suite UTC mode is known.
- return value
- if "next" in value or "previous" in value:
- # Handle this later, as for "now".
- return value
- if value.isdigit():
- # Could be an old date-time cycle point format, or integer format.
- return value
- if "P" not in value and (
- value.startswith('-') or value.startswith('+')):
- # We don't know the value given for num expanded year digits...
- for i in range(1, 101):
- try:
- TimePointParser(num_expanded_year_digits=i).parse(value)
- except ValueError:
- continue
- return value
- raise IllegalValueError('cycle point', keys, value)
- if "P" in value:
- # ICP is an offset
- parser = DurationParser()
- try:
- if value.startswith("-"):
- # parser doesn't allow negative duration with this setup?
- parser.parse(value[1:])
- else:
- parser.parse(value)
- return value
- except ValueError:
- raise IllegalValueError("cycle point", keys, value)
- try:
- TimePointParser().parse(value)
- except ValueError:
- raise IllegalValueError('cycle point', keys, value)
- return value
-
- @classmethod
- def coerce_cycle_point_format(cls, value, keys):
- """Coerce to a cycle point format (either CCYYMM... or %Y%m...)."""
- value = cls.strip_and_unquote(keys, value)
- if not value:
- return None
- test_timepoint = TimePoint(year=2001, month_of_year=3, day_of_month=1,
- hour_of_day=4, minute_of_hour=30,
- second_of_minute=54)
- if '/' in value:
- raise IllegalValueError('cycle point format', keys, value)
- if '%' in value:
- try:
- TimePointDumper().strftime(test_timepoint, value)
- except ValueError:
- raise IllegalValueError('cycle point format', keys, value)
- return value
- if 'X' in value:
- for i in range(1, 101):
- dumper = TimePointDumper(num_expanded_year_digits=i)
- try:
- dumper.dump(test_timepoint, value)
- except ValueError:
- continue
- return value
- raise IllegalValueError('cycle point format', keys, value)
- dumper = TimePointDumper()
- try:
- dumper.dump(test_timepoint, value)
- except ValueError:
- raise IllegalValueError('cycle point format', keys, value)
- return value
-
- @classmethod
- def coerce_cycle_point_time_zone(cls, value, keys):
- """Coerce value to a cycle point time zone format - Z, +13, -0800..."""
- value = cls.strip_and_unquote(keys, value)
- if not value:
- return None
- test_timepoint = TimePoint(year=2001, month_of_year=3, day_of_month=1,
- hour_of_day=4, minute_of_hour=30,
- second_of_minute=54)
- dumper = TimePointDumper()
- test_timepoint_string = dumper.dump(test_timepoint, 'CCYYMMDDThhmmss')
- test_timepoint_string += value
- parser = TimePointParser(allow_only_basic=True)
- try:
- parser.parse(test_timepoint_string)
- except ValueError:
- raise IllegalValueError(
- 'cycle point time zone format', keys, value)
- return value
-
- def coerce_interval(self, value, keys):
- """Coerce an ISO 8601 interval (or number: back-comp) into seconds."""
- value = self.strip_and_unquote(keys, value)
- if not value:
- # Allow explicit empty values.
- return None
- try:
- interval = DurationParser().parse(value)
- except ValueError:
- raise IllegalValueError("ISO 8601 interval", keys, value)
- days, seconds = interval.get_days_and_seconds()
- return DurationFloat(
- days * Calendar.default().SECONDS_IN_DAY + seconds)
-
- def coerce_interval_list(self, value, keys):
- """Coerce a list of intervals (or numbers: back-comp) into seconds."""
- return self.expand_list(
- self.strip_and_unquote_list(keys, value),
- keys,
- lambda v: self.coerce_interval(v, keys))
-
- @classmethod
- def coerce_parameter_list(cls, value, keys):
- """Coerce parameter list.
-
- Args:
- value (str):
- This can be a list of str values. Each str value must conform
- to the same restriction as a task name.
- Otherwise, this can be a mixture of int ranges and int values.
- keys (list):
- Keys in nested dict that represents the raw configuration.
-
- Return (list):
- A list of strings or a list of sorted integers.
-
- Raise:
- IllegalValueError:
- If value has both str and int range or if a str value breaks
- the task name restriction.
- """
- items = []
- can_only_be = None # A flag to prevent mixing str and int range
- for item in cls.strip_and_unquote_list(keys, value):
- values = cls.parse_int_range(item)
- if values is not None:
- if can_only_be == str:
- raise IllegalValueError(
- 'parameter', keys, value, 'mixing int range and str')
- can_only_be = int
- items.extend(values)
- elif cls._REC_NAME_SUFFIX.match(item):
- try:
- int(item)
- except ValueError:
- if can_only_be == int:
- raise IllegalValueError(
- 'parameter', keys, value,
- 'mixing int range and str')
- can_only_be = str
- items.append(item)
- else:
- raise IllegalValueError(
- 'parameter', keys, value, '%s: bad value' % item)
- try:
- return [int(item) for item in items]
- except ValueError:
- return items
-
- def coerce_xtrigger(self, value, keys):
- """Coerce a string into an xtrigger function context object.
-
- func_name(*func_args, **func_kwargs)
- Checks for legal string templates in arg values too.
-
- """
-
- label = keys[-1]
- value = self.strip_and_unquote(keys, value)
- if not value:
- raise IllegalValueError("xtrigger", keys, value)
- fname = None
- args = []
- kwargs = {}
- match = self._REC_TRIG_FUNC.match(value)
- if match is None:
- raise IllegalValueError("xtrigger", keys, value)
- fname, fargs, intvl = match.groups()
- if intvl:
- intvl = self.coerce_interval(intvl, keys)
-
- if fargs:
- # Extract function args and kwargs.
- for farg in fargs.split(r','):
- try:
- key, val = farg.strip().split(r'=', 1)
- except ValueError:
- args.append(self._coerce_type(farg.strip()))
- else:
- kwargs[key.strip()] = self._coerce_type(val.strip())
-
- return SubFuncContext(label, fname, args, kwargs, intvl)
-
- @classmethod
- def _coerce_type(cls, value):
- """Convert value to int, float, or bool, if possible."""
- try:
- val = int(value)
- except ValueError:
- try:
- val = float(value)
- except ValueError:
- if value == 'False':
- val = False
- elif value == 'True':
- val = True
- else:
- # Leave as string.
- val = cls.strip_and_unquote([], value)
- return val
-
-
-def cylc_config_validate(cfg_root, spec_root):
- """Short for "CylcConfigValidator().validate(...)"."""
- return CylcConfigValidator().validate(cfg_root, spec_root)
diff --git a/lib/cylc/parsec/validate.py b/lib/cylc/parsec/validate.py
index 1796679fd97..31023d32e2f 100644
--- a/lib/cylc/parsec/validate.py
+++ b/lib/cylc/parsec/validate.py
@@ -20,15 +20,21 @@
Check all items are legal.
Check all values are legal (type; min, max, allowed options).
Coerce value type from string (to int, float, list, etc.).
+Coerce more value type from string (to time point, duration, xtriggers, etc.).
Also provides default values from the spec as a nested dict.
"""
-from collections import deque
import re
+from collections import deque
from textwrap import dedent
+from isodatetime.data import Duration, TimePoint, Calendar
+from isodatetime.dumpers import TimePointDumper
+from isodatetime.parsers import TimePointParser, DurationParser
+
from cylc.parsec.exceptions import (
ListValueError, IllegalValueError, IllegalItemError)
+from cylc.subprocctx import SubFuncContext
class ParsecValidator(object):
@@ -404,3 +410,265 @@ def _unquoted_list_parse(cls, keys, value):
def parsec_validate(cfg_root, spec_root):
"""Short for "ParsecValidator().validate(...)"."""
return ParsecValidator().validate(cfg_root, spec_root)
+
+
+class DurationFloat(float):
+ """Duration in floating point seconds, but stringify as ISO8601 format."""
+
+ def __str__(self):
+ return str(Duration(seconds=self, standardize=True))
+
+
+class CylcConfigValidator(ParsecValidator):
+ """Type validator and coercer for Cylc configurations.
+
+ Attributes:
+ .coercers (dict):
+ Map value type keys with coerce methods.
+ """
+ # Parameterized names containing at least one comma.
+ _REC_NAME_SUFFIX = re.compile(r'\A[\w\-+%@]+\Z')
+ _REC_TRIG_FUNC = re.compile(r'(\w+)\((.*)\)(?::(\w+))?')
+
+ # Value type constants
+ V_CYCLE_POINT = 'V_CYCLE_POINT'
+ V_CYCLE_POINT_FORMAT = 'V_CYCLE_POINT_FORMAT'
+ V_CYCLE_POINT_TIME_ZONE = 'V_CYCLE_POINT_TIME_ZONE'
+ V_INTERVAL = 'V_INTERVAL'
+ V_INTERVAL_LIST = 'V_INTERVAL_LIST'
+ V_PARAMETER_LIST = 'V_PARAMETER_LIST'
+ V_XTRIGGER = 'V_XTRIGGER'
+
+ def __init__(self):
+ ParsecValidator.__init__(self)
+ self.coercers.update({
+ self.V_CYCLE_POINT: self.coerce_cycle_point,
+ self.V_CYCLE_POINT_FORMAT: self.coerce_cycle_point_format,
+ self.V_CYCLE_POINT_TIME_ZONE: self.coerce_cycle_point_time_zone,
+ self.V_INTERVAL: self.coerce_interval,
+ self.V_INTERVAL_LIST: self.coerce_interval_list,
+ self.V_PARAMETER_LIST: self.coerce_parameter_list,
+ self.V_XTRIGGER: self.coerce_xtrigger,
+ })
+
+ @classmethod
+ def coerce_cycle_point(cls, value, keys):
+ """Coerce value to a cycle point."""
+ if not value:
+ return None
+ value = cls.strip_and_unquote(keys, value)
+ if value == 'now':
+ # Handle this later in config.py when the suite UTC mode is known.
+ return value
+ if "next" in value or "previous" in value:
+ # Handle this later, as for "now".
+ return value
+ if value.isdigit():
+ # Could be an old date-time cycle point format, or integer format.
+ return value
+ if "P" not in value and (
+ value.startswith('-') or value.startswith('+')):
+ # We don't know the value given for num expanded year digits...
+ for i in range(1, 101):
+ try:
+ TimePointParser(num_expanded_year_digits=i).parse(value)
+ except ValueError:
+ continue
+ return value
+ raise IllegalValueError('cycle point', keys, value)
+ if "P" in value:
+ # ICP is an offset
+ parser = DurationParser()
+ try:
+ if value.startswith("-"):
+ # parser doesn't allow negative duration with this setup?
+ parser.parse(value[1:])
+ else:
+ parser.parse(value)
+ return value
+ except ValueError:
+ raise IllegalValueError("cycle point", keys, value)
+ try:
+ TimePointParser().parse(value)
+ except ValueError:
+ raise IllegalValueError('cycle point', keys, value)
+ return value
+
+ @classmethod
+ def coerce_cycle_point_format(cls, value, keys):
+ """Coerce to a cycle point format (either CCYYMM... or %Y%m...)."""
+ value = cls.strip_and_unquote(keys, value)
+ if not value:
+ return None
+ test_timepoint = TimePoint(year=2001, month_of_year=3, day_of_month=1,
+ hour_of_day=4, minute_of_hour=30,
+ second_of_minute=54)
+ if '/' in value:
+ raise IllegalValueError('cycle point format', keys, value)
+ if '%' in value:
+ try:
+ TimePointDumper().strftime(test_timepoint, value)
+ except ValueError:
+ raise IllegalValueError('cycle point format', keys, value)
+ return value
+ if 'X' in value:
+ for i in range(1, 101):
+ dumper = TimePointDumper(num_expanded_year_digits=i)
+ try:
+ dumper.dump(test_timepoint, value)
+ except ValueError:
+ continue
+ return value
+ raise IllegalValueError('cycle point format', keys, value)
+ dumper = TimePointDumper()
+ try:
+ dumper.dump(test_timepoint, value)
+ except ValueError:
+ raise IllegalValueError('cycle point format', keys, value)
+ return value
+
+ @classmethod
+ def coerce_cycle_point_time_zone(cls, value, keys):
+ """Coerce value to a cycle point time zone format - Z, +13, -0800..."""
+ value = cls.strip_and_unquote(keys, value)
+ if not value:
+ return None
+ test_timepoint = TimePoint(year=2001, month_of_year=3, day_of_month=1,
+ hour_of_day=4, minute_of_hour=30,
+ second_of_minute=54)
+ dumper = TimePointDumper()
+ test_timepoint_string = dumper.dump(test_timepoint, 'CCYYMMDDThhmmss')
+ test_timepoint_string += value
+ parser = TimePointParser(allow_only_basic=True)
+ try:
+ parser.parse(test_timepoint_string)
+ except ValueError:
+ raise IllegalValueError(
+ 'cycle point time zone format', keys, value)
+ return value
+
+ def coerce_interval(self, value, keys):
+ """Coerce an ISO 8601 interval (or number: back-comp) into seconds."""
+ value = self.strip_and_unquote(keys, value)
+ if not value:
+ # Allow explicit empty values.
+ return None
+ try:
+ interval = DurationParser().parse(value)
+ except ValueError:
+ raise IllegalValueError("ISO 8601 interval", keys, value)
+ days, seconds = interval.get_days_and_seconds()
+ return DurationFloat(
+ days * Calendar.default().SECONDS_IN_DAY + seconds)
+
+ def coerce_interval_list(self, value, keys):
+ """Coerce a list of intervals (or numbers: back-comp) into seconds."""
+ return self.expand_list(
+ self.strip_and_unquote_list(keys, value),
+ keys,
+ lambda v: self.coerce_interval(v, keys))
+
+ @classmethod
+ def coerce_parameter_list(cls, value, keys):
+ """Coerce parameter list.
+
+ Args:
+ value (str):
+ This can be a list of str values. Each str value must conform
+ to the same restriction as a task name.
+ Otherwise, this can be a mixture of int ranges and int values.
+ keys (list):
+ Keys in nested dict that represents the raw configuration.
+
+ Return (list):
+ A list of strings or a list of sorted integers.
+
+ Raise:
+ IllegalValueError:
+ If value has both str and int range or if a str value breaks
+ the task name restriction.
+ """
+ items = []
+ can_only_be = None # A flag to prevent mixing str and int range
+ for item in cls.strip_and_unquote_list(keys, value):
+ values = cls.parse_int_range(item)
+ if values is not None:
+ if can_only_be == str:
+ raise IllegalValueError(
+ 'parameter', keys, value, 'mixing int range and str')
+ can_only_be = int
+ items.extend(values)
+ elif cls._REC_NAME_SUFFIX.match(item):
+ try:
+ int(item)
+ except ValueError:
+ if can_only_be == int:
+ raise IllegalValueError(
+ 'parameter', keys, value,
+ 'mixing int range and str')
+ can_only_be = str
+ items.append(item)
+ else:
+ raise IllegalValueError(
+ 'parameter', keys, value, '%s: bad value' % item)
+ try:
+ return [int(item) for item in items]
+ except ValueError:
+ return items
+
+ def coerce_xtrigger(self, value, keys):
+ """Coerce a string into an xtrigger function context object.
+
+ func_name(*func_args, **func_kwargs)
+ Checks for legal string templates in arg values too.
+
+ """
+
+ label = keys[-1]
+ value = self.strip_and_unquote(keys, value)
+ if not value:
+ raise IllegalValueError("xtrigger", keys, value)
+ fname = None
+ args = []
+ kwargs = {}
+ match = self._REC_TRIG_FUNC.match(value)
+ if match is None:
+ raise IllegalValueError("xtrigger", keys, value)
+ fname, fargs, intvl = match.groups()
+ if intvl:
+ intvl = self.coerce_interval(intvl, keys)
+
+ if fargs:
+ # Extract function args and kwargs.
+ for farg in fargs.split(r','):
+ try:
+ key, val = farg.strip().split(r'=', 1)
+ except ValueError:
+ args.append(self._coerce_type(farg.strip()))
+ else:
+ kwargs[key.strip()] = self._coerce_type(val.strip())
+
+ return SubFuncContext(label, fname, args, kwargs, intvl)
+
+ @classmethod
+ def _coerce_type(cls, value):
+ """Convert value to int, float, or bool, if possible."""
+ try:
+ val = int(value)
+ except ValueError:
+ try:
+ val = float(value)
+ except ValueError:
+ if value == 'False':
+ val = False
+ elif value == 'True':
+ val = True
+ else:
+ # Leave as string.
+ val = cls.strip_and_unquote([], value)
+ return val
+
+
+def cylc_config_validate(cfg_root, spec_root):
+ """Short for "CylcConfigValidator().validate(...)"."""
+ return CylcConfigValidator().validate(cfg_root, spec_root)
diff --git a/lib/cylc/scheduler.py b/lib/cylc/scheduler.py
index 5d48b2a3230..d44c0d25672 100644
--- a/lib/cylc/scheduler.py
+++ b/lib/cylc/scheduler.py
@@ -1318,7 +1318,7 @@ def set_auto_restart(self, restart_delay=None,
Restart handled by `suite_auto_restart`.
Args:
- restart_delay (cylc.cfgvalidate.DurationFloat):
+ restart_delay (cylc.parsec.DurationFloat):
Suite will wait a random period between 0 and
`restart_delay` seconds before attempting to stop/restart in
order to avoid multiple suites restarting simultaneously.
diff --git a/lib/cylc/tests/parsec/test_config.py b/lib/cylc/tests/parsec/test_config.py
index b9512c71b1b..099f0c36f87 100644
--- a/lib/cylc/tests/parsec/test_config.py
+++ b/lib/cylc/tests/parsec/test_config.py
@@ -19,12 +19,12 @@
import tempfile
import unittest
-from cylc.parsec import config, validate
-from cylc.parsec.exceptions import ParsecError
-from cylc.cfgvalidate import CylcConfigValidator as VDR
-from cylc.cfgvalidate import cylc_config_validate
+from cylc.parsec import config
from cylc.parsec.OrderedDict import OrderedDictWithDefaults
+from cylc.parsec.exceptions import ParsecError
from cylc.parsec.upgrade import upgrader
+from cylc.parsec.validate import (
+ cylc_config_validate, IllegalItemError, CylcConfigValidator as VDR)
SAMPLE_SPEC_1 = {
'section1': {
@@ -176,7 +176,7 @@ def test_validate(self):
sparse = OrderedDictWithDefaults()
parsec_config.validate(sparse) # empty dict is OK
- with self.assertRaises(validate.IllegalItemError):
+ with self.assertRaises(IllegalItemError):
sparse = OrderedDictWithDefaults()
sparse['name'] = 'True'
parsec_config.validate(sparse) # name is not valid
diff --git a/lib/cylc/tests/parsec/test_validate.py b/lib/cylc/tests/parsec/test_validate.py
index cb4a42bfa74..778cabee429 100644
--- a/lib/cylc/tests/parsec/test_validate.py
+++ b/lib/cylc/tests/parsec/test_validate.py
@@ -18,9 +18,12 @@
import unittest
-from cylc.cfgvalidate import CylcConfigValidator as VDR
from cylc.parsec.OrderedDict import OrderedDictWithDefaults
-from cylc.parsec.validate import *
+from cylc.parsec.exceptions import IllegalValueError
+from cylc.parsec.validate import (
+ CylcConfigValidator as VDR,
+ CylcConfigValidator, DurationFloat, ListValueError,
+ IllegalItemError, ParsecValidator, parsec_validate)
SAMPLE_SPEC_1 = {
'section1': {
@@ -467,6 +470,133 @@ def test_strip_and_unquote_list_multiparam(self):
['a'], 'a, b, c'
)
+ def test_coerce_cycle_point(self):
+ """Test coerce_cycle_point."""
+ validator = CylcConfigValidator()
+ # The good
+ for value, result in [
+ ('', None),
+ ('3', '3'),
+ ('2018', '2018'),
+ ('20181225T12Z', '20181225T12Z'),
+ ('2018-12-25T12:00+11:00', '2018-12-25T12:00+11:00')]:
+ self.assertEqual(
+ validator.coerce_cycle_point(value, ['whatever']), result)
+ # The bad
+ for value in [
+ 'None', ' Who cares? ', 'True', '1, 2', '20781340E10']:
+ self.assertRaises(
+ IllegalValueError,
+ validator.coerce_cycle_point, value, ['whatever'])
+
+ def test_coerce_cycle_point_format(self):
+ """Test coerce_cycle_point_format."""
+ validator = CylcConfigValidator()
+ # The good
+ for value, result in [
+ ('', None),
+ ('%Y%m%dT%H%M%z', '%Y%m%dT%H%M%z'),
+ ('CCYYMMDDThhmmZ', 'CCYYMMDDThhmmZ'),
+ ('XCCYYMMDDThhmmZ', 'XCCYYMMDDThhmmZ')]:
+ self.assertEqual(
+ validator.coerce_cycle_point_format(value, ['whatever']),
+ result)
+ # The bad
+ for value in ['%i%j', 'Y/M/D']:
+ self.assertRaises(
+ IllegalValueError,
+ validator.coerce_cycle_point_format, value, ['whatever'])
+
+ def test_coerce_cycle_point_time_zone(self):
+ """Test coerce_cycle_point_time_zone."""
+ validator = CylcConfigValidator()
+ # The good
+ for value, result in [
+ ('', None),
+ ('Z', 'Z'),
+ ('+0000', '+0000'),
+ ('+0100', '+0100'),
+ ('+1300', '+1300'),
+ ('-0630', '-0630')]:
+ self.assertEqual(
+ validator.coerce_cycle_point_time_zone(value, ['whatever']),
+ result)
+ # The bad
+ for value in ['None', 'Big Bang Time', 'Standard Galaxy Time']:
+ self.assertRaises(
+ IllegalValueError,
+ validator.coerce_cycle_point_time_zone, value, ['whatever'])
+
+ def test_coerce_interval(self):
+ """Test coerce_interval."""
+ validator = CylcConfigValidator()
+ # The good
+ for value, result in [
+ ('', None),
+ ('P3D', DurationFloat(259200)),
+ ('PT10M10S', DurationFloat(610))]:
+ self.assertEqual(
+ validator.coerce_interval(value, ['whatever']), result)
+ # The bad
+ for value in ['None', '5 days', '20', '-12']:
+ self.assertRaises(
+ IllegalValueError,
+ validator.coerce_interval, value, ['whatever'])
+
+ def test_coerce_interval_list(self):
+ """Test coerce_interval_list."""
+ validator = CylcConfigValidator()
+ # The good
+ for value, results in [
+ ('', []),
+ ('P3D', [DurationFloat(259200)]),
+ ('P3D, PT10M10S', [DurationFloat(259200), DurationFloat(610)]),
+ ('25*PT30M,10*PT1H',
+ [DurationFloat(1800)] * 25 + [DurationFloat(3600)] * 10)]:
+ items = validator.coerce_interval_list(value, ['whatever'])
+ for item, result in zip(items, results):
+ self.assertAlmostEqual(item, result)
+ # The bad
+ for value in ['None', '5 days', '20', 'PT10S, -12']:
+ self.assertRaises(
+ IllegalValueError,
+ validator.coerce_interval_list, value, ['whatever'])
+
+ def test_coerce_parameter_list(self):
+ """Test coerce_parameter_list."""
+ validator = CylcConfigValidator()
+ # The good
+ for value, result in [
+ ('', []),
+ ('planet', ['planet']),
+ ('planet, star, galaxy', ['planet', 'star', 'galaxy']),
+ ('1..5, 21..25', [1, 2, 3, 4, 5, 21, 22, 23, 24, 25]),
+ ('-15, -10, -5, -1..1', [-15, -10, -5, -1, 0, 1])]:
+ self.assertEqual(
+ validator.coerce_parameter_list(value, ['whatever']), result)
+ # The bad
+ for value in ['foo/bar', 'p1, 1..10', '2..3, 4, p']:
+ self.assertRaises(
+ IllegalValueError,
+ validator.coerce_parameter_list, value, ['whatever'])
+
+ def test_coerce_xtrigger(self):
+ """Test coerce_xtrigger."""
+ validator = CylcConfigValidator()
+ # The good
+ for value, result in [
+ ('foo(x="bar")', 'foo(x=bar)'),
+ ('foo(x, y, z="zebra")', 'foo(x, y, z=zebra)')]:
+ self.assertEqual(
+ validator.coerce_xtrigger(value, ['whatever']).get_signature(),
+ result)
+ # The bad
+ for value in [
+ '', 'foo(', 'foo)', 'foo,bar']:
+ self.assertRaises(
+ IllegalValueError,
+ validator.coerce_xtrigger, value, ['whatever'])
+
if __name__ == '__main__':
unittest.main()
diff --git a/lib/cylc/tests/test_cfgvalidate.py b/lib/cylc/tests/test_cfgvalidate.py
deleted file mode 100644
index ebf477d453b..00000000000
--- a/lib/cylc/tests/test_cfgvalidate.py
+++ /dev/null
@@ -1,158 +0,0 @@
-#!/usr/bin/env python3
-
-# THIS FILE IS PART OF THE CYLC SUITE ENGINE.
-# Copyright (C) 2008-2019 NIWA & British Crown (Met Office) & Contributors.
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-"""Unit Tests for cylc.cfgvalidate.CylcConfigValidator.coerce* methods."""
-
-import unittest
-
-from cylc.cfgvalidate import (
- CylcConfigValidator, DurationFloat, IllegalValueError)
-
-
-class TestCylcConfigValidator(unittest.TestCase):
- """Unit Tests for cylc.cfgvalidate.CylcConfigValidator.coerce* methods."""
-
- def test_coerce_cycle_point(self):
- """Test coerce_cycle_point."""
- validator = CylcConfigValidator()
- # The good
- for value, result in [
- ('', None),
- ('3', '3'),
- ('2018', '2018'),
- ('20181225T12Z', '20181225T12Z'),
- ('2018-12-25T12:00+11:00', '2018-12-25T12:00+11:00')]:
- self.assertEqual(
- validator.coerce_cycle_point(value, ['whatever']), result)
- # The bad
- for value in [
- 'None', ' Who cares? ', 'True', '1, 2', '20781340E10']:
- self.assertRaises(
- IllegalValueError,
- validator.coerce_cycle_point, value, ['whatever'])
-
- def test_coerce_cycle_point_format(self):
- """Test coerce_cycle_point_format."""
- validator = CylcConfigValidator()
- # The good
- for value, result in [
- ('', None),
- ('%Y%m%dT%H%M%z', '%Y%m%dT%H%M%z'),
- ('CCYYMMDDThhmmZ', 'CCYYMMDDThhmmZ'),
- ('XCCYYMMDDThhmmZ', 'XCCYYMMDDThhmmZ')]:
- self.assertEqual(
- validator.coerce_cycle_point_format(value, ['whatever']),
- result)
- # The bad
- for value in ['%i%j', 'Y/M/D']:
- self.assertRaises(
- IllegalValueError,
- validator.coerce_cycle_point_format, value, ['whatever'])
-
- def test_coerce_cycle_point_time_zone(self):
- """Test coerce_cycle_point_time_zone."""
- validator = CylcConfigValidator()
- # The good
- for value, result in [
- ('', None),
- ('Z', 'Z'),
- ('+0000', '+0000'),
- ('+0100', '+0100'),
- ('+1300', '+1300'),
- ('-0630', '-0630')]:
- self.assertEqual(
- validator.coerce_cycle_point_time_zone(value, ['whatever']),
- result)
- # The bad
- for value in ['None', 'Big Bang Time', 'Standard Galaxy Time']:
- self.assertRaises(
- IllegalValueError,
- validator.coerce_cycle_point_time_zone, value, ['whatever'])
-
- def test_coerce_interval(self):
- """Test coerce_interval."""
- validator = CylcConfigValidator()
- # The good
- for value, result in [
- ('', None),
- ('P3D', DurationFloat(259200)),
- ('PT10M10S', DurationFloat(610))]:
- self.assertEqual(
- validator.coerce_interval(value, ['whatever']), result)
- # The bad
- for value in ['None', '5 days', '20', '-12']:
- self.assertRaises(
- IllegalValueError,
- validator.coerce_interval, value, ['whatever'])
-
- def test_coerce_interval_list(self):
- """Test coerce_interval_list."""
- validator = CylcConfigValidator()
- # The good
- for value, results in [
- ('', []),
- ('P3D', [DurationFloat(259200)]),
- ('P3D, PT10M10S', [DurationFloat(259200), DurationFloat(610)]),
- ('25*PT30M,10*PT1H',
- [DurationFloat(1800)] * 25 + [DurationFloat(3600)] * 10)]:
- items = validator.coerce_interval_list(value, ['whatever'])
- for item, result in zip(items, results):
- self.assertAlmostEqual(item, result)
- # The bad
- for value in ['None', '5 days', '20', 'PT10S, -12']:
- self.assertRaises(
- IllegalValueError,
- validator.coerce_interval_list, value, ['whatever'])
-
- def test_coerce_parameter_list(self):
- """Test coerce_parameter_list."""
- validator = CylcConfigValidator()
- # The good
- for value, result in [
- ('', []),
- ('planet', ['planet']),
- ('planet, star, galaxy', ['planet', 'star', 'galaxy']),
- ('1..5, 21..25', [1, 2, 3, 4, 5, 21, 22, 23, 24, 25]),
- ('-15, -10, -5, -1..1', [-15, -10, -5, -1, 0, 1])]:
- self.assertEqual(
- validator.coerce_parameter_list(value, ['whatever']), result)
- # The bad
- for value in ['foo/bar', 'p1, 1..10', '2..3, 4, p']:
- self.assertRaises(
- IllegalValueError,
- validator.coerce_parameter_list, value, ['whatever'])
-
- def test_coerce_xtrigger(self):
- """Test coerce_xtrigger."""
- validator = CylcConfigValidator()
- # The good
- for value, result in [
- ('foo(x="bar")', 'foo(x=bar)'),
- ('foo(x, y, z="zebra")', 'foo(x, y, z=zebra)')]:
- self.assertEqual(
- validator.coerce_xtrigger(value, ['whatever']).get_signature(),
- result)
- # The bad
- for value in [
- '', 'foo(', 'foo)', 'foo,bar']:
- self.assertRaises(
- IllegalValueError,
- validator.coerce_xtrigger, value, ['whatever'])
-
-
-if __name__ == '__main__':
- unittest.main()