sprout-cli 0.6.0__tar.gz → 0.7.0__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.
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/PKG-INFO +1 -1
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/__init__.py +1 -1
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/utils.py +38 -7
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/tests/test_integration.py +55 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/tests/test_utils.py +93 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/.gitignore +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/LICENSE +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/README.md +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/pyproject.toml +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/__main__.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/cli.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/commands/__init__.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/commands/create.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/commands/ls.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/commands/path.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/commands/rm.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/exceptions.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/src/sprout/types.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/tests/__init__.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/tests/test_commands.py +0 -0
- {sprout_cli-0.6.0 → sprout_cli-0.7.0}/tests/test_multi_env.py +0 -0
|
@@ -155,26 +155,56 @@ def parse_env_template(
|
|
|
155
155
|
file_ports.update(used_ports)
|
|
156
156
|
|
|
157
157
|
for line in content.splitlines():
|
|
158
|
-
# Process {{ auto_port() }} placeholders
|
|
158
|
+
# Process {{ auto_port() | default }} placeholders
|
|
159
159
|
def replace_auto_port(match: re.Match[str]) -> str:
|
|
160
|
+
# Extract default value if present (group 1)
|
|
161
|
+
default_value = match.group(1)
|
|
162
|
+
if default_value is not None:
|
|
163
|
+
default_value = default_value.strip()
|
|
164
|
+
|
|
165
|
+
# Generate available port
|
|
160
166
|
port = find_available_port()
|
|
161
167
|
while port in file_ports:
|
|
162
168
|
port = find_available_port()
|
|
163
169
|
file_ports.add(port)
|
|
164
170
|
return str(port)
|
|
165
171
|
|
|
166
|
-
line = re.sub(r"{{\s*auto_port\(\)
|
|
172
|
+
line = re.sub(r"{{\s*auto_port\(\)(?:\s*\|\s*([^}]*))?\s*}}", replace_auto_port, line)
|
|
173
|
+
|
|
174
|
+
# Process {{ branch() | default }} placeholders
|
|
175
|
+
def replace_branch(match: re.Match[str]) -> str:
|
|
176
|
+
# Extract default value if present (group 1)
|
|
177
|
+
default_value = match.group(1)
|
|
178
|
+
if default_value is not None:
|
|
179
|
+
default_value = default_value.strip()
|
|
180
|
+
|
|
181
|
+
# Use branch_name if provided, otherwise use default
|
|
182
|
+
if branch_name:
|
|
183
|
+
return branch_name
|
|
184
|
+
elif default_value is not None:
|
|
185
|
+
return default_value
|
|
186
|
+
else:
|
|
187
|
+
# No branch name and no default - keep placeholder unchanged
|
|
188
|
+
return match.group(0)
|
|
167
189
|
|
|
168
|
-
|
|
169
|
-
if branch_name:
|
|
170
|
-
line = re.sub(r"{{\s*branch\(\)\s*}}", branch_name, line)
|
|
190
|
+
line = re.sub(r"{{\s*branch\(\)(?:\s*\|\s*([^}]*))?\s*}}", replace_branch, line)
|
|
171
191
|
|
|
172
|
-
# Process {{ VARIABLE }} placeholders
|
|
192
|
+
# Process {{ VARIABLE | default }} placeholders
|
|
173
193
|
def replace_variable(match: re.Match[str]) -> str:
|
|
174
194
|
var_name = match.group(1).strip()
|
|
195
|
+
# Extract default value if present (group 2)
|
|
196
|
+
default_value = match.group(2)
|
|
197
|
+
if default_value is not None:
|
|
198
|
+
default_value = default_value.strip()
|
|
199
|
+
|
|
175
200
|
# Check environment variable first
|
|
176
201
|
value = os.environ.get(var_name)
|
|
177
202
|
if value is None:
|
|
203
|
+
# If default value is provided, use it
|
|
204
|
+
if default_value is not None:
|
|
205
|
+
return default_value
|
|
206
|
+
|
|
207
|
+
# No default value - prompt user for value
|
|
178
208
|
# Create a relative path for display
|
|
179
209
|
try:
|
|
180
210
|
display_path = template_path.relative_to(Path.cwd())
|
|
@@ -196,7 +226,8 @@ def parse_env_template(
|
|
|
196
226
|
return value
|
|
197
227
|
|
|
198
228
|
# Only match variables that don't look like function calls (no parentheses)
|
|
199
|
-
|
|
229
|
+
# Now also captures optional | default_value
|
|
230
|
+
line = re.sub(r"{{\s*([^}()]+?)(?:\s*\|\s*([^}]*))?\s*}}", replace_variable, line)
|
|
200
231
|
|
|
201
232
|
lines.append(line)
|
|
202
233
|
|
|
@@ -239,6 +239,61 @@ class TestIntegrationWorkflow:
|
|
|
239
239
|
assert "PORT=" in env_content # Should have a port number
|
|
240
240
|
assert "COMPOSE_VAR=${BRANCH_NAME}" in env_content # Docker syntax preserved
|
|
241
241
|
|
|
242
|
+
def test_default_values(self, git_repo, monkeypatch):
|
|
243
|
+
"""Test that default values work correctly in .env.example templates."""
|
|
244
|
+
git_repo, default_branch = git_repo
|
|
245
|
+
monkeypatch.chdir(git_repo)
|
|
246
|
+
|
|
247
|
+
# Set only some environment variables
|
|
248
|
+
monkeypatch.setenv("SET_VAR", "from_environment")
|
|
249
|
+
# UNSET_VAR is not set - should use default
|
|
250
|
+
|
|
251
|
+
# Create .env.example with default values
|
|
252
|
+
env_example = git_repo / ".env.example"
|
|
253
|
+
env_example.write_text(
|
|
254
|
+
"# Test default values\n"
|
|
255
|
+
"SET_VAR={{ SET_VAR | default_value }}\n"
|
|
256
|
+
"UNSET_VAR={{ UNSET_VAR | localhost }}\n"
|
|
257
|
+
"EMPTY_VAR={{ EMPTY_VAR | }}\n"
|
|
258
|
+
"PORT={{ auto_port() | 8080 }}\n"
|
|
259
|
+
"BRANCH={{ branch() | develop }}\n"
|
|
260
|
+
"NO_DEFAULT={{ NO_DEFAULT }}\n" # Will be prompted
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Add to git
|
|
264
|
+
subprocess.run(["git", "add", ".env.example"], cwd=git_repo, check=True)
|
|
265
|
+
subprocess.run(
|
|
266
|
+
["git", "commit", "-m", "Add .env.example with defaults"],
|
|
267
|
+
cwd=git_repo,
|
|
268
|
+
check=True,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Set NO_DEFAULT to avoid prompt
|
|
272
|
+
monkeypatch.setenv("NO_DEFAULT", "prompted_value")
|
|
273
|
+
|
|
274
|
+
# Create worktree
|
|
275
|
+
branch_name = "feature-test"
|
|
276
|
+
result = runner.invoke(app, ["create", branch_name])
|
|
277
|
+
assert result.exit_code == 0
|
|
278
|
+
|
|
279
|
+
# Verify .env was created with correct values
|
|
280
|
+
env_file = git_repo / ".sprout" / branch_name / ".env"
|
|
281
|
+
env_content = env_file.read_text()
|
|
282
|
+
|
|
283
|
+
# Check that environment variable overrides default
|
|
284
|
+
assert "SET_VAR=from_environment" in env_content
|
|
285
|
+
# Check that default is used when variable is not set
|
|
286
|
+
assert "UNSET_VAR=localhost" in env_content
|
|
287
|
+
# Check empty string default
|
|
288
|
+
assert "EMPTY_VAR=" in env_content or "EMPTY_VAR=\n" in env_content
|
|
289
|
+
# Check that auto_port generates a port (default not used)
|
|
290
|
+
assert "PORT=" in env_content
|
|
291
|
+
assert "PORT=8080" not in env_content # Should be a generated port
|
|
292
|
+
# Check that branch name is used (not default)
|
|
293
|
+
assert f"BRANCH={branch_name}" in env_content
|
|
294
|
+
# Check prompted value
|
|
295
|
+
assert "NO_DEFAULT=prompted_value" in env_content
|
|
296
|
+
|
|
242
297
|
def test_error_cases(self, git_repo, monkeypatch, tmp_path):
|
|
243
298
|
"""Test various error conditions."""
|
|
244
299
|
git_repo, default_branch = git_repo
|
|
@@ -220,6 +220,99 @@ class TestEnvTemplateParser:
|
|
|
220
220
|
"COMPOSE_VAR=${COMPOSE_VAR}"
|
|
221
221
|
)
|
|
222
222
|
|
|
223
|
+
def test_parse_env_template_variable_with_default(self, tmp_path, mocker):
|
|
224
|
+
"""Test parsing {{ VARIABLE | default }} when variable is not set."""
|
|
225
|
+
# Ensure the variable is not in environment
|
|
226
|
+
mocker.patch.dict(os.environ, {}, clear=False)
|
|
227
|
+
if "TEST_VAR" in os.environ:
|
|
228
|
+
del os.environ["TEST_VAR"]
|
|
229
|
+
|
|
230
|
+
template = tmp_path / ".env.example"
|
|
231
|
+
template.write_text("TEST_VAR={{ TEST_VAR | localhost }}")
|
|
232
|
+
|
|
233
|
+
result = parse_env_template(template)
|
|
234
|
+
assert result == "TEST_VAR=localhost"
|
|
235
|
+
|
|
236
|
+
def test_parse_env_template_variable_with_default_env_set(self, tmp_path, mocker):
|
|
237
|
+
"""Test that environment variable overrides default value."""
|
|
238
|
+
mocker.patch.dict(os.environ, {"TEST_VAR": "custom_value"})
|
|
239
|
+
|
|
240
|
+
template = tmp_path / ".env.example"
|
|
241
|
+
template.write_text("TEST_VAR={{ TEST_VAR | localhost }}")
|
|
242
|
+
|
|
243
|
+
result = parse_env_template(template)
|
|
244
|
+
assert result == "TEST_VAR=custom_value"
|
|
245
|
+
|
|
246
|
+
def test_parse_env_template_variable_with_empty_default(self, tmp_path, mocker):
|
|
247
|
+
"""Test parsing {{ VARIABLE | }} with empty string as default."""
|
|
248
|
+
mocker.patch.dict(os.environ, {}, clear=False)
|
|
249
|
+
if "EMPTY_VAR" in os.environ:
|
|
250
|
+
del os.environ["EMPTY_VAR"]
|
|
251
|
+
|
|
252
|
+
template = tmp_path / ".env.example"
|
|
253
|
+
template.write_text("EMPTY_VAR={{ EMPTY_VAR | }}")
|
|
254
|
+
|
|
255
|
+
result = parse_env_template(template)
|
|
256
|
+
assert result == "EMPTY_VAR="
|
|
257
|
+
|
|
258
|
+
def test_parse_env_template_variable_whitespace_trimming(self, tmp_path, mocker):
|
|
259
|
+
"""Test that whitespace is trimmed from default values."""
|
|
260
|
+
mocker.patch.dict(os.environ, {}, clear=False)
|
|
261
|
+
if "TEST_VAR" in os.environ:
|
|
262
|
+
del os.environ["TEST_VAR"]
|
|
263
|
+
|
|
264
|
+
template = tmp_path / ".env.example"
|
|
265
|
+
template.write_text("TEST_VAR={{ TEST_VAR | localhost }}")
|
|
266
|
+
|
|
267
|
+
result = parse_env_template(template)
|
|
268
|
+
assert result == "TEST_VAR=localhost"
|
|
269
|
+
|
|
270
|
+
def test_parse_env_template_branch_with_default(self, tmp_path):
|
|
271
|
+
"""Test parsing {{ branch() | default }} when branch_name is None."""
|
|
272
|
+
template = tmp_path / ".env.example"
|
|
273
|
+
template.write_text("BRANCH={{ branch() | main }}")
|
|
274
|
+
|
|
275
|
+
result = parse_env_template(template, branch_name=None)
|
|
276
|
+
assert result == "BRANCH=main"
|
|
277
|
+
|
|
278
|
+
def test_parse_env_template_branch_with_default_branch_set(self, tmp_path):
|
|
279
|
+
"""Test that branch_name overrides default value."""
|
|
280
|
+
template = tmp_path / ".env.example"
|
|
281
|
+
template.write_text("BRANCH={{ branch() | main }}")
|
|
282
|
+
|
|
283
|
+
result = parse_env_template(template, branch_name="feature-xyz")
|
|
284
|
+
assert result == "BRANCH=feature-xyz"
|
|
285
|
+
|
|
286
|
+
def test_parse_env_template_auto_port_with_default(self, tmp_path, mocker):
|
|
287
|
+
"""Test {{ auto_port() | default }} - default is captured but not used."""
|
|
288
|
+
mocker.patch("sprout.utils.find_available_port", return_value=9000)
|
|
289
|
+
|
|
290
|
+
template = tmp_path / ".env.example"
|
|
291
|
+
template.write_text("PORT={{ auto_port() | 8080 }}")
|
|
292
|
+
|
|
293
|
+
# auto_port() always generates a port, so default is not used
|
|
294
|
+
result = parse_env_template(template)
|
|
295
|
+
assert result == "PORT=9000"
|
|
296
|
+
|
|
297
|
+
def test_parse_env_template_mixed_with_defaults(self, tmp_path, mocker):
|
|
298
|
+
"""Test parsing mixed placeholders with default values."""
|
|
299
|
+
mocker.patch("sprout.utils.find_available_port", return_value=8080)
|
|
300
|
+
mocker.patch.dict(os.environ, {"SET_VAR": "from_env"}, clear=False)
|
|
301
|
+
if "UNSET_VAR" in os.environ:
|
|
302
|
+
del os.environ["UNSET_VAR"]
|
|
303
|
+
|
|
304
|
+
template = tmp_path / ".env.example"
|
|
305
|
+
template.write_text(
|
|
306
|
+
"SET_VAR={{ SET_VAR | default1 }}\n"
|
|
307
|
+
"UNSET_VAR={{ UNSET_VAR | default2 }}\n"
|
|
308
|
+
"PORT={{ auto_port() | 3000 }}\n"
|
|
309
|
+
"BRANCH={{ branch() | develop }}\n"
|
|
310
|
+
"EMPTY={{ EMPTY | }}"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
result = parse_env_template(template, branch_name=None)
|
|
314
|
+
assert result == ("SET_VAR=from_env\nUNSET_VAR=default2\nPORT=8080\nBRANCH=develop\nEMPTY=")
|
|
315
|
+
|
|
223
316
|
def test_parse_env_template_file_not_found(self, tmp_path):
|
|
224
317
|
"""Test error when template file doesn't exist."""
|
|
225
318
|
template = tmp_path / "nonexistent.env"
|
|
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
|