Skip to content

Commit f5518df

Browse files
committed
feat: update ConfigManager API Design
1 parent 0367be9 commit f5518df

File tree

8 files changed

+1292
-644
lines changed

8 files changed

+1292
-644
lines changed

assets/images/coverage.svg

+2-2
Loading

conftier/core.py

+81-28
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
SCHEMA_TYPE_DATACLASS = "dataclass"
4242
SCHEMA_TYPE_DICT = "dict"
4343
SchemaType = Literal[SCHEMA_TYPE_PYDANTIC, SCHEMA_TYPE_DATACLASS, SCHEMA_TYPE_DICT]
44+
ConfigPath = str
4445

4546

4647
class ConfigModel:
@@ -272,7 +273,8 @@ def __init__(
272273
config_name: str,
273274
config_schema: Type[T], # Supports pydantic.BaseModel, dataclass, or dict
274275
version: str = "1.0.0",
275-
auto_create: bool = True,
276+
auto_create_user: bool = False,
277+
auto_create_project: bool = False,
276278
):
277279
"""
278280
Initialize the configuration manager
@@ -282,12 +284,15 @@ def __init__(
282284
config_schema: Configuration schema definition (pydantic model, dataclass,
283285
or dict)
284286
version: Configuration schema version
285-
auto_create: Whether to automatically create default config files
287+
auto_create_user: Whether to automatically create user config file if not
288+
exists auto_create_project: Whether to automatically create project config
289+
file if not exists.
286290
"""
287291
self.config_name: str = config_name
288292
self.config_schema: Type[T] = config_schema
289293
self.version: str = version
290-
self.auto_create: bool = auto_create
294+
self.auto_create_user: bool = auto_create_user
295+
self.auto_create_project: bool = auto_create_project
291296
self.schema_type: SchemaType
292297

