Coverage for dotenv_cli/core.py: 69%
48 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-07-01 12:34 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-07-01 12:34 +0000
1# remove when we don't support py38 anymore
2from __future__ import annotations
4import atexit
5import logging
6import os
7from subprocess import Popen
8from typing import NoReturn
10logger = logging.getLogger(__name__)
13def read_dotenv(filename: str) -> dict[str, str]:
14 """Read dotenv file.
16 Parameters
17 ----------
18 filename
19 path to the filename
21 Returns
22 -------
23 dict
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 = ""
36 res = {}
37 for line in data.splitlines():
38 logger.debug(line)
40 line = line.strip()
42 # ignore comments
43 if line.startswith("#"):
44 continue
46 # ignore empty lines or lines w/o '='
47 if "=" not in line:
48 continue
50 key, value = line.split("=", 1)
52 # allow export
53 if key.startswith("export "):
54 key = key.split(" ", 1)[-1]
56 key = key.strip()
57 value = value.strip()
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")
65 elif len(value) >= 2 and value[0] == value[-1] == "'":
66 value = value[1:-1]
68 res[key] = value
69 logger.debug(res)
70 return res
73def run_dotenv(filename: str, command: list[str]) -> NoReturn | int:
74 """Run dotenv.
76 This function executes the commands with the environment variables
77 parsed from filename.
79 Parameters
80 ----------
81 filename
82 path to the .env file
83 command
84 command to execute
86 Returns
87 -------
88 NoReturn | int
89 The exit status code in Windows. In POSIX-compatible systems, the
90 function does not return normally.
92 """
93 # read dotenv
94 dotenv = read_dotenv(filename)
96 # update env
97 env = os.environ.copy()
98 env.update(dotenv)
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)
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 )
118 def terminate_proc() -> None:
119 """Kill child process.
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.
126 """
127 proc.kill()
129 # register
130 atexit.register(terminate_proc)
132 _, _ = proc.communicate()
134 # unregister
135 atexit.unregister(terminate_proc)
137 return proc.returncode