1# -*- test-case-name: foolscap.test.test_banana -*-
2
3from __future__ import print_function
4from twisted.python import log
5from twisted.internet.defer import Deferred
6from foolscap.tokens import Violation
7from foolscap.slicer import BaseSlicer, BaseUnslicer
8from foolscap.constraint import OpenerConstraint, Any, IConstraint
9from foolscap.util import AsyncAND
10
11
12class ListSlicer(BaseSlicer):
13    opentype = ("list",)
14    trackReferences = True
15    slices = list
16
17    def sliceBody(self, streamable, banana):
18        for i in self.obj:
19            yield i
20
21class ListUnslicer(BaseUnslicer):
22    opentype = ("list",)
23
24    maxLength = None
25    itemConstraint = None
26    debug = False
27
28    def setConstraint(self, constraint):
29        if isinstance(constraint, Any):
30            return
31        assert isinstance(constraint, ListConstraint)
32        self.maxLength = constraint.maxLength
33        self.itemConstraint = constraint.constraint
34
35    def start(self, count):
36        #self.opener = foo # could replace it if we wanted to
37        self.list = []
38        self.count = count
39        if self.debug:
40            log.msg("%s[%d].start with %s" % (self, self.count, self.list))
41        self.protocol.setObject(count, self.list)
42        self._ready_deferreds = []
43
44    def checkToken(self, typebyte, size):
45        if self.maxLength != None and len(self.list) >= self.maxLength:
46            # list is full, no more tokens accepted
47            # this is hit if the max+1 item is a primitive type
48            raise Violation("the list is full")
49        if self.itemConstraint:
50            self.itemConstraint.checkToken(typebyte, size)
51
52    def doOpen(self, opentype):
53        # decide whether the given object type is acceptable here. Raise a
54        # Violation exception if not, otherwise give it to our opener (which
55        # will normally be the RootUnslicer). Apply a constraint to the new
56        # unslicer.
57        if self.maxLength != None and len(self.list) >= self.maxLength:
58            # this is hit if the max+1 item is a non-primitive type
59            raise Violation("the list is full")
60        if self.itemConstraint:
61            self.itemConstraint.checkOpentype(opentype)
62        unslicer = self.open(opentype)
63        if unslicer:
64            if self.itemConstraint:
65                unslicer.setConstraint(self.itemConstraint)
66        return unslicer
67
68    def update(self, obj, index):
69        # obj has already passed typechecking
70        if self.debug:
71            log.msg("%s[%d].update: [%d]=%s" % (self, self.count, index, obj))
72        assert isinstance(index, int)
73        self.list[index] = obj
74        return obj
75
76    def receiveChild(self, obj, ready_deferred=None):
77        if ready_deferred:
78            self._ready_deferreds.append(ready_deferred)
79        if self.debug:
80            log.msg("%s[%d].receiveChild(%s)" % (self, self.count, obj))
81        # obj could be a primitive type, a Deferred, or a complex type like
82        # those returned from an InstanceUnslicer. However, the individual
83        # object has already been through the schema validation process. The
84        # only remaining question is whether the larger schema will accept
85        # it.
86        if self.maxLength != None and len(self.list) >= self.maxLength:
87            # this is redundant
88            # (if it were a non-primitive one, it would be caught in doOpen)
89            # (if it were a primitive one, it would be caught in checkToken)
90            raise Violation("the list is full")
91        if isinstance(obj, Deferred):
92            if self.debug:
93                log.msg(" adding my update[%d] to %s" % (len(self.list), obj))
94            obj.addCallback(self.update, len(self.list))
95            obj.addErrback(self.printErr)
96            placeholder = "list placeholder for arg[%d], rd=%s" % \
97                          (len(self.list), ready_deferred)
98            self.list.append(placeholder)
99        else:
100            self.list.append(obj)
101
102    def printErr(self, why):
103        print("ERR!")
104        print(why.getBriefTraceback())
105        log.err(why)
106
107    def receiveClose(self):
108        ready_deferred = None
109        if self._ready_deferreds:
110            ready_deferred = AsyncAND(self._ready_deferreds)
111        return self.list, ready_deferred
112
113    def describe(self):
114        return "[%d]" % len(self.list)
115
116
117class ListConstraint(OpenerConstraint):
118    """The object must be a list of objects, with a given maximum length. To
119    accept lists of any length, use maxLength=None. All member objects must
120    obey the given constraint."""
121
122    opentypes = [("list",)]
123    name = "ListConstraint"
124
125    def __init__(self, constraint, maxLength=None, minLength=0):
126        self.constraint = IConstraint(constraint)
127        self.maxLength = maxLength
128        self.minLength = minLength
129
130    def checkObject(self, obj, inbound):
131        if not isinstance(obj, list):
132            raise Violation("not a list")
133        if self.maxLength is not None and len(obj) > self.maxLength:
134            raise Violation("list too long")
135        if len(obj) < self.minLength:
136            raise Violation("list too short")
137        for o in obj:
138            self.constraint.checkObject(o, inbound)
139