Almad: Thanks!

Before merging in, I'd like to do two things:

* Add test case for pessimistic scenario (codify behavior on conflicting command arguments)
* Add docs to describe how this should behave
rcoup: Ahem, forgot all about this one :)

Added some more tests and docs as requested.
Almad: @rcoup ...and I have missed this one because of the failed cross, ahem forever :dagger: 

Would you mind rebasing now please? It should work, and should be straightforward to release now ;) 
rcoup: @Almad done! :shipit: 
diff --git a/docs/source/pavement.rst b/docs/source/pavement.rst
index cf09246..9dae827 100644
--- a/docs/source/pavement.rst
+++ b/docs/source/pavement.rst
@@ -203,6 +203,22 @@ For sharing, following must be fullfilled:
 
 Otherwise, ``PavementError`` is raised.
 
+You can combine both ``@consume_args`` and ``@cmdopts`` together::
+
+    @task
+    @cmdopts([
+        ('username=', 'u', 'Username to use when logging in to the servers')
+    ])
+    @consume_args
+    def exec(options):
+        pass
+
+
+* ``paver exec -u root`` will result in ``options.username = 'root', options.args = []``
+* ``paver exec -u root production`` will result in ``options.username = 'root', options.args = ['production']``
+* ``paver exec production -u root`` will result in ``options.args = ['production', '-u', 'root']`` with no ``options.username`` attribute.
+* ``paver exec -u root production -u other`` will result in ``options.username = 'root', options.args = ['production', '-u', 'other']``
+
 Hiding tasks
 ---------------
 
diff --git a/paver/tasks.py b/paver/tasks.py
index 7300969..09f7d4e 100644
--- a/paver/tasks.py
+++ b/paver/tasks.py
@@ -741,7 +741,8 @@ def _parse_command_line(args):
     if not isinstance(task, Task):
         raise BuildFailure("%s is not a Task" % taskname)
 
-    if task.consume_args != float('inf'):
+    if task.user_options or task.consume_args != float('inf'):
+        # backwards compatibility around mixing of @cmdopts & @consume_args
         args = task.parse_args(args)
     if task.consume_args > 0:
         args = _consume_nargs(task, args)

Define behavior with both @cmdopts and @consume_args set
Latter now overwrites former, which is not what one generally wants.

Parse for cmdopts and remove it from args. 

diff --git a/paver/tests/test_tasks.py b/paver/tests/test_tasks.py
index 7b971f2..1df7c4e 100644
--- a/paver/tests/test_tasks.py
+++ b/paver/tests/test_tasks.py
@@ -404,6 +404,74 @@ def test_consume_args():
     tasks._process_commands("t3 -v 1".split())
     assert t3.called
 
+def test_consume_args_and_options():
+    @tasks.task
+    @tasks.cmdopts([
+        ("foo=", "f", "Help for foo")
+    ])
+    @tasks.consume_args
+    def t1(options):
+        assert options.foo == "1"
+        assert options.t1.foo == "1"
+        assert options.args == ['abc', 'def']
+
+    environment = _set_environment(t1=t1)
+    tasks._process_commands([
+        't1', '--foo', '1', 'abc', 'def',
+    ])
+    assert t1.called
+
+def test_consume_args_and_options_2():
+    @tasks.task
+    @tasks.cmdopts([
+        ("foo=", "f", "Help for foo")
+    ])
+    @tasks.consume_args
+    def t1(options):
+        assert not hasattr(options, 'foo')
+        assert not hasattr(options.t1, 'foo')
+        assert options.args == ['abc', 'def', '--foo', '1']
+
+    environment = _set_environment(t1=t1)
+    tasks._process_commands([
+        't1', 'abc', 'def', '--foo', '1',
+    ])
+    assert t1.called
+
+def test_consume_args_and_options_3():
+    @tasks.task
+    @tasks.cmdopts([
+        ("foo=", "f", "Help for foo")
+    ])
+    @tasks.consume_args
+    def t1(options):
+        assert options.foo == "1"
+        assert options.t1.foo == "1"
+        assert options.args == []
+
+    environment = _set_environment(t1=t1)
+    tasks._process_commands([
+        't1', '--foo', '1',
+    ])
+    assert t1.called
+
+def test_consume_args_and_options_conflict():
+    @tasks.task
+    @tasks.cmdopts([
+        ("foo=", "f", "Help for foo")
+    ])
+    @tasks.consume_args
+    def t1(options):
+        assert options.foo == "1"
+        assert options.t1.foo == "1"
+        assert options.args == ['abc', 'def', '--foo', '2']
+
+    environment = _set_environment(t1=t1)
+    tasks._process_commands([
+        't1', '--foo', '1', 'abc', 'def', '--foo', '2',
+    ])
+    assert t1.called
+
 def test_consume_nargs():
     # consume all args on first task
     @tasks.task

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist pytest-mock pytest-asyncio
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==7.8.0
exceptiongroup==1.2.2
execnet==2.1.1
iniconfig==2.1.0
packaging==24.2
-e git+https://github.com/paver/paver.git@45609b2ad1901144e68746b903345d7de1d03404#egg=Paver
pluggy==1.5.0
pytest==8.3.5
pytest-asyncio==0.26.0
pytest-cov==6.0.0
pytest-mock==3.14.0
pytest-xdist==3.6.1
six==1.17.0
tomli==2.2.1
typing_extensions==4.13.0

