Coverage for dotenv_cli/core.py: 69%

48 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-06 13:26 +0000

1# remove when we don't support py38 anymore 

2from __future__ import annotations 

3 

4import atexit 

5import logging 

6import os 

7from subprocess import Popen 

8from typing import NoReturn 

9 

10logger = logging.getLogger(__name__) 

11 

12 

13def read_dotenv(filename: str) -> dict[str, str]: 

14 """Read dotenv file. 

15 

16 Parameters 

17 ---------- 

18 filename 

19 path to the filename 

20 

21 Returns 

22 ------- 

23 dict 

24 

25 """ 

26 try: 

27 with open(filename, "r") as fh: 

28 data = fh.read() 

29 except FileNotFoundError: 

30 logger.warning( 

31 f"{filename} does not exist, continuing without " 

32 "setting environment variables." 

33 ) 

34 data = "" 

35 

36 res = {} 

37 for line in data.splitlines(): 

38 logger.debug(line) 

39 

40 line = line.strip() 

41 

42 # ignore comments 

43 if line.startswith("#"): 

44 continue 

45 

46 # ignore empty lines or lines w/o '=' 

47 if "=" not in line: 

48 continue 

49 

50 key, value = line.split("=", 1) 

51 

52 # allow export 

53 if key.startswith("export "): 

54 key = key.split(" ", 1)[-1] 

55 

56 key = key.strip() 

57 value = value.strip() 

58 

59 # remove quotes (not sure if this is standard behaviour) 

60 if len(value) >= 2 and value[0] == value[-1] == '"': 

61 value = value[1:-1] 

62 # escape escape characters 

63 value = bytes(value, "utf-8").decode("unicode-escape") 

64 

65 elif len(value) >= 2 and value[0] == value[-1] == "'": 

66 value = value[1:-1] 

67 

68 res[key] = value 

69 logger.debug(res) 

70 return res 

71 

72 

73def run_dotenv(filename: str, command: list[str]) -> NoReturn | int: 

74 """Run dotenv. 

75 

76 This function executes the commands with the environment variables 

77 parsed from filename. 

78 

79 Parameters 

80 ---------- 

81 filename 

82 path to the .env file 

83 command 

84 command to execute 

85 

86 Returns 

87 ------- 

88 NoReturn | int 

89 The exit status code in Windows. In POSIX-compatible systems, the 

90 function does not return normally. 

91 

92 """ 

93 # read dotenv 

94 dotenv = read_dotenv(filename) 

95 

96 # update env 

97 env = os.environ.copy() 

98 env.update(dotenv) 

99 

100 # in POSIX, we replace the current process with the command, execvpe does 

101 # not return 

102 if os.name == "posix": 

103 os.execvpe(command[0], command, env) 

104 

105 # in Windows, we spawn a new process 

106 # execute 

107 proc = Popen( 

108 command, 

109 # stdin=PIPE, 

110 # stdout=PIPE, 

111 # stderr=STDOUT, 

112 universal_newlines=True, 

113 bufsize=0, 

114 shell=False, 

115 env=env, 

116 ) 

117 

118 def terminate_proc() -> None: 

119 """Kill child process. 

120 

121 All signals should be forwarded to the child processes 

122 automatically, however child processes are also free to ignore 

123 some of them. With this we make sure the child processes get 

124 killed once dotenv exits. 

125 

126 """ 

127 proc.kill() 

128 

129 # register 

130 atexit.register(terminate_proc) 

131 

132 _, _ = proc.communicate() 

133 

134 # unregister 

135 atexit.unregister(terminate_proc) 

136 

137 return proc.returncode