envstack 0.7.4__tar.gz → 0.7.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {envstack-0.7.4/lib/envstack.egg-info → envstack-0.7.5}/PKG-INFO +1 -1
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack/__init__.py +1 -1
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack/util.py +72 -9
- {envstack-0.7.4 → envstack-0.7.5/lib/envstack.egg-info}/PKG-INFO +1 -1
- {envstack-0.7.4 → envstack-0.7.5}/setup.py +1 -1
- {envstack-0.7.4 → envstack-0.7.5}/tests/test_util.py +86 -0
- {envstack-0.7.4 → envstack-0.7.5}/LICENSE +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/README.md +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/dist.json +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack/cli.py +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack/config.py +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack/env.py +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack/exceptions.py +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack/logger.py +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack/path.py +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack/wrapper.py +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack.egg-info/SOURCES.txt +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack.egg-info/dependency_links.txt +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack.egg-info/entry_points.txt +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack.egg-info/not-zip-safe +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack.egg-info/requires.txt +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/lib/envstack.egg-info/top_level.txt +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/setup.cfg +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/tests/test_cmds.py +0 -0
- {envstack-0.7.4 → envstack-0.7.5}/tests/test_env.py +0 -0
|
@@ -48,11 +48,14 @@ from envstack.exceptions import CyclicalReference
|
|
|
48
48
|
# value for unresolvable variables
|
|
49
49
|
null = ""
|
|
50
50
|
|
|
51
|
-
# regular expression pattern for
|
|
51
|
+
# regular expression pattern for bash-like variable expansion
|
|
52
52
|
variable_pattern = re.compile(
|
|
53
53
|
r"\$\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([=?])(\$\{[a-zA-Z_][a-zA-Z0-9_]*\}|[^}]*))?\}"
|
|
54
54
|
)
|
|
55
55
|
|
|
56
|
+
# regular expression pattern for matching windows drive letters
|
|
57
|
+
drive_letter_pattern = re.compile(r"(?P<sep>[:;])?(?P<drive>[A-Z]:[/\\])")
|
|
58
|
+
|
|
56
59
|
|
|
57
60
|
def clear_sys_path(var: str = "PYTHONPATH"):
|
|
58
61
|
"""
|
|
@@ -97,11 +100,67 @@ def dedupe_list(lst: list):
|
|
|
97
100
|
deduplicating paths.
|
|
98
101
|
|
|
99
102
|
:param lst: The list to deduplicate.
|
|
100
|
-
:
|
|
103
|
+
:returns: The deduplicated list.
|
|
101
104
|
"""
|
|
102
105
|
return list(OrderedDict.fromkeys(lst))
|
|
103
106
|
|
|
104
107
|
|
|
108
|
+
def split_windows_paths(path_str: str):
|
|
109
|
+
"""
|
|
110
|
+
Splits a windows-style path string that may contain a mix of colon and
|
|
111
|
+
semicolon delimiters, while preserving drive letter patterns. Drive letters
|
|
112
|
+
must be uppercase.
|
|
113
|
+
|
|
114
|
+
Example:
|
|
115
|
+
Input: "C:\\Program Files\\Python:D:/path2:E:/path3:/usr/local/bin"
|
|
116
|
+
Output: ['C:\\Program Files\\Python', 'D:/path2', 'E:/path3', '/usr/local/bin']
|
|
117
|
+
|
|
118
|
+
:param path_str: The input path string.
|
|
119
|
+
:returns: The split path list.
|
|
120
|
+
"""
|
|
121
|
+
result = []
|
|
122
|
+
tokens = [token.strip() for token in path_str.split(";") if token.strip()]
|
|
123
|
+
|
|
124
|
+
for token in tokens:
|
|
125
|
+
# token is windows-style, insert a marker before drive letters
|
|
126
|
+
if re.match(r"^[A-Z]:[/\\]", token) or "\\" in token:
|
|
127
|
+
modified = drive_letter_pattern.sub(lambda m: "|" + m.group("drive"), token)
|
|
128
|
+
# split on the marker, then on colons that are not in drive-letters
|
|
129
|
+
result += [
|
|
130
|
+
p
|
|
131
|
+
for part in modified.split("|")
|
|
132
|
+
for p in re.split(r"(?<![A-Z]):", part)
|
|
133
|
+
if p
|
|
134
|
+
]
|
|
135
|
+
else:
|
|
136
|
+
result += [p for p in token.split(":") if p]
|
|
137
|
+
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def dedupe_paths(
|
|
142
|
+
path_str: str, joiner: str = os.pathsep, platform: str = config.PLATFORM
|
|
143
|
+
):
|
|
144
|
+
"""
|
|
145
|
+
Deduplicates paths from a colon-separated string.
|
|
146
|
+
|
|
147
|
+
:param path_str: The input path string.
|
|
148
|
+
:param joiner: The path separator to use.
|
|
149
|
+
:platform: The platform to use.
|
|
150
|
+
:returns: The deduplicated path string.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
if platform == "windows":
|
|
154
|
+
deduped = dedupe_list(split_windows_paths(path_str))
|
|
155
|
+
else:
|
|
156
|
+
deduped = dedupe_list(path_str.split(":"))
|
|
157
|
+
|
|
158
|
+
# remove empty paths
|
|
159
|
+
# deduped = [p for p in deduped if p]
|
|
160
|
+
|
|
161
|
+
return joiner.join(deduped)
|
|
162
|
+
|
|
163
|
+
|
|
105
164
|
def dict_diff(dict1: dict, dict2: dict):
|
|
106
165
|
"""
|
|
107
166
|
Compare two dictionaries and return their differences.
|
|
@@ -238,7 +297,7 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
|
|
|
238
297
|
|
|
239
298
|
# dedupe paths and convert to platform-specific path separators
|
|
240
299
|
if ":" in result:
|
|
241
|
-
result =
|
|
300
|
+
result = dedupe_paths(result)
|
|
242
301
|
|
|
243
302
|
# detect recursion errors
|
|
244
303
|
except RecursionError:
|
|
@@ -248,16 +307,20 @@ def evaluate_modifiers(expression: str, environ: dict = os.environ):
|
|
|
248
307
|
except TypeError:
|
|
249
308
|
if isinstance(expression, list):
|
|
250
309
|
result = [
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
310
|
+
(
|
|
311
|
+
variable_pattern.sub(substitute_variable, str(v))
|
|
312
|
+
if isinstance(v, str)
|
|
313
|
+
else v
|
|
314
|
+
)
|
|
254
315
|
for v in expression
|
|
255
316
|
]
|
|
256
317
|
elif isinstance(expression, dict):
|
|
257
318
|
result = {
|
|
258
|
-
k:
|
|
259
|
-
|
|
260
|
-
|
|
319
|
+
k: (
|
|
320
|
+
variable_pattern.sub(substitute_variable, str(v))
|
|
321
|
+
if isinstance(v, str)
|
|
322
|
+
else v
|
|
323
|
+
)
|
|
261
324
|
for k, v in expression.items()
|
|
262
325
|
}
|
|
263
326
|
else:
|
|
@@ -40,7 +40,7 @@ with open(os.path.join(here, "README.md")) as f:
|
|
|
40
40
|
|
|
41
41
|
setup(
|
|
42
42
|
name="envstack",
|
|
43
|
-
version="0.7.
|
|
43
|
+
version="0.7.5",
|
|
44
44
|
description="Stacked environment variable management system",
|
|
45
45
|
long_description=long_description,
|
|
46
46
|
long_description_content_type="text/markdown",
|
|
@@ -136,6 +136,92 @@ class TestUtils(unittest.TestCase):
|
|
|
136
136
|
get_stack_name(name)
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
class TestDedupePaths(unittest.TestCase):
|
|
140
|
+
def test_dedupe_list(self):
|
|
141
|
+
"""Test dedupe_list function."""
|
|
142
|
+
from envstack.util import dedupe_list
|
|
143
|
+
|
|
144
|
+
paths = [
|
|
145
|
+
"/usr/bin",
|
|
146
|
+
"/usr/local/bin",
|
|
147
|
+
"/usr/local/bin",
|
|
148
|
+
"/usr/bin",
|
|
149
|
+
"/usr/local/bin",
|
|
150
|
+
"/some/other/path",
|
|
151
|
+
]
|
|
152
|
+
result = dedupe_list(paths)
|
|
153
|
+
self.assertEqual(result, ["/usr/bin", "/usr/local/bin", "/some/other/path"])
|
|
154
|
+
|
|
155
|
+
paths = ["/usr/bin"]
|
|
156
|
+
result = dedupe_list(paths)
|
|
157
|
+
self.assertEqual(result, ["/usr/bin"])
|
|
158
|
+
|
|
159
|
+
paths = []
|
|
160
|
+
result = dedupe_list(paths)
|
|
161
|
+
self.assertEqual(result, [])
|
|
162
|
+
|
|
163
|
+
def test_dedupe_paths(self):
|
|
164
|
+
"""Test dedupe_paths function."""
|
|
165
|
+
from envstack.util import dedupe_paths
|
|
166
|
+
|
|
167
|
+
paths = [
|
|
168
|
+
"/usr/bin",
|
|
169
|
+
"/usr/local/bin",
|
|
170
|
+
"/usr/bin",
|
|
171
|
+
"/usr/local/bin",
|
|
172
|
+
"/usr/local/bin",
|
|
173
|
+
"/some/other/path",
|
|
174
|
+
]
|
|
175
|
+
result = dedupe_paths(":".join(paths))
|
|
176
|
+
self.assertEqual(result, "/usr/bin:/usr/local/bin:/some/other/path")
|
|
177
|
+
|
|
178
|
+
paths = ["/usr/bin"]
|
|
179
|
+
result = dedupe_paths(":".join(paths))
|
|
180
|
+
self.assertEqual(result, "/usr/bin")
|
|
181
|
+
|
|
182
|
+
paths = ["/usr/bin", ""]
|
|
183
|
+
result = dedupe_paths(":".join(paths))
|
|
184
|
+
self.assertEqual(result, "/usr/bin:")
|
|
185
|
+
|
|
186
|
+
paths = []
|
|
187
|
+
result = dedupe_paths(":".join(paths))
|
|
188
|
+
self.assertEqual(result, "")
|
|
189
|
+
|
|
190
|
+
def test_dedupe_paths_windows(self):
|
|
191
|
+
"""Test dedupe_paths function on windows."""
|
|
192
|
+
from envstack.util import dedupe_paths
|
|
193
|
+
|
|
194
|
+
paths = [
|
|
195
|
+
"C:\\Program Files\\Python",
|
|
196
|
+
"D:/path2",
|
|
197
|
+
"E:/path3",
|
|
198
|
+
]
|
|
199
|
+
result = dedupe_paths(":".join(paths), joiner=";", platform="windows")
|
|
200
|
+
self.assertEqual(result, "C:\\Program Files\\Python;D:/path2;E:/path3")
|
|
201
|
+
|
|
202
|
+
paths = [
|
|
203
|
+
"C:\\Program Files\\Python",
|
|
204
|
+
"C:\\Program Files\\Python",
|
|
205
|
+
"D:/path2",
|
|
206
|
+
"E:/path3",
|
|
207
|
+
"E:/path3",
|
|
208
|
+
"/usr/local/bin",
|
|
209
|
+
]
|
|
210
|
+
path = ":".join(paths)
|
|
211
|
+
result = dedupe_paths(path, joiner=";", platform="windows")
|
|
212
|
+
self.assertEqual(result, "C:\\Program Files\\Python;D:/path2;E:/path3;/usr/local/bin")
|
|
213
|
+
|
|
214
|
+
# mixed paths
|
|
215
|
+
path = "X:/pipe/prod/env;X:/pipe/prod/env:/home/user/envstack/env"
|
|
216
|
+
result = dedupe_paths(path, joiner=";", platform="windows")
|
|
217
|
+
self.assertEqual(result, "X:/pipe/prod/env;/home/user/envstack/env")
|
|
218
|
+
|
|
219
|
+
# mixed paths with duplicate
|
|
220
|
+
path = "C:\\Program Files\\Python;D:/path2;E:/path3:/usr/local/bin:/usr/local/bin"
|
|
221
|
+
result = dedupe_paths(path, joiner=";", platform="windows")
|
|
222
|
+
self.assertEqual(result, "C:\\Program Files\\Python;D:/path2;E:/path3;/usr/local/bin")
|
|
223
|
+
|
|
224
|
+
|
|
139
225
|
class TestSafeEval(unittest.TestCase):
|
|
140
226
|
def test_safe_eval_string(self):
|
|
141
227
|
value = "hello"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|