Skip to content

Commit

Permalink
Macro validation fix (#2670)
Browse files Browse the repository at this point in the history
macro: replicate Python 2 comparison behaviour classes for int, float and str

* Preserves metadata compatibility with Rose 2019.
* Permits the `this` variable to be an empty string without killing comparison logic.
  • Loading branch information
wxtim authored Feb 9, 2023
1 parent 610a351 commit 6948b05
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 1 deletion.
11 changes: 11 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ creating a new release entry be sure to copy & paste the span tag with the
`actions:bind` attribute, which is used by a regex to find the text to be
updated. Only the first match gets replaced, so it's fine to leave the old
ones in. -->

--------------------------------------------------------------------------------

## 2.0.3 (<span actions:bind='release-date'>Coming Soon</span>)

### Fixes

[#2670](https://github.com/metomi/rose/pull/2670) - Rose Macro made to
follow Python 2 type comparison rules.


--------------------------------------------------------------------------------

## 2.0.2 (<span actions:bind='release-date'>Released 2022-11-08</span>)
Expand Down
143 changes: 142 additions & 1 deletion metomi/rose/macros/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,139 @@
)


class Int(int):
"""Override integer to maintain Python2 style interface
"""
def __lt__(self, other):
"""
Examples:
>>> Int(4) < Int(6)
True
>>> Int(4) < Float(6.1)
True
>>> Int(4) < Str('Zaphod Beeblebrox')
True
>>> Int(99999) < Str('Zaphod Beeblebrox')
True
>>> Int(4) < Float(-5.5)
False
>>> Int(42) < 42
False
>>> Int(77) < ''
False
"""
try:
return int(self) < other
except TypeError:
return False

def __gt__(self, other):
"""
Examples:
>>> Int(2) > Float(2.0)
False
>>> Int(3) > Float(2.0)
True
"""
try:
return int(self) > other
except TypeError:
return True

def __le__(self, other):
return not self.__gt__(other)

def __ge__(self, other):
return not self.__lt__(other)


class Float(float):
def __lt__(self, other):
"""
Examples:
>>> Int(4) < Int(6)
True
>>> Int(4) < Float(6.1)
True
>>> Int(4) < Str('Zaphod Beeblebrox')
True
>>> Int(99999) < Str('Zaphod Beeblebrox')
True
>>> Int(4) < Float(-5.5)
False
>>> Int(1199) < 1199
False
"""
try:
return float(self) < float(other)
except (TypeError, ValueError):
return True

def __gt__(self, other):
"""
Examples:
>>> Int(2) > Float(2.0)
False
>>> Int(3) > Float(2.0)
True
"""
try:
return float(self) > float(other)
except (TypeError, ValueError):
return False

def __le__(self, other):
return not self.__gt__(other)

def __ge__(self, other):
return not self.__lt__(other)


class Str(str):
def __lt__(self, other):
"""
Examples:
>>> Str('aardvaark') < Str('zebra')
True
>>> Str('alligator') < Int(400)
False
>>> Str('pink fairy armadillo') < 'syrian hamster'
True
"""
if isinstance(other, (int, float, Float, Int)):
return False
elif isinstance(other, Str):
return str(self) < str(other)
else:
return str(self) < other

def __gt__(self, other):
"""
Examples:
>>> Str('aardvaark') > Str('zebra')
False
>>> Str('alligator') > Int(400)
True
>>> Str('pink fairy armadillo') > 'syrian hamster'
False
"""
if isinstance(other, (int, float, Float, Int)):
return True
elif isinstance(other, Str):
return str(self) > str(other)
else:
return str(self) > other

def __le__(self, other):
return not self.__gt__(other)

def __ge__(self, other):
return not self.__lt__(other)


MYTYPES = {str: Str, int: Int, bool: Int, float: Float}


class RuleValueError(Exception):
def __init__(self, *args):
self.args = args
Expand Down Expand Up @@ -211,12 +344,20 @@ def evaluate_rule(self, rule, setting_id, config, meta_config):
rule, setting_id, config, meta_config
)
template = jinja2.Template(rule_template_str)

# Recast to our own implementations of base types to maintain
# Python 2 behaviour
for key, value in rule_id_values.items():
for basetype, mytype in MYTYPES.items():
if isinstance(value, basetype):
rule_id_values[key] = mytype(rule_id_values[key])

return_string = template.render(rule_id_values)
return ast.literal_eval(return_string)

