Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support recursive condition #114

Merged
merged 12 commits into from
Oct 30, 2023
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
All notable changes to this project will be documented in this file.

## 0.21.0
### Updates
### Fixes
- Fix resolving conditions recursively [#114](https://github.com/Skyscanner/pycfmodel/pull/114)
### Enhancements
- Compatible with Python3.11 [#110](https://github.com/Skyscanner/pycfmodel/pull/110)
- Compatible with Python3.12 [#115](https://github.com/Skyscanner/pycfmodel/pull/115)
### Updates
Expand Down
9 changes: 5 additions & 4 deletions pycfmodel/model/cf_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@ def resolve(self, extra_params=None) -> "CFModel":
dict_value = self.dict()

conditions = dict_value.pop("Conditions", {})
resolved_conditions = {
key: _extended_bool(resolve(value, extended_parameters, self.Mappings, {}))
for key, value in conditions.items()
}
resolved_conditions = {}
for key, value in conditions.items():
resolved_conditions.update(
{key: _extended_bool(resolve(value, extended_parameters, self.Mappings, resolved_conditions))}
)

resources = dict_value.pop("Resources")
resolved_resources = {
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"pip-tools>=2.0.2",
"pytest>=6.0.1",
"pytest-cov>=2.10.1",
"pytest-repeat==0.9.3",
]

docs_requires = [
Expand Down
127 changes: 127 additions & 0 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,133 @@ def test_condition(function, expected_output):
assert resolve(function, parameters, mappings, conditions) == expected_output


# We will repeat the test 10 times, in order to check conditions don't have a different order
# and break the resolving of the model when they are depending of other conditions
@pytest.mark.repeat(10)
@pytest.mark.parametrize(
"num_custom_tags, expected",
[
(0, []),
(1, ["HasAtLeast1Tags"]),
(2, ["HasAtLeast1Tags", "HasAtLeast2Tags"]),
(3, ["HasAtLeast1Tags", "HasAtLeast2Tags", "HasAtLeast3Tags"]),
(4, ["HasAtLeast1Tags", "HasAtLeast2Tags", "HasAtLeast3Tags", "HasAtLeast4Tags"]),
(5, ["HasAtLeast1Tags", "HasAtLeast2Tags", "HasAtLeast3Tags", "HasAtLeast4Tags", "HasAtLeast5Tags"]),
(
6,
[
"HasAtLeast1Tags",
"HasAtLeast2Tags",
"HasAtLeast3Tags",
"HasAtLeast4Tags",
"HasAtLeast5Tags",
"HasAtLeast6Tags",
],
),
(
7,
[
"HasAtLeast1Tags",
"HasAtLeast2Tags",
"HasAtLeast3Tags",
"HasAtLeast4Tags",
"HasAtLeast5Tags",
"HasAtLeast6Tags",
"HasAtLeast7Tags",
],
),
(
8,
[
"HasAtLeast1Tags",
"HasAtLeast2Tags",
"HasAtLeast3Tags",
"HasAtLeast4Tags",
"HasAtLeast5Tags",
"HasAtLeast6Tags",
"HasAtLeast7Tags",
"HasAtLeast8Tags",
],
),
(
9,
[
"HasAtLeast1Tags",
"HasAtLeast2Tags",
"HasAtLeast3Tags",
"HasAtLeast4Tags",
"HasAtLeast5Tags",
"HasAtLeast6Tags",
"HasAtLeast7Tags",
"HasAtLeast8Tags",
"HasAtLeast9Tags",
],
),
(
10,
[
"HasAtLeast1Tags",
"HasAtLeast2Tags",
"HasAtLeast3Tags",
"HasAtLeast4Tags",
"HasAtLeast5Tags",
"HasAtLeast6Tags",
"HasAtLeast7Tags",
"HasAtLeast8Tags",
"HasAtLeast9Tags",
"HasAtLeast10Tags",
],
),
(11, []),
],
)
def test_resolve_recursive_conditions(num_custom_tags, expected):
template = {
"Parameters": {
"NumCustomTags": {"Type": "Number", "Default": 0},
},
"Conditions": {
"HasAtLeast10Tags": {"Fn::Equals": [{"Ref": "NumCustomTags"}, 10]}, # this is the condition stopper
"HasAtLeast9Tags": {
"Fn::Or": [{"Fn::Equals": [{"Ref": "NumCustomTags"}, 9]}, {"Condition": "HasAtLeast10Tags"}]
},
"HasAtLeast8Tags": {
"Fn::Or": [{"Fn::Equals": [{"Ref": "NumCustomTags"}, 8]}, {"Condition": "HasAtLeast9Tags"}]
},
"HasAtLeast7Tags": {
"Fn::Or": [{"Fn::Equals": [{"Ref": "NumCustomTags"}, 7]}, {"Condition": "HasAtLeast8Tags"}]
},
"HasAtLeast6Tags": {
"Fn::Or": [{"Fn::Equals": [{"Ref": "NumCustomTags"}, 6]}, {"Condition": "HasAtLeast7Tags"}]
},
"HasAtLeast5Tags": {
"Fn::Or": [{"Fn::Equals": [{"Ref": "NumCustomTags"}, 5]}, {"Condition": "HasAtLeast6Tags"}]
},
"HasAtLeast4Tags": {
"Fn::Or": [{"Fn::Equals": [{"Ref": "NumCustomTags"}, 4]}, {"Condition": "HasAtLeast5Tags"}]
},
"HasAtLeast3Tags": {
"Fn::Or": [{"Fn::Equals": [{"Ref": "NumCustomTags"}, 3]}, {"Condition": "HasAtLeast4Tags"}]
},
"HasAtLeast2Tags": {
"Fn::Or": [{"Fn::Equals": [{"Ref": "NumCustomTags"}, 2]}, {"Condition": "HasAtLeast3Tags"}]
},
"HasAtLeast1Tags": {
"Fn::Or": [{"Fn::Equals": [{"Ref": "NumCustomTags"}, 1]}, {"Condition": "HasAtLeast2Tags"}]
},
},
"Resources": {},
}

model = parse(template).resolve(extra_params={"NumCustomTags": num_custom_tags})

# retrieve positive conditions in the model
positive_conditions = [
condition_name for condition_name, condition_value in model.Conditions.items() if condition_value
]
assert expected.sort() == positive_conditions.sort()


def test_select_and_ref():
parameters = {"DbSubnetIpBlocks": ["10.0.48.0/24", "10.0.112.0/24", "10.0.176.0/24"]}
mappings = {}
Expand Down
20 changes: 10 additions & 10 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ class Model(BaseModel):
@pytest.mark.parametrize(
"value",
[
("192.168.0.0/24"),
("192.168.128.0/30"),
"192.168.0.0/24",
"192.168.128.0/30",
(2**32 - 1), # no mask equals to mask /32
(b"\xff\xff\xff\xff"), # /32
(("192.168.0.0", 24)),
b"\xff\xff\xff\xff", # /32
("192.168.0.0", 24),
(IPv4Network("192.168.0.0/24")),
],
)
Expand All @@ -53,10 +53,10 @@ class Model(BaseModel):
@pytest.mark.parametrize(
"value",
[
("2001:db00::0/120"),
(20_282_409_603_651_670_423_947_251_286_015), # /128
(b"\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"),
(("2001:db00::0", 120)),
"2001:db00::0/120",
20_282_409_603_651_670_423_947_251_286_015, # /128
b"\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff",
("2001:db00::0", 120),
(IPv6Network("2001:db00::0/120")),
],
)
Expand All @@ -67,7 +67,7 @@ class Model(BaseModel):
assert Model(ip=value).ip == IPv6Network(value)


@pytest.mark.parametrize("value", [("213.174.214.100/27"), ("192.168.56.101/16"), ("192.0.2.1/24")])
@pytest.mark.parametrize("value", ["213.174.214.100/27", "192.168.56.101/16", "192.0.2.1/24"])
def test_loose_ip_v4_is_not_strict(value):
class Model(BaseModel):
ip: LooseIPv4Network = None
Expand All @@ -79,7 +79,7 @@ class Model(BaseModel):

@pytest.mark.parametrize(
"value",
[("2012::1234:abcd:ffff:c0a8:101/64"), ("2022::1234:abcd:ffff:c0a8:101/64"), ("2032::1234:abcd:ffff:c0a8:101/64")],
["2012::1234:abcd:ffff:c0a8:101/64", "2022::1234:abcd:ffff:c0a8:101/64", "2032::1234:abcd:ffff:c0a8:101/64"],
)
def test_loose_ip_v6_is_not_strict(value):
class Model(BaseModel):
Expand Down
Loading