todo-agent 0.2.9__tar.gz → 0.3.1__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 (61) hide show
  1. {todo_agent-0.2.9 → todo_agent-0.3.1}/PKG-INFO +1 -1
  2. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_core/test_todo_manager.py +159 -76
  3. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_todo_shell.py +31 -41
  4. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/_version.py +3 -3
  5. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/core/todo_manager.py +33 -1
  6. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/inference.py +4 -3
  7. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/openrouter_client.py +5 -5
  8. todo_agent-0.3.1/todo_agent/infrastructure/prompts/system_prompt.txt +399 -0
  9. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/todo_shell.py +27 -13
  10. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/interface/cli.py +2 -2
  11. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/interface/tools.py +71 -118
  12. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/PKG-INFO +1 -1
  13. todo_agent-0.2.9/todo_agent/infrastructure/prompts/system_prompt.txt +0 -413
  14. {todo_agent-0.2.9 → todo_agent-0.3.1}/.gitignore +0 -0
  15. {todo_agent-0.2.9 → todo_agent-0.3.1}/LICENSE +0 -0
  16. {todo_agent-0.2.9 → todo_agent-0.3.1}/MANIFEST.in +0 -0
  17. {todo_agent-0.2.9 → todo_agent-0.3.1}/Makefile +0 -0
  18. {todo_agent-0.2.9 → todo_agent-0.3.1}/README.md +0 -0
  19. {todo_agent-0.2.9 → todo_agent-0.3.1}/docs/publishing.md +0 -0
  20. {todo_agent-0.2.9 → todo_agent-0.3.1}/pyproject.toml +0 -0
  21. {todo_agent-0.2.9 → todo_agent-0.3.1}/requirements-dev.txt +0 -0
  22. {todo_agent-0.2.9 → todo_agent-0.3.1}/requirements.txt +0 -0
  23. {todo_agent-0.2.9 → todo_agent-0.3.1}/setup.cfg +0 -0
  24. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/__init__.py +0 -0
  25. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_core/__init__.py +0 -0
  26. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_core/test_conversation_manager.py +0 -0
  27. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/__init__.py +0 -0
  28. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_calendar_utils.py +0 -0
  29. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_config.py +0 -0
  30. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_inference.py +0 -0
  31. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
  32. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_ollama_client.py +0 -0
  33. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_openrouter_client.py +0 -0
  34. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_infrastructure/test_token_counter.py +0 -0
  35. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_interface/__init__.py +0 -0
  36. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_interface/test_cli.py +0 -0
  37. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_interface/test_formatters.py +0 -0
  38. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_interface/test_tools.py +0 -0
  39. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_linting.py +0 -0
  40. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_logger.py +0 -0
  41. {todo_agent-0.2.9 → todo_agent-0.3.1}/tests/test_main.py +0 -0
  42. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/__init__.py +0 -0
  43. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/core/__init__.py +0 -0
  44. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/core/conversation_manager.py +0 -0
  45. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/core/exceptions.py +0 -0
  46. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/__init__.py +0 -0
  47. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/calendar_utils.py +0 -0
  48. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/config.py +0 -0
  49. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/llm_client.py +0 -0
  50. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/llm_client_factory.py +0 -0
  51. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/logger.py +0 -0
  52. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/ollama_client.py +0 -0
  53. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/infrastructure/token_counter.py +0 -0
  54. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/interface/__init__.py +0 -0
  55. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/interface/formatters.py +0 -0
  56. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent/main.py +0 -0
  57. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/SOURCES.txt +0 -0
  58. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/dependency_links.txt +0 -0
  59. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/entry_points.txt +0 -0
  60. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/requires.txt +0 -0
  61. {todo_agent-0.2.9 → todo_agent-0.3.1}/todo_agent.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: todo-agent
