Files
OpenBB/cli/tests/test_codegen_pydantic_gen.py
Danglewood 9a30186174 [V5] CLI 2.0 - Pluggable Backends, Non-TTY Default, HTTP Dispatcher, Spec Files + Codegen (#7433)
* stash some changes

* add more robust testing

* mypy

* point PR at V5

* introduce spec file

* codespell

* test fix

* fix workflow environment setup

* fix workflow environment setup

* fix workflow environment setup

* add pyyaml to dependencies

* split lint jobs

* fix workflow environment setup

* fix workflow environment setup

* workflow env setup

* workflow env setup

* clean up code comments

* add auth hook entrypoints

* codespell

* add codegen feature

* codespell

* move _unpack into dispatchers for consistency with codegen packages

* surface nested models in the response

* fix missing coverage in CI

* socrata updates

* test fix

* detect plotly output

* add --include and --exclude flags from generate-extension command

* cap test matrix at python 3.14

* no useless comments

* platform controller command description split

* merge URL overloads from path params

* exclude none and unset from model dump

---------

Co-authored-by: deeleeramone <>
Co-authored-by: Copilot <copilot@github.com>
2026-06-01 19:14:38 +03:00

996 lines
32 KiB
Python

"""Tests for openbb_cli.codegen.pydantic_gen — schema → Pydantic source."""
from __future__ import annotations
import pytest
from openbb_cli.codegen import pydantic_gen as pg
# --- Naming helpers ---
def test_safe_field_name_passes_through_valid_identifier():
assert pg.safe_field_name("symbol") == ("symbol", False)
def test_safe_field_name_replaces_invalid_chars():
assert pg.safe_field_name("X-API-Key") == ("X_API_Key", True)
def test_safe_field_name_prefixes_leading_digit_with_letter():
"""``f_`` (not ``_``) so Pydantic v2 accepts it — leading-underscore
field names are rejected with "must not use names with leading
underscores"."""
assert pg.safe_field_name("404") == ("f_404", True)
def test_safe_field_name_handles_empty_input():
safe, alias = pg.safe_field_name("")
assert safe == "field"
assert alias is True
def test_safe_field_name_strips_leading_dollar_for_socrata_soql_params():
"""``$select`` → ``select`` (with alias ``$select``) — the leading
underscore that ``re.sub`` would otherwise produce trips Pydantic's
"Fields must not use names with leading underscores" check."""
assert pg.safe_field_name("$select") == ("select", True)
def test_safe_field_name_strips_leading_underscore_from_already_underscore_name():
"""An already-underscore-prefixed name (rare in practice) loses the
underscore so the result is Pydantic-compatible."""
assert pg.safe_field_name("_internal") == ("internal", True)
def test_safe_field_name_appends_underscore_for_keyword():
assert pg.safe_field_name("class") == ("class_", True)
def test_safe_field_name_appends_underscore_for_pydantic_reserved():
assert pg.safe_field_name("model_config") == ("model_config_", True)
def test_class_name_from_camel_cases_dotted_path():
assert pg.class_name_from("equity.price.quote", "Data") == "EquityPriceQuoteData"
def test_class_name_from_drops_falsy_parts():
assert pg.class_name_from("", "law", None) == "Law" # type: ignore[arg-type]
def test_class_name_from_handles_only_special_chars():
assert pg.class_name_from("---") == "Anonymous"
def test_class_name_from_prefixes_leading_digit():
"""Names starting with a digit get an underscore prefix to be valid identifiers."""
out = pg.class_name_from("123start")
assert out.startswith("_")
assert out.isidentifier()
# --- schema_to_type primitive coverage ---
@pytest.mark.parametrize(
"schema,expected",
[
({"type": "string"}, "str"),
({"type": "integer"}, "int"),
({"type": "number"}, "float"),
({"type": "boolean"}, "bool"),
],
)
def test_schema_to_type_primitives(schema, expected):
nested: list = []
imports: set[str] = set()
out = pg.schema_to_type(
schema,
parent_class_name="P",
field_name="x",
nested_classes=nested,
imports=imports,
)
assert out == expected
def test_schema_to_type_string_date_format_emits_date_import():
nested: list = []
imports: set[str] = set()
out = pg.schema_to_type(
{"type": "string", "format": "date"},
parent_class_name="P",
field_name="x",
nested_classes=nested,
imports=imports,
)
assert out == "datetime.date"
assert "import datetime" in imports
def test_schema_to_type_string_datetime_format_emits_datetime_import():
imports: set[str] = set()
out = pg.schema_to_type(
{"type": "string", "format": "date-time"},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=imports,
)
assert out == "datetime.datetime"
assert "import datetime" in imports
def test_schema_to_type_unknown_type_falls_back_to_any():
imports: set[str] = set()
out = pg.schema_to_type(
{},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=imports,
)
assert out == "Any"
assert "from typing import Any" in imports
def test_schema_to_type_treats_non_dict_as_any():
"""Defensive: a stray list / string in the schema doesn't crash."""
imports: set[str] = set()
out = pg.schema_to_type(
"not-a-schema",
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=imports,
)
assert out == "Any"
def test_schema_to_type_ref_cycle_collapses_to_dict():
"""A ``$ref`` left over from cycle detection becomes ``dict[str, Any]``."""
imports: set[str] = set()
out = pg.schema_to_type(
{"$ref": "#/components/schemas/Tree"},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=imports,
)
assert out == "dict[str, Any]"
# --- enum / const ---
def test_schema_to_type_enum_returns_literal_with_typing_import():
"""``enum`` resolves to ``Literal[...]`` — ``generate_class`` later
appends ``| None`` for optional fields (default=None) so the field
accepts the choices or the omit/None case (no filter applied)."""
imports: set[str] = set()
out = pg.schema_to_type(
{"type": "string", "enum": ["buy", "sell"]},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=imports,
)
assert out == "Literal['buy', 'sell']"
assert "from typing import Literal" in imports
def test_schema_to_type_enum_emits_literal_when_type_omitted():
"""Spec authors sometimes omit ``type`` next to ``enum``; the values
drive the Literal regardless."""
imports: set[str] = set()
out = pg.schema_to_type(
{"enum": [1, 2, 3]},
parent_class_name="P",
field_name="n",
nested_classes=[],
imports=imports,
)
assert out == "Literal[1, 2, 3]"
def test_schema_to_type_const_returns_single_member_literal():
imports: set[str] = set()
out = pg.schema_to_type(
{"type": "string", "const": "fred"},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=imports,
)
assert out == "Literal['fred']"
def test_schema_to_type_const_emits_literal_from_value_alone():
"""``const`` without an explicit ``type`` still emits ``Literal[<value>]``."""
imports: set[str] = set()
out = pg.schema_to_type(
{"const": 42},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=imports,
)
assert out == "Literal[42]"
# --- arrays ---
def test_schema_to_type_array_of_strings():
out = pg.schema_to_type(
{"type": "array", "items": {"type": "string"}},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=set(),
)
assert out == "list[str]"
def test_schema_to_type_array_with_no_items_falls_back_to_any():
out = pg.schema_to_type(
{"type": "array"},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=set(),
)
assert out == "list[Any]"
# --- unions ---
def test_schema_to_type_optional_collapses_to_pipe_none():
out = pg.schema_to_type(
{"anyOf": [{"type": "string"}, {"type": "null"}]},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=set(),
)
assert out == "str | None"
def test_schema_to_type_union_of_two_types():
out = pg.schema_to_type(
{"anyOf": [{"type": "string"}, {"type": "integer"}]},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=set(),
)
assert out == "str | int"
def test_schema_to_type_union_dedupes_repeated_branches():
"""``anyOf: [str, str]`` → ``str``, not ``str | str``."""
out = pg.schema_to_type(
{"anyOf": [{"type": "string"}, {"type": "string"}]},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=set(),
)
assert out == "str"
def test_schema_to_type_union_only_null_falls_back_to_any():
out = pg.schema_to_type(
{"anyOf": [{"type": "null"}]},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=set(),
)
assert out == "Any"
def test_schema_to_type_oneof_treated_same_as_anyof():
out = pg.schema_to_type(
{"oneOf": [{"type": "string"}, {"type": "null"}]},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=set(),
)
assert out == "str | None"
# --- nested objects ---
def test_schema_to_type_object_with_properties_creates_nested_class():
nested: list = []
out = pg.schema_to_type(
{
"type": "object",
"properties": {"name": {"type": "string"}},
"title": "Inner",
},
parent_class_name="Outer",
field_name="security",
nested_classes=nested,
imports=set(),
)
assert out == "Inner"
assert len(nested) == 1
assert nested[0].name == "Inner"
assert "name: str | None" in nested[0].source
def test_schema_to_type_object_without_title_derives_name_from_parent_field():
"""Anonymous nested object names come from ``<parent><field>`` Camel."""
nested: list = []
out = pg.schema_to_type(
{"type": "object", "properties": {"v": {"type": "integer"}}},
parent_class_name="Wrapper",
field_name="payload",
nested_classes=nested,
imports=set(),
)
assert out == "WrapperPayload"
assert nested[0].name == "WrapperPayload"
def test_schema_to_type_open_object_falls_back_to_dict_any():
"""``additionalProperties: true`` with no properties → ``dict[str, Any]``."""
imports: set[str] = set()
out = pg.schema_to_type(
{"type": "object", "additionalProperties": True},
parent_class_name="P",
field_name="x",
nested_classes=[],
imports=imports,
)
assert out == "dict[str, Any]"
def test_schema_to_type_typed_additional_properties_keeps_value_type():
"""``additionalProperties: {"type": "number"}`` → ``dict[str, float]``."""
out = pg.schema_to_type(
{"type": "object", "additionalProperties": {"type": "number"}},
parent_class_name="P",
field_name="coeffs",
nested_classes=[],
imports=set(),
)
assert out == "dict[str, float]"
def test_schema_to_type_nested_typed_additional_properties():
"""``dict[str, dict[str, float]]`` — confidence-interval style."""
out = pg.schema_to_type(
{
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": {"type": "number"},
},
},
parent_class_name="P",
field_name="conf_int",
nested_classes=[],
imports=set(),
)
assert out == "dict[str, dict[str, float]]"
# --- generate_class ---
def test_generate_class_required_and_optional_fields():
schema = {
"type": "object",
"required": ["symbol"],
"properties": {
"symbol": {"type": "string", "description": "Ticker."},
"limit": {"type": "integer", "default": 5, "description": "Page size."},
"label": {"type": "string", "description": "Pretty label."},
},
}
cls = pg.generate_class(schema, class_name="Quote")
src = cls.source
assert "class Quote(BaseModel):" in src
assert "symbol: str = Field(description='Ticker.')" in src
assert "limit: int | None = Field(default=5, description='Page size.')" in src
assert "label: str | None = Field(default=None, description='Pretty label.')" in src
def test_generate_class_field_alias_for_dotted_property_name():
schema = {
"type": "object",
"properties": {"X-API-Key": {"type": "string"}},
}
cls = pg.generate_class(schema, class_name="Headers")
assert "X_API_Key:" in cls.source
assert "alias='X-API-Key'" in cls.source
def test_generate_class_no_properties_emits_pass_body():
cls = pg.generate_class({"type": "object"}, class_name="Empty")
assert " pass" in cls.source
def test_generate_class_uses_provided_docstring_summary():
cls = pg.generate_class(
{"type": "object", "properties": {"x": {"type": "string"}}},
class_name="C",
docstring="Custom docs.",
)
# Summary appears at the top of the Numpy-style block
assert '"""Custom docs.' in cls.source
def test_generate_class_falls_back_to_class_name_when_no_docstring():
"""Summary defaults to the bare class name when the schema has none."""
cls = pg.generate_class(
{"type": "object", "properties": {"x": {"type": "string"}}},
class_name="C",
)
assert '"""C.' in cls.source
def test_generate_class_emits_numpy_parameters_block():
"""Each field appears in the Numpy ``Parameters`` block with type + description."""
schema = {
"type": "object",
"required": ["symbol"],
"properties": {
"symbol": {"type": "string", "description": "Ticker."},
"limit": {"type": "integer", "default": 5, "description": "Page size."},
},
}
cls = pg.generate_class(schema, class_name="Quote")
src = cls.source
assert "Parameters" in src
assert "----------" in src
# Required field: bare type
assert "symbol : str" in src
# Optional field: ``, optional`` suffix + default in description
assert "limit : int, optional" in src
assert "(default: 5)" in src
def test_generate_class_docstring_summary_gets_trailing_period():
"""A summary without a trailing ``.`` has one added — matches D415."""
cls = pg.generate_class(
{"type": "object", "properties": {"x": {"type": "string"}}},
class_name="C",
docstring="Custom docs",
)
assert '"""Custom docs.' in cls.source
def test_generate_class_docstring_field_with_no_description_omits_body_line():
"""A field with no description renders as ``name : type`` only."""
cls = pg.generate_class(
{"type": "object", "properties": {"x": {"type": "string"}}},
class_name="C",
)
block = cls.source.split('"""')[1]
assert "x : str, optional" in block
lines = block.split("\n")
x_idx = next(i for i, line in enumerate(lines) if "x : str" in line)
next_line = lines[x_idx + 1] if x_idx + 1 < len(lines) else ""
assert next_line.strip() != "."
def test_generate_class_handles_nested_object_via_separate_class():
"""A plain nested object becomes its own ``BaseModel`` class so Pydantic
can recursively validate the runtime row."""
schema = {
"type": "object",
"properties": {
"inner": {
"type": "object",
"title": "Inner",
"properties": {"v": {"type": "integer"}},
}
},
}
cls = pg.generate_class(schema, class_name="Outer")
assert cls.nested
assert cls.nested[0].name == "Inner"
assert "inner: Inner | None" in cls.source
def test_generate_class_handles_deep_nesting_via_flatten():
"""Nested → nested objects all show up in topological order in ``flatten``."""
schema = {
"type": "object",
"properties": {
"a": {
"type": "object",
"title": "A",
"properties": {
"b": {
"type": "object",
"title": "B",
"properties": {"v": {"type": "integer"}},
}
},
}
},
}
cls = pg.generate_class(schema, class_name="Root")
flat = cls.flatten()
names = [c.name for c in flat]
# B comes before A because A depends on B; Root last because it depends on A
assert names.index("B") < names.index("A")
assert names.index("A") < names.index("Root")
def test_generate_class_collect_imports_unions_descendants():
schema = {
"type": "object",
"properties": {
"when": {"type": "string", "format": "date"},
"limit": {"type": "integer"},
},
}
cls = pg.generate_class(schema, class_name="C")
imports = cls.collect_imports()
assert "import datetime" in imports
assert "from pydantic import BaseModel, Field" in imports
def test_generate_class_array_of_objects_emits_nested_item_class():
schema = {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"title": "Item",
"properties": {"id": {"type": "integer"}},
},
}
},
}
cls = pg.generate_class(schema, class_name="List")
assert any(c.name == "Item" for c in cls.nested)
assert "items: list[Item]" in cls.source
# --- render_module ---
def test_render_module_emits_imports_then_classes_in_topological_order():
schema = {
"type": "object",
"title": "Outer",
"properties": {
"inner": {
"type": "object",
"title": "Inner",
"properties": {"x": {"type": "integer"}},
}
},
}
cls = pg.generate_class(schema, class_name="Outer")
rendered = pg.render_module(cls, module_docstring="Generated module.")
# Docstring at the top (single-line form is fine, just must be present first)
assert rendered.lstrip().startswith(("'", '"'))
assert "Generated module." in rendered.split("\n", 3)[0]
# Inner class defined before Outer
assert rendered.index("class Inner") < rendered.index("class Outer")
# Pydantic import present
assert "from pydantic import BaseModel, Field" in rendered
def test_render_module_compiles_to_valid_python():
"""The emitted source actually parses and produces working Pydantic models."""
import ast
import importlib.util
import sys
import textwrap
schema = {
"type": "object",
"required": ["symbol"],
"properties": {
"symbol": {"type": "string"},
"side": {"type": "string", "enum": ["buy", "sell"]},
"qty": {"type": "integer", "default": 1},
"tags": {"type": "array", "items": {"type": "string"}},
"when": {"type": "string", "format": "date-time"},
"details": {
"type": "object",
"title": "Details",
"properties": {"note": {"type": "string"}},
},
"freeform": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
},
}
cls = pg.generate_class(schema, class_name="Order")
rendered = pg.render_module(cls)
# 1. parses
ast.parse(rendered)
# 2. evaluating it produces a real Pydantic class with the expected fields
spec = importlib.util.spec_from_loader("generated_test_mod", loader=None)
assert spec is not None
mod = importlib.util.module_from_spec(spec)
sys.modules["generated_test_mod"] = mod
try:
exec(textwrap.dedent(rendered), mod.__dict__) # noqa: S102 — controlled source
Order = mod.Order
instance = Order(symbol="AAPL", side="buy", details={"note": "x"})
assert instance.symbol == "AAPL"
assert instance.side == "buy"
assert instance.qty == 1
assert instance.details.note == "x"
finally:
sys.modules.pop("generated_test_mod", None)
@pytest.mark.parametrize(
"default,expected",
[
(True, "True"),
(False, "False"),
(42, "42"),
(1.5, "1.5"),
("text", "'text'"),
(None, "None"),
([1, 2], "[1, 2]"),
((1, 2), "(1, 2)"),
({"k": "v"}, "{'k': 'v'}"),
(object(), "<"), # Falls through to repr() for unknown types
],
)
def test_render_field_default_covers_every_branch(default, expected):
"""Every literal branch in ``render_field_default`` round-trips correctly."""
out = pg.render_field_default(default)
assert expected in out
def test_python_string_literal_handles_multiline_text():
"""Multi-line text uses triple-quoted form."""
out = pg._python_string_literal("line one\nline two")
assert out.startswith('"""')
assert out.endswith('"""')
assert "line one" in out
assert "line two" in out
def test_python_string_literal_escapes_embedded_triple_quotes():
"""A description containing ``\"\"\"`` mustn't break the literal."""
out = pg._python_string_literal('contains """ inside\nmore text')
# The triple quotes inside the string are escaped, not closed
assert out.count('"""') == 2 # only the opening and closing
def test_generate_class_optional_field_emits_default_none_with_empty_description():
"""Optional field with no spec text emits ``description=''`` (not omitted) so
openbb-core's DocstringGenerator doesn't render the literal string ``"None"``."""
schema = {"type": "object", "properties": {"x": {"type": "string"}}}
cls = pg.generate_class(schema, class_name="C")
assert "x: str | None = Field(default=None, description='')" in cls.source
def test_generate_class_uses_triple_quoted_docstring_for_multiline():
"""A multi-line docstring lands as a triple-quoted block."""
cls = pg.generate_class(
{"type": "object", "properties": {"x": {"type": "string"}}},
class_name="C",
docstring="line one\nline two",
)
assert '"""' in cls.source
assert "line two" in cls.source
# --- render_function_docstring ---
def test_render_function_docstring_minimum_summary_only():
out = pg.render_function_docstring("Fetch the data")
assert out == ' """Fetch the data.\n """'
def test_render_function_docstring_summary_keeps_existing_period():
out = pg.render_function_docstring("Already punctuated.")
assert "Already punctuated." in out
# No double period
assert "..\n" not in out
def test_render_function_docstring_emits_parameters_section():
out = pg.render_function_docstring(
"Send a request",
parameters=[
("query", "QueryParams", "Validated query parameters."),
("credentials", "dict[str, str] | None", None),
],
)
assert "Parameters" in out
assert "----------" in out
assert "query : QueryParams" in out
assert " Validated query parameters." in out
assert "credentials : dict[str, str] | None" in out
cred_idx = out.index("credentials :")
after = out[cred_idx:]
next_line = after.split("\n", 2)[1] if "\n" in after else ""
assert next_line.strip() != "."
def test_render_function_docstring_emits_returns_section():
out = pg.render_function_docstring(
"Build something",
returns=("BillData", "The fetched bill record."),
)
assert "Returns" in out
assert "-------" in out
assert "BillData" in out
assert " The fetched bill record." in out
def test_render_function_docstring_returns_section_with_no_description():
out = pg.render_function_docstring(
"Build something",
returns=("int", None),
)
assert "Returns" in out
assert "int" in out
assert " ." not in out
def test_render_function_docstring_custom_indent_for_nested_definitions():
"""Method docstrings indent by 8 spaces (4 for class + 4 for method body)."""
out = pg.render_function_docstring("Nested method.", indent=" ")
assert out.startswith(' """')
# --- _annotation_for_docstring ---
def test_annotation_for_docstring_strips_pipe_none():
assert pg._annotation_for_docstring("str | None") == ("str", True)
def test_annotation_for_docstring_keeps_required_type_intact():
assert pg._annotation_for_docstring("list[int]") == ("list[int]", False)
def test_render_module_against_real_openbb_response_schema():
"""End-to-end: shape resembling a real OBBject inner data class."""
import ast
# Truncated copy of OLSRegressionResults's properties
schema = {
"type": "object",
"title": "OLSRegressionResults",
"required": ["params", "rsquared", "nobs"],
"properties": {
"params": {
"type": "object",
"additionalProperties": {"type": "number"},
"description": "Coefficient estimates.",
},
"rsquared": {"type": "number", "description": "R-squared."},
"nobs": {"type": "integer", "description": "Observation count."},
"fvalue": {
"anyOf": [{"type": "number"}, {"type": "null"}],
"description": "F-statistic.",
},
},
}
cls = pg.generate_class(schema, class_name="OLSRegressionResults")
rendered = pg.render_module(cls)
ast.parse(rendered)
assert "rsquared: float = Field(description='R-squared.')" in rendered
assert "fvalue: float | None" in rendered
def test_build_field_call_wraps_when_single_line_exceeds_budget():
"""Lines 355-356: long Field args force the multi-line wrap path."""
long_description = "This is a deliberately verbose description " * 4
out = pg._build_field_call(
description=long_description,
default=None,
has_default=False,
required=True,
alias=None,
leading_width=20,
)
assert out.startswith("Field(\n ")
assert out.endswith(",\n )")
def test_field_falls_back_to_example_when_no_description():
"""Schema with only ``example`` (no ``description``) gets ``"Example: <val>"``
so the rendered Field / docstring shows useful text instead of ``None``."""
schema = {
"type": "object",
"properties": {"auctionStatus": {"type": "string", "example": "Results"}},
}
cls = pg.generate_class(schema, class_name="C")
src = cls.source
assert "description='Example: Results.'" in src
# And the docstring section also picks up the example
block = src.split('"""')[1]
assert "Example: Results" in block
def test_field_keeps_description_over_example_when_both_present():
schema = {
"type": "object",
"properties": {
"x": {"type": "string", "description": "Real description.", "example": "EX"}
},
}
cls = pg.generate_class(schema, class_name="C")
assert "Real description." in cls.source
assert "Example: EX" not in cls.source
def test_optional_enum_field_emits_literal_with_optional_none_suffix():
"""``{type: string, enum: [P, S]}`` becomes ``Literal['P', 'S'] | None``
so Pydantic enforces the choices at construction time. ``None``
(the default) means "no filter applied"; passing ``'P'`` / ``'S'``
constrains; passing anything else raises. The openbb-core renderer
fix in the same change set keeps ``Literal[...] | None`` from
collapsing to the bogus ``Optional[None]`` in docstrings."""
schema = {
"type": "object",
"properties": {
"operationDirection": {
"type": "string",
"enum": ["P", "S"],
"example": "P",
}
},
}
cls = pg.generate_class(schema, class_name="Auction")
src = cls.source
assert "operationDirection: Literal['P', 'S'] | None" in src
# Choices still surface in the description for user-facing context.
assert "Choices: P, S" in src
assert "Example: P" in src
def test_field_with_no_description_no_example_emits_empty_description():
"""No spec text -> ``description=''``, not omitted. Empty string keeps
openbb-core's DocstringGenerator from rendering the literal ``"None"``."""
schema = {
"type": "object",
"properties": {"numDealers": {"type": "integer"}},
}
cls = pg.generate_class(schema, class_name="C")
assert "description=''" in cls.source
assert "None" not in cls.source.split("class C")[1].split("\n", 5)[0]
def test_render_module_appends_period_to_docstring_summary():
"""Line 681: a docstring without trailing punctuation gets a period."""
schema = {"type": "object", "properties": {"x": {"type": "integer"}}}
cls = pg.generate_class(schema, class_name="P")
rendered = pg.render_module(cls, module_docstring="No trailing period")
first_line = rendered.split("\n", 1)[0]
assert first_line == '"""No trailing period."""'
# --- _enum_primitive ---
def test_enum_primitive_renders_string_values_as_literal():
imports: set[str] = set()
assert pg._enum_primitive(["A", "B"], None, imports) == "Literal['A', 'B']"
assert "from typing import Literal" in imports
def test_enum_primitive_renders_numeric_values_as_literal():
imports: set[str] = set()
assert pg._enum_primitive([1, 2, 3], None, imports) == "Literal[1, 2, 3]"
def test_enum_primitive_renders_boolean_values_as_literal():
imports: set[str] = set()
assert pg._enum_primitive([True, False], None, imports) == "Literal[True, False]"
def test_enum_primitive_falls_back_to_any_for_empty_choice_set():
"""``enum: []`` carries no information — collapse to ``Any`` rather
than emit a ``Literal[]`` that Pydantic can't construct."""
imports: set[str] = set()
out = pg._enum_primitive([], None, imports)
assert out == "Any"
assert "from typing import Any" in imports
# --- _build_field_call empty branch ---
def test_build_field_call_returns_empty_when_no_metadata_to_emit():
"""A required field with no description / alias yields no ``Field(...)``
so the caller drops the trailing ``=`` entirely."""
out = pg._build_field_call(
description=None,
default=None,
has_default=False,
required=True,
alias=None,
leading_width=10,
)
assert out == ""
# --- _nested_object_breakout ---
def test_nested_object_breakout_descends_into_oneof_variant():
"""``{oneOf: [{properties: {...}}]}`` items expose their inner fields."""
prop = {
"items": {
"oneOf": [
{"properties": {"flag": {"type": "string"}, "id": {"type": "integer"}}}
]
}
}
out = pg._nested_object_breakout(prop)
assert "flag (str)" in out
assert "id (int)" in out
assert out.startswith("Inner item fields:")
def test_nested_object_breakout_handles_non_dict_inner_schema():
"""A property whose inner schema is a stray scalar still names the field
in the breakout — it just can't render a type label for it."""
prop = {"properties": {"a": "not-a-dict", "b": {"type": "integer"}}}
out = pg._nested_object_breakout(prop)
assert "a, b (int)" in out
def test_nested_object_breakout_returns_empty_for_primitive_field():
"""No nested properties anywhere → no breakout to render."""
assert pg._nested_object_breakout({"type": "string"}) == ""
# --- _quick_schema_type ---
def test_quick_schema_type_returns_str_for_enum():
"""An ``enum`` schema labels the field as ``str`` regardless of declared type."""
assert pg._quick_schema_type({"enum": ["A", "B"]}) == "str"
def test_quick_schema_type_renders_array_with_inner_type():
assert pg._quick_schema_type({"type": "array", "items": {"type": "integer"}}) == (
"list[int]"
)
def test_quick_schema_type_array_without_items_falls_back_to_any():
assert pg._quick_schema_type({"type": "array"}) == "list[Any]"
def test_quick_schema_type_recurses_into_nested_array():
assert (
pg._quick_schema_type(
{"type": "array", "items": {"type": "array", "items": {"type": "string"}}}
)
== "list[list[str]]"
)
def test_quick_schema_type_renders_date_format():
assert pg._quick_schema_type({"type": "string", "format": "date"}) == "date"
def test_quick_schema_type_renders_datetime_format():
assert pg._quick_schema_type({"type": "string", "format": "date-time"}) == (
"datetime"
)
def test_quick_schema_type_unknown_type_falls_back_to_any():
"""A schema with no ``type`` and no other handled keys collapses to ``Any``."""
assert pg._quick_schema_type({}) == "Any"