Function Arguments/Return Type-Testing Decorator

[Jan-2015] As part of a recent project, I extended an arguments type-checking function+method decorator that appears in Chapter 39 of Learning Python, 5th Edition, to also perform return type testing, and use 3.X function attributes instead of decorator arguments. That makes it 3.X only, but it's simple to translate this code to use 2.X-compatible decorator arguments instead, as shown in the book.

Caveats

This decorator might be useful during development to ensure coding-time expectations, but has some major downsides:

Update Apr-2015: A similar model is being proposed as standard type declarations for Python 3.5; it threatens to escalate the same caveats to best practice. For an arguably better use case for decorators, see the Private/Public attributes class decorator example from LP5E.

Usage

See the decorator module's self-test code for usage examples, and the book for the logic underlying the decorator itself (its origins go back to this post from 2008/2009).

As another use case—and an example of bytes and bit-level processing—I used this decorator to verify that type annotations coded in simple file obfuscation functions were being respected. Here's a partial excerpt from this (not yet published) client:

"""
[Scheme 2 of 4]
>>> data = do_encode2(b'spam', ord('x'))
120 b'spam'
bytearray(b'\x00\xeb\x00\xe8\x00\xd9\x00\xe5')
 
>>> data = do_decode2(bytes(data), ord('x'))         # need bytes() for decorator only!
120 bytearray(b'\x00\xeb\x00\xe8\x00\xd9\x00\xe5')
bytearray(b'spam')
"""

from debugtypes import debugtypes

@debugtypes
def do_encode2(data: bytes, adder: int) -> bytearray:
    trace(adder, data[:4])
    newdata = bytearray()
    for byte in data:                         # bytes => int (or via bytes[i])
        word  = byte + adder                  # add scrambler
        byte1 = (word & 0xFF00) >> 8          # upper byte 
        byte2 = (word & 0X00FF)               # lower byte
        newdata.extend([byte1, byte2])        # 2 bytes for 1, to binary file
    trace(newdata[:8])                        # int => bytes (or bytes([int]))
    return newdata

@debugtypes
def do_decode2(data: bytes, adder: int) -> bytearray:
    trace(adder, data[:8])
    newdata = bytearray()
    ix = 0
    while ix < len(data):
        byte1, byte2 = data[ix], data[ix+1]   # bytes => int
        word = (byte1 << 8) + byte2           # 1 word to 2 bytes
        word -= adder                         # remove scrambler
        newdata.append(word)                  # retain low byte
        ix += 2
    trace(newdata[:4])
    return newdata

...
from encoder import *
encode, decode = do_encode2, do_decode2       # choose your weapon
data = open(filename, 'rb').read()            # load from original name
data = encode(data, adder)                    # scramble byte data
newname = filename + encext                   # write to new enc name
with open(newname, 'wb') as newfile:          # guarantee closes
    newfile.write(data)


[Python Logo] Home Books Programs Blog Python Author Training Email Search ©M.Lutz