Skip to content
This repository has been archived by the owner on Feb 3, 2023. It is now read-only.

Commit

Permalink
Import Value, input_float, input_value from core (#3)
Browse files Browse the repository at this point in the history
* Add Value from core.bodylabs.measurements.models

* Add input_float, input_value from core

* Update cached_property import

* Update value import to blmath

* Add cached_property to requirements

* Update readme

* Bump version to 1.1.0

* Add plumbum req

* Add test_value.py

* Set up BlmathJSONDecoder

* Bump baiji-serialization > 2

* Delete line for lint

* Delete comment

* Simplfy BlmathJSONDecoder, remove test for error we no longer raise
  • Loading branch information
jbwhite committed May 22, 2017
1 parent ed190c2 commit 9acf81a
Show file tree
Hide file tree
Showing 7 changed files with 443 additions and 2 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,21 @@ Other modules:
- [blmath.geometry.segment](segment.py) provides functions for working with
line segments in n-space.

blmath.value
------------
Class for wrapping and manipulating `value`/`units` pairs.

blmath.units
------------
TODO write something here

blmath.console
------------
- [blmath.console.input_float](console.py) reads and returns a float from console.
- [blmath.console.input_value](console.py) combines `units` with a float input from console
and returns `Value` object.



Development
-----------
Expand Down
2 changes: 1 addition & 1 deletion blmath/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '1.0.3'
__version__ = '1.1.0'
37 changes: 37 additions & 0 deletions blmath/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@


def input_float(prompt, allow_empty=False):
'''
Read a float from the console, showing the given prompt.
prompt: The prompt message.
allow_empty: When `True`, allows an empty input. The default is to repeat
until a valid float is entered.
'''
from plumbum.cli import terminal
if allow_empty:
return terminal.prompt(prompt, type=float, default=None)
else:
return terminal.prompt(prompt, type=float)

def input_value(label, units, allow_empty=False):
'''
Read a value from the console, and return an instance of `Value`. The
units are specified by the caller, but displayed to the user.
label: A label for the value (included in the prompt)
units: The units (included in the prompt)
allow_empty: When `True`, allows an empty input. The default is to repeat
until a valid float is entered.
'''
from blmath.value import Value

value = input_float(
prompt='{} ({}): '.format(label, units),
allow_empty=allow_empty
)

if value is None:
return None

return Value(value, units)
182 changes: 182 additions & 0 deletions blmath/test_value.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import unittest
import numpy as np
from blmath.value import Value
from blmath.util import json

class TestValueClass(unittest.TestCase):
def test_value_initializes_correctly(self):
with self.assertRaises(TypeError):
_ = Value() # It's a failure test. pylint: disable=no-value-for-parameter
with self.assertRaises(ValueError):
_ = Value(13, 'mugglemeters')
with self.assertRaises(ValueError):
_ = Value(None, 'mugglemeters')
with self.assertRaises(ValueError):
_ = Value(None, 'mm')

x = Value(13, 'mm')
self.assertIsInstance(x, Value)
self.assertFalse(isinstance(x, float))
self.assertTrue(hasattr(x, 'units'))
self.assertEqual(x.units, 'mm')

def test_value_initializes_with_unitless_values(self):
_ = Value(1, None)

def test_exception_raised_if_value_is_nonsense(self):
with self.assertRaises(ValueError):
Value('x', 'mm')
v = Value(0, 'mm')
with self.assertRaises(ValueError):
v.value = 'x'

def test_conversions(self):
x = Value(25, 'cm')
self.assertAlmostEqual(x.convert('m'), 0.25)
self.assertAlmostEqual(x.convert('cm'), 25)
self.assertAlmostEqual(x.convert('mm'), 250)
self.assertAlmostEqual(x.convert('in'), 9.8425197) # from google
self.assertAlmostEqual(x.convert('ft'), 0.82021) # from google
self.assertAlmostEqual(x.convert('fathoms'), 0.136701662) # from google
self.assertAlmostEqual(x.convert('cubits'), 0.546806649) # from google
x = Value(1, 'kg')
self.assertAlmostEqual(x.convert('kg'), 1)
self.assertAlmostEqual(x.convert('g'), 1000)
self.assertAlmostEqual(x.convert('lbs'), 2.20462) # from google
self.assertAlmostEqual(x.convert('stone'), 0.157473) # from google
x = Value(90, 'deg')
self.assertAlmostEqual(x.convert('rad'), np.pi/2)
self.assertAlmostEqual(x.convert('deg'), 90)
x = Value(30, 'min')
self.assertAlmostEqual(x.convert('sec'), 30*60)
self.assertAlmostEqual(x.convert('minutes'), 30)
self.assertAlmostEqual(x.convert('hours'), 0.5)
x = Value(2, 'days')
self.assertAlmostEqual(x.convert('min'), 2*24*60)
x = Value(1, 'year')
self.assertAlmostEqual(x.convert('min'), 525948.48)

def test_value_does_not_convert_unitless_values(self):
x = Value(1, None)
with self.assertRaises(ValueError):
x.convert('kg')

def test_behaves_like_tuple(self):
x = Value(25, 'cm')
self.assertAlmostEqual(x[0], 25)
self.assertAlmostEqual(x[1], 'cm')

def test_easy_conversion_properties(self):
x = Value(25, 'cm')
self.assertAlmostEqual(x.m, 0.25)
self.assertAlmostEqual(x.cm, 25)
self.assertAlmostEqual(x.mm, 250)
with self.assertRaises(AttributeError):
_ = x.mugglemeters

def test_comparison(self):
self.assertTrue(Value(25, 'cm') == Value(25, 'cm'))
self.assertTrue(Value(25, 'cm') == Value(250, 'mm'))
self.assertTrue(Value(25, 'cm') > Value(240, 'mm'))
self.assertTrue(Value(25, 'cm') < Value(260, 'mm'))
self.assertTrue(Value(25, 'cm') != Value(260, 'mm'))
# When comparison is to a number, we assume that the units are the same as ours
self.assertTrue(Value(25, 'cm') == 25)

def test_multiplication(self):
x = Value(25, 'cm')
self.assertEqual(x * 2, 50)
self.assertIsInstance(x * 2, Value)
self.assertEqual(2 * x, 50)
self.assertIsInstance(2 * x, Value)
with self.assertRaises(ValueError):
# This would be cm^2; for our present purposes, multpying Value*Value is an error
_ = x * x
self.assertEqual([1, 2, 3] * x, [Value(25, 'cm'), Value(50, 'cm'), Value(75, 'cm')])
for y in [1, 2, 3] * x:
self.assertIsInstance(y, Value)

def test_addition_and_subtraction(self):
x = Value(25, 'cm')
self.assertEqual(x + 2, Value(27, 'cm'))
self.assertIsInstance(x + 2, Value)
self.assertEqual(2 + x, Value(27, 'cm'))
self.assertIsInstance(2 + x, Value)
self.assertEqual(x - 2, Value(23, 'cm'))
self.assertIsInstance(x - 2, Value)
# Note that although this is sort of poorly defined, we need to support this case in order to make comparisons easy
self.assertEqual(2 - x, Value(-23, 'cm'))
self.assertIsInstance(2 - x, Value)
self.assertEqual(x + Value(25, 'cm'), Value(50, 'cm'))
self.assertIsInstance(x + Value(25, 'cm'), Value)
self.assertEqual(x + Value(5, 'mm'), Value(255, 'mm'))
self.assertEqual([1, 2, 3] + x, [Value(26, 'cm'), Value(27, 'cm'), Value(28, 'cm')])
for y in [1, 2, 3] + x:
self.assertIsInstance(y, Value)

def test_other_numeric_methods(self):
x = Value(25, 'cm')
self.assertEqual(str(x), "25.000000 cm")
self.assertEqual(x / 2, Value(12.5, 'cm'))
self.assertIsInstance(x / 2, Value)
self.assertEqual(x / Value(1, 'cm'), Value(25, None))
self.assertEqual(x / Value(1, 'm'), Value(0.25, None))
self.assertEqual(x // 2, Value(12, 'cm'))
self.assertEqual(x // Value(1, 'cm'), Value(25, None))
self.assertEqual(x // Value(1, 'm'), Value(0, None))
self.assertIsInstance(x // 2, Value)
with self.assertRaises(AttributeError):
_ = x % 2
with self.assertRaises(AttributeError):
_ = x ** 2
with self.assertRaises(ValueError):
_ = 2 / x
self.assertEqual(+x, Value(25, 'cm'))
self.assertIsInstance(+x, Value)
self.assertEqual(-x, Value(-25, 'cm'))
self.assertIsInstance(-x, Value)
self.assertEqual(abs(Value(-25, 'cm')), Value(25, 'cm'))
self.assertIsInstance(abs(Value(-25, 'cm')), Value)

def test_cast(self):
x = Value(25, 'cm')
self.assertEqual(float(x), x.value)
self.assertEqual(int(x), x.value)
self.assertEqual(int(Value(25.5, 'cm')), 25)

def test_make_numpy_array_out_of_values(self):
x = np.array([Value(i, 'cm') for i in range(10)])
res = np.sum(x)
self.assertIsInstance(res, Value)

class TestValueSerialization(unittest.TestCase):

def test_basic_serialization(self):
x = Value(25, 'cm')
x_json = json.dumps(x)

self.assertEquals(x_json, '{"__value__": {"units": "cm", "value": 25.0}}')
x_obj = json.loads(x_json)
self.assertEquals(x, x_obj)

def test_complex_serialization(self):
x = {str(i): Value(i, 'cm') for i in range(10)}
x_json = json.dumps(x)
x_obj = json.loads(x_json)
self.assertEquals(x, x_obj)

class TestValueDeserialization(unittest.TestCase):

def test_loads(self):
x_str = json.dumps({'__value__': {'value': 25.0, 'units': 'cm'}})
x = json.loads(x_str)
self.assertEquals(x.value, 25.0)
self.assertEquals(x.units, 'cm')

def test_from_json(self):
x = Value.from_json({'__value__': {'value': 25.0, 'units': 'cm'}})
self.assertEquals(x.value, 25.0)
self.assertEquals(x.units, 'cm')

if __name__ == '__main__':
unittest.main()
26 changes: 26 additions & 0 deletions blmath/util/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from baiji.serialization import json
from baiji.serialization.json import JSONDecoder

class BlmathJSONDecoder(JSONDecoder):
def __init__(self):
super(BlmathJSONDecoder, self).__init__()
self.register(self.decode_value)

def decode_value(self, dct):
from blmath.value import Value
if "__value__" in dct.keys():
return Value.from_json(dct)

def dump(obj, f, *args, **kwargs):
return json.dump(obj, f, *args, **kwargs)

def load(f, *args, **kwargs):
kwargs.update(decoder=BlmathJSONDecoder())
return json.load(f, *args, **kwargs)

def dumps(*args, **kwargs):
return json.dumps(*args, **kwargs)

def loads(*args, **kwargs):
kwargs.update(decoder=BlmathJSONDecoder())
return json.loads(*args, **kwargs)

0 comments on commit 9acf81a

Please sign in to comment.