def evaluate_rule_id_usage(self, rule, setting_id, meta_config):
"""Return a set of setting ids referenced in the provided rule."""
log_ids = set([])
log_ids = set()
self._process_rule(
rule, setting_id, None, meta_config, log_ids=log_ids
)
Expand Down
183 changes: 183 additions & 0 deletions metomi/rose/tests/macros/test_py2_compat_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright (C) 2012-2020 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 <http://www.gnu.org/licenses/>.
# -----------------------------------------------------------------------------
"""
Tests for Python 2 compatibility types
These types Int, Float and Str over-ride Python3 types
to produce Python2 behaviour to provide back compatibility for Rose.
The tests were generated as follows:
#!/usr/bin/env python2
# Examine what python2 does in a variety of cases
import itertools
floats = [-999.0, -5.1, -1., 0., 1., 5.1, 999.]
ints = [-999, -5, -1, 0, 1, 5, 999]
strings = ['aardvaark', 'zebra']
data = {}
for combo in itertools.permutations(floats + ints + strings, 2):
gt = combo[0] > combo[1]
eq = combo[0] == combo[1]
data[combo] = {}
data[combo]["gt"] = gt
data[combo]["eq"] = eq
print(data)
"""
import pytest
from metomi.rose.macros.rule import Float, Int, Str

