This commit is contained in:
2025-06-06 08:20:45 +03:00
parent 346ffd4255
commit caf367262c
205 changed files with 6115 additions and 200 deletions

0
tests/__init__.py Normal file
View File

224
tests/conftest.py Normal file
View File

@@ -0,0 +1,224 @@
from collections.abc import Callable
from dataclasses import dataclass
import inspect
import os
from types import GenericAlias, UnionType
from typing import Any, get_args
from urllib3 import disable_warnings
import pytest
import requests
import dynamix_sdk.types as sdk_types
from dynamix_sdk.base import (
gen_api_params_cls_name,
create_api_params_cls,
BaseAPIFunctionProtocol,
base_proto_to_http_method,
)
from dynamix_sdk.utils import JSON, HTTPMethod
@dataclass(kw_only=True)
class SDKFunction:
api_cls: type[sdk_types.BaseAPI]
call_attrs: tuple[str, ...]
url_path: str
proto_cls: type[BaseAPIFunctionProtocol]
proto_method: Callable
http_method: HTTPMethod
params_model_cls: type[sdk_types.BaseAPIParamsModel]
result_cls: type[sdk_types.BaseAPIResult]
@dataclass(kw_only=True)
class APISubgroup:
name: str
cls: sdk_types.BaseAPI
functions: tuple[SDKFunction, ...]
@dataclass(kw_only=True)
class APIGroup:
name: str
cls: sdk_types.BaseAPI
subgroups: tuple[APISubgroup, ...]
@pytest.fixture(scope='session')
def api_groups():
result_list: list[APIGroup] = []
for attr_name, attr_annot in sdk_types.API.__annotations__.items():
api_group_name = attr_name
api_group_cls = attr_annot
api_subgroups: list[APISubgroup] = []
for attr_name, attr_annot in api_group_cls.__annotations__.items():
api_subgroup_name = attr_name
api_subgroup_cls = attr_annot
sdk_functions: list[SDKFunction] = []
for attr_name in dir(api_subgroup_cls):
if attr_name.startswith('_'):
continue
attr = getattr(api_subgroup_cls, attr_name)
if not callable(attr):
continue
method_name = attr_name
method = attr
for mixin_cls in api_subgroup_cls.__bases__[1:]:
if not hasattr(mixin_cls, method_name):
continue
assert issubclass(mixin_cls, BaseAPIFunctionProtocol), (
f'Class {mixin_cls.__qualname__}'
f' must be inherited from'
f' {BaseAPIFunctionProtocol.__qualname__}.'
)
valid_bases = base_proto_to_http_method.keys()
mixin_cls_base = mixin_cls.__base__
assert (
mixin_cls_base
and issubclass(mixin_cls_base, BaseAPIFunctionProtocol)
and mixin_cls_base in valid_bases
), (
f'Class {mixin_cls.__qualname__}'
f' must be inherited from one of these classes:'
f" {', '.join(p.__qualname__ for p in valid_bases)}."
)
proto_cls = mixin_cls
proto_cls_base = mixin_cls_base
break
else:
raise LookupError(
f'{api_subgroup_cls.__qualname__}:'
f'mixin class for method "{method_name}" not found.'
)
attr_names = (
api_group_name,
api_subgroup_name,
attr_name,
)
api_func_url_path = ''
for sdk_func_path_part in attr_names:
url_path_part = api_subgroup_cls._path_mapping_dict.get(
sdk_func_path_part,
sdk_func_path_part
)
api_func_url_path = f'{api_func_url_path}/{url_path_part}'
api_params_cls_name = gen_api_params_cls_name(
api_path=api_func_url_path,
)
result_cls = inspect.signature(method).return_annotation
sdk_functions.append(
SDKFunction(
api_cls=api_subgroup_cls,
call_attrs=attr_names,
url_path=api_func_url_path,
proto_cls=proto_cls,
proto_method=method,
http_method=base_proto_to_http_method[proto_cls_base],
params_model_cls=create_api_params_cls(
cls_name=api_params_cls_name,
module_name=result_cls.__module__,
protocol_method=method,
),
result_cls=result_cls,
)
)
api_subgroups.append(
APISubgroup(
name=api_subgroup_name,
cls=api_subgroup_cls,
functions=tuple(sdk_functions),
)
)
result_list.append(
APIGroup(
name=api_group_name,
cls=api_group_cls,
subgroups=tuple(api_subgroups),
)
)
return tuple(result_list)
@pytest.fixture(scope='session')
def sdk_dx_functions(api_groups):
result_list: list[SDKFunction] = []
for api_group in api_groups:
for api_subgroup in api_group.subgroups:
result_list += api_subgroup.functions
return tuple(result_list)
@pytest.fixture(scope='session')
def dx_models(sdk_dx_functions: tuple[SDKFunction, ...]):
def get_models_from_annotation(annotation: Any):
if not annotation:
raise TypeError
models = []
if annotation is Any:
return models
if isinstance(annotation, (UnionType, GenericAlias)):
for annotation in get_args(annotation):
models += get_models_from_annotation(annotation=annotation)
elif issubclass(annotation, sdk_types.BaseModel):
model_cls = annotation
models.append(model_cls)
return set(models)
def get_nested_models(model_cls: type[sdk_types.BaseModel]):
models = []
for field_info in model_cls.model_fields.values():
models += get_models_from_annotation(
annotation=field_info.annotation,
)
for model in models:
models += get_nested_models(model_cls=model)
return set(models)
dx_models = []
for sdk_func in sdk_dx_functions:
dx_models.append(sdk_func.params_model_cls)
dx_models += get_nested_models(model_cls=sdk_func.params_model_cls)
if issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultModel):
dx_models.append(sdk_func.result_cls)
dx_models += get_nested_models(model_cls=sdk_func.result_cls)
return set(dx_models)
@pytest.fixture(scope='session')
def dx_url():
dx_url = os.getenv('DYNAMIX_URL')
assert dx_url
return dx_url
@pytest.fixture(scope='session')
def dx_api_definition(dx_url: str) -> JSON:
API_DEFINITION_API_PATH = '/restmachine/system/docgenerator/prepareCatalog'
disable_warnings()
api_definition_resp = requests.post(
url=f'{dx_url}{API_DEFINITION_API_PATH}',
verify=False,
)
api_definition_resp.raise_for_status()
return api_definition_resp.json()

