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

Commit 9acf81a

Browse files
authored
Import Value, input_float, input_value from core (#3)
* 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
1 parent ed190c2 commit 9acf81a

File tree

7 files changed

+443
-2
lines changed

7 files changed

+443
-2
lines changed

README.md

+11
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,21 @@ Other modules:
9797
- [blmath.geometry.segment](segment.py) provides functions for working with
9898
line segments in n-space.
9999

100+
blmath.value
101+
------------
102+
Class for wrapping and manipulating `value`/`units` pairs.
103+
100104
blmath.units
101105
------------
102106
TODO write something here
103107

108+
blmath.console
109+
------------
110+
- [blmath.console.input_float](console.py) reads and returns a float from console.
111+
- [blmath.console.input_value](console.py) combines `units` with a float input from console
112+
and returns `Value` object.
113+
114+
104115

105116
Development
106117
-----------

blmath/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.0.3'
1+
__version__ = '1.1.0'

blmath/console.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
3+
def input_float(prompt, allow_empty=False):
4+
'''
5+
Read a float from the console, showing the given prompt.
6+
7+
prompt: The prompt message.
8+
allow_empty: When `True`, allows an empty input. The default is to repeat
9+
until a valid float is entered.
10+
'''
11+
from plumbum.cli import terminal
12+
if allow_empty:
13+
return terminal.prompt(prompt, type=float, default=None)
14+
else:
15+
return terminal.prompt(prompt, type=float)
16+
17+
def input_value(label, units, allow_empty=False):
18+
'''
19+
Read a value from the console, and return an instance of `Value`. The
20+
units are specified by the caller, but displayed to the user.
21+
22+
label: A label for the value (included in the prompt)
23+
units: The units (included in the prompt)
24+
allow_empty: When `True`, allows an empty input. The default is to repeat
25+
until a valid float is entered.
26+
'''
27+
from blmath.value import Value
28+
29+
value = input_float(
30+
prompt='{} ({}): '.format(label, units),
31+
allow_empty=allow_empty
32+
)
33+
34+
if value is None:
35+
return None
36+
37+
return Value(value, units)

blmath/test_value.py

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import unittest
2+
import numpy as np
3+
from blmath.value import Value
4+
from blmath.util import json
5+
6+
class TestValueClass(unittest.TestCase):
7+
def test_value_initializes_correctly(self):
8+
with self.assertRaises(TypeError):
9+
_ = Value() # It's a failure test. pylint: disable=no-value-for-parameter
10+
with self.assertRaises(ValueError):
11+
_ = Value(13, 'mugglemeters')
12+
with self.assertRaises(ValueError):
13+
_ = Value(None, 'mugglemeters')
14+
with self.assertRaises(ValueError):
15+
_ = Value(None, 'mm')
16+
17+
x = Value(13, 'mm')
18+
self.assertIsInstance(x, Value)
19+
self.assertFalse(isinstance(x, float))
20+
self.assertTrue(hasattr(x, 'units'))
21+
self.assertEqual(x.units, 'mm')
22+
23+
def test_value_initializes_with_unitless_values(self):
24+
_ = Value(1, None)
25+
26+
def test_exception_raised_if_value_is_nonsense(self):
27+
with self.assertRaises(ValueError):
28+
Value('x', 'mm')
29+
v = Value(0, 'mm')
30+
with self.assertRaises(ValueError):
31+
v.value = 'x'
32+
33+
def test_conversions(self):
34+
x = Value(25, 'cm')
35+
self.assertAlmostEqual(x.convert('m'), 0.25)
36+
self.assertAlmostEqual(x.convert('cm'), 25)
37+
self.assertAlmostEqual(x.convert('mm'), 250)
38+
self.assertAlmostEqual(x.convert('in'), 9.8425197) # from google
39+
self.assertAlmostEqual(x.convert('ft'), 0.82021) # from google
40+
self.assertAlmostEqual(x.convert('fathoms'), 0.136701662) # from google
41+
self.assertAlmostEqual(x.convert('cubits'), 0.546806649) # from google
42+
x = Value(1, 'kg')
43+
self.assertAlmostEqual(x.convert('kg'), 1)
44+
self.assertAlmostEqual(x.convert('g'), 1000)
45+
self.assertAlmostEqual(x.convert('lbs'), 2.20462) # from google
46+
self.assertAlmostEqual(x.convert('stone'), 0.157473) # from google
47+
x = Value(90, 'deg')
48+
self.assertAlmostEqual(x.convert('rad'), np.pi/2)
49+
self.assertAlmostEqual(x.convert('deg'), 90)
50+
x = Value(30, 'min')
51+
self.assertAlmostEqual(x.convert('sec'), 30*60)
52+
self.assertAlmostEqual(x.convert('minutes'), 30)
53+
self.assertAlmostEqual(x.convert('hours'), 0.5)
54+
x = Value(2, 'days')
55+
self.assertAlmostEqual(x.convert('min'), 2*24*60)
56+
x = Value(1, 'year')
57+
self.assertAlmostEqual(x.convert('min'), 525948.48)
58+
59+
def test_value_does_not_convert_unitless_values(self):
60+
x = Value(1, None)
61+
with self.assertRaises(ValueError):
62+
x.convert('kg')
63+
64+
def test_behaves_like_tuple(self):
65+
x = Value(25, 'cm')
66+
self.assertAlmostEqual(x[0], 25)
67+
self.assertAlmostEqual(x[1], 'cm')
68+
69+
def test_easy_conversion_properties(self):
70+
x = Value(25, 'cm')
71+
self.assertAlmostEqual(x.m, 0.25)
72+
self.assertAlmostEqual(x.cm, 25)
73+
self.assertAlmostEqual(x.mm, 250)
74+
with self.assertRaises(AttributeError):
75+
_ = x.mugglemeters
76+
77+
def test_comparison(self):
78+
self.assertTrue(Value(25, 'cm') == Value(25, 'cm'))
79+
self.assertTrue(Value(25, 'cm') == Value(250, 'mm'))
80+
self.assertTrue(Value(25, 'cm') > Value(240, 'mm'))
81+
self.assertTrue(Value(25, 'cm') < Value(260, 'mm'))
82+
self.assertTrue(Value(25, 'cm') != Value(260, 'mm'))
83+
# When comparison is to a number, we assume that the units are the same as ours
84+
self.assertTrue(Value(25, 'cm') == 25)
85+
86+
def test_multiplication(self):
87+
x = Value(25, 'cm')
88+
self.assertEqual(x * 2, 50)
89+
self.assertIsInstance(x * 2, Value)
90+
self.assertEqual(2 * x, 50)
91+
self.assertIsInstance(2 * x, Value)
92+
with self.assertRaises(ValueError):
93+
# This would be cm^2; for our present purposes, multpying Value*Value is an error
94+
_ = x * x
95+
self.assertEqual([1, 2, 3] * x, [Value(25, 'cm'), Value(50, 'cm'), Value(75, 'cm')])
96+
for y in [1, 2, 3] * x:
97+
self.assertIsInstance(y, Value)
98+
99+
def test_addition_and_subtraction(self):
100+
x = Value(25, 'cm')
101+
self.assertEqual(x + 2, Value(27, 'cm'))
102+
self.assertIsInstance(x + 2, Value)
103+
self.assertEqual(2 + x, Value(27, 'cm'))
104+
self.assertIsInstance(2 + x, Value)
105+
self.assertEqual(x - 2, Value(23, 'cm'))
106+
self.assertIsInstance(x - 2, Value)
107+
# Note that although this is sort of poorly defined, we need to support this case in order to make comparisons easy
108+
self.assertEqual(2 - x, Value(-23, 'cm'))
109+
self.assertIsInstance(2 - x, Value)
110+
self.assertEqual(x + Value(25, 'cm'), Value(50, 'cm'))
111+
self.assertIsInstance(x + Value(25, 'cm'), Value)
112+
self.assertEqual(x + Value(5, 'mm'), Value(255, 'mm'))
113+
self.assertEqual([1, 2, 3] + x, [Value(26, 'cm'), Value(27, 'cm'), Value(28, 'cm')])
114+
for y in [1, 2, 3] + x:
115+
self.assertIsInstance(y, Value)
116+
117+
def test_other_numeric_methods(self):
118+
x = Value(25, 'cm')
119+
self.assertEqual(str(x), "25.000000 cm")
120+
self.assertEqual(x / 2, Value(12.5, 'cm'))
121+
self.assertIsInstance(x / 2, Value)
122+
self.assertEqual(x / Value(1, 'cm'), Value(25, None))
123+
self.assertEqual(x / Value(1, 'm'), Value(0.25, None))
124+
self.assertEqual(x // 2, Value(12, 'cm'))
125+
self.assertEqual(x // Value(1, 'cm'), Value(25, None))
126+
self.assertEqual(x // Value(1, 'm'), Value(0, None))
127+
self.assertIsInstance(x // 2, Value)
128+
with self.assertRaises(AttributeError):
129+
_ = x % 2
130+
with self.assertRaises(AttributeError):
131+
_ = x ** 2
132+
with self.assertRaises(ValueError):
133+
_ = 2 / x
134+
self.assertEqual(+x, Value(25, 'cm'))
135+
self.assertIsInstance(+x, Value)
136+
self.assertEqual(-x, Value(-25, 'cm'))
137+
self.assertIsInstance(-x, Value)
138+
self.assertEqual(abs(Value(-25, 'cm')), Value(25, 'cm'))
139+
self.assertIsInstance(abs(Value(-25, 'cm')), Value)
140+
141+
def test_cast(self):
142+
x = Value(25, 'cm')
143+
self.assertEqual(float(x), x.value)
144+
self.assertEqual(int(x), x.value)
145+
self.assertEqual(int(Value(25.5, 'cm')), 25)
146+
147+
def test_make_numpy_array_out_of_values(self):
148+
x = np.array([Value(i, 'cm') for i in range(10)])
149+
res = np.sum(x)
150+
self.assertIsInstance(res, Value)
151+
152+
class TestValueSerialization(unittest.TestCase):
153+
154+
def test_basic_serialization(self):
155+
x = Value(25, 'cm')
156+
x_json = json.dumps(x)
157+
158+
self.assertEquals(x_json, '{"__value__": {"units": "cm", "value": 25.0}}')
159+
x_obj = json.loads(x_json)
160+
self.assertEquals(x, x_obj)
161+
162+
def test_complex_serialization(self):
163+
x = {str(i): Value(i, 'cm') for i in range(10)}
164+
x_json = json.dumps(x)
165+
x_obj = json.loads(x_json)
166+
self.assertEquals(x, x_obj)
167+
168+
class TestValueDeserialization(unittest.TestCase):
169+
170+
def test_loads(self):
171+
x_str = json.dumps({'__value__': {'value': 25.0, 'units': 'cm'}})
172+
x = json.loads(x_str)
173+
self.assertEquals(x.value, 25.0)
174+
self.assertEquals(x.units, 'cm')
175+
176+
def test_from_json(self):
177+
x = Value.from_json({'__value__': {'value': 25.0, 'units': 'cm'}})
178+
self.assertEquals(x.value, 25.0)
179+
self.assertEquals(x.units, 'cm')
180+
181+
if __name__ == '__main__':
182+
unittest.main()

blmath/util/json.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from baiji.serialization import json
2+
from baiji.serialization.json import JSONDecoder
3+
4+
class BlmathJSONDecoder(JSONDecoder):
5+
def __init__(self):
6+
super(BlmathJSONDecoder, self).__init__()
7+
self.register(self.decode_value)
8+
9+
def decode_value(self, dct):
10+
from blmath.value import Value
11+
if "__value__" in dct.keys():
12+
return Value.from_json(dct)
13+
14+
def dump(obj, f, *args, **kwargs):
15+
return json.dump(obj, f, *args, **kwargs)
16+
17+
def load(f, *args, **kwargs):
18+
kwargs.update(decoder=BlmathJSONDecoder())
19+
return json.load(f, *args, **kwargs)
20+
21+
def dumps(*args, **kwargs):
22+
return json.dumps(*args, **kwargs)
23+
24+
def loads(*args, **kwargs):
25+
kwargs.update(decoder=BlmathJSONDecoder())
26+
return json.loads(*args, **kwargs)

0 commit comments

Comments
 (0)