Repository: openapi-generators/openapi-python-client (language: python)

Issue:
generated code fails linting if an object has no properties
**Describe the bug**

If a schema has `type: object`, `properties: {}`, and `additionalProperties: false`, schema parsing succeeds and the generator generates appropriate code... but it fails in the post-generation linting step, with an error like this:

```
empty_object_example_client/models/get_example_response_200.py:25:9: F841 Local variable `d` is assigned to but never used
   |
23 |     @classmethod
24 |     def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
25 |         d = src_dict.copy()
   |         ^ F841
26 |         get_example_response_200 = cls(
27 |         )
```

The problem is that the template assumes it'll be doing _something_ with `d`— either reading specific named properties out of it, or saving any unrecognized ones as "additional". But in this case neither of those things can happen, so `d` is unused.

**OpenAPI Spec File**
A link to an OpenAPI document which produces this issue. Ideally, write a minimal reproduction only containing the problematic pieces.

https://gist.github.com/eli-bl/0e433b0dcde331bddff2842f2b6633d2

**Desktop (please complete the following information):**
 - OS: MacOS 14.5
 - Python Version: 3.8.15
 - openapi-python-client version: 0.21.4

**Additional context**
This may seem like a contrived example, but I have seen API scenarios where an empty object is actually the desired value.


Discussion / hints:

Fix patch:
diff --git a/.changeset/no_properties_fix.md b/.changeset/no_properties_fix.md
new file mode 100644
index 000000000..9b38ad1ba
--- /dev/null
+++ b/.changeset/no_properties_fix.md
@@ -0,0 +1,7 @@
+---
+default: patch
+---
+
+# Produce valid code for an object that has no properties at all
+
+Fixed by PR #1109. Thanks @eli-bl!
diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja
index 44f5bf148..2d22efe05 100644
--- a/openapi_python_client/templates/model.py.jinja
+++ b/openapi_python_client/templates/model.py.jinja
@@ -138,6 +138,7 @@ return field_dict
     {% for lazy_import in model.lazy_imports %}
         {{ lazy_import }}
     {% endfor %}
+{% if (model.required_properties or model.optional_properties or model.additional_properties) %}
         d = src_dict.copy()
 {% for property in model.required_properties + model.optional_properties %}
     {% if property.required %}
@@ -153,6 +154,7 @@ return field_dict
     {% endif %}
 
 {% endfor %}
+{% endif %}
         {{ module_name }} = cls(
 {% for property in model.required_properties + model.optional_properties %}
             {{ property.python_name }}={{ property.python_name }},


Test patch:
diff --git a/end_to_end_tests/baseline_openapi_3.0.json b/end_to_end_tests/baseline_openapi_3.0.json
index 6f9d711f9..68ff2a9d0 100644
--- a/end_to_end_tests/baseline_openapi_3.0.json
+++ b/end_to_end_tests/baseline_openapi_3.0.json
@@ -2219,6 +2219,11 @@
           }
         ]
       },
+      "ModelWithNoProperties": {
+        "type": "object",
+        "properties": {},
+        "additionalProperties": false
+      },
       "AllOfSubModel": {
         "title": "AllOfSubModel",
         "type": "object",
diff --git a/end_to_end_tests/baseline_openapi_3.1.yaml b/end_to_end_tests/baseline_openapi_3.1.yaml
index 6bea1ec32..b39be67c0 100644
--- a/end_to_end_tests/baseline_openapi_3.1.yaml
+++ b/end_to_end_tests/baseline_openapi_3.1.yaml
@@ -2213,6 +2213,11 @@ info:
         }
       ]
     },
+    "ModelWithNoProperties": {
+      "type": "object",
+      "properties": {},
+      "additionalProperties": false
+    },
     "AllOfSubModel": {
       "title": "AllOfSubModel",
       "type": "object",
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py
index cd0ea68da..d28f05268 100644
--- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py
@@ -60,6 +60,7 @@
 from .model_with_circular_ref_in_additional_properties_b import ModelWithCircularRefInAdditionalPropertiesB
 from .model_with_date_time_property import ModelWithDateTimeProperty
 from .model_with_discriminated_union import ModelWithDiscriminatedUnion
+from .model_with_no_properties import ModelWithNoProperties
 from .model_with_primitive_additional_properties import ModelWithPrimitiveAdditionalProperties
 from .model_with_primitive_additional_properties_a_date_holder import ModelWithPrimitiveAdditionalPropertiesADateHolder
 from .model_with_property_ref import ModelWithPropertyRef
@@ -137,6 +138,7 @@
     "ModelWithCircularRefInAdditionalPropertiesB",
     "ModelWithDateTimeProperty",
     "ModelWithDiscriminatedUnion",
+    "ModelWithNoProperties",
     "ModelWithPrimitiveAdditionalProperties",
     "ModelWithPrimitiveAdditionalPropertiesADateHolder",
     "ModelWithPropertyRef",
diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py
new file mode 100644
index 000000000..506239f32
--- /dev/null
+++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_no_properties.py
@@ -0,0 +1,21 @@
+from typing import Any, Dict, Type, TypeVar
+
+from attrs import define as _attrs_define
+
+T = TypeVar("T", bound="ModelWithNoProperties")
+
+
+@_attrs_define
+class ModelWithNoProperties:
+    """ """
+
+    def to_dict(self) -> Dict[str, Any]:
+        field_dict: Dict[str, Any] = {}
+
+        return field_dict
+
+    @classmethod
+    def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
+        model_with_no_properties = cls()
+
+        return model_with_no_properties