0
tests/local/__init__.py Normal file
View File

View File

@@ -0,0 +1,99 @@
import dynamix_sdk.types as sdk_types
from dynamix_sdk.utils import gen_cls_name_from_url_path
def test_api_class_naming(api_groups):
for api_group in api_groups:
correct_class_name = f'{api_group.name.capitalize()}API'
assert api_group.cls.__qualname__ == correct_class_name, (
f'\nAPI group: {api_group.name}'
f'\nCorrect API class name: {correct_class_name}'
)
for api_subgroup in api_group.subgroups:
correct_class_name = (
f'{api_group.name.capitalize()}'
f'{api_subgroup.name.capitalize()}'
f'API'
)
assert api_subgroup.cls.__qualname__ == correct_class_name, (
f'\nAPI group: {api_group.name}'
f'\nAPI subgroup: {api_subgroup.name}'
f'\nCorrect API class name: {correct_class_name}'
)
def test_protocol_class_naming(sdk_dx_functions):
for sdk_func in sdk_dx_functions:
correct_class_name = gen_cls_name_from_url_path(
url_path=sdk_func.url_path,
postfix='Protocol',
)
assert sdk_func.proto_cls.__qualname__ == correct_class_name, (
f'\nFunction call attributes: {sdk_func.call_attrs}'
f'\nURL path: {sdk_func.url_path}'
f'\nCorrect Protocol class name: {correct_class_name}'
)
def get_subclasses(cls: type, result: list[type] | None = None):
if result:
_result = result
else:
_result: list[type] = []
for subclass in cls.__subclasses__():
if issubclass(subclass.__bases__[0], cls):
_result.append(subclass)
_result += get_subclasses(subclass)
return _result
def test_params_nested_model_class_naming():
base_cls = sdk_types.BaseAPIParamsNestedModel
for params_nm_cls in get_subclasses(base_cls):
suffix = 'APIParamsNM'
assert params_nm_cls.__qualname__.endswith(suffix), (
f'Class {params_nm_cls.__qualname__}:'
f' all subclasses of {base_cls.__qualname__}'
f' must have a name with suffix "{suffix}".'
)
def test_result_class_naming(sdk_dx_functions):
for sdk_func in sdk_dx_functions:
if issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultModel):
result_cls_postfix = 'ResultModel'
elif issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultStr):
result_cls_postfix = 'ResultStr'
elif issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultInt):
result_cls_postfix = 'ResultInt'
elif issubclass(sdk_func.result_cls, sdk_types.BaseAPIResultBool):
result_cls_postfix = 'ResultBool'
else:
raise TypeError
result_cls_name = gen_cls_name_from_url_path(
url_path=sdk_func.url_path,
postfix=result_cls_postfix,
)
assert sdk_func.result_cls.__qualname__ == result_cls_name, (
f'\nFunction call attributes: {sdk_func.call_attrs}'
f'\nURL path: {sdk_func.url_path}'
f'\nResult base class:'
f' {sdk_func.result_cls.__bases__[0].__qualname__}'
f'\nCorrect result class name: {result_cls_name}'
)
def test_result_nested_model_class_naming():
base_cls = sdk_types.BaseAPIResultNestedModel
for result_nm_cls in get_subclasses(base_cls):
suffix = 'APIResultNM'
assert result_nm_cls.__qualname__.endswith(suffix), (
f'Class {result_nm_cls.__qualname__}:'
f' all subclasses of {base_cls.__qualname__}'
f' must have a name with suffix "{suffix}".'
)

