Skip to content

Commit 880704b

Browse files
author
Doug Borg
committed
feat(templates): skip JSON parsing for 204 across httpx/requests/aiohttp; add cross-library 204 tests
1 parent 1f4f6c7 commit 880704b

File tree

5 files changed

+69
-7
lines changed

5 files changed

+69
-7
lines changed

src/openapi_python_generator/language_converters/python/templates/aiohttp.jinja2

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ async def {{ operation_id }}(api_config_override : Optional[APIConfig] = None{%
3030
json = {{ body_param }}
3131
{% endif %}
3232
{% endif %}
33-
) as inital_response:
34-
if inital_response.status != {{ return_type.status_code }}:
35-
raise HTTPException(inital_response.status, f'{{ operationId }} failed with status code: {inital_response.status}')
36-
response = await inital_response.json()
33+
) as initial_response:
34+
if initial_response.status != {{ return_type.status_code }}:
35+
raise HTTPException(initial_response.status, f'{{ operation_id }} failed with status code: {initial_response.status}')
36+
# Only parse JSON when a body is expected (avoid errors on 204 No Content)
37+
response = None if {{ return_type.status_code }} == 204 else await initial_response.json()
3738

3839
{% if return_type.type is none or return_type.type.converted_type is none %}
3940
return None

src/openapi_python_generator/language_converters/python/templates/httpx.jinja2

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ with httpx.Client(base_url=base_path, verify=api_config.verify) as client:
3838
)
3939

4040
if response.status_code != {{ return_type.status_code }}:
41-
raise HTTPException(response.status_code, f'{{ operationId }} failed with status code: {response.status_code}')
41+
raise HTTPException(response.status_code, f'{{ operation_id }} failed with status code: {response.status_code}')
42+
43+
{% if return_type.status_code == 204 %}
44+
# 204 No Content: return early without attempting to parse body
45+
return None
46+
{% endif %}
4247

4348
{% if return_type.type is none or return_type.type.converted_type is none %}
4449
return None

src/openapi_python_generator/language_converters/python/templates/requests.jinja2

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ def {{ operation_id }}(api_config_override : Optional[APIConfig] = None{% if par
3232
{% endif %}
3333
)
3434
if response.status_code != {{ return_type.status_code }}:
35-
raise HTTPException(response.status_code, f'{{ operationId }} failed with status code: {response.status_code}')
35+
raise HTTPException(response.status_code, f'{{ operation_id }} failed with status code: {response.status_code}')
36+
37+
{% if return_type.status_code == 204 %}
38+
# 204 No Content: return early without attempting to parse body
39+
return None
40+
{% endif %}
3641

3742
{% if return_type.type is none or return_type.type.converted_type is none %}
3843
return None

src/openapi_python_generator/language_converters/python/templates/service.jinja2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import *
22
import {{ library_import }}
3-
import json
3+
44
{% if use_orjson %}
55
import orjson
66
from uuid import UUID

tests/test_service_generator.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,54 @@ def test_generate_services(model_data):
386386
)
387387
for i in result:
388388
compile(i.content, "<string>", "exec")
389+
390+
391+
def test_default_tag_and_path_param_injection():
392+
"""Untagged operation should generate default_service and include path placeholder as param."""
393+
from openapi_pydantic.v3 import PathItem
394+
395+
# Minimal GET with no tags and no explicit parameters but a placeholder in path
396+
# Cast responses dict to expected mapping type (Response | Reference)
397+
op = Operation(responses={k: v for k, v in default_responses.items()})
398+
paths = {"/items/{itemId}": PathItem(get=op)}
399+
services = generate_services(paths, library_config_dict[HTTPLibrary.httpx])
400+
# Find generated sync default service
401+
default_service = [s for s in services if s.file_name == "default_service"]
402+
assert default_service, "default_service should be generated for untagged operation"
403+
content = default_service[0].content
404+
# Operation id will be derived; ensure parameter itemId injected
405+
assert "itemId" in content or "item_id" in content
406+
407+
408+
def test_aiohttp_204_no_json_parsing():
409+
"""204 response should not attempt to parse JSON in aiohttp template."""
410+
from openapi_pydantic.v3 import PathItem
411+
412+
op = Operation(responses={"204": Response(description="No Content")})
413+
paths = {"/resources/{rid}": PathItem(delete=op)}
414+
services = generate_services(paths, library_config_dict[HTTPLibrary.aiohttp])
415+
aio_services = [s for s in services if s.async_client]
416+
assert aio_services
417+
content = aio_services[0].content
418+
# We expect conditional assignment that avoids json parsing when 204
419+
assert "== 204 else" in content
420+
# Should still return None
421+
assert "return None" in content
422+
423+
424+
@pytest.mark.parametrize("library", [HTTPLibrary.httpx, HTTPLibrary.requests, HTTPLibrary.aiohttp])
425+
def test_204_skip_parsing_all_libraries(library):
426+
"""All libraries should skip JSON parsing for a 204 response and just return None."""
427+
from openapi_pydantic.v3 import PathItem
428+
429+
op = Operation(responses={"204": Response(description="No Content")})
430+
paths = {"/things/{tid}": PathItem(delete=op)}
431+
services = generate_services(paths, library_config_dict[library])
432+
# Pick a service that actually has generated operation content
433+
service = next((s for s in services if s.content.strip()), services[0])
434+
content = service.content
435+
# Ensure no .json() invocation occurs when status_code == 204 within this function body
436+
# Simpler heuristic: our injected early return comment for sync libs or conditional assignment for aiohttp
437+
assert "204 No Content" in content or "== 204 else" in content
438+
# Should contain 'return None'
439+
assert "return None" in content

0 commit comments

Comments
 (0)