[Django] #35926: Support capturing the remainder of a command-line arguments in BaseCommand

4 views
Skip to first unread message

Django

unread,
Nov 20, 2024, 8:08:09 AM11/20/24
to django-...@googlegroups.com
#35926: Support capturing the remainder of a command-line arguments in BaseCommand
-------------------------------------+-------------------------------------
Reporter: Daniel Quinn | Type: New
| feature
Status: new | Component: Core
| (Management commands)
Version: 5.1 | Severity: Normal
Keywords: management command, | Triage Stage:
remainder | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
I wanted to write a management command that would accept a few known
arguments and then capture the remaining arbitrary arguments passed as a
separate value. For example:


{{{
./manage.py mycommand --expected=argument --something-arbitrary -x -y -z
}}}

So `--expected-argument` would be defined in `.add_arguments()`, but the
other args would just be available as something like
`options["remainder"]` or whatever.

This is usually done by calling `parse_known_args()` rather than the more
common `.parse_args()`, but unfortunately there's currently no way to
cleanly indicate this with BaseCommand. The only way seems to be to
copy/paste the entirety of `BaseCommand.run_from_argv()` and then change
where`.parse_args()` is invoked:

{{{
def run_from_argv(self, argv):
"""
Copypasta from the parent class, so I can change `.parse_args()`
to
`.parse_known_args()`.
"""

self._called_from_command_line = True
parser = self.create_parser(argv[0], argv[1])

# Change
--------------------------------------------------------------
options, remainder = parser.parse_known_args(argv[2:])
cmd_options = vars(options)
cmd_options["remainder"] = remainder
# /Change
-------------------------------------------------------------

# Move positional args out of options to mimic legacy optparse
args = cmd_options.pop("args", ())
handle_default_options(options)
try:
self.execute(*args, **cmd_options)
except CommandError as e:
if options.traceback:
raise

# SystemCheckError takes care of its own formatting.
if isinstance(e, SystemCheckError):
self.stderr.write(str(e), lambda x: x)
else:
self.stderr.write("%s: %s" % (e.__class__.__name__, e))
sys.exit(e.returncode)
finally:
try:
connections.close_all()
except ImproperlyConfigured:
# Ignore if connections aren't setup at this point (e.g.
no
# configured settings).
pass
}}}

Obviously that's not ideal.

One option might be to just use `.parse_known_args()` and then allow the
user to indicate whether they want to capture the remainder or not, and if
so, as what attribute, but I have no strong feelings about implementation.
--
Ticket URL: <https://code.djangoproject.com/ticket/35926>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Nov 20, 2024, 9:25:13 AM11/20/24
to django-...@googlegroups.com
#35926: Support capturing the remainder of a command-line arguments in BaseCommand
-------------------------------------+-------------------------------------
Reporter: Daniel Quinn | Owner: (none)
Type: New feature | Status: closed
Component: Core (Management | Version: dev
commands) |
Severity: Normal | Resolution: wontfix
Keywords: unknown args | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Natalia Bidart):

* keywords: management command, remainder => unknown args
* resolution: => wontfix
* status: new => closed
* version: 5.1 => dev

Comment:

Hello Daniel, thank you for taking the time to create this ticket. I see
two sides in your request:

== Side A ==
As a Django Fellow, I think that the provided example seems a very
specific need arising from a niche use case. I don't think this applies to
the broader ecosystem, and Django is a framework designed to offer robust
and accurate solutions for common scenarios. Furthermore, as a seasoned
developer, I think that allowing any amount of unknown named params in a
command is a bad programming pattern, so I would advice against that
pattern.

What you could do instead, is to allow any number of arguments associated
with a single argument name that is defined in your parser, something
similar to what [https://docs.djangoproject.com/en/5.1/howto/custom-
management-commands/ the docs shows for custom management commands] when
defining nargs for `polls_ids`:

{{{#!python
def add_arguments(self, parser):
parser.add_argument("poll_ids", nargs="+", type=int)
}}}

(Yes, this limits the type of the nargs but I think that is a good thing!)

== Side B ==
Assuming that we consider the provided use case and want to build a
solution, I think the best approach would be, in your code, to monkeypatch
`CommandParser` to be replaced with a custom class that reimplements
`parse_args` and stores the unknown args to do something with them when
needed:

{{{#!diff
diff --git a/django/core/management/base.py
b/django/core/management/base.py
index 6232b42bd4..d8ea38ac3c 100644
--- a/django/core/management/base.py
+++ b/django/core/management/base.py
@@ -65,7 +65,9 @@ class CommandParser(ArgumentParser):
args or any(not arg.startswith("-") for arg in args)
):
self.error(self.missing_args_message)
- return super().parse_args(args, namespace)
+ known, unknown = super().parse_known_args(args, namespace)
+ # Do something with unknown args.
+ return known

def error(self, message):
}}}

Given the above, I'll close the ticket accordingly, but if you disagree,
you can consider starting a new conversation on the
[https://forum.djangoproject.com/c/internals/5 Django Forum], where you'll
reach a wider audience and likely get extra feedback. More information in
[https://docs.djangoproject.com/en/stable/internals/contributing/bugs-and-
features/#requesting-features the documented guidelines for requesting
features].
--
Ticket URL: <https://code.djangoproject.com/ticket/35926#comment:1>
Reply all
Reply to author
Forward
0 new messages