View File

@@ -0,0 +1,68 @@
from dynamix_sdk import types as sdk_types
from dynamix_sdk.base import (
get_alias,
name_mapping_dict,
)
def test_missing_mappings_in_name_mapping_file(dx_models):
attrs_without_mapping = []
for model_cls in dx_models:
for field_name in model_cls.model_fields.keys():
try:
get_alias(
field_name=field_name,
model_cls=model_cls,
name_mapping_dict=name_mapping_dict
)
except KeyError:
attrs_without_mapping.append(
f'{model_cls.__qualname__}.{field_name}'
)
attrs_without_mapping.sort()
assert not attrs_without_mapping, (
f'{len(attrs_without_mapping)} attributes without mapping:'
f' {attrs_without_mapping}'
)
def test_unused_mappings_in_name_mapping_file(dx_models):
mapping_dict_keys = set(name_mapping_dict.keys())
def exclude_used_keys(
model_cls: type[sdk_types.BaseModel],
mapping_dict_keys: set[str],
):
for field_name in model_cls.__annotations__.keys():
used_key = None
individual_alias_key = f'{field_name}__{model_cls.__qualname__}'
if individual_alias_key in mapping_dict_keys:
used_key = individual_alias_key
elif field_name in mapping_dict_keys:
used_key = field_name
if used_key and used_key in mapping_dict_keys:
mapping_dict_keys.remove(used_key)
for base_cls in model_cls.__bases__:
if issubclass(base_cls, sdk_types.BaseModel):
exclude_used_keys(
model_cls=base_cls,
mapping_dict_keys=mapping_dict_keys,
)
for model_cls in dx_models:
exclude_used_keys(
model_cls=model_cls,
mapping_dict_keys=mapping_dict_keys,
)
unused_mapping_dict_keys = sorted(mapping_dict_keys)
assert not unused_mapping_dict_keys, (
f'{len(unused_mapping_dict_keys)} unused keys in mapping file:'
f' {unused_mapping_dict_keys}.'
)

154
tests/local/test_types.py Normal file
View File

