Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added 'store' command suggested by @nfraprado. #7

Merged
merged 3 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion rpnpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 24 additions & 7 deletions rpnpy/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -375,6 +378,8 @@ def execute(self, line):
'previous error' % command)
return False

return True

def _executeOneCommand(self, command, modifiers, count):
"""
Execute one command.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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
25 changes: 25 additions & 0 deletions rpnpy/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}')
terrycojones marked this conversation as resolved.
Show resolved Hide resolved
nPop = len(args) + 1
if len(args) == 1:
# Only one argument from the stack, so run join on the value of
Expand Down Expand Up @@ -303,6 +304,29 @@ 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

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.

Expand Down Expand Up @@ -343,6 +367,7 @@ def map_(calc, modifiers, count):
reduce,
reverse,
stack,
store,
swap,
undo,
variables,
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def version():
url='https://github.com/terrycojones/rpnpy',
download_url='https://github.com/terrycojones/rpnpy',
author='Terry Jones',
author_email='[email protected]',
author_email='[email protected]',
keywords=['python reverse polish calculator'],
classifiers=[
'Programming Language :: Python :: 3',
Expand Down
Empty file added test/__init__.py
Empty file.
107 changes: 96 additions & 11 deletions test/test_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 :!')
Expand All @@ -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"""
Expand Down Expand Up @@ -484,19 +492,94 @@ 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\)$"
self.assertRaisesRegex(StackError, error, c.findCallableAndArgs,
'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')
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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\)$"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down