strictcli 0.4.1__tar.gz → 0.5.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.
Files changed (68) hide show
  1. strictcli-0.5.0/.rlsbl/changes/.validated +1 -0
  2. strictcli-0.5.0/.rlsbl/changes/0.5.0.jsonl +3 -0
  3. strictcli-0.5.0/.rlsbl/changes/0.5.0.md +5 -0
  4. strictcli-0.5.0/.rlsbl/version +1 -0
  5. {strictcli-0.4.1 → strictcli-0.5.0}/CHANGELOG.md +6 -0
  6. {strictcli-0.4.1 → strictcli-0.5.0}/PKG-INFO +1 -1
  7. {strictcli-0.4.1 → strictcli-0.5.0}/package.json +1 -1
  8. {strictcli-0.4.1 → strictcli-0.5.0}/pyproject.toml +1 -1
  9. {strictcli-0.4.1 → strictcli-0.5.0}/strictcli/__init__.py +66 -6
  10. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_dependencies.py +269 -1
  11. {strictcli-0.4.1 → strictcli-0.5.0}/uv.lock +1 -1
  12. strictcli-0.4.1/.rlsbl/changes/.validated +0 -1
  13. strictcli-0.4.1/.rlsbl/version +0 -1
  14. {strictcli-0.4.1 → strictcli-0.5.0}/.claude/settings.json +0 -0
  15. {strictcli-0.4.1 → strictcli-0.5.0}/.github/workflows/ci.yml +0 -0
  16. {strictcli-0.4.1 → strictcli-0.5.0}/.github/workflows/publish.yml +0 -0
  17. {strictcli-0.4.1 → strictcli-0.5.0}/.gitignore +0 -0
  18. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.claude/settings.json +0 -0
  19. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  20. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  21. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.gitignore +0 -0
  22. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  23. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  24. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  25. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  26. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  27. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  28. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  29. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/CLAUDE.md +0 -0
  30. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/bases/LICENSE +0 -0
  31. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  32. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/changes/0.4.0.md +0 -0
  33. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  34. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/changes/0.4.1.md +0 -0
  35. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  36. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/config.json +0 -0
  37. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/hashes.json +0 -0
  38. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/hooks/post-release.sh +0 -0
  39. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/hooks/pre-checks.sh +0 -0
  40. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/hooks/pre-release.sh +0 -0
  41. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/lint/go.toml +0 -0
  42. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/lint/npm.toml +0 -0
  43. {strictcli-0.4.1 → strictcli-0.5.0}/.rlsbl/lint/python.toml +0 -0
  44. {strictcli-0.4.1 → strictcli-0.5.0}/CLAUDE.md +0 -0
  45. {strictcli-0.4.1 → strictcli-0.5.0}/LICENSE +0 -0
  46. {strictcli-0.4.1 → strictcli-0.5.0}/README.md +0 -0
  47. {strictcli-0.4.1 → strictcli-0.5.0}/index.js +0 -0
  48. {strictcli-0.4.1 → strictcli-0.5.0}/package-lock.json +0 -0
  49. {strictcli-0.4.1 → strictcli-0.5.0}/postinstall.js +0 -0
  50. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_arg_default.py +0 -0
  51. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_choices.py +0 -0
  52. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_e2e.py +0 -0
  53. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_env.py +0 -0
  54. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_exit_codes.py +0 -0
  55. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_global_flags.py +0 -0
  56. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_help.py +0 -0
  57. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_int_type.py +0 -0
  58. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_mutex.py +0 -0
  59. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_nesting.py +0 -0
  60. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_parser.py +0 -0
  61. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_passthrough.py +0 -0
  62. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_registration.py +0 -0
  63. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_repeatable.py +0 -0
  64. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_tags.py +0 -0
  65. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_validate.py +0 -0
  66. {strictcli-0.4.1 → strictcli-0.5.0}/tests/test_variadic.py +0 -0
  67. {strictcli-0.4.1 → strictcli-0.5.0}/todo/.defer/deferred.md +0 -0
  68. {strictcli-0.4.1 → strictcli-0.5.0}/todo/.done/original-idea.md +0 -0
