diff --git a/linode_api4/objects/linode_interfaces.py b/linode_api4/objects/linode_interfaces.py index f12865c99..391cb8650 100644 --- a/linode_api4/objects/linode_interfaces.py +++ b/linode_api4/objects/linode_interfaces.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field -from typing import List, Optional +from typing import List, Optional, Union -from linode_api4.objects.base import Base, Property +from linode_api4.objects.base import Base, ExplicitNullValue, Property from linode_api4.objects.dbase import DerivedBase from linode_api4.objects.networking import Firewall from linode_api4.objects.serializable import JSONObject @@ -193,13 +193,13 @@ class LinodeInterfaceOptions(JSONObject): NOTE: Linode interfaces may not currently be available to all users. """ - always_include = { - # If a default firewall_id isn't configured, the API requires that - # firewall_id is defined in the LinodeInterface POST body. - "firewall_id" - } + # If a default firewall_id isn't configured, the API requires that + # firewall_id is defined in the LinodeInterface POST body. + # + # To create a Linode Interface without a firewall, this field should + # be set to `ExplicitNullValue()`. + firewall_id: Union[int, ExplicitNullValue, None] = None - firewall_id: Optional[int] = None default_route: Optional[LinodeInterfaceDefaultRouteOptions] = None vpc: Optional[LinodeInterfaceVPCOptions] = None public: Optional[LinodeInterfacePublicOptions] = None diff --git a/linode_api4/objects/serializable.py b/linode_api4/objects/serializable.py index 1660795aa..c1f59f6d4 100644 --- a/linode_api4/objects/serializable.py +++ b/linode_api4/objects/serializable.py @@ -186,6 +186,16 @@ def attempt_serialize(value: Any) -> Any: if issubclass(type(value), JSONObject): return value._serialize(is_put=is_put) + # Needed to avoid circular imports without a breaking change + from linode_api4.objects.base import ( # pylint: disable=import-outside-toplevel + ExplicitNullValue, + ) + + if value == ExplicitNullValue or isinstance( + value, ExplicitNullValue + ): + return None + return value def should_include(key: str, value: Any) -> bool: diff --git a/test/integration/conftest.py b/test/integration/conftest.py index dfa01abed..617dbb923 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -13,6 +13,7 @@ from requests.exceptions import ConnectionError, RequestException from linode_api4 import ( + ExplicitNullValue, InterfaceGeneration, LinodeInterfaceDefaultRouteOptions, LinodeInterfaceOptions, @@ -585,6 +586,7 @@ def linode_with_linode_interfaces( public=LinodeInterfacePublicOptions(), ), LinodeInterfaceOptions( + firewall_id=ExplicitNullValue, vpc=LinodeInterfaceVPCOptions( subnet_id=subnet.id, ), diff --git a/test/integration/models/linode/interfaces/test_interfaces.py b/test/integration/models/linode/interfaces/test_interfaces.py index 6a81bb8bc..07dffd66a 100644 --- a/test/integration/models/linode/interfaces/test_interfaces.py +++ b/test/integration/models/linode/interfaces/test_interfaces.py @@ -14,6 +14,7 @@ LinodeInterfacePublicIPv6RangeOptions, LinodeInterfacePublicOptions, LinodeInterfaceVLANOptions, + LinodeInterfaceVPCIPv4AddressOptions, LinodeInterfaceVPCIPv4Options, LinodeInterfaceVPCIPv4RangeOptions, LinodeInterfaceVPCOptions, @@ -145,19 +146,18 @@ def linode_interface_vpc( vpc=LinodeInterfaceVPCOptions( subnet_id=subnet.id, ipv4=LinodeInterfaceVPCIPv4Options( - # TODO (Enhanced Interfaces): Not currently working as expected - # addresses=[ - # LinodeInterfaceVPCIPv4AddressOptions( - # address="auto", - # primary=True, - # nat_1_1_address="any", - # ) - # ], + addresses=[ + LinodeInterfaceVPCIPv4AddressOptions( + address="auto", + primary=True, + nat_1_1_address="auto", + ) + ], ranges=[ LinodeInterfaceVPCIPv4RangeOptions( - range="/29", + range="/32", ) - ] + ], ), ), ), instance, vpc, subnet @@ -263,9 +263,9 @@ def test_linode_interface_create_vpc(linode_interface_vpc): assert len(iface.vpc.ipv4.addresses[0].address) > 0 assert iface.vpc.ipv4.addresses[0].primary - assert iface.vpc.ipv4.addresses[0].nat_1_1_address is None + assert iface.vpc.ipv4.addresses[0].nat_1_1_address is not None - assert iface.vpc.ipv4.ranges[0].range.split("/")[1] == "29" + assert iface.vpc.ipv4.ranges[0].range.split("/")[1] == "32" def test_linode_interface_update_vpc(linode_interface_vpc): diff --git a/test/unit/objects/serializable_test.py b/test/unit/objects/serializable_test.py index 9a775ccf1..f7dff4297 100644 --- a/test/unit/objects/serializable_test.py +++ b/test/unit/objects/serializable_test.py @@ -1,8 +1,8 @@ from dataclasses import dataclass from test.unit.base import ClientBaseCase -from typing import Optional +from typing import Optional, Union -from linode_api4 import Base, JSONObject, Property +from linode_api4 import Base, ExplicitNullValue, JSONObject, Property class JSONObjectTest(ClientBaseCase): @@ -14,18 +14,21 @@ class Foo(JSONObject): foo: Optional[str] = None bar: Optional[str] = None baz: str = None + foobar: Union[str, ExplicitNullValue, None] = None foo = Foo().dict assert foo["foo"] is None assert "bar" not in foo assert foo["baz"] is None + assert "foobar" not in foo - foo = Foo(foo="test", bar="test2", baz="test3").dict + foo = Foo(foo="test", bar="test2", baz="test3", foobar="test4").dict assert foo["foo"] == "test" assert foo["bar"] == "test2" assert foo["baz"] == "test3" + assert foo["foobar"] == "test4" def test_serialize_optional_include_None(self): @dataclass @@ -35,18 +38,23 @@ class Foo(JSONObject): foo: Optional[str] = None bar: Optional[str] = None baz: str = None + foobar: Union[str, ExplicitNullValue, None] = None foo = Foo().dict assert foo["foo"] is None assert foo["bar"] is None assert foo["baz"] is None + assert foo["foobar"] is None - foo = Foo(foo="test", bar="test2", baz="test3").dict + foo = Foo( + foo="test", bar="test2", baz="test3", foobar=ExplicitNullValue() + ).dict assert foo["foo"] == "test" assert foo["bar"] == "test2" assert foo["baz"] == "test3" + assert foo["foobar"] is None def test_serialize_put_class(self): """