Files
OpenBB/cookiecutter/tests/test_template.py
Danglewood 869cfed125 [Release] openbb-v5.0.0 (#7741)
* merge branch develop with main

* bump desktop version

* fix the merge

* sync Cargo.lock to desktop 2.0.0

* fix cli conftest.py

---------

Co-authored-by: deeleeramone <>
2026-09-29 10:58:44 -07:00

214 lines
7.3 KiB
Python

"""Tests for the project template."""
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
from cookiecutter.exceptions import FailedHookException
from cookiecutter.main import cookiecutter
from openbb_cookiecutter import get_template_path
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
PACKAGE = "extension_template"
def generate(tmp_path: Path, extension_types: str, **context: str) -> Path:
return Path(
cookiecutter(
str(get_template_path()),
output_dir=str(tmp_path),
no_input=True,
extra_context={"extension_types": extension_types, **context},
)
)
def files(project: Path) -> set[str]:
return {
p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()
}
def entry_points(project: Path) -> dict:
return tomllib.loads((project / "pyproject.toml").read_text(encoding="utf-8"))[
"project"
].get("entry-points", {})
class TestGeneratedFiles:
"""Each extension type generates only its own modules and tests."""
@pytest.mark.parametrize(
("extension_types", "expected"),
[
(
"router",
{
f"{PACKAGE}/routers/template.py",
"tests/test_router.py",
},
),
(
"provider",
{
f"{PACKAGE}/providers/template/models/example.py",
f"{PACKAGE}/providers/template/models/equity_historical.py",
"tests/test_provider.py",
},
),
(
"obbject",
{
f"{PACKAGE}/obbject/template_ext/__init__.py",
"tests/test_obbject.py",
},
),
(
"on_command_output",
{
f"{PACKAGE}/obbject/template_ext/on_command_output.py",
"tests/test_on_command_output.py",
},
),
(
"charting",
{
f"{PACKAGE}/routers/template_views.py",
"tests/test_views.py",
},
),
],
)
def test_single_type(self, tmp_path, extension_types, expected):
generated = files(generate(tmp_path, extension_types))
optional = {
f"{PACKAGE}/routers/template.py",
f"{PACKAGE}/routers/template_views.py",
f"{PACKAGE}/providers/template/models/example.py",
f"{PACKAGE}/obbject/template_ext/on_command_output.py",
"tests/test_router.py",
"tests/test_views.py",
"tests/test_provider.py",
"tests/test_obbject.py",
"tests/test_on_command_output.py",
}
assert expected <= generated
assert not (optional - expected) & generated
def test_all_types(self, tmp_path):
generated = files(generate(tmp_path, "all"))
assert {
"pyproject.toml",
"README.md",
".gitignore",
f"{PACKAGE}/routers/template.py",
f"{PACKAGE}/routers/template_views.py",
f"{PACKAGE}/providers/template/__init__.py",
f"{PACKAGE}/obbject/template_ext/__init__.py",
f"{PACKAGE}/obbject/template_ext/on_command_output.py",
"tests/test_router.py",
"tests/test_views.py",
"tests/test_provider.py",
"tests/test_obbject.py",
"tests/test_on_command_output.py",
} <= generated
def test_local_artifacts_are_not_copied(self, tmp_path):
generated = files(generate(tmp_path, "all"))
artifacts = (".ruff_cache", ".ipynb_checkpoints", "__pycache__", ".DS_Store")
assert not [f for f in generated if any(a in f for a in artifacts)]
class TestPyproject:
"""The generated pyproject declares V5 packaging and entry points."""
def test_build_and_dependencies(self, tmp_path):
pyproject = tomllib.loads(
(generate(tmp_path, "all") / "pyproject.toml").read_text(encoding="utf-8")
)
assert pyproject["build-system"]["build-backend"] == "hatchling.build"
assert pyproject["project"]["dependencies"] == ["openbb-core[pandas]>=2.0.0"]
assert pyproject["project"]["optional-dependencies"] == {
"charting": ["openbb-charting>=3.0.0"]
}
assert "openbb-devtools>=2.0.1" in pyproject["dependency-groups"]["dev"]
def test_entry_points_for_all_types(self, tmp_path):
assert entry_points(generate(tmp_path, "all")) == {
"openbb_core_extension": {
"template": f"{PACKAGE}.routers.template:router",
},
"openbb_charting_extension": {
"template": f"{PACKAGE}.routers.template_views:TemplateViews",
},
"openbb_provider_extension": {
"template": f"{PACKAGE}.providers.template:template_provider",
},
"openbb_obbject_extension": {
"to_string": f"{PACKAGE}.obbject.template_ext:ext",
"template_ext": f"{PACKAGE}.obbject.template_ext:class_ext",
"nonblocking_plugin": (
f"{PACKAGE}.obbject.template_ext.on_command_output:nonblocking_plugin"
),
},
}
def test_entry_points_for_router_only(self, tmp_path):
assert entry_points(generate(tmp_path, "router")) == {
"openbb_core_extension": {
"template": f"{PACKAGE}.routers.template:router",
},
}
class TestHooks:
"""The post-generation hook validates names."""
def test_rejects_invalid_package_name(self, tmp_path):
with pytest.raises(FailedHookException):
generate(tmp_path, "router", package_name="1-bad")
def test_rejects_shared_provider_and_obbject_name(self, tmp_path):
with pytest.raises(FailedHookException):
generate(tmp_path, "all", obbject_name="template")
@pytest.mark.skipif(shutil.which("ruff") is None, reason="ruff is not installed")
class TestGeneratedCodeQuality:
"""Generated projects pass their own lint and type checks."""
@pytest.mark.parametrize(
"extension_types",
["router", "provider", "obbject", "on_command_output", "charting", "all"],
)
def test_ruff(self, tmp_path, extension_types):
project = generate(tmp_path, extension_types)
for command in (["format", "--check"], ["check", "--no-fix"]):
result = subprocess.run(
["ruff", *command, "--no-cache", "."],
cwd=project,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stdout + result.stderr
@pytest.mark.skipif(shutil.which("ty") is None, reason="ty is not installed")
def test_ty(self, tmp_path):
project = generate(tmp_path, "all")
result = subprocess.run(
["ty", "check", "--python", sys.executable, PACKAGE],
cwd=project,
capture_output=True,
text=True,
check=False,
)
assert result.returncode == 0, result.stdout + result.stderr