Skip to content
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):
Comment thread
waketzheng marked this conversation as resolved.
Outdated
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
Comment thread
waketzheng marked this conversation as resolved.
Outdated

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)
Comment thread
waketzheng marked this conversation as resolved.
Outdated
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)
)
Comment thread
waketzheng marked this conversation as resolved.

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
Comment thread
waketzheng marked this conversation as resolved.
Outdated

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")
Comment thread
waketzheng marked this conversation as resolved.
Outdated
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