xref: /freebsd-src/contrib/lib9p/pytest/pfod.py (revision 134e17798c9af53632b372348ab828e75e65bf46)
1*134e1779SJakub Wojciech Klama#! /usr/bin/env python
2*134e1779SJakub Wojciech Klama
3*134e1779SJakub Wojciech Klamafrom __future__ import print_function
4*134e1779SJakub Wojciech Klama
5*134e1779SJakub Wojciech Klama__all__ = ['pfod', 'OrderedDict']
6*134e1779SJakub Wojciech Klama
7*134e1779SJakub Wojciech Klama### shameless stealing from namedtuple here
8*134e1779SJakub Wojciech Klama
9*134e1779SJakub Wojciech Klama"""
10*134e1779SJakub Wojciech Klamapfod - prefilled OrderedDict
11*134e1779SJakub Wojciech Klama
12*134e1779SJakub Wojciech KlamaThis is basically a hybrid of a class and an OrderedDict,
13*134e1779SJakub Wojciech Klamaor, sort of a data-only class.  When an instance of the
14*134e1779SJakub Wojciech Klamaclass is created, all its fields are set to None if not
15*134e1779SJakub Wojciech Klamainitialized.
16*134e1779SJakub Wojciech Klama
17*134e1779SJakub Wojciech KlamaBecause it is an OrderedDict you can add extra fields to an
18*134e1779SJakub Wojciech Klamainstance, and they will be in inst.keys().  Because it
19*134e1779SJakub Wojciech Klamabehaves in a class-like way, if the keys are 'foo' and 'bar'
20*134e1779SJakub Wojciech Klamayou can write print(inst.foo) or inst.bar = 3.  Setting an
21*134e1779SJakub Wojciech Klamaattribute that does not currently exist causes a new key
22*134e1779SJakub Wojciech Klamato be added to the instance.
23*134e1779SJakub Wojciech Klama"""
24*134e1779SJakub Wojciech Klama
25*134e1779SJakub Wojciech Klamaimport sys as _sys
26*134e1779SJakub Wojciech Klamafrom keyword import iskeyword as _iskeyword
27*134e1779SJakub Wojciech Klamafrom collections import OrderedDict
28*134e1779SJakub Wojciech Klamafrom collections import deque as _deque
29*134e1779SJakub Wojciech Klama
30*134e1779SJakub Wojciech Klama_class_template = '''\
31*134e1779SJakub Wojciech Klamaclass {typename}(OrderedDict):
32*134e1779SJakub Wojciech Klama    '{typename}({arg_list})'
33*134e1779SJakub Wojciech Klama    __slots__ = ()
34*134e1779SJakub Wojciech Klama
35*134e1779SJakub Wojciech Klama    _fields = {field_names!r}
36*134e1779SJakub Wojciech Klama
37*134e1779SJakub Wojciech Klama    def __init__(self, *args, **kwargs):
38*134e1779SJakub Wojciech Klama        'Create new instance of {typename}()'
39*134e1779SJakub Wojciech Klama        super({typename}, self).__init__()
40*134e1779SJakub Wojciech Klama        args = _deque(args)
41*134e1779SJakub Wojciech Klama        for field in self._fields:
42*134e1779SJakub Wojciech Klama            if field in kwargs:
43*134e1779SJakub Wojciech Klama                self[field] = kwargs.pop(field)
44*134e1779SJakub Wojciech Klama            elif len(args) > 0:
45*134e1779SJakub Wojciech Klama                self[field] = args.popleft()
46*134e1779SJakub Wojciech Klama            else:
47*134e1779SJakub Wojciech Klama                self[field] = None
48*134e1779SJakub Wojciech Klama        if len(kwargs):
49*134e1779SJakub Wojciech Klama            raise TypeError('unexpected kwargs %s' % kwargs.keys())
50*134e1779SJakub Wojciech Klama        if len(args):
51*134e1779SJakub Wojciech Klama            raise TypeError('unconsumed args %r' % tuple(args))
52*134e1779SJakub Wojciech Klama
53*134e1779SJakub Wojciech Klama    def _copy(self):
54*134e1779SJakub Wojciech Klama        'copy to new instance'
55*134e1779SJakub Wojciech Klama        new = {typename}()
56*134e1779SJakub Wojciech Klama        new.update(self)
57*134e1779SJakub Wojciech Klama        return new
58*134e1779SJakub Wojciech Klama
59*134e1779SJakub Wojciech Klama    def __getattr__(self, attr):
60*134e1779SJakub Wojciech Klama        if attr in self:
61*134e1779SJakub Wojciech Klama            return self[attr]
62*134e1779SJakub Wojciech Klama        raise AttributeError('%r object has no attribute %r' %
63*134e1779SJakub Wojciech Klama            (self.__class__.__name__, attr))
64*134e1779SJakub Wojciech Klama
65*134e1779SJakub Wojciech Klama    def __setattr__(self, attr, val):
66*134e1779SJakub Wojciech Klama        if attr.startswith('_OrderedDict_'):
67*134e1779SJakub Wojciech Klama            super({typename}, self).__setattr__(attr, val)
68*134e1779SJakub Wojciech Klama        else:
69*134e1779SJakub Wojciech Klama            self[attr] = val
70*134e1779SJakub Wojciech Klama
71*134e1779SJakub Wojciech Klama    def __repr__(self):
72*134e1779SJakub Wojciech Klama        'Return a nicely formatted representation string'
73*134e1779SJakub Wojciech Klama        return '{typename}({repr_fmt})'.format(**self)
74*134e1779SJakub Wojciech Klama'''
75*134e1779SJakub Wojciech Klama
76*134e1779SJakub Wojciech Klama_repr_template = '{name}={{{name}!r}}'
77*134e1779SJakub Wojciech Klama
78*134e1779SJakub Wojciech Klama# Workaround for py2k exec-as-statement, vs py3k exec-as-function.
79*134e1779SJakub Wojciech Klama# Since the syntax differs, we have to exec the definition of _exec!
80*134e1779SJakub Wojciech Klamaif _sys.version_info[0] < 3:
81*134e1779SJakub Wojciech Klama    # py2k: need a real function.  (There is a way to deal with
82*134e1779SJakub Wojciech Klama    # this without a function if the py2k is new enough, but this
83*134e1779SJakub Wojciech Klama    # works in more cases.)
84*134e1779SJakub Wojciech Klama    exec("""def _exec(string, gdict, ldict):
85*134e1779SJakub Wojciech Klama        "Python 2: exec string in gdict, ldict"
86*134e1779SJakub Wojciech Klama        exec string in gdict, ldict""")
87*134e1779SJakub Wojciech Klamaelse:
88*134e1779SJakub Wojciech Klama    # py3k: just make an alias for builtin function exec
89*134e1779SJakub Wojciech Klama    exec("_exec = exec")
90*134e1779SJakub Wojciech Klama
91*134e1779SJakub Wojciech Klamadef pfod(typename, field_names, verbose=False, rename=False):
92*134e1779SJakub Wojciech Klama    """
93*134e1779SJakub Wojciech Klama    Return a new subclass of OrderedDict with named fields.
94*134e1779SJakub Wojciech Klama
95*134e1779SJakub Wojciech Klama    Fields are accessible by name.  Note that this means
96*134e1779SJakub Wojciech Klama    that to copy a PFOD you must use _copy() - field names
97*134e1779SJakub Wojciech Klama    may not start with '_' unless they are all numeric.
98*134e1779SJakub Wojciech Klama
99*134e1779SJakub Wojciech Klama    When creating an instance of the new class, fields
100*134e1779SJakub Wojciech Klama    that are not initialized are set to None.
101*134e1779SJakub Wojciech Klama
102*134e1779SJakub Wojciech Klama    >>> Point = pfod('Point', ['x', 'y'])
103*134e1779SJakub Wojciech Klama    >>> Point.__doc__                   # docstring for the new class
104*134e1779SJakub Wojciech Klama    'Point(x, y)'
105*134e1779SJakub Wojciech Klama    >>> p = Point(11, y=22)             # instantiate with positional args or keywords
106*134e1779SJakub Wojciech Klama    >>> p
107*134e1779SJakub Wojciech Klama    Point(x=11, y=22)
108*134e1779SJakub Wojciech Klama    >>> p['x'] + p['y']                 # indexable
109*134e1779SJakub Wojciech Klama    33
110*134e1779SJakub Wojciech Klama    >>> p.x + p.y                       # fields also accessable by name
111*134e1779SJakub Wojciech Klama    33
112*134e1779SJakub Wojciech Klama    >>> p._copy()
113*134e1779SJakub Wojciech Klama    Point(x=11, y=22)
114*134e1779SJakub Wojciech Klama    >>> p2 = Point()
115*134e1779SJakub Wojciech Klama    >>> p2.extra = 2
116*134e1779SJakub Wojciech Klama    >>> p2
117*134e1779SJakub Wojciech Klama    Point(x=None, y=None)
118*134e1779SJakub Wojciech Klama    >>> p2.extra
119*134e1779SJakub Wojciech Klama    2
120*134e1779SJakub Wojciech Klama    >>> p2['extra']
121*134e1779SJakub Wojciech Klama    2
122*134e1779SJakub Wojciech Klama    """
123*134e1779SJakub Wojciech Klama
124*134e1779SJakub Wojciech Klama    # Validate the field names.  At the user's option, either generate an error
125*134e1779SJakub Wojciech Klama    if _sys.version_info[0] >= 3:
126*134e1779SJakub Wojciech Klama        string_type = str
127*134e1779SJakub Wojciech Klama    else:
128*134e1779SJakub Wojciech Klama        string_type = basestring
129*134e1779SJakub Wojciech Klama    # message or automatically replace the field name with a valid name.
130*134e1779SJakub Wojciech Klama    if isinstance(field_names, string_type):
131*134e1779SJakub Wojciech Klama        field_names = field_names.replace(',', ' ').split()
132*134e1779SJakub Wojciech Klama    field_names = list(map(str, field_names))
133*134e1779SJakub Wojciech Klama    typename = str(typename)
134*134e1779SJakub Wojciech Klama    if rename:
135*134e1779SJakub Wojciech Klama        seen = set()
136*134e1779SJakub Wojciech Klama        for index, name in enumerate(field_names):
137*134e1779SJakub Wojciech Klama            if (not all(c.isalnum() or c=='_' for c in name)
138*134e1779SJakub Wojciech Klama                or _iskeyword(name)
139*134e1779SJakub Wojciech Klama                or not name
140*134e1779SJakub Wojciech Klama                or name[0].isdigit()
141*134e1779SJakub Wojciech Klama                or name.startswith('_')
142*134e1779SJakub Wojciech Klama                or name in seen):
143*134e1779SJakub Wojciech Klama                field_names[index] = '_%d' % index
144*134e1779SJakub Wojciech Klama            seen.add(name)
145*134e1779SJakub Wojciech Klama    for name in [typename] + field_names:
146*134e1779SJakub Wojciech Klama        if type(name) != str:
147*134e1779SJakub Wojciech Klama            raise TypeError('Type names and field names must be strings')
148*134e1779SJakub Wojciech Klama        if not all(c.isalnum() or c=='_' for c in name):
149*134e1779SJakub Wojciech Klama            raise ValueError('Type names and field names can only contain '
150*134e1779SJakub Wojciech Klama                             'alphanumeric characters and underscores: %r' % name)
151*134e1779SJakub Wojciech Klama        if _iskeyword(name):
152*134e1779SJakub Wojciech Klama            raise ValueError('Type names and field names cannot be a '
153*134e1779SJakub Wojciech Klama                             'keyword: %r' % name)
154*134e1779SJakub Wojciech Klama        if name[0].isdigit():
155*134e1779SJakub Wojciech Klama            raise ValueError('Type names and field names cannot start with '
156*134e1779SJakub Wojciech Klama                             'a number: %r' % name)
157*134e1779SJakub Wojciech Klama    seen = set()
158*134e1779SJakub Wojciech Klama    for name in field_names:
159*134e1779SJakub Wojciech Klama        if name.startswith('_OrderedDict_'):
160*134e1779SJakub Wojciech Klama            raise ValueError('Field names cannot start with _OrderedDict_: '
161*134e1779SJakub Wojciech Klama                             '%r' % name)
162*134e1779SJakub Wojciech Klama        if name.startswith('_') and not rename:
163*134e1779SJakub Wojciech Klama            raise ValueError('Field names cannot start with an underscore: '
164*134e1779SJakub Wojciech Klama                             '%r' % name)
165*134e1779SJakub Wojciech Klama        if name in seen:
166*134e1779SJakub Wojciech Klama            raise ValueError('Encountered duplicate field name: %r' % name)
167*134e1779SJakub Wojciech Klama        seen.add(name)
168*134e1779SJakub Wojciech Klama
169*134e1779SJakub Wojciech Klama    # Fill-in the class template
170*134e1779SJakub Wojciech Klama    class_definition = _class_template.format(
171*134e1779SJakub Wojciech Klama        typename = typename,
172*134e1779SJakub Wojciech Klama        field_names = tuple(field_names),
173*134e1779SJakub Wojciech Klama        arg_list = repr(tuple(field_names)).replace("'", "")[1:-1],
174*134e1779SJakub Wojciech Klama        repr_fmt = ', '.join(_repr_template.format(name=name)
175*134e1779SJakub Wojciech Klama                             for name in field_names),
176*134e1779SJakub Wojciech Klama    )
177*134e1779SJakub Wojciech Klama    if verbose:
178*134e1779SJakub Wojciech Klama        print(class_definition,
179*134e1779SJakub Wojciech Klama            file=verbose if isinstance(verbose, file) else _sys.stdout)
180*134e1779SJakub Wojciech Klama
181*134e1779SJakub Wojciech Klama    # Execute the template string in a temporary namespace and support
182*134e1779SJakub Wojciech Klama    # tracing utilities by setting a value for frame.f_globals['__name__']
183*134e1779SJakub Wojciech Klama    namespace = dict(__name__='PFOD%s' % typename,
184*134e1779SJakub Wojciech Klama                     OrderedDict=OrderedDict, _deque=_deque)
185*134e1779SJakub Wojciech Klama    try:
186*134e1779SJakub Wojciech Klama        _exec(class_definition, namespace, namespace)
187*134e1779SJakub Wojciech Klama    except SyntaxError as e:
188*134e1779SJakub Wojciech Klama        raise SyntaxError(e.message + ':\n' + class_definition)
189*134e1779SJakub Wojciech Klama    result = namespace[typename]
190*134e1779SJakub Wojciech Klama
191*134e1779SJakub Wojciech Klama    # For pickling to work, the __module__ variable needs to be set to the frame
192*134e1779SJakub Wojciech Klama    # where the named tuple is created.  Bypass this step in environments where
193*134e1779SJakub Wojciech Klama    # sys._getframe is not defined (Jython for example) or sys._getframe is not
194*134e1779SJakub Wojciech Klama    # defined for arguments greater than 0 (IronPython).
195*134e1779SJakub Wojciech Klama    try:
196*134e1779SJakub Wojciech Klama        result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
197*134e1779SJakub Wojciech Klama    except (AttributeError, ValueError):
198*134e1779SJakub Wojciech Klama        pass
199*134e1779SJakub Wojciech Klama
200*134e1779SJakub Wojciech Klama    return result
201*134e1779SJakub Wojciech Klama
202*134e1779SJakub Wojciech Klamaif __name__ == '__main__':
203*134e1779SJakub Wojciech Klama    import doctest
204*134e1779SJakub Wojciech Klama    doctest.testmod()
205