diff --git a/cli/openbb_cli/codegen/router_gen.py b/cli/openbb_cli/codegen/router_gen.py index 474c16b3fe9..648eeffa903 100644 --- a/cli/openbb_cli/codegen/router_gen.py +++ b/cli/openbb_cli/codegen/router_gen.py @@ -2,6 +2,7 @@ from __future__ import annotations +import keyword import re from dataclasses import dataclass, field from typing import TYPE_CHECKING @@ -56,9 +57,18 @@ class GeneratedRouters: def _safe_segment(name: str) -> str: - """Lower-case + replace non-alphanumerics with underscores.""" - cleaned = re.sub(r"[^0-9a-zA-Z]+", "_", name).strip("_") - return cleaned.lower() or "ns" + """Lower-case + replace non-alphanumerics with underscores. + + A result that starts with a digit gets a leading underscore and a Python + keyword gets a trailing one (as pydantic_gen does for field names), so it + stays a valid function and module identifier. + """ + cleaned = (re.sub(r"[^0-9a-zA-Z]+", "_", name).strip("_") or "ns").lower() + if cleaned[0].isdigit(): + cleaned = f"_{cleaned}" + elif keyword.iskeyword(cleaned): + cleaned = f"{cleaned}_" + return cleaned def _module_name_for(path: str) -> str: diff --git a/cli/tests/test_codegen_router_gen.py b/cli/tests/test_codegen_router_gen.py index 330b6a0fa3b..11f55459008 100644 --- a/cli/tests/test_codegen_router_gen.py +++ b/cli/tests/test_codegen_router_gen.py @@ -407,3 +407,42 @@ def test_generate_routers_command_that_is_also_a_namespace_lives_in_its_own_rout assert "async def count(" in by_module["alerts_active"].source for r in out.routers: ast.parse(r.source) + + +def test_safe_segment_prefixes_digits_and_suffixes_keywords(): + assert rg._safe_segment("24hr") == "_24hr" + assert rg._safe_segment("class") == "class_" + assert rg._safe_segment("Return") == "return_" + + +def test_generate_routers_digit_and_keyword_commands_are_valid_python(): + tree = nt.build_namespace_tree( + { + "ticker.24hr": {"providers": ["fmp"], "description": "24h stats"}, + "ticker.class": {"providers": ["fmp"], "description": "Class"}, + "24hr": {"providers": ["fmp"], "description": "Root 24h"}, + } + ) + fetchers = { + key: _Fetcher( + module_name=key.replace(".", "_"), + fetcher_class="F", + model_name="M", + ) + for key in ("ticker.24hr", "ticker.class", "24hr") + } + out = rg.generate_routers( + tree, + package_name="openbb_codegen", + provider_name="fmp", + fetchers_by_command=fetchers, + post_commands_by_command={}, + ) + assert out.routers + for router in out.routers: + ast.parse(router.source) + for block in out.root_commands: + ast.parse(block) + generated = "".join(r.source for r in out.routers) + "".join(out.root_commands) + assert "async def _24hr(" in generated + assert "async def class_(" in generated