3
- Version: 0.2.9
3
+ Version: 0.3.1
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -3,10 +3,9 @@ Tests for TodoManager.
3
3
  """
4
4
 
5
5
  import unittest
6
- from unittest.mock import Mock
6
+ from unittest.mock import Mock, patch
7
7
 
8
8
  import pytest
9
- from unittest.mock import patch
10
9
 
11
10
  try:
12
11
  from todo_agent.core.todo_manager import TodoManager
@@ -187,6 +186,36 @@ class TestTodoManager(unittest.TestCase):
187
186
  self.todo_manager.add_task("Test task", recurring="rec:weekly:0")
188
187
  self.assertIn("Must be a positive integer", str(context.exception))
189
188
 
189
+ def test_add_task_with_invalid_duration_format(self):
190
+ """Test adding a task with invalid duration format raises ValueError."""
191
+ with self.assertRaises(ValueError) as context:
192
+ self.todo_manager.add_task("Test task", duration="30s")
193
+ self.assertIn("Invalid duration format", str(context.exception))
194
+
195
+ def test_add_task_with_invalid_duration_unit(self):
196
+ """Test adding a task with invalid duration unit raises ValueError."""
197
+ with self.assertRaises(ValueError) as context:
198
+ self.todo_manager.add_task("Test task", duration="30s")
199
+ self.assertIn("Invalid duration format", str(context.exception))
200
+
201
+ def test_add_task_with_invalid_duration_value(self):
202
+ """Test adding a task with invalid duration value raises ValueError."""
203
+ with self.assertRaises(ValueError) as context:
204
+ self.todo_manager.add_task("Test task", duration="0m")
205
+ self.assertIn("Must be a positive number", str(context.exception))
206
+
207
+ def test_add_task_with_negative_duration(self):
208
+ """Test adding a task with negative duration raises ValueError."""
209
+ with self.assertRaises(ValueError) as context:
210
+ self.todo_manager.add_task("Test task", duration="-30m")
211
+ self.assertIn("Must be a positive number", str(context.exception))
212
+
213
+ def test_add_task_with_empty_duration(self):
214
+ """Test adding a task with empty duration raises ValueError."""
215
+ with self.assertRaises(ValueError) as context:
216
+ self.todo_manager.add_task("Test task", duration="")
217
+ self.assertIn("Duration must be a non-empty string", str(context.exception))
218
+
190
219
  def test_add_task_with_all_parameters_including_recurring(self):
191
220
  """Test adding a task with all parameters including recurring."""
192
221
  self.todo_shell.add.return_value = "Test task"
@@ -205,6 +234,36 @@ class TestTodoManager(unittest.TestCase):
205
234
  "(A) Test task +work @office due:2024-01-15 rec:daily"
206
235
  )
207
236
 
237
+ def test_add_task_with_duration(self):
238
+ """Test adding a task with duration parameter."""
239
+ self.todo_shell.add.return_value = "Test task"
240
+ result = self.todo_manager.add_task(
241
+ "Test task",
242
+ duration="30m",
243
+ )
244
+ self.assertEqual(result, "Added task: Test task duration:30m")
245
+ self.todo_shell.add.assert_called_once_with("Test task duration:30m")
246
+
247
+ def test_add_task_with_all_parameters_including_duration(self):
248
+ """Test adding a task with all parameters including duration."""
249
+ self.todo_shell.add.return_value = "Test task"
250
+ result = self.todo_manager.add_task(
251
+ "Test task",
252
+ priority="A",
253
+ project="work",
254
+ context="office",
255
+ due="2024-01-15",
256
+ recurring="rec:daily",
257
+ duration="2h",
258
+ )
259
+ self.assertEqual(
260
+ result,
261
+ "Added task: (A) Test task +work @office due:2024-01-15 rec:daily duration:2h",
262
+ )
263
+ self.todo_shell.add.assert_called_once_with(
264
+ "(A) Test task +work @office due:2024-01-15 rec:daily duration:2h"
265
+ )
266
+
208
267
  def test_list_tasks(self):
209
268
  """Test listing tasks."""
210
269
  self.todo_shell.list_tasks.return_value = "1. Task 1\n2. Task 2"
@@ -299,60 +358,62 @@ class TestTodoManager(unittest.TestCase):
299
358
  self.assertEqual(result, "No completed tasks found matching the criteria.")
300
359
  self.todo_shell.list_completed.assert_called_once_with("+nonexistent")
301
360
 
302
-
303
-
304
361
  def test_add_task_sanitizes_inputs(self):
305
362
  """Test that add_task sanitizes inputs to prevent duplicates."""
306
363
  # Mock the todo_shell
307
364
  mock_shell = Mock()
308
365
  mock_shell.add.return_value = "1 Test task +project1 @context1"
309
-
366
+
310
367
  manager = TodoManager(mock_shell)
311
-
368
+
312
369
  # Test with inputs that already have + and @ symbols
313
370
  result = manager.add_task(
314
371
  description="Test task",
315
372
  project="+project1", # Already has + symbol
316
- context="@context1" # Already has @ symbol
373
+ context="@context1", # Already has @ symbol
317
374
  )
318
-
375
+
319
376
  # Verify the task was added with properly formatted projects and contexts
320
377
  mock_shell.add.assert_called_once()
321
378
  call_args = mock_shell.add.call_args[0][0]
322
-
379
+
323
380
  # Should have exactly one instance of each (sanitization prevents duplicates)
324
381
  assert call_args.count("+project1") == 1
325
382
  assert call_args.count("@context1") == 1
326
-
383
+
327
384
  # Verify the result message
328
385
  assert "Added task:" in result
329
386
 
330
-
331
-
332
387
  def test_parse_task_components_deduplicates(self):
333
388
  """Test that _parse_task_components deduplicates projects and contexts."""
334
389
  from todo_agent.infrastructure.todo_shell import TodoShell
335
-
390
+
336
391
  # Create a TodoShell instance
337
392
  shell = TodoShell("/tmp/todo.txt")
338
-
393
+
339
394
  # Test with duplicate projects and contexts in the task line
340
395
  task_line = "1 (A) Task description +project1 +project1 +project2 @context1 @context1 @context2"
341
396
  components = shell._parse_task_components(task_line)
342
-
397
+
343
398
  # Verify deduplication
344
- assert components["projects"] == ["+project1", "+project2"] # Sorted and deduplicated
345
- assert components["contexts"] == ["@context1", "@context2"] # Sorted and deduplicated
399
+ assert components["projects"] == [
400
+ "+project1",
401
+ "+project2",
402
+ ] # Sorted and deduplicated
403
+ assert components["contexts"] == [
404
+ "@context1",
405
+ "@context2",
406
+ ] # Sorted and deduplicated
346
407
  assert components["priority"] == "A"
347
408
  assert components["description"] == "Task description"
348
409
 
349
410
  def test_reconstruct_task_maintains_deduplication(self):
350
411
  """Test that _reconstruct_task maintains deduplication."""
351
412
  from todo_agent.infrastructure.todo_shell import TodoShell
352
-
413
+
353
414
  # Create a TodoShell instance
354
415
  shell = TodoShell("/tmp/todo.txt")
355
-
416
+
356
417
  # Create components with potential duplicates
357
418
  components = {
358
419
  "priority": "A",
@@ -361,12 +422,12 @@ class TestTodoManager(unittest.TestCase):
361
422
  "contexts": ["@context1", "@context1", "@context2"], # Duplicate context1
362
423
  "due": "2024-01-01",
363
424
  "recurring": None,
364
- "other_tags": []
425
+ "other_tags": [],
365
426
  }
366
-
427
+
367
428
  # Reconstruct the task
368
429
  reconstructed = shell._reconstruct_task(components)
369
-
430
+
370
431
  # Verify that duplicates are preserved in reconstruction (since we handle deduplication in parsing)
371
432
  # This is expected behavior - reconstruction should preserve the exact components given
372
433
  assert reconstructed.count("+project1") == 2
@@ -376,83 +437,96 @@ class TestTodoManager(unittest.TestCase):
376
437
 
377
438
  def test_todo_shell_set_project_actually_prevents_duplicates(self):
378
439
  """Test that the todo_shell set_project method actually prevents duplicates."""
379
- from todo_agent.infrastructure.todo_shell import TodoShell
380
440
  from unittest.mock import patch
381
-
441
+
442
+ from todo_agent.infrastructure.todo_shell import TodoShell
443
+
382
444
  # Create a TodoShell instance
383
445
  shell = TodoShell("/tmp/todo.txt")
384
-
446
+
385
447
  # Mock the list_tasks method to return a task with existing projects
386
- with patch.object(shell, 'list_tasks') as mock_list_tasks:
387
- mock_list_tasks.return_value = "1 (A) Existing task +existing_project @existing_context"
388
-
448
+ with patch.object(shell, "list_tasks") as mock_list_tasks:
449
+ mock_list_tasks.return_value = (
450
+ "1 (A) Existing task +existing_project @existing_context"
451
+ )
452
+
389
453
  # Mock the replace method to capture what gets called
390
- with patch.object(shell, 'replace') as mock_replace:
391
- mock_replace.return_value = "1 (A) Existing task +existing_project @existing_context"
392
-
454
+ with patch.object(shell, "replace") as mock_replace:
455
+ mock_replace.return_value = (
456
+ "1 (A) Existing task +existing_project @existing_context"
457
+ )
458
+
393
459
  # Try to add a project that already exists
394
460
  result = shell.set_project(1, ["existing_project"])
395
-
461
+
396
462
  # Since the project already exists, replace should NOT be called
397
463
  # (our deduplication logic prevents unnecessary updates)
398
464
  mock_replace.assert_not_called()
399
-
465
+
400
466
  # The result should be the reconstructed task without duplicates
401
467
  assert "+existing_project" in result
402
468
  assert result.count("+existing_project") == 1 # Only one instance
403
469
 
404
470
  def test_todo_shell_set_context_actually_prevents_duplicates(self):
405
471
  """Test that the todo_shell set_context method actually prevents duplicates."""
406
- from todo_agent.infrastructure.todo_shell import TodoShell
407
472
  from unittest.mock import patch
408
-
473
+
474
+ from todo_agent.infrastructure.todo_shell import TodoShell
475
+
409
476
  # Create a TodoShell instance
410
477
  shell = TodoShell("/tmp/todo.txt")
411
-
478
+
412
479
  # Mock the list_tasks method to return a task with existing context
413
- with patch.object(shell, 'list_tasks') as mock_list_tasks:
414
- mock_list_tasks.return_value = "1 (A) Existing task +existing_project @existing_context"
415
-
480
+ with patch.object(shell, "list_tasks") as mock_list_tasks:
481
+ mock_list_tasks.return_value = (
482
+ "1 (A) Existing task +existing_project @existing_context"
483
+ )
484
+
416
485
  # Mock the replace method to capture what gets called
417
- with patch.object(shell, 'replace') as mock_replace:
418
- mock_replace.return_value = "1 (A) Existing task +existing_project @existing_context"
419
-
486
+ with patch.object(shell, "replace") as mock_replace:
487
+ mock_replace.return_value = (
488
+ "1 (A) Existing task +existing_project @existing_context"
489
+ )
490
+
420
491
  # Try to set a context that already exists
421
492
  result = shell.set_context(1, "existing_context")
422
-
493
+
423
494
  # Since the context already exists, replace should NOT be called
424
495
  # (our deduplication logic prevents unnecessary updates)
425
496
  mock_replace.assert_not_called()
426
-
497
+
427
498
  # The result should be the reconstructed task without duplicates
428
499
  assert "@existing_context" in result
429
500
  assert result.count("@existing_context") == 1 # Only one instance
430
501
 
431
502
  def test_todo_shell_set_project_adds_new_projects(self):
432
503
  """Test that the todo_shell set_project method adds new projects and prevents duplicates."""
433
- from todo_agent.infrastructure.todo_shell import TodoShell
434
504
  from unittest.mock import patch
435
-
505
+
506
+ from todo_agent.infrastructure.todo_shell import TodoShell
507
+
436
508
  # Create a TodoShell instance
437
509
  shell = TodoShell("/tmp/todo.txt")
438
-
510
+
439
511
  # Mock the list_tasks method to return a task with existing projects
440
- with patch.object(shell, 'list_tasks') as mock_list_tasks:
441
- mock_list_tasks.return_value = "1 (A) Existing task +existing_project @existing_context"
442
-
512
+ with patch.object(shell, "list_tasks") as mock_list_tasks:
513
+ mock_list_tasks.return_value = (
514
+ "1 (A) Existing task +existing_project @existing_context"
515
+ )
516
+
443
517
  # Mock the replace method to capture what gets called
444
- with patch.object(shell, 'replace') as mock_replace:
518
+ with patch.object(shell, "replace") as mock_replace:
445
519
  mock_replace.return_value = "1 (A) Existing task +existing_project +new_project @existing_context"
446
-
520
+
447
521
  # Try to add a new project
448
- result = shell.set_project(1, ["new_project"])
449
-
522
+ shell.set_project(1, ["new_project"])
523
+
450
524
  # Since we're adding a new project, replace should be called
451
525
  mock_replace.assert_called_once()
452
-
526
+
453
527
  # Get the actual task description that was passed to replace
454
528
  actual_task_description = mock_replace.call_args[0][1]
455
-
529
+
456
530
  # Verify that the new project was added and no duplicates exist
457
531
  assert "+new_project" in actual_task_description
458
532
  assert actual_task_description.count("+existing_project") == 1
@@ -460,56 +534,65 @@ class TestTodoManager(unittest.TestCase):
460
534
 
461
535
  def test_todo_shell_set_context_adds_new_contexts(self):
462
536
  """Test that the todo_shell set_context method adds new contexts and prevents duplicates."""
463
- from todo_agent.infrastructure.todo_shell import TodoShell
464
537
  from unittest.mock import patch
465
-
538
+
539
+ from todo_agent.infrastructure.todo_shell import TodoShell
540
+
466
541
  # Create a TodoShell instance
467
542
  shell = TodoShell("/tmp/todo.txt")
468
-
543
+
469
544
  # Mock the list_tasks method to return a task with existing context
470
- with patch.object(shell, 'list_tasks') as mock_list_tasks:
471
- mock_list_tasks.return_value = "1 (A) Existing task +existing_project @existing_context"
472
-
545
+ with patch.object(shell, "list_tasks") as mock_list_tasks:
546
+ mock_list_tasks.return_value = (
547
+ "1 (A) Existing task +existing_project @existing_context"
548
+ )
549
+
473
550
  # Mock the replace method to capture what gets called
474
- with patch.object(shell, 'replace') as mock_replace:
475
- mock_replace.return_value = "1 (A) Existing task +existing_project @new_context"
476
-
551
+ with patch.object(shell, "replace") as mock_replace:
552
+ mock_replace.return_value = (
553
+ "1 (A) Existing task +existing_project @new_context"
554
+ )
555
+
477
556
  # Try to set a new context
478
- result = shell.set_context(1, "new_context")
479
-
557
+ shell.set_context(1, "new_context")
558
+
480
559
  # Since we're adding a new context, replace should be called
481
560
  mock_replace.assert_called_once()
482
-
561
+
483
562
  # Get the actual task description that was passed to replace
484
563
  actual_task_description = mock_replace.call_args[0][1]
485
-
564
+
486
565
  # Verify that the new context was added and the old context was replaced
487
566
  # (set_context replaces all contexts with the new one)
488
567
  assert "@new_context" in actual_task_description
489
- assert "@existing_context" not in actual_task_description # Old context replaced
490
- assert actual_task_description.count("@new_context") == 1 # Only one instance
568
+ assert (
569
+ "@existing_context" not in actual_task_description
570
+ ) # Old context replaced
571
+ assert (
572
+ actual_task_description.count("@new_context") == 1
573
+ ) # Only one instance
491
574
 
492
575
  def test_parse_task_components_actually_deduplicates(self):
493
576
  """Test that _parse_task_components actually removes duplicates from input."""
494
577
  from todo_agent.infrastructure.todo_shell import TodoShell
495
-
578
+
496
579
  # Create a TodoShell instance
497
580
  shell = TodoShell("/tmp/todo.txt")
498
-
581
+
499
582
  # Test with a task line that has multiple duplicates
500
583
  task_line = "1 (A) Task description +project1 +project1 +project1 +project2 @context1 @context1 @context2"
501
584
  components = shell._parse_task_components(task_line)
502
-
585
+
503
586
  # Verify that duplicates were actually removed
504
587
  assert len(components["projects"]) == 2 # Should only have 2 unique projects
505
588
  assert len(components["contexts"]) == 2 # Should only have 2 unique contexts
506
-
589
+
507
590
  # Verify the specific projects and contexts
508
591
  assert "+project1" in components["projects"]
509
592
  assert "+project2" in components["projects"]
510
593
  assert "@context1" in components["contexts"]
511
594
  assert "@context2" in components["contexts"]
512
-
595
+
513
596
  # Verify no duplicates exist in the lists
514
597
  assert components["projects"].count("+project1") == 1
515
598
  assert components["projects"].count("+project2") == 1
@@ -219,7 +219,7 @@ class TestTodoShell:
219
219
  sample_task = "1 (B) Call dentist +health @phone"
220
220
  with patch.object(
221
221
  self.todo_shell, "list_tasks", return_value=sample_task
222
- ) as mock_list, patch.object(
222
+ ), patch.object(
223
223
  self.todo_shell, "replace", return_value="Task updated"
224
224
  ) as mock_replace:
225
225
  result = self.todo_shell.set_due_date(1, "2025-01-20")
@@ -236,7 +236,7 @@ class TestTodoShell:
236
236
  sample_task = "1 (C) Review quarterly report +work @office rec:weekly due:2025-01-10 custom:tag"
237
237
  with patch.object(
238
238
  self.todo_shell, "list_tasks", return_value=sample_task
239
- ) as mock_list, patch.object(
239
+ ), patch.object(
240
240
  self.todo_shell, "replace", return_value="Task updated"
241
241
  ) as mock_replace:
242
242
  result = self.todo_shell.set_due_date(1, "2025-01-25")
@@ -253,7 +253,7 @@ class TestTodoShell:
253
253
  sample_task = "1 Buy milk +shopping @grocery"
254
254
  with patch.object(
255
255
  self.todo_shell, "list_tasks", return_value=sample_task
256
- ) as mock_list, patch.object(
256
+ ), patch.object(
257
257
  self.todo_shell, "replace", return_value="Task updated"
258
258
  ) as mock_replace:
259
259
  result = self.todo_shell.set_due_date(1, "2025-01-30")
@@ -268,9 +268,7 @@ class TestTodoShell:
268
268
  """Test that set_due_date raises error for invalid task number."""
269
269
  # Mock the list_tasks to return only one task
270
270
  sample_task = "1 Test task"
271
- with patch.object(
272
- self.todo_shell, "list_tasks", return_value=sample_task
273
- ) as mock_list:
271
+ with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
274
272
  with pytest.raises(TodoShellError, match="Task number 5 not found"):
275
273
  self.todo_shell.set_due_date(5, "2025-01-15")
276
274
 
@@ -280,7 +278,7 @@ class TestTodoShell:
280
278
  sample_task = "1 (A) Buy groceries +shopping @home due:2025-01-10"
281
279
  with patch.object(
282
280
  self.todo_shell, "list_tasks", return_value=sample_task
283
- ) as mock_list, patch.object(
281
+ ), patch.object(
284
282
  self.todo_shell, "replace", return_value="Task updated"
285
283
  ) as mock_replace:
286
284
  result = self.todo_shell.set_due_date(1, "")
@@ -295,7 +293,7 @@ class TestTodoShell:
295
293
  sample_task = "1 (B) Call dentist +health @phone due:2025-01-15"
296
294
  with patch.object(
297
295
  self.todo_shell, "list_tasks", return_value=sample_task
298
- ) as mock_list, patch.object(
296
+ ), patch.object(
299
297
  self.todo_shell, "replace", return_value="Task updated"
300
298
  ) as mock_replace:
301
299
  result = self.todo_shell.set_due_date(1, " ")
@@ -310,7 +308,7 @@ class TestTodoShell:
310
308
  sample_task = "1 Buy milk +shopping @grocery due:2025-01-20"
311
309
  with patch.object(
312
310
  self.todo_shell, "list_tasks", return_value=sample_task
313
- ) as mock_list, patch.object(
311
+ ), patch.object(
314
312
  self.todo_shell, "replace", return_value="Task updated"
315
313
  ) as mock_replace:
316
314
  result = self.todo_shell.set_due_date(1, "")
@@ -344,7 +342,7 @@ class TestTodoShell:
344
342
  sample_task = "1 (B) Call dentist +health"
345
343
  with patch.object(
346
344
  self.todo_shell, "list_tasks", return_value=sample_task
347
- ) as mock_list, patch.object(
345
+ ), patch.object(
348
346
  self.todo_shell, "replace", return_value="Task updated"
349
347
  ) as mock_replace:
350
348
  result = self.todo_shell.set_context(1, "phone")
@@ -359,7 +357,7 @@ class TestTodoShell:
359
357
  sample_task = "1 (C) Review quarterly report +work @office rec:weekly due:2025-01-10 custom:tag"
360
358
  with patch.object(
361
359
  self.todo_shell, "list_tasks", return_value=sample_task
362
- ) as mock_list, patch.object(
360
+ ), patch.object(
363
361
  self.todo_shell, "replace", return_value="Task updated"
364
362
  ) as mock_replace:
365
363
  result = self.todo_shell.set_context(1, "home")
@@ -375,7 +373,7 @@ class TestTodoShell:
375
373
  sample_task = "1 Buy milk +shopping @grocery"
376
374
  with patch.object(
377
375
  self.todo_shell, "list_tasks", return_value=sample_task
378
- ) as mock_list, patch.object(
376
+ ), patch.object(
379
377
  self.todo_shell, "replace", return_value="Task updated"
380
378
  ) as mock_replace:
381
379
  result = self.todo_shell.set_context(1, "store")
@@ -390,7 +388,7 @@ class TestTodoShell:
390
388
  sample_task = "1 (A) Buy groceries +shopping @home due:2025-01-10"
391
389
  with patch.object(
392
390
  self.todo_shell, "list_tasks", return_value=sample_task
393
- ) as mock_list, patch.object(
391
+ ), patch.object(
394
392
  self.todo_shell, "replace", return_value="Task updated"
395
393
  ) as mock_replace:
396
394
  result = self.todo_shell.set_context(1, "")
@@ -407,7 +405,7 @@ class TestTodoShell:
407
405
  sample_task = "1 (B) Call dentist +health @phone"
408
406
  with patch.object(
409
407
  self.todo_shell, "list_tasks", return_value=sample_task
410
- ) as mock_list, patch.object(
408
+ ), patch.object(
411
409
  self.todo_shell, "replace", return_value="Task updated"
412
410
  ) as mock_replace:
413
411
  result = self.todo_shell.set_context(1, " ")
@@ -422,7 +420,7 @@ class TestTodoShell:
422
420
  sample_task = "1 Test task +project"
423
421
  with patch.object(
424
422
  self.todo_shell, "list_tasks", return_value=sample_task
425
- ) as mock_list, patch.object(
423
+ ), patch.object(
426
424
  self.todo_shell, "replace", return_value="Task updated"
427
425
  ) as mock_replace:
428
426
  result = self.todo_shell.set_context(1, "@office")
@@ -435,9 +433,7 @@ class TestTodoShell:
435
433
  """Test that set_context raises error for context that becomes empty after cleaning."""
436
434
  # Mock the list_tasks to return a task
437
435
  sample_task = "1 Test task"
438
- with patch.object(
439
- self.todo_shell, "list_tasks", return_value=sample_task
440
- ) as mock_list:
436
+ with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
441
437
  with pytest.raises(TodoShellError, match="Context name cannot be empty"):
442
438
  self.todo_shell.set_context(1, "@")
443
439
 
@@ -445,9 +441,7 @@ class TestTodoShell:
445
441
  """Test that set_context raises error for invalid task number."""
446
442
  # Mock the list_tasks to return only one task
447
443
  sample_task = "1 Test task"
448
- with patch.object(
449
- self.todo_shell, "list_tasks", return_value=sample_task
450
- ) as mock_list:
444
+ with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
451
445
  with pytest.raises(TodoShellError, match="Task number 5 not found"):
452
446
  self.todo_shell.set_context(5, "office")
453
447
 
@@ -590,7 +584,7 @@ class TestTodoShell:
590
584
  sample_task = "1 (A) Buy groceries @home due:2025-01-10"
591
585
  with patch.object(
592
586
  self.todo_shell, "list_tasks", return_value=sample_task
593
- ) as mock_list, patch.object(
587
+ ), patch.object(
594
588
  self.todo_shell, "replace", return_value="Task updated"
595
589
  ) as mock_replace:
596
590
  result = self.todo_shell.set_project(1, ["shopping", "errands"])
@@ -607,7 +601,7 @@ class TestTodoShell:
607
601
  sample_task = "1 (B) Call dentist +health @phone"
608
602
  with patch.object(
609
603
  self.todo_shell, "list_tasks", return_value=sample_task
610
- ) as mock_list, patch.object(
604
+ ), patch.object(
611
605
  self.todo_shell, "replace", return_value="Task updated"
612
606
  ) as mock_replace:
613
607
  result = self.todo_shell.set_project(1, ["appointment", "personal"])
@@ -624,7 +618,7 @@ class TestTodoShell:
624
618
  sample_task = "1 (C) Review report +work +urgent +review @office"
625
619
  with patch.object(
626
620
  self.todo_shell, "list_tasks", return_value=sample_task
627
- ) as mock_list, patch.object(
621
+ ), patch.object(
628
622
  self.todo_shell, "replace", return_value="Task updated"
629
623
  ) as mock_replace:
630
624
  result = self.todo_shell.set_project(1, ["-urgent", "-review"])
@@ -639,7 +633,7 @@ class TestTodoShell:
639
633
  sample_task = "1 (A) Task with projects +old +keep @context"
640
634
  with patch.object(
641
635
  self.todo_shell, "list_tasks", return_value=sample_task
642
- ) as mock_list, patch.object(
636
+ ), patch.object(
643
637
  self.todo_shell, "replace", return_value="Task updated"
644
638
  ) as mock_replace:
645
639
  result = self.todo_shell.set_project(1, ["-old", "new", "another"])
@@ -656,7 +650,7 @@ class TestTodoShell:
656
650
  sample_task = "1 (B) Test task +existing @context"
657
651
  with patch.object(
658
652
  self.todo_shell, "list_tasks", return_value=sample_task
659
- ) as mock_list, patch.object(
653
+ ), patch.object(
660
654
  self.todo_shell, "replace", return_value="Task updated"
661
655
  ) as mock_replace:
662
656
  result = self.todo_shell.set_project(1, [])
@@ -672,7 +666,7 @@ class TestTodoShell:
672
666
  sample_task = "1 (C) Test task +existing @context"
673
667
  with patch.object(
674
668
  self.todo_shell, "list_tasks", return_value=sample_task
675
- ) as mock_list, patch.object(
669
+ ), patch.object(
676
670
  self.todo_shell, "replace", return_value="Task updated"
677
671
  ) as mock_replace:
678
672
  result = self.todo_shell.set_project(1, ["", "valid", " ", "another"])
@@ -689,7 +683,7 @@ class TestTodoShell:
689
683
  sample_task = "1 Test task @context"
690
684
  with patch.object(
691
685
  self.todo_shell, "list_tasks", return_value=sample_task
692
- ) as mock_list, patch.object(
686
+ ), patch.object(
693
687
  self.todo_shell, "replace", return_value="Task updated"
694
688
  ) as mock_replace:
695
689
  result = self.todo_shell.set_project(1, ["+work", "+home"])
@@ -704,7 +698,7 @@ class TestTodoShell:
704
698
  sample_task = "1 Test task +work +home +shopping @context"
705
699
  with patch.object(
706
700
  self.todo_shell, "list_tasks", return_value=sample_task
707
- ) as mock_list, patch.object(
701
+ ), patch.object(
708
702
  self.todo_shell, "replace", return_value="Task updated"
709
703
  ) as mock_replace:
710
704
  result = self.todo_shell.set_project(1, ["-+work", "-+shopping"])
@@ -716,10 +710,12 @@ class TestTodoShell:
716
710
  def test_set_project_preserves_all_other_components(self):
717
711
  """Test that set_project preserves all task components when modifying projects."""
718
712
  # Mock the list_tasks to return a complex task
719
- sample_task = "1 (A) Complex task +old +keep @office rec:weekly due:2025-01-15 custom:tag"
713
+ sample_task = (
714
+ "1 (A) Complex task +old +keep @office rec:weekly due:2025-01-15 custom:tag"
715
+ )
720
716
  with patch.object(
721
717
  self.todo_shell, "list_tasks", return_value=sample_task
722
- ) as mock_list, patch.object(
718
+ ), patch.object(
723
719
  self.todo_shell, "replace", return_value="Task updated"
724
720
  ) as mock_replace:
725
721
  result = self.todo_shell.set_project(1, ["-old", "new"])
@@ -733,9 +729,7 @@ class TestTodoShell:
733
729
  """Test that set_project raises error for project that becomes empty after cleaning."""
734
730
  # Mock the list_tasks to return a task
735
731
  sample_task = "1 Test task"
736
- with patch.object(
737
- self.todo_shell, "list_tasks", return_value=sample_task
738
- ) as mock_list:
732
+ with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
739
733
  with pytest.raises(TodoShellError, match="Project name cannot be empty"):
740
734
  self.todo_shell.set_project(1, ["+"])
741
735
 
@@ -743,9 +737,7 @@ class TestTodoShell:
743
737
  """Test that set_project raises error for remove project that becomes empty after cleaning."""
744
738
  # Mock the list_tasks to return a task
745
739
  sample_task = "1 Test task"
746
- with patch.object(
747
- self.todo_shell, "list_tasks", return_value=sample_task
748
- ) as mock_list:
740
+ with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
749
741
  with pytest.raises(TodoShellError, match="Project name cannot be empty"):
750
742
  self.todo_shell.set_project(1, ["-+"])
751
743
 
@@ -753,9 +745,7 @@ class TestTodoShell:
753
745
  """Test that set_project raises error for invalid task number."""
754
746
  # Mock the list_tasks to return only one task
755
747
  sample_task = "1 Test task"
756
- with patch.object(
757
- self.todo_shell, "list_tasks", return_value=sample_task
758
- ) as mock_list:
748
+ with patch.object(self.todo_shell, "list_tasks", return_value=sample_task):
759
749
  with pytest.raises(TodoShellError, match="Task number 5 not found"):
760
750
  self.todo_shell.set_project(5, ["work"])
761
751
 
@@ -765,7 +755,7 @@ class TestTodoShell:
765
755
  sample_task = "1 Test task +work @context"
766
756
  with patch.object(
767
757
  self.todo_shell, "list_tasks", return_value=sample_task
768
- ) as mock_list, patch.object(
758
+ ), patch.object(
769
759
  self.todo_shell, "replace", return_value="Task updated"
770
760
  ) as mock_replace:
771
761
  result = self.todo_shell.set_project(1, ["work", "new"])