Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""
Tests for tortoise.config module - TortoiseConfig class.
"""

from pathlib import Path

import orjson
import pytest
import yaml

from tortoise.backends.base.config_generator import expand_db_url
from tortoise.config import TortoiseConfig
from tortoise.exceptions import ConfigurationError


class TestTortoiseConfig:
def test_from_invalid_dict(self):
with pytest.raises(
ConfigurationError, match="TortoiseConfig must be created from a mapping"
):
TortoiseConfig.from_dict([])
with pytest.raises(ConfigurationError, match='Config must define "connections" section'):
TortoiseConfig.from_dict({})
with pytest.raises(ConfigurationError, match='Config must define "apps" section'):
TortoiseConfig.from_dict({"connections": ""})
with pytest.raises(ConfigurationError, match='Config "connections" must be a mapping'):
TortoiseConfig.from_dict({"connections": "", "apps": ""})
with pytest.raises(ConfigurationError, match="Connection values must be mapping or string"):
TortoiseConfig.from_dict({"connections": {"default": []}, "apps": ""})
with pytest.raises(ConfigurationError, match="DBUrlConfig.url must be a non-empty string"):
TortoiseConfig.from_dict({"connections": {"default": ""}, "apps": ""})
with pytest.raises(ConfigurationError, match='Config "apps" must be a mapping'):
TortoiseConfig.from_dict({"connections": {"default": "db.sqlite3"}, "apps": ""})
with pytest.raises(ConfigurationError, match="App values must be mappings"):
TortoiseConfig.from_dict(
{"connections": {"default": "db.sqlite3"}, "apps": {"auth": ""}}
)
with pytest.raises(ConfigurationError, match='AppConfig requires "models"'):
TortoiseConfig.from_dict(
{"connections": {"default": "db.sqlite3"}, "apps": {"auth": {}}, "routers": {}}
)
with pytest.raises(
ConfigurationError, match="AppConfig.models must be a non-empty list of strings"
):
TortoiseConfig.from_dict(
{
"connections": {"default": "db.sqlite3"},
"apps": {"auth": {"models": []}},
"routers": {},
}
)
with pytest.raises(
ConfigurationError, match="TortoiseConfig.routers must be a list or None"
):
TortoiseConfig.from_dict(
{
"connections": {"default": "db.sqlite3"},
"apps": {"auth": {"models": ["models"]}},
"routers": "",
}
)

def test_from_dict(self):
simple = {
"connections": {"default": "sqlite://db.sqlite3"},
"apps": {"app": {"models": ["app.models"]}},
}
assert TortoiseConfig.from_dict(simple) is not None
full = {
"connections": {
"default": "sqlite://db.sqlite3",
"second": "sqlite://db2.sqlite3",
},
"apps": {
"app1": {"models": ["app1.models"]},
"app2": {
"models": ["app2.models"],
"default_connection": "second",
},
},
"routers": ["path.Router"],
"use_tz": True,
"timezone": "UTC",
}
assert TortoiseConfig.from_dict(full) is not None

def test_from_config_file(self, tmp_path: Path):
simple = {
"connections": {"default": "sqlite://db.sqlite3"},
"apps": {"app": {"models": ["app.models"]}},
}
file = tmp_path / "tortoise_conf.json"
file.write_bytes(orjson.dumps(simple))
filename: str = file.as_posix()
assert (
TortoiseConfig.from_config_file(file)
== TortoiseConfig.from_config_file(filename)
== TortoiseConfig.from_dict(simple)
== TortoiseConfig.resolve_args(config_file=file)
)

yaml_file = file.with_suffix(".yml")
with yaml_file.open("w") as f:
yaml.safe_dump(dict(simple), f, default_flow_style=False)
yaml_file_2 = file.with_suffix(".yaml")
with yaml_file_2.open("w") as f2:
yaml.safe_dump(simple, f2, default_flow_style=False)
assert (
TortoiseConfig.from_config_file(yaml_file)
== TortoiseConfig.from_config_file(str(yaml_file))
== TortoiseConfig.from_config_file(yaml_file_2)
== TortoiseConfig.from_config_file(file)
== TortoiseConfig.resolve_args(config_file=yaml_file)
)

def test_from_db_url_and_modules(self):
simple = {
"connections": {"default": "sqlite://db.sqlite3"},
"apps": {
"app": {
"models": ["app.models"],
"default_connection": "default",
}
},
}
db_url = simple["connections"]["default"]
modules = {"app": simple["apps"]["app"]["models"]}
typed_config = TortoiseConfig.from_db_url_and_modules(db_url, modules)
assert typed_config == TortoiseConfig.resolve_args(db_url=db_url, modules=modules)
assert typed_config.apps == TortoiseConfig.from_dict(simple).apps

def test_resolve_args(self, tmp_path: Path):
with pytest.raises(
ConfigurationError,
match="Must provide either 'config', 'config_file', or both 'db_url' and 'modules'",
):
TortoiseConfig.resolve_args()
with pytest.raises(
ConfigurationError,
match="Must provide either 'config', 'config_file', or both 'db_url' and 'modules'",
):
TortoiseConfig.resolve_args(db_url="")
with pytest.raises(
ConfigurationError, match="Cannot specify both 'config' and 'config_file'"
):
TortoiseConfig.resolve_args(config={}, config_file="a.json")
db_url = "sqlite://db.sqlite3"
config = {
"connections": {"default": db_url},
"apps": {
"app": {
"models": ["app.models"],
"default_connection": "default",
}
},
}
config_file = tmp_path / "config.json"
config_file.write_bytes(orjson.dumps(config))
typed_config = TortoiseConfig.resolve_args(config)
assert typed_config == TortoiseConfig.resolve_args(config_file=config_file)

typed_config_2 = TortoiseConfig.resolve_args(db_url=db_url, modules={"app": ["app.models"]})
assert typed_config.apps == typed_config_2.apps
assert (
expand_db_url(str(typed_config.connections["default"].to_config()))
== typed_config_2.connections["default"].to_config()
)
41 changes: 18 additions & 23 deletions tortoise/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from __future__ import annotations

import json
import os
from collections.abc import Mapping
from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any

from tortoise.backends.base.config_generator import generate_config
Expand Down Expand Up @@ -217,32 +217,32 @@ def from_dict(cls, data: Mapping[str, Any]) -> Self:
)

@classmethod
def from_config_file(cls, config_file: str) -> Self:
def from_config_file(cls, config_file: Path | str) -> Self:
"""
Load configuration from a YAML or JSON file.

