4

I would like to get this with argparse library :

PROG --yesterday | begin-date [end-date]

I tried to combine mutual exclusion and argument groups but I didn't succeed.

This program should only accept that :

PROG --yesterday
PROG 2015-11-12
PROG 2015-11-12 2015-11-15

Is it possible to do this with argparse ?


Thanks hpaulj. See the final result :

import argparse
from datetime import datetime
import pytz


def argument_date(str_date):
    try:
        return datetime.strptime(str_date, "%Y-%m-%d").replace(tzinfo=pytz.utc)
    except ValueError as e:
        raise argparse.ArgumentTypeError(e)

parser = argparse.ArgumentParser(prog='PROG')
parser.usage = """PROG [-h] [--yesterday | start [end]]"""
parser.add_argument('start', type=argument_date, nargs='?', help='Start date (format YYYY-MM-DD)')
parser.add_argument('end', type=argument_date, nargs='?', help='End date (format YYYY-MM-DD)')
parser.add_argument('--yesterday', action='store_true', help='Only yesterday')

args = parser.parse_args()

if args.yesterday and args.start:
    raise parser.error("--yesterday option is not incompatible with start argument")

if not args.yesterday and not args.start:
    raise parser.error("--yesterday option or start argument should be filled")

if args.end and (args.start >= args.end):
    raise parser.error("end argument should be granter than start")
Aurélien
  • 259
  • 4
  • 12
  • Is there any real reason to use `--yesterday` instead of allowing `yesterday` as a special value for `begin-date`? – chepner Dec 17 '15 at 20:40
  • `start` and `end` arguments should be a datetime object not a simple string. So, I think it isn't consistent from a data point of view. – Aurélien Dec 18 '15 at 09:44
  • All arguments are simple strings until you actually turn them into `datetime`objects. It's a relatively simple matter to check for special cases like "yesterday" before doing so. – chepner Dec 18 '15 at 13:05

2 Answers2

4

--yesterday is redundant, since it is just a shortcut for setting start_date to yesterday's day. Instead, let "yesterday" be an allowable value for start_date. In fact, you can generalize datetime to allow other abbreviations, for either argument, as desired. For example:

def argument_date(str_date):
    # Not the most efficient to roundtrip like this, but
    # fits well with your existing code
    now = datetime.datetime.utcnow().date()
    if str_date == "yesterday":
        str_date = str(now - datetime.timedelta(1))
    elif str_date == "today"
        str_date = str(now)

    try:
        return datetime.strptime(str_date, "%Y-%m-%d").replace(tzinfo=pytz.utc)
    except ValueError as e:
        raise argparse.ArgumentTypeError(e)

Once you've done this, your code simply becomes:

parser = argparse.ArgumentParser(prog='PROG')
parser.add_argument('start', type=argument_date, help='Start date (YYYY-MM-DD, yesterday, today)')
parser.add_argument('end', type=argument_date, nargs='?', help='End date (YYYY-MM-DD, yesterday, today)')
chepner
  • 497,756
  • 71
  • 530
  • 681
  • A bit off-topic but upvoted because of effective use of custom type `type=argument_date`., which is the way to go here IMO. – Fuujuhi Feb 15 '22 at 12:20
3

Your best choice is to test values after parsing, and if needed, provide your own custom usage.

A mutually_exclusive_group can work with one optional positional, e.g.

group = parser.add_mutually_exclusive_group()
group.add_argument('-y','--yesterday', action='store_true')
group.add_argument('dates',nargs='?')

I was thinking it would work with nargs='*', but I get ValueError: mutually exclusive arguments must be optional error.

So one optional positional value works, but there's no way of using this test with 2 optional positional values.

parser.add_argument('--yesterday',action='store_true')
parser.add_argument('start',nargs='?')
parser.add_argument('end',nargs='?')

And then test for args.yesterday, args.start is None and args.end is None. If some combination of those is wrong, then raise parser.error('....').

As long as you can distinguish between the default values and the user given ones, testing after parsing is just as good as anything that you might force the parser to do.

It is also a good idea to think about what usage message makes sense to your users. e.g.

For example:

PROG  [--yesterday | [start [end]]]

is not something that argparse can generate automatically.

hpaulj
  • 221,503
  • 14
  • 230
  • 353
  • in this scenario, nargs='\*' will only raise ValueError if you do not provide a default value. if you use default=[] it should work fine with nargs ='\*' – Mazino Feb 10 '20 at 21:00