Backport of decimal module context management updates from rev 51694 to 2.5 release branch

This commit is contained in:
Nick Coghlan 2006-09-03 01:08:30 +00:00
parent f07b590d7e
commit c48daf5bc4
5 changed files with 107 additions and 73 deletions

View File

@ -443,28 +443,29 @@ the \function{getcontext()} and \function{setcontext()} functions:
\end{funcdesc} \end{funcdesc}
Beginning with Python 2.5, you can also use the \keyword{with} statement Beginning with Python 2.5, you can also use the \keyword{with} statement
to temporarily change the active context. For example the following code and the \function{localcontext()} function to temporarily change the
increases the current decimal precision by 2 places, performs a active context.
calculation, and then automatically restores the previous context:
\begin{funcdesc}{localcontext}{\optional{c}}
Return a context manager that will set the current context for
the active thread to a copy of \var{c} on entry to the with-statement
and restore the previous context when exiting the with-statement. If
no context is specified, a copy of the current context is used.
\versionadded{2.5}
For example, the following code sets the current decimal precision
to 42 places, performs a calculation, and then automatically restores
the previous context:
\begin{verbatim} \begin{verbatim}
from __future__ import with_statement from __future__ import with_statement
import decimal from decimal import localcontext
with decimal.getcontext() as ctx: with localcontext() as ctx:
ctx.prec += 2 # add 2 more digits of precision ctx.prec = 42 # Perform a high precision calculation
calculate_something() s = calculate_something()
s = +s # Round the final result back to the default precision
\end{verbatim} \end{verbatim}
\end{funcdesc}
The context that's active in the body of the \keyword{with} statement is
a \emph{copy} of the context you provided to the \keyword{with}
statement, so modifying its attributes doesn't affect anything except
that temporary copy.
You can use any decimal context in a \keyword{with} statement, but if
you just want to make a temporary change to some aspect of the current
context, it's easiest to just use \function{getcontext()} as shown
above.
New contexts can also be created using the \class{Context} constructor New contexts can also be created using the \class{Context} constructor
described below. In addition, the module provides three pre-made described below. In addition, the module provides three pre-made

View File

@ -683,22 +683,22 @@ with lock:
The lock is acquired before the block is executed and always released once The lock is acquired before the block is executed and always released once
the block is complete. the block is complete.
The \module{decimal} module's contexts, which encapsulate the desired The new \function{localcontext()} function in the \module{decimal} module
precision and rounding characteristics for computations, provide a makes it easy to save and restore the current decimal context, which
\method{context_manager()} method for getting a context manager: encapsulates the desired precision and rounding characteristics for
computations:
\begin{verbatim} \begin{verbatim}
import decimal from decimal import Decimal, Context, localcontext
# Displays with default precision of 28 digits # Displays with default precision of 28 digits
v1 = decimal.Decimal('578') v = Decimal('578')
print v1.sqrt() print v.sqrt()
ctx = decimal.Context(prec=16) with localcontext(Context(prec=16)):
with ctx.context_manager():
# All code in this block uses a precision of 16 digits. # All code in this block uses a precision of 16 digits.
# The original context is restored on exiting the block. # The original context is restored on exiting the block.
print v1.sqrt() print v.sqrt()
\end{verbatim} \end{verbatim}
\subsection{Writing Context Managers\label{context-managers}} \subsection{Writing Context Managers\label{context-managers}}

View File

@ -131,7 +131,7 @@ __all__ = [
'ROUND_FLOOR', 'ROUND_UP', 'ROUND_HALF_DOWN', 'ROUND_FLOOR', 'ROUND_UP', 'ROUND_HALF_DOWN',
# Functions for manipulating contexts # Functions for manipulating contexts
'setcontext', 'getcontext' 'setcontext', 'getcontext', 'localcontext'
] ]
import copy as _copy import copy as _copy
@ -458,6 +458,49 @@ else:
del threading, local # Don't contaminate the namespace del threading, local # Don't contaminate the namespace
def localcontext(ctx=None):
"""Return a context manager for a copy of the supplied context
Uses a copy of the current context if no context is specified
The returned context manager creates a local decimal context
in a with statement:
def sin(x):
with localcontext() as ctx:
ctx.prec += 2
# Rest of sin calculation algorithm
# uses a precision 2 greater than normal
return +s # Convert result to normal precision
def sin(x):
with localcontext(ExtendedContext):
# Rest of sin calculation algorithm
# uses the Extended Context from the
# General Decimal Arithmetic Specification
return +s # Convert result to normal context
"""
# The below can't be included in the docstring until Python 2.6
# as the doctest module doesn't understand __future__ statements
"""
>>> from __future__ import with_statement
>>> print getcontext().prec
28
>>> with localcontext():
... ctx = getcontext()
... ctx.prec() += 2
... print ctx.prec
...
30
>>> with localcontext(ExtendedContext):
... print getcontext().prec
...
9
>>> print getcontext().prec
28
"""
if ctx is None: ctx = getcontext()
return _ContextManager(ctx)
##### Decimal class ########################################### ##### Decimal class ###########################################
@ -2173,23 +2216,14 @@ for name in rounding_functions:
del name, val, globalname, rounding_functions del name, val, globalname, rounding_functions
class ContextManager(object): class _ContextManager(object):
"""Helper class to simplify Context management. """Context manager class to support localcontext().
Sample usage:
with decimal.ExtendedContext:
s = ...
return +s # Convert result to normal precision
with decimal.getcontext() as ctx:
ctx.prec += 2
s = ...
return +s
Sets a copy of the supplied context in __enter__() and restores
the previous decimal context in __exit__()
""" """
def __init__(self, new_context): def __init__(self, new_context):
self.new_context = new_context self.new_context = new_context.copy()
def __enter__(self): def __enter__(self):
self.saved_context = getcontext() self.saved_context = getcontext()
setcontext(self.new_context) setcontext(self.new_context)
@ -2248,9 +2282,6 @@ class Context(object):
s.append('traps=[' + ', '.join([t.__name__ for t, v in self.traps.items() if v]) + ']') s.append('traps=[' + ', '.join([t.__name__ for t, v in self.traps.items() if v]) + ']')
return ', '.join(s) + ')' return ', '.join(s) + ')'
def get_manager(self):
return ContextManager(self.copy())
def clear_flags(self): def clear_flags(self):
"""Reset all flags to zero""" """Reset all flags to zero"""
for flag in self.flags: for flag in self.flags:

