click-extended 0.0.1__tar.gz → 0.0.3__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 (27) hide show
  1. {click_extended-0.0.1/click_extended.egg-info → click_extended-0.0.3}/PKG-INFO +16 -5
  2. click_extended-0.0.3/README.md +38 -0
  3. {click_extended-0.0.1 → click_extended-0.0.3/click_extended.egg-info}/PKG-INFO +16 -5
  4. {click_extended-0.0.1 → click_extended-0.0.3}/click_extended.egg-info/SOURCES.txt +1 -0
  5. {click_extended-0.0.1 → click_extended-0.0.3}/pyproject.toml +1 -1
  6. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_env.py +264 -0
  7. click_extended-0.0.3/tests/test_global_node.py +749 -0
  8. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_tree.py +135 -0
  9. click_extended-0.0.1/README.md +0 -27
  10. {click_extended-0.0.1 → click_extended-0.0.3}/AUTHORS.md +0 -0
  11. {click_extended-0.0.1 → click_extended-0.0.3}/LICENSE +0 -0
  12. {click_extended-0.0.1 → click_extended-0.0.3}/click_extended/__init__.py +0 -0
  13. {click_extended-0.0.1 → click_extended-0.0.3}/click_extended/errors.py +0 -0
  14. {click_extended-0.0.1 → click_extended-0.0.3}/click_extended/types.py +0 -0
  15. {click_extended-0.0.1 → click_extended-0.0.3}/click_extended.egg-info/dependency_links.txt +0 -0
  16. {click_extended-0.0.1 → click_extended-0.0.3}/click_extended.egg-info/requires.txt +0 -0
  17. {click_extended-0.0.1 → click_extended-0.0.3}/click_extended.egg-info/top_level.txt +0 -0
  18. {click_extended-0.0.1 → click_extended-0.0.3}/setup.cfg +0 -0
  19. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_argument.py +0 -0
  20. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_child_node.py +0 -0
  21. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_command.py +0 -0
  22. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_group.py +0 -0
  23. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_option.py +0 -0
  24. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_parent_node.py +0 -0
  25. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_root_node.py +0 -0
  26. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_tag.py +0 -0
  27. {click_extended-0.0.1 → click_extended-0.0.3}/tests/test_transform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: click_extended
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: An extension to Click with additional features like automatic async support, aliasing and a modular decorator system.
5
5
  Author-email: Marcus Fredriksson <marcus@marcusfredriksson.com>
6
6
  License: MIT License
@@ -65,7 +65,14 @@ Dynamic: license-file
65
65
 
66
66
  # Click Extended
67
67
 
