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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sprout-cli
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: CLI tool to automate git worktree and Docker Compose development workflows
5
5
  Author: SecDevLab Inc.
6
6
  License: MIT
@@ -1,3 +1,3 @@
1
1
  """sprout - CLI tool to automate git worktree and Docker Compose development workflows."""
2
2
 
3
- __version__ = "0.6.0"
3
+ __version__ = "0.7.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\(\)\s*}}", replace_auto_port, line)
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
- # Process {{ branch() }} placeholders
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
- line = re.sub(r"{{\s*([^}()]+)\s*}}", replace_variable, line)
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