openapi-generators / openapi-generators/openapi-python-client

Multi-body endpoints with required requestBody emit `| Unset` without importing `Unset`

未關閉
#1,451 1 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

還沒有人認領這個 Issue。

主要語言
Python
星號
2k
分支
293
平均合併
34 分鐘
30 天內合併 PR
1

描述

Describe the bug

When an endpoint has a required requestBody with multiple content types, the generator appends | Unset = UNSET to the type annotation - even though the body is required.

Furthermore, Unset is not added to the imports, so the generated file references an undefined name - this causes ruff to report multiple instances of F821 Undefined name 'Unset'.

To Reproduce

Create openapi.yaml with the following spec:

openapi: 3.0.3
info:
  title: Unset Bug Demo
  version: 1.0.0
paths:
  /items/{id}:
    put:
      operationId: updateItem
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true # <-- must be true
        content:
          application/json: # <-- must have multiple content types
            schema:
              $ref: '#/components/schemas/Item'
          multipart/form-data: # <-- must have multiple content types
            schema:
              $ref: '#/components/schemas/Item'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Item'
components:
  schemas:
    Item:
      type: object
      properties:
        name:
          type: string

Generate the client and lint:

# Tested on CachyOS with uv 0.11.21 and the following:
uvx --python 3.14 openapi-python-client==0.29.0 generate --path openapi.yaml --output-path out
uvx ruff==0.15.17 check out
Generated code snippet

In out/unset_bug_demo_client/api/default/update_item.py:

from ...types import UNSET, Response  # <-- Unset is not imported

def _get_kwargs(
    id: str,
    *,
    body: Item | Item | Unset = UNSET,  # <-- F821: Undefined name 'Unset' (also note how Item is duplicated)
) -> dict[str, Any]:
    ...

Potential root cause (unverified)

The following was generated by an LLM and has not been verified!

I'm not really familiar with the codebase, but I might take a deeper look in the coming weeks if I find time and turn this into a proper fix/PR...

1. The template always appends | Unset — even for required bodies

In templates/endpoint_macros.py.jinja, the multi-body branch of the arguments macro uses an uninitialized variable > body_required:

{% elif endpoint.bodies | length > 1 %}
body:
    {%- for body in endpoint.bodies -%}{% set body_required = body_required and body.prop.required %}
    {{ body.prop.get_type_string(no_optional=True) }} {% if not loop.last %} | {% endif %}
    {%- endfor -%}{% if not body_required %} | Unset = UNSET{% endif %}
,
{% endif %}

body_required is referenced before it is ever initialized. On the first loop iteration body_required is Jinja Undefined, and Undefined and body.prop.required evaluates to Undefined (falsy). So it stays falsy for the whole loop, and after the loop {% if not body_required %} is always True. Result: | Unset = UNSET is appended for every multi-body endpoint, regardless of whether the bodies are required. (It should have been seeded to True > before the loop so the and chain actually reflects "all bodies required".)

2. The Unset import is never added for required bodies

Each member is rendered with get_type_string(no_optional=True), and the import set is driven by get_imports():

# parser/properties/protocol.py
def get_imports(self, *, prefix: str) -> set[str]:
    ...
    imports = set()
    if not self.required:
        imports.add(f"from {prefix}types import UNSET, Unset")
    return imports

Unset/UNSET are only imported when the property is not required. Endpoint imports are collected from exactly this > method:

# parser/openapi.py
result.bodies.append(body)
result.relative_imports.update(body.prop.get_imports(prefix=models_relative_prefix))

And the module's static imports include only UNSET, never Unset:

# templates/endpoint_module.py.jinja
from ...types import Response, UNSET

貢獻指南

開啟貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

研究方向

根據提供的 openapi.yaml 重現產生的用戶端,然後檢查 templates/endpoint_macros.py.jinja、parser/properties/protocol.py、parser/openapi.py 和 templates/endpoint_module.py.jinja。追蹤必要的多個 body 引數和匯入是如何收集的。完成的標準是,產生的必要 request body 沒有不必要的 Unset 預設值、沒有重複的型別,且 ruff 不會回報未定義的 Unset 名稱。

由索引模型根據 Issue 內容生成。

評估

技術堆疊
python
領域
api, tooling
Issue 類型
缺陷
難度
3/5
預估耗時
1-2 天
活躍度
冷清
描述清晰度
描述清楚
新手友好度
72/100

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。