Repository: pallets/click
Pull request #538 (2016-03-17 01:41:24)

Add Float Range in Types
When using the `IntRange` type I assumed that a similar `FloatRange` would exist.


Patch:
diff --git a/CHANGES b/CHANGES
index 8b865b9..93a6383 100644
--- a/CHANGES
+++ b/CHANGES
@@ -10,6 +10,7 @@ Version 7.0
 
 - The exception objects now store unicode properly.
 - Added the ability to hide commands and options from help.
+- Added Float Range in Types.
 
 Version 6.3
 -----------
diff --git a/click/__init__.py b/click/__init__.py
index 774d988..58df503 100644
--- a/click/__init__.py
+++ b/click/__init__.py
@@ -28,7 +28,7 @@ from .decorators import pass_context, pass_obj, make_pass_decorator, \
 
 # Types
 from .types import ParamType, File, Path, Choice, IntRange, Tuple, \
-     STRING, INT, FLOAT, BOOL, UUID, UNPROCESSED
+     STRING, INT, FLOAT, BOOL, UUID, UNPROCESSED, FloatRange
 
 # Utilities
 from .utils import echo, get_binary_stream, get_text_stream, open_file, \
@@ -66,7 +66,7 @@ __all__ = [
 
     # Types
     'ParamType', 'File', 'Path', 'Choice', 'IntRange', 'Tuple', 'STRING',
-    'INT', 'FLOAT', 'BOOL', 'UUID', 'UNPROCESSED',
+    '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 7a24747..418b45a 100644
--- a/click/types.py
+++ b/click/types.py
@@ -214,6 +214,59 @@ class IntRange(IntParamType):
         return 'IntRange(%r, %r)' % (self.min, self.max)
 
 
+class FloatParamType(ParamType):
+    name = 'float'
+
+    def convert(self, value, param, ctx):
+        try:
+            return float(value)
+        except (UnicodeError, ValueError):
+            self.fail('%s is not a valid floating point value' %
+                      value, param, ctx)
+
+    def __repr__(self):
+        return 'FLOAT'
+
+
+class FloatRange(FloatParamType):
+    """A parameter that works similar to :data:`click.FLOAT` but restricts
+    the value to fit into a range.  The default behavior is to fail if the
+    value falls outside the range, but it can also be silently clamped
+    between the two edges.
+
+    See :ref:`ranges` for an example.
+    """
+    name = 'float range'
+
+    def __init__(self, min=None, max=None, clamp=False):
+        self.min = min
+        self.max = max
+        self.clamp = clamp
+
+    def convert(self, value, param, ctx):
+        rv = FloatParamType.convert(self, value, param, ctx)
+        if self.clamp:
+            if self.min is not None and rv < self.min:
+                return self.min
+            if self.max is not None and rv > self.max:
+                return self.max
+        if self.min is not None and rv < self.min or \
+           self.max is not None and rv > self.max:
+            if self.min is None:
+                self.fail('%s is bigger than the maximum valid value '
+                          '%s.' % (rv, self.max), param, ctx)
+            elif self.max is None:
+                self.fail('%s is smaller than the minimum valid value '
+                          '%s.' % (rv, self.min), param, ctx)
+            else:
+                self.fail('%s is not in the valid range of %s to %s.'
+                          % (rv, self.min, self.max), param, ctx)
+        return rv
+
+    def __repr__(self):
+        return 'FloatRange(%r, %r)' % (self.min, self.max)
+
+
 class BoolParamType(ParamType):
     name = 'boolean'
 
@@ -231,20 +284,6 @@ class BoolParamType(ParamType):
         return 'BOOL'
 
 
-class FloatParamType(ParamType):
-    name = 'float'
-
-    def convert(self, value, param, ctx):
-        try:
-            return float(value)
-        except (UnicodeError, ValueError):
-            self.fail('%s is not a valid floating point value' %
-                      value, param, ctx)
-
-    def __repr__(self):
-        return 'FLOAT'
-
-
 class UUIDParameterType(ParamType):
     name = 'uuid'
 
diff --git a/docs/parameters.rst b/docs/parameters.rst
index 920b9be..e545343 100644
--- a/docs/parameters.rst
+++ b/docs/parameters.rst
@@ -67,6 +67,9 @@ different behavior and some are supported out of the box:
 .. autoclass:: IntRange
    :noindex:
 
+.. autoclass:: FloatRange
+  :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.


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 421ba41..ec4df7d 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -343,6 +343,39 @@ def test_int_range_option(runner):
     assert result.output == '0\n'
 
 
+def test_float_range_option(runner):
+    @click.command()
+    @click.option('--x', type=click.FloatRange(0, 5))
+    def cli(x):
+        click.echo(x)
+
+    result = runner.invoke(cli, ['--x=5.0'])
+    assert not result.exception
+    assert result.output == '5.0\n'
+
+    result = runner.invoke(cli, ['--x=6.0'])
+    assert result.exit_code == 2
+    assert 'Invalid value for "--x": 6.0 is not in the valid range of 0 to 5.\n' \
+        in result.output
+
+    @click.command()
+    @click.option('--x', type=click.FloatRange(0, 5, clamp=True))
+    def clamp(x):
+        click.echo(x)
+
+    result = runner.invoke(clamp, ['--x=5.0'])
+    assert not result.exception
+    assert result.output == '5.0\n'
+
+    result = runner.invoke(clamp, ['--x=6.0'])
+    assert not result.exception
+    assert result.output == '5\n'
+
+    result = runner.invoke(clamp, ['--x=-1.0'])
+    assert not result.exception
+    assert result.output == '0\n'
+
+
 def test_required_option(runner):
     @click.command()
     @click.option('--foo', required=True)