text: # -*- test-case-name: foolscap.test.test_banana -*-
  
  import six
  from twisted.python.components import registerAdapter
  from twisted.python import log
  from zope.interface import implementer
  from twisted.internet.defer import Deferred
  from . import tokens
  from .tokens import Violation, BananaError
  from foolscap.ipb import IBroker
  from foolscap.util import ensure_tuple_str
  
  class SlicerClass(type):
      # auto-register Slicers
      def __init__(self, name, bases, dict):
          type.__init__(self, name, bases, dict)
          typ = dict.get('slices')
          #reg = dict.get('slicerRegistry')
          if typ:
              registerAdapter(self, typ, tokens.ISlicer)
  
  @six.add_metaclass(SlicerClass)
  @implementer(tokens.ISlicer)
  class BaseSlicer(object):
      slices = None
  
      parent = None
      sendOpen = True
      opentype = ()
      trackReferences = False
  
      def __init__(self, obj):
          # this simplifies Slicers which are adapters
          self.obj = obj
  
      def requireBroker(self, protocol):
          broker = IBroker(protocol, None)
          if not broker:
              msg = "This object can only be serialized by a broker"
              raise Violation(msg)
          return broker
  
      def registerRefID(self, refid, obj):
          # optimize: most Slicers will delegate this up to the Root
          return self.parent.registerRefID(refid, obj)
      def slicerForObject(self, obj):
          # optimize: most Slicers will delegate this up to the Root
          return self.parent.slicerForObject(obj)
      def slice(self, streamable, banana):
          # this is what makes us ISlicer
          self.streamable = streamable
          assert self.opentype
          for o in self.opentype:
              # our wire protocol, which originated in py2, uses bytes for the
              # index tokens
              yield six.ensure_binary(o)
          for t in self.sliceBody(streamable, banana):
              yield t
      def sliceBody(self, streamable, banana):
          raise NotImplementedError
      def childAborted(self, f):
          return f
  
      def describe(self):
          return "??"
  
  
  class ScopedSlicer(BaseSlicer):
      """This Slicer provides a containing scope for referenceable things like
      lists. The same list will not be serialized twice within this scope, but
      it will not survive outside it."""
  
      def __init__(self, obj):
          BaseSlicer.__init__(self, obj)
          self.references = {} # maps id(obj) -> (obj,refid)
  
      def registerRefID(self, refid, obj):
          # keep references here, not in the actual PBRootSlicer
  
          # This use of id(obj) requires a bit of explanation. We are making
          # the assumption that the object graph remains unmodified until
          # serialization is complete. In particular, we assume that all the
          # objects in it remain alive, and no new objects are added to it,
          # until serialization is complete. id(obj) is only unique for live
          # objects: once the object is garbage-collected, a new object may be
          # created with the same id(obj) value.
          #
          # The concern is that a custom Slicer will call something that
          # mutates the object graph before it has finished being serialized.
          # This might be one which calls some user-level function during
          # Slicing, or one which uses a Deferred to put off serialization for
          # a while, creating an opportunity for some other code to get
          # control.
  
          # The specific concern is that if, in the middle of serialization, an
          # object that was already serialized is gc'ed, and a new object is
          # created and attached to a portion of the object graph that hasn't
          # been serialized yet, and if the new object gets the same id(obj) as
          # the dead object, then we could be tricked into sending the
          # reference number of the old (dead) object. On the receiving end,
          # this would result in a mangled object graph.
  
          # User code isn't supposed to allow the object graph to change during
          # serialization, so this mangling "should not happen" under normal
          # circumstances. However, as a reasonably cheap way to mitigate the
          # worst sort of mangling when user code *does* mess up,
          # self.references maps from id(obj) to a tuple of (obj,refid) instead
          # of just the refid. This insures that the object will stay alive
          # until the ScopedSlicer dies, guaranteeing that we won't get
          # duplicate id(obj) values. If user code mutates the object graph
          # during serialization we might still get inconsistent results, but
          # they'll be the ordinary kind of inconsistent results (snapshots of
          # different branches of the object graph at different points in time)
          # rather than the blatantly wrong mangling that would occur with
          # re-used id(obj) values.
  
          self.references[id(obj)] = (obj,refid)
  
      def slicerForObject(self, obj):
          # check for an object which was sent previously or has at least
          # started sending
          obj_refid = self.references.get(id(obj), None)
          if obj_refid is not None:
              # we've started to send this object already, so just include a
              # reference to it
              return ReferenceSlicer(obj_refid[1])
          # otherwise go upstream so we can serialize the object completely
          return self.parent.slicerForObject(obj)
  
  UnslicerRegistry = {}
  BananaUnslicerRegistry = {}
  
  def registerUnslicer(opentype, factory, registry=None):
      opentype = ensure_tuple_str(opentype)
      if registry is None:
          registry = UnslicerRegistry
      assert opentype not in registry
      registry[opentype] = factory
  
  class UnslicerClass(type):
      # auto-register Unslicers
      def __init__(self, name, bases, dict):
          type.__init__(self, name, bases, dict)
          opentype = dict.get('opentype')
          reg = dict.get('unslicerRegistry')
          if opentype:
              registerUnslicer(opentype, self, reg)
  
  @six.add_metaclass(UnslicerClass)
  @implementer(tokens.IUnslicer)
  class BaseUnslicer(object):
      opentype = None
  
      def __init__(self):
          pass
  
      def describe(self):
          return "??"
  
      def setConstraint(self, constraint):
          pass
  
      def start(self, count):
          pass
  
      def checkToken(self, typebyte, size):
          return # no restrictions
  
      def openerCheckToken(self, typebyte, size, opentype):
          return self.parent.openerCheckToken(typebyte, size, opentype)
  
      def open(self, opentype):
          """Return an IUnslicer object based upon the 'opentype' tuple.
          Subclasses that wish to change the way opentypes are mapped to
          Unslicers can do so by changing this behavior.
  
          This method does not apply constraints, it only serves to map
          opentype into Unslicer. Most subclasses will implement this by
          delegating the request to their parent (and thus, eventually, to the
          RootUnslicer), and will set the new child's .opener attribute so
          that they can do the same. Subclasses that wish to change the way
          opentypes are mapped to Unslicers can do so by changing this
          behavior."""
  
          return self.parent.open(opentype)
  
      def doOpen(self, opentype):
          """Return an IUnslicer object based upon the 'opentype' tuple. This
          object will receive all tokens destined for the subnode.
  
          If you want to enforce a constraint, you must override this method
          and do two things: make sure your constraint accepts the opentype,
          and set a per-item constraint on the new child unslicer.
  
          This method gets the IUnslicer from our .open() method. That might
          return None instead of a child unslicer if the they want a
          multi-token opentype tuple, so be sure to check for Noneness before
          adding a per-item constraint.
          """
  
          return self.open(opentype)
  
      def receiveChild(self, obj, ready_deferred=None):
          """Unslicers for containers should accumulate their children's
          ready_deferreds, then combine them in an AsyncAND when receiveClose()
          happens, and return the AsyncAND as the ready_deferreds half of the
          receiveClose() return value.
          """
          pass
  
      def reportViolation(self, why):
          return why
  
      def receiveClose(self):
          raise NotImplementedError
  
      def finish(self):
          pass
  
  
      def setObject(self, counter, obj):
          """To pass references to previously-sent objects, the [OPEN,
          'reference', number, CLOSE] sequence is used. The numbers are
          generated implicitly by the sending Banana, counting from 0 for the
          object described by the very first OPEN sent over the wire,
          incrementing for each subsequent one. The objects themselves are
          stored in any/all Unslicers who cares to. Generally this is the
          RootUnslicer, but child slices could do it too if they wished.
          """
          # TODO: examine how abandoned child objects could mess up this
          # counter
          pass
  
      def getObject(self, counter):
          """'None' means 'ask our parent instead'.
          """
          return None
  
      def explode(self, failure):
          """If something goes wrong in a Deferred callback, it may be too late
          to reject the token and to normal error handling. I haven't figured
          out how to do sensible error-handling in this situation. This method
          exists to make sure that the exception shows up *somewhere*. If this
          is called, it is also likely that a placeholder (probably a Deferred)
          will be left in the unserialized object graph about to be handed to
          the RootUnslicer.
          """
  
          # RootUnslicer pays attention to this .exploded attribute and refuses
          # to deliver anything if it is set. But PBRootUnslicer ignores it.
          # TODO: clean this up, and write some unit tests to trigger it (by
          # violating schemas?)
          log.msg("BaseUnslicer.explode: %s" % failure)
          self.protocol.exploded = failure
  
  class ScopedUnslicer(BaseUnslicer):
      """This Unslicer provides a containing scope for referenceable things
      like lists. It corresponds to the ScopedSlicer base class."""
  
      def __init__(self):
          BaseUnslicer.__init__(self)
          self.references = {}
  
      def setObject(self, counter, obj):
          if self.protocol.debugReceive:
              print("setObject(%s): %s{%s}" % (counter, obj, id(obj)))
          self.references[counter] = obj
  
      def getObject(self, counter):
          obj = self.references.get(counter)
          if self.protocol.debugReceive:
              print("getObject(%s) -> %s{%s}" % (counter, obj, id(obj)))
          return obj
  
  
  class LeafUnslicer(BaseUnslicer):
      # inherit from this to reject any child nodes
  
      # .checkToken in LeafUnslicer subclasses should reject OPEN tokens
  
      def doOpen(self, opentype):
          raise Violation("'%s' does not accept sub-objects" % self)
  
  
  # References are special enough to put here instead of slicers/
  
  class ReferenceSlicer(BaseSlicer):
      # this is created explicitly, not as an adapter
      opentype = ('reference',)
      trackReferences = False
  
      def __init__(self, refid):
          assert type(refid) is int
          self.refid = refid
      def sliceBody(self, streamable, banana):
          yield self.refid
  
  class ReferenceUnslicer(LeafUnslicer):
      opentype = ('reference',)
  
      constraint = None
      finished = False
  
      def setConstraint(self, constraint):
          self.constraint = constraint
  
      def checkToken(self, typebyte,size):
          if typebyte != tokens.INT:
              raise BananaError("ReferenceUnslicer only accepts INTs")
  
      def receiveChild(self, obj, ready_deferred=None):
          assert not isinstance(obj, Deferred)
          assert ready_deferred is None
          if self.finished:
              raise BananaError("ReferenceUnslicer only accepts one int")
          self.obj = self.protocol.getObject(obj)
          self.finished = True
          # assert that this conforms to the constraint
          if self.constraint:
              self.constraint.checkObject(self.obj, True)
          # TODO: it might be a Deferred, but we should know enough about the
          # incoming value to check the constraint. This requires a subclass
          # of Deferred which can give us the metadata.
  
      def receiveClose(self):
          return self.obj, None
  