Repository: fractal-analytics-platform/fractal-client (language: python)

Issue:
Forbid abbreviations in argparse
Currently, the action triggered by
```
fractal task edit ... --default-args-file /some/file.json
```
is the same as the one triggered by any of
```
fractal task edit ... --default-arg /some/file.json
fractal task edit ... --default /some/file.json
fractal task edit ... --defa /some/file.json
...
```

Let's **not** allow this (default argparse) behavior, by setting `allow_abbrev=False` (see https://docs.python.org/3/library/argparse.html#prefix-matching). That is, the user should type the full argument name, and abbreviations should not be allowed.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 11be93a92..22128a031 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,9 @@
 # Unreleased
 
+* Update `_TaskBase` schema from `fractal-common` (\#431).
 * Drop support for python 3.8 (\#438).
-* Update `_TaskBase` schema from fractal-common (\#431).
 * Update `fractal task edit` command (\#439).
+* Disable `argparse` abbreviation for CLI commands (\#441).
 
 # 1.0.5
 
diff --git a/fractal/parser.py b/fractal/parser.py
index 994ea4caf..7d47fce12 100644
--- a/fractal/parser.py
+++ b/fractal/parser.py
@@ -17,7 +17,8 @@
 
 
 parser_main = ap.ArgumentParser(
-    description="Command-line interface for Fractal Client"
+    description="Command-line interface for Fractal Client",
+    allow_abbrev=False,
 )
 
 parser_main.add_argument(
@@ -50,7 +51,9 @@
 
 # PROJECT GROUP
 project_parser = subparsers_main.add_parser(
-    "project", description="Project commands"
+    "project",
+    description="Project commands",
+    allow_abbrev=False,
 )
 project_subparsers = project_parser.add_subparsers(
     title="Valid subcommands:", dest="subcmd", required=True
@@ -58,7 +61,9 @@
 
 # project new
 project_new_parser = project_subparsers.add_parser(
-    "new", description="Create new project"
+    "new",
+    description="Create new project",
+    allow_abbrev=False,
 )
 project_new_parser.add_argument("name", help="Name of new project", type=str)
 project_new_parser.add_argument(
@@ -79,12 +84,16 @@
 
 # project list
 project_list_parser = project_subparsers.add_parser(
-    "list", description="List projects"
+    "list",
+    description="List projects",
+    allow_abbrev=False,
 )
 
 # project show
 project_show_parser = project_subparsers.add_parser(
-    "show", description="Show details of single project"
+    "show",
+    description="Show details of single project",
+    allow_abbrev=False,
 )
 project_show_parser.add_argument(
     "project_id", type=int, help="ID of project to show"
@@ -92,7 +101,9 @@
 
 # project delete
 project_delete_parser = project_subparsers.add_parser(
-    "delete", description="Delete project"
+    "delete",
+    description="Delete project",
+    allow_abbrev=False,
 )
 project_delete_parser.add_argument(
     "project_id", type=int, help="ID of project to delete"
@@ -100,7 +111,9 @@
 
 # project add-dataset
 project_add_dataset_parser = project_subparsers.add_parser(
-    "add-dataset", description="Add dataset to project"
+    "add-dataset",
+    description="Add dataset to project",
+    allow_abbrev=False,
 )
 project_add_dataset_parser.add_argument(
     "project_id", type=int, help="ID of project to add the new dataset to"
@@ -116,7 +129,9 @@
 
 # DATASET GROUP
 dataset_parser = subparsers_main.add_parser(
-    "dataset", description="Dataset commands"
+    "dataset",
+    description="Dataset commands",
+    allow_abbrev=False,
 )
 dataset_subparsers = dataset_parser.add_subparsers(
     title="Valid subcommands:", dest="subcmd", required=True
@@ -124,7 +139,9 @@
 
 # dataset add-resource
 dataset_add_resource_parser = dataset_subparsers.add_parser(
-    "add-resource", description="Add resource to existing dataset"
+    "add-resource",
+    description="Add resource to existing dataset",
+    allow_abbrev=False,
 )
 dataset_add_resource_parser.add_argument(
     "project_id", type=int, help="Project ID"
@@ -139,7 +156,9 @@
 
 # dataset rm-resource
 dataset_rm_resource_parser = dataset_subparsers.add_parser(
-    "rm-resource", description="Remove resource to existing dataset"
+    "rm-resource",
+    description="Remove resource to existing dataset",
+    allow_abbrev=False,
 )
 dataset_rm_resource_parser.add_argument(
     "project_id", type=int, help="Project ID"
@@ -153,7 +172,10 @@
 
 # dataset edit
 dataset_edit_parser = dataset_subparsers.add_parser(
-    "edit", description="Edit dataset", argument_default=ap.SUPPRESS
+    "edit",
+    description="Edit dataset",
+    argument_default=ap.SUPPRESS,
+    allow_abbrev=False,
 )
 dataset_edit_parser.add_argument("project_id", type=int, help="Project ID")
 dataset_edit_parser.add_argument("dataset_id", type=int, help="Dataset ID")
@@ -182,32 +204,47 @@
 
 # dataset show
 dataset_show_parser = dataset_subparsers.add_parser(
-    "show", description="Show dataset", argument_default=ap.SUPPRESS
+    "show",
+    description="Show dataset",
+    argument_default=ap.SUPPRESS,
+    allow_abbrev=False,
 )
 dataset_show_parser.add_argument("project_id", type=int, help="Project ID")
 dataset_show_parser.add_argument("dataset_id", type=int, help="Dataset ID")
 
 # dataset delete
 dataset_delete_parser = dataset_subparsers.add_parser(
-    "delete", description="Delete dataset", argument_default=ap.SUPPRESS
+    "delete",
+    description="Delete dataset",
+    argument_default=ap.SUPPRESS,
+    allow_abbrev=False,
 )
 dataset_delete_parser.add_argument("project_id", type=int, help="Project ID")
 dataset_delete_parser.add_argument("dataset_id", type=int, help="Dataset ID")
 
 
 # TASK GROUP
-task_parser = subparsers_main.add_parser("task", description="Task commands")
+task_parser = subparsers_main.add_parser(
+    "task",
+    description="Task commands",
+    allow_abbrev=False,
+)
 task_subparsers = task_parser.add_subparsers(
     title="Valid subcommands:", dest="subcmd", required=True
 )
 
 # task list
-task_list_parser = task_subparsers.add_parser("list", description="List tasks")
+task_list_parser = task_subparsers.add_parser(
+    "list",
+    description="List tasks",
+    allow_abbrev=False,
+)
 
 # task collect
 task_collect_parser = task_subparsers.add_parser(
     "collect",
     description="Install and collect all tasks from a pip-installable package",
+    allow_abbrev=False,
 )
 task_collect_parser.add_argument(
     "package",
@@ -241,6 +278,7 @@
 task_check_collection_parser = task_subparsers.add_parser(
     "check-collection",
     description="Check status of background task collection processes",
+    allow_abbrev=False,
 )
 task_check_collection_parser.add_argument(
     "state_id",
@@ -255,7 +293,10 @@
 
 # task new
 task_new_parser = task_subparsers.add_parser(
-    "new", description="Create new task", argument_default=ap.SUPPRESS
+    "new",
+    description="Create new task",
+    argument_default=ap.SUPPRESS,
+    allow_abbrev=False,
 )
 task_new_parser.add_argument(
     "name", help="A human readable name for the task", type=str
@@ -289,7 +330,10 @@
 
 # task edit
 task_edit_parser = task_subparsers.add_parser(
-    "edit", description="Edit task", argument_default=ap.SUPPRESS
+    "edit",
+    description="Edit task",
+    argument_default=ap.SUPPRESS,
+    allow_abbrev=False,
 )
 task_edit_parser.add_argument(
     "task_id_or_name", help="ID or name of task to edit", type=str
@@ -318,7 +362,10 @@
 
 # task delete
 task_delete_parser = task_subparsers.add_parser(
-    "delete", description="Delete task", argument_default=ap.SUPPRESS
+    "delete",
+    description="Delete task",
+    argument_default=ap.SUPPRESS,
+    allow_abbrev=False,
 )
 task_delete_parser.add_argument(
     "task_id_or_name", help="ID or name of task to delete", type=str
@@ -328,7 +375,9 @@
 # WORKFLOW GROUP
 
 workflow_parser = subparsers_main.add_parser(
-    "workflow", description="Workflow commands"
+    "workflow",
+    description="Workflow commands",
+    allow_abbrev=False,
 )
 workflow_subparsers = workflow_parser.add_subparsers(
     title="Valid subcommand", dest="subcmd", required=True
@@ -336,7 +385,9 @@
 
 # workflow list
 workflow_list_parser = workflow_subparsers.add_parser(
-    "list", description="List workflows for given project"
+    "list",
+    description="List workflows for given project",
+    allow_abbrev=False,
 )
 workflow_list_parser.add_argument(
     "project_id",
@@ -345,7 +396,9 @@
 
 # workflow new
 workflow_new_parser = workflow_subparsers.add_parser(
-    "new", description="Create new workflow"
+    "new",
+    description="Create new workflow",
+    allow_abbrev=False,
 )
 workflow_new_parser.add_argument(
     "name",
@@ -358,7 +411,9 @@
 
 # workflow show
 workflow_new_parser = workflow_subparsers.add_parser(
-    "show", description="Show workflow"
+    "show",
+    description="Show workflow",
+    allow_abbrev=False,
 )
 workflow_new_parser.add_argument(
     "workflow_id",
@@ -367,7 +422,10 @@
 
 # workflow edit
 workflow_edit_parser = workflow_subparsers.add_parser(
-    "edit", description="Edit workflow", argument_default=ap.SUPPRESS
+    "edit",
+    description="Edit workflow",
+    argument_default=ap.SUPPRESS,
+    allow_abbrev=False,
 )
 workflow_edit_parser.add_argument(
     "workflow_id",
@@ -378,7 +436,9 @@
 
 # workflow delete
 workflow_delete_parser = workflow_subparsers.add_parser(
-    "delete", description="Delete workflow"
+    "delete",
+    description="Delete workflow",
+    allow_abbrev=False,
 )
 workflow_delete_parser.add_argument(
     "workflow_id",
@@ -388,7 +448,9 @@
 
 # workflow add task
 workflow_add_task_parser = workflow_subparsers.add_parser(
-    "add-task", description="Add new task to specific workflow"
+    "add-task",
+    description="Add new task to specific workflow",
+    allow_abbrev=False,
 )
 workflow_add_task_parser.add_argument(
     "workflow_id",
@@ -417,7 +479,9 @@
 
 # workflow edit task
 workflow_edit_task_parser = workflow_subparsers.add_parser(
-    "edit-task", description="Edit task within specific workflow"
+    "edit-task",
+    description="Edit task within specific workflow",
+    allow_abbrev=False,
 )
 workflow_edit_task_parser.add_argument(
     "workflow_id",
@@ -444,7 +508,9 @@
 
 # workflow remove task
 workflow_remove_task_parser = workflow_subparsers.add_parser(
-    "rm-task", description="Remove task from a specific workflow"
+    "rm-task",
+    description="Remove task from a specific workflow",
+    allow_abbrev=False,
 )
 workflow_remove_task_parser.add_argument(
     "workflow_id",
@@ -460,6 +526,7 @@
     "apply",
     description="Apply workflow to dataset",
     argument_default=ap.SUPPRESS,
+    allow_abbrev=False,
 )
 workflow_apply_parser.add_argument("workflow_id")
 workflow_apply_parser.add_argument("input_dataset_id")
@@ -487,6 +554,7 @@
 workflow_import_parser = workflow_subparsers.add_parser(
     "import",
     description="Import workflow to project from file",
+    allow_abbrev=False,
 )
 workflow_import_parser.add_argument(
     "--project-id",
@@ -502,6 +570,7 @@
 workflow_export_parser = workflow_subparsers.add_parser(
     "export",
     description="Export workflow to file",
+    allow_abbrev=False,
 )
 workflow_export_parser.add_argument(
     "workflow_id",
@@ -515,14 +584,20 @@
 
 # JOB GROUP
 
-job_parser = subparsers_main.add_parser("job", description="Job commands")
+job_parser = subparsers_main.add_parser(
+    "job",
+    description="Job commands",
+    allow_abbrev=False,
+)
 job_subparsers = job_parser.add_subparsers(
     title="Valid subcommand", dest="subcmd", required=True
 )
 
 # job list
 job_list_parser = job_subparsers.add_parser(
-    "list", description="List jobs for given project"
+    "list",
+    description="List jobs for given project",
+    allow_abbrev=False,
 )
 job_list_parser.add_argument(
     "project_id",
@@ -534,6 +609,7 @@
     "show",
     description="Query status of workflow-execution job",
     argument_default=ap.SUPPRESS,
+    allow_abbrev=False,
 )
 job_show_parser.add_argument(
     "job_id",
@@ -551,6 +627,7 @@
 job_download_logs_parser = job_subparsers.add_parser(
     "download-logs",
     description="Download full folder of workflow-execution job",
+    allow_abbrev=False,
 )
 job_download_logs_parser.add_argument(
     "job_id",
@@ -566,7 +643,9 @@
 
 # VERSION GROUP
 version_parser = subparsers_main.add_parser(
-    "version", description="Print version and exit"
+    "version",
+    description="Print version and exit",
+    allow_abbrev=False,
 )
 
 
@@ -575,6 +654,7 @@
 user_parser = subparsers_main.add_parser(
     "user",
     description="User commands",
+    allow_abbrev=False,
 )
 user_subparsers = user_parser.add_subparsers(
     title="Valid subcommand", dest="subcmd", required=True
@@ -584,11 +664,14 @@
 user_whoami_parser = user_subparsers.add_parser(
     "whoami",
     description="Get info on current user (fails if user is not registered)",
+    allow_abbrev=False,
 )
 
 # user register
 user_register_parser = user_subparsers.add_parser(
-    "register", description="Register a new user with the Fractal server"
+    "register",
+    description="Register a new user with the Fractal server",
+    allow_abbrev=False,
 )
 user_register_parser.add_argument(
     "new_email", help="Email to be used as username"
@@ -612,17 +695,22 @@
 user_list_parser = user_subparsers.add_parser(
     "list",
     description="List all users",
+    allow_abbrev=False,
 )
 
 # user show
 user_show_parser = user_subparsers.add_parser(
-    "show", description="Show details of single user"
+    "show",
+    description="Show details of single user",
+    allow_abbrev=False,
 )
 user_show_parser.add_argument("user_id", help="ID of the user")
 
 # user edit
 user_edit_parser = user_subparsers.add_parser(
-    "edit", description="Edit details of single user"
+    "edit",
+    description="Edit details of single user",
+    allow_abbrev=False,
 )
 user_edit_parser.add_argument("user_id", help="ID of the user")
 user_edit_parser.add_argument(
@@ -651,6 +739,8 @@
 
 # user delete
 user_delete_parser = user_subparsers.add_parser(
-    "delete", description="Delete a single user"
+    "delete",
+    description="Delete a single user",
+    allow_abbrev=False,
 )
 user_delete_parser.add_argument("user_id", help="ID of the user")


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 28f6fb75c..f916dfaca 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -84,3 +84,26 @@ async def test_missing_credentials(monkeypatch):
         debug(e.value)
         debug(e.value.args[0])
         assert "FRACTAL_USER" in e.value.args[0]
+
+
+async def test_argparse_abbreviation(invoke_as_superuser):
+    """
+    Check that argparse abbreviations are disabled on at least one command.
+
+    Refs:
+    * https://github.com/fractal-analytics-platform/fractal/issues/440
+    * https://docs.python.org/3/library/argparse.html#prefix-matching
+    """
+
+    # Successful invoke
+    res = await invoke_as_superuser(
+        "user register test@mail.com secret --superuser"
+    )
+    res.show()
+    assert res.retcode == 0
+
+    # Failed (abbreviation-based) invoke
+    with pytest.raises(SystemExit):
+        await invoke_as_superuser(
+            "user register test2@mail.com secret2 --super"
+        )