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

fix(client): properly configure model set fields #98

Merged
merged 1 commit into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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