@@ -0,0 +1,154 @@
from enum import Enum
import inspect
from types import GenericAlias, ModuleType, UnionType
from typing import Any, get_args
import dynamix_sdk.types as sdk_types
import dynamix_sdk.api._nested.params as nested_params
import dynamix_sdk.api._nested.result as nested_result
import dynamix_sdk.api._nested.enums as nested_enums
from tests.conftest import SDKFunction
def check_model_field_annotation(
annotation: Any,
field_descr: str,
valid_nested_model_cls: type[sdk_types.BaseModel],
):
if not annotation:
raise TypeError
if annotation is Any:
return
assert annotation is not list, (
f'{field_descr}: missing list elements type annotation.'
)
if isinstance(annotation, (UnionType, GenericAlias)):
for annotation in get_args(annotation):
check_model_field_annotation(
annotation=annotation,
field_descr=field_descr,
valid_nested_model_cls=valid_nested_model_cls,
)
elif issubclass(annotation, sdk_types.BaseModel):
model_cls = annotation
assert issubclass(model_cls, valid_nested_model_cls), (
f'{field_descr}: nested model class must be'
f' a subclass of {valid_nested_model_cls.__qualname__}.'
)
check_model_field_annotations(
model_cls=model_cls,
valid_nested_model_cls=valid_nested_model_cls,
)
def check_model_field_annotations(
model_cls: type[sdk_types.BaseModel],
valid_nested_model_cls: type[sdk_types.BaseModel],
):
for field_name, field_info in model_cls.model_fields.items():
field_descr = f'{model_cls.__qualname__}.{field_name}'
check_model_field_annotation(
annotation=field_info.annotation,
field_descr=field_descr,
valid_nested_model_cls=valid_nested_model_cls,
)
def test_params_model_fields_type(sdk_dx_functions: tuple[SDKFunction, ...]):
for sdk_func in sdk_dx_functions:
params = inspect.signature(sdk_func.proto_method).parameters
for param_name, param in params.items():
if param_name == 'self':
continue
field_descr = (
f'{sdk_func.proto_cls.__qualname__}.'
f'{sdk_func.call_attrs[-1]}.'
f'{param_name}'
)
assert param.annotation is not inspect.Parameter.empty, (
f'{field_descr}: missing type annotation.'
)
assert param.kind == param.KEYWORD_ONLY, (
f'Parameter {field_descr} must be keyword-only.'
)
check_model_field_annotation(
annotation=param.annotation,
field_descr=field_descr,
valid_nested_model_cls=sdk_types.BaseAPIParamsNestedModel,
)
def test_function_return_type(sdk_dx_functions):
for sdk_func in sdk_dx_functions:
assert issubclass(sdk_func.result_cls, sdk_types.BaseAPIResult), (
f'Return type for method'
f' {sdk_func.proto_cls.__qualname__}.{sdk_func.call_attrs[-1]}'
f' must be a subclass of BaseAPIResult.'
)
def test_result_model_fields_type():
valid_nested_model_cls = sdk_types.BaseAPIResultNestedModel
for subcls in sdk_types.BaseAPIResultModel.__subclasses__():
check_model_field_annotations(
model_cls=subcls,
valid_nested_model_cls=valid_nested_model_cls,
)
for subcls in sdk_types.BaseAPIResultNestedModel.__subclasses__():
check_model_field_annotations(
model_cls=subcls,
valid_nested_model_cls=valid_nested_model_cls,
)
def check_class_inheritance_in_module(
module: ModuleType,
valid_base_cls: type,
):
for attr_name in dir(module):
if attr_name.startswith('_'):
continue
attr = getattr(module, attr_name)
if not isinstance(attr, type):
continue
nested_result_cls = attr
assert issubclass(nested_result_cls, valid_base_cls), (
f'Class {nested_result_cls.__qualname__}:'
f' must be a subclass of class {valid_base_cls.__qualname__}.'
)
def test_class_inheritance_in_nested_result_module():
check_class_inheritance_in_module(
module=nested_result,
valid_base_cls=sdk_types.BaseAPIResultNestedModel,
)
def test_class_inheritance_in_nested_params_module():
check_class_inheritance_in_module(
module=nested_params,
valid_base_cls=sdk_types.BaseAPIParamsNestedModel,
)
def test_class_inheritance_in_nested_enums_module():
check_class_inheritance_in_module(
module=nested_enums,
valid_base_cls=Enum,
)

View File