293298
if (
@@ -326,18 +331,11 @@ def __init__(
326331
config_name, str(self.project_root) if self.project_root else None
327332
)
328333

329-
if auto_create:
330-
self._ensure_user_config_exists()
334+
if auto_create_user:
335+
self.create_user_config_template()
331336

332-
def _ensure_user_config_exists(self) -> None:
333-
"""Ensure user config file and directory exist"""
334-
if not self.user_config_path.parent.exists():
335-
self.user_config_path.parent.mkdir(parents=True, exist_ok=True)
336-
337-
if not self.user_config_path.exists():
338-
default_config = self._get_default_dict()
339-
with open(self.user_config_path, "w") as f:
340-
yaml.dump(default_config, f, default_flow_style=False, sort_keys=False)
337+
if auto_create_project and self.project_config_path:
338+
self.create_project_config_template()
341339

342340
def _get_default_dict(self) -> Dict[str, Any]:
343341
"""Get default configuration as a dictionary"""
@@ -355,14 +353,46 @@ def load(self) -> T:
355353
356354
Returns:
357355
Merged final configuration object (same type as schema)
356+
357+
Raises:
358+
FileNotFoundError: If both user and project configuration files don't exist
359+
and auto_create options are disabled
358360
"""
359361
default_config_model: ConfigModel = ConfigModel.from_schema(self.config_schema)
360362

361-
# Load user and project configs using helper method
363+
user_config_exists = self.user_config_path.exists()
364+
project_config_exists = (
365+
self.project_config_path and self.project_config_path.exists()
366+
)
367+
368+
if not user_config_exists and self.auto_create_user:
369+
self.create_user_config_template()
370+
user_config_exists = True
371+
372+
if (
373+
self.project_config_path
374+
and not project_config_exists
375+
and self.auto_create_project
376+
):
377+
self.create_project_config_template()
378+
project_config_exists = True
379+
380+
if not user_config_exists and not project_config_exists:
381+
error_message = "Configuration files not found. "
382+
383+
if self.project_config_path:
384+
error_message += (
385+
f"Project config missing at {self.project_config_path}. "
386+
)
387+
388+
error_message += f"User config missing at {self.user_config_path}. "
389+
error_message += "Use create_user_config_template() or create_project_config_template() to create them, " # noqa
390+
error_message += "or set auto_create_user=True or auto_create_project=True."
391+
raise FileNotFoundError(error_message)
392+
362393
user_config_model, _ = self._load_config_from_path(self.user_config_path)
363394
project_config_model, _ = self._load_config_from_path(self.project_config_path)
364395

365-
# Merge configurations using ConfigModel
366396
merged_config_model = default_config_model
367397
if user_config_model:
368398
merged_config_model = merged_config_model.merge(user_config_model)
@@ -542,9 +572,24 @@ def update_project_config(self, config_update: Dict[str, Any]) -> None:
542572
self._config = None
543573
self._config_model = None
544574

545-
def create_project_template(self, path: Optional[str] = None) -> str:
575+
def create_user_config_template(self) -> ConfigPath:
576+
"""Create a user configuration template if it doesn't exist
577+
578+
Returns:
579+
Path to the created configuration file
546580
"""
547-
Create a project configuration template
581+
if not self.user_config_path.parent.exists():
582+
self.user_config_path.parent.mkdir(parents=True, exist_ok=True)
583+
584+
if not self.user_config_path.exists():
585+
default_config = self._get_default_dict()
586+
with open(self.user_config_path, "w") as f:
587+
yaml.dump(default_config, f, default_flow_style=False, sort_keys=False)
588+
589+
return str(self.user_config_path)
590+
591+
def create_project_config_template(self, path: Optional[str] = None) -> ConfigPath:
592+
"""Create a project configuration template
548593
549594
Args:
550595
path: Optional project path, defaults to current directory
@@ -566,6 +611,19 @@ def create_project_template(self, path: Optional[str] = None) -> str:
566611

567612
return str(config_file)
568613

614+
def create_project_template(self, path: Optional[str] = None) -> ConfigPath:
615+
"""Create a project configuration template (deprecated)
616+
617+
Use create_project_config_template instead.
618+
619+
Args:
620+
path: Optional project path, defaults to current directory
621+
622+
Returns:
623+
Path to the created configuration file
624+
"""
625+
return self.create_project_config_template(path)
626+
569627

570628
def get_user_config_path(config_name: str) -> Path:
571629
"""Get the path to the user-level configuration file"""
@@ -588,25 +646,22 @@ def get_project_config_path(
588646
return project_root / f".{config_name}" / "config.yaml"
589647

590648

649+
# TODO: need to optimize
591650
def find_project_root() -> Optional[Path]:
592-
"""
593-
Find the project root directory by looking for common project files
651+
"""Find the project root directory by looking for common project files
594652
like .git, pyproject.toml, etc.
595653
"""
596654
cwd = Path.cwd()
597655

598-
# Common project root indicators
599656
indicators = [".git", "pyproject.toml", "setup.py", "package.json", "Cargo.toml"]
600657

601-
# Check current directory and parents until root
602658
current = cwd
603-
while current.parent != current: # Stop at system root
659+
while current.parent != current:
604660
for indicator in indicators:
605661
if (current / indicator).exists():
606662
return current
607663
current = current.parent
608664

609-
# No project root found
610665
return None
611666

612667

@@ -615,8 +670,7 @@ def merge_configs_dict(
615670
user_config: Dict[str, Any],
616671
project_config: Dict[str, Any],
617672
) -> Dict[str, Any]:
618-
"""
619-
Merge multiple configuration levels
673+
"""Merge multiple configuration levels
620674
621675
Args:
622676
default_config: Default configuration
@@ -642,8 +696,7 @@ def merge_configs_dict(
642696
def deep_update(
643697
base_dict: Dict[str, Any], update_dict: Dict[str, Any]
644698
) -> Dict[str, Any]:
645-
"""
646-
Recursively update a dictionary
699+
"""Recursively update a dictionary
647700
648701
Args:
649702
base_dict: The base dictionary to update

docs/.vitepress/config.mts

+12
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,24 @@ export default withPwa(
130130
{ text: "Home", link: "/" },
131131
{ text: "Get Started", link: "/guide/introduction" },
132132
],
133+
socialLinks: [
134+
{ icon: "github", link: "https://github.com/undertone0809/conftier" },
135+
{ icon: "x", link: "https://x.com/kfhedRk3lXofRIB" },
136+
],
133137
sidebar: [
134138
{
135139
text: "Get Started",
136140
items: [
137141
{ text: "Introduction", link: "/guide/introduction" },
138142
{ text: "Quick Start", link: "/guide/quick-start" },
143+
{
144+
text: "Framework Developer Journey",
145+
link: "/guide/framework-journey",
146+
},
147+
{
148+
text: "Application Developer Journey",
149+
link: "/guide/application-journey",
150+
},
139151
],
140152
},
141153
{

0 commit comments

Comments
 (0)