mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 20:42:26 +00:00
* 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>
277 lines
9.3 KiB
Python
277 lines
9.3 KiB
Python
"""Session-scoped fixture that builds & installs a synthetic OpenBB extension.
|
|
|
|
Every test under ``cli/`` (both ``cli/tests/`` and ``cli/integration/``) runs
|
|
against the same generated static-package surface. The fixture writes a
|
|
self-contained extension distribution to disk, ``pip install``s it into the
|
|
current interpreter, and runs ``openbb-build`` so the static package under
|
|
``openbb_platform/core/openbb/package/`` is regenerated with the test
|
|
extension's commands baked in.
|
|
|
|
Modeled directly on ``openbb_platform/core/integration/conftest.py``; reuses
|
|
the same snapshot-and-restore approach so the production package dir is
|
|
returned to its pre-test state.
|
|
|
|
Because the static package is wired via entry points discovered at
|
|
*interpreter start*, tests that import ``openbb`` after the build must do so
|
|
in a fresh ``python`` subprocess. The ``run_in_obb`` helper does exactly
|
|
that.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import textwrap
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
CORE_DIR = REPO_ROOT / "openbb_platform" / "core"
|
|
OPENBB_DIR = CORE_DIR / "openbb"
|
|
PACKAGE_DIR = OPENBB_DIR / "package"
|
|
ASSETS_DIR = OPENBB_DIR / "assets"
|
|
|
|
EXT_NAME = "openbb-cli-fake"
|
|
EXT_PKG = "openbb_cli_fake"
|
|
|
|
|
|
PYPROJECT = """\
|
|
[project]
|
|
name = "{ext_name}"
|
|
version = "0.0.1"
|
|
requires-python = ">=3.10,<4"
|
|
dependencies = ["openbb-core"]
|
|
|
|
[project.entry-points."openbb_core_extension"]
|
|
cli_test = "{pkg}.router:router"
|
|
|
|
[build-system]
|
|
requires = ["hatchling"]
|
|
build-backend = "hatchling.build"
|
|
|
|
[tool.hatch.build.targets.wheel]
|
|
packages = ["{pkg}"]
|
|
"""
|
|
|
|
INIT_PY = '''\
|
|
"""Synthetic extension exposing deterministic commands for openbb-cli tests."""
|
|
'''
|
|
|
|
ROUTER_PY = '''\
|
|
"""Router for the openbb-cli synthetic test extension.
|
|
|
|
Exposes a small surface under ``obb.cli_test.*`` so the CLI's dispatcher,
|
|
controller, and output-adapter tests can resolve real commands through the
|
|
production ``obb`` namespace — same code path as installed extensions.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from openbb_core.app.model.obbject import OBBject
|
|
from openbb_core.app.router import Router
|
|
|
|
router = Router(prefix="", description="openbb-cli synthetic test extension.")
|
|
|
|
|
|
@router.command(methods=["GET"])
|
|
async def echo(value: str = "hi") -> OBBject[dict[str, Any]]:
|
|
"""Return the input wrapped in an OBBject — sync-style payload."""
|
|
return OBBject(results={"echo": value})
|
|
|
|
|
|
@router.command(methods=["GET"])
|
|
async def quote(symbol: str) -> OBBject[dict[str, Any]]:
|
|
"""Return a deterministic quote-shaped payload, async-style."""
|
|
return OBBject(results={"symbol": symbol, "async": True})
|
|
|
|
|
|
@router.command(methods=["GET"])
|
|
async def bomb() -> OBBject[dict[str, Any]]:
|
|
"""Raise to exercise dispatcher error isolation."""
|
|
raise RuntimeError("boom")
|
|
|
|
|
|
@router.command(methods=["GET"])
|
|
async def rows(n: int = 3) -> OBBject[list[dict[str, Any]]]:
|
|
"""Return a deterministic list payload — exercises tabular output adapters."""
|
|
return OBBject(results=[{"i": i, "v": i * 2} for i in range(n)])
|
|
'''
|
|
|
|
|
|
def _snapshot_dir(src: Path, dst: Path) -> None:
|
|
if src.exists():
|
|
shutil.copytree(src, dst, dirs_exist_ok=True)
|
|
|
|
|
|
def _restore_dir(src: Path, dst: Path) -> None:
|
|
shutil.rmtree(dst, ignore_errors=True)
|
|
if src.exists():
|
|
shutil.copytree(src, dst)
|
|
|
|
|
|
def _pip(*args: str) -> None:
|
|
subprocess.run( # noqa: S603
|
|
[sys.executable, "-m", "pip", *args],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def _subprocess_env(extra: dict[str, str] | None = None) -> dict[str, str]:
|
|
"""Build a subprocess env that opts into coverage.py auto-startup if available.
|
|
|
|
Strips ``COV_CORE_*`` so the child does not try to bootstrap via
|
|
``pytest_cov.embed.init()`` (would race with ``coverage.process_startup``
|
|
and write into the parent's locked data file).
|
|
"""
|
|
env = {k: v for k, v in os.environ.items() if not k.startswith("COV_CORE_")}
|
|
rcfile = Path(__file__).resolve().parent / ".coveragerc"
|
|
if rcfile.exists():
|
|
env["COVERAGE_PROCESS_START"] = str(rcfile)
|
|
if extra:
|
|
env.update(extra)
|
|
return env
|
|
|
|
|
|
def _run_openbb_build() -> None:
|
|
"""Trigger ``openbb-build`` via auto-build on first import in a fresh subprocess.
|
|
|
|
A child process is required because ``ExtensionLoader``/``RouterLoader``
|
|
cache entry points at module load time; the parent process cannot
|
|
discover the freshly-installed distribution without a restart.
|
|
"""
|
|
env = _subprocess_env({"OPENBB_AUTO_BUILD": "true"})
|
|
subprocess.run( # noqa: S603
|
|
[sys.executable, "-c", "import openbb"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def cli_fake_extension_installed(tmp_path_factory):
|
|
"""Build, install, openbb-build, yield, then uninstall and restore.
|
|
|
|
Auto-used at session scope so every test in ``cli/`` runs against the
|
|
same generated static package surface. The synthetic extension exposes
|
|
``obb.cli_test.{echo,quote,bomb,rows}`` for dispatcher / output / controller
|
|
tests to resolve through the real plugin-discovery path.
|
|
"""
|
|
work = tmp_path_factory.mktemp("cli_fake_ext_dist")
|
|
pkg_dir = work / EXT_PKG
|
|
pkg_dir.mkdir()
|
|
(pkg_dir / "__init__.py").write_text(INIT_PY, encoding="utf-8")
|
|
(pkg_dir / "router.py").write_text(ROUTER_PY, encoding="utf-8")
|
|
(work / "pyproject.toml").write_text(
|
|
PYPROJECT.format(ext_name=EXT_NAME, pkg=EXT_PKG), encoding="utf-8"
|
|
)
|
|
|
|
snapshot_root = tmp_path_factory.mktemp("openbb_snapshot")
|
|
pkg_snapshot = snapshot_root / "package"
|
|
assets_snapshot = snapshot_root / "assets"
|
|
_snapshot_dir(PACKAGE_DIR, pkg_snapshot)
|
|
_snapshot_dir(ASSETS_DIR, assets_snapshot)
|
|
|
|
try:
|
|
_pip("install", "--no-deps", "-e", str(work))
|
|
if str(work) not in sys.path:
|
|
sys.path.insert(0, str(work))
|
|
_run_openbb_build()
|
|
|
|
ext_pkg_module = PACKAGE_DIR / "cli_test.py"
|
|
if not ext_pkg_module.exists():
|
|
pytest.fail(
|
|
"openbb-build did not emit cli_test.py; "
|
|
f"contents of {PACKAGE_DIR}: "
|
|
f"{sorted(p.name for p in PACKAGE_DIR.glob('*.py'))}"
|
|
)
|
|
|
|
yield {
|
|
"ext_name": EXT_NAME,
|
|
"ext_pkg": EXT_PKG,
|
|
"package_dir": PACKAGE_DIR,
|
|
"assets_dir": ASSETS_DIR,
|
|
"work_dir": work,
|
|
}
|
|
finally:
|
|
with contextlib.suppress(subprocess.CalledProcessError):
|
|
_pip("uninstall", "-y", EXT_NAME)
|
|
with contextlib.suppress(ValueError):
|
|
sys.path.remove(str(work))
|
|
for mod_name in [m for m in sys.modules if m.startswith(EXT_PKG)]:
|
|
del sys.modules[mod_name]
|
|
_restore_dir(pkg_snapshot, PACKAGE_DIR)
|
|
_restore_dir(assets_snapshot, ASSETS_DIR)
|
|
_combine_partial_coverage()
|
|
|
|
|
|
def _combine_partial_coverage() -> None:
|
|
"""Merge ``.coverage.<host>.pid<N>.<rand>`` partials and erase them.
|
|
|
|
``parallel = True`` in ``.coveragerc`` makes every subprocess (the
|
|
``_run_openbb_build`` build call and every ``run_in_obb`` snippet) write a
|
|
uniquely-named partial. Without ``coverage combine`` they accumulate at
|
|
the repo root across runs. Run combine if any partial exists; otherwise
|
|
no-op so suites that don't use the subprocess path stay clean.
|
|
"""
|
|
here = Path(__file__).resolve().parent
|
|
if not list(here.glob(".coverage.*")):
|
|
return
|
|
with contextlib.suppress(subprocess.CalledProcessError, FileNotFoundError):
|
|
subprocess.run( # noqa: S603
|
|
[sys.executable, "-m", "coverage", "combine"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=str(here),
|
|
)
|
|
for partial in here.glob(".coverage.*"):
|
|
with contextlib.suppress(OSError):
|
|
partial.unlink()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def run_in_obb(cli_fake_extension_installed): # noqa: ARG001
|
|
"""Run an inline snippet under a fresh subprocess that imports ``openbb``.
|
|
|
|
Returns a callable ``run(snippet: str) -> dict``. The snippet must assign
|
|
a JSON-serializable value to ``RESULT``; the wrapper ``json.dumps`` it to
|
|
stdout and the helper parses it back into Python.
|
|
|
|
Use this for any test that needs to resolve commands via the real
|
|
generated static ``obb`` namespace — entry-point caching makes
|
|
in-process resolution unreliable after a fresh build.
|
|
"""
|
|
|
|
def _run(snippet: str) -> dict:
|
|
wrapper = textwrap.dedent("""\
|
|
import json, sys
|
|
from openbb import obb # noqa: F401
|
|
{snippet}
|
|
sys.stdout.write(json.dumps(RESULT, default=str))
|
|
""").format(snippet=textwrap.dedent(snippet).strip())
|
|
proc = subprocess.run( # noqa: S603
|
|
[sys.executable, "-c", wrapper],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
env=_subprocess_env({"OPENBB_AUTO_BUILD": "false"}),
|
|
)
|
|
if proc.returncode != 0:
|
|
raise AssertionError(
|
|
f"obb subprocess failed (rc={proc.returncode})\n"
|
|
f"--- stdout ---\n{proc.stdout}\n"
|
|
f"--- stderr ---\n{proc.stderr}"
|
|
)
|
|
return json.loads(proc.stdout)
|
|
|
|
return _run
|