99from dateutil import parser as dateparser
1010from typeguard import typechecked
1111
12- from dj_toml_settings .exceptions import InvalidActionError
12+ from dj_toml_settings .value_parsers .dict_value_parsers import (
13+ EnvValueParser ,
14+ InsertValueParser ,
15+ NoneValueParser ,
16+ PathValueParser ,
17+ TypeValueParser ,
18+ ValueValueParser ,
19+ )
1320
1421logger = logging .getLogger (__name__ )
1522
1623
1724@typechecked
18- def parse_file (path : Path , data : dict | None = None ):
19- """Parse data from the specified TOML file to use for Django settings.
20-
21- The sections get parsed in the following order with the later sections overriding the earlier:
22- 1. `[tool.django]`
23- 2. `[tool.django.apps.*]`
24- 3. `[tool.django.envs.{ENVIRONMENT}]` where {ENVIRONMENT} is defined in the `ENVIRONMENT` env variable
25- """
26-
27- toml_data = get_data (path )
28- data = data or {}
29-
30- # Get potential settings from `tool.django.apps` and `tool.django.envs`
31- apps_data = toml_data .pop ("apps" , {})
32- envs_data = toml_data .pop ("envs" , {})
33-
34- # Add default settings from `tool.django`
35- for key , value in toml_data .items ():
36- logger .debug (f"tool.django: Update '{ key } ' with '{ value } '" )
37-
38- data .update (parse_key_value (data , key , value , path ))
39-
40- # Add settings from `tool.django.apps.*`
41- for apps_name , apps_value in apps_data .items ():
42- for app_key , app_value in apps_value .items ():
43- logger .debug (f"tool.django.apps.{ apps_name } : Update '{ app_key } ' with '{ app_value } '" )
44-
45- data .update (parse_key_value (data , app_key , app_value , path ))
46-
47- # Add settings from `tool.django.envs.*` if it matches the `ENVIRONMENT` env variable
48- if environment_env_variable := os .getenv ("ENVIRONMENT" ):
49- for envs_name , envs_value in envs_data .items ():
50- if environment_env_variable == envs_name :
51- for env_key , env_value in envs_value .items ():
52- logger .debug (f"tool.django.envs.{ envs_name } : Update '{ env_key } ' with '{ env_value } '" )
53-
54- data .update (parse_key_value (data , env_key , env_value , path ))
55-
56- return data
57-
58-
59- @typechecked
60- def get_data (path : Path ) -> dict :
61- """Gets the data from the passed-in TOML file."""
62-
63- data = {}
64-
65- try :
66- data = toml .load (path )
67- except FileNotFoundError :
68- logger .warning (f"Cannot find file at: { path } " )
69- except toml .TomlDecodeError :
70- logger .error (f"Cannot parse TOML at: { path } " )
71-
72- return data .get ("tool" , {}).get ("django" , {}) or {}
73-
74-
75- @typechecked
76- def parse_key_value (data : dict , key : str , value : Any , path : Path ) -> dict :
77- """Handle special cases for `value`.
78-
79- Special cases:
80- - `dict` keys
81- - `$env`: retrieves an environment variable; optional `default` argument
82- - `$path`: converts string to a `Path`; handles relative path
83- - `$insert`: inserts the value to an array; optional `index` argument
84- - `$none`: inserts the `None` value
85- - variables in `str`
86- - `datetime`
87- """
88-
89- if isinstance (value , dict ):
90- # Defaults to "$env" and "$default"
91- env_special_key = _get_special_key (data , "env" )
92- default_special_key = _get_special_key (data , "default" )
93-
94- # Defaults to "$path"
95- path_special_key = _get_special_key (data , "path" )
96-
97- # Defaults to "$insert" and "$variable"
98- insert_special_key = _get_special_key (data , "insert" )
99- index_special_key = _get_special_key (data , "index" )
100-
101- # Defaults to "$none"
102- none_special_key = _get_special_key (data , "none" )
103-
104- if env_special_key in value :
105- default_value = value .get (default_special_key )
106-
107- value = os .getenv (value [env_special_key ], default_value )
108- elif path_special_key in value :
109- file_name = value [path_special_key ]
110-
111- value = _parse_path (path , file_name )
112- elif insert_special_key in value :
113- insert_data = data .get (key , [])
114-
115- # Check the existing value is an array
116- if not isinstance (insert_data , list ):
117- raise InvalidActionError (f"`insert` cannot be used for value of type: { type (data [key ])} " )
118-
119- # Insert the data
120- index = value .get (index_special_key , len (insert_data ))
121- insert_data .insert (index , value [insert_special_key ])
122-
123- # Set the value to the new data
124- value = insert_data
125- elif none_special_key in value and value .get (none_special_key ):
126- value = None
127- elif isinstance (value , str ):
128- # Handle variable substitution
129- for match in re .finditer (r"\$\{\w+\}" , value ):
130- data_key = value [match .start () : match .end ()][2 :- 1 ]
131-
132- if variable := data .get (data_key ):
133- if isinstance (variable , Path ):
134- path_str = _combine_bookends (value , match , variable )
135-
136- value = Path (path_str )
137- elif callable (variable ):
138- value = variable
139- elif isinstance (variable , int ):
140- value = _combine_bookends (value , match , variable )
141-
142- try :
143- value = int (value )
144- except Exception : # noqa: S110
145- pass
146- elif isinstance (variable , float ):
147- value = _combine_bookends (value , match , variable )
148-
149- try :
150- value = float (value )
151- except Exception : # noqa: S110
152- pass
153- elif isinstance (variable , list ):
154- value = variable
155- elif isinstance (variable , dict ):
156- value = variable
157- elif isinstance (variable , datetime ):
158- value = dateparser .isoparse (str (variable ))
159- else :
160- value = value .replace (match .string , str (variable ))
161- else :
162- logger .warning (f"Missing variable substitution { value } " )
163- elif isinstance (value , datetime ):
164- value = dateparser .isoparse (str (value ))
165-
166- return {key : value }
167-
168-
169- @typechecked
170- def _parse_path (path : Path , file_name : str ) -> Path :
171- """Parse a path string relative to a base path.
172-
173- Args:
174- file_name: Relative or absolute file name.
175- path: Base path to resolve file_name against.
176- """
177-
178- _path = Path (path ).parent if path .is_file () else path
179-
180- return (_path / file_name ).resolve ()
181-
182-
183- @typechecked
184- def _combine_bookends (original : str , match : re .Match , middle : Any ) -> str :
25+ def combine_bookends (original : str , match : re .Match , middle : Any ) -> str :
18526 """Get the beginning of the original string before the match, and the
18627 end of the string after the match and smush the replaced value in between
18728 them to generate a new string.
@@ -196,18 +37,139 @@ def _combine_bookends(original: str, match: re.Match, middle: Any) -> str:
19637 return start + str (middle ) + ending
19738
19839
199- @typechecked
200- def _get_special_key (data : dict , key : str ) -> str :
201- """Gets the key for the special operator. Defaults to "$" as the prefix, and "" as the suffix.
202-
203- To change in the included TOML settings, set:
204- ```
205- TOML_SETTINGS_SPECIAL_PREFIX = ""
206- TOML_SETTINGS_SPECIAL_SUFFIX = ""
207- ```
208- """
209-
210- prefix = data .get ("TOML_SETTINGS_SPECIAL_PREFIX" , "$" )
211- suffix = data .get ("TOML_SETTINGS_SPECIAL_SUFFIX" , "" )
40+ class Parser :
41+ path : Path
42+ data : dict
43+
44+ def __init__ (self , path : Path , data : dict | None = None ):
45+ self .path = path
46+ self .data = data or {}
47+
48+ @typechecked
49+ def parse_file (self ):
50+ """Parse data from the specified TOML file to use for Django settings.
51+
52+ The sections get parsed in the following order with the later sections overriding the earlier:
53+ 1. `[tool.django]`
54+ 2. `[tool.django.apps.*]`
55+ 3. `[tool.django.envs.{ENVIRONMENT}]` where {ENVIRONMENT} is defined in the `ENVIRONMENT` env variable
56+ """
57+
58+ toml_data = self .get_data ()
59+
60+ # Get potential settings from `tool.django.apps` and `tool.django.envs`
61+ apps_data = toml_data .pop ("apps" , {})
62+ envs_data = toml_data .pop ("envs" , {})
63+
64+ # Add default settings from `tool.django`
65+ for key , value in toml_data .items ():
66+ logger .debug (f"tool.django: Update '{ key } ' with '{ value } '" )
67+
68+ self .data .update (self .parse_key_value (key , value ))
69+
70+ # Add settings from `tool.django.apps.*`
71+ for apps_name , apps_value in apps_data .items ():
72+ for app_key , app_value in apps_value .items ():
73+ logger .debug (f"tool.django.apps.{ apps_name } : Update '{ app_key } ' with '{ app_value } '" )
74+
75+ self .data .update (self .parse_key_value (app_key , app_value ))
76+
77+ # Add settings from `tool.django.envs.*` if it matches the `ENVIRONMENT` env variable
78+ if environment_env_variable := os .getenv ("ENVIRONMENT" ):
79+ for envs_name , envs_value in envs_data .items ():
80+ if environment_env_variable == envs_name :
81+ for env_key , env_value in envs_value .items ():
82+ logger .debug (f"tool.django.envs.{ envs_name } : Update '{ env_key } ' with '{ env_value } '" )
83+
84+ self .data .update (self .parse_key_value (env_key , env_value ))
85+
86+ return self .data
87+
88+ @typechecked
89+ def get_data (self ) -> dict :
90+ """Gets the data from the passed-in TOML file."""
91+
92+ data = {}
93+
94+ try :
95+ data = toml .load (self .path )
96+ except FileNotFoundError :
97+ logger .warning (f"Cannot find file at: { self .path } " )
98+ except toml .TomlDecodeError :
99+ logger .error (f"Cannot parse TOML at: { self .path } " )
100+
101+ return data .get ("tool" , {}).get ("django" , {}) or {}
102+
103+ @typechecked
104+ def parse_key_value (self , key : str , value : Any ) -> dict :
105+ """Handle special cases for `value`.
106+
107+ Special cases:
108+ - `dict` keys
109+ - `$env`: retrieves an environment variable; optional `default` argument
110+ - `$path`: converts string to a `Path`; handles relative path
111+ - `$insert`: inserts the value to an array; optional `index` argument
112+ - `$none`: inserts the `None` value
113+ - `$value`: literal value
114+ - `$type`: casts the value to a particular type
115+ - variables in `str`
116+ - `datetime`
117+ """
118+
119+ if isinstance (value , dict ):
120+ type_parser = TypeValueParser (data = self .data , value = value )
121+ env_parser = EnvValueParser (data = self .data , value = value )
122+ path_parser = PathValueParser (data = self .data , value = value , path = self .path )
123+ value_parser = ValueValueParser (data = self .data , value = value )
124+ none_parser = NoneValueParser (data = self .data , value = value )
125+ insert_parser = InsertValueParser (data = self .data , value = value , data_key = key )
126+
127+ # Check for a match for all specials (except $type)
128+ for parser in [env_parser , path_parser , value_parser , insert_parser , none_parser ]:
129+ if parser .match ():
130+ value = parser .parse ()
131+ break
132+
133+ # Parse $type last because it can operate on the resolved value from the other parsers
134+ if type_parser .match ():
135+ value = type_parser .parse (value )
136+ elif isinstance (value , str ):
137+ # Handle variable substitution
138+ for match in re .finditer (r"\$\{\w+\}" , value ):
139+ data_key = value [match .start () : match .end ()][2 :- 1 ]
140+
141+ if variable := self .data .get (data_key ):
142+ if isinstance (variable , Path ):
143+ path_str = combine_bookends (value , match , variable )
144+
145+ value = Path (path_str )
146+ elif callable (variable ):
147+ value = variable
148+ elif isinstance (variable , int ):
149+ value = combine_bookends (value , match , variable )
150+
151+ try :
152+ value = int (value )
153+ except Exception : # noqa: S110
154+ pass
155+ elif isinstance (variable , float ):
156+ value = combine_bookends (value , match , variable )
157+
158+ try :
159+ value = float (value )
160+ except Exception : # noqa: S110
161+ pass
162+ elif isinstance (variable , list ):
163+ value = variable
164+ elif isinstance (variable , dict ):
165+ value = variable
166+ elif isinstance (variable , datetime ):
167+ value = dateparser .isoparse (str (variable ))
168+ else :
169+ value = value .replace (match .string , str (variable ))
170+ else :
171+ logger .warning (f"Missing variable substitution { value } " )
172+ elif isinstance (value , datetime ):
173+ value = dateparser .isoparse (str (value ))
212174
213- return f" { prefix } { key } { suffix } "
175+ return { key : value }
0 commit comments