View File

@ -330,32 +330,6 @@ class LockContextTestCase(unittest.TestCase):
return True return True
self.boilerPlate(lock, locked) self.boilerPlate(lock, locked)
class DecimalContextTestCase(unittest.TestCase):
# XXX Somebody should write more thorough tests for this
def testBasic(self):
ctx = decimal.getcontext()
orig_context = ctx.copy()
try:
ctx.prec = save_prec = decimal.ExtendedContext.prec + 5
with decimal.ExtendedContext.get_manager():
self.assertEqual(decimal.getcontext().prec,
decimal.ExtendedContext.prec)
self.assertEqual(decimal.getcontext().prec, save_prec)
try:
with decimal.ExtendedContext.get_manager():
self.assertEqual(decimal.getcontext().prec,
decimal.ExtendedContext.prec)
1/0
except ZeroDivisionError:
self.assertEqual(decimal.getcontext().prec, save_prec)
else:
self.fail("Didn't raise ZeroDivisionError")
finally:
decimal.setcontext(orig_context)
# This is needed to make the test actually run under regrtest.py! # This is needed to make the test actually run under regrtest.py!
def test_main(): def test_main():
run_suite( run_suite(

View File

@ -23,6 +23,7 @@ or Behaviour) to test each part, or without parameter to test both parts. If
you're working through IDLE, you can import this test module and call test_main() you're working through IDLE, you can import this test module and call test_main()
with the corresponding argument. with the corresponding argument.
""" """
from __future__ import with_statement
import unittest import unittest
import glob import glob
@ -1064,6 +1065,32 @@ class ContextAPItests(unittest.TestCase):
self.assertNotEqual(id(c.flags), id(d.flags)) self.assertNotEqual(id(c.flags), id(d.flags))
self.assertNotEqual(id(c.traps), id(d.traps)) self.assertNotEqual(id(c.traps), id(d.traps))
class WithStatementTest(unittest.TestCase):
# Can't do these as docstrings until Python 2.6
# as doctest can't handle __future__ statements
def test_localcontext(self):
# Use a copy of the current context in the block
orig_ctx = getcontext()
with localcontext() as enter_ctx:
set_ctx = getcontext()
final_ctx = getcontext()
self.assert_(orig_ctx is final_ctx, 'did not restore context correctly')
self.assert_(orig_ctx is not set_ctx, 'did not copy the context')
self.assert_(set_ctx is enter_ctx, '__enter__ returned wrong context')
def test_localcontextarg(self):
# Use a copy of the supplied context in the block
orig_ctx = getcontext()
new_ctx = Context(prec=42)
with localcontext(new_ctx) as enter_ctx:
set_ctx = getcontext()
final_ctx = getcontext()
self.assert_(orig_ctx is final_ctx, 'did not restore context correctly')
self.assert_(set_ctx.prec == new_ctx.prec, 'did not set correct context')
self.assert_(new_ctx is not set_ctx, 'did not copy the context')
self.assert_(set_ctx is enter_ctx, '__enter__ returned wrong context')
def test_main(arith=False, verbose=None): def test_main(arith=False, verbose=None):
""" Execute the tests. """ Execute the tests.
@ -1084,6 +1111,7 @@ def test_main(arith=False, verbose=None):
DecimalPythonAPItests, DecimalPythonAPItests,
ContextAPItests, ContextAPItests,
DecimalTest, DecimalTest,
WithStatementTest,
] ]
try: try: