From 1e09c69f3262d4a71d5f7db1f8e6688ce4b5ad25 Mon Sep 17 00:00:00 2001 From: Stainless Bot Date: Thu, 14 Sep 2023 16:17:35 +0000 Subject: [PATCH] fix(client): properly configure model set fields 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. --- README.md | 18 ++++++++++++++++-- src/finch/_models.py | 11 +++++++++-- tests/test_models.py | 14 ++++++++++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0ae153d8..30cd7429 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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. diff --git a/src/finch/_models.py b/src/finch/_models.py index 296d36c0..7bbdca3b 100644 --- a/src/finch/_models.py +++ b/src/finch/_models.py @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/tests/test_models.py b/tests/test_models.py index a839f68d..e6b67abe 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -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