diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 08ca30f7..a53ffdb7 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 diff --git a/docs/source/fundamentals/substitutions.rst b/docs/source/fundamentals/substitutions.rst index efed7a64..e21e054b 100644 --- a/docs/source/fundamentals/substitutions.rst +++ b/docs/source/fundamentals/substitutions.rst @@ -169,6 +169,15 @@ As we saw above, a parameter value starting with ``=`` invokes the formula parse * ``STRIPEXT(`` *path* ``)`` returns the path minus the extension. + * ``IS_NUM(arg)`` true if the argument is a numeric type. + + * ``IS_STR(arg)`` true if the argument is a string type. + + * ``VALID(arg)`` true if the argument is valid, and evaluates to non-zero. This is a useful pattern when dealing + with parameters of a mixed type (that can be e.g. strings or numbers). For example, ``recipe.a > 0`` would throw an + error is ``a`` is a string, but ``VALID(recipe.a > 0)`` would return False in this case. + + As should be evident from the list above, certain functions expect arguments of a particular type (for example, the pathname manipulation functions expect strings). Note that function arguments are treated as fully-fledged expressions of their own (with the exception of the first argument of ``IFSET()``, which must be a namespace lookup by definition.) In particular, {}-substitutions are applied to string arguments. For example, the following can be a legit (and useful) invocation:: diff --git a/pyproject.toml b/pyproject.toml index 0be688d9..e526084c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ packages = [ ] [tool.poetry.dependencies] -python = "^3.8" +python = "^3.9" +numpy = "^1.20" munch = "^2.5.0" omegaconf = "^2.1" importlib_metadata = { version = "4.13.0", python = "3.7" } diff --git a/scabha/evaluator.py b/scabha/evaluator.py index 06f873c2..31b4be0d 100644 --- a/scabha/evaluator.py +++ b/scabha/evaluator.py @@ -3,6 +3,7 @@ import os.path import fnmatch import pyparsing +import numpy as np pyparsing.ParserElement.enable_packrat() from pyparsing import * from pyparsing import common @@ -156,12 +157,33 @@ def RANGE(self, evaluator, args): def make_range(*x): return list(range(*x)) return self.evaluate_generic_callable(evaluator, "RANGE", make_range, args, min_args=1, max_args=3) + + def VALID(self, evaluator, args): + if len(args) != 1: + raise FormulaError(f"{'.'.join(evaluator.location)}: VALID() expects one argument, got {len(args)}") + try: + result = evaluator._evaluate_result(args[0], allow_unset=True) + except (TypeError, ValueError) as exc: + return False + if type(result) is UNSET: + return False + return result def MIN(self, evaluator, args): return self.evaluate_generic_callable(evaluator, "MIN", min, args, min_args=1) def MAX(self, evaluator, args): return self.evaluate_generic_callable(evaluator, "MAX", max, args, min_args=1) + + def IS_STR(self, evaluator, args): + def is_str(x): + return type(x) is str + return self.evaluate_generic_callable(evaluator, "IS_STR", is_str, args, min_args=1, max_args=1) + + def IS_NUM(self, evaluator, args): + def is_num(x): + return np.isscalar(x) and (np.isreal(x) or np.isscalar(x)) + return self.evaluate_generic_callable(evaluator, "IS_NUM", is_num, args, min_args=1, max_args=1) def IF(self, evaluator, args): if len(args) < 3 or len(args) > 4: @@ -300,7 +322,7 @@ def construct_parser(): # functions functions = reduce(operator.or_, map(Keyword, ["IF", "IFSET", "GLOB", "EXISTS", "LIST", - "BASENAME", "DIRNAME", "EXTENSION", "STRIPEXT", "MIN", "MAX", "RANGE", "NOSUBST", "SORT", "RSORT"])) + "BASENAME", "DIRNAME", "EXTENSION", "STRIPEXT", "MIN", "MAX", "IS_STR", "IS_NUM", "VALID", "RANGE", "NOSUBST", "SORT", "RSORT"])) # these functions take one argument, which could also be a sequence anyseq_functions = reduce(operator.or_, map(Keyword, ["GLOB", "EXISTS"]))