name: paver
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.8.0
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - iniconfig==2.1.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - pytest-asyncio==0.26.0
      - pytest-cov==6.0.0
      - pytest-mock==3.14.0
      - pytest-xdist==3.6.1
      - six==1.17.0
      - tomli==2.2.1
      - typing-extensions==4.13.0
prefix: /opt/conda/envs/paver

paver/tests/test_tasks.py::test_consume_args_and_options paver/tests/test_tasks.py::test_consume_args_and_options_2 paver/tests/test_tasks.py::test_consume_args_and_options_3 paver/tests/test_tasks.py::test_consume_args_and_options_conflict
paver/tests/test_tasks.py::test_basic_dependencies paver/tests/test_tasks.py::test_longname_resolution_in_dependencies paver/tests/test_tasks.py::test_chained_dependencies paver/tests/test_tasks.py::test_backwards_compatible_needs paver/tests/test_tasks.py::test_tasks_dont_repeat paver/tests/test_tasks.py::test_basic_command_line paver/tests/test_tasks.py::test_list_tasks paver/tests/test_tasks.py::test_environment_insertion paver/tests/test_tasks.py::test_add_options_to_environment paver/tests/test_tasks.py::test_shortname_access paver/tests/test_tasks.py::test_longname_access paver/tests/test_tasks.py::test_task_command_line_options paver/tests/test_tasks.py::test_setting_of_options_with_equals paver/tests/test_tasks.py::test_options_inherited_via_needs paver/tests/test_tasks.py::test_options_inherited_via_needs_even_from_grandparents paver/tests/test_tasks.py::test_options_shouldnt_overlap paver/tests/test_tasks.py::test_options_shouldnt_overlap_when_bad_task_specified paver/tests/test_tasks.py::test_options_may_overlap_if_explicitly_allowed paver/tests/test_tasks.py::test_exactly_same_parameters_must_be_specified_in_order_to_allow_sharing paver/tests/test_tasks.py::test_dest_parameter_should_map_opt_to_property paver/tests/test_tasks.py::test_dotted_options paver/tests/test_tasks.py::test_dry_run paver/tests/test_tasks.py::test_consume_args paver/tests/test_tasks.py::test_consume_nargs paver/tests/test_tasks.py::test_consume_nargs_and_options paver/tests/test_tasks.py::test_optional_args_in_tasks paver/tests/test_tasks.py::test_debug_logging paver/tests/test_tasks.py::test_base_logging paver/tests/test_tasks.py::test_error_show_up_no_matter_what paver/tests/test_tasks.py::test_all_messages_for_a_task_are_captured paver/tests/test_tasks.py::test_messages_with_formatting_and_no_args_still_work paver/tests/test_tasks.py::test_alternate_pavement_option paver/tests/test_tasks.py::test_captured_output_shows_up_on_exception paver/tests/test_tasks.py::test_calling_subpavement paver/tests/test_tasks.py::test_task_finders paver/tests/test_tasks.py::test_calling_a_function_rather_than_task paver/tests/test_tasks.py::test_depending_on_a_function_rather_than_task paver/tests/test_tasks.py::test_description_retrieval_trial paver/tests/test_tasks.py::test_description_empty_without_docstring paver/tests/test_tasks.py::test_description_retrieval_first_sentence paver/tests/test_tasks.py::test_description_retrieval_first_sentence_even_with_version_numbers paver/tests/test_tasks.py::test_auto_task_is_not_run_with_noauto paver/tests/test_tasks.py::test_auto_task_is_run_when_present paver/tests/test_tasks.py::test_task_can_be_called_repeatedly paver/tests/test_tasks.py::test_options_passed_to_task paver/tests/test_tasks.py::test_calling_task_with_option_arguments paver/tests/test_tasks.py::test_calling_task_with_arguments_do_not_overwrite_it_for_other_tasks paver/tests/test_tasks.py::test_options_might_be_provided_if_task_might_be_called paver/tests/test_tasks.py::test_calling_task_with_arguments paver/tests/test_tasks.py::test_calling_task_with_empty_arguments paver/tests/test_tasks.py::test_calling_nonconsuming_task_with_arguments paver/tests/test_tasks.py::test_options_may_overlap_between_multiple_tasks_even_when_specified_in_reverse_order paver/tests/test_tasks.py::test_options_might_be_shared_both_way paver/tests/test_tasks.py::test_paver_doesnt_crash_on_task_function_with_annotations
docs/source/pavement.rst paver/tasks.py
docs/source/pavement.rst paver/tasks.py