summaryrefslogtreecommitdiff
path: root/foundry/utils.py
blob: b5d58b65d3f14a5c5993b031a0e8ecb1536689ff (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# vim: set filencoding=utf8
"""
Random Stuff

@author: Mike Crute (mcrute@gmail.com)
@organization: SoftGroup Interactive, Inc.
@date: May 02, 2010

Random stuff that doesn't deserve it's own module
but is still needed for the rest of the program.
"""

class frozendict(dict):
    """
    A frozen dictionary implementation can not be modified once
    it has been constructed, much like a tuple. Frozen dictionaries
    are hashable.
    """

    def __new__(cls, indict):
        inst = dict.__new__(cls)
        inst.__hash = hash(tuple(sorted(indict.items())))
        inst.__slots__ = indict.keys()
        dict.__init__(inst, indict)
        return inst

    @property
    def __blocked(self):
        raise AttributeError("Can't modify frozendict instance.")

    __delitem__ = __setitem__ = clear = pop = __blocked
    popitem = setdefault = update = __blocked

    __hash__ = lambda self: self.__hash
    __repr__ = lambda self: "frozendict({1})".format(dict.__repr__(self))


def implements(interface, debug=False):
    """
    Verify that a class conforms to a specified interface.
    This decorator is not perfect, for example it can not
    check exceptions or return values. But it does ensure
    that all public methods exist and their arguments
    conform to the interface.

    The debug flag allows overriding checking of the runtime
    flag for testing purposes, it should never be set in
    production code.

    NOTE: This decorator does nothing if -d is not passed
    to the Python runtime.
    """
    import sys
    if not sys.flags.debug and not debug:
        return lambda func: func

    # Defer this import until we know we're supposed to run
    import inspect

    def get_filtered_members(item):
        "Gets non-private or non-protected symbols."
        return dict([(key, value) for key, value in inspect.getmembers(item)
                if not key.startswith('_')])

    def build_contract(item):
        """
        Builds a function contract string. The contract
        string will ignore the name of positional params
        but will consider the name of keyword arguments.
        """
        argspec = inspect.getargspec(item)

        if argspec.defaults:
            num_keywords = len(argspec.defaults)
            args = ['_'] * (len(argspec.args) - num_keywords)
            args.extend(argspec.args[num_keywords-1:])
        else:
            args = ['_'] * len(argspec.args)

        if argspec.varargs:
            args.append('*args')

        if argspec.keywords:
            args.append('**kwargs')

        return ', '.join(args)

    def tester(klass):
        "Verifies conformance to the interface."
        interface_elements = get_filtered_members(interface)
        class_elements = get_filtered_members(klass)

        for key, value in interface_elements.items():
            assert key in class_elements, \
                    "{0!r} is required but missing.".format(key)

            if inspect.isfunction(value) or inspect.ismethod(value):
                contract = build_contract(value)
                implementation = build_contract(class_elements[key])

                assert implementation == contract, \
                        "{0!r} doesn't conform to interface.".format(key)

        return klass

    return tester