Args:
config_file (str): Path to the configuration file. Supported extensions: .yml, .yaml, .json.
config_file: Path to the configuration file. Supported extensions: .yml, .yaml, .json.

Returns:
Self: The constructed TortoiseConfig.

Raises:
ConfigurationError: If the file is missing, unsupported, or contents are invalid.
"""
_, extension = os.path.splitext(config_file)
if extension in (".yml", ".yaml"):
import yaml # pylint: disable=C0415

with open(config_file) as f:
config = yaml.safe_load(f)
elif extension == ".json":
with open(config_file) as f:
config = json.load(f)
else:
raise ConfigurationError(
f"Unknown config extension {extension}, only .yml and .json are supported"
)
config_path = Path(config_file)
match config_path.suffix:
case ".yml" | ".yaml":
import yaml # pylint: disable=C0415

with open(config_file) as f:
config = yaml.safe_load(f)
case ".json":
config = json.loads(config_path.read_bytes())
case _ as extension:
raise ConfigurationError(
f"Unknown config extension {extension}, only .yml and .json are supported"
)
return cls.from_dict(config)

@classmethod
Expand Down Expand Up @@ -274,7 +274,7 @@ def from_db_url_and_modules(
def resolve_args(
cls,
config: dict[str, Any] | Self | None = None,
config_file: str | None = None,
config_file: Path | str | None = None,
db_url: str | None = None,
modules: dict[str, Iterable[str | ModuleType]] | None = None,
) -> Self:
Expand All @@ -286,14 +286,9 @@ def resolve_args(
- `config_file` path,
- or both `db_url` and `modules`.

Args:
config (dict[str, Any] | TortoiseConfig | None):
config_file (str | None): Path to a config YAML or JSON file.
db_url (str | None): Database URL for config generation.
modules (dict[str, Iterable[str | ModuleType]] | None): App modules for config generation.
Args:
config: A configuration dict or TortoiseConfig instance.
config_file: Path to config file.
config_file: Path to a config YAML or JSON file.
db_url: Database URL for config generation.
modules: App modules for config generation.

Expand Down
Loading