Context Management

The FixedPoint class offers richly-featured context management (see PEP 343) that allows for some unique approaches to programatic arithmetic. Three functions are utilized:

FixedPoint.__call__() allows properties to be assigned in the with statement at the start of the context; this is called context initialization.

FixedPoint.__enter__() save off the current state of the FixedPoint and assigns the properties specified by __call__() in the new context.

FixedPoint.__exit__() restores the original context unless the safe_retain keyword was specified in __call__().

Basic Usage

Use the with statement to generate a scope in which changes to the original object can be undone:

>>> from fixedpoint import FixedPoint
>>> x = FixedPoint(1/9, signed=1)
>>> x.qformat
'Q1.54'

>>> with x: # save off the current state of x
...    x.signed = 0
...    x.m = 42
...    x.qformat # show the changes that were made within the context
'UQ42.54'

>>> x.qformat # outisde of the with context, original x is restored
'Q1.54'

Any property or attribute of the original FixedPoint can be changed within the context. All changes made to FixedPoint properties are restored at context exit. These properties include:

Even the value can be changed with Arithmetic or Initializers.

>>> float(x)
0.1111111111111111

>>> with x:
...    x.from_string("0x7FFFFFAAAA5555")
...    float(x)
-7.947407237862691e-08

>>> float(x)
0.1111111111111111

New FixedPoints generated inside the context manager are valid and available outside of the context. This is useful for temporarily overriding properties. You can also rename a variable if desired.

>>> x = FixedPoint(0.2)
>>> y = FixedPoint(0.7)
>>> x.qformat, y.qformat
('UQ0.54', 'UQ0.52')

>>> z = x - y
Traceback (most recent call last):
    ...
fixedpoint.FixedPointOverflowError: [SN3] Unsigned subtraction causes overflow.

>>> with x as xtmp, y as ytmp:
...    xtmp.m, ytmp.m = 1, 1
...    xtmp.signed, ytmp.signed = 1, 1
...    z = x - y
...    xtmp.qformat, ytmp.qformat, z.qformat
('Q1.54', 'Q1.52', 'Q2.54')

>>> x.qformat, y.qformat, z.qformat, float(round(z, 1))
('UQ0.54', 'UQ0.52', 'Q2.54', -0.5)

Context managers can be nested:

>>> def nest(x):
...     print(f'0) {x.rounding=}')
...     with x as y:
...         print(f'1) {x.rounding=}')
...         x.rounding = 'in'
...         print(f'2) {x.rounding=}')
...         with y as z:
...             print(f'3) {x.rounding=}')
...             z.rounding = 'convergent'
...             print(f'4) {x.rounding=}')
...         print(f'5) {x.rounding=}')
...     print(f'6) {x.rounding=}')

>>> nest(FixedPoint(31))
0) x.rounding='nearest'
1) x.rounding='nearest'
2) x.rounding='in'
3) x.rounding='in'
4) x.rounding='convergent'
5) x.rounding='in'
6) x.rounding='nearest'

Context Initialization

In addition to saving off the current context of FixedPoint objects, the with statement can also initialize the new context for you. Given x, y, and z below,

>>> x = FixedPoint(-1)
>>> y = FixedPoint(1, mismatch_alert='error')
>>> z = x + y
Traceback (most recent call last):
    ...
fixedpoint.MismatchError: [SN2] Non-matching mismatch_alert behaviors ['warning', 'error'].

the following two code blocks accomplish the same goal:

>>> with x, y:
...     x.rounding = 'nearest'
...     x.mismatch_alert = 'error'
...     z = x + y
>>> float(z)
0.0
>>> with x(rounding='nearest', mismatch_alert='error'):
...     z = x + y
>>> float(z)
0.0

Any keywordable argument from the FixedPoint constructor can be used in the context manager. All initilization arguments must be keyworded. The __call__() keywords can be specified in a dict if preferred.

>>> xprop = {'rounding': 'nearest', 'mismatch_alert': 'warning'}
>>> yprop = {'mismatch_alert': 'warning'}
>>> with x(**xprop), y(**yprop):
...     z = x + y

>>> x.rounding, x.mismatch_alert, y.rounding, y.mismatch_alert
('convergent', 'warning', 'nearest', 'error')

Retaining the Context

Context initialization also supports a safe_retain keyword that, when True, will not restore the original FixedPoint context as long as no exceptions occur.

>>> x = FixedPoint(3, str_base=10)
>>> x.qformat
'UQ2.0'

>>> with x(safe_retain=True):
...     x.signed = True
Traceback (most recent call last):
    ...
fixedpoint.FixedPointOverflowError: [SN1] Changing signedness on 3 causes overflow.

>>> x.signed, x.qformat # Changes were not retained because of exception
(False, 'UQ2.0')

>>> with x(m=3, safe_retain=True):
...     x.signed = True

>>> x.signed, x.qformat # Changes were retained
(True, 'Q3.0')

This is useful when several properties/attributes might change, and if all changes are made successfully, the properties should be retained. In fact, this is exactly how FixedPoint.resize() is implemented:

    def resize(self: FixedPointType, m: int, n: int, /, rounding: str = None,
               overflow: str = None, alert: str = None) -> None:
        """Resize integer and fractional bit widths.

        Overflow handling, sign-extension, and rounding are employed.

        Override rounding, overflow, and overflow_alert settings for the
        scope of this method by specifying the appropriate arguments.
        """
        old = self._overflow, self._rounding, self._overflow_alert
        try:
            with self(safe_retain=True,
                      overflow=overflow or self.overflow,
                      rounding=rounding or self.rounding,
                      overflow_alert=alert or self.overflow_alert):
                self.n = n
                self.m = m
        except Exception:
            raise
        else:
            self._overflow, self._rounding, self._overflow_alert = old

The magic here is that if self.m = m raises an exception, then the assignment on the line just before it is undone by the context manager. However, if no exception occurs, then the assignments to the m and n attributes are kept and the number is resized.