From 6e62332349db6e93decf47666c4cd6fe20df6b02 Mon Sep 17 00:00:00 2001 From: Santiago Basulto Date: Tue, 8 Sep 2015 23:11:26 -0300 Subject: [PATCH 1/2] Included click.DateTime type (with tests) --- click/__init__.py | 7 ++++--- click/types.py | 34 ++++++++++++++++++++++++++++++++++ tests/test_basic.py | 36 ++++++++++++++++++++++++++++++++++++ tests/test_imports.py | 2 +- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/click/__init__.py b/click/__init__.py index 6de314b31..fc40c7eaa 100644 --- 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 index 0d4b122fa..582014683 100644 --- 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,39 @@ def __repr__(self): return 'Choice(%r)' % list(self.choices) +class DateTime(ParamType): + name = 'datetime' + + def __init__(self, formats=None): + self.formats = formats or [ + '%Y-%m-%d', + '%Y-%m-%dT%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' diff --git a/tests/test_basic.py b/tests/test_basic.py index 8ba251fa1..414fd1c6d 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -332,6 +332,42 @@ 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)' in result.output + + result = runner.invoke(cli, ['--help']) + assert '--start_date [%Y-%m-%d|%Y-%m-%dT%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 index f400fa854..8e9a97df6 100644 --- 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: From 5fe0b7e7795f9bb04ae926d00868cfeb1fa33187 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Mon, 27 Aug 2018 22:16:12 +0000 Subject: [PATCH 2/2] Add documentation for DateTime param type Add a docstring which defines the behaviors of the class, including default formats supported, usage of ``datetime.strptime`` and "first successful parse wins" behavior. Also add '%Y-%m-%d %H:%M:%S' to the default formats, as it is at least as commonly seen as '%Y-%m-%dT%H:%M:%S'. Minor test fix to handle the new format. Add ``click.DateTime`` to changelog. --- CHANGES.rst | 2 ++ click/types.py | 22 +++++++++++++++++++++- docs/parameters.rst | 3 +++ tests/test_basic.py | 8 +++++--- 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4601053e6..d56bbff8c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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`_) @@ -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 diff --git a/click/types.py b/click/types.py index 582014683..1f88032f5 100644 --- a/click/types.py +++ b/click/types.py @@ -184,12 +184,32 @@ def __repr__(self): 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-%dT%H:%M:%S', + '%Y-%m-%d %H:%M:%S' ] def get_metavar(self, param): diff --git a/docs/parameters.rst b/docs/parameters.rst index 112e8fac4..9ca320842 100644 --- a/docs/parameters.rst +++ b/docs/parameters.rst @@ -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. diff --git a/tests/test_basic.py b/tests/test_basic.py index 414fd1c6d..8de6301a8 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -349,11 +349,13 @@ def cli(start_date): 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)' in result.output + 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]' in result.output + 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):