Skip to content

Commit 221afdc

Browse files
Linode Interfaces: Allow specifying ExplicitNullValue for LinodeInterfaceOptions firewall ID (#565)
* Add ExplicitNullValue support * Fix failing integration tests * Add unit tests * Add docs disclaimer
1 parent 06f8a5f commit 221afdc

File tree

5 files changed

+44
-24
lines changed

5 files changed

+44
-24
lines changed

linode_api4/objects/linode_interfaces.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from dataclasses import dataclass, field
2-
from typing import List, Optional
2+
from typing import List, Optional, Union
33

4-
from linode_api4.objects.base import Base, Property
4+
from linode_api4.objects.base import Base, ExplicitNullValue, Property
55
from linode_api4.objects.dbase import DerivedBase
66
from linode_api4.objects.networking import Firewall
77
from linode_api4.objects.serializable import JSONObject
@@ -193,13 +193,13 @@ class LinodeInterfaceOptions(JSONObject):
193193
NOTE: Linode interfaces may not currently be available to all users.
194194
"""
195195

196-
always_include = {
197-
# If a default firewall_id isn't configured, the API requires that
198-
# firewall_id is defined in the LinodeInterface POST body.
199-
"firewall_id"
200-
}
196+
# If a default firewall_id isn't configured, the API requires that
197+
# firewall_id is defined in the LinodeInterface POST body.
198+
#
199+
# To create a Linode Interface without a firewall, this field should
200+
# be set to `ExplicitNullValue()`.
201+
firewall_id: Union[int, ExplicitNullValue, None] = None
201202

202-
firewall_id: Optional[int] = None
203203
default_route: Optional[LinodeInterfaceDefaultRouteOptions] = None
204204
vpc: Optional[LinodeInterfaceVPCOptions] = None
205205
public: Optional[LinodeInterfacePublicOptions] = None

linode_api4/objects/serializable.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@ def attempt_serialize(value: Any) -> Any:
186186
if issubclass(type(value), JSONObject):
187187
return value._serialize(is_put=is_put)
188188

189+
# Needed to avoid circular imports without a breaking change
190+
from linode_api4.objects.base import ( # pylint: disable=import-outside-toplevel
191+
ExplicitNullValue,
192+
)
193+
194+
if value == ExplicitNullValue or isinstance(
195+
value, ExplicitNullValue
196+
):
197+
return None
198+
189199
return value
190200

191201
def should_include(key: str, value: Any) -> bool:

test/integration/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from requests.exceptions import ConnectionError, RequestException
1414

1515
from linode_api4 import (
16+
ExplicitNullValue,
1617
InterfaceGeneration,
1718
LinodeInterfaceDefaultRouteOptions,
1819
LinodeInterfaceOptions,
@@ -585,6 +586,7 @@ def linode_with_linode_interfaces(
585586
public=LinodeInterfacePublicOptions(),
586587
),
587588
LinodeInterfaceOptions(
589+
firewall_id=ExplicitNullValue,
588590
vpc=LinodeInterfaceVPCOptions(
589591
subnet_id=subnet.id,
590592
),

test/integration/models/linode/interfaces/test_interfaces.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
LinodeInterfacePublicIPv6RangeOptions,
1515
LinodeInterfacePublicOptions,
1616
LinodeInterfaceVLANOptions,
17+
LinodeInterfaceVPCIPv4AddressOptions,
1718
LinodeInterfaceVPCIPv4Options,
1819
LinodeInterfaceVPCIPv4RangeOptions,
1920
LinodeInterfaceVPCOptions,
@@ -145,19 +146,18 @@ def linode_interface_vpc(
145146
vpc=LinodeInterfaceVPCOptions(
146147
subnet_id=subnet.id,
147148
ipv4=LinodeInterfaceVPCIPv4Options(
148-
# TODO (Enhanced Interfaces): Not currently working as expected
149-
# addresses=[
150-
# LinodeInterfaceVPCIPv4AddressOptions(
151-
# address="auto",
152-
# primary=True,
153-
# nat_1_1_address="any",
154-
# )
155-
# ],
149+
addresses=[
150+
LinodeInterfaceVPCIPv4AddressOptions(
151+
address="auto",
152+
primary=True,
153+
nat_1_1_address="auto",
154+
)
155+
],
156156
ranges=[
157157
LinodeInterfaceVPCIPv4RangeOptions(
158-
range="/29",
158+
range="/32",
159159
)
160-
]
160+
],
161161
),
162162
),
163163
), instance, vpc, subnet
@@ -263,9 +263,9 @@ def test_linode_interface_create_vpc(linode_interface_vpc):
263263

264264
assert len(iface.vpc.ipv4.addresses[0].address) > 0
265265
assert iface.vpc.ipv4.addresses[0].primary
266-
assert iface.vpc.ipv4.addresses[0].nat_1_1_address is None
266+
assert iface.vpc.ipv4.addresses[0].nat_1_1_address is not None
267267

268-
assert iface.vpc.ipv4.ranges[0].range.split("/")[1] == "29"
268+
assert iface.vpc.ipv4.ranges[0].range.split("/")[1] == "32"
269269

270270

271271
def test_linode_interface_update_vpc(linode_interface_vpc):

test/unit/objects/serializable_test.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from dataclasses import dataclass
22
from test.unit.base import ClientBaseCase
3-
from typing import Optional
3+
from typing import Optional, Union
44

5-
from linode_api4 import Base, JSONObject, Property
5+
from linode_api4 import Base, ExplicitNullValue, JSONObject, Property
66

77

88
class JSONObjectTest(ClientBaseCase):
@@ -14,18 +14,21 @@ class Foo(JSONObject):
1414
foo: Optional[str] = None
1515
bar: Optional[str] = None
1616
baz: str = None
17+
foobar: Union[str, ExplicitNullValue, None] = None
1718

1819
foo = Foo().dict
1920

2021
assert foo["foo"] is None
2122
assert "bar" not in foo
2223
assert foo["baz"] is None
24+
assert "foobar" not in foo
2325

24-
foo = Foo(foo="test", bar="test2", baz="test3").dict
26+
foo = Foo(foo="test", bar="test2", baz="test3", foobar="test4").dict
2527

2628
assert foo["foo"] == "test"
2729
assert foo["bar"] == "test2"
2830
assert foo["baz"] == "test3"
31+
assert foo["foobar"] == "test4"
2932

3033
def test_serialize_optional_include_None(self):
3134
@dataclass
@@ -35,18 +38,23 @@ class Foo(JSONObject):
3538
foo: Optional[str] = None
3639
bar: Optional[str] = None
3740
baz: str = None
41+
foobar: Union[str, ExplicitNullValue, None] = None
3842

3943
foo = Foo().dict
4044

4145
assert foo["foo"] is None
4246
assert foo["bar"] is None
4347
assert foo["baz"] is None
48+
assert foo["foobar"] is None
4449

45-
foo = Foo(foo="test", bar="test2", baz="test3").dict
50+
foo = Foo(
51+
foo="test", bar="test2", baz="test3", foobar=ExplicitNullValue()
52+
).dict
4653

4754
assert foo["foo"] == "test"
4855
assert foo["bar"] == "test2"
4956
assert foo["baz"] == "test3"
57+
assert foo["foobar"] is None
5058

5159
def test_serialize_put_class(self):
5260
"""

0 commit comments

Comments
 (0)