Skip to content

Commit

Permalink
fix(client): properly configure model set fields (#98)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stainless-bot authored and rattrayalex committed Sep 21, 2023
1 parent 8b67f6c commit d855b84
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 4 deletions.
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,21 @@ client = Finch(
)
```

## Advanced: Configuring custom URLs, proxies, and transports
## Advanced

### How to tell whether `None` means `null` or missing

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`:

```py
if response.my_field is None:
if 'my_field' not in response.model_fields_set:
print('Got json like {}, without a "my_field" key present at all.')
else:
print('Got json like {"my_field": null}.')
```

### Configuring custom URLs, proxies, and transports

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

Expand All @@ -289,7 +303,7 @@ client = Finch(

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.

## Advanced: Managing HTTP resources
### Managing HTTP resources

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.

Expand Down
11 changes: 9 additions & 2 deletions src/finch/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ class BaseModel(pydantic.BaseModel):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="allow")
else:

@property
def model_fields_set(self) -> set[str]:
# a forwards-compat shim for pydantic v2
return self.__fields_set__ # type: ignore

class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated]
extra: Any = Extra.allow # type: ignore

Expand All @@ -74,6 +79,9 @@ def construct(
else config.get("populate_by_name")
)

if _fields_set is None:
_fields_set = set()

model_fields = get_model_fields(cls)
for name, field in model_fields.items():
key = field.alias
Expand All @@ -82,6 +90,7 @@ def construct(

if key in values:
fields_values[name] = _construct_field(value=values[key], field=field, key=key)
_fields_set.add(name)
else:
fields_values[name] = field_get_default(field)

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

object.__setattr__(m, "__dict__", fields_values)
if _fields_set is None:
_fields_set = set(fields_values.keys())

if PYDANTIC_V2:
# these properties are copied from Pydantic's `model_construct()` method
Expand Down
14 changes: 14 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,17 @@ def model_id(self) -> str:
assert m.model_id == "id"
assert m.resource_id == "id"
assert m.resource_id is m.model_id


def test_omitted_fields() -> None:
class Model(BaseModel):
resource_id: Optional[str] = None

m = Model.construct()
assert "resource_id" not in m.model_fields_set

m = Model.construct(resource_id=None)
assert "resource_id" in m.model_fields_set

m = Model.construct(resource_id="foo")
assert "resource_id" in m.model_fields_set

0 comments on commit d855b84

Please sign in to comment.