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

getters should not change choice #394

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
51 changes: 44 additions & 7 deletions openapiart/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from google.protobuf import json_format
import sanity_pb2_grpc as pb2_grpc
import sanity_pb2 as pb2
from inspect import stack

try:
from typing import Union, Dict, List, Any, Literal
Expand Down Expand Up @@ -612,7 +613,7 @@ class OpenApiObject(OpenApiBase, OpenApiValidator):
"""

__slots__ = (
"__warnings__", "_properties", "_parent", "_choice"
"__warnings__", "_properties", "_parent", "_choice", "_parent_choice"
)
_DEFAULTS = {}
_TYPES = {}
Expand All @@ -622,6 +623,7 @@ def __init__(self, parent=None, choice=None):
super(OpenApiObject, self).__init__()
self._parent = parent
self._choice = choice
self._parent_choice = None
self._properties = {}
self.__warnings__ = []

Expand Down Expand Up @@ -653,13 +655,17 @@ def _get_property(
if name in self._properties and self._properties[name] is not None:
return self._properties[name]
if isinstance(default_value, type) is True:
self._set_choice(name)
if "_choice" in default_value.__slots__:
self._properties[name] = default_value(
parent=parent, choice=choice
)
else:
self._properties[name] = default_value(parent=parent)
if self._is_internal_call("_get_property"):
self._set_choice(name)
self._properties[name] = default_value(parent=parent)
else:
self._properties[name] = default_value(parent=parent)
self._properties[name]._parent_choice = choice
if (
"_DEFAULTS" in dir(self._properties[name])
and "choice" in self._properties[name]._DEFAULTS
Expand All @@ -670,24 +676,55 @@ def _get_property(
)
else:
if default_value is None and name in self._DEFAULTS:
self._set_choice(name)
self._properties[name] = self._DEFAULTS[name]
if self._is_internal_call("_get_property"):
self._set_choice(name)
self._properties[name] = self._DEFAULTS[name]
else:
return self._DEFAULTS[name]
else:
self._properties[name] = default_value
return self._properties[name]

def _set_property(self, name, value, choice=None):
if name in self._DEFAULTS and value is None:
if name == "choice":
self._set_choice(value)
self._properties[name] = value
elif name in self._DEFAULTS and value is None:
self._set_choice(name)
self._properties[name] = self._DEFAULTS[name]
else:
self._set_choice(name)
self._properties[name] = value
self._validate_unique_and_name(name, value)
self._validate_constraint(name, value)
if self._parent is not None and self._choice is not None and value is not None:
if (
self._parent is not None
and self._choice is not None
and value is not None
and self._is_internal_call("_set_property")
):
self._parent._set_property("choice", self._choice)
elif self._parent is not None and self._parent_choice is not None:
self._parent._set_property("choice", self._parent_choice)

def _is_internal_call(self, function_name):
"""
This fucntion basically checks the call stack and returns true if the current call to the function is from the internal code base.
Basically it helps us to distinguish between user calls and internal code base calls
"""

call_stack = stack()
if function_name == "_set_property":
internal_func = "__init__"
if call_stack[2][3] != internal_func and call_stack[3][3] != internal_func:
return True
elif function_name == "_get_property":
if stack()[3][3] in ["__init__", "get", "_get_property"]:
return True
else:
raise NotImplementedError("currently function %s is not supported" % function_name )
return False

def _encode(self):
"""Helper method for serialization"""
output = {}
Expand Down
2 changes: 2 additions & 0 deletions openapiart/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,8 @@ def _write_openapi_object(self, ref, choice_method_name=None):
self._write(1, "def __init__(%s):" % (params))
self._write(2, "super(%s, self).__init__()" % class_name)
self._write(2, "self._parent = parent")
if "choice" in params:
self._write(2, "self._choice = choice")
for property_name in properties:
self._write(
2,
Expand Down
3 changes: 2 additions & 1 deletion openapiart/tests/test_auto.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ def test_auto(config):
assert config.auto_field_test.choice == "value"

config.auto_field_test.auto
assert config.auto_field_test.choice == "auto"
# just fetching attribute should not change choice
assert config.auto_field_test.choice == "value"
154 changes: 154 additions & 0 deletions openapiart/tests/test_choice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import pytest


def test_getting_leaf_nodes_without_default(api):
m = api.metrics_request()

# setting properties should change choice
m.port = "p1"
assert m.choice == "port"
assert m._properties.get("port", None) == "p1"
assert m._properties.get("flow", None) is None
m.flow = "f1"
assert m.choice == "flow"
assert m._properties.get("port", None) is None
assert m._properties.get("flow", None) == "f1"

# should be able to set choice as well
m.choice = "port"
assert m._properties.get("choice") == "port"
assert m.choice == "port"
m.choice = "flow"
assert m._properties.get("choice") == "flow"
assert m.choice == "flow"


def test_getting_leaf_nodes_with_default(api):
config = api.prefix_config()

# checking default values
assert config.f.choice == "f_a"
assert config.f._properties.get("choice") == "f_a"

# fetching properties should not change choice
config.f.f_a
assert config.f.choice == "f_a"
config.f.f_b
assert config.f.choice == "f_a"

# setting properties should change choice
config.f.f_a = "p1"
assert config.f.choice == "f_a"
assert config.f._properties.get("f_a", None) == "p1"
assert config.f._properties.get("f_b", None) is None
config.f.f_b = 3.45
assert config.f.choice == "f_b"
assert config.f._properties.get("f_a", None) is None
assert config.f._properties.get("f_b", None) == 3.45


def test_get_set_for_choice_heirarchy(api):
config = api.prefix_config()
j = config.j.add()

# check default for parent
assert j.choice == "j_a"
assert j._properties.get("choice") == "j_a"

# fetching properties should not change choice
j.j_b.f_b
assert j.j_b.choice == "f_a"
assert j.j_b._properties.get("f_a") == "some string"
assert j.j_b._properties.get("f_b") is None
j.j_b.f_a
assert j.j_b.choice == "f_a"
assert j.j_b._properties.get("f_a") == "some string"
assert j.j_b._properties.get("f_b") is None

# mix of set and get of properties should handle choice properly
j.j_b.f_b = 3.4
assert j.j_b.choice == "f_b"
assert j.j_b._properties.get("f_b") == 3.4
assert j.j_b._properties.get("f_a") is None
j.j_b.f_a
assert j.j_b.choice == "f_b"
assert j.j_b._properties.get("f_b") == 3.4
assert j.j_b._properties.get("f_a") is None

j.j_b.f_a = "asd"
assert j.j_b.choice == "f_a"
assert j.j_b._properties.get("f_a") == "asd"
assert j.j_b._properties.get("f_b") is None
j.j_b.f_b
assert j.j_b.choice == "f_a"
assert j.j_b._properties.get("f_a") == "asd"
assert j.j_b._properties.get("f_b") is None


def test_get_set_for_parent_choice_objects(api):

config = api.prefix_config()
j = config.j.add()

# check default for parent
assert j.choice == "j_a"
assert j._properties.get("j_a", None) is not None
assert j._properties.get("j_b", None) is None

# fetching properties should not change choice
f = j.j_b
assert j.choice == "j_a"
assert j._properties.get("j_a", None) is not None

# setting properties should change parent choice
f.f_a = "asd"
assert j.choice == "j_b"
assert j._properties.get("j_b", None) is not None
assert j._properties.get("j_a", None) is None

j.j_a.e_a = 123
assert j.choice == "j_a"
assert j._properties.get("j_a", None) is not None
assert j._properties.get("j_b", None) is None


def test_get_set_for_pattern_properties(api):
config = api.prefix_config()
ip = config.ipv4_pattern.ipv4

# check default
assert ip._properties.get("choice", None) == "value"
assert ip._properties.get("value", None) is not None
assert ip._properties.get("values", None) is None
assert ip._properties.get("increment", None) is None
assert ip._properties.get("decrement", None) is None

# fetching properties should not change choice
ip.value
assert ip._properties.get("choice", None) == "value"
assert ip._properties.get("value", None) is not None

ip.values
assert ip._properties.get("choice", None) == "value"
assert ip._properties.get("value", None) is not None

ip.increment
assert ip._properties.get("choice", None) == "value"
assert ip._properties.get("value", None) is not None

ip.decrement
assert ip._properties.get("choice", None) == "value"
assert ip._properties.get("value", None) is not None

# setting the values should change the choice
ip.increment.count = 5
assert ip._properties.get("choice", None) == "increment"
assert ip._properties.get("increment", None) is not None

ip.values = ["1.1.1.1"]
assert ip._properties.get("choice", None) == "values"
assert ip._properties.get("values", None) is not None

ip.decrement.count = 5
assert ip._properties.get("choice", None) == "decrement"
assert ip._properties.get("decrement", None) is not None