@@ -0,0 +1 @@
1
+ d7f1969ff795da06e8ed45792a8508559487c830
@@ -0,0 +1,3 @@
1
+ {"commits":["2a230acb24171d4d380ef4c499f20dc834db9aae"],"user_facing":true,"description":"**New feature.** `Implies` flag dependency type: when a trigger flag is set, automatically set a target bool flag to a specified value. Explicit contradictions are parse errors.","type":"feature"}
2
+ {"commits":["8802c6d7d49c9d0243e29439ff0488db0d817caa"],"user_facing":false}
3
+ {"commits":["af7f4514b16a02cb275b8cae467a6d6c9952c558"],"user_facing":false}
@@ -0,0 +1,5 @@
1
+ ## 0.5.0
2
+
3
+ ### Features
4
+
5
+ - **New feature.** `Implies` flag dependency type: when a trigger flag is set, automatically set a target bool flag to a specified value. Explicit contradictions are parse errors.
@@ -0,0 +1 @@
1
+ 0.32.0
@@ -2,6 +2,12 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## 0.5.0
6
+
7
+ ### Features
8
+
9
+ - **New feature.** `Implies` flag dependency type: when a trigger flag is set, automatically set a target bool flag to a specified value. Explicit contradictions are parse errors.
10
+
5
11
  ## 0.4.1
6
12
 
7
13
  ### Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.4.1
3
+ Version: 0.5.0
4
4
  Summary: A strict, zero-dependency CLI framework for Python
5
5
  Project-URL: Homepage, https://github.com/smm-h/strictcli
6
6
  Project-URL: Repository, https://github.com/smm-h/strictcli
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "A strict, zero-dependency CLI framework for Python (npm wrapper)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "strictcli"
7
- version = "0.4.1"
7
+ version = "0.5.0"
8
8
  description = "A strict, zero-dependency CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,11 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.4.1"
5
+ __version__ = "0.5.0"
6
6
 
7
7
  __all__ = [
8
8
  "App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
9
- "Passthrough", "Result", "flag", "arg",
9
+ "Implies", "Passthrough", "Result", "flag", "arg",
10
10
  ]
11
11
 
12
12
  import contextlib
@@ -175,6 +175,15 @@ class Requires:
175
175
  depends_on: str
176
176
 
177
177
 
178
+ @dataclass
179
+ class Implies:
180
+ """When a trigger flag is provided, automatically set a target flag to a value."""
181
+
182
+ flag: str # trigger flag name
183
+ implies: str # target flag name
184
+ value: bool # value to set on target when trigger is present
185
+
186
+
178
187
  @dataclass
179
188
  class Passthrough:
