click-extended 0.0.2__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.2/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.2 → click_extended-0.0.3/click_extended.egg-info}/PKG-INFO +16 -5
  4. {click_extended-0.0.2 → click_extended-0.0.3}/click_extended.egg-info/SOURCES.txt +1 -0
  5. {click_extended-0.0.2 → click_extended-0.0.3}/pyproject.toml +1 -1
  6. click_extended-0.0.3/tests/test_global_node.py +749 -0
  7. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_tree.py +135 -0
  8. click_extended-0.0.2/README.md +0 -27
  9. {click_extended-0.0.2 → click_extended-0.0.3}/AUTHORS.md +0 -0
  10. {click_extended-0.0.2 → click_extended-0.0.3}/LICENSE +0 -0
  11. {click_extended-0.0.2 → click_extended-0.0.3}/click_extended/__init__.py +0 -0
  12. {click_extended-0.0.2 → click_extended-0.0.3}/click_extended/errors.py +0 -0
  13. {click_extended-0.0.2 → click_extended-0.0.3}/click_extended/types.py +0 -0
  14. {click_extended-0.0.2 → click_extended-0.0.3}/click_extended.egg-info/dependency_links.txt +0 -0
  15. {click_extended-0.0.2 → click_extended-0.0.3}/click_extended.egg-info/requires.txt +0 -0
  16. {click_extended-0.0.2 → click_extended-0.0.3}/click_extended.egg-info/top_level.txt +0 -0
  17. {click_extended-0.0.2 → click_extended-0.0.3}/setup.cfg +0 -0
  18. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_argument.py +0 -0
  19. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_child_node.py +0 -0
  20. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_command.py +0 -0
  21. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_env.py +0 -0
  22. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_group.py +0 -0
  23. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_option.py +0 -0
  24. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_parent_node.py +0 -0
  25. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_root_node.py +0 -0
  26. {click_extended-0.0.2 → click_extended-0.0.3}/tests/test_tag.py +0 -0
  27. {click_extended-0.0.2 → 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.2
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.2
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.2"
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" },
@@ -0,0 +1,749 @@
1
+ """Tests for the GlobalNode class and global node registration."""
2
+
3
+ from typing import Any
4
+
5
+ import pytest
6
+
7
+ from click_extended.core._global_node import GlobalNode
8
+ from click_extended.core._parent_node import ParentNode
9
+ from click_extended.core._root_node import RootNode
10
+ from click_extended.core._tree import Tree, get_pending_nodes, queue_global
11
+ from click_extended.core.tag import Tag
12
+
13
+
14
+ class DummyRootNode(RootNode):
15
+ """Dummy RootNode for testing."""
16
+
17
+ @classmethod
18
+ def _get_click_decorator(cls) -> Any:
19
+ """Return a dummy decorator."""
20
+
21
+ def outer(**kwargs: Any) -> Any:
22
+ def inner(f: Any) -> Any:
23
+ return f
24
+
25
+ return inner
26
+
27
+ return outer
28
+
29
+ @classmethod
30
+ def _get_click_cls(cls) -> Any:
31
+ """Return a dummy class."""
32
+ return object
33
+
34
+
35
+ class DummyParentNode(ParentNode):
36
+ """Dummy ParentNode for testing."""
37
+
38
+ def __init__(self, name: str, **kwargs: Any) -> None:
39
+ super().__init__(name=name, **kwargs)
40
+
41
+ def get_raw_value(self) -> Any:
42
+ """Return a test value."""
43
+ return "test_value"
44
+
45
+
46
+ class ConcreteGlobalNode(GlobalNode):
47
+ """Concrete GlobalNode implementation for testing."""
48
+
49
+ def __init__(
50
+ self, name: str | None = None, delay: bool = False, **kwargs: Any
51
+ ) -> None:
52
+ """Initialize with optional return value."""
53
+ super().__init__(name=name, delay=delay)
54
+ self.return_value = kwargs.get("return_value", None)
55
+ self.call_count = 0
56
+ self.last_call_args: dict[str, Any] = {}
57
+
58
+ def process(
59
+ self,
60
+ tree: Tree,
61
+ root: RootNode,
62
+ parents: list[ParentNode],
63
+ tags: dict[str, Tag],
64
+ globals: list[GlobalNode],
65
+ call_args: tuple[Any, ...],
66
+ call_kwargs: dict[str, Any],
67
+ *args: Any,
68
+ **kwargs: Any,
69
+ ) -> Any:
70
+ """Record the call and return configured value."""
71
+ self.call_count += 1
72
+ self.last_call_args = {
73
+ "tree": tree,
74
+ "root": root,
75
+ "parents": parents,
76
+ "tags": tags,
77
+ "globals": globals,
78
+ "call_args": call_args,
79
+ "call_kwargs": call_kwargs,
80
+ "args": args,
81
+ "kwargs": kwargs,
82
+ }
83
+ return self.return_value
84
+
85
+
86
+ class TestGlobalNodeInitialization:
87
+ """Tests for GlobalNode initialization."""
88
+
89
+ def test_init_with_name(self) -> None:
90
+ """Test GlobalNode initialization with injection name."""
91
+ node = ConcreteGlobalNode(name="test_var")
92
+
93
+ assert node.inject_name == "test_var"
94
+ assert node.delay is False
95
+ assert node.call_count == 0
96
+
97
+ def test_init_without_name(self) -> None:
98
+ """Test GlobalNode initialization in observer mode."""
99
+ node = ConcreteGlobalNode()
100
+
101
+ assert node.inject_name is None
102
+ assert node.delay is False
103
+
104
+ def test_init_with_delay(self) -> None:
105
+ """Test GlobalNode initialization with delay parameter."""
106
+ node = ConcreteGlobalNode(delay=True)
107
+
108
+ assert node.inject_name is None
109
+ assert node.delay is True
110
+
111
+ def test_init_with_name_and_delay(self) -> None:
112
+ """Test GlobalNode initialization with both name and delay."""
113
+ node = ConcreteGlobalNode(name="var", delay=True)
114
+
115
+ assert node.inject_name == "var"
116
+ assert node.delay is True
117
+
118
+ def test_init_creates_unique_internal_name(self) -> None:
119
+ """Test that GlobalNode creates unique internal names."""
120
+ node1 = ConcreteGlobalNode()
121
+ node2 = ConcreteGlobalNode()
122
+
123
+ assert node1.name != node2.name
124
+ assert "_global_" in node1.name
125
+ assert "_global_" in node2.name
126
+
127
+ def test_init_uses_inject_name_as_internal_name(self) -> None:
128
+ """Test that inject_name is used as internal name when provided."""
129
+ node = ConcreteGlobalNode(name="test_var")
130
+
131
+ assert node.name == "test_var"
132
+ assert node.inject_name == "test_var"
133
+
134
+ def test_init_stores_process_kwargs(self) -> None:
135
+ """Test that initialization stores process_kwargs."""
136
+ node = ConcreteGlobalNode(return_value="test")
137
+
138
+ assert node.process_kwargs == {}
139
+ assert node.return_value == "test"
140
+
141
+
142
+ class TestGlobalNodeProcess:
143
+ """Tests for GlobalNode.process method."""
144
+
145
+ def test_process_is_abstract(self) -> None:
146
+ """Test that GlobalNode.process is abstract."""
147
+
148
+ with pytest.raises(TypeError):
149
+ GlobalNode() # type: ignore
150
+
151
+ def test_process_receives_tree(self) -> None:
152
+ """Test that process receives the tree structure."""
153
+ tree = Tree()
154
+ root = DummyRootNode(name="root")
155
+ tree.root = root
156
+
157
+ node = ConcreteGlobalNode()
158
+ node.process(tree, root, [], {}, [], (), {})
159
+
160
+ assert node.last_call_args["tree"] is tree
161
+
162
+ def test_process_receives_root(self) -> None:
163
+ """Test that process receives the root node."""
164
+ tree = Tree()
165
+ root = DummyRootNode(name="root")
166
+
167
+ node = ConcreteGlobalNode()
168
+ node.process(tree, root, [], {}, [], (), {})
169
+
170
+ assert node.last_call_args["root"] is root
171
+
172
+ def test_process_receives_parents(self) -> None:
173
+ """Test that process receives parent nodes list."""
174
+ tree = Tree()
175
+ root = DummyRootNode(name="root")
176
+ parent1 = DummyParentNode(name="parent1")
177
+ parent2 = DummyParentNode(name="parent2")
178
+
179
+ node = ConcreteGlobalNode()
180
+ node.process(tree, root, [parent1, parent2], {}, [], (), {})
181
+
182
+ assert len(node.last_call_args["parents"]) == 2
183
+ assert node.last_call_args["parents"][0] is parent1
184
+ assert node.last_call_args["parents"][1] is parent2
185
+
186
+ def test_process_receives_tags(self) -> None:
187
+ """Test that process receives tags dictionary."""
188
+ tree = Tree()
189
+ root = DummyRootNode(name="root")
190
+ tag = Tag(name="test_tag")
191
+
192
+ node = ConcreteGlobalNode()
193
+ node.process(tree, root, [], {"test_tag": tag}, [], (), {})
194
+
195
+ assert "test_tag" in node.last_call_args["tags"]
196
+ assert node.last_call_args["tags"]["test_tag"] is tag
197
+
198
+ def test_process_receives_globals_list(self) -> None:
199
+ """Test that process receives globals list including itself."""
200
+ tree = Tree()
201
+ root = DummyRootNode(name="root")
202
+ node1 = ConcreteGlobalNode(name="node1")
203
+ node2 = ConcreteGlobalNode(name="node2")
204
+
205
+ node1.process(tree, root, [], {}, [node1, node2], (), {})
206
+
207
+ assert len(node1.last_call_args["globals"]) == 2
208
+
209
+ def test_process_receives_call_args(self) -> None:
210
+ """Test that process receives call arguments."""
211
+ tree = Tree()
212
+ root = DummyRootNode(name="root")
213
+ call_args = ("arg1", "arg2")
214
+
215
+ node = ConcreteGlobalNode()
216
+ node.process(tree, root, [], {}, [], call_args, {})
217
+
218
+ assert node.last_call_args["call_args"] == call_args
219
+
220
+ def test_process_receives_call_kwargs(self) -> None:
221
+ """Test that process receives call keyword arguments."""
222
+ tree = Tree()
223
+ root = DummyRootNode(name="root")
224
+ call_kwargs = {"key1": "value1", "key2": "value2"}
225
+
226
+ node = ConcreteGlobalNode()
227
+ node.process(tree, root, [], {}, [], (), call_kwargs)
228
+
229
+ assert node.last_call_args["call_kwargs"] == call_kwargs
230
+
231
+ def test_process_receives_additional_args(self) -> None:
232
+ """Test that process receives additional positional arguments."""
233
+ tree = Tree()
234
+ root = DummyRootNode(name="root")
235
+
236
+ node = ConcreteGlobalNode()
237
+ node.process(tree, root, [], {}, [], (), {}, "extra1", "extra2")
238
+
239
+ assert node.last_call_args["args"] == ("extra1", "extra2")
240
+
241
+ def test_process_receives_additional_kwargs(self) -> None:
242
+ """Test that process receives additional keyword arguments."""
243
+ tree = Tree()
244
+ root = DummyRootNode(name="root")
245
+
246
+ node = ConcreteGlobalNode()
247
+ node.process(tree, root, [], {}, [], (), {}, extra_key="extra_value")
248
+
249
+ assert node.last_call_args["kwargs"]["extra_key"] == "extra_value"
250
+
251
+ def test_process_return_value(self) -> None:
252
+ """Test that process returns configured value."""
253
+ tree = Tree()
254
+ root = DummyRootNode(name="root")
255
+
256
+ node = ConcreteGlobalNode(return_value={"test": "data"})
257
+ result = node.process(tree, root, [], {}, [], (), {})
258
+
259
+ assert result == {"test": "data"}
260
+
261
+ def test_process_increments_call_count(self) -> None:
262
+ """Test that process increments call count."""
263
+ tree = Tree()
264
+ root = DummyRootNode(name="root")
265
+
266
+ node = ConcreteGlobalNode()
267
+
268
+ assert node.call_count == 0
269
+ node.process(tree, root, [], {}, [], (), {})
270
+ assert node.call_count == 1
271
+ node.process(tree, root, [], {}, [], (), {})
272
+ assert node.call_count == 2
273
+
274
+
275
+ class TestGlobalNodeAsDecorator:
276
+ """Tests for GlobalNode.as_decorator class method."""
277
+
278
+ def setup_method(self) -> None:
279
+ """Clear pending nodes before each test."""
280
+ get_pending_nodes()
281
+
282
+ def test_as_decorator_returns_callable(self) -> None:
283
+ """Test that as_decorator returns a callable."""
284
+ decorator = ConcreteGlobalNode.as_decorator()
285
+
286
+ assert callable(decorator)
287
+
288
+ def test_as_decorator_with_name(self) -> None:
289
+ """Test as_decorator with injection name."""
290
+ decorator = ConcreteGlobalNode.as_decorator("test_var")
291
+
292
+ def test_func() -> None:
293
+ pass
294
+
295
+ result = decorator(test_func)
296
+ assert callable(result)
297
+
298
+ def test_as_decorator_without_name(self) -> None:
299
+ """Test as_decorator in observer mode."""
300
+ decorator = ConcreteGlobalNode.as_decorator()
301
+
302
+ def test_func() -> None:
303
+ pass
304
+
305
+ result = decorator(test_func)
306
+ assert callable(result)
307
+
308
+ def test_as_decorator_with_delay(self) -> None:
309
+ """Test as_decorator with delay parameter."""
310
+ decorator = ConcreteGlobalNode.as_decorator(delay=True)
311
+
312
+ def test_func() -> None:
313
+ pass
314
+
315
+ result = decorator(test_func)
316
+ assert callable(result)
317
+
318
+ def test_as_decorator_with_name_and_delay(self) -> None:
319
+ """Test as_decorator with both name and delay."""
320
+ decorator = ConcreteGlobalNode.as_decorator("var", delay=True)
321
+
322
+ def test_func() -> None:
323
+ pass
324
+
325
+ result = decorator(test_func)
326
+ assert callable(result)
327
+
328
+ def test_as_decorator_queues_node(self) -> None:
329
+ """Test that as_decorator queues the global node."""
330
+ decorator = ConcreteGlobalNode.as_decorator("test")
331
+
332
+ def test_func() -> None:
333
+ pass
334
+
335
+ decorator(test_func)
336
+ pending = get_pending_nodes()
337
+
338
+ assert len(pending) == 1
339
+ assert pending[0][0] == "global"
340
+ assert isinstance(pending[0][1], ConcreteGlobalNode)
341
+
342
+ def test_as_decorator_stores_kwargs(self) -> None:
343
+ """Test that as_decorator stores additional kwargs."""
344
+ decorator = ConcreteGlobalNode.as_decorator(
345
+ "test", custom_param="value"
346
+ )
347
+
348
+ def test_func() -> None:
349
+ pass
350
+
351
+ decorator(test_func)
352
+ pending = get_pending_nodes()
353
+
354
+ node = pending[0][1]
355
+ assert isinstance(node, ConcreteGlobalNode)
356
+ assert node.process_kwargs["custom_param"] == "value"
357
+
358
+ def test_as_decorator_preserves_function(self) -> None:
359
+ """Test that decorated function remains callable."""
360
+
361
+ @ConcreteGlobalNode.as_decorator("test")
362
+ def test_func(x: int) -> int:
363
+ return x * 2
364
+
365
+ assert test_func(5) == 10
366
+
367
+ def test_as_decorator_preserves_function_metadata(self) -> None:
368
+ """Test that decorator preserves function metadata."""
369
+
370
+ @ConcreteGlobalNode.as_decorator("test")
371
+ def test_func() -> None:
372
+ """Test function docstring."""
373
+ pass
374
+
375
+ assert test_func.__name__ == "test_func"
376
+ assert test_func.__doc__ == "Test function docstring."
377
+
378
+
379
+ class TestQueueGlobal:
380
+ """Tests for queue_global function."""
381
+
382
+ def setup_method(self) -> None:
383
+ """Clear pending nodes before each test."""
384
+ get_pending_nodes()
385
+
386
+ def test_queue_global_adds_to_pending(self) -> None:
387
+ """Test that queue_global adds a global node to pending queue."""
388
+ node = ConcreteGlobalNode(name="test")
389
+ queue_global(node)
390
+
391
+ pending = get_pending_nodes()
392
+ assert len(pending) == 1
393
+ assert pending[0] == ("global", node)
394
+
395
+ def test_queue_global_preserves_node_reference(self) -> None:
396
+ """Test that queued global node maintains its identity."""
397
+ node = ConcreteGlobalNode(name="test")
398
+ queue_global(node)
399
+
400
+ pending = get_pending_nodes()
401
+ retrieved_node = pending[0][1]
402
+
403
+ assert retrieved_node is node
404
+
405
+ def test_queue_global_multiple_nodes(self) -> None:
406
+ """Test that multiple global nodes can be queued."""
407
+ node1 = ConcreteGlobalNode(name="node1")
408
+ node2 = ConcreteGlobalNode(name="node2")
409
+
410
+ queue_global(node1)
411
+ queue_global(node2)
412
+
413
+ pending = get_pending_nodes()
414
+ assert len(pending) == 2
415
+ assert pending[0][1] is node1
416
+ assert pending[1][1] is node2
417
+
418
+
419
+ class TestTreeGlobalsList:
420
+ """Tests for Tree.globals list."""
421
+
422
+ def test_tree_has_globals_list(self) -> None:
423
+ """Test that Tree initializes with globals list."""
424
+ tree = Tree()
425
+
426
+ assert hasattr(tree, "globals")
427
+ assert isinstance(tree.globals, list)
428
+ assert len(tree.globals) == 0
429
+
430
+ def test_tree_globals_list_is_mutable(self) -> None:
431
+ """Test that Tree.globals list can be modified."""
432
+ tree = Tree()
433
+ node = ConcreteGlobalNode(name="test")
434
+
435
+ tree.globals.append(node)
436
+
437
+ assert len(tree.globals) == 1
438
+ assert tree.globals[0] is node
439
+
440
+
441
+ class TestTreeRegisterRootWithGlobals:
442
+ """Tests for Tree.register_root with global nodes."""
443
+
444
+ def setup_method(self) -> None:
445
+ """Clear pending nodes before each test."""
446
+ get_pending_nodes()
447
+
448
+ def test_register_root_processes_global_nodes(self) -> None:
449
+ """Test that register_root processes pending global nodes."""
450
+ tree = Tree()
451
+ root = DummyRootNode(name="root")
452
+ node = ConcreteGlobalNode(name="test")
453
+
454
+ queue_global(node)
455
+ tree.register_root(root)
456
+
457
+ assert len(tree.globals) == 1
458
+ assert tree.globals[0] is node
459
+
460
+ def test_register_root_multiple_globals(self) -> None:
461
+ """Test that register_root processes multiple global nodes."""
462
+ tree = Tree()
463
+ root = DummyRootNode(name="root")
464
+ node1 = ConcreteGlobalNode(name="node1")
465
+ node2 = ConcreteGlobalNode(name="node2")
466
+ node3 = ConcreteGlobalNode(name="node3")
467
+
468
+ queue_global(node1)
469
+ queue_global(node2)
470
+ queue_global(node3)
471
+ tree.register_root(root)
472
+
473
+ assert len(tree.globals) == 3
474
+ assert tree.globals[0] is node1
475
+ assert tree.globals[1] is node2
476
+ assert tree.globals[2] is node3
477
+
478
+ def test_register_root_with_mixed_node_types(self) -> None:
479
+ """Test register_root with global and parent nodes."""
480
+ tree = Tree()
481
+ root = DummyRootNode(name="root")
482
+ parent = DummyParentNode(name="parent")
483
+ global_node = ConcreteGlobalNode(name="global")
484
+
485
+ from click_extended.core._tree import queue_parent
486
+
487
+ queue_global(global_node)
488
+ queue_parent(parent)
489
+ tree.register_root(root)
490
+
491
+ assert tree.root is root
492
+ assert len(tree.globals) == 1
493
+ assert tree.globals[0] is global_node
494
+ assert root.children is not None
495
+ assert "parent" in root.children
496
+
497
+ def test_register_root_globals_order_preserved(self) -> None:
498
+ """Test that global nodes maintain decorator order."""
499
+ tree = Tree()
500
+ root = DummyRootNode(name="root")
501
+
502
+ nodes = [ConcreteGlobalNode(name=f"node{i}") for i in range(5)]
503
+ for node in nodes:
504
+ queue_global(node)
505
+
506
+ tree.register_root(root)
507
+
508
+ for i, node in enumerate(nodes):
509
+ assert tree.globals[i] is node
510
+
511
+
512
+ class TestGlobalNodeObserverMode:
513
+ """Tests for GlobalNode observer mode (name=None)."""
514
+
515
+ def test_observer_mode_inject_name_is_none(self) -> None:
516
+ """Test that observer mode has inject_name=None."""
517
+ node = ConcreteGlobalNode()
518
+
519
+ assert node.inject_name is None
520
+
521
+ def test_observer_mode_can_return_value(self) -> None:
522
+ """Test that observer mode can return values."""
523
+ tree = Tree()
524
+ root = DummyRootNode(name="root")
525
+
526
+ node = ConcreteGlobalNode(return_value="observed")
527
+ result = node.process(tree, root, [], {}, [], (), {})
528
+
529
+ assert result == "observed"
530
+
531
+ def test_observer_mode_return_value_ignored(self) -> None:
532
+ """Test that observer mode return value is not injected."""
533
+ node = ConcreteGlobalNode(return_value="data")
534
+
535
+ assert node.inject_name is None
536
+ assert node.return_value == "data"
537
+
538
+
539
+ class TestGlobalNodeInjectionMode:
540
+ """Tests for GlobalNode injection mode (name="var")."""
541
+
542
+ def test_injection_mode_has_inject_name(self) -> None:
543
+ """Test that injection mode stores inject_name."""
544
+ node = ConcreteGlobalNode(name="my_var")
545
+
546
+ assert node.inject_name == "my_var"
547
+
548
+ def test_injection_mode_with_delay(self) -> None:
549
+ """Test injection mode with delayed execution."""
550
+ node = ConcreteGlobalNode(name="my_var", delay=True)
551
+
552
+ assert node.inject_name == "my_var"
553
+ assert node.delay is True
554
+
555
+ def test_injection_mode_return_value_available(self) -> None:
556
+ """Test that injection mode return value is available."""
557
+ tree = Tree()
558
+ root = DummyRootNode(name="root")
559
+
560
+ node = ConcreteGlobalNode(name="data_var", return_value={"key": "val"})
561
+ result = node.process(tree, root, [], {}, [], (), {})
562
+
563
+ assert result == {"key": "val"}
564
+ assert node.inject_name == "data_var"
565
+
566
+
567
+ class TestGlobalNodeDelayParameter:
568
+ """Tests for GlobalNode delay parameter."""
569
+
570
+ def test_delay_false_by_default(self) -> None:
571
+ """Test that delay defaults to False."""
572
+ node = ConcreteGlobalNode()
573
+
574
+ assert node.delay is False
575
+
576
+ def test_delay_true_when_specified(self) -> None:
577
+ """Test that delay can be set to True."""
578
+ node = ConcreteGlobalNode(delay=True)
579
+
580
+ assert node.delay is True
581
+
582
+ def test_delay_with_name(self) -> None:
583
+ """Test delay parameter with injection name."""
584
+ node = ConcreteGlobalNode(name="var", delay=True)
585
+
586
+ assert node.inject_name == "var"
587
+ assert node.delay is True
588
+
589
+ def test_delay_affects_timing(self) -> None:
590
+ """Test that delay value is accessible for timing logic."""
591
+ early_node = ConcreteGlobalNode(delay=False)
592
+ late_node = ConcreteGlobalNode(delay=True)
593
+
594
+ assert early_node.delay is False
595
+ assert late_node.delay is True
596
+
597
+
598
+ class TestGlobalNodeEdgeCases:
599
+ """Tests for GlobalNode edge cases."""
600
+
601
+ def test_empty_parents_list(self) -> None:
602
+ """Test process with empty parents list."""
603
+ tree = Tree()
604
+ root = DummyRootNode(name="root")
605
+
606
+ node = ConcreteGlobalNode()
607
+ node.process(tree, root, [], {}, [], (), {})
608
+
609
+ assert node.last_call_args["parents"] == []
610
+
611
+ def test_empty_tags_dict(self) -> None:
612
+ """Test process with empty tags dictionary."""
613
+ tree = Tree()
614
+ root = DummyRootNode(name="root")
615
+
616
+ node = ConcreteGlobalNode()
617
+ node.process(tree, root, [], {}, [], (), {})
618
+
619
+ assert node.last_call_args["tags"] == {}
620
+
621
+ def test_empty_globals_list(self) -> None:
622
+ """Test process with empty globals list."""
623
+ tree = Tree()
624
+ root = DummyRootNode(name="root")
625
+
626
+ node = ConcreteGlobalNode()
627
+ node.process(tree, root, [], {}, [], (), {})
628
+
629
+ assert node.last_call_args["globals"] == []
630
+
631
+ def test_none_return_value(self) -> None:
632
+ """Test that None can be returned from process."""
633
+ tree = Tree()
634
+ root = DummyRootNode(name="root")
635
+
636
+ node = ConcreteGlobalNode(return_value=None)
637
+ result = node.process(tree, root, [], {}, [], (), {})
638
+
639
+ assert result is None
640
+
641
+ def test_complex_return_value(self) -> None:
642
+ """Test that complex objects can be returned."""
643
+ tree = Tree()
644
+ root = DummyRootNode(name="root")
645
+
646
+ complex_data: dict[str, Any] = {
647
+ "tree": tree,
648
+ "list": [1, 2, 3],
649
+ "nested": {"key": "value"},
650
+ }
651
+ node = ConcreteGlobalNode(return_value=complex_data)
652
+ result = node.process(tree, root, [], {}, [], (), {})
653
+
654
+ assert result is complex_data
655
+ assert result["tree"] is tree
656
+
657
+ def test_multiple_process_calls(self) -> None:
658
+ """Test that process can be called multiple times."""
659
+ tree = Tree()
660
+ root = DummyRootNode(name="root")
661
+
662
+ node = ConcreteGlobalNode(return_value=1)
663
+
664
+ result1 = node.process(tree, root, [], {}, [], (), {})
665
+ assert result1 == 1
666
+ assert node.call_count == 1
667
+
668
+ node.return_value = 2
669
+ result2 = node.process(tree, root, [], {}, [], (), {})
670
+ assert result2 == 2
671
+ assert node.call_count == 2
672
+
673
+ def test_inject_name_special_characters(self) -> None:
674
+ """Test inject_name with underscores and numbers."""
675
+ node = ConcreteGlobalNode(name="_test_var_123")
676
+
677
+ assert node.inject_name == "_test_var_123"
678
+ assert node.name == "_test_var_123"
679
+
680
+ def test_process_with_large_parents_list(self) -> None:
681
+ """Test process with many parent nodes."""
682
+ tree = Tree()
683
+ root = DummyRootNode(name="root")
684
+ parents: list[ParentNode] = [
685
+ DummyParentNode(name=f"p{i}") for i in range(100)
686
+ ]
687
+
688
+ node = ConcreteGlobalNode()
689
+ node.process(tree, root, parents, {}, [], (), {})
690
+
691
+ assert len(node.last_call_args["parents"]) == 100
692
+
693
+
694
+ class TestGlobalNodeIntegration:
695
+ """Integration tests for GlobalNode with Tree."""
696
+
697
+ def setup_method(self) -> None:
698
+ """Clear pending nodes before each test."""
699
+ get_pending_nodes()
700
+
701
+ def test_full_registration_flow(self) -> None:
702
+ """Test complete flow of global node registration."""
703
+ tree = Tree()
704
+ root = DummyRootNode(name="root")
705
+
706
+ @ConcreteGlobalNode.as_decorator("test_var")
707
+ def dummy_func() -> None: # type: ignore
708
+ pass
709
+
710
+ tree.register_root(root)
711
+
712
+ assert len(tree.globals) == 1
713
+ assert isinstance(tree.globals[0], ConcreteGlobalNode)
714
+ assert tree.globals[0].inject_name == "test_var"
715
+
716
+ def test_multiple_globals_registration(self) -> None:
717
+ """Test registration of multiple global nodes."""
718
+ tree = Tree()
719
+ root = DummyRootNode(name="root")
720
+
721
+ @ConcreteGlobalNode.as_decorator("var1")
722
+ @ConcreteGlobalNode.as_decorator("var2", delay=True)
723
+ @ConcreteGlobalNode.as_decorator()
724
+ def dummy_func() -> None: # type: ignore
725
+ pass
726
+
727
+ tree.register_root(root)
728
+
729
+ assert len(tree.globals) == 3
730
+ assert tree.globals[0].inject_name is None
731
+ assert tree.globals[1].inject_name == "var2"
732
+ assert tree.globals[2].inject_name == "var1"
733
+
734
+ def test_globals_with_parents(self) -> None:
735
+ """Test global nodes registered alongside parent nodes."""
736
+ from click_extended.core._tree import queue_parent
737
+
738
+ tree = Tree()
739
+ root = DummyRootNode(name="root")
740
+ parent = DummyParentNode(name="option")
741
+
742
+ queue_global(ConcreteGlobalNode(name="global1"))
743
+ queue_parent(parent)
744
+ queue_global(ConcreteGlobalNode(name="global2"))
745
+
746
+ tree.register_root(root)
747
+
748
+ assert len(tree.globals) == 2
749
+ assert "option" in root.children
@@ -5,12 +5,14 @@ from typing import Any
5
5
  import pytest
6
6
 
7
7
  from click_extended.core._child_node import ChildNode
8
+ from click_extended.core._global_node import GlobalNode
8
9
  from click_extended.core._parent_node import ParentNode
9
10
  from click_extended.core._root_node import RootNode
10
11
  from click_extended.core._tree import (
11
12
  Tree,
12
13
  get_pending_nodes,
13
14
  queue_child,
15
+ queue_global,
14
16
  queue_parent,
15
17
  )
16
18
  from click_extended.errors import (
@@ -61,6 +63,32 @@ class DummyChildNode(ChildNode):
61
63
  return value
62
64
 
63
65
 
66
+ class DummyGlobalNode(GlobalNode):
67
+ """Dummy GlobalNode for testing."""
68
+
69
+ def __init__(
70
+ self, name: str | None = None, delay: bool = False, **kwargs: Any
71
+ ) -> None:
72
+ """Initialize with optional return value."""
73
+ super().__init__(name=name, delay=delay)
74
+ self.return_value = kwargs.get("return_value", None)
75
+
76
+ def process(
77
+ self,
78
+ tree: Tree,
79
+ root: "RootNode",
80
+ parents: list["ParentNode"],
81
+ tags: dict[str, Any],
82
+ globals: list["GlobalNode"],
83
+ call_args: tuple[Any, ...],
84
+ call_kwargs: dict[str, Any],
85
+ *args: Any,
86
+ **kwargs: Any,
87
+ ) -> Any:
88
+ """Return configured value."""
89
+ return self.return_value
90
+
91
+
64
92
  class TestTreeInitialization:
65
93
  """Tests for Tree initialization."""
66
94
 
@@ -70,6 +98,12 @@ class TestTreeInitialization:
70
98
  assert tree.root is None
71
99
  assert tree.recent is None
72
100
 
101
+ def test_init_creates_empty_globals_list(self) -> None:
102
+ """Test that Tree initializes with empty globals list."""
103
+ tree = Tree()
104
+ assert isinstance(tree.globals, list)
105
+ assert len(tree.globals) == 0
106
+
73
107
  def test_multiple_trees_are_independent(self) -> None:
74
108
  """Test that multiple Tree instances are independent."""
75
109
  tree1 = Tree()
@@ -108,6 +142,15 @@ class TestPendingNodesQueue:
108
142
  assert len(pending) == 1
109
143
  assert pending[0] == ("child", child)
110
144
 
145
+ def test_queue_global_adds_to_pending(self) -> None:
146
+ """Test that queue_global adds a global node to pending queue."""
147
+ global_node = DummyGlobalNode(name="test_global")
148
+ queue_global(global_node)
149
+
150
+ pending = get_pending_nodes()
151
+ assert len(pending) == 1
152
+ assert pending[0] == ("global", global_node)
153
+
111
154
  def test_get_pending_nodes_clears_queue(self) -> None:
112
155
  """Test that get_pending_nodes clears the queue after retrieval."""
113
156
  parent = DummyParentNode(name="test_parent")
@@ -135,6 +178,22 @@ class TestPendingNodesQueue:
135
178
  assert pending[1] == ("child", child1)
136
179
  assert pending[2] == ("child", child2)
137
180
 
181
+ def test_mixed_node_types_queued_in_order(self) -> None:
182
+ """Test that mixed node types are queued in order."""
183
+ parent = DummyParentNode(name="parent")
184
+ child = DummyChildNode(name="child")
185
+ global_node = DummyGlobalNode(name="global")
186
+
187
+ queue_global(global_node)
188
+ queue_parent(parent)
189
+ queue_child(child)
190
+
191
+ pending = get_pending_nodes()
192
+ assert len(pending) == 3
193
+ assert pending[0] == ("global", global_node)
194
+ assert pending[1] == ("parent", parent)
195
+ assert pending[2] == ("child", child)
196
+
138
197
  def test_queue_preserves_node_references(self) -> None:
139
198
  """Test that queued nodes maintain their identity."""
140
199
  parent = DummyParentNode(name="test")
@@ -203,6 +262,36 @@ class TestTreeRegisterRoot:
203
262
  assert 0 in parent.children
204
263
  assert parent.children[0] is child
205
264
 
265
+ def test_register_root_processes_pending_globals(self) -> None:
266
+ """Test that register_root processes pending global nodes."""
267
+ tree = Tree()
268
+ root = DummyRootNode(name="root")
269
+ global_node = DummyGlobalNode(name="test_global")
270
+
271
+ queue_global(global_node)
272
+ tree.register_root(root)
273
+
274
+ assert len(tree.globals) == 1
275
+ assert tree.globals[0] is global_node
276
+
277
+ def test_register_root_processes_multiple_globals(self) -> None:
278
+ """Test that register_root processes multiple global nodes."""
279
+ tree = Tree()
280
+ root = DummyRootNode(name="root")
281
+ global1 = DummyGlobalNode(name="global1")
282
+ global2 = DummyGlobalNode(name="global2")
283
+ global3 = DummyGlobalNode(name="global3")
284
+
285
+ queue_global(global1)
286
+ queue_global(global2)
287
+ queue_global(global3)
288
+ tree.register_root(root)
289
+
290
+ assert len(tree.globals) == 3
291
+ assert tree.globals[0] is global1
292
+ assert tree.globals[1] is global2
293
+ assert tree.globals[2] is global3
294
+
206
295
  def test_register_root_reverses_pending_order(self) -> None:
207
296
  """Test that register_root processes nodes in reverse order."""
208
297
  tree = Tree()
@@ -242,6 +331,28 @@ class TestTreeRegisterRoot:
242
331
  with pytest.raises(NoParentError):
243
332
  tree.register_root(root)
244
333
 
334
+ def test_register_root_with_mixed_node_types(self) -> None:
335
+ """Test register_root with parent, child, and global nodes."""
336
+ tree = Tree()
337
+ root = DummyRootNode(name="root")
338
+ parent = DummyParentNode(name="parent")
339
+ child = DummyChildNode(name="child")
340
+ global_node = DummyGlobalNode(name="global")
341
+
342
+ queue_global(global_node)
343
+ queue_child(child)
344
+ queue_parent(parent)
345
+ tree.register_root(root)
346
+
347
+ assert tree.root is root
348
+ assert len(tree.globals) == 1
349
+ assert tree.globals[0] is global_node
350
+ assert root.children is not None
351
+ assert "parent" in root.children
352
+ assert parent.children is not None
353
+ assert 0 in parent.children
354
+ assert parent.children[0] is child
355
+
245
356
  def test_register_root_sets_recent_to_last_parent(self) -> None:
246
357
  """Test that register_root sets recent to the most recent parent."""
247
358
  tree = Tree()
@@ -641,3 +752,27 @@ class TestTreeEdgeCases:
641
752
 
642
753
  assert tree.root is None
643
754
  assert tree.recent is None
755
+
756
+ def test_globals_list_independence(self) -> None:
757
+ """Test that globals list is independent between trees."""
758
+ tree1 = Tree()
759
+ tree2 = Tree()
760
+
761
+ global1 = DummyGlobalNode(name="global1")
762
+ global2 = DummyGlobalNode(name="global2")
763
+
764
+ tree1.globals.append(global1)
765
+ tree2.globals.append(global2)
766
+
767
+ assert len(tree1.globals) == 1
768
+ assert len(tree2.globals) == 1
769
+ assert tree1.globals[0] is global1
770
+ assert tree2.globals[0] is global2
771
+
772
+ def test_empty_globals_list_preserved(self) -> None:
773
+ """Test that empty globals list remains empty."""
774
+ tree = Tree()
775
+
776
+ assert len(tree.globals) == 0
777
+ _ = tree.globals
778
+ assert len(tree.globals) == 0
@@ -1,27 +0,0 @@
1
- ![Banner](./assets/click-extended-banner.png)
2
-
3
- # Click Extended
4
-
5
- TBD
6
-
7
- ## Installation
8
-
9
- ```bash
10
- pip install click-extended
11
- ```
12
-
13
- ## Requirements
14
-
15
- TBD
16
-
17
- ## License
18
-
19
- This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
20
-
21
- ## Contributing
22
-
23
- TBD
24
-
25
- ## Acknowledgements
26
-
27
- This project is built on top of the [Click](https://github.com/pallets/click) library.
File without changes
File without changes