Repository: pallets/click

Issue:
Included click.DateTime type (with tests)
Anyone interested in something like this?

``` python
@click.command()
@click.option('--start_date', type=click.DateTime())
def cli(start_date):
    pass
```

You can always specify the formats you expect.

``` python
@click.command()
@click.option('--start_date', type=click.DateTime(formats=['%A %B %d, %Y']))
def cli(start_date):
    pass
```



Discussion:

Resolving patch:
diff --git a/click/__init__.py b/click/__init__.py
--- a/click/__init__.py
+++ b/click/__init__.py
@@ -28,7 +28,7 @@
 
 # Types
 from .types import ParamType, File, Path, Choice, IntRange, Tuple, \
-     STRING, INT, FLOAT, BOOL, UUID, UNPROCESSED, FloatRange
+     DateTime, STRING, INT, FLOAT, BOOL, UUID, UNPROCESSED, FloatRange
 
 # Utilities
 from .utils import echo, get_binary_stream, get_text_stream, open_file, \
@@ -65,8 +65,9 @@
     'version_option', 'help_option',
 
     # Types
-    'ParamType', 'File', 'Path', 'Choice', 'IntRange', 'Tuple', 'STRING',
-    'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED', 'FloatRange',
+    'ParamType', 'File', 'Path', 'Choice', 'IntRange', 'Tuple',
+    'DateTime', 'STRING', 'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED',
+    'FloatRange',
 
     # Utilities
     'echo', 'get_binary_stream', 'get_text_stream', 'open_file',
diff --git a/click/types.py b/click/types.py
--- a/click/types.py
+++ b/click/types.py
@@ -1,5 +1,6 @@
 import os
 import stat
+from datetime import datetime
 
 from ._compat import open_stream, text_type, filename_to_ui, \
     get_filesystem_encoding, get_streerror, _get_argv_encoding, PY2
@@ -182,6 +183,59 @@ def __repr__(self):
         return 'Choice(%r)' % list(self.choices)
 
 
+class DateTime(ParamType):
+    """The DateTime type converts date strings into `datetime` objects.
+
+    The format strings which are checked are configurable, but default to some
+    common (non-timezone aware) ISO 8601 formats.
+
+    When specifying *DateTime* formats, you should only pass a list or a tuple.
+    Other iterables, like generators, may lead to surprising results.
+
+    The format strings are processed using ``datetime.strptime``, and this
+    consequently defines the format strings which are allowed.
+
+    Parsing is tried using each format, in order, and the first format which
+    parses successfully is used.
+
+    :param formats: A list or tuple of date format strings, in the order in
+                    which they should be tried. Defaults to
+                    ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``,
+                    ``'%Y-%m-%d %H:%M:%S'``.
+    """
+    name = 'datetime'
+
+    def __init__(self, formats=None):
+        self.formats = formats or [
+            '%Y-%m-%d',
+            '%Y-%m-%dT%H:%M:%S',
+            '%Y-%m-%d %H:%M:%S'
+        ]
+
+    def get_metavar(self, param):
+        return '[{}]'.format('|'.join(self.formats))
+
+    def _try_to_convert_date(self, value, format):
+        try:
+            return datetime.strptime(value, format)
+        except ValueError:
+            return None
+
+    def convert(self, value, param, ctx):
+        # Exact match
+        for format in self.formats:
+            dtime = self._try_to_convert_date(value, format)
+            if dtime:
+                return dtime
+
+        self.fail(
+            'invalid datetime format: {}. (choose from {})'.format(
+                value, ', '.join(self.formats)))
+
+    def __repr__(self):
+        return 'DateTime'
+
+
 class IntParamType(ParamType):
     name = 'integer'
 


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -332,6 +332,44 @@ def cli(method):
     assert '--method [foo|bar|baz]' in result.output
 
 
+def test_datetime_option_default(runner):
+
+    @click.command()
+    @click.option('--start_date', type=click.DateTime())
+    def cli(start_date):
+        click.echo(start_date.strftime('%Y-%m-%dT%H:%M:%S'))
+
+    result = runner.invoke(cli, ['--start_date=2015-09-29'])
+    assert not result.exception
+    assert result.output == '2015-09-29T00:00:00\n'
+
+    result = runner.invoke(cli, ['--start_date=2015-09-29T09:11:22'])
+    assert not result.exception
+    assert result.output == '2015-09-29T09:11:22\n'
+
+    result = runner.invoke(cli, ['--start_date=2015-09'])
+    assert result.exit_code == 2
+    assert ('Invalid value for "--start_date": '
+            'invalid datetime format: 2015-09. '
+            '(choose from %Y-%m-%d, %Y-%m-%dT%H:%M:%S, %Y-%m-%d %H:%M:%S)'
+            ) in result.output
+
+    result = runner.invoke(cli, ['--help'])
+    assert '--start_date [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S]' in result.output
+
+
+def test_datetime_option_custom(runner):
+    @click.command()
+    @click.option('--start_date',
+                  type=click.DateTime(formats=['%A %B %d, %Y']))
+    def cli(start_date):
+        click.echo(start_date.strftime('%Y-%m-%dT%H:%M:%S'))
+
+    result = runner.invoke(cli, ['--start_date=Wednesday June 05, 2010'])
+    assert not result.exception
+    assert result.output == '2010-06-05T00:00:00\n'
+
+
 def test_int_range_option(runner):
     @click.command()
     @click.option('--x', type=click.IntRange(0, 5))
diff --git a/tests/test_imports.py b/tests/test_imports.py
--- a/tests/test_imports.py
+++ b/tests/test_imports.py
@@ -32,7 +32,7 @@ def tracking_import(module, locals=None, globals=None, fromlist=None,
 ALLOWED_IMPORTS = set([
     'weakref', 'os', 'struct', 'collections', 'sys', 'contextlib',
     'functools', 'stat', 're', 'codecs', 'inspect', 'itertools', 'io',
-    'threading', 'colorama', 'errno', 'fcntl'
+    'threading', 'colorama', 'errno', 'fcntl', 'datetime'
 ])
 
 if WIN: