diff --git a/.github/workflows/hygiene.yaml b/.github/workflows/hygiene.yaml index cfb96da..9fcce52 100644 --- a/.github/workflows/hygiene.yaml +++ b/.github/workflows/hygiene.yaml @@ -11,7 +11,7 @@ on: jobs: hygiene: runs-on: ubuntu-latest - name: Ruff + name: ruff and basedpyright steps: - uses: actions/checkout@v3 - name: setup-python diff --git a/src/implicitdict/__init__.py b/src/implicitdict/__init__.py index 75fb39d..4ae1d7e 100644 --- a/src/implicitdict/__init__.py +++ b/src/implicitdict/__init__.py @@ -160,10 +160,13 @@ def __getattribute__(self, item): self_type_name = _fullname(self_type) if self_type_name in fields_info_by_type: if item in fields_info_by_type[self_type_name].all_fields: - try: - return self[item] - except KeyError: - raise AttributeError + if item in fields_info_by_type[self_type_name].optional_fields: + return self[item] if item in self else None + else: + try: + return self[item] + except KeyError: + raise AttributeError return super().__getattribute__(item) def __setattr__(self, key, value): diff --git a/tests/test_docpattern.py b/tests/test_docpattern.py index 2029c21..e9f1954 100644 --- a/tests/test_docpattern.py +++ b/tests/test_docpattern.py @@ -8,10 +8,16 @@ class ResponseType(ImplicitDict): pass +class QueryResponse(ImplicitDict): + json: dict + + class Query(ImplicitDict): participant_id: str | None """If specified, identifier of the USS/participant hosting the server involved in this query.""" + response: QueryResponse + def parse_json_result(self, parse_type: type[ResponseType]) -> ResponseType: """Parses the JSON result into the specified type. diff --git a/tests/test_normal_usage.py b/tests/test_normal_usage.py index e08cc6e..7634c26 100644 --- a/tests/test_normal_usage.py +++ b/tests/test_normal_usage.py @@ -24,8 +24,8 @@ def test_basic_usage(): # Optional fields that aren't specified simply don't exist assert "baz" not in data - with pytest.raises(AttributeError): - assert data.baz == 0 + # ...but they default to None when the attribute is accessed + assert data.baz is None # Optional fields can be omitted (fields with defaults are optional) data = NormalUsageData(foo="asdf") diff --git a/tests/test_optional.py b/tests/test_optional.py index 3205ec2..9148685 100644 --- a/tests/test_optional.py +++ b/tests/test_optional.py @@ -1,7 +1,5 @@ import json -import pytest - from implicitdict import ImplicitDict from .test_types import OptionalData @@ -43,11 +41,12 @@ def test_minimally_defined(): assert "field_with_default" in data assert "optional_field2_with_none_default" in data assert "optional_field3_with_default" in data - with pytest.raises(AttributeError): - # Trying to reference the Optional field will result in a AttributeError - # To determine whether an Optional field is present, the user must check - # whether `"" in ` (see above). - assert data.optional_field1 is None + + # Referencing the Optional field will return None. + # To differentiate between an actual value of None and the field being absent, + # the user must check whether `"" in ` (see above). + assert data.optional_field1 is None + s = json.dumps(data) assert "required_field" in s assert "optional_field1" not in s @@ -62,7 +61,10 @@ def test_getattr(): data = OptionalData.example_values()["minimally_defined"] assert "optional_field1" not in data - assert getattr(data, "optional_field1", "getattrdefault") == "getattrdefault" + + # To differentiate between an actual value of None and the field being absent, + # the user must check whether `"" in ` (see above). + assert getattr(data, "optional_field1", "getattrdefault") is None def test_provide_optional_field():