1- import os
2- import shutil
3- from dataclasses import dataclass , asdict
1+ from __future__ import annotations
42
5- import yaml
3+ import os
4+ from pathlib import Path
5+ from dataclasses import dataclass , asdict , field , MISSING
66
7+ from pfund .utils .utils import load_yaml_file , dump_yaml_file
78from pfund_plot .const .paths import (
89 PROJ_NAME ,
10+ MAIN_PATH ,
911 DATA_PATH ,
1012 CACHE_PATH ,
11- CONFIG_PATH ,
1213 CONFIG_FILE_PATH
1314)
1415
2324class Configuration :
2425 data_path : str = str (DATA_PATH )
2526 cache_path : str = str (CACHE_PATH )
27+ static_dirs : dict [str , str ] = field (default_factory = dict )
2628
2729 _instance = None
2830 _verbose = False
31+
32+ # REVIEW: this won't be needed if we use pydantic.BaseModel instead of dataclass
33+ def _enforce_types (self ):
34+ config_dict = asdict (self )
35+ for k , v in config_dict .items ():
36+ _field = self .__dataclass_fields__ [k ]
37+ if _field .type == 'Path' and isinstance (v , str ):
38+ setattr (self , k , Path (v ))
2939
3040 @classmethod
3141 def get_instance (cls ):
3242 if cls ._instance is None :
43+ cls ._load_env_file ()
3344 cls ._instance = cls .load ()
3445 return cls ._instance
3546
3647 @classmethod
3748 def set_verbose (cls , verbose : bool ):
3849 cls ._verbose = verbose
3950
51+ @classmethod
52+ def _load_env_file (cls ):
53+ from dotenv import find_dotenv , load_dotenv
54+ env_file_path = find_dotenv (usecwd = True , raise_error_if_not_found = False )
55+ if env_file_path :
56+ load_dotenv (env_file_path , override = True )
57+ if cls ._verbose :
58+ print (f'{ PROJ_NAME } .env file loaded from { env_file_path } ' )
59+ else :
60+ if cls ._verbose :
61+ print (f'{ PROJ_NAME } .env file is not found' )
62+
4063 @classmethod
41- def load (cls ):
64+ def load (cls ) -> Configuration :
4265 '''Loads user's config file and returns a Configuration object'''
4366 CONFIG_FILE_PATH .parent .mkdir (parents = True , exist_ok = True )
4467 # Create default config from dataclass fields
45- default_config = {
46- field .name : field .default
47- for field in cls .__dataclass_fields__ .values ()
48- if not field .name .startswith ('_' ) # Skip private fields
49- }
68+ default_config = {}
69+ for _field in cls .__dataclass_fields__ .values ():
70+ if _field .name .startswith ('_' ): # Skip private fields
71+ continue
72+ if _field .default_factory is not MISSING :
73+ default_config [_field .name ] = _field .default_factory ()
74+ else :
75+ default_config [_field .name ] = _field .default
5076 needs_update = False
5177 if CONFIG_FILE_PATH .is_file ():
52- with open (CONFIG_FILE_PATH , 'r' ) as f :
53- saved_config = yaml .safe_load (f ) or {}
54- if cls ._verbose :
55- print (f"{ PROJ_NAME } config loaded from { CONFIG_FILE_PATH } ." )
56- # Check for new or removed fields
57- new_fields = set (default_config .keys ()) - set (saved_config .keys ())
58- removed_fields = set (saved_config .keys ()) - set (default_config .keys ())
59- needs_update = bool (new_fields or removed_fields )
60-
61- if cls ._verbose and needs_update :
62- if new_fields :
63- print (f"New config fields detected: { new_fields } " )
64- if removed_fields :
65- print (f"Removed config fields detected: { removed_fields } " )
66-
67- # Filter out removed fields and merge with defaults
68- saved_config = {k : v for k , v in saved_config .items () if k in default_config }
69- config = {** default_config , ** saved_config }
78+ current_config = load_yaml_file (CONFIG_FILE_PATH ) or {}
79+ if cls ._verbose :
80+ print (f"Loaded { CONFIG_FILE_PATH } " )
81+ # Check for new or removed fields
82+ new_fields = set (default_config .keys ()) - set (current_config .keys ())
83+ removed_fields = set (current_config .keys ()) - set (default_config .keys ())
84+ needs_update = bool (new_fields or removed_fields )
85+
86+ if cls ._verbose and needs_update :
87+ if new_fields :
88+ print (f"New config fields detected: { new_fields } " )
89+ if removed_fields :
90+ print (f"Removed config fields detected: { removed_fields } " )
91+
92+ # Filter out removed fields and merge with defaults
93+ current_config = {k : v for k , v in current_config .items () if k in default_config }
94+ config = {** default_config , ** current_config }
7095 else :
7196 config = default_config
7297 needs_update = True
73- config_handler = cls (** config )
98+ config = cls (** config )
7499 if needs_update :
75- config_handler .dump ()
76- return config_handler
77-
78- @classmethod
79- def reset (cls ):
80- '''Resets the config by deleting the user config directory and reloading the config'''
81- shutil .rmtree (CONFIG_PATH )
82- if cls ._verbose :
83- print (f"{ PROJ_NAME } config successfully reset." )
84- return cls .load ()
100+ config .dump ()
101+ return config
85102
86103 def dump (self ):
87- with open (CONFIG_FILE_PATH , 'w' ) as f :
88- yaml .dump (asdict (self ), f , default_flow_style = False )
89- if self ._verbose :
90- print (f"{ PROJ_NAME } config saved to { CONFIG_FILE_PATH } ." )
104+ dump_yaml_file (CONFIG_FILE_PATH , asdict (self ))
105+ if self ._verbose :
106+ print (f"Created { CONFIG_FILE_PATH } " )
107+
108+ @property
109+ def file_path (self ):
110+ return CONFIG_FILE_PATH
91111
92112 def __post_init__ (self ):
93- self ._initialize_configs ()
113+ self ._initialize ()
94114
95- def _initialize_configs (self ):
115+ def _initialize (self ):
116+ self ._enforce_types ()
117+ self ._initialize_static_dirs ()
118+ self ._initialize_file_paths ()
119+
120+ def _initialize_static_dirs (self ):
121+ if 'assets' not in self .static_dirs :
122+ self .static_dirs ['assets' ] = str ((Path (MAIN_PATH ) / "ui" / "static" ))
123+
124+ def _initialize_file_paths (self ):
96125 for path in [self .data_path , self .cache_path ]:
97126 if not os .path .exists (path ):
98127 os .makedirs (path )
@@ -103,12 +132,14 @@ def _initialize_configs(self):
103132def configure (
104133 data_path : str | None = None ,
105134 cache_path : str | None = None ,
135+ static_dirs : dict [str , str ] | None = None ,
106136 verbose : bool = False ,
107137 write : bool = False ,
108138):
109139 '''Configures the global config object.
110140 It will override the existing config values from the existing config file or the default values.
111141 Args:
142+ static_dirs: a dict of static directories to be used in pn.serve(static_dirs=...)
112143 write: If True, the config will be saved to the config file.
113144 '''
114145 NON_CONFIG_KEYS = ['verbose' , 'write' ]
@@ -117,7 +148,11 @@ def configure(
117148 config_updates .pop (k )
118149 config_updates .pop ('NON_CONFIG_KEYS' )
119150
120- config = get_config (verbose = verbose )
151+ static_dirs = static_dirs or {}
152+ assert 'assets' not in static_dirs , "'assets' is a reserved key in static_dirs"
153+
154+ Configuration .set_verbose (verbose )
155+ config = get_config ()
121156
122157 # Apply updates for non-None values
123158 for k , v in config_updates .items ():
@@ -127,10 +162,9 @@ def configure(
127162 if write :
128163 config .dump ()
129164
130- config ._initialize_configs ()
165+ config ._initialize ()
131166 return config
132167
133168
134- def get_config (verbose : bool = False ) -> Configuration :
135- Configuration .set_verbose (verbose )
169+ def get_config () -> Configuration :
136170 return Configuration .get_instance ()
0 commit comments