41
41
SCHEMA_TYPE_DATACLASS = "dataclass"
42
42
SCHEMA_TYPE_DICT = "dict"
43
43
SchemaType = Literal [SCHEMA_TYPE_PYDANTIC , SCHEMA_TYPE_DATACLASS , SCHEMA_TYPE_DICT ]
44
+ ConfigPath = str
44
45
45
46
46
47
class ConfigModel :
@@ -272,7 +273,8 @@ def __init__(
272
273
config_name : str ,
273
274
config_schema : Type [T ], # Supports pydantic.BaseModel, dataclass, or dict
274
275
version : str = "1.0.0" ,
275
- auto_create : bool = True ,
276
+ auto_create_user : bool = False ,
277
+ auto_create_project : bool = False ,
276
278
):
277
279
"""
278
280
Initialize the configuration manager
@@ -282,12 +284,15 @@ def __init__(
282
284
config_schema: Configuration schema definition (pydantic model, dataclass,
283
285
or dict)
284
286
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.
286
290
"""
287
291
self .config_name : str = config_name
288
292
self .config_schema : Type [T ] = config_schema
289
293
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
291
296
self .schema_type : SchemaType
292
297
293
298
if (
@@ -326,18 +331,11 @@ def __init__(
326
331
config_name , str (self .project_root ) if self .project_root else None
327
332
)
328
333
329
- if auto_create :
330
- self ._ensure_user_config_exists ()
334
+ if auto_create_user :
335
+ self .create_user_config_template ()
331
336
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 ()
341
339
342
340
def _get_default_dict (self ) -> Dict [str , Any ]:
343
341
"""Get default configuration as a dictionary"""
@@ -355,14 +353,46 @@ def load(self) -> T:
355
353
356
354
Returns:
357
355
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
358
360
"""
359
361
default_config_model : ConfigModel = ConfigModel .from_schema (self .config_schema )
360
362
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
+
362
393
user_config_model , _ = self ._load_config_from_path (self .user_config_path )
363
394
project_config_model , _ = self ._load_config_from_path (self .project_config_path )
364
395
365
- # Merge configurations using ConfigModel
366
396
merged_config_model = default_config_model
367
397
if user_config_model :
368
398
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:
542
572
self ._config = None
543
573
self ._config_model = None
544
574
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
546
580
"""
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
548
593
549
594
Args:
550
595
path: Optional project path, defaults to current directory
@@ -566,6 +611,19 @@ def create_project_template(self, path: Optional[str] = None) -> str:
566
611
567
612
return str (config_file )
568
613
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
+
569
627
570
628
def get_user_config_path (config_name : str ) -> Path :
571
629
"""Get the path to the user-level configuration file"""
@@ -588,25 +646,22 @@ def get_project_config_path(
588
646
return project_root / f".{ config_name } " / "config.yaml"
589
647
590
648
649
+ # TODO: need to optimize
591
650
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
594
652
like .git, pyproject.toml, etc.
595
653
"""
596
654
cwd = Path .cwd ()
597
655
598
- # Common project root indicators
599
656
indicators = [".git" , "pyproject.toml" , "setup.py" , "package.json" , "Cargo.toml" ]
600
657
601
- # Check current directory and parents until root
602
658
current = cwd
603
- while current .parent != current : # Stop at system root
659
+ while current .parent != current :
604
660
for indicator in indicators :
605
661
if (current / indicator ).exists ():
606
662
return current
607
663
current = current .parent
608
664
609
- # No project root found
610
665
return None
611
666
612
667
@@ -615,8 +670,7 @@ def merge_configs_dict(
615
670
user_config : Dict [str , Any ],
616
671
project_config : Dict [str , Any ],
617
672
) -> Dict [str , Any ]:
618
- """
619
- Merge multiple configuration levels
673
+ """Merge multiple configuration levels
620
674
621
675
Args:
622
676
default_config: Default configuration
@@ -642,8 +696,7 @@ def merge_configs_dict(
642
696
def deep_update (
643
697
base_dict : Dict [str , Any ], update_dict : Dict [str , Any ]
644
698
) -> Dict [str , Any ]:
645
- """
646
- Recursively update a dictionary
699
+ """Recursively update a dictionary
647
700
648
701
Args:
649
702
base_dict: The base dictionary to update
0 commit comments