Repository: fniessink/next-action

Issue:
Allow for specifying a context
`$ next-action @home`
(A) Install smoke detectors @home

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b32444..4fbf629 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
 
 ### Added
 
+- Allow for limiting the next action to a specific context. Closes #5.
 - Version number command line argument (`--version`) to display Next-action's version number.
 
 ### Fixed
diff --git a/README.md b/README.md
index 35543d7..2d235e0 100644
--- a/README.md
+++ b/README.md
@@ -23,16 +23,17 @@ Next-action requires Python 3.6 or newer.
 
 ```console
 $ next_action --help
-
-usage: next_action [-h] [-f FILE] [--version]
+usage: next_action [-h] [--version] [-f FILE] [@CONTEXT]
 
 Show the next action in your todo.txt
 
+positional arguments:
+  @CONTEXT              Show the next action in the specified context (default: None)
+
 optional arguments:
   -h, --help            show this help message and exit
-  -f FILE, --file FILE  filename of the todo.txt file to read (default:
-                        todo.txt)
   --version             show program's version number and exit
+  -f FILE, --file FILE  filename of the todo.txt file to read (default: todo.txt)
 ```
 
 ## Develop
diff --git a/next_action/__init__.py b/next_action/__init__.py
index 7fe93c7..bdbeff0 100644
--- a/next_action/__init__.py
+++ b/next_action/__init__.py
@@ -1,3 +1,4 @@
 from .pick_action import next_action_based_on_priority
 
+__title__ = "next-action"
 __version__ = "0.0.3"
diff --git a/next_action/arguments.py b/next_action/arguments.py
index 90f69bf..8485bf5 100644
--- a/next_action/arguments.py
+++ b/next_action/arguments.py
@@ -1,13 +1,31 @@
 import argparse
+import os
+import shutil
 
 import next_action
 
 
+class ContextAction(argparse.Action):
+    """ A context action that checks whether contexts start with an @. """
+    def __call__(self, parser, namespace, values, option_string=None):
+        if not values:
+            return
+        if values.startswith("@"):
+            setattr(namespace, self.dest, values)
+        else:
+            parser.error("Contexts should start with an @.")
+
+
 def parse_arguments() -> argparse.Namespace:
     """ Parse the command line arguments. """