TESTS = {
(1.0, "zebra"): {"gt": False, "eq": False},
("zebra", 0.0): {"gt": True, "eq": False},
(-1.0, 0.0): {"gt": False, "eq": False},
(1.0, -5): {"gt": True, "eq": False},
(-999.0, 1.0): {"gt": False, "eq": False},
(5.1, "zebra"): {"gt": False, "eq": False},
("zebra", -1.0): {"gt": True, "eq": False},
(-5, 1.0): {"gt": False, "eq": False},
(-5.1, 1.0): {"gt": False, "eq": False},
(999.0, -1.0): {"gt": True, "eq": False},
(-5.1, "aardvaark"): {"gt": False, "eq": False},
(-5, -5.1): {"gt": True, "eq": False},
(-999.0, -5): {"gt": False, "eq": False},
("aardvaark", -999.0): {"gt": True, "eq": False},
("zebra", 5): {"gt": True, "eq": False},
(-5.1, -5): {"gt": False, "eq": False},
(-1.0, 999.0): {"gt": False, "eq": False},
(1.0, 1): {"gt": False, "eq": True},
("aardvaark", -5.1): {"gt": True, "eq": False},
(5, 999.0): {"gt": False, "eq": False},
(-999.0, 5.1): {"gt": False, "eq": False},
(0.0, "aardvaark"): {"gt": False, "eq": False},
(-1.0, "aardvaark"): {"gt": False, "eq": False},
(0.0, 1.0): {"gt": False, "eq": False},
(-5.1, "zebra"): {"gt": False, "eq": False},
(5.1, -5): {"gt": True, "eq": False},
(-999.0, -5.1): {"gt": False, "eq": False},
("aardvaark", 0.0): {"gt": True, "eq": False},
(5, -1.0): {"gt": True, "eq": False},
(-999.0, 999.0): {"gt": False, "eq": False},
(-999.0, 5): {"gt": False, "eq": False},
(-5, 5.1): {"gt": False, "eq": False},
(-5, "aardvaark"): {"gt": False, "eq": False},
(5.1, 1.0): {"gt": True, "eq": False},
(-5, 5): {"gt": False, "eq": False},
(5.1, -1.0): {"gt": True, "eq": False},
(999.0, 5.1): {"gt": True, "eq": False},
(-1.0, 1.0): {"gt": False, "eq": False},
(5.1, "aardvaark"): {"gt": False, "eq": False},
(999.0, 1.0): {"gt": True, "eq": False},
("aardvaark", 5): {"gt": True, "eq": False},
("aardvaark", 1.0): {"gt": True, "eq": False},
(0.0, 999.0): {"gt": False, "eq": False},
(999.0, "aardvaark"): {"gt": False, "eq": False},
(1.0, 5): {"gt": False, "eq": False},
(-999.0, 0.0): {"gt": False, "eq": False},
(-1.0, -1): {"gt": False, "eq": True},
(-1.0, -5.1): {"gt": True, "eq": False},
(-5, 0.0): {"gt": False, "eq": False},
(-5, 999.0): {"gt": False, "eq": False},
(-5.1, 0.0): {"gt": False, "eq": False},
(0.0, 5.1): {"gt": False, "eq": False},
(999.0, 999): {"gt": False, "eq": True},
(-999.0, -999): {"gt": False, "eq": True},
(0.0, 5): {"gt": False, "eq": False},
(-999.0, "zebra"): {"gt": False, "eq": False},
(1.0, 0.0): {"gt": True, "eq": False},
(-1.0, -5): {"gt": True, "eq": False},
("zebra", 5.1): {"gt": True, "eq": False},
(-5, "zebra"): {"gt": False, "eq": False},
("aardvaark", -1.0): {"gt": True, "eq": False},
(-5.1, 5.1): {"gt": False, "eq": False},
(0.0, -1.0): {"gt": True, "eq": False},
(5.1, 5): {"gt": True, "eq": False},
(-5, -999.0): {"gt": True, "eq": False},
(-1.0, 5): {"gt": False, "eq": False},
(1.0, -5.1): {"gt": True, "eq": False},
(1.0, -1.0): {"gt": True, "eq": False},
("zebra", "aardvaark"): {"gt": True, "eq": False},
(0.0, -5): {"gt": True, "eq": False},
(0.0, "zebra"): {"gt": False, "eq": False},
(1.0, -999.0): {"gt": True, "eq": False},
(5, 5.1): {"gt": False, "eq": False},
("zebra", -5): {"gt": True, "eq": False},
(5.1, 0.0): {"gt": True, "eq": False},
(0.0, -5.1): {"gt": True, "eq": False},
(-5.1, -1.0): {"gt": False, "eq": False},
(999.0, 5): {"gt": True, "eq": False},
(-5, -1.0): {"gt": False, "eq": False},
(999.0, -5): {"gt": True, "eq": False},
(-1.0, 5.1): {"gt": False, "eq": False},
(5, -5.1): {"gt": True, "eq": False},
(-999.0, -1.0): {"gt": False, "eq": False},
(5, 0.0): {"gt": True, "eq": False},
(-999.0, "aardvaark"): {"gt": False, "eq": False},
(0.0, -999.0): {"gt": True, "eq": False},
(5.1, -999.0): {"gt": True, "eq": False},
("aardvaark", -5): {"gt": True, "eq": False},
("aardvaark", "zebra"): {"gt": False, "eq": False},
("zebra", 999.0): {"gt": True, "eq": False},
(999.0, -5.1): {"gt": True, "eq": False},
("zebra", -999.0): {"gt": True, "eq": False},
(5, "aardvaark"): {"gt": False, "eq": False},
(5.1, 999.0): {"gt": False, "eq": False},
(-5.1, -999.0): {"gt": True, "eq": False},
(5, 1.0): {"gt": True, "eq": False},
(5, -5): {"gt": True, "eq": False},
(1.0, 999.0): {"gt": False, "eq": False},
(999.0, 0.0): {"gt": True, "eq": False},
(5.1, -5.1): {"gt": True, "eq": False},
(1.0, 5.1): {"gt": False, "eq": False},
(5, -999.0): {"gt": True, "eq": False},
(-5.1, 5): {"gt": False, "eq": False},
("aardvaark", 5.1): {"gt": True, "eq": False},
(-1.0, -999.0): {"gt": True, "eq": False},
("aardvaark", 999.0): {"gt": True, "eq": False},
(0.0, 0): {"gt": False, "eq": True},
(-1.0, "zebra"): {"gt": False, "eq": False},
(-5.1, 999.0): {"gt": False, "eq": False},
(5, "zebra"): {"gt": False, "eq": False},
("zebra", -5.1): {"gt": True, "eq": False},
(999.0, "zebra"): {"gt": False, "eq": False},
(1.0, "aardvaark"): {"gt": False, "eq": False},
(999.0, -999.0): {"gt": True, "eq": False},
("zebra", 1.0): {"gt": True, "eq": False},
}
MYTYPES = {int: Int, float: Float, str: Str}


@pytest.mark.parametrize('test', TESTS.items())
def test_python2_compat_classes(test):
"""Subclassed types (MYTYPES.values) behave like Python2 types.
"""
first, second = test[0]
# Convert test values from base to subclassed types:
for basetype, mytype in MYTYPES.items():
if isinstance(first, basetype):
first = mytype(first)
if isinstance(second, basetype):
second = mytype(second)
assert (first > second) == test[1]['gt']
assert (first == second) == test[1]['eq']

0 comments on commit 6948b05

Please sign in to comment.