180
189
  """Marks a command as passthrough -- all tokens after the command name are
@@ -194,7 +203,7 @@ class Command:
194
203
  args: list[Arg] = field(default_factory=list)
195
204
  tags: list[Tag] = field(default_factory=list)
196
205
  mutex: list[MutexGroup] = field(default_factory=list)
197
- dependencies: list[CoRequired | Requires] = field(default_factory=list)
206
+ dependencies: list[CoRequired | Requires | Implies] = field(default_factory=list)
198
207
  passthrough: Passthrough | None = None
199
208
 
200
209
  def __post_init__(self) -> None:
@@ -222,7 +231,7 @@ class Group:
222
231
  args: list[Arg] | None = None,
223
232
  tags: list[Tag] | None = None,
224
233
  mutex: list[MutexGroup] | None = None,
225
- dependencies: list[CoRequired | Requires] | None = None,
234
+ dependencies: list[CoRequired | Requires | Implies] | None = None,
226
235
  passthrough: Passthrough | None = None,
227
236
  ) -> Callable:
228
237
  """Decorator to register a command within this group."""
@@ -281,7 +290,7 @@ class App:
281
290
  args: list[Arg] | None = None,
282
291
  tags: list[Tag] | None = None,
283
292
  mutex: list[MutexGroup] | None = None,
284
- dependencies: list[CoRequired | Requires] | None = None,
293
+ dependencies: list[CoRequired | Requires | Implies] | None = None,
285
294
  passthrough: Passthrough | None = None,
286
295
  ) -> Callable:
287
296
  """Decorator to register a top-level command."""
@@ -849,6 +858,22 @@ def _parse_command(
849
858
  names = ", ".join(f"--{f.name}" for f in mg.flags)
850
859
  raise _ParseError(f"one of {names} is required")
851
860
 
861
+ # Step 4.55: resolve Implies dependencies (before dependency checks, so
862
+ # implied values participate in downstream CoRequired/Requires validation)
863
+ for dep in cmd.dependencies:
864
+ if isinstance(dep, Implies):
865
+ if dep.flag in cli_set:
866
+ if dep.implies in cli_set:
867
+ if cli_set[dep.implies] != dep.value:
868
+ neg = "no-" if not dep.value else ""
869
+ explicit_neg = "" if not dep.value else "no-"
870
+ raise _ParseError(
871
+ f"flag '--{dep.flag}' implies '--{neg}{dep.implies}', "
872
+ f"but '--{explicit_neg}{dep.implies}' was explicitly provided"
873
+ )
874
+ else:
875
+ cli_set[dep.implies] = dep.value
876
+
852
877
  # Step 4.6: enforce flag dependencies (before defaults, so cli_set only
853
878
  # contains values explicitly provided via CLI or env)
854
879
  for dep in cmd.dependencies:
@@ -974,7 +999,7 @@ def _build_and_validate_command(
974
999
  args: list[Arg] | None,
975
1000
  tags: list[Tag] | None,
976
1001
  mutex: list[MutexGroup] | None,
977
- dependencies: list[CoRequired | Requires] | None = None,
1002
+ dependencies: list[CoRequired | Requires | Implies] | None = None,
978
1003
  env_prefix: str | None,
979
1004
  global_flags: list[Flag] | None = None,
980
1005
  passthrough: Passthrough | None = None,
@@ -1182,6 +1207,41 @@ def _build_and_validate_command(
1182
1207
  f'command "{name}": Requires flag and depends_on cannot be '
1183
1208
  f'the same ("{dep.flag}")'
1184
1209
  )
1210
+ elif isinstance(dep, Implies):
1211
+ if dep.flag not in seen_flag_names:
1212
+ raise ValueError(
1213
+ f'command "{name}": Implies references unknown flag '
1214
+ f'"{dep.flag}"'
1215
+ )
1216
+ if dep.implies not in seen_flag_names:
1217
+ raise ValueError(
1218
+ f'command "{name}": Implies references unknown flag '
1219
+ f'"{dep.implies}"'
1220
+ )
1221
+ if dep.flag == dep.implies:
1222
+ raise ValueError(
1223
+ f'command "{name}": Implies flag and implies cannot be '
1224
+ f'the same ("{dep.flag}")'
1225
+ )
1226
+ # Look up the actual Flag objects to validate types
1227
+ all_flags_by_name = {f.name: f for f in all_flags}
1228
+ trigger_flag = all_flags_by_name[dep.flag]
1229
+ target_flag = all_flags_by_name[dep.implies]
1230
+ if trigger_flag.type is not bool:
1231
+ raise ValueError(
1232
+ f'command "{name}": Implies trigger flag "{dep.flag}" '
1233
+ f"must be type=bool"
1234
+ )
1235
+ if target_flag.type is not bool:
1236
+ raise ValueError(
1237
+ f'command "{name}": Implies target flag "{dep.implies}" '
1238
+ f"must be type=bool"
1239
+ )
1240
+ if not isinstance(dep.value, bool):
1241
+ raise ValueError(
1242
+ f'command "{name}": Implies value must be a bool, '
1243
+ f"got {type(dep.value).__name__!r}"
1244
+ )
1185
1245
 
1186
1246
  return Command(
1187
1247
  name=name,
@@ -1,4 +1,4 @@
1
- """Tests for CoRequired and Requires flag dependencies."""
1
+ """Tests for CoRequired, Requires, and Implies flag dependencies."""
2
2
 
3
3
  import pytest
4
4
 
@@ -398,3 +398,271 @@ def test_corequired_duplicate_flag_error():
398
398
  @strictcli.flag("output", type=str, help="output path", default=None)
399
399
  def cmd(output):
400
400
  pass
401
+
402
+
403
+ # ===========================================================================
404
+ # Implies tests
405
+ # ===========================================================================
406
+
407
+
408
+ # ---------------------------------------------------------------------------
409
+ # Implies: trigger set -> target auto-set to implied value
410
+ # ---------------------------------------------------------------------------
411
+
412
+
413
+ def test_implies_trigger_set_target_auto_set():
414
+ """Implies: when trigger is set, target is automatically set to implied value."""
415
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
416
+
417
+ @app.command(
418
+ "cmd", help="a command",
419
+ dependencies=[strictcli.Implies(flag="fast", implies="embeddings", value=False)],
420
+ )
421
+ @strictcli.flag("fast", type=bool, help="fast mode")
422
+ @strictcli.flag("embeddings", type=bool, help="use embeddings")
423
+ def cmd(fast, embeddings):
424
+ print(f"fast={fast} embeddings={embeddings}")
425
+
426
+ r = app.test(["cmd", "--fast"])
427
+ assert r.exit_code == 0
428
+ assert "fast=True" in r.stdout
429
+ assert "embeddings=False" in r.stdout
430
+
431
+
432
+ # ---------------------------------------------------------------------------
433
+ # Implies: trigger not set -> target gets normal default
434
+ # ---------------------------------------------------------------------------
435
+
436
+
437
+ def test_implies_trigger_not_set_target_gets_default():
438
+ """Implies: when trigger is not set, target gets its normal default."""
439
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
440
+
441
+ @app.command(
442
+ "cmd", help="a command",
443
+ dependencies=[strictcli.Implies(flag="fast", implies="embeddings", value=False)],
444
+ )
445
+ @strictcli.flag("fast", type=bool, help="fast mode")
446
+ @strictcli.flag("embeddings", type=bool, help="use embeddings", default=True)
447
+ def cmd(fast, embeddings):
448
+ print(f"fast={fast} embeddings={embeddings}")
449
+
450
+ r = app.test(["cmd"])
451
+ assert r.exit_code == 0
452
+ assert "fast=False" in r.stdout
453
+ assert "embeddings=True" in r.stdout
454
+
455
+
456
+ # ---------------------------------------------------------------------------
457
+ # Implies: explicit conflict -> parse error
458
+ # ---------------------------------------------------------------------------
459
+
460
+
461
+ def test_implies_explicit_conflict_error():
462
+ """Implies: trigger + contradicting explicit target -> error."""
463
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
464
+
465
+ @app.command(
466
+ "cmd", help="a command",
467
+ dependencies=[strictcli.Implies(flag="fast", implies="embeddings", value=False)],
468
+ )
469
+ @strictcli.flag("fast", type=bool, help="fast mode")
470
+ @strictcli.flag("embeddings", type=bool, help="use embeddings")
471
+ def cmd(fast, embeddings):
472
+ pass
473
+
474
+ r = app.test(["cmd", "--fast", "--embeddings"])
475
+ assert r.exit_code == 1
476
+ assert "implies" in r.stderr
477
+ assert "--fast" in r.stderr
478
+ assert "--no-embeddings" in r.stderr
479
+ assert "explicitly provided" in r.stderr
480
+
481
+
482
+ # ---------------------------------------------------------------------------
483
+ # Implies: explicit agreement -> no error
484
+ # ---------------------------------------------------------------------------
485
+
486
+
487
+ def test_implies_explicit_agreement_ok():
488
+ """Implies: trigger + matching explicit target -> OK."""
489
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
490
+
491
+ @app.command(
492
+ "cmd", help="a command",
493
+ dependencies=[strictcli.Implies(flag="fast", implies="embeddings", value=False)],
494
+ )
495
+ @strictcli.flag("fast", type=bool, help="fast mode")
496
+ @strictcli.flag("embeddings", type=bool, help="use embeddings")
497
+ def cmd(fast, embeddings):
498
+ print(f"fast={fast} embeddings={embeddings}")
499
+
500
+ r = app.test(["cmd", "--fast", "--no-embeddings"])
501
+ assert r.exit_code == 0
502
+ assert "fast=True" in r.stdout
503
+ assert "embeddings=False" in r.stdout
504
+
505
+
506
+ # ---------------------------------------------------------------------------
507
+ # Registration error: unknown trigger flag
508
+ # ---------------------------------------------------------------------------
509
+
510
+
511
+ def test_implies_unknown_trigger_flag_error():
512
+ """Implies: unknown trigger flag -> ValueError at registration."""
513
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
514
+
515
+ with pytest.raises(ValueError, match='Implies references unknown flag "nonexistent"'):
516
+
517
+ @app.command(
518
+ "cmd", help="a command",
519
+ dependencies=[strictcli.Implies(flag="nonexistent", implies="embeddings", value=False)],
520
+ )
521
+ @strictcli.flag("embeddings", type=bool, help="use embeddings")
522
+ def cmd(embeddings):
523
+ pass
524
+
525
+
526
+ # ---------------------------------------------------------------------------
527
+ # Registration error: unknown target flag
528
+ # ---------------------------------------------------------------------------
529
+
530
+
531
+ def test_implies_unknown_target_flag_error():
532
+ """Implies: unknown target flag -> ValueError at registration."""
533
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
534
+
535
+ with pytest.raises(ValueError, match='Implies references unknown flag "nonexistent"'):
536
+
537
+ @app.command(
538
+ "cmd", help="a command",
539
+ dependencies=[strictcli.Implies(flag="fast", implies="nonexistent", value=False)],
540
+ )
541
+ @strictcli.flag("fast", type=bool, help="fast mode")
542
+ def cmd(fast):
543
+ pass
544
+
545
+
546
+ # ---------------------------------------------------------------------------
547
+ # Registration error: self-implication
548
+ # ---------------------------------------------------------------------------
549
+
550
+
551
+ def test_implies_self_implication_error():
552
+ """Implies: flag == implies -> ValueError at registration."""
553
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
554
+
555
+ with pytest.raises(ValueError, match="cannot be the same"):
556
+
557
+ @app.command(
558
+ "cmd", help="a command",
559
+ dependencies=[strictcli.Implies(flag="fast", implies="fast", value=True)],
560
+ )
561
+ @strictcli.flag("fast", type=bool, help="fast mode")
562
+ def cmd(fast):
563
+ pass
564
+
565
+
566
+ # ---------------------------------------------------------------------------
567
+ # Registration error: trigger flag is not bool
568
+ # ---------------------------------------------------------------------------
569
+
570
+
571
+ def test_implies_trigger_not_bool_error():
572
+ """Implies: trigger flag not type=bool -> ValueError at registration."""
573
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
574
+
575
+ with pytest.raises(ValueError, match='trigger flag "name" must be type=bool'):
576
+
577
+ @app.command(
578
+ "cmd", help="a command",
579
+ dependencies=[strictcli.Implies(flag="name", implies="embeddings", value=False)],
580
+ )
581
+ @strictcli.flag("name", type=str, help="a name", default="")
582
+ @strictcli.flag("embeddings", type=bool, help="use embeddings")
583
+ def cmd(name, embeddings):
584
+ pass
585
+
586
+
587
+ # ---------------------------------------------------------------------------
588
+ # Registration error: target flag is not bool
589
+ # ---------------------------------------------------------------------------
590
+
591
+
592
+ def test_implies_target_not_bool_error():
593
+ """Implies: target flag not type=bool -> ValueError at registration."""
594
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
595
+
596
+ with pytest.raises(ValueError, match='target flag "name" must be type=bool'):
597
+
598
+ @app.command(
599
+ "cmd", help="a command",
600
+ dependencies=[strictcli.Implies(flag="fast", implies="name", value=False)],
601
+ )
602
+ @strictcli.flag("fast", type=bool, help="fast mode")
603
+ @strictcli.flag("name", type=str, help="a name", default="")
604
+ def cmd(fast, name):
605
+ pass
606
+
607
+
608
+ # ---------------------------------------------------------------------------
609
+ # Env var trigger: trigger set via env var also triggers implication
610
+ # ---------------------------------------------------------------------------
611
+
612
+
613
+ def test_implies_env_var_trigger(monkeypatch):
614
+ """Implies: trigger set via env var -> target auto-set."""
615
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
616
+
617
+ @app.command(
618
+ "cmd", help="a command",
619
+ dependencies=[strictcli.Implies(flag="fast", implies="embeddings", value=False)],
620
+ )
621
+ @strictcli.flag("fast", type=bool, help="fast mode",
622
+ env="TEST_IMPLIES_FAST", prefixed=False)
623
+ @strictcli.flag("embeddings", type=bool, help="use embeddings")
624
+ def cmd(fast, embeddings):
625
+ print(f"fast={fast} embeddings={embeddings}")
626
+
627
+ monkeypatch.setenv("TEST_IMPLIES_FAST", "true")
628
+ r = app.test(["cmd"])
629
+ assert r.exit_code == 0
630
+ assert "fast=True" in r.stdout
631
+ assert "embeddings=False" in r.stdout
632
+
633
+
634
+ # ---------------------------------------------------------------------------
635
+ # Interaction: Implies + Requires on same command
636
+ # ---------------------------------------------------------------------------
637
+
638
+
639
+ def test_implies_with_requires_interaction():
640
+ """Implies + Requires on same command work together."""
641
+ app = strictcli.App(name="test", version="1.0.0", help="test app")
642
+
643
+ @app.command(
644
+ "cmd", help="a command",
645
+ dependencies=[
646
+ strictcli.Implies(flag="fast", implies="embeddings", value=False),
647
+ strictcli.Requires(flag="output", depends_on="fast"),
648
+ ],
649
+ )
650
+ @strictcli.flag("fast", type=bool, help="fast mode")
651
+ @strictcli.flag("embeddings", type=bool, help="use embeddings")
652
+ @strictcli.flag("output", type=str, help="output path", default=None)
653
+ def cmd(fast, embeddings, output):
654
+ print(f"fast={fast} embeddings={embeddings} output={output}")
655
+
656
+ # --fast --output works (Requires satisfied, Implies sets embeddings=False)
657
+ r = app.test(["cmd", "--fast", "--output", "file.txt"])
658
+ assert r.exit_code == 0
659
+ assert "fast=True" in r.stdout
660
+ assert "embeddings=False" in r.stdout
661
+ assert "output=file.txt" in r.stdout
662
+
663
+ # --output without --fast -> error (Requires violation)
664
+ r = app.test(["cmd", "--output", "file.txt"])
665
+ assert r.exit_code == 1
666
+ assert "requires" in r.stderr
667
+ assert "--output" in r.stderr
668
+ assert "--fast" in r.stderr
@@ -232,7 +232,7 @@ wheels = [
232
232
 
233
233
  [[package]]
234
234
  name = "strictcli"
235
- version = "0.4.0"
235
+ version = "0.4.1"
236
236
  source = { editable = "." }
237
237
 
238
238
  [package.dev-dependencies]
@@ -1 +0,0 @@
1
- 9f2ad6a550f949c15aeb8301b16a999601dd3a79
@@ -1 +0,0 @@
1
- 0.31.0
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