+    # Ensure that the help info is printed using all columns available
+    os.environ['COLUMNS'] = str(shutil.get_terminal_size().columns)
+
     parser = argparse.ArgumentParser(description="Show the next action in your todo.txt",
                                      formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+    parser.add_argument("--version", action="version", version="Next-action {0}".format(next_action.__version__))
     parser.add_argument("-f", "--file", help="filename of the todo.txt file to read",
                         type=str, default="todo.txt")
-    parser.add_argument("--version", action="version", version="Next-action {0}".format(next_action.__version__))
+    parser.add_argument("context", metavar="@CONTEXT", help="Show the next action in the specified context", nargs="?",
+                        type=str, action=ContextAction)
     return parser.parse_args()
diff --git a/next_action/cli.py b/next_action/cli.py
index baebe98..45dc8d8 100644
--- a/next_action/cli.py
+++ b/next_action/cli.py
@@ -4,7 +4,8 @@ from next_action.arguments import parse_arguments
 
 
 def next_action() -> None:
-    filename: str = parse_arguments().file
+    arguments = parse_arguments()
+    filename: str = arguments.file
     try:
         todotxt_file = open(filename, "r")
     except FileNotFoundError:
@@ -12,5 +13,5 @@ def next_action() -> None:
         return
     with todotxt_file:
         tasks = [Task(line.strip()) for line in todotxt_file.readlines()]
-    action = next_action_based_on_priority(tasks)
+    action = next_action_based_on_priority(tasks, arguments.context)
     print(action.text if action else "Nothing to do!")
diff --git a/next_action/pick_action.py b/next_action/pick_action.py
index 7366341..35dd9fd 100644
--- a/next_action/pick_action.py
+++ b/next_action/pick_action.py
@@ -3,8 +3,12 @@ from typing import Optional, Iterable
 from .todotxt import Task
 
 
-def next_action_based_on_priority(tasks: Iterable[Task]) -> Optional[Task]:
+def next_action_based_on_priority(tasks: Iterable[Task], context: str = "") -> Optional[Task]:
     """ Return the next action from the collection of tasks. """
     uncompleted_tasks = [task for task in tasks if not task.is_completed()]
-    sorted_tasks = sorted(uncompleted_tasks, key=lambda task: task.priority() or "ZZZ")
+    if context:
+        tasks_in_context = [task for task in tasks if context.strip("@") in task.contexts()]
+    else:
+        tasks_in_context = uncompleted_tasks
+    sorted_tasks = sorted(tasks_in_context, key=lambda task: task.priority() or "ZZZ")
     return sorted_tasks[0] if sorted_tasks else None
diff --git a/setup.py b/setup.py
index a7d4775..46d8260 100644
--- a/setup.py
+++ b/setup.py
@@ -4,15 +4,15 @@ import next_action
 
 
 setup(
-    name="next-action",
+    name=next_action.__title__,
     version=next_action.__version__,
-    description="Show the next action to work on from a todo.txt file",
+    description="Command-line application to show the next action to work on from a todo.txt file",
     long_description="""Show the next action to work on from a todo.txt file, based on context, priority,
 and more.""",
-    author='Frank Niessink',
-    author_email='frank@niessink.com',
-    url='https://github.com/fniessink/next-action',
-    license='Apache License, Version 2.0',
+    author="Frank Niessink",
+    author_email="frank@niessink.com",
+    url="https://github.com/fniessink/next-action",
+    license="Apache License, Version 2.0",
     python_requires=">=3.6",
     packages=find_packages(),
     entry_points={


Test patch:
diff --git a/tests/unittests/test_arguments.py b/tests/unittests/test_arguments.py
index 48c5f17..3156a25 100644
--- a/tests/unittests/test_arguments.py
+++ b/tests/unittests/test_arguments.py
@@ -1,6 +1,7 @@
-import unittest
-from unittest.mock import patch
+import os
 import sys
+import unittest
+from unittest.mock import patch, call
 
 from next_action.arguments import parse_arguments
 
@@ -22,3 +23,23 @@ class ArgumentParserTest(unittest.TestCase):
     def test_long_filename_argument(self):
         """ Test that the argument parser accepts a filename. """
         self.assertEqual("my_other_todo.txt", parse_arguments().file)
+
+    @patch.object(sys, "argv", ["next_action"])
+    def test_no_context(self):
+        """ Test that the argument parser returns no context if the user doesn't pass one. """
+        self.assertEqual(None, parse_arguments().context)
+
+    @patch.object(sys, "argv", ["next_action", "@home"])
+    def test_one_context(self):
+        """ Test that the argument parser returns the context if the user passes one. """
+        self.assertEqual("@home", parse_arguments().context)
+
+    @patch.object(sys, "argv", ["next_action", "home"])
+    @patch.object(sys.stderr, "write")
+    def test_faulty_context(self, mock_stderr_write):
+        """ Test that the argument parser exits if the context is faulty. """
+        os.environ['COLUMNS'] = "120"  # Fake that the terminal is wide enough.
+        self.assertRaises(SystemExit, parse_arguments)
+        self.assertEqual([call("usage: next_action [-h] [--version] [-f FILE] [@CONTEXT]\n"),
+                          call("next_action: error: Contexts should start with an @.\n")],
+                         mock_stderr_write.call_args_list)
\ No newline at end of file
diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py
index b84f948..3cdefe1 100644
--- a/tests/unittests/test_cli.py
+++ b/tests/unittests/test_cli.py
@@ -1,6 +1,7 @@
+import os
+import sys
 import unittest
 from unittest.mock import patch, mock_open, call
-import sys
 
 from next_action.cli import next_action
 from next_action import __version__
@@ -37,20 +38,20 @@ class CLITest(unittest.TestCase):
     @patch.object(sys, "argv", ["next_action", "--help"])
     @patch.object(sys.stdout, "write")
     def test_help(self, mock_stdout_write):
-        """ Test that the help contains the default filename. """
-        try:
-            next_action()
-        except SystemExit:
-            pass
-        self.assertEqual(call("""usage: next_action [-h] [-f FILE] [--version]
+        """ Test the help message. """
+        os.environ['COLUMNS'] = "120"  # Fake that the terminal is wide enough.
+        self.assertRaises(SystemExit, next_action)
+        self.assertEqual(call("""usage: next_action [-h] [--version] [-f FILE] [@CONTEXT]
 
 Show the next action in your todo.txt
 
+positional arguments:
+  @CONTEXT              Show the next action in the specified context (default: None)
+
 optional arguments:
   -h, --help            show this help message and exit
-  -f FILE, --file FILE  filename of the todo.txt file to read (default:
-                        todo.txt)
   --version             show program's version number and exit
+  -f FILE, --file FILE  filename of the todo.txt file to read (default: todo.txt)
 """),
                         mock_stdout_write.call_args_list[0])
 
@@ -58,8 +59,5 @@ optional arguments:
     @patch.object(sys.stdout, "write")
     def test_version(self, mock_stdout_write):
         """ Test that --version shows the version number. """
-        try:
-            next_action()
-        except SystemExit:
-            pass
+        self.assertRaises(SystemExit, next_action)
         self.assertEqual([call("Next-action {0}\n".format(__version__))], mock_stdout_write.call_args_list)
\ No newline at end of file
diff --git a/tests/unittests/test_pick_action.py b/tests/unittests/test_pick_action.py
index 8910ff4..16f37f6 100644
--- a/tests/unittests/test_pick_action.py
+++ b/tests/unittests/test_pick_action.py
@@ -40,4 +40,11 @@ class PickActionTest(unittest.TestCase):
         completed_task = todotxt.Task("x Completed")
         uncompleted_task = todotxt.Task("Todo")
         self.assertEqual(uncompleted_task,
-                         pick_action.next_action_based_on_priority([completed_task, uncompleted_task]))
\ No newline at end of file
+                         pick_action.next_action_based_on_priority([completed_task, uncompleted_task]))
+
+    def test_next_action_limited_to_context(self):
+        """ Test that the next action can be limited to a specific context. """
+        task1 = todotxt.Task("Todo 1 @work")
+        task2 = todotxt.Task("(B) Todo 2 @work")
+        task3 = todotxt.Task("(A) Todo 3 @home")
+        self.assertEqual(task2, pick_action.next_action_based_on_priority([task1, task2, task3], context="work"))