11from typing import Optional , Union
22import os , sys
3+ import string
34from pathlib import Path
45
56if sys .version_info >= (3 , 11 ):
910from agentstack import conf
1011
1112
12- ENV_FILEMANE = ".env"
13+ ENV_FILENAME = ".env"
1314PYPROJECT_FILENAME = "pyproject.toml"
1415
1516
1617class EnvFile :
1718 """
1819 Interface for interacting with the .env file inside a project directory.
1920 Unlike the ConfigFile, we do not re-write the entire file on every change,
20- and instead just append new lines to the end of the file. This preseres
21+ and instead just append new lines to the end of the file. This preserves
2122 comments and other formatting that the user may have added and prevents
2223 opportunities for data loss.
2324
25+ If the value of a variable is None, it will be commented out when it is written
26+ to the file. This gives the user a suggestion, but doesn't override values that
27+ may have been set by the user via other means (for example, but the user's shell).
28+ Commented variable are not re-parsed when the file is read.
29+
2430 `path` is the directory where the .env file is located. Defaults to the
2531 current working directory.
2632 `filename` is the name of the .env file, defaults to '.env'.
@@ -34,47 +40,59 @@ class EnvFile:
3440
3541 variables : dict [str , str ]
3642
37- def __init__ (self , filename : str = ENV_FILEMANE ):
43+ def __init__ (self , filename : str = ENV_FILENAME ):
3844 self ._filename = filename
3945 self .read ()
4046
41- def __getitem__ (self , key ):
47+ def __getitem__ (self , key ) -> str :
4248 return self .variables [key ]
4349
44- def __setitem__ (self , key , value ):
50+ def __setitem__ (self , key , value ) -> None :
4551 if key in self .variables :
4652 raise ValueError ("EnvFile does not allow overwriting values." )
4753 self .append_if_new (key , value )
4854
4955 def __contains__ (self , key ) -> bool :
5056 return key in self .variables
5157
52- def append_if_new (self , key , value ):
58+ def append_if_new (self , key , value ) -> None :
59+ """Setting a non-existent key will append it to the end of the file."""
5360 if key not in self .variables :
5461 self .variables [key ] = value
5562 self ._new_variables [key ] = value
5663
57- def read (self ):
58- def parse_line (line ):
64+ def read (self ) -> None :
65+ def parse_line (line ) -> tuple [str , str ]:
66+ """
67+ Parse a line from the .env file.
68+ Pairs are split on the first '=' character, and stripped of whitespace & quotes.
69+ Only the last occurrence of a variable is stored.
70+ """
5971 key , value = line .split ('=' )
60- return key .strip (), value .strip ()
72+ return key .strip (), value .strip (string . whitespace + '"' )
6173
6274 if os .path .exists (conf .PATH / self ._filename ):
6375 with open (conf .PATH / self ._filename , 'r' ) as f :
64- self .variables = dict ([parse_line (line ) for line in f .readlines () if '=' in line ])
76+ self .variables = dict (
77+ [parse_line (line ) for line in f .readlines () if '=' in line and not line .startswith ('#' )]
78+ )
6579 else :
6680 self .variables = {}
6781 self ._new_variables = {}
6882
69- def write (self ):
83+ def write (self ) -> None :
84+ """Append new variables to the end of the file."""
7085 with open (conf .PATH / self ._filename , 'a' ) as f :
7186 for key , value in self ._new_variables .items ():
72- f .write (f"\n { key } ={ value } " )
87+ if value is None :
88+ f .write (f'\n #{ key } =""' ) # comment-out empty values
89+ else :
90+ f .write (f'\n { key } ={ value } ' )
7391
7492 def __enter__ (self ) -> 'EnvFile' :
7593 return self
7694
77- def __exit__ (self , * args ):
95+ def __exit__ (self , * args ) -> None :
7896 self .write ()
7997
8098
0 commit comments