Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions fastapi_jsonrpc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@
from fastapi.dependencies.utils import solve_dependencies, get_dependant, get_flat_dependant, \
get_parameterless_sub_dependant
from fastapi.exceptions import RequestValidationError, HTTPException
from fastapi.routing import APIRoute, APIRouter, serialize_response
from fastapi.routing import APIRoute, APIRouter, request_response, serialize_response
from pydantic import BaseModel, ValidationError, StrictStr, Field, create_model, ConfigDict
from starlette.background import BackgroundTasks
from starlette.concurrency import run_in_threadpool
from starlette.requests import Request
from starlette.responses import Response, JSONResponse
from starlette.routing import Match, request_response, compile_path
from starlette.routing import Match, compile_path
import fastapi.params
import aiojobs
import warnings
Expand Down Expand Up @@ -1419,7 +1419,10 @@ def update_refs(value):

fine_schema = {}
for key, schema in data['components']['schemas'].items():
fine_schema_name = key[:-len(schema['title'].replace('.', '__'))].replace('__', '.') + schema['title']
if 'title' in schema:
fine_schema_name = key[:-len(schema['title'].replace('.', '__'))].replace('__', '.') + schema['title']
else:
fine_schema_name = key.replace('__', '.')
old2new_schema_name[key] = fine_schema_name
fine_schema[fine_schema_name] = schema
data['components']['schemas'] = fine_schema
Expand Down Expand Up @@ -1600,4 +1603,4 @@ def echo(

app.bind_entrypoint(api_v1)

uvicorn.run(app, port=5000, debug=True, access_log=False) # noqa
uvicorn.run(app, port=5000, debug=True, access_log=False) # noqa
6 changes: 6 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import platform
import sys
from json import dumps as json_dumps
from unittest.mock import ANY

Expand Down Expand Up @@ -162,3 +163,8 @@ def _openapi_compatible(obj: dict):

return obj
return _openapi_compatible


collect_ignore = []
if sys.version_info < (3, 12):
collect_ignore.append("test_openrpc_type_keyword.py")
39 changes: 22 additions & 17 deletions tests/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ def probe(
unique_param_name=web_param_name,
),
)
return api_dir
return request.param
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not change the fixture return type.
If you need access to some of the fixture params, you can follow one of the two ways:

  • return a "smart" object and access its attributes
  • create a fixture and access/control params through it

I would prefer the second method for this case:

@pytest.fixture(params=['no-collide', 'collide'])
def api_package_signature(request):
    return request.param

@pytest.fixture()
def api_package(..., api_package_signature):
     ...



def test_component_name_isolated_by_their_path(pytester, api_package):
Expand Down Expand Up @@ -677,25 +677,30 @@ def test_no_collide(app_client):

paths = resp_json['paths']
schemas = resp_json['components']['schemas']

mobile_path = '/api/v1/mobile/jsonrpc/probe'
web_path = '/api/v1/web/jsonrpc/probe'

for path in (
'/api/v1/mobile/jsonrpc/probe',
'/api/v1/web/jsonrpc/probe',
):
for path in (mobile_path, web_path):
assert path in paths

# Response model the same and deduplicated
assert '_Response[probe]' in schemas

if '_Params[probe]' not in schemas:
for component_name in (
'api.mobile._Params[probe]',
'api.mobile._Request[probe]',
'api.web._Params[probe]',
'api.web._Request[probe]',
):
assert component_name in schemas
''')
web_response_ref = paths[web_path]['post']['responses']['200']['content']['application/json']['schema']['$ref']
mobile_response_ref = paths[mobile_path]['post']['responses']['200']['content']['application/json']['schema']['$ref']

assert web_response_ref == mobile_response_ref

web_request_ref = paths[web_path]['post']['requestBody']['content']['application/json']['schema']['$ref']
mobile_request_ref = paths[mobile_path]['post']['requestBody']['content']['application/json']['schema']['$ref']

if '{package_type}' == 'same-sig':
assert web_request_ref == mobile_request_ref
assert web_request_ref.split('/')[-1] in schemas
else:
assert web_request_ref != mobile_request_ref
assert web_request_ref.split('/')[-1] in schemas
assert mobile_request_ref.split('/')[-1] in schemas
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we bring back hard-coded values to make the test clearer about which values are expected?

'''.format(package_type=api_package))

# force reload module to drop component cache
# it's more efficient than use pytest.runpytest_subprocess()
Expand Down Expand Up @@ -732,4 +737,4 @@ def test_no_entrypoints__ok(fastapi_jsonrpc_components_fine_names):
app_client = TestClient(app)
resp = app_client.get('/openapi.json')
resp.raise_for_status()
assert resp.status_code == 200
assert resp.status_code == 200
1 change: 1 addition & 0 deletions tests/test_openrpc.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sys
from typing import Dict, List, Optional

import pytest
Expand Down
82 changes: 82 additions & 0 deletions tests/test_openrpc_type_keyword.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from typing import List

from pydantic import BaseModel


def test_type_keyword_field(ep, app, app_client):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_type_keyword_field(ep, app, app_client):
def test_py312_type_keyword_field(ep, app, app_client):

class Model1(BaseModel):
x: int

class Model2(BaseModel):
y: int

type Model = Model1 | Model2

class Input(BaseModel):
data: Model

class Output(BaseModel):
result: List[int]

@ep.method()
def my_method_type_keyword(inp: Input) -> Output:
if isinstance(inp.data, Model1):
return Output(result=[inp.data.x])
return Output(result=[inp.data.y])

app.bind_entrypoint(ep)

resp = app_client.get('/openrpc.json')
schema = resp.json()

assert len(schema['methods']) == 1
assert schema['methods'][0]['params'] == [
{
'name': 'inp',
'schema': {'$ref': '#/components/schemas/Input'},
'required': True,
}
]
assert schema['methods'][0]['result'] == {
'name': 'my_method_type_keyword_Result',
'schema': {'$ref': '#/components/schemas/Output'},
}

assert schema['components']['schemas'] == {
'Input': {
'properties': {'data': {'$ref': '#/components/schemas/Model'}},
'required': ['data'],
'title': 'Input',
'type': 'object',
},
'Model': {
'anyOf': [
{'$ref': '#/components/schemas/Model1'},
{'$ref': '#/components/schemas/Model2'},
]
},
'Model1': {
'properties': {'x': {'title': 'X', 'type': 'integer'}},
'required': ['x'],
'title': 'Model1',
'type': 'object',
},
'Model2': {
'properties': {'y': {'title': 'Y', 'type': 'integer'}},
'required': ['y'],
'title': 'Model2',
'type': 'object',
},
'Output': {
'properties': {
'result': {
'items': {'type': 'integer'},
'title': 'Result',
'type': 'array',
}
},
'required': ['result'],
'title': 'Output',
'type': 'object',
},
}