Repository: jupyterhub/jupyterhub

Issue:
Fractional memory / CPU limits / guarantees fail
**How to reproduce the issue**

Set memory limit (or guarantee, or cpu limit / guarantee) to a non-integral spec:

```python
c.Spawner.mem_limit = "1.5G"
```

**What you expected to happen**

(In supported spawners) memory limit is set to 1.5 gigabytes of RAM

**What actually happens**

JupyterHub refuses to start, with:

```
[E 2017-04-18 05:39:02.270 JupyterHub app:1527]
    Traceback (most recent call last):
      File "/usr/local/lib/python3.4/dist-packages/jupyterhub/app.py", line 1524, in launch_instance_async
        yield self.initialize(argv)
      File "/usr/local/lib/python3.4/dist-packages/jupyterhub/app.py", line 1315, in initialize
        yield self.init_spawners()
      File "/usr/local/lib/python3.4/dist-packages/jupyterhub/app.py", line 1084, in init_spawners
        self.users[orm_user.id] = user = User(orm_user, self.tornado_settings)
      File "/usr/local/lib/python3.4/dist-packages/jupyterhub/user.py", line 128, in __init__
        config=self.settings.get('config'),
      File "/usr/local/lib/python3.4/dist-packages/kubespawner/spawner.py", line 29, in __init__
        super().__init__(*args, **kwargs)
      File "/usr/local/lib/python3.4/dist-packages/jupyterhub/spawner.py", line 345, in __init__
        super(Spawner, self).__init__(**kwargs)
      File "/usr/local/lib/python3.4/dist-packages/traitlets/config/configurable.py", line 84, in __init__
        self.config = config
      File "/usr/local/lib/python3.4/dist-packages/traitlets/traitlets.py", line 585, in __set__
        self.set(obj, value)
      File "/usr/local/lib/python3.4/dist-packages/traitlets/traitlets.py", line 574, in set
        obj._notify_trait(self.name, old_value, new_value)
      File "/usr/local/lib/python3.4/dist-packages/traitlets/traitlets.py", line 1139, in _notify_trait
        type='change',
      File "/usr/local/lib/python3.4/dist-packages/traitlets/traitlets.py", line 1176, in notify_change
        c(change)
      File "/usr/local/lib/python3.4/dist-packages/traitlets/traitlets.py", line 819, in compatible_observer
        return func(self, change)
      File "/usr/local/lib/python3.4/dist-packages/traitlets/config/configurable.py", line 186, in _config_changed
        self._load_config(change.new, traits=traits, section_names=section_names)
      File "/usr/local/lib/python3.4/dist-packages/traitlets/config/configurable.py", line 153, in _load_config
        setattr(self, name, deepcopy(config_value))
      File "/usr/local/lib/python3.4/dist-packages/traitlets/traitlets.py", line 585, in __set__
        self.set(obj, value)
      File "/usr/local/lib/python3.4/dist-packages/traitlets/traitlets.py", line 559, in set
        new_value = self._validate(obj, value)
      File "/usr/local/lib/python3.4/dist-packages/traitlets/traitlets.py", line 591, in _validate
        value = self.validate(obj, value)
      File "/usr/local/lib/python3.4/dist-packages/jupyterhub/traitlets.py", line 71, in validate
        return int(num) * ByteSpecification.UNIT_SUFFIXES[suffix]
    ValueError: invalid literal for int() with base 10: '1.5'
```

**Share what version of JupyterHub you are using**

0.72.


Discussion:
@yuvipanda @minrk Has recent work closed this?
I think this bug still exists. Should be an easy enough fix to use `float` instead of `int` here, and then cast to int after multiplying.
I'm going to try to fix this for 0.8.

Resolving patch:
diff --git a/jupyterhub/traitlets.py b/jupyterhub/traitlets.py
--- a/jupyterhub/traitlets.py
+++ b/jupyterhub/traitlets.py
@@ -48,7 +48,7 @@ class ByteSpecification(Integer):
         'K': 1024,
         'M': 1024 * 1024,
         'G': 1024 * 1024 * 1024,
-        'T': 1024 * 1024 * 1024 * 1024
+        'T': 1024 * 1024 * 1024 * 1024,
     }
 
     # Default to allowing None as a value
@@ -62,11 +62,15 @@ def validate(self, obj, value):
         If it has one of the suffixes, it is converted into the appropriate
         pure byte value.
         """
-        if isinstance(value, int):
-            return value
-        num = value[:-1]
+        if isinstance(value, (int, float)):
+            return int(value)
+
+        try:
+            num = float(value[:-1])
+        except ValueError:
+            raise TraitError('{val} is not a valid memory specification. Must be an int or a string with suffix K, M, G, T'.format(val=value))
         suffix = value[-1]
-        if not num.isdigit() and suffix not in ByteSpecification.UNIT_SUFFIXES:
+        if suffix not in self.UNIT_SUFFIXES:
             raise TraitError('{val} is not a valid memory specification. Must be an int or a string with suffix K, M, G, T'.format(val=value))
         else:
-            return int(num) * ByteSpecification.UNIT_SUFFIXES[suffix]
+            return int(float(num) * self.UNIT_SUFFIXES[suffix])


Test patch:
diff --git a/jupyterhub/tests/test_traitlets.py b/jupyterhub/tests/test_traitlets.py
--- a/jupyterhub/tests/test_traitlets.py
+++ b/jupyterhub/tests/test_traitlets.py
@@ -34,18 +34,27 @@ class C(HasTraits):
     c = C()
 
     c.mem = 1024
+    assert isinstance(c.mem, int)
     assert c.mem == 1024
 
     c.mem = '1024K'
+    assert isinstance(c.mem, int)
     assert c.mem == 1024 * 1024
 
     c.mem = '1024M'
+    assert isinstance(c.mem, int)
     assert c.mem == 1024 * 1024 * 1024
 
+    c.mem = '1.5M'
+    assert isinstance(c.mem, int)
+    assert c.mem == 1.5 * 1024 * 1024
+
     c.mem = '1024G'
+    assert isinstance(c.mem, int)
     assert c.mem == 1024 * 1024 * 1024 * 1024
 
     c.mem = '1024T'
+    assert isinstance(c.mem, int)
     assert c.mem == 1024 * 1024 * 1024 * 1024 * 1024
 
     with pytest.raises(TraitError):