68
- TBD
68
+ An extension of the [Click](https://github.com/pallets/click) library with additional features like aliasing, asynchronous support, an extended decorator system and more.
69
+
70
+ ## Features
71
+
72
+ - **Aliasing**: Add multiple aliases to a group or command.
73
+ - **Async supprt**: Automatically run both synchronous and asynchronous functions.
74
+ - **Extended decorator system**: Create or use pre-made validation and transformation decorators, inject values into the context and more.
75
+ - **Environment variables**: Automatically validate and inject environment variables into the function.
69
76
 
70
77
  ## Installation
71
78
 
@@ -75,16 +82,20 @@ pip install click-extended
75
82
 
76
83
  ## Requirements
77
84
 
78
- TBD
85
+ - **Python**: 3.10 or higher
79
86
 
80
- ## License
87
+ ## Usage
81
88
 
82
- This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
89
+ TBD
83
90
 
84
91
  ## Contributing
85
92
 
86
93
  TBD
87
94
 
95
+ ## License
96
+
97
+ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
98
+
88
99
  ## Acknowledgements
89
100
 
90
101
  This project is built on top of the [Click](https://github.com/pallets/click) library.
@@ -0,0 +1,38 @@
1
+ ![Banner](./assets/click-extended-banner.png)
2
+
3
+ # Click Extended
4
+
5
+ An extension of the [Click](https://github.com/pallets/click) library with additional features like aliasing, asynchronous support, an extended decorator system and more.
6
+
7
+ ## Features
8
+
9
+ - **Aliasing**: Add multiple aliases to a group or command.
10
+ - **Async supprt**: Automatically run both synchronous and asynchronous functions.
11
+ - **Extended decorator system**: Create or use pre-made validation and transformation decorators, inject values into the context and more.
12
+ - **Environment variables**: Automatically validate and inject environment variables into the function.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install click-extended
18
+ ```
19
+
20
+ ## Requirements
21
+
22
+ - **Python**: 3.10 or higher
23
+
24
+ ## Usage
25
+
26
+ TBD
27
+
28
+ ## Contributing
29
+
30
+ TBD
31
+
32
+ ## License
33
+
34
+ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
35
+
36
+ ## Acknowledgements
37
+
38
+ This project is built on top of the [Click](https://github.com/pallets/click) library.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: click_extended
3
- Version: 0.0.1
3
+ Version: 0.0.3
4
4
  Summary: An extension to Click with additional features like automatic async support, aliasing and a modular decorator system.
5
5
  Author-email: Marcus Fredriksson <marcus@marcusfredriksson.com>
6
6
  License: MIT License
@@ -65,7 +65,14 @@ Dynamic: license-file
65
65
 
66
66
  # Click Extended
67
67
 
68
- TBD
68
+ An extension of the [Click](https://github.com/pallets/click) library with additional features like aliasing, asynchronous support, an extended decorator system and more.
69
+
70
+ ## Features
71
+
72
+ - **Aliasing**: Add multiple aliases to a group or command.
73
+ - **Async supprt**: Automatically run both synchronous and asynchronous functions.
74
+ - **Extended decorator system**: Create or use pre-made validation and transformation decorators, inject values into the context and more.
75
+ - **Environment variables**: Automatically validate and inject environment variables into the function.
69
76
 
70
77
  ## Installation
71
78
 
@@ -75,16 +82,20 @@ pip install click-extended
75
82
 
76
83
  ## Requirements
77
84
 
78
- TBD
85
+ - **Python**: 3.10 or higher
79
86
 
80
- ## License
87
+ ## Usage
81
88
 
82
- This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
89
+ TBD
83
90
 
84
91
  ## Contributing
85
92
 
86
93
  TBD
87
94
 
95
+ ## License
96
+
97
+ This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
98
+
88
99
  ## Acknowledgements
89
100
 
90
101
  This project is built on top of the [Click](https://github.com/pallets/click) library.
@@ -14,6 +14,7 @@ tests/test_argument.py
14
14
  tests/test_child_node.py
15
15
  tests/test_command.py
16
16
  tests/test_env.py
17
+ tests/test_global_node.py
17
18
  tests/test_group.py
18
19
  tests/test_option.py
19
20
  tests/test_parent_node.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "click_extended"
3
- version = "0.0.1"
3
+ version = "0.0.3"
4
4
  description = "An extension to Click with additional features like automatic async support, aliasing and a modular decorator system."
5
5
  authors = [
6
6
  { name = "Marcus Fredriksson", email = "marcus@marcusfredriksson.com" },
@@ -629,3 +629,267 @@ class TestEnvIntegration:
629
629
  with patch.dict(os.environ, {}, clear=True):
630
630
  e = Env(name="test", env_name="NONEXISTENT_VAR")
631
631
  assert e.get_raw_value() is None
632
+
633
+
634
+ class TestEnvCheckRequired:
635
+ """Test Env.check_required functionality."""
636
+
637
+ def test_check_required_returns_none_when_env_var_set(self) -> None:
638
+ """Test check_required returns None when required env var is set."""
639
+ with patch.dict(os.environ, {"TEST_VAR": "value"}):
640
+ e = Env(name="test", env_name="TEST_VAR", required=True)
641
+ assert e.check_required() is None
642
+
643
+ def test_check_required_returns_env_name_when_missing(self) -> None:
644
+ """Test check_required returns env_name when required var is missing."""
645
+ with patch.dict(os.environ, {}, clear=True):
646
+ e = Env(name="test", env_name="MISSING_VAR", required=True)
647
+ assert e.check_required() == "MISSING_VAR"
648
+
649
+ def test_check_required_returns_none_when_not_required(self) -> None:
650
+ """Test check_required returns None when env var is not required."""
651
+ with patch.dict(os.environ, {}, clear=True):
652
+ e = Env(name="test", env_name="OPTIONAL_VAR", required=False)
653
+ assert e.check_required() is None
654
+
655
+ def test_check_required_with_required_false_and_env_set(self) -> None:
656
+ """Test check_required with optional env var that is set."""
657
+ with patch.dict(os.environ, {"OPTIONAL_VAR": "value"}):
658
+ e = Env(name="test", env_name="OPTIONAL_VAR", required=False)
659
+ assert e.check_required() is None
660
+
661
+ def test_check_required_ignores_default_value(self) -> None:
662
+ """Test check_required ignores default value when required=True."""
663
+ with patch.dict(os.environ, {}, clear=True):
664
+ e = Env(
665
+ name="test",
666
+ env_name="MISSING_VAR",
667
+ required=True,
668
+ default="default_value",
669
+ )
670
+ assert e.check_required() == "MISSING_VAR"
671
+
672
+ def test_check_required_does_not_raise_exception(self) -> None:
673
+ """Test check_required does not raise exception (unlike get_raw_value)."""
674
+ with patch.dict(os.environ, {}, clear=True):
675
+ e = Env(name="test", env_name="MISSING_VAR", required=True)
676
+ result = e.check_required()
677
+ assert result == "MISSING_VAR"
678
+
679
+ def test_check_required_multiple_times(self) -> None:
680
+ """Test check_required can be called multiple times."""
681
+ with patch.dict(os.environ, {}, clear=True):
682
+ e = Env(name="test", env_name="MISSING_VAR", required=True)
683
+ assert e.check_required() == "MISSING_VAR"
684
+ assert e.check_required() == "MISSING_VAR"
685
+ assert e.check_required() == "MISSING_VAR"
686
+
687
+ def test_check_required_with_empty_string_env_var(self) -> None:
688
+ """Test check_required treats empty string as set."""
689
+ with patch.dict(os.environ, {"EMPTY_VAR": ""}):
690
+ e = Env(name="test", env_name="EMPTY_VAR", required=True)
691
+ assert e.check_required() is None
692
+
693
+
694
+ class TestEnvMultipleMissingVariables:
695
+ """Test multiple missing required environment variables error handling."""
696
+
697
+ def test_single_missing_variable_error_message(self) -> None:
698
+ """Test error message for single missing required variable."""
699
+ from click_extended.core.command import command
700
+
701
+ @command()
702
+ @env("MISSING_VAR_1", required=True)
703
+ def test_cmd(**kwargs: Any) -> None:
704
+ pass
705
+
706
+ with patch.dict(os.environ, {}, clear=True):
707
+ try:
708
+ test_cmd([], standalone_mode=False)
709
+ assert False, "Expected ValueError"
710
+ except ValueError as ex:
711
+ error_msg = str(ex)
712
+ assert (
713
+ "Required environment variable 'MISSING_VAR_1' is not set"
714
+ in error_msg
715
+ )
716
+ assert " and " not in error_msg
717
+
718
+ def test_two_missing_variables_error_message(self) -> None:
719
+ """Test error message for two missing required variables."""
720
+ from click_extended.core.command import command
721
+
722
+ @command()
723
+ @env("MISSING_VAR_1", required=True)
724
+ @env("MISSING_VAR_2", required=True)
725
+ def test_cmd(**kwargs: Any) -> None:
726
+ pass
727
+
728
+ with patch.dict(os.environ, {}, clear=True):
729
+ try:
730
+ test_cmd([], standalone_mode=False)
731
+ assert False, "Expected ValueError"
732
+ except ValueError as ex:
733
+ error_msg = str(ex)
734
+ assert "Required environment variables" in error_msg
735
+ assert "'MISSING_VAR_1'" in error_msg
736
+ assert "'MISSING_VAR_2'" in error_msg
737
+ assert " and " in error_msg
738
+ assert error_msg.count("'") == 4
739
+
740
+ def test_three_missing_variables_error_message(self) -> None:
741
+ """Test error message for three missing required variables."""
742
+ from click_extended.core.command import command
743
+
744
+ @command()
745
+ @env("MISSING_VAR_1", required=True)
746
+ @env("MISSING_VAR_2", required=True)
747
+ @env("MISSING_VAR_3", required=True)
748
+ def test_cmd(**kwargs: Any) -> None:
749
+ pass
750
+
751
+ with patch.dict(os.environ, {}, clear=True):
752
+ try:
753
+ test_cmd([], standalone_mode=False)
754
+ assert False, "Expected ValueError"
755
+ except ValueError as ex:
756
+ error_msg = str(ex)
757
+ assert "Required environment variables" in error_msg
758
+ assert "'MISSING_VAR_1'" in error_msg
759
+ assert "'MISSING_VAR_2'" in error_msg
760
+ assert "'MISSING_VAR_3'" in error_msg
761
+ assert " and " in error_msg
762
+ assert "," in error_msg
763
+
764
+ def test_mixed_required_and_optional_variables(self) -> None:
765
+ """Test only required missing variables are reported."""
766
+ from click_extended.core.command import command
767
+
768
+ @command()
769
+ @env("REQUIRED_VAR", required=True)
770
+ @env("OPTIONAL_VAR", required=False)
771
+ @env("REQUIRED_VAR_2", required=True)
772
+ def test_cmd(**kwargs: Any) -> None:
773
+ pass
774
+
775
+ with patch.dict(os.environ, {}, clear=True):
776
+ try:
777
+ test_cmd([], standalone_mode=False)
778
+ assert False, "Expected ValueError"
779
+ except ValueError as ex:
780
+ error_msg = str(ex)
781
+ assert "'REQUIRED_VAR'" in error_msg
782
+ assert "'REQUIRED_VAR_2'" in error_msg
783
+ assert "'OPTIONAL_VAR'" not in error_msg
784
+
785
+ def test_some_required_variables_set(self) -> None:
786
+ """Test error message when some required variables are set."""
787
+ from click_extended.core.command import command
788
+
789
+ @command()
790
+ @env("SET_VAR", required=True)
791
+ @env("MISSING_VAR_1", required=True)
792
+ @env("MISSING_VAR_2", required=True)
793
+ def test_cmd(**kwargs: Any) -> None:
794
+ pass
795
+
796
+ with patch.dict(os.environ, {"SET_VAR": "value"}, clear=True):
797
+ try:
798
+ test_cmd([], standalone_mode=False)
799
+ assert False, "Expected ValueError"
800
+ except ValueError as ex:
801
+ error_msg = str(ex)
802
+ assert "'MISSING_VAR_1'" in error_msg
803
+ assert "'MISSING_VAR_2'" in error_msg
804
+ assert "'SET_VAR'" not in error_msg
805
+
806
+ def test_all_required_variables_set_no_error(self) -> None:
807
+ """Test no error when all required variables are set."""
808
+ from click_extended.core.command import command
809
+
810
+ call_count = 0
811
+
812
+ @command()
813
+ @env("VAR_1", required=True)
814
+ @env("VAR_2", required=True)
815
+ @env("VAR_3", required=True)
816
+ def test_cmd(**kwargs: Any) -> None:
817
+ nonlocal call_count
818
+ call_count += 1
819
+
820
+ with patch.dict(
821
+ os.environ,
822
+ {"VAR_1": "v1", "VAR_2": "v2", "VAR_3": "v3"},
823
+ clear=True,
824
+ ):
825
+ test_cmd([], standalone_mode=False)
826
+ assert call_count == 1
827
+
828
+ def test_error_raised_before_processing(self) -> None:
829
+ """Test that validation happens before any processing."""
830
+ from click_extended.core.command import command
831
+
832
+ process_called = False
833
+
834
+ class TestChild(ChildNode): # type: ignore
835
+ def process(
836
+ self,
837
+ value: Any,
838
+ *args: Any,
839
+ siblings: list[str],
840
+ tags: dict[str, Tag],
841
+ parent: ParentNode | Tag,
842
+ **kwargs: Any,
843
+ ) -> Any:
844
+ nonlocal process_called
845
+ process_called = True
846
+ return value
847
+
848
+ @command()
849
+ @env("MISSING_VAR", required=True)
850
+ def test_cmd(**kwargs: Any) -> None:
851
+ pass
852
+
853
+ with patch.dict(os.environ, {}, clear=True):
854
+ try:
855
+ test_cmd([], standalone_mode=False)
856
+ assert False, "Expected ValueError"
857
+ except ValueError:
858
+ assert not process_called
859
+
860
+ def test_error_message_grammar_for_one_variable(self) -> None:
861
+ """Test proper grammar (singular) for one missing variable."""
862
+ from click_extended.core.command import command
863
+
864
+ @command()
865
+ @env("MISSING_VAR", required=True)
866
+ def test_cmd(**kwargs: Any) -> None:
867
+ pass
868
+
869
+ with patch.dict(os.environ, {}, clear=True):
870
+ try:
871
+ test_cmd([], standalone_mode=False)
872
+ assert False, "Expected ValueError"
873
+ except ValueError as ex:
874
+ error_msg = str(ex)
875
+ assert "variable" in error_msg.lower()
876
+ assert "is not set" in error_msg
877
+
878
+ def test_error_message_grammar_for_multiple_variables(self) -> None:
879
+ """Test proper grammar (plural) for multiple missing variables."""
880
+ from click_extended.core.command import command
881
+
882
+ @command()
883
+ @env("VAR_1", required=True)
884
+ @env("VAR_2", required=True)
885
+ def test_cmd(**kwargs: Any) -> None:
886
+ pass
887
+
888
+ with patch.dict(os.environ, {}, clear=True):
889
+ try:
890
+ test_cmd([], standalone_mode=False)
891
+ assert False, "Expected ValueError"
892
+ except ValueError as ex:
893
+ error_msg = str(ex)
894
+ assert "variables" in error_msg.lower()
895
+ assert "are not set" in error_msg