Skip to content
Merged
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
36 changes: 8 additions & 28 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 @@ -453,29 +453,6 @@ def fix_query_dependencies(dependant: Dependant):
fix_query_dependencies(sub_dependant)


def clone_dependant(dependant: Dependant) -> Dependant:
new_dependant = Dependant()
new_dependant.path_params = dependant.path_params
new_dependant.query_params = dependant.query_params
new_dependant.header_params = dependant.header_params
new_dependant.cookie_params = dependant.cookie_params
new_dependant.body_params = dependant.body_params
new_dependant.dependencies = dependant.dependencies
new_dependant.security_requirements = dependant.security_requirements
new_dependant.request_param_name = dependant.request_param_name
new_dependant.websocket_param_name = dependant.websocket_param_name
new_dependant.response_param_name = dependant.response_param_name
new_dependant.background_tasks_param_name = dependant.background_tasks_param_name
new_dependant.security_scopes = dependant.security_scopes
new_dependant.security_scopes_param_name = dependant.security_scopes_param_name
new_dependant.name = dependant.name
new_dependant.call = dependant.call
new_dependant.use_cache = dependant.use_cache
new_dependant.path = dependant.path
new_dependant.cache_key = dependant.cache_key
return new_dependant


def insert_dependencies(target: Dependant, dependencies: Optional[Sequence[Depends]] = None):
assert target.path
if not dependencies:
Expand Down Expand Up @@ -1032,7 +1009,7 @@ def endpoint(__request__: _Request):
f"params={flat_dependant.query_params}"
)

self.shared_dependant = clone_dependant(self.dependant)
self.shared_dependant = copy.copy(self.dependant)

# No shared 'Body' params, because each JSON-RPC request in batch has own body
self.shared_dependant.body_params = []
Expand Down Expand Up @@ -1419,7 +1396,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 +1580,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")
36 changes: 19 additions & 17 deletions tests/test_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,8 +588,13 @@ def probe(
})


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


@pytest.fixture()
def api_package(api_package_signature, pytester):
"""Create package with structure
api \
mobile.py
Expand Down Expand Up @@ -624,11 +629,11 @@ def probe(
return [1, 2, 3]
"""

if request.param == 'uniq-sig':
if api_package_signature == 'no-collide':
mobile_param_name = 'mobile_data'
web_param_name = 'web_data'
else:
assert request.param == 'same-sig'
assert api_package_signature == 'collide'
mobile_param_name = web_param_name = 'data'

api_dir = pytester.mkpydir('api')
Expand All @@ -650,7 +655,8 @@ def probe(
return api_dir


def test_component_name_isolated_by_their_path(pytester, api_package):
@pytest.mark.usefixtures('api_package')
def test_component_name_isolated_by_their_path(pytester, api_package_signature):
"""Test we can mix methods with same names in one openapi.json schema
"""

Expand Down Expand Up @@ -685,17 +691,13 @@ def test_no_collide(app_client):
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
''')
if '{package_type}' == 'collide':
assert ('api.mobile._Params[probe]' in schemas) ^ ('api.web._Params[probe]' in schemas)
assert ('api.mobile._Request[probe]' in schemas) ^ ('api.web._Request[probe]' in schemas)
else:
assert 'api.mobile._Params[probe]' in schemas and 'api.web._Params[probe]' in schemas
assert 'api.mobile._Request[probe]' in schemas and 'api.web._Request[probe]' in schemas
'''.format(package_type=api_package_signature))

# force reload module to drop component cache
# it's more efficient than use pytest.runpytest_subprocess()
Expand Down Expand Up @@ -732,4 +734,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
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_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',
},
}