Repository: fniessink/next-action

Issue:
Unrecognised argument error when filtering on project after context and excluded context
```console
$ next-action @context -@excluded_context +project
Usage: next-action [-h] [--version] [-c [<config.cfg>] | -w] [-f <todo.txt> ...] [-r <ref>] [-s [<style>]] [-a | -n <number>] [-d [<due date>] | -o] [-p [<priority>]] [--] [<context|project> ...]
next-action: error: unrecognized arguments: +project
```

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ced4b2..47525ec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,12 +5,13 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
-## [Unreleased] - 2018-06-30
+## [Unreleased] - 2018-07-01
 
 ### Fixed
 
-- When generating a configuration file with `--write-config-file` also include context and project filters passed on the command-line. Closes #141.
-- When generating a configuration file with `--write-config-file` also include the minimum priority if passed on the command-line. Closes #142.
+- When generating a configuration file with `--write-config-file` also include context and project filters passed on the command-line. Fixes #141.
+- When generating a configuration file with `--write-config-file` also include the minimum priority if passed on the command-line. Fixes #142.
+- Accept other arguments after excluded contexts and projects. Fixes #143.
 
 ## [1.5.0] - 2018-06-30
 
diff --git a/README.md b/README.md
index 69b3265..be958a8 100644
--- a/README.md
+++ b/README.md
@@ -448,9 +448,9 @@ To run the unit tests:
 
 ```console
 $ python -m unittest
-..........................................................................................................................................................................................................................................
+...........................................................................................................................................................................................................................................
 ----------------------------------------------------------------------
-Ran 234 tests in 2.059s
+Ran 235 tests in 1.970s
 
 OK
 ```
@@ -461,9 +461,9 @@ To create the unit test coverage report run the unit tests under coverage with:
 
 ```console
 $ coverage run --branch -m unittest
-..........................................................................................................................................................................................................................................
+...........................................................................................................................................................................................................................................
 ----------------------------------------------------------------------
-Ran 234 tests in 2.740s
+Ran 235 tests in 3.107s
 
 OK
 ```
@@ -475,7 +475,7 @@ $ coverage report --fail-under=100 --omit=".venv/*" --skip-covered
 Name    Stmts   Miss Branch BrPart  Cover
 -----------------------------------------
 -----------------------------------------
-TOTAL    1329      0    170      0   100%
+TOTAL    1341      0    173      0   100%
 
 25 files skipped due to complete coverage.
 ```
diff --git a/next_action/arguments/parser.py b/next_action/arguments/parser.py
index aabbcdb..6e55749 100644
--- a/next_action/arguments/parser.py
+++ b/next_action/arguments/parser.py
@@ -122,7 +122,7 @@ class NextActionArgumentParser(argparse.ArgumentParser):
 
     def parse_args(self, args=None, namespace=None) -> argparse.Namespace:
         """ Parse the command-line arguments. """
-        namespace, remaining = self.parse_known_args(args, namespace)
+        namespace, remaining = self.parse_known_args(self.sorted_args(args), namespace)
         self.parse_remaining_args(remaining, namespace)
         if getattr(namespace, "config_file", self.get_default("config_file")) is not None:
             self.process_config_file(namespace)
@@ -132,10 +132,18 @@ class NextActionArgumentParser(argparse.ArgumentParser):
             self.exit()
         return namespace
 
+    @classmethod
+    def sorted_args(cls, args: List[str] = None) -> List[str]:
+        """ Sort the arguments so that the excluded contexts and projects are last and can be parsed by
+            parse_remaining_args. """
+        args = args or sys.argv[1:]
+        return [arg for arg in args if not cls.is_excluded_filter(arg)] + \
+               [arg for arg in args if cls.is_excluded_filter(arg)]
+
     def parse_remaining_args(self, remaining: List[str], namespace: argparse.Namespace) -> None:
         """ Parse the remaining command line arguments. """
         for value in remaining:
-            if value.startswith("-@") or value.startswith("-+"):
+            if self.is_excluded_filter(value):
                 argument = value[len("-"):]
                 if not argument[len("@"):]:
                     argument_type = "context" if argument.startswith("@") else "project"
@@ -198,6 +206,11 @@ class NextActionArgumentParser(argparse.ArgumentParser):
         return not any([command_line_arg.startswith(argument) for argument in arguments
                         for command_line_arg in sys.argv])
 
+    @staticmethod
+    def is_excluded_filter(argument: str) -> bool:
+        """ Return whether the argument is an excluded context or project. """
+        return argument.startswith("-@") or argument.startswith("-+")
+
     @staticmethod
     def filter_not_specified(filtered: str) -> bool:
         """ Return whether the context or project or its opposite were specified on the command line. """


Test patch:
diff --git a/tests/unittests/arguments/test_parser.py b/tests/unittests/arguments/test_parser.py
index 6e59879..116659e 100644
--- a/tests/unittests/arguments/test_parser.py
+++ b/tests/unittests/arguments/test_parser.py
@@ -173,6 +173,14 @@ class FilterArgumentTest(ParserTestCase):
         self.assertEqual({"home", "weekend"}, parse_arguments()[1].contexts)
         self.assertEqual({"DogHouse", "PaintHouse"}, parse_arguments()[1].projects)
 
+    @patch.object(sys, "argv", ["next-action", "@home", "-@work", "+PaintHouse"])
+    def test_project_after_excluded(self):
+        """ Test project after excluded context. """
+        namespace = parse_arguments()[1]
+        self.assertEqual({"home"}, namespace.contexts)
+        self.assertEqual({"work"}, namespace.excluded_contexts)
+        self.assertEqual({"PaintHouse"}, namespace.projects)
+
     @patch.object(sys, "argv", ["next-action", "home"])
     @patch.object(sys.stderr, "write")
     def test_faulty_option(self, mock_stderr_write):