@@ -0,0 +1,351 @@
from enum import Enum
import inspect
from types import GenericAlias, NoneType, UnionType
from typing import Any, get_args
from warnings import warn
from dynamix_sdk import types as sdk_types
from dynamix_sdk.base import (
get_alias,
name_mapping_dict,
)
from dynamix_sdk.utils import JSON, get_nested_value
from tests.conftest import SDKFunction
expected_inconsistencies: list[str] = [
'cloudapi.disks.limit_io: API has parameter "iops" but this SDK function doesn\'t have corresponding parameter.', # noqa: E501
'cloudapi.image.list: API has parameter "architecture" but this SDK function doesn\'t have corresponding parameter.', # noqa: E501
'cloudapi.bservice.group_stop: default value of parameter "force" must be None.', # noqa: E501
'cloudapi.bservice.group_stop: annotation of parameter "force" must be Union.', # noqa: E501
'cloudapi.bservice.delete: default value of parameter "permanently" must be None.', # noqa: E501
'cloudapi.bservice.delete: annotation of parameter "permanently" must be Union.', # noqa: E501
'cloudapi.compute.net_attach: result type must be child of BaseAPIResultBool.', # noqa: E501
'cloudapi.compute.net_attach: default value of parameter "mtu" must be 1500.', # noqa: E501
'''cloudapi.compute.update: parameter "cpu_pin", target_annotation = None | bool, expected_annot = <class 'bool'>''', # noqa: E501
'cloudapi.disks.create: default value of parameter "size_gb" must be 10.',
'''cloudapi.disks.create: API has parameter "ssdSize" but this SDK function doesn't have corresponding parameter.''', # noqa: E501
'cloudbroker.account.create: annotation of parameter "uniq_pools" must contain int.', # noqa: E501
'cloudapi.kvmx86.create: annotation of parameter "ci_user_data" must contain BaseAPIParamsNestedModel.', # noqa: E501
'''cloudapi.image.create: API has parameter "architecture" but this SDK function doesn't have corresponding parameter.''', # noqa: E501
'cloudapi.lb.create: annotation of parameter "sysctl_params" must contain BaseAPIParamsNestedModel.', # noqa: E501
]
def test_with_api_definition(
dx_api_definition: JSON,
sdk_dx_functions: tuple[SDKFunction, ...],
):
inconsistencies: list[str] = []
for sdk_func in sdk_dx_functions:
func_full_api_path = f'/restmachine{sdk_func.url_path}'
if not isinstance(dx_api_definition, dict):
raise TypeError
dx_api_definition_paths = dx_api_definition['paths']
if not isinstance(dx_api_definition_paths, dict):
raise TypeError
if func_full_api_path not in dx_api_definition_paths:
inconsistencies.append(
f"{'.'.join(sdk_func.call_attrs)}:"
f' url path {sdk_func.url_path} not found in'
f' DX API definition.'
)
continue
func_api_definition = dx_api_definition_paths[func_full_api_path]
if not isinstance(func_api_definition, dict):
raise TypeError
http_method_lower = sdk_func.http_method.lower()
if http_method_lower not in func_api_definition:
api_def_http_methods = [
m.upper() for m in func_api_definition.keys()
]
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' HTTP method {sdk_func.http_method} not found.'
f' DX API definition for {sdk_func.url_path} has'
f' only these HTTP methods:'
f" {', '.join(api_def_http_methods)}."
)
continue
func_api_def_method = func_api_definition[http_method_lower]
if not isinstance(func_api_def_method, dict):
raise TypeError
type_mappings = {
'integer': int,
'string': str,
'boolean': bool,
'object': sdk_types.BaseAPIParamsNestedModel,
'number': float,
}
if api_def_result_type := get_nested_value(
d=func_api_def_method,
keys=('responses', '200', 'schema', 'type'),
):
expected_result_type = type_mappings[api_def_result_type]
if expected_result_type is bool:
expected_result_type = sdk_types.BaseAPIResultBool
if not issubclass(sdk_func.result_cls, expected_result_type):
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' result type must be child of'
f' {expected_result_type.__qualname__}.'
)
sdk_func_params: dict[str, inspect.Parameter] = {}
for p in inspect.signature(sdk_func.proto_method).parameters.values():
if p.name == 'self':
continue
alias = get_alias(
field_name=p.name,
model_cls=sdk_func.params_model_cls,
name_mapping_dict=name_mapping_dict,
)
sdk_func_params[alias] = p
api_definition_params = func_api_def_method.get('parameters')
if not api_definition_params:
if sdk_func_params:
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' this function must not have parameters.'
)
continue
if not isinstance(api_definition_params, list):
raise TypeError
dx_param_names = set()
for p in api_definition_params:
if not isinstance(p, dict):
raise TypeError
dx_param_names.add(p['name'])
if len(sdk_func_params) > len(dx_param_names):
unused_sdk_param_names = []
for sdk_param_alias, sdk_param in sdk_func_params.items():
if sdk_param_alias not in dx_param_names:
unused_sdk_param_names.append(sdk_param.name)
if unused_sdk_param_names:
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' unused parameters:'
f' {", ".join(sorted(unused_sdk_param_names))}'
)
for api_def_param in api_definition_params:
if not isinstance(api_def_param, dict):
raise TypeError
api_def_param_enum = api_def_param.get('enum')
if (
api_def_param_enum is not None
and not isinstance(api_def_param_enum, list)
):
raise TypeError
api_def_param_default = api_def_param.get('default')
api_def_param_type = api_def_param.get('type')
if (
api_def_param_type is not None
and not isinstance(api_def_param_type, str)
):
raise TypeError
api_def_param_items = api_def_param.get('items')
if (
api_def_param_items is not None
and not isinstance(api_def_param_items, dict)
):
raise TypeError
necessary_dx_param = not (
api_def_param_enum
and len(api_def_param_enum) == 1
and api_def_param_default is not None
)
sdk_param_alias = api_def_param['name']
if not isinstance(sdk_param_alias, str):
raise TypeError
param_exists_in_sdk = sdk_param_alias in sdk_func_params.keys()
if not param_exists_in_sdk:
if necessary_dx_param:
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' API has parameter "{sdk_param_alias}"'
f" but this SDK function doesn't have"
f' corresponding parameter.'
)
continue
sdk_param_name = sdk_func_params[sdk_param_alias].name
if not necessary_dx_param:
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' parameter "{sdk_param_name} is unnecessary in SDK.'
)
continue
sdk_func_param = sdk_func_params[sdk_param_alias]
if sdk_func_param.annotation is Any:
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' annotation of parameter "{sdk_param_name}"'
f' must not be Any.'
)
continue
if api_def_param_default and api_def_param_default != -1:
if sdk_func_param.default != api_def_param_default:
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' default value of parameter "{sdk_param_name}"'
f' must be {api_def_param_default}.'
)
target_annotation = sdk_func_param.annotation
if not api_def_param.get('required') and (
api_def_param_default is None
or (
api_def_param_default == 0
and api_def_param_default is not False
)
or api_def_param_default == ''
or api_def_param_default == -1
):
if sdk_func_param.default is not None:
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' default value of parameter "{sdk_param_name}"'
f' must be None.'
)
if not isinstance(sdk_func_param.annotation, UnionType):
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' annotation of parameter "{sdk_param_name}"'
f' must be Union.'
)
continue
annot_union_args = get_args(sdk_func_param.annotation)
if NoneType not in annot_union_args:
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' annotation of parameter "{sdk_param_name}"'
f' must contain None.'
)
continue
if not len(annot_union_args) == 2:
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' annotation of parameter "{sdk_param_name}"'
f' must contain two types.'
)
continue
if annot_union_args[0] is NoneType:
target_annotation = annot_union_args[1]
else:
target_annotation = annot_union_args[0]
if api_def_param_type == 'array':
if (
not isinstance(target_annotation, GenericAlias)
or target_annotation.__origin__ is not list
):
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' annotation of parameter "{sdk_param_name}"'
f' must contain list[...].'
)
continue
if api_def_param_items is not None:
api_def_param_items_type = api_def_param_items.get('type')
if api_def_param_items_type is None:
continue
if not isinstance(api_def_param_items_type, str):
raise TypeError
target_annotation = get_args(target_annotation)[0]
expected_annot = type_mappings.get(
api_def_param_items_type
)
if expected_annot is None:
continue
elif api_def_param_type is not None:
expected_annot = type_mappings.get(api_def_param_type)
if expected_annot is None:
continue
if not inspect.isclass(target_annotation):
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' parameter "{sdk_param_name}",'
f' {target_annotation = },'
f' {expected_annot = }'
)
continue
if api_def_param_enum:
if not issubclass(target_annotation, Enum):
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' annotation of parameter "{sdk_param_name}"'
f' must contain enum.'
)
continue
enum_from_annot = target_annotation
enum_values = enum_from_annot.__members__.values()
if sorted(api_def_param_enum) != sorted(enum_values):
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' parameter "{sdk_param_name}":'
f' enum {enum_from_annot.__qualname__}'
f' must contain these values: {api_def_param_enum}.'
)
elif not issubclass(target_annotation, expected_annot):
inconsistencies.append(
f'{".".join(sdk_func.call_attrs)}:'
f' annotation of parameter "{sdk_param_name}"'
f' must contain {expected_annot.__qualname__}.'
)
continue
for expected_inconsistency in expected_inconsistencies:
if expected_inconsistency in inconsistencies:
inconsistencies.remove(expected_inconsistency)
else:
inconsistencies.append(
'This expected inconsistency not found:'
f' {expected_inconsistency}'
)
if expected_inconsistencies:
warn(
'\nExpected inconsistencies:\n'
+ '\n'.join(expected_inconsistencies)
)
assert not inconsistencies, (
f'found {len(inconsistencies)} inconsistencies:\n'
+ '\n'.join(inconsistencies)
)