Skip to content

Commit

Permalink
Merge pull request #1099 from sirosen/type-datetime
Browse files Browse the repository at this point in the history
Rebase of #423 to add docs, hopefully ready for merge
  • Loading branch information
davidism authored Sep 13, 2018
2 parents 3e17a3d + 5fe0b7e commit 2c622ee
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Unreleased
- Usage errors now hint at the ``--help`` option. (`#393`_, `#557`_)
- Implement streaming pager. (`#409`_, `#889`_)
- Extract bar formatting to its own method. (`#414`_)
- Add ``DateTime`` type for converting input in given date time formats. (`#423`_)
- ``secho``'s first argument can now be ``None``, like in ``echo``. (`#424`_)
- Fixes a ``ZeroDivisionError`` in ``ProgressBar.make_step``, when the arg passed to the first call of ``ProgressBar.update`` is 0. (`#447`_, `#1012`_)
- Show progressbar only if total execution time is visible. (`#487`_)
Expand Down Expand Up @@ -93,6 +94,7 @@ Unreleased
.. _#393: https://github.com/pallets/click/issues/393
.. _#409: https://github.com/pallets/click/issues/409
.. _#414: https://github.com/pallets/click/pull/414
.. _#423: https://github.com/pallets/click/pull/423
.. _#424: https://github.com/pallets/click/pull/424
.. _#447: https://github.com/pallets/click/issues/447
.. _#487: https://github.com/pallets/click/pull/487
Expand Down
7 changes: 4 additions & 3 deletions click/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand Down Expand Up @@ -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',
Expand Down
54 changes: 54 additions & 0 deletions click/types.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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'

Expand Down
3 changes: 3 additions & 0 deletions docs/parameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ different behavior and some are supported out of the box:
.. autoclass:: FloatRange
:noindex:

.. autoclass:: DateTime
:noindex:

Custom parameter types can be implemented by subclassing
:class:`click.ParamType`. For simple cases, passing a Python function that
fails with a `ValueError` is also supported, though discouraged.
Expand Down
38 changes: 38 additions & 0 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion tests/test_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 2c622ee

Please sign in to comment.