From 190f82b7b0129057edf6146e5a032be6646b2f4e Mon Sep 17 00:00:00 2001 From: terrycojones Date: Wed, 2 Dec 2020 19:58:55 +0100 Subject: [PATCH 1/3] Added 'store' command suggested by @nfraprado. --- CHANGELOG.md | 5 ++ README.md | 5 +- rpnpy/__init__.py | 2 +- rpnpy/calculator.py | 31 +++++++++--- rpnpy/functions.py | 26 ++++++++++ setup.py | 2 +- test/__init__.py | 0 test/test_calculator.py | 107 +++++++++++++++++++++++++++++++++++----- 8 files changed, 157 insertions(+), 21 deletions(-) create mode 100644 test/__init__.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 76e9b5d..b3d2741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 1.0.31 December 2, 2020 + +Added special `store` command to save stack values into a variable, +following suggestion from @nfraprado. + # 1.0.30 October 26, 2020 Added support for engineering notation for numbers by using the diff --git a/README.md b/README.md index 7d3e450..abb3283 100644 --- a/README.md +++ b/README.md @@ -525,8 +525,11 @@ There are two kinds of commands: special and normal. * `quit` (or `q`): Quit * `reverse`: Reverse the `count` (default 2) top stack items. * `reduce`: Repeatedly apply a function to stack items (see - [functools.reduce](https://docs.python.org/3.7/library/functools.html#functools.reduce). + [functools.reduce](https://docs.python.org/3.7/library/functools.html#functools.reduce)). * `stack` (or `s` or `f`): Print the whole stack. +* `store`: Store the value on the top of the stack into a variable (whose name has + previously been pushed onto the stack). If given a numeric argument, that number + of items from the stack will be stored into the variable as a list. * `swap`: Swap the top two stack elements. * `undo`: Undo the last stack-changing operation and variable settings. * `variables`: Show all known variables and their values. diff --git a/rpnpy/__init__.py b/rpnpy/__init__.py index 710e60d..837a377 100644 --- a/rpnpy/__init__.py +++ b/rpnpy/__init__.py @@ -9,7 +9,7 @@ # will not be found by the version() function in ../setup.py # # Remember to update ../CHANGELOG.md describing what's new in each version. -__version__ = '1.0.30' +__version__ = '1.0.31' # Keep Python linters quiet. _ = Calculator diff --git a/rpnpy/calculator.py b/rpnpy/calculator.py index 9d102da..8121fe1 100644 --- a/rpnpy/calculator.py +++ b/rpnpy/calculator.py @@ -360,6 +360,9 @@ def execute(self, line): except IncompatibleModifiersError as e: self.err('Incompatible modifiers: %s' % e.args[0]) return False + except CalculatorError as e: + self.err('Incompatible modifiers: %s' % e.args[0]) + return False except StopIteration: break else: @@ -375,6 +378,8 @@ def execute(self, line): 'previous error' % command) return False + return True + def _executeOneCommand(self, command, modifiers, count): """ Execute one command. @@ -552,8 +557,8 @@ def _tryEvalExec(self, command, modifiers, count): possibleWhiteSpace = True if possibleWhiteSpace: - errors.append('Did you accidentally include whitespace ' - 'in a command line?') + errors.append('Did you accidentally include ' + 'whitespace in a command line?') raise CalculatorError(*errors) else: self.debug('exec(%r) worked.' % command) @@ -628,11 +633,13 @@ def toggleDebug(self, newValue=None): def _findWithArgs(self, command, description, predicate, defaultArgCount, modifiers, count): - """Look for a callable function and its arguments on the stack. + """ + Look for something (e.g., a callable function or a string) and its + arguments on the stack. @param command: The C{str} name of the command that was invoked. @param description: A C{str} describing what is being sought. Used in - error messages if not suitable stack item is found. + error messages if no suitable stack item is found. @param predicate: A one-arg C{callable} that will be passed stack items and must return C{True} when it identifies something that satisfies the need of the caller. @@ -643,8 +650,9 @@ def _findWithArgs(self, command, description, predicate, defaultArgCount, @param count: An C{int} count of the number of arguments wanted (or C{None} if no count was given). @raise StackError: If there is a problem. - @return: A 2-C{tuple} of the function and a C{tuple} of its arguments. - If a suitable stack item cannot be found, return (None, None). + @return: A 2-C{tuple} of the found item (satisfying the predicate) and + a C{tuple} of its arguments. If a suitable stack item cannot be + found, return (None, None). """ stackLen = len(self) @@ -719,7 +727,7 @@ def findStringAndArgs(self, command, modifiers, count): """Look for a string its arguments on the stack. @param modifier: A C{Modifiers} instance. - @return: A 2-C{tuple} of the function and a C{tuple} of its arguments. + @return: A 2-C{tuple} of the string and a C{tuple} of its arguments. """ def predicate(x): return isinstance(x, str) @@ -729,3 +737,12 @@ def defaultArgCount(x): return self._findWithArgs(command, 'a string', predicate, defaultArgCount, modifiers, count) + + def setVariable(self, variable, value): + """ + Set the value of a variable. + + @param variable: The C{str} variable name. + @param value: The value to give the variable. + """ + self._variables[variable] = value diff --git a/rpnpy/functions.py b/rpnpy/functions.py index 2290c3a..28c2ee8 100644 --- a/rpnpy/functions.py +++ b/rpnpy/functions.py @@ -169,6 +169,7 @@ def join(calc, modifiers, count): @param count: An C{int} count of the number of arguments to pass. """ sep, args = calc.findStringAndArgs('join', modifiers, count) + print(f'sep is {sep}, args is {args!r}') nPop = len(args) + 1 if len(args) == 1: # Only one argument from the stack, so run join on the value of @@ -303,6 +304,30 @@ def list_(calc, modifiers, count): list_.names = ('list',) +def store(calc, modifiers, count): + """Store some stack items into a variable. + + @param calc: A C{Calculator} instance. + @param modifiers: A C{Modifiers} instance. + @param count: An C{int} count of the number of arguments to pass. + """ + variable, args = calc.findStringAndArgs('store', modifiers, count) + if len(args) == 1: + # Only one argument from the stack, so we'll set the variable to + # have that value. + value = args[0] + else: + value = args + + print(f'var is {variable}, args is {args!r}') + calc.setVariable(variable, value) + calc._finalize(None, nPop=len(args) + 1, modifiers=modifiers, noValue=True) + return calc.NO_VALUE + + +store.names = ('store',) + + def map_(calc, modifiers, count): """Map a function over some arguments. @@ -343,6 +368,7 @@ def map_(calc, modifiers, count): reduce, reverse, stack, + store, swap, undo, variables, diff --git a/setup.py b/setup.py index 16ddb8f..43a36cb 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ def version(): url='https://github.com/terrycojones/rpnpy', download_url='https://github.com/terrycojones/rpnpy', author='Terry Jones', - author_email='tcj25@cam.ac.uk', + author_email='terry@jon.es', keywords=['python reverse polish calculator'], classifiers=[ 'Programming Language :: Python :: 3', diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_calculator.py b/test/test_calculator.py index 2149168..1d59c9e 100644 --- a/test/test_calculator.py +++ b/test/test_calculator.py @@ -110,7 +110,7 @@ def testAddOneArg(self): err.getvalue()) def testAddVariables(self): - "add must work correctly when Variable instances are on the stack" + "Add must work correctly when Variable instances are on the stack" c = Calculator() c.execute('a=4 b=5') c.execute('a :!') @@ -119,6 +119,14 @@ def testAddVariables(self): (result,) = c.stack self.assertEqual(9, result) + def testSetVariable(self): + "The setVariable method must have the expected effect." + c = Calculator() + c.setVariable('a', 5) + c.execute('a') + (result,) = c.stack + self.assertEqual(5, result) + def testRegisterWithArgCount(self): """Registering and calling a new function and passing its argument count must work""" @@ -484,11 +492,86 @@ def testAllStackReversed(self): self.assertEqual('3-4-5-6', result) +class TestStore(TestCase): + "Test the store special function" + + def testEmptyStack(self): + "Calling store on an empty stack must fail." + c = Calculator() + self.assertFalse(c.execute('store')) + + def testStackWithOneItem(self): + "Calling store on a stack with one item must fail." + c = Calculator() + self.assertFalse(c.execute('4 store')) + + def testStackWithTwoItemsClearsStack(self): + "Calling store on a stack with two items must result in an empty stack" + c = Calculator() + c.execute('"a" 4 store') + self.assertEqual([], c.stack) + + def testStackWithTwoItemsPreservingStack(self): + """ + Calling store on a stack with two items and asking for the stack to be + preserved must work as expected. + """ + c = Calculator() + c.execute('"a" 4 store:=') + self.assertEqual(['a', 4], c.stack) + + def testStackWithTwoItems(self): + """ + Calling store on a stack with two items must result in the variable + being set as expected. + """ + c = Calculator() + c.execute('"a" 4 store a') + (result,) = c.stack + self.assertEqual(4, result) + + def testStackWithThreeItems(self): + """ + Calling store on a stack with three items and a numeric modifier must + result in the variable being set as expected. + """ + c = Calculator() + c.execute('"a" 4 5 store:2 a') + self.assertEqual([[4, 5]], c.stack) + + def testStackWithThreeItemsReversed(self): + """ + Calling store on a stack with three items and a numeric modifier and + the reverse modifier must result in the variable being set as expected. + """ + c = Calculator() + c.execute('4 5 "a" store:r2 a') + self.assertEqual([[4, 5]], c.stack) + + def testStarModifier(self): + """ + Calling store on a stack with three items and a * modifier must + result in the variable being set as expected. + """ + c = Calculator() + c.execute('"a" 4 5 6 store:* a') + self.assertEqual([[4, 5, 6]], c.stack) + + def testStarModifierReversed(self): + """ + Calling store on a stack with three items and a * modifier and the + reverse modifier must result in the variable being set as expected. + """ + c = Calculator() + c.execute('4 5 6 "a" store:r* a') + self.assertEqual([[4, 5, 6]], c.stack) + + class TestFindCallableAndArgs(TestCase): "Test the findCallableAndArgs function" def testEmptyStack(self): - "Calling on an empty stack must return None, None" + "Calling on an empty stack must raise a StackError." c = Calculator() error = r"^Cannot run 'cmd' \(stack has only 0 items\)$" @@ -496,7 +579,7 @@ def testEmptyStack(self): 'cmd', Modifiers(), None) def testStackLengthOne(self): - "Calling on a stack with only one item must return None, None" + "Calling on a stack with only one item must raise a StackError." errfp = StringIO() c = Calculator(errfp=errfp) c.execute('4') @@ -564,18 +647,18 @@ class TestFindCallableAndArgsReversed(TestCase): "Test the findCallableAndArgs function when the reversed modifier is used" def testEmptyStack(self): - "Calling on an empty stack must return None, None" + "Calling on an empty stack must raise a StackError." c = Calculator() error = r"Cannot run 'cmd' \(stack has only 0 items\)$" - self.assertRaisesRegex(StackError, error, c.findStringAndArgs, + self.assertRaisesRegex(StackError, error, c.findCallableAndArgs, 'cmd', strToModifiers('r'), None) def testStackLengthOne(self): - "Calling on a stack with only one item must return None, None" + "Calling on a stack with only one item must raise a StackError." c = Calculator() c.execute('4') error = r"Cannot run 'cmd' \(stack has only 1 item\)$" - self.assertRaisesRegex(StackError, error, c.findStringAndArgs, + self.assertRaisesRegex(StackError, error, c.findCallableAndArgs, 'cmd', strToModifiers('r'), None) def testStackLengthTwoNoCount(self): @@ -625,14 +708,14 @@ class TestFindStringAndArgs(TestCase): "Test the findStringAndArgs function" def testEmptyStack(self): - "Calling on an empty stack must return None, None" + "Calling on an empty stack must raise a StackError." c = Calculator() error = r"^Cannot run 'cmd' \(stack has only 0 items\)$" self.assertRaisesRegex(StackError, error, c.findStringAndArgs, 'cmd', Modifiers(), None) def testStackLengthOne(self): - "Calling on a stack with only one item must return None, None" + "Calling on a stack with only one item must raise a StackError." c = Calculator() c.execute('4') error = r"^Cannot run 'cmd' \(stack has only 1 item\)$" @@ -698,7 +781,7 @@ class TestFindStringAndArgsReversed(TestCase): "Test the findStringAndArgs function when the reversed modifier is used" def testEmptyStack(self): - "Calling on an empty stack must return None, None" + "Calling on an empty stack must raise a StackError." c = Calculator() error = r"^Cannot run 'cmd' \(stack has only 0 items\)$" self.assertRaisesRegex(StackError, error, c.findStringAndArgs, @@ -904,11 +987,13 @@ def testIterateGenerator(self): (result,) = c.stack self.assertEqual(['1', '2', '3'], result) + class TestEngineeringNotation(TestCase): "Test the engineering notation for values" def testInput(self): - "A value suffixed by a unit should be recognized as engineering notation" + """A value suffixed by a unit should be recognized as engineering + notation.""" c = Calculator() c.execute('2k') (result,) = c.stack From a4f7857418fd8aad45ea51304eb3d4bc91724d4a Mon Sep 17 00:00:00 2001 From: terrycojones Date: Thu, 3 Dec 2020 00:29:03 +0100 Subject: [PATCH 2/3] Removed debugging print statement. --- rpnpy/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rpnpy/functions.py b/rpnpy/functions.py index 28c2ee8..3e7115b 100644 --- a/rpnpy/functions.py +++ b/rpnpy/functions.py @@ -319,7 +319,6 @@ def store(calc, modifiers, count): else: value = args - print(f'var is {variable}, args is {args!r}') calc.setVariable(variable, value) calc._finalize(None, nPop=len(args) + 1, modifiers=modifiers, noValue=True) return calc.NO_VALUE From 11eaa0ea513b44f3de8c8f961262664b47f3ad6b Mon Sep 17 00:00:00 2001 From: terrycojones Date: Thu, 3 Dec 2020 00:51:06 +0100 Subject: [PATCH 3/3] Removed another debugging print statement! --- rpnpy/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rpnpy/functions.py b/rpnpy/functions.py index 3e7115b..71b4b7c 100644 --- a/rpnpy/functions.py +++ b/rpnpy/functions.py @@ -169,7 +169,6 @@ def join(calc, modifiers, count): @param count: An C{int} count of the number of arguments to pass. """ sep, args = calc.findStringAndArgs('join', modifiers, count) - print(f'sep is {sep}, args is {args!r}') nPop = len(args) + 1 if len(args) == 1: # Only one argument from the stack, so run join on the value of