Skip to content

Commit ada3ad6

Browse files
fix(client): properly configure model set fields (#98)
This means you can check if a field was included in the response by accessing `model_fields_set` in pydantic v2 and `__fields_set__` in v1.
1 parent d9719a7 commit ada3ad6

File tree

3 files changed

+39
-4
lines changed

3 files changed

+39
-4
lines changed

README.md

+16-2
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,21 @@ client = Finch(
271271
)
272272
```
273273

274-
## Advanced: Configuring custom URLs, proxies, and transports
274+
## Advanced
275+
276+
### How to tell whether `None` means `null` or missing
277+
278+
In an API response, a field may be explicitly null, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`:
279+
280+
```py
281+
if response.my_field is None:
282+
if 'my_field' not in response.model_fields_set:
283+
print('Got json like {}, without a "my_field" key present at all.')
284+
else:
285+
print('Got json like {"my_field": null}.')
286+
```
287+
288+
### Configuring custom URLs, proxies, and transports
275289

276290
You can configure the following keyword arguments when instantiating the client:
277291

@@ -289,7 +303,7 @@ client = Finch(
289303

290304
See the httpx documentation for information about the [`proxies`](https://www.python-httpx.org/advanced/#http-proxying) and [`transport`](https://www.python-httpx.org/advanced/#custom-transports) keyword arguments.
291305

292-
## Advanced: Managing HTTP resources
306+
### Managing HTTP resources
293307

294308
By default we will close the underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__) is called but you can also manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting.
295309

src/finch/_models.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ class BaseModel(pydantic.BaseModel):
4949
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
5050
else:
5151

52+
@property
53+
def model_fields_set(self) -> set[str]:
54+
# a forwards-compat shim for pydantic v2
55+
return self.__fields_set__ # type: ignore
56+
5257
class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
5358
extra: Any = Extra.allow # type: ignore
5459

@@ -74,6 +79,9 @@ def construct(
7479
else config.get("populate_by_name")
7580
)
7681

82+
if _fields_set is None:
83+
_fields_set = set()
84+
7785
model_fields = get_model_fields(cls)
7886
for name, field in model_fields.items():
7987
key = field.alias
@@ -82,6 +90,7 @@ def construct(
8290

8391
if key in values:
8492
fields_values[name] = _construct_field(value=values[key], field=field, key=key)
93+
_fields_set.add(name)
8594
else:
8695
fields_values[name] = field_get_default(field)
8796

@@ -94,8 +103,6 @@ def construct(
94103
fields_values[key] = value
95104

96105
object.__setattr__(m, "__dict__", fields_values)
97-
if _fields_set is None:
98-
_fields_set = set(fields_values.keys())
99106

100107
if PYDANTIC_V2:
101108
# these properties are copied from Pydantic's `model_construct()` method

tests/test_models.py

+14
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,17 @@ def model_id(self) -> str:
471471
assert m.model_id == "id"
472472
assert m.resource_id == "id"
473473
assert m.resource_id is m.model_id
474+
475+
476+
def test_omitted_fields() -> None:
477+
class Model(BaseModel):
478+
resource_id: Optional[str] = None
479+
480+
m = Model.construct()
481+
assert "resource_id" not in m.model_fields_set
482+
483+
m = Model.construct(resource_id=None)
484+
assert "resource_id" in m.model_fields_set
485+
486+
m = Model.construct(resource_id="foo")
487+
assert "resource_id" in m.model_fields_set

0 commit comments

Comments
 (0)