mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 13:51:09 -05:00
okay fine
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted Spread: Spreadable (Distributed) Computing.
|
||||
|
||||
@author: Glyph Lefkowitz
|
||||
"""
|
||||
403
.venv/lib/python3.12/site-packages/twisted/spread/banana.py
Normal file
403
.venv/lib/python3.12/site-packages/twisted/spread/banana.py
Normal file
@@ -0,0 +1,403 @@
|
||||
# -*- test-case-name: twisted.spread.test.test_banana -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Banana -- s-exp based protocol.
|
||||
|
||||
Future Plans: This module is almost entirely stable. The same caveat applies
|
||||
to it as applies to L{twisted.spread.jelly}, however. Read its future plans
|
||||
for more details.
|
||||
|
||||
@author: Glyph Lefkowitz
|
||||
"""
|
||||
|
||||
|
||||
import copy
|
||||
import struct
|
||||
from io import BytesIO
|
||||
|
||||
from twisted.internet import protocol
|
||||
from twisted.persisted import styles
|
||||
from twisted.python import log
|
||||
from twisted.python.compat import iterbytes
|
||||
from twisted.python.reflect import fullyQualifiedName
|
||||
|
||||
|
||||
class BananaError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def int2b128(integer, stream):
|
||||
if integer == 0:
|
||||
stream(b"\0")
|
||||
return
|
||||
assert integer > 0, "can only encode positive integers"
|
||||
while integer:
|
||||
stream(bytes((integer & 0x7F,)))
|
||||
integer = integer >> 7
|
||||
|
||||
|
||||
def b1282int(st):
|
||||
"""
|
||||
Convert an integer represented as a base 128 string into an L{int}.
|
||||
|
||||
@param st: The integer encoded in a byte string.
|
||||
@type st: L{bytes}
|
||||
|
||||
@return: The integer value extracted from the byte string.
|
||||
@rtype: L{int}
|
||||
"""
|
||||
e = 1
|
||||
i = 0
|
||||
for char in iterbytes(st):
|
||||
n = ord(char)
|
||||
i += n * e
|
||||
e <<= 7
|
||||
return i
|
||||
|
||||
|
||||
# delimiter characters.
|
||||
LIST = b"\x80"
|
||||
INT = b"\x81"
|
||||
STRING = b"\x82"
|
||||
NEG = b"\x83"
|
||||
FLOAT = b"\x84"
|
||||
# "optional" -- these might be refused by a low-level implementation.
|
||||
LONGINT = b"\x85"
|
||||
LONGNEG = b"\x86"
|
||||
# really optional; this is part of the 'pb' vocabulary
|
||||
VOCAB = b"\x87"
|
||||
|
||||
HIGH_BIT_SET = b"\x80"
|
||||
|
||||
|
||||
def setPrefixLimit(limit):
|
||||
"""
|
||||
Set the limit on the prefix length for all Banana connections
|
||||
established after this call.
|
||||
|
||||
The prefix length limit determines how many bytes of prefix a banana
|
||||
decoder will allow before rejecting a potential object as too large.
|
||||
|
||||
@type limit: L{int}
|
||||
@param limit: The number of bytes of prefix for banana to allow when
|
||||
decoding.
|
||||
"""
|
||||
global _PREFIX_LIMIT
|
||||
_PREFIX_LIMIT = limit
|
||||
|
||||
|
||||
_PREFIX_LIMIT = None
|
||||
setPrefixLimit(64)
|
||||
|
||||
SIZE_LIMIT = 640 * 1024 # 640k is all you'll ever need :-)
|
||||
|
||||
|
||||
class Banana(protocol.Protocol, styles.Ephemeral):
|
||||
"""
|
||||
L{Banana} implements the I{Banana} s-expression protocol, client and
|
||||
server.
|
||||
|
||||
@ivar knownDialects: These are the profiles supported by this Banana
|
||||
implementation.
|
||||
@type knownDialects: L{list} of L{bytes}
|
||||
"""
|
||||
|
||||
# The specification calls these profiles but this implementation calls them
|
||||
# dialects instead.
|
||||
knownDialects = [b"pb", b"none"]
|
||||
|
||||
prefixLimit = None
|
||||
sizeLimit = SIZE_LIMIT
|
||||
|
||||
def setPrefixLimit(self, limit):
|
||||
"""
|
||||
Set the prefix limit for decoding done by this protocol instance.
|
||||
|
||||
@see: L{setPrefixLimit}
|
||||
"""
|
||||
self.prefixLimit = limit
|
||||
self._smallestLongInt = -(2 ** (limit * 7)) + 1
|
||||
self._smallestInt = -(2**31)
|
||||
self._largestInt = 2**31 - 1
|
||||
self._largestLongInt = 2 ** (limit * 7) - 1
|
||||
|
||||
def connectionReady(self):
|
||||
"""Surrogate for connectionMade
|
||||
Called after protocol negotiation.
|
||||
"""
|
||||
|
||||
def _selectDialect(self, dialect):
|
||||
self.currentDialect = dialect
|
||||
self.connectionReady()
|
||||
|
||||
def callExpressionReceived(self, obj):
|
||||
if self.currentDialect:
|
||||
self.expressionReceived(obj)
|
||||
else:
|
||||
# this is the first message we've received
|
||||
if self.isClient:
|
||||
# if I'm a client I have to respond
|
||||
for serverVer in obj:
|
||||
if serverVer in self.knownDialects:
|
||||
self.sendEncoded(serverVer)
|
||||
self._selectDialect(serverVer)
|
||||
break
|
||||
else:
|
||||
# I can't speak any of those dialects.
|
||||
log.msg(
|
||||
"The client doesn't speak any of the protocols "
|
||||
"offered by the server: disconnecting."
|
||||
)
|
||||
self.transport.loseConnection()
|
||||
else:
|
||||
if obj in self.knownDialects:
|
||||
self._selectDialect(obj)
|
||||
else:
|
||||
# the client just selected a protocol that I did not suggest.
|
||||
log.msg(
|
||||
"The client selected a protocol the server didn't "
|
||||
"suggest and doesn't know: disconnecting."
|
||||
)
|
||||
self.transport.loseConnection()
|
||||
|
||||
def connectionMade(self):
|
||||
self.setPrefixLimit(_PREFIX_LIMIT)
|
||||
self.currentDialect = None
|
||||
if not self.isClient:
|
||||
self.sendEncoded(self.knownDialects)
|
||||
|
||||
def gotItem(self, item):
|
||||
l = self.listStack
|
||||
if l:
|
||||
l[-1][1].append(item)
|
||||
else:
|
||||
self.callExpressionReceived(item)
|
||||
|
||||
buffer = b""
|
||||
|
||||
def dataReceived(self, chunk):
|
||||
buffer = self.buffer + chunk
|
||||
listStack = self.listStack
|
||||
gotItem = self.gotItem
|
||||
while buffer:
|
||||
assert self.buffer != buffer, "This ain't right: {} {}".format(
|
||||
repr(self.buffer),
|
||||
repr(buffer),
|
||||
)
|
||||
self.buffer = buffer
|
||||
pos = 0
|
||||
for ch in iterbytes(buffer):
|
||||
if ch >= HIGH_BIT_SET:
|
||||
break
|
||||
pos = pos + 1
|
||||
else:
|
||||
if pos > self.prefixLimit:
|
||||
raise BananaError(
|
||||
"Security precaution: more than %d bytes of prefix"
|
||||
% (self.prefixLimit,)
|
||||
)
|
||||
return
|
||||
num = buffer[:pos]
|
||||
typebyte = buffer[pos : pos + 1]
|
||||
rest = buffer[pos + 1 :]
|
||||
if len(num) > self.prefixLimit:
|
||||
raise BananaError(
|
||||
"Security precaution: longer than %d bytes worth of prefix"
|
||||
% (self.prefixLimit,)
|
||||
)
|
||||
if typebyte == LIST:
|
||||
num = b1282int(num)
|
||||
if num > SIZE_LIMIT:
|
||||
raise BananaError("Security precaution: List too long.")
|
||||
listStack.append((num, []))
|
||||
buffer = rest
|
||||
elif typebyte == STRING:
|
||||
num = b1282int(num)
|
||||
if num > SIZE_LIMIT:
|
||||
raise BananaError("Security precaution: String too long.")
|
||||
if len(rest) >= num:
|
||||
buffer = rest[num:]
|
||||
gotItem(rest[:num])
|
||||
else:
|
||||
return
|
||||
elif typebyte == INT:
|
||||
buffer = rest
|
||||
num = b1282int(num)
|
||||
gotItem(num)
|
||||
elif typebyte == LONGINT:
|
||||
buffer = rest
|
||||
num = b1282int(num)
|
||||
gotItem(num)
|
||||
elif typebyte == LONGNEG:
|
||||
buffer = rest
|
||||
num = b1282int(num)
|
||||
gotItem(-num)
|
||||
elif typebyte == NEG:
|
||||
buffer = rest
|
||||
num = -b1282int(num)
|
||||
gotItem(num)
|
||||
elif typebyte == VOCAB:
|
||||
buffer = rest
|
||||
num = b1282int(num)
|
||||
item = self.incomingVocabulary[num]
|
||||
if self.currentDialect == b"pb":
|
||||
# the sender issues VOCAB only for dialect pb
|
||||
gotItem(item)
|
||||
else:
|
||||
raise NotImplementedError(f"Invalid item for pb protocol {item!r}")
|
||||
elif typebyte == FLOAT:
|
||||
if len(rest) >= 8:
|
||||
buffer = rest[8:]
|
||||
gotItem(struct.unpack("!d", rest[:8])[0])
|
||||
else:
|
||||
return
|
||||
else:
|
||||
raise NotImplementedError(f"Invalid Type Byte {typebyte!r}")
|
||||
while listStack and (len(listStack[-1][1]) == listStack[-1][0]):
|
||||
item = listStack.pop()[1]
|
||||
gotItem(item)
|
||||
self.buffer = b""
|
||||
|
||||
def expressionReceived(self, lst):
|
||||
"""Called when an expression (list, string, or int) is received."""
|
||||
raise NotImplementedError()
|
||||
|
||||
outgoingVocabulary = {
|
||||
# Jelly Data Types
|
||||
b"None": 1,
|
||||
b"class": 2,
|
||||
b"dereference": 3,
|
||||
b"reference": 4,
|
||||
b"dictionary": 5,
|
||||
b"function": 6,
|
||||
b"instance": 7,
|
||||
b"list": 8,
|
||||
b"module": 9,
|
||||
b"persistent": 10,
|
||||
b"tuple": 11,
|
||||
b"unpersistable": 12,
|
||||
# PB Data Types
|
||||
b"copy": 13,
|
||||
b"cache": 14,
|
||||
b"cached": 15,
|
||||
b"remote": 16,
|
||||
b"local": 17,
|
||||
b"lcache": 18,
|
||||
# PB Protocol Messages
|
||||
b"version": 19,
|
||||
b"login": 20,
|
||||
b"password": 21,
|
||||
b"challenge": 22,
|
||||
b"logged_in": 23,
|
||||
b"not_logged_in": 24,
|
||||
b"cachemessage": 25,
|
||||
b"message": 26,
|
||||
b"answer": 27,
|
||||
b"error": 28,
|
||||
b"decref": 29,
|
||||
b"decache": 30,
|
||||
b"uncache": 31,
|
||||
}
|
||||
|
||||
incomingVocabulary = {}
|
||||
for k, v in outgoingVocabulary.items():
|
||||
incomingVocabulary[v] = k
|
||||
|
||||
def __init__(self, isClient=1):
|
||||
self.listStack = []
|
||||
self.outgoingSymbols = copy.copy(self.outgoingVocabulary)
|
||||
self.outgoingSymbolCount = 0
|
||||
self.isClient = isClient
|
||||
|
||||
def sendEncoded(self, obj):
|
||||
"""
|
||||
Send the encoded representation of the given object:
|
||||
|
||||
@param obj: An object to encode and send.
|
||||
|
||||
@raise BananaError: If the given object is not an instance of one of
|
||||
the types supported by Banana.
|
||||
|
||||
@return: L{None}
|
||||
"""
|
||||
encodeStream = BytesIO()
|
||||
self._encode(obj, encodeStream.write)
|
||||
value = encodeStream.getvalue()
|
||||
self.transport.write(value)
|
||||
|
||||
def _encode(self, obj, write):
|
||||
if isinstance(obj, (list, tuple)):
|
||||
if len(obj) > SIZE_LIMIT:
|
||||
raise BananaError("list/tuple is too long to send (%d)" % (len(obj),))
|
||||
int2b128(len(obj), write)
|
||||
write(LIST)
|
||||
for elem in obj:
|
||||
self._encode(elem, write)
|
||||
elif isinstance(obj, int):
|
||||
if obj < self._smallestLongInt or obj > self._largestLongInt:
|
||||
raise BananaError("int is too large to send (%d)" % (obj,))
|
||||
if obj < self._smallestInt:
|
||||
int2b128(-obj, write)
|
||||
write(LONGNEG)
|
||||
elif obj < 0:
|
||||
int2b128(-obj, write)
|
||||
write(NEG)
|
||||
elif obj <= self._largestInt:
|
||||
int2b128(obj, write)
|
||||
write(INT)
|
||||
else:
|
||||
int2b128(obj, write)
|
||||
write(LONGINT)
|
||||
elif isinstance(obj, float):
|
||||
write(FLOAT)
|
||||
write(struct.pack("!d", obj))
|
||||
elif isinstance(obj, bytes):
|
||||
# TODO: an API for extending banana...
|
||||
if self.currentDialect == b"pb" and obj in self.outgoingSymbols:
|
||||
symbolID = self.outgoingSymbols[obj]
|
||||
int2b128(symbolID, write)
|
||||
write(VOCAB)
|
||||
else:
|
||||
if len(obj) > SIZE_LIMIT:
|
||||
raise BananaError(
|
||||
"byte string is too long to send (%d)" % (len(obj),)
|
||||
)
|
||||
int2b128(len(obj), write)
|
||||
write(STRING)
|
||||
write(obj)
|
||||
else:
|
||||
raise BananaError(
|
||||
"Banana cannot send {} objects: {!r}".format(
|
||||
fullyQualifiedName(type(obj)), obj
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# For use from the interactive interpreter
|
||||
_i = Banana()
|
||||
_i.connectionMade()
|
||||
_i._selectDialect(b"none")
|
||||
|
||||
|
||||
def encode(lst):
|
||||
"""Encode a list s-expression."""
|
||||
encodeStream = BytesIO()
|
||||
_i.transport = encodeStream
|
||||
_i.sendEncoded(lst)
|
||||
return encodeStream.getvalue()
|
||||
|
||||
|
||||
def decode(st):
|
||||
"""
|
||||
Decode a banana-encoded string.
|
||||
"""
|
||||
l = []
|
||||
_i.expressionReceived = l.append
|
||||
try:
|
||||
_i.dataReceived(st)
|
||||
finally:
|
||||
_i.buffer = b""
|
||||
del _i.expressionReceived
|
||||
return l[0]
|
||||
651
.venv/lib/python3.12/site-packages/twisted/spread/flavors.py
Normal file
651
.venv/lib/python3.12/site-packages/twisted/spread/flavors.py
Normal file
@@ -0,0 +1,651 @@
|
||||
# -*- test-case-name: twisted.spread.test.test_pb -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
This module represents flavors of remotely accessible objects.
|
||||
|
||||
Currently this is only objects accessible through Perspective Broker, but will
|
||||
hopefully encompass all forms of remote access which can emulate subsets of PB
|
||||
(such as XMLRPC or SOAP).
|
||||
|
||||
Future Plans: Optimization. Exploitation of new-style object model.
|
||||
Optimizations to this module should not affect external-use semantics at all,
|
||||
but may have a small impact on users who subclass and override methods.
|
||||
|
||||
@author: Glyph Lefkowitz
|
||||
"""
|
||||
|
||||
|
||||
# NOTE: this module should NOT import pb; it is supposed to be a module which
|
||||
# abstractly defines remotely accessible types. Many of these types expect to
|
||||
# be serialized by Jelly, but they ought to be accessible through other
|
||||
# mechanisms (like XMLRPC)
|
||||
|
||||
import sys
|
||||
|
||||
from zope.interface import Interface, implementer
|
||||
|
||||
from twisted.python import log, reflect
|
||||
from twisted.python.compat import cmp, comparable
|
||||
from .jelly import (
|
||||
Jellyable,
|
||||
Unjellyable,
|
||||
_createBlank,
|
||||
getInstanceState,
|
||||
setInstanceState,
|
||||
setUnjellyableFactoryForClass,
|
||||
setUnjellyableForClass,
|
||||
setUnjellyableForClassTree,
|
||||
unjellyableRegistry,
|
||||
)
|
||||
|
||||
# compatibility
|
||||
setCopierForClass = setUnjellyableForClass
|
||||
setCopierForClassTree = setUnjellyableForClassTree
|
||||
setFactoryForClass = setUnjellyableFactoryForClass
|
||||
copyTags = unjellyableRegistry
|
||||
|
||||
copy_atom = b"copy"
|
||||
cache_atom = b"cache"
|
||||
cached_atom = b"cached"
|
||||
remote_atom = b"remote"
|
||||
|
||||
|
||||
class NoSuchMethod(AttributeError):
|
||||
"""Raised if there is no such remote method"""
|
||||
|
||||
|
||||
class IPBRoot(Interface):
|
||||
"""Factory for root Referenceable objects for PB servers."""
|
||||
|
||||
def rootObject(broker):
|
||||
"""Return root Referenceable for broker."""
|
||||
|
||||
|
||||
class Serializable(Jellyable):
|
||||
"""An object that can be passed remotely.
|
||||
|
||||
I am a style of object which can be serialized by Perspective
|
||||
Broker. Objects which wish to be referenceable or copied remotely
|
||||
have to subclass Serializable. However, clients of Perspective
|
||||
Broker will probably not want to directly subclass Serializable; the
|
||||
Flavors of transferable objects are listed below.
|
||||
|
||||
What it means to be \"Serializable\" is that an object can be
|
||||
passed to or returned from a remote method. Certain basic types
|
||||
(dictionaries, lists, tuples, numbers, strings) are serializable by
|
||||
default; however, classes need to choose a specific serialization
|
||||
style: L{Referenceable}, L{Viewable}, L{Copyable} or L{Cacheable}.
|
||||
|
||||
You may also pass C{[lists, dictionaries, tuples]} of L{Serializable}
|
||||
instances to or return them from remote methods, as many levels deep
|
||||
as you like.
|
||||
"""
|
||||
|
||||
def processUniqueID(self):
|
||||
"""Return an ID which uniquely represents this object for this process.
|
||||
|
||||
By default, this uses the 'id' builtin, but can be overridden to
|
||||
indicate that two values are identity-equivalent (such as proxies
|
||||
for the same object).
|
||||
"""
|
||||
|
||||
return id(self)
|
||||
|
||||
|
||||
class Referenceable(Serializable):
|
||||
perspective = None
|
||||
"""I am an object sent remotely as a direct reference.
|
||||
|
||||
When one of my subclasses is sent as an argument to or returned
|
||||
from a remote method call, I will be serialized by default as a
|
||||
direct reference.
|
||||
|
||||
This means that the peer will be able to call methods on me;
|
||||
a method call xxx() from my peer will be resolved to methods
|
||||
of the name remote_xxx.
|
||||
"""
|
||||
|
||||
def remoteMessageReceived(self, broker, message, args, kw):
|
||||
"""A remote message has been received. Dispatch it appropriately.
|
||||
|
||||
The default implementation is to dispatch to a method called
|
||||
'remote_messagename' and call it with the same arguments.
|
||||
"""
|
||||
args = broker.unserialize(args)
|
||||
kw = broker.unserialize(kw)
|
||||
# Need this to interoperate with Python 2 clients
|
||||
# which may try to send use keywords where keys are of type
|
||||
# bytes.
|
||||
if [key for key in kw.keys() if isinstance(key, bytes)]:
|
||||
kw = {k.decode("utf8"): v for k, v in kw.items()}
|
||||
|
||||
if not isinstance(message, str):
|
||||
message = message.decode("utf8")
|
||||
|
||||
method = getattr(self, "remote_%s" % message, None)
|
||||
if method is None:
|
||||
raise NoSuchMethod(f"No such method: remote_{message}")
|
||||
try:
|
||||
state = method(*args, **kw)
|
||||
except TypeError:
|
||||
log.msg(f"{method} didn't accept {args} and {kw}")
|
||||
raise
|
||||
return broker.serialize(state, self.perspective)
|
||||
|
||||
def jellyFor(self, jellier):
|
||||
"""(internal)
|
||||
|
||||
Return a tuple which will be used as the s-expression to
|
||||
serialize this to a peer.
|
||||
"""
|
||||
|
||||
return [b"remote", jellier.invoker.registerReference(self)]
|
||||
|
||||
|
||||
@implementer(IPBRoot)
|
||||
class Root(Referenceable):
|
||||
"""I provide a root object to L{pb.Broker}s for a L{pb.PBClientFactory} or
|
||||
L{pb.PBServerFactory}.
|
||||
|
||||
When a factory produces a L{pb.Broker}, it supplies that
|
||||
L{pb.Broker} with an object named \"root\". That object is obtained
|
||||
by calling my rootObject method.
|
||||
"""
|
||||
|
||||
def rootObject(self, broker):
|
||||
"""A factory is requesting to publish me as a root object.
|
||||
|
||||
When a factory is sending me as the root object, this
|
||||
method will be invoked to allow per-broker versions of an
|
||||
object. By default I return myself.
|
||||
"""
|
||||
return self
|
||||
|
||||
|
||||
class ViewPoint(Referenceable):
|
||||
"""
|
||||
I act as an indirect reference to an object accessed through a
|
||||
L{pb.IPerspective}.
|
||||
|
||||
Simply put, I combine an object with a perspective so that when a
|
||||
peer calls methods on the object I refer to, the method will be
|
||||
invoked with that perspective as a first argument, so that it can
|
||||
know who is calling it.
|
||||
|
||||
While L{Viewable} objects will be converted to ViewPoints by default
|
||||
when they are returned from or sent as arguments to a remote
|
||||
method, any object may be manually proxied as well. (XXX: Now that
|
||||
this class is no longer named C{Proxy}, this is the only occurrence
|
||||
of the term 'proxied' in this docstring, and may be unclear.)
|
||||
|
||||
This can be useful when dealing with L{pb.IPerspective}s, L{Copyable}s,
|
||||
and L{Cacheable}s. It is legal to implement a method as such on
|
||||
a perspective::
|
||||
|
||||
| def perspective_getViewPointForOther(self, name):
|
||||
| defr = self.service.getPerspectiveRequest(name)
|
||||
| defr.addCallbacks(lambda x, self=self: ViewPoint(self, x), log.msg)
|
||||
| return defr
|
||||
|
||||
This will allow you to have references to Perspective objects in two
|
||||
different ways. One is through the initial 'attach' call -- each
|
||||
peer will have a L{pb.RemoteReference} to their perspective directly. The
|
||||
other is through this method; each peer can get a L{pb.RemoteReference} to
|
||||
all other perspectives in the service; but that L{pb.RemoteReference} will
|
||||
be to a L{ViewPoint}, not directly to the object.
|
||||
|
||||
The practical offshoot of this is that you can implement 2 varieties
|
||||
of remotely callable methods on this Perspective; view_xxx and
|
||||
C{perspective_xxx}. C{view_xxx} methods will follow the rules for
|
||||
ViewPoint methods (see ViewPoint.L{remoteMessageReceived}), and
|
||||
C{perspective_xxx} methods will follow the rules for Perspective
|
||||
methods.
|
||||
"""
|
||||
|
||||
def __init__(self, perspective, object):
|
||||
"""Initialize me with a Perspective and an Object."""
|
||||
self.perspective = perspective
|
||||
self.object = object
|
||||
|
||||
def processUniqueID(self):
|
||||
"""Return an ID unique to a proxy for this perspective+object combination."""
|
||||
return (id(self.perspective), id(self.object))
|
||||
|
||||
def remoteMessageReceived(self, broker, message, args, kw):
|
||||
"""A remote message has been received. Dispatch it appropriately.
|
||||
|
||||
The default implementation is to dispatch to a method called
|
||||
'C{view_messagename}' to my Object and call it on my object with
|
||||
the same arguments, modified by inserting my Perspective as
|
||||
the first argument.
|
||||
"""
|
||||
args = broker.unserialize(args, self.perspective)
|
||||
kw = broker.unserialize(kw, self.perspective)
|
||||
|
||||
if not isinstance(message, str):
|
||||
message = message.decode("utf8")
|
||||
|
||||
method = getattr(self.object, "view_%s" % message)
|
||||
try:
|
||||
state = method(*(self.perspective,) + args, **kw)
|
||||
except TypeError:
|
||||
log.msg(f"{method} didn't accept {args} and {kw}")
|
||||
raise
|
||||
rv = broker.serialize(state, self.perspective, method, args, kw)
|
||||
return rv
|
||||
|
||||
|
||||
class Viewable(Serializable):
|
||||
"""I will be converted to a L{ViewPoint} when passed to or returned from a remote method.
|
||||
|
||||
The beginning of a peer's interaction with a PB Service is always
|
||||
through a perspective. However, if a C{perspective_xxx} method returns
|
||||
a Viewable, it will be serialized to the peer as a response to that
|
||||
method.
|
||||
"""
|
||||
|
||||
def jellyFor(self, jellier):
|
||||
"""Serialize a L{ViewPoint} for me and the perspective of the given broker."""
|
||||
return ViewPoint(jellier.invoker.serializingPerspective, self).jellyFor(jellier)
|
||||
|
||||
|
||||
class Copyable(Serializable):
|
||||
"""Subclass me to get copied each time you are returned from or passed to a remote method.
|
||||
|
||||
When I am returned from or passed to a remote method call, I will be
|
||||
converted into data via a set of callbacks (see my methods for more
|
||||
info). That data will then be serialized using Jelly, and sent to
|
||||
the peer.
|
||||
|
||||
The peer will then look up the type to represent this with; see
|
||||
L{RemoteCopy} for details.
|
||||
"""
|
||||
|
||||
def getStateToCopy(self):
|
||||
"""Gather state to send when I am serialized for a peer.
|
||||
|
||||
I will default to returning self.__dict__. Override this to
|
||||
customize this behavior.
|
||||
"""
|
||||
|
||||
return self.__dict__
|
||||
|
||||
def getStateToCopyFor(self, perspective):
|
||||
"""
|
||||
Gather state to send when I am serialized for a particular
|
||||
perspective.
|
||||
|
||||
I will default to calling L{getStateToCopy}. Override this to
|
||||
customize this behavior.
|
||||
"""
|
||||
|
||||
return self.getStateToCopy()
|
||||
|
||||
def getTypeToCopy(self):
|
||||
"""Determine what type tag to send for me.
|
||||
|
||||
By default, send the string representation of my class
|
||||
(package.module.Class); normally this is adequate, but
|
||||
you may override this to change it.
|
||||
"""
|
||||
|
||||
return reflect.qual(self.__class__).encode("utf-8")
|
||||
|
||||
def getTypeToCopyFor(self, perspective):
|
||||
"""Determine what type tag to send for me.
|
||||
|
||||
By default, defer to self.L{getTypeToCopy}() normally this is
|
||||
adequate, but you may override this to change it.
|
||||
"""
|
||||
|
||||
return self.getTypeToCopy()
|
||||
|
||||
def jellyFor(self, jellier):
|
||||
"""Assemble type tag and state to copy for this broker.
|
||||
|
||||
This will call L{getTypeToCopyFor} and L{getStateToCopy}, and
|
||||
return an appropriate s-expression to represent me.
|
||||
"""
|
||||
|
||||
if jellier.invoker is None:
|
||||
return getInstanceState(self, jellier)
|
||||
p = jellier.invoker.serializingPerspective
|
||||
t = self.getTypeToCopyFor(p)
|
||||
state = self.getStateToCopyFor(p)
|
||||
sxp = jellier.prepare(self)
|
||||
sxp.extend([t, jellier.jelly(state)])
|
||||
return jellier.preserve(self, sxp)
|
||||
|
||||
|
||||
class Cacheable(Copyable):
|
||||
"""A cached instance.
|
||||
|
||||
This means that it's copied; but there is some logic to make sure
|
||||
that it's only copied once. Additionally, when state is retrieved,
|
||||
it is passed a "proto-reference" to the state as it will exist on
|
||||
the client.
|
||||
|
||||
XXX: The documentation for this class needs work, but it's the most
|
||||
complex part of PB and it is inherently difficult to explain.
|
||||
"""
|
||||
|
||||
def getStateToCacheAndObserveFor(self, perspective, observer):
|
||||
"""
|
||||
Get state to cache on the client and client-cache reference
|
||||
to observe locally.
|
||||
|
||||
This is similar to getStateToCopyFor, but it additionally
|
||||
passes in a reference to the client-side RemoteCache instance
|
||||
that will be created when it is unserialized. This allows
|
||||
Cacheable instances to keep their RemoteCaches up to date when
|
||||
they change, such that no changes can occur between the point
|
||||
at which the state is initially copied and the client receives
|
||||
it that are not propagated.
|
||||
"""
|
||||
|
||||
return self.getStateToCopyFor(perspective)
|
||||
|
||||
def jellyFor(self, jellier):
|
||||
"""Return an appropriate tuple to serialize me.
|
||||
|
||||
Depending on whether this broker has cached me or not, this may
|
||||
return either a full state or a reference to an existing cache.
|
||||
"""
|
||||
if jellier.invoker is None:
|
||||
return getInstanceState(self, jellier)
|
||||
luid = jellier.invoker.cachedRemotelyAs(self, 1)
|
||||
if luid is None:
|
||||
luid = jellier.invoker.cacheRemotely(self)
|
||||
p = jellier.invoker.serializingPerspective
|
||||
type_ = self.getTypeToCopyFor(p)
|
||||
observer = RemoteCacheObserver(jellier.invoker, self, p)
|
||||
state = self.getStateToCacheAndObserveFor(p, observer)
|
||||
l = jellier.prepare(self)
|
||||
jstate = jellier.jelly(state)
|
||||
l.extend([type_, luid, jstate])
|
||||
return jellier.preserve(self, l)
|
||||
else:
|
||||
return cached_atom, luid
|
||||
|
||||
def stoppedObserving(self, perspective, observer):
|
||||
"""This method is called when a client has stopped observing me.
|
||||
|
||||
The 'observer' argument is the same as that passed in to
|
||||
getStateToCacheAndObserveFor.
|
||||
"""
|
||||
|
||||
|
||||
class RemoteCopy(Unjellyable):
|
||||
"""I am a remote copy of a Copyable object.
|
||||
|
||||
When the state from a L{Copyable} object is received, an instance will
|
||||
be created based on the copy tags table (see setUnjellyableForClass) and
|
||||
sent the L{setCopyableState} message. I provide a reasonable default
|
||||
implementation of that message; subclass me if you wish to serve as
|
||||
a copier for remote data.
|
||||
|
||||
NOTE: copiers are invoked with no arguments. Do not implement a
|
||||
constructor which requires args in a subclass of L{RemoteCopy}!
|
||||
"""
|
||||
|
||||
def setCopyableState(self, state):
|
||||
"""I will be invoked with the state to copy locally.
|
||||
|
||||
'state' is the data returned from the remote object's
|
||||
'getStateToCopyFor' method, which will often be the remote
|
||||
object's dictionary (or a filtered approximation of it depending
|
||||
on my peer's perspective).
|
||||
"""
|
||||
if not state:
|
||||
state = {}
|
||||
state = {
|
||||
x.decode("utf8") if isinstance(x, bytes) else x: y for x, y in state.items()
|
||||
}
|
||||
self.__dict__ = state
|
||||
|
||||
def unjellyFor(self, unjellier, jellyList):
|
||||
if unjellier.invoker is None:
|
||||
return setInstanceState(self, unjellier, jellyList)
|
||||
self.setCopyableState(unjellier.unjelly(jellyList[1]))
|
||||
return self
|
||||
|
||||
|
||||
class RemoteCache(RemoteCopy, Serializable):
|
||||
"""A cache is a local representation of a remote L{Cacheable} object.
|
||||
|
||||
This represents the last known state of this object. It may
|
||||
also have methods invoked on it -- in order to update caches,
|
||||
the cached class generates a L{pb.RemoteReference} to this object as
|
||||
it is originally sent.
|
||||
|
||||
Much like copy, I will be invoked with no arguments. Do not
|
||||
implement a constructor that requires arguments in one of my
|
||||
subclasses.
|
||||
"""
|
||||
|
||||
def remoteMessageReceived(self, broker, message, args, kw):
|
||||
"""A remote message has been received. Dispatch it appropriately.
|
||||
|
||||
The default implementation is to dispatch to a method called
|
||||
'C{observe_messagename}' and call it on my with the same arguments.
|
||||
"""
|
||||
if not isinstance(message, str):
|
||||
message = message.decode("utf8")
|
||||
|
||||
args = broker.unserialize(args)
|
||||
kw = broker.unserialize(kw)
|
||||
method = getattr(self, "observe_%s" % message)
|
||||
try:
|
||||
state = method(*args, **kw)
|
||||
except TypeError:
|
||||
log.msg(f"{method} didn't accept {args} and {kw}")
|
||||
raise
|
||||
return broker.serialize(state, None, method, args, kw)
|
||||
|
||||
def jellyFor(self, jellier):
|
||||
"""serialize me (only for the broker I'm for) as the original cached reference"""
|
||||
if jellier.invoker is None:
|
||||
return getInstanceState(self, jellier)
|
||||
assert (
|
||||
jellier.invoker is self.broker
|
||||
), "You cannot exchange cached proxies between brokers."
|
||||
return b"lcache", self.luid
|
||||
|
||||
def unjellyFor(self, unjellier, jellyList):
|
||||
if unjellier.invoker is None:
|
||||
return setInstanceState(self, unjellier, jellyList)
|
||||
self.broker = unjellier.invoker
|
||||
self.luid = jellyList[1]
|
||||
borgCopy = self._borgify()
|
||||
# XXX questionable whether this was a good design idea...
|
||||
init = getattr(borgCopy, "__init__", None)
|
||||
if init:
|
||||
init()
|
||||
unjellier.invoker.cacheLocally(jellyList[1], self)
|
||||
borgCopy.setCopyableState(unjellier.unjelly(jellyList[2]))
|
||||
# Might have changed due to setCopyableState method; we'll assume that
|
||||
# it's bad form to do so afterwards.
|
||||
self.__dict__ = borgCopy.__dict__
|
||||
# chomp, chomp -- some existing code uses "self.__dict__ =", some uses
|
||||
# "__dict__.update". This is here in order to handle both cases.
|
||||
self.broker = unjellier.invoker
|
||||
self.luid = jellyList[1]
|
||||
return borgCopy
|
||||
|
||||
## def __really_del__(self):
|
||||
## """Final finalization call, made after all remote references have been lost.
|
||||
## """
|
||||
|
||||
def __cmp__(self, other):
|
||||
"""Compare me [to another RemoteCache."""
|
||||
if isinstance(other, self.__class__):
|
||||
return cmp(id(self.__dict__), id(other.__dict__))
|
||||
else:
|
||||
return cmp(id(self.__dict__), other)
|
||||
|
||||
def __hash__(self):
|
||||
"""Hash me."""
|
||||
return int(id(self.__dict__) % sys.maxsize)
|
||||
|
||||
broker = None
|
||||
luid = None
|
||||
|
||||
def __del__(self):
|
||||
"""Do distributed reference counting on finalize."""
|
||||
try:
|
||||
# log.msg( ' --- decache: %s %s' % (self, self.luid) )
|
||||
if self.broker:
|
||||
self.broker.decCacheRef(self.luid)
|
||||
except BaseException:
|
||||
log.deferr()
|
||||
|
||||
def _borgify(self):
|
||||
"""
|
||||
Create a new object that shares its state (i.e. its C{__dict__}) and
|
||||
type with this object, but does not share its identity.
|
||||
|
||||
This is an instance of U{the Borg design pattern
|
||||
<https://code.activestate.com/recipes/66531/>} originally described by
|
||||
Alex Martelli, but unlike the example given there, this is not a
|
||||
replacement for a Singleton. Instead, it is for lifecycle tracking
|
||||
(and distributed garbage collection). The purpose of these separate
|
||||
objects is to have a separate object tracking each application-level
|
||||
reference to the root L{RemoteCache} object being tracked by the
|
||||
broker, and to have their C{__del__} methods be invoked.
|
||||
|
||||
This may be achievable via a weak value dictionary to track the root
|
||||
L{RemoteCache} instances instead, but this implementation strategy
|
||||
predates the availability of weak references in Python.
|
||||
|
||||
@return: The new instance.
|
||||
@rtype: C{self.__class__}
|
||||
"""
|
||||
blank = _createBlank(self.__class__)
|
||||
blank.__dict__ = self.__dict__
|
||||
return blank
|
||||
|
||||
|
||||
def unjellyCached(unjellier, unjellyList):
|
||||
luid = unjellyList[1]
|
||||
return unjellier.invoker.cachedLocallyAs(luid)._borgify()
|
||||
|
||||
|
||||
setUnjellyableForClass("cached", unjellyCached)
|
||||
|
||||
|
||||
def unjellyLCache(unjellier, unjellyList):
|
||||
luid = unjellyList[1]
|
||||
obj = unjellier.invoker.remotelyCachedForLUID(luid)
|
||||
return obj
|
||||
|
||||
|
||||
setUnjellyableForClass("lcache", unjellyLCache)
|
||||
|
||||
|
||||
def unjellyLocal(unjellier, unjellyList):
|
||||
obj = unjellier.invoker.localObjectForID(unjellyList[1])
|
||||
return obj
|
||||
|
||||
|
||||
setUnjellyableForClass("local", unjellyLocal)
|
||||
|
||||
|
||||
@comparable
|
||||
class RemoteCacheMethod:
|
||||
"""A method on a reference to a L{RemoteCache}."""
|
||||
|
||||
def __init__(self, name, broker, cached, perspective):
|
||||
"""(internal) initialize."""
|
||||
self.name = name
|
||||
self.broker = broker
|
||||
self.perspective = perspective
|
||||
self.cached = cached
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp((self.name, self.broker, self.perspective, self.cached), other)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.broker, self.perspective, self.cached))
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
"""(internal) action method."""
|
||||
cacheID = self.broker.cachedRemotelyAs(self.cached)
|
||||
if cacheID is None:
|
||||
from twisted.spread.pb import ProtocolError
|
||||
|
||||
raise ProtocolError(
|
||||
"You can't call a cached method when the object hasn't been given to the peer yet."
|
||||
)
|
||||
return self.broker._sendMessage(
|
||||
b"cache", self.perspective, cacheID, self.name, args, kw
|
||||
)
|
||||
|
||||
|
||||
@comparable
|
||||
class RemoteCacheObserver:
|
||||
"""I am a reverse-reference to the peer's L{RemoteCache}.
|
||||
|
||||
I am generated automatically when a cache is serialized. I
|
||||
represent a reference to the client's L{RemoteCache} object that
|
||||
will represent a particular L{Cacheable}; I am the additional
|
||||
object passed to getStateToCacheAndObserveFor.
|
||||
"""
|
||||
|
||||
def __init__(self, broker, cached, perspective):
|
||||
"""(internal) Initialize me.
|
||||
|
||||
@param broker: a L{pb.Broker} instance.
|
||||
|
||||
@param cached: a L{Cacheable} instance that this L{RemoteCacheObserver}
|
||||
corresponds to.
|
||||
|
||||
@param perspective: a reference to the perspective who is observing this.
|
||||
"""
|
||||
|
||||
self.broker = broker
|
||||
self.cached = cached
|
||||
self.perspective = perspective
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<RemoteCacheObserver({}, {}, {}) at {}>".format(
|
||||
self.broker,
|
||||
self.cached,
|
||||
self.perspective,
|
||||
id(self),
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
"""Generate a hash unique to all L{RemoteCacheObserver}s for this broker/perspective/cached triplet"""
|
||||
|
||||
return (
|
||||
(hash(self.broker) % 2**10)
|
||||
+ (hash(self.perspective) % 2**10)
|
||||
+ (hash(self.cached) % 2**10)
|
||||
)
|
||||
|
||||
def __cmp__(self, other):
|
||||
"""Compare me to another L{RemoteCacheObserver}."""
|
||||
|
||||
return cmp((self.broker, self.perspective, self.cached), other)
|
||||
|
||||
def callRemote(self, _name, *args, **kw):
|
||||
"""(internal) action method."""
|
||||
cacheID = self.broker.cachedRemotelyAs(self.cached)
|
||||
if isinstance(_name, str):
|
||||
_name = _name.encode("utf-8")
|
||||
if cacheID is None:
|
||||
from twisted.spread.pb import ProtocolError
|
||||
|
||||
raise ProtocolError(
|
||||
"You can't call a cached method when the "
|
||||
"object hasn't been given to the peer yet."
|
||||
)
|
||||
return self.broker._sendMessage(
|
||||
b"cache", self.perspective, cacheID, _name, args, kw
|
||||
)
|
||||
|
||||
def remoteMethod(self, key):
|
||||
"""Get a L{pb.RemoteMethod} for this key."""
|
||||
return RemoteCacheMethod(key, self.broker, self.cached, self.perspective)
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted Spread Interfaces.
|
||||
"""
|
||||
|
||||
from zope.interface import Interface
|
||||
|
||||
|
||||
class IJellyable(Interface):
|
||||
def jellyFor(jellier):
|
||||
"""
|
||||
Jelly myself for jellier.
|
||||
"""
|
||||
|
||||
|
||||
class IUnjellyable(Interface):
|
||||
def unjellyFor(jellier, jellyList):
|
||||
"""
|
||||
Unjelly myself for the jellier.
|
||||
|
||||
@param jellier: A stateful object which exists for the lifetime of a
|
||||
single call to L{unjelly}.
|
||||
|
||||
@param jellyList: The C{list} which represents the jellied state of the
|
||||
object to be unjellied.
|
||||
|
||||
@return: The object which results from unjellying.
|
||||
"""
|
||||
1092
.venv/lib/python3.12/site-packages/twisted/spread/jelly.py
Normal file
1092
.venv/lib/python3.12/site-packages/twisted/spread/jelly.py
Normal file
File diff suppressed because it is too large
Load Diff
1682
.venv/lib/python3.12/site-packages/twisted/spread/pb.py
Normal file
1682
.venv/lib/python3.12/site-packages/twisted/spread/pb.py
Normal file
File diff suppressed because it is too large
Load Diff
144
.venv/lib/python3.12/site-packages/twisted/spread/publish.py
Normal file
144
.venv/lib/python3.12/site-packages/twisted/spread/publish.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# -*- test-case-name: twisted.spread.test.test_pb -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Persistently cached objects for PB.
|
||||
|
||||
Maintainer: Glyph Lefkowitz
|
||||
|
||||
Future Plans: None known.
|
||||
"""
|
||||
|
||||
|
||||
import time
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.spread import banana, flavors, jelly
|
||||
|
||||
|
||||
class Publishable(flavors.Cacheable):
|
||||
"""An object whose cached state persists across sessions."""
|
||||
|
||||
def __init__(self, publishedID):
|
||||
self.republish()
|
||||
self.publishedID = publishedID
|
||||
|
||||
def republish(self):
|
||||
"""Set the timestamp to current and (TODO) update all observers."""
|
||||
self.timestamp = time.time()
|
||||
|
||||
def view_getStateToPublish(self, perspective):
|
||||
"(internal)"
|
||||
return self.getStateToPublishFor(perspective)
|
||||
|
||||
def getStateToPublishFor(self, perspective):
|
||||
"""Implement me to special-case your state for a perspective."""
|
||||
return self.getStateToPublish()
|
||||
|
||||
def getStateToPublish(self):
|
||||
"""Implement me to return state to copy as part of the publish phase."""
|
||||
raise NotImplementedError("%s.getStateToPublishFor" % self.__class__)
|
||||
|
||||
def getStateToCacheAndObserveFor(self, perspective, observer):
|
||||
"""Get all necessary metadata to keep a clientside cache."""
|
||||
if perspective:
|
||||
pname = perspective.perspectiveName
|
||||
sname = perspective.getService().serviceName
|
||||
else:
|
||||
pname = "None"
|
||||
sname = "None"
|
||||
|
||||
return {
|
||||
"remote": flavors.ViewPoint(perspective, self),
|
||||
"publishedID": self.publishedID,
|
||||
"perspective": pname,
|
||||
"service": sname,
|
||||
"timestamp": self.timestamp,
|
||||
}
|
||||
|
||||
|
||||
class RemotePublished(flavors.RemoteCache):
|
||||
"""The local representation of remote Publishable object."""
|
||||
|
||||
isActivated = 0
|
||||
_wasCleanWhenLoaded = 0
|
||||
|
||||
def getFileName(self, ext="pub"):
|
||||
return "{}-{}-{}.{}".format(
|
||||
self.service,
|
||||
self.perspective,
|
||||
str(self.publishedID),
|
||||
ext,
|
||||
)
|
||||
|
||||
def setCopyableState(self, state):
|
||||
self.__dict__.update(state)
|
||||
self._activationListeners = []
|
||||
try:
|
||||
with open(self.getFileName(), "rb") as dataFile:
|
||||
data = dataFile.read()
|
||||
except OSError:
|
||||
recent = 0
|
||||
else:
|
||||
newself = jelly.unjelly(banana.decode(data))
|
||||
recent = newself.timestamp == self.timestamp
|
||||
if recent:
|
||||
self._cbGotUpdate(newself.__dict__)
|
||||
self._wasCleanWhenLoaded = 1
|
||||
else:
|
||||
self.remote.callRemote("getStateToPublish").addCallbacks(self._cbGotUpdate)
|
||||
|
||||
def __getstate__(self):
|
||||
other = self.__dict__.copy()
|
||||
# Remove PB-specific attributes
|
||||
del other["broker"]
|
||||
del other["remote"]
|
||||
del other["luid"]
|
||||
# remove my own runtime-tracking stuff
|
||||
del other["_activationListeners"]
|
||||
del other["isActivated"]
|
||||
return other
|
||||
|
||||
def _cbGotUpdate(self, newState):
|
||||
self.__dict__.update(newState)
|
||||
self.isActivated = 1
|
||||
# send out notifications
|
||||
for listener in self._activationListeners:
|
||||
listener(self)
|
||||
self._activationListeners = []
|
||||
self.activated()
|
||||
with open(self.getFileName(), "wb") as dataFile:
|
||||
dataFile.write(banana.encode(jelly.jelly(self)))
|
||||
|
||||
def activated(self):
|
||||
"""Implement this method if you want to be notified when your
|
||||
publishable subclass is activated.
|
||||
"""
|
||||
|
||||
def callWhenActivated(self, callback):
|
||||
"""Externally register for notification when this publishable has received all relevant data."""
|
||||
if self.isActivated:
|
||||
callback(self)
|
||||
else:
|
||||
self._activationListeners.append(callback)
|
||||
|
||||
|
||||
def whenReady(d):
|
||||
"""
|
||||
Wrap a deferred returned from a pb method in another deferred that
|
||||
expects a RemotePublished as a result. This will allow you to wait until
|
||||
the result is really available.
|
||||
|
||||
Idiomatic usage would look like::
|
||||
|
||||
publish.whenReady(serverObject.getMeAPublishable()).addCallback(lookAtThePublishable)
|
||||
"""
|
||||
d2 = defer.Deferred()
|
||||
d.addCallbacks(_pubReady, d2.errback, callbackArgs=(d2,))
|
||||
return d2
|
||||
|
||||
|
||||
def _pubReady(result, d2):
|
||||
"(internal)"
|
||||
result.callWhenActivated(d2.callback)
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.spread}.
|
||||
"""
|
||||
@@ -0,0 +1,413 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
import sys
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
|
||||
from twisted.internet import main, protocol
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.python import failure
|
||||
from twisted.python.compat import iterbytes
|
||||
from twisted.spread import banana
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
_maxint = 9223372036854775807
|
||||
|
||||
|
||||
class MathTests(TestCase):
|
||||
def test_int2b128(self):
|
||||
funkylist = (
|
||||
list(range(0, 100))
|
||||
+ list(range(1000, 1100))
|
||||
+ list(range(1000000, 1000100))
|
||||
+ [1024**10]
|
||||
)
|
||||
for i in funkylist:
|
||||
x = BytesIO()
|
||||
banana.int2b128(i, x.write)
|
||||
v = x.getvalue()
|
||||
y = banana.b1282int(v)
|
||||
self.assertEqual(y, i)
|
||||
|
||||
|
||||
def selectDialect(protocol, dialect):
|
||||
"""
|
||||
Dictate a Banana dialect to use.
|
||||
|
||||
@param protocol: A L{banana.Banana} instance which has not yet had a
|
||||
dialect negotiated.
|
||||
|
||||
@param dialect: A L{bytes} instance naming a Banana dialect to select.
|
||||
"""
|
||||
# We can't do this the normal way by delivering bytes because other setup
|
||||
# stuff gets in the way (for example, clients and servers have incompatible
|
||||
# negotiations for this step). So use the private API to make this happen.
|
||||
protocol._selectDialect(dialect)
|
||||
|
||||
|
||||
def encode(bananaFactory, obj):
|
||||
"""
|
||||
Banana encode an object using L{banana.Banana.sendEncoded}.
|
||||
|
||||
@param bananaFactory: A no-argument callable which will return a new,
|
||||
unconnected protocol instance to use to do the encoding (this should
|
||||
most likely be a L{banana.Banana} instance).
|
||||
|
||||
@param obj: The object to encode.
|
||||
@type obj: Any type supported by Banana.
|
||||
|
||||
@return: A L{bytes} instance giving the encoded form of C{obj}.
|
||||
"""
|
||||
transport = StringTransport()
|
||||
banana = bananaFactory()
|
||||
banana.makeConnection(transport)
|
||||
transport.clear()
|
||||
|
||||
banana.sendEncoded(obj)
|
||||
return transport.value()
|
||||
|
||||
|
||||
class BananaTestBase(TestCase):
|
||||
"""
|
||||
The base for test classes. It defines commonly used things and sets up a
|
||||
connection for testing.
|
||||
"""
|
||||
|
||||
encClass = banana.Banana
|
||||
|
||||
def setUp(self):
|
||||
self.io = BytesIO()
|
||||
self.enc = self.encClass()
|
||||
self.enc.makeConnection(protocol.FileWrapper(self.io))
|
||||
selectDialect(self.enc, b"none")
|
||||
self.enc.expressionReceived = self.putResult
|
||||
self.encode = partial(encode, self.encClass)
|
||||
|
||||
def putResult(self, result):
|
||||
"""
|
||||
Store an expression received by C{self.enc}.
|
||||
|
||||
@param result: The object that was received.
|
||||
@type result: Any type supported by Banana.
|
||||
"""
|
||||
self.result = result
|
||||
|
||||
def tearDown(self):
|
||||
self.enc.connectionLost(failure.Failure(main.CONNECTION_DONE))
|
||||
del self.enc
|
||||
|
||||
|
||||
class BananaTests(BananaTestBase):
|
||||
"""
|
||||
General banana tests.
|
||||
"""
|
||||
|
||||
def test_string(self):
|
||||
self.enc.sendEncoded(b"hello")
|
||||
self.enc.dataReceived(self.io.getvalue())
|
||||
assert self.result == b"hello"
|
||||
|
||||
def test_unsupportedUnicode(self):
|
||||
"""
|
||||
Banana does not support unicode. ``Banana.sendEncoded`` raises
|
||||
``BananaError`` if called with an instance of ``unicode``.
|
||||
"""
|
||||
self._unsupportedTypeTest("hello", "builtins.str")
|
||||
|
||||
def test_unsupportedBuiltinType(self):
|
||||
"""
|
||||
Banana does not support arbitrary builtin types like L{type}.
|
||||
L{banana.Banana.sendEncoded} raises L{banana.BananaError} if called
|
||||
with an instance of L{type}.
|
||||
"""
|
||||
# type is an instance of type
|
||||
self._unsupportedTypeTest(type, "builtins.type")
|
||||
|
||||
def test_unsupportedUserType(self):
|
||||
"""
|
||||
Banana does not support arbitrary user-defined types (such as those
|
||||
defined with the ``class`` statement). ``Banana.sendEncoded`` raises
|
||||
``BananaError`` if called with an instance of such a type.
|
||||
"""
|
||||
self._unsupportedTypeTest(MathTests(), __name__ + ".MathTests")
|
||||
|
||||
def _unsupportedTypeTest(self, obj, name):
|
||||
"""
|
||||
Assert that L{banana.Banana.sendEncoded} raises L{banana.BananaError}
|
||||
if called with the given object.
|
||||
|
||||
@param obj: Some object that Banana does not support.
|
||||
@param name: The name of the type of the object.
|
||||
|
||||
@raise: The failure exception is raised if L{Banana.sendEncoded} does
|
||||
not raise L{banana.BananaError} or if the message associated with the
|
||||
exception is not formatted to include the type of the unsupported
|
||||
object.
|
||||
"""
|
||||
exc = self.assertRaises(banana.BananaError, self.enc.sendEncoded, obj)
|
||||
self.assertIn(f"Banana cannot send {name} objects", str(exc))
|
||||
|
||||
def test_int(self):
|
||||
"""
|
||||
A positive integer less than 2 ** 32 should round-trip through
|
||||
banana without changing value and should come out represented
|
||||
as an C{int} (regardless of the type which was encoded).
|
||||
"""
|
||||
self.enc.sendEncoded(10151)
|
||||
self.enc.dataReceived(self.io.getvalue())
|
||||
self.assertEqual(self.result, 10151)
|
||||
self.assertIsInstance(self.result, int)
|
||||
|
||||
def _getSmallest(self):
|
||||
# How many bytes of prefix our implementation allows
|
||||
bytes = self.enc.prefixLimit
|
||||
# How many useful bits we can extract from that based on Banana's
|
||||
# base-128 representation.
|
||||
bits = bytes * 7
|
||||
# The largest number we _should_ be able to encode
|
||||
largest = 2**bits - 1
|
||||
# The smallest number we _shouldn't_ be able to encode
|
||||
smallest = largest + 1
|
||||
return smallest
|
||||
|
||||
def test_encodeTooLargeLong(self):
|
||||
"""
|
||||
Test that a long above the implementation-specific limit is rejected
|
||||
as too large to be encoded.
|
||||
"""
|
||||
smallest = self._getSmallest()
|
||||
self.assertRaises(banana.BananaError, self.enc.sendEncoded, smallest)
|
||||
|
||||
def test_decodeTooLargeLong(self):
|
||||
"""
|
||||
Test that a long above the implementation specific limit is rejected
|
||||
as too large to be decoded.
|
||||
"""
|
||||
smallest = self._getSmallest()
|
||||
self.enc.setPrefixLimit(self.enc.prefixLimit * 2)
|
||||
self.enc.sendEncoded(smallest)
|
||||
encoded = self.io.getvalue()
|
||||
self.io.truncate(0)
|
||||
self.enc.setPrefixLimit(self.enc.prefixLimit // 2)
|
||||
|
||||
self.assertRaises(banana.BananaError, self.enc.dataReceived, encoded)
|
||||
|
||||
def _getLargest(self):
|
||||
return -self._getSmallest()
|
||||
|
||||
def test_encodeTooSmallLong(self):
|
||||
"""
|
||||
Test that a negative long below the implementation-specific limit is
|
||||
rejected as too small to be encoded.
|
||||
"""
|
||||
largest = self._getLargest()
|
||||
self.assertRaises(banana.BananaError, self.enc.sendEncoded, largest)
|
||||
|
||||
def test_decodeTooSmallLong(self):
|
||||
"""
|
||||
Test that a negative long below the implementation specific limit is
|
||||
rejected as too small to be decoded.
|
||||
"""
|
||||
largest = self._getLargest()
|
||||
self.enc.setPrefixLimit(self.enc.prefixLimit * 2)
|
||||
self.enc.sendEncoded(largest)
|
||||
encoded = self.io.getvalue()
|
||||
self.io.truncate(0)
|
||||
self.enc.setPrefixLimit(self.enc.prefixLimit // 2)
|
||||
|
||||
self.assertRaises(banana.BananaError, self.enc.dataReceived, encoded)
|
||||
|
||||
def test_integer(self):
|
||||
self.enc.sendEncoded(1015)
|
||||
self.enc.dataReceived(self.io.getvalue())
|
||||
self.assertEqual(self.result, 1015)
|
||||
|
||||
def test_negative(self):
|
||||
self.enc.sendEncoded(-1015)
|
||||
self.enc.dataReceived(self.io.getvalue())
|
||||
self.assertEqual(self.result, -1015)
|
||||
|
||||
def test_float(self):
|
||||
self.enc.sendEncoded(1015.0)
|
||||
self.enc.dataReceived(self.io.getvalue())
|
||||
self.assertEqual(self.result, 1015.0)
|
||||
|
||||
def test_list(self):
|
||||
foo = [
|
||||
1,
|
||||
2,
|
||||
[3, 4],
|
||||
[30.5, 40.2],
|
||||
5,
|
||||
[b"six", b"seven", [b"eight", 9]],
|
||||
[10],
|
||||
[],
|
||||
]
|
||||
self.enc.sendEncoded(foo)
|
||||
self.enc.dataReceived(self.io.getvalue())
|
||||
self.assertEqual(self.result, foo)
|
||||
|
||||
def test_partial(self):
|
||||
"""
|
||||
Test feeding the data byte per byte to the receiver. Normally
|
||||
data is not split.
|
||||
"""
|
||||
foo = [
|
||||
1,
|
||||
2,
|
||||
[3, 4],
|
||||
[30.5, 40.2],
|
||||
5,
|
||||
[b"six", b"seven", [b"eight", 9]],
|
||||
[10],
|
||||
# TODO: currently the C implementation's a bit buggy...
|
||||
sys.maxsize * 3,
|
||||
sys.maxsize * 2,
|
||||
sys.maxsize * -2,
|
||||
]
|
||||
self.enc.sendEncoded(foo)
|
||||
self.feed(self.io.getvalue())
|
||||
self.assertEqual(self.result, foo)
|
||||
|
||||
def feed(self, data):
|
||||
"""
|
||||
Feed the data byte per byte to the receiver.
|
||||
|
||||
@param data: The bytes to deliver.
|
||||
@type data: L{bytes}
|
||||
"""
|
||||
for byte in iterbytes(data):
|
||||
self.enc.dataReceived(byte)
|
||||
|
||||
def test_oversizedList(self):
|
||||
data = b"\x02\x01\x01\x01\x01\x80"
|
||||
# list(size=0x0101010102, about 4.3e9)
|
||||
self.assertRaises(banana.BananaError, self.feed, data)
|
||||
|
||||
def test_oversizedString(self):
|
||||
data = b"\x02\x01\x01\x01\x01\x82"
|
||||
# string(size=0x0101010102, about 4.3e9)
|
||||
self.assertRaises(banana.BananaError, self.feed, data)
|
||||
|
||||
def test_crashString(self):
|
||||
crashString = b"\x00\x00\x00\x00\x04\x80"
|
||||
# string(size=0x0400000000, about 17.2e9)
|
||||
|
||||
# cBanana would fold that into a 32-bit 'int', then try to allocate
|
||||
# a list with PyList_New(). cBanana ignored the NULL return value,
|
||||
# so it would segfault when trying to free the imaginary list.
|
||||
|
||||
# This variant doesn't segfault straight out in my environment.
|
||||
# Instead, it takes up large amounts of CPU and memory...
|
||||
# crashString = '\x00\x00\x00\x00\x01\x80'
|
||||
# print repr(crashString)
|
||||
# self.failUnlessRaises(Exception, self.enc.dataReceived, crashString)
|
||||
try:
|
||||
# should now raise MemoryError
|
||||
self.enc.dataReceived(crashString)
|
||||
except banana.BananaError:
|
||||
pass
|
||||
|
||||
def test_crashNegativeLong(self):
|
||||
# There was a bug in cBanana which relied on negating a negative integer
|
||||
# always giving a positive result, but for the lowest possible number in
|
||||
# 2s-complement arithmetic, that's not true, i.e.
|
||||
# long x = -2147483648;
|
||||
# long y = -x;
|
||||
# x == y; /* true! */
|
||||
# (assuming 32-bit longs)
|
||||
self.enc.sendEncoded(-2147483648)
|
||||
self.enc.dataReceived(self.io.getvalue())
|
||||
self.assertEqual(self.result, -2147483648)
|
||||
|
||||
def test_sizedIntegerTypes(self):
|
||||
"""
|
||||
Test that integers below the maximum C{INT} token size cutoff are
|
||||
serialized as C{INT} or C{NEG} and that larger integers are
|
||||
serialized as C{LONGINT} or C{LONGNEG}.
|
||||
"""
|
||||
baseIntIn = +2147483647
|
||||
baseNegIn = -2147483648
|
||||
|
||||
baseIntOut = b"\x7f\x7f\x7f\x07\x81"
|
||||
self.assertEqual(self.encode(baseIntIn - 2), b"\x7d" + baseIntOut)
|
||||
self.assertEqual(self.encode(baseIntIn - 1), b"\x7e" + baseIntOut)
|
||||
self.assertEqual(self.encode(baseIntIn - 0), b"\x7f" + baseIntOut)
|
||||
|
||||
baseLongIntOut = b"\x00\x00\x00\x08\x85"
|
||||
self.assertEqual(self.encode(baseIntIn + 1), b"\x00" + baseLongIntOut)
|
||||
self.assertEqual(self.encode(baseIntIn + 2), b"\x01" + baseLongIntOut)
|
||||
self.assertEqual(self.encode(baseIntIn + 3), b"\x02" + baseLongIntOut)
|
||||
|
||||
baseNegOut = b"\x7f\x7f\x7f\x07\x83"
|
||||
self.assertEqual(self.encode(baseNegIn + 2), b"\x7e" + baseNegOut)
|
||||
self.assertEqual(self.encode(baseNegIn + 1), b"\x7f" + baseNegOut)
|
||||
self.assertEqual(self.encode(baseNegIn + 0), b"\x00\x00\x00\x00\x08\x83")
|
||||
|
||||
baseLongNegOut = b"\x00\x00\x00\x08\x86"
|
||||
self.assertEqual(self.encode(baseNegIn - 1), b"\x01" + baseLongNegOut)
|
||||
self.assertEqual(self.encode(baseNegIn - 2), b"\x02" + baseLongNegOut)
|
||||
self.assertEqual(self.encode(baseNegIn - 3), b"\x03" + baseLongNegOut)
|
||||
|
||||
|
||||
class DialectTests(BananaTestBase):
|
||||
"""
|
||||
Tests for Banana's handling of dialects.
|
||||
"""
|
||||
|
||||
vocab = b"remote"
|
||||
legalPbItem = bytes((banana.Banana.outgoingVocabulary[vocab],)) + banana.VOCAB
|
||||
illegalPbItem = bytes((122,)) + banana.VOCAB
|
||||
|
||||
def test_dialectNotSet(self):
|
||||
"""
|
||||
If no dialect has been selected and a PB VOCAB item is received,
|
||||
L{NotImplementedError} is raised.
|
||||
"""
|
||||
self.assertRaises(NotImplementedError, self.enc.dataReceived, self.legalPbItem)
|
||||
|
||||
def test_receivePb(self):
|
||||
"""
|
||||
If the PB dialect has been selected, a PB VOCAB item is accepted.
|
||||
"""
|
||||
selectDialect(self.enc, b"pb")
|
||||
self.enc.dataReceived(self.legalPbItem)
|
||||
self.assertEqual(self.result, self.vocab)
|
||||
|
||||
def test_receiveIllegalPb(self):
|
||||
"""
|
||||
If the PB dialect has been selected and an unrecognized PB VOCAB item
|
||||
is received, L{banana.Banana.dataReceived} raises L{KeyError}.
|
||||
"""
|
||||
selectDialect(self.enc, b"pb")
|
||||
self.assertRaises(KeyError, self.enc.dataReceived, self.illegalPbItem)
|
||||
|
||||
def test_sendPb(self):
|
||||
"""
|
||||
if pb dialect is selected, the sender must be able to send things in
|
||||
that dialect.
|
||||
"""
|
||||
selectDialect(self.enc, b"pb")
|
||||
self.enc.sendEncoded(self.vocab)
|
||||
self.assertEqual(self.legalPbItem, self.io.getvalue())
|
||||
|
||||
|
||||
class GlobalCoderTests(TestCase):
|
||||
"""
|
||||
Tests for the free functions L{banana.encode} and L{banana.decode}.
|
||||
"""
|
||||
|
||||
def test_statelessDecode(self):
|
||||
"""
|
||||
Calls to L{banana.decode} are independent of each other.
|
||||
"""
|
||||
# Banana encoding of 2 ** 449
|
||||
undecodable = b"\x7f" * 65 + b"\x85"
|
||||
self.assertRaises(banana.BananaError, banana.decode, undecodable)
|
||||
|
||||
# Banana encoding of 1. This should be decodable even though the
|
||||
# previous call passed un-decodable data and triggered an exception.
|
||||
decodable = b"\x01\x81"
|
||||
self.assertEqual(banana.decode(decodable), 1)
|
||||
@@ -0,0 +1,647 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Test cases for L{jelly} object serialization.
|
||||
"""
|
||||
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.spread import banana, jelly, pb
|
||||
from twisted.trial import unittest
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class TestNode(jelly.Jellyable):
|
||||
"""
|
||||
An object to test jellyfying of new style class instances.
|
||||
"""
|
||||
|
||||
classAttr = 4
|
||||
|
||||
def __init__(self, parent=None):
|
||||
if parent:
|
||||
self.id = parent.id + 1
|
||||
parent.children.append(self)
|
||||
else:
|
||||
self.id = 1
|
||||
self.parent = parent
|
||||
self.children = []
|
||||
|
||||
|
||||
class A:
|
||||
"""
|
||||
Dummy class.
|
||||
"""
|
||||
|
||||
def amethod(self):
|
||||
"""
|
||||
Method to be used in serialization tests.
|
||||
"""
|
||||
|
||||
|
||||
def afunc(self):
|
||||
"""
|
||||
A dummy function to test function serialization.
|
||||
"""
|
||||
|
||||
|
||||
class B:
|
||||
"""
|
||||
Dummy class.
|
||||
"""
|
||||
|
||||
def bmethod(self):
|
||||
"""
|
||||
Method to be used in serialization tests.
|
||||
"""
|
||||
|
||||
|
||||
class C:
|
||||
"""
|
||||
Dummy class.
|
||||
"""
|
||||
|
||||
def cmethod(self):
|
||||
"""
|
||||
Method to be used in serialization tests.
|
||||
"""
|
||||
|
||||
|
||||
class D:
|
||||
"""
|
||||
Dummy new-style class.
|
||||
"""
|
||||
|
||||
|
||||
class E:
|
||||
"""
|
||||
Dummy new-style class with slots.
|
||||
"""
|
||||
|
||||
__slots__ = ("x", "y")
|
||||
|
||||
def __init__(self, x=None, y=None):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def __getstate__(self):
|
||||
return {"x": self.x, "y": self.y}
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.x = state["x"]
|
||||
self.y = state["y"]
|
||||
|
||||
|
||||
class SimpleJellyTest:
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def isTheSameAs(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
|
||||
|
||||
def jellyRoundTrip(testCase, toSerialize):
|
||||
"""
|
||||
Verify that the given object round-trips through jelly & banana and comes
|
||||
out equivalent to the input.
|
||||
"""
|
||||
jellied = jelly.jelly(toSerialize)
|
||||
encoded = banana.encode(jellied)
|
||||
decoded = banana.decode(encoded)
|
||||
unjellied = jelly.unjelly(decoded)
|
||||
testCase.assertEqual(toSerialize, unjellied)
|
||||
|
||||
|
||||
class JellyTests(TestCase):
|
||||
"""
|
||||
Testcases for L{jelly} module serialization.
|
||||
|
||||
@cvar decimalData: serialized version of decimal data, to be used in tests.
|
||||
@type decimalData: L{list}
|
||||
"""
|
||||
|
||||
decimalData = [
|
||||
b"list",
|
||||
[b"decimal", 995, -2],
|
||||
[b"decimal", 0, 0],
|
||||
[b"decimal", 123456, 0],
|
||||
[b"decimal", -78901, -3],
|
||||
]
|
||||
|
||||
def _testSecurity(self, inputList, atom):
|
||||
"""
|
||||
Helper test method to test security options for a type.
|
||||
|
||||
@param inputList: a sample input for the type.
|
||||
@type inputList: L{list}
|
||||
|
||||
@param atom: atom identifier for the type.
|
||||
@type atom: L{str}
|
||||
"""
|
||||
c = jelly.jelly(inputList)
|
||||
taster = jelly.SecurityOptions()
|
||||
taster.allowBasicTypes()
|
||||
# By default, it should succeed
|
||||
jelly.unjelly(c, taster)
|
||||
taster.allowedTypes.pop(atom)
|
||||
# But it should raise an exception when disallowed
|
||||
self.assertRaises(jelly.InsecureJelly, jelly.unjelly, c, taster)
|
||||
|
||||
def test_methodsNotSelfIdentity(self):
|
||||
"""
|
||||
If a class change after an instance has been created, L{jelly.unjelly}
|
||||
shoud raise a C{TypeError} when trying to unjelly the instance.
|
||||
"""
|
||||
a = A()
|
||||
b = B()
|
||||
c = C()
|
||||
a.bmethod = c.cmethod
|
||||
b.a = a
|
||||
savecmethod = C.cmethod
|
||||
del C.cmethod
|
||||
try:
|
||||
self.assertRaises(TypeError, jelly.unjelly, jelly.jelly(b))
|
||||
finally:
|
||||
C.cmethod = savecmethod
|
||||
|
||||
def test_newStyle(self):
|
||||
"""
|
||||
Test that a new style class can be jellied and unjellied with its
|
||||
objects and attribute values preserved.
|
||||
"""
|
||||
n = D()
|
||||
n.x = 1
|
||||
n2 = D()
|
||||
n.n2 = n2
|
||||
n.n3 = n2
|
||||
c = jelly.jelly(n)
|
||||
m = jelly.unjelly(c)
|
||||
self.assertIsInstance(m, D)
|
||||
self.assertIs(m.n2, m.n3)
|
||||
self.assertEqual(m.x, 1)
|
||||
|
||||
def test_newStyleWithSlots(self):
|
||||
"""
|
||||
A class defined with I{slots} can be jellied and unjellied with the
|
||||
values for its attributes preserved.
|
||||
"""
|
||||
n = E()
|
||||
n.x = 1
|
||||
c = jelly.jelly(n)
|
||||
m = jelly.unjelly(c)
|
||||
self.assertIsInstance(m, E)
|
||||
self.assertEqual(n.x, 1)
|
||||
|
||||
def test_typeNewStyle(self):
|
||||
"""
|
||||
Test that a new style class type can be jellied and unjellied
|
||||
to the original type.
|
||||
"""
|
||||
t = [D]
|
||||
r = jelly.unjelly(jelly.jelly(t))
|
||||
self.assertEqual(t, r)
|
||||
|
||||
def test_typeBuiltin(self):
|
||||
"""
|
||||
Test that a builtin type can be jellied and unjellied to the original
|
||||
type.
|
||||
"""
|
||||
t = [str]
|
||||
r = jelly.unjelly(jelly.jelly(t))
|
||||
self.assertEqual(t, r)
|
||||
|
||||
def test_dateTime(self):
|
||||
"""
|
||||
Jellying L{datetime.timedelta} instances and then unjellying the result
|
||||
should produce objects which represent the values of the original
|
||||
inputs.
|
||||
"""
|
||||
dtn = datetime.datetime.now()
|
||||
dtd = datetime.datetime.now() - dtn
|
||||
inputList = [dtn, dtd]
|
||||
c = jelly.jelly(inputList)
|
||||
output = jelly.unjelly(c)
|
||||
self.assertEqual(inputList, output)
|
||||
self.assertIsNot(inputList, output)
|
||||
|
||||
def test_bananaTimeTypes(self):
|
||||
"""
|
||||
Jellying L{datetime.time}, L{datetime.timedelta}, L{datetime.datetime},
|
||||
and L{datetime.date} objects should result in jellied objects which can
|
||||
be serialized and unserialized with banana.
|
||||
"""
|
||||
sampleDate = datetime.date(2020, 7, 11)
|
||||
sampleTime = datetime.time(1, 16, 5, 344)
|
||||
sampleDateTime = datetime.datetime.combine(sampleDate, sampleTime)
|
||||
sampleTimeDelta = sampleDateTime - datetime.datetime(2020, 7, 3)
|
||||
jellyRoundTrip(self, sampleDate)
|
||||
jellyRoundTrip(self, sampleTime)
|
||||
jellyRoundTrip(self, sampleDateTime)
|
||||
jellyRoundTrip(self, sampleTimeDelta)
|
||||
|
||||
def test_decimal(self):
|
||||
"""
|
||||
Jellying L{decimal.Decimal} instances and then unjellying the result
|
||||
should produce objects which represent the values of the original
|
||||
inputs.
|
||||
"""
|
||||
inputList = [
|
||||
decimal.Decimal("9.95"),
|
||||
decimal.Decimal(0),
|
||||
decimal.Decimal(123456),
|
||||
decimal.Decimal("-78.901"),
|
||||
]
|
||||
c = jelly.jelly(inputList)
|
||||
output = jelly.unjelly(c)
|
||||
self.assertEqual(inputList, output)
|
||||
self.assertIsNot(inputList, output)
|
||||
|
||||
def test_decimalUnjelly(self):
|
||||
"""
|
||||
Unjellying the s-expressions produced by jelly for L{decimal.Decimal}
|
||||
instances should result in L{decimal.Decimal} instances with the values
|
||||
represented by the s-expressions.
|
||||
|
||||
This test also verifies that L{decimalData} contains valid jellied
|
||||
data. This is important since L{test_decimalMissing} re-uses
|
||||
L{decimalData} and is expected to be unable to produce
|
||||
L{decimal.Decimal} instances even though the s-expression correctly
|
||||
represents a list of them.
|
||||
"""
|
||||
expected = [
|
||||
decimal.Decimal("9.95"),
|
||||
decimal.Decimal(0),
|
||||
decimal.Decimal(123456),
|
||||
decimal.Decimal("-78.901"),
|
||||
]
|
||||
output = jelly.unjelly(self.decimalData)
|
||||
self.assertEqual(output, expected)
|
||||
|
||||
def test_decimalSecurity(self):
|
||||
"""
|
||||
By default, C{decimal} objects should be allowed by
|
||||
L{jelly.SecurityOptions}. If not allowed, L{jelly.unjelly} should raise
|
||||
L{jelly.InsecureJelly} when trying to unjelly it.
|
||||
"""
|
||||
inputList = [decimal.Decimal("9.95")]
|
||||
self._testSecurity(inputList, b"decimal")
|
||||
|
||||
def test_set(self):
|
||||
"""
|
||||
Jellying C{set} instances and then unjellying the result
|
||||
should produce objects which represent the values of the original
|
||||
inputs.
|
||||
"""
|
||||
inputList = [{1, 2, 3}]
|
||||
output = jelly.unjelly(jelly.jelly(inputList))
|
||||
self.assertEqual(inputList, output)
|
||||
self.assertIsNot(inputList, output)
|
||||
|
||||
def test_frozenset(self):
|
||||
"""
|
||||
Jellying L{frozenset} instances and then unjellying the result
|
||||
should produce objects which represent the values of the original
|
||||
inputs.
|
||||
"""
|
||||
inputList = [frozenset([1, 2, 3])]
|
||||
output = jelly.unjelly(jelly.jelly(inputList))
|
||||
self.assertEqual(inputList, output)
|
||||
self.assertIsNot(inputList, output)
|
||||
|
||||
def test_setSecurity(self):
|
||||
"""
|
||||
By default, C{set} objects should be allowed by
|
||||
L{jelly.SecurityOptions}. If not allowed, L{jelly.unjelly} should raise
|
||||
L{jelly.InsecureJelly} when trying to unjelly it.
|
||||
"""
|
||||
inputList = [{1, 2, 3}]
|
||||
self._testSecurity(inputList, b"set")
|
||||
|
||||
def test_frozensetSecurity(self):
|
||||
"""
|
||||
By default, L{frozenset} objects should be allowed by
|
||||
L{jelly.SecurityOptions}. If not allowed, L{jelly.unjelly} should raise
|
||||
L{jelly.InsecureJelly} when trying to unjelly it.
|
||||
"""
|
||||
inputList = [frozenset([1, 2, 3])]
|
||||
self._testSecurity(inputList, b"frozenset")
|
||||
|
||||
def test_simple(self):
|
||||
"""
|
||||
Simplest test case.
|
||||
"""
|
||||
self.assertTrue(
|
||||
SimpleJellyTest("a", "b").isTheSameAs(SimpleJellyTest("a", "b"))
|
||||
)
|
||||
a = SimpleJellyTest(1, 2)
|
||||
cereal = jelly.jelly(a)
|
||||
b = jelly.unjelly(cereal)
|
||||
self.assertTrue(a.isTheSameAs(b))
|
||||
|
||||
def test_identity(self):
|
||||
"""
|
||||
Test to make sure that objects retain identity properly.
|
||||
"""
|
||||
x = []
|
||||
y = x
|
||||
x.append(y)
|
||||
x.append(y)
|
||||
self.assertIs(x[0], x[1])
|
||||
self.assertIs(x[0][0], x)
|
||||
s = jelly.jelly(x)
|
||||
z = jelly.unjelly(s)
|
||||
self.assertIs(z[0], z[1])
|
||||
self.assertIs(z[0][0], z)
|
||||
|
||||
def test_str(self):
|
||||
x = "blah"
|
||||
y = jelly.unjelly(jelly.jelly(x))
|
||||
self.assertEqual(x, y)
|
||||
self.assertEqual(type(x), type(y))
|
||||
|
||||
def test_stressReferences(self):
|
||||
reref = []
|
||||
toplevelTuple = ({"list": reref}, reref)
|
||||
reref.append(toplevelTuple)
|
||||
s = jelly.jelly(toplevelTuple)
|
||||
z = jelly.unjelly(s)
|
||||
self.assertIs(z[0]["list"], z[1])
|
||||
self.assertIs(z[0]["list"][0], z)
|
||||
|
||||
def test_moreReferences(self):
|
||||
a = []
|
||||
t = (a,)
|
||||
a.append((t,))
|
||||
s = jelly.jelly(t)
|
||||
z = jelly.unjelly(s)
|
||||
self.assertIs(z[0][0][0], z)
|
||||
|
||||
def test_typeSecurity(self):
|
||||
"""
|
||||
Test for type-level security of serialization.
|
||||
"""
|
||||
taster = jelly.SecurityOptions()
|
||||
dct = jelly.jelly({})
|
||||
self.assertRaises(jelly.InsecureJelly, jelly.unjelly, dct, taster)
|
||||
|
||||
def test_newStyleClasses(self):
|
||||
uj = jelly.unjelly(D)
|
||||
self.assertIs(D, uj)
|
||||
|
||||
def test_lotsaTypes(self):
|
||||
"""
|
||||
Test for all types currently supported in jelly
|
||||
"""
|
||||
a = A()
|
||||
jelly.unjelly(jelly.jelly(a))
|
||||
jelly.unjelly(jelly.jelly(a.amethod))
|
||||
items = [
|
||||
afunc,
|
||||
[1, 2, 3],
|
||||
not bool(1),
|
||||
bool(1),
|
||||
"test",
|
||||
20.3,
|
||||
(1, 2, 3),
|
||||
None,
|
||||
A,
|
||||
unittest,
|
||||
{"a": 1},
|
||||
A.amethod,
|
||||
]
|
||||
for i in items:
|
||||
self.assertEqual(i, jelly.unjelly(jelly.jelly(i)))
|
||||
|
||||
def test_setState(self):
|
||||
global TupleState
|
||||
|
||||
class TupleState:
|
||||
def __init__(self, other):
|
||||
self.other = other
|
||||
|
||||
def __getstate__(self):
|
||||
return (self.other,)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.other = state[0]
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.other)
|
||||
|
||||
a = A()
|
||||
t1 = TupleState(a)
|
||||
t2 = TupleState(a)
|
||||
t3 = TupleState((t1, t2))
|
||||
d = {t1: t1, t2: t2, t3: t3, "t3": t3}
|
||||
t3prime = jelly.unjelly(jelly.jelly(d))["t3"]
|
||||
self.assertIs(t3prime.other[0].other, t3prime.other[1].other)
|
||||
|
||||
def test_classSecurity(self):
|
||||
"""
|
||||
Test for class-level security of serialization.
|
||||
"""
|
||||
taster = jelly.SecurityOptions()
|
||||
taster.allowInstancesOf(A, B)
|
||||
a = A()
|
||||
b = B()
|
||||
c = C()
|
||||
# add a little complexity to the data
|
||||
a.b = b
|
||||
a.c = c
|
||||
# and a backreference
|
||||
a.x = b
|
||||
b.c = c
|
||||
# first, a friendly insecure serialization
|
||||
friendly = jelly.jelly(a, taster)
|
||||
x = jelly.unjelly(friendly, taster)
|
||||
self.assertIsInstance(x.c, jelly.Unpersistable)
|
||||
# now, a malicious one
|
||||
mean = jelly.jelly(a)
|
||||
self.assertRaises(jelly.InsecureJelly, jelly.unjelly, mean, taster)
|
||||
self.assertIs(x.x, x.b, "Identity mismatch")
|
||||
# test class serialization
|
||||
friendly = jelly.jelly(A, taster)
|
||||
x = jelly.unjelly(friendly, taster)
|
||||
self.assertIs(x, A, "A came back: %s" % x)
|
||||
|
||||
def test_unjellyable(self):
|
||||
"""
|
||||
Test that if Unjellyable is used to deserialize a jellied object,
|
||||
state comes out right.
|
||||
"""
|
||||
|
||||
class JellyableTestClass(jelly.Jellyable):
|
||||
pass
|
||||
|
||||
jelly.setUnjellyableForClass(JellyableTestClass, jelly.Unjellyable)
|
||||
input = JellyableTestClass()
|
||||
input.attribute = "value"
|
||||
output = jelly.unjelly(jelly.jelly(input))
|
||||
self.assertEqual(output.attribute, "value")
|
||||
self.assertIsInstance(output, jelly.Unjellyable)
|
||||
|
||||
def test_persistentStorage(self):
|
||||
perst = [{}, 1]
|
||||
|
||||
def persistentStore(obj, jel, perst=perst):
|
||||
perst[1] = perst[1] + 1
|
||||
perst[0][perst[1]] = obj
|
||||
return str(perst[1])
|
||||
|
||||
def persistentLoad(pidstr, unj, perst=perst):
|
||||
pid = int(pidstr)
|
||||
return perst[0][pid]
|
||||
|
||||
a = SimpleJellyTest(1, 2)
|
||||
b = SimpleJellyTest(3, 4)
|
||||
c = SimpleJellyTest(5, 6)
|
||||
|
||||
a.b = b
|
||||
a.c = c
|
||||
c.b = b
|
||||
|
||||
jel = jelly.jelly(a, persistentStore=persistentStore)
|
||||
x = jelly.unjelly(jel, persistentLoad=persistentLoad)
|
||||
|
||||
self.assertIs(x.b, x.c.b)
|
||||
self.assertTrue(perst[0], "persistentStore was not called.")
|
||||
self.assertIs(x.b, a.b, "Persistent storage identity failure.")
|
||||
|
||||
def test_newStyleClassesAttributes(self):
|
||||
n = TestNode()
|
||||
n1 = TestNode(n)
|
||||
TestNode(n1)
|
||||
TestNode(n)
|
||||
# Jelly it
|
||||
jel = jelly.jelly(n)
|
||||
m = jelly.unjelly(jel)
|
||||
# Check that it has been restored ok
|
||||
self._check_newstyle(n, m)
|
||||
|
||||
def _check_newstyle(self, a, b):
|
||||
self.assertEqual(a.id, b.id)
|
||||
self.assertEqual(a.classAttr, 4)
|
||||
self.assertEqual(b.classAttr, 4)
|
||||
self.assertEqual(len(a.children), len(b.children))
|
||||
for x, y in zip(a.children, b.children):
|
||||
self._check_newstyle(x, y)
|
||||
|
||||
def test_referenceable(self):
|
||||
"""
|
||||
A L{pb.Referenceable} instance jellies to a structure which unjellies to
|
||||
a L{pb.RemoteReference}. The C{RemoteReference} has a I{luid} that
|
||||
matches up with the local object key in the L{pb.Broker} which sent the
|
||||
L{Referenceable}.
|
||||
"""
|
||||
ref = pb.Referenceable()
|
||||
jellyBroker = pb.Broker()
|
||||
jellyBroker.makeConnection(StringTransport())
|
||||
j = jelly.jelly(ref, invoker=jellyBroker)
|
||||
|
||||
unjellyBroker = pb.Broker()
|
||||
unjellyBroker.makeConnection(StringTransport())
|
||||
|
||||
uj = jelly.unjelly(j, invoker=unjellyBroker)
|
||||
self.assertIn(uj.luid, jellyBroker.localObjects)
|
||||
|
||||
|
||||
class JellyDeprecationTests(TestCase):
|
||||
"""
|
||||
Tests for deprecated Jelly things
|
||||
"""
|
||||
|
||||
def test_deprecatedInstanceAtom(self):
|
||||
"""
|
||||
L{jelly.instance_atom} is deprecated since 15.0.0.
|
||||
"""
|
||||
jelly.instance_atom
|
||||
warnings = self.flushWarnings([self.test_deprecatedInstanceAtom])
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(
|
||||
warnings[0]["message"],
|
||||
"twisted.spread.jelly.instance_atom was deprecated in Twisted "
|
||||
"15.0.0: instance_atom is unused within Twisted.",
|
||||
)
|
||||
self.assertEqual(warnings[0]["category"], DeprecationWarning)
|
||||
|
||||
def test_deprecatedUnjellyingInstanceAtom(self):
|
||||
"""
|
||||
Unjellying the instance atom is deprecated with 15.0.0.
|
||||
"""
|
||||
jelly.unjelly(
|
||||
["instance", ["class", "twisted.spread.test.test_jelly.A"], ["dictionary"]]
|
||||
)
|
||||
warnings = self.flushWarnings()
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(
|
||||
warnings[0]["message"],
|
||||
"Unjelly support for the instance atom is deprecated since "
|
||||
"Twisted 15.0.0. Upgrade peer for modern instance support.",
|
||||
)
|
||||
self.assertEqual(warnings[0]["category"], DeprecationWarning)
|
||||
|
||||
|
||||
class ClassA(pb.Copyable, pb.RemoteCopy):
|
||||
def __init__(self):
|
||||
self.ref = ClassB(self)
|
||||
|
||||
|
||||
class ClassB(pb.Copyable, pb.RemoteCopy):
|
||||
def __init__(self, ref):
|
||||
self.ref = ref
|
||||
|
||||
|
||||
class CircularReferenceTests(TestCase):
|
||||
"""
|
||||
Tests for circular references handling in the jelly/unjelly process.
|
||||
"""
|
||||
|
||||
def test_simpleCircle(self):
|
||||
jelly.setUnjellyableForClass(ClassA, ClassA)
|
||||
jelly.setUnjellyableForClass(ClassB, ClassB)
|
||||
a = jelly.unjelly(jelly.jelly(ClassA()))
|
||||
self.assertIs(a.ref.ref, a, "Identity not preserved in circular reference")
|
||||
|
||||
def test_circleWithInvoker(self):
|
||||
class DummyInvokerClass:
|
||||
pass
|
||||
|
||||
dummyInvoker = DummyInvokerClass()
|
||||
dummyInvoker.serializingPerspective = None
|
||||
a0 = ClassA()
|
||||
jelly.setUnjellyableForClass(ClassA, ClassA)
|
||||
jelly.setUnjellyableForClass(ClassB, ClassB)
|
||||
j = jelly.jelly(a0, invoker=dummyInvoker)
|
||||
a1 = jelly.unjelly(j)
|
||||
self.failUnlessIdentical(
|
||||
a1.ref.ref, a1, "Identity not preserved in circular reference"
|
||||
)
|
||||
|
||||
def test_set(self):
|
||||
"""
|
||||
Check that a C{set} can contain a circular reference and be serialized
|
||||
and unserialized without losing the reference.
|
||||
"""
|
||||
s = set()
|
||||
a = SimpleJellyTest(s, None)
|
||||
s.add(a)
|
||||
res = jelly.unjelly(jelly.jelly(a))
|
||||
self.assertIsInstance(res.x, set)
|
||||
self.assertEqual(list(res.x), [res])
|
||||
|
||||
def test_frozenset(self):
|
||||
"""
|
||||
Check that a L{frozenset} can contain a circular reference and be
|
||||
serialized and unserialized without losing the reference.
|
||||
"""
|
||||
a = SimpleJellyTest(None, None)
|
||||
s = frozenset([a])
|
||||
a.x = s
|
||||
res = jelly.unjelly(jelly.jelly(a))
|
||||
self.assertIsInstance(res.x, frozenset)
|
||||
self.assertEqual(list(res.x), [res])
|
||||
2041
.venv/lib/python3.12/site-packages/twisted/spread/test/test_pb.py
Normal file
2041
.venv/lib/python3.12/site-packages/twisted/spread/test/test_pb.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,450 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for error handling in PB.
|
||||
"""
|
||||
|
||||
from io import StringIO
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.python import log
|
||||
from twisted.python.reflect import qual
|
||||
from twisted.spread import flavors, jelly, pb
|
||||
from twisted.test.iosim import connectedServerAndClient
|
||||
from twisted.trial import unittest
|
||||
|
||||
# Test exceptions
|
||||
|
||||
|
||||
class AsynchronousException(Exception):
|
||||
"""
|
||||
Helper used to test remote methods which return Deferreds which fail with
|
||||
exceptions which are not L{pb.Error} subclasses.
|
||||
"""
|
||||
|
||||
|
||||
class SynchronousException(Exception):
|
||||
"""
|
||||
Helper used to test remote methods which raise exceptions which are not
|
||||
L{pb.Error} subclasses.
|
||||
"""
|
||||
|
||||
|
||||
class AsynchronousError(pb.Error):
|
||||
"""
|
||||
Helper used to test remote methods which return Deferreds which fail with
|
||||
exceptions which are L{pb.Error} subclasses.
|
||||
"""
|
||||
|
||||
|
||||
class SynchronousError(pb.Error):
|
||||
"""
|
||||
Helper used to test remote methods which raise exceptions which are
|
||||
L{pb.Error} subclasses.
|
||||
"""
|
||||
|
||||
|
||||
class JellyError(flavors.Jellyable, pb.Error, pb.RemoteCopy):
|
||||
pass
|
||||
|
||||
|
||||
class SecurityError(pb.Error, pb.RemoteCopy):
|
||||
pass
|
||||
|
||||
|
||||
pb.setUnjellyableForClass(JellyError, JellyError)
|
||||
pb.setUnjellyableForClass(SecurityError, SecurityError)
|
||||
pb.globalSecurity.allowInstancesOf(SecurityError)
|
||||
|
||||
|
||||
# Server-side
|
||||
class SimpleRoot(pb.Root):
|
||||
def remote_asynchronousException(self):
|
||||
"""
|
||||
Fail asynchronously with a non-pb.Error exception.
|
||||
"""
|
||||
return defer.fail(AsynchronousException("remote asynchronous exception"))
|
||||
|
||||
def remote_synchronousException(self):
|
||||
"""
|
||||
Fail synchronously with a non-pb.Error exception.
|
||||
"""
|
||||
raise SynchronousException("remote synchronous exception")
|
||||
|
||||
def remote_asynchronousError(self):
|
||||
"""
|
||||
Fail asynchronously with a pb.Error exception.
|
||||
"""
|
||||
return defer.fail(AsynchronousError("remote asynchronous error"))
|
||||
|
||||
def remote_synchronousError(self):
|
||||
"""
|
||||
Fail synchronously with a pb.Error exception.
|
||||
"""
|
||||
raise SynchronousError("remote synchronous error")
|
||||
|
||||
def remote_unknownError(self):
|
||||
"""
|
||||
Fail with error that is not known to client.
|
||||
"""
|
||||
|
||||
class UnknownError(pb.Error):
|
||||
pass
|
||||
|
||||
raise UnknownError("I'm not known to client!")
|
||||
|
||||
def remote_jelly(self):
|
||||
self.raiseJelly()
|
||||
|
||||
def remote_security(self):
|
||||
self.raiseSecurity()
|
||||
|
||||
def remote_deferredJelly(self):
|
||||
d = defer.Deferred()
|
||||
d.addCallback(self.raiseJelly)
|
||||
d.callback(None)
|
||||
return d
|
||||
|
||||
def remote_deferredSecurity(self):
|
||||
d = defer.Deferred()
|
||||
d.addCallback(self.raiseSecurity)
|
||||
d.callback(None)
|
||||
return d
|
||||
|
||||
def raiseJelly(self, results=None):
|
||||
raise JellyError("I'm jellyable!")
|
||||
|
||||
def raiseSecurity(self, results=None):
|
||||
raise SecurityError("I'm secure!")
|
||||
|
||||
|
||||
class SaveProtocolServerFactory(pb.PBServerFactory):
|
||||
"""
|
||||
A L{pb.PBServerFactory} that saves the latest connected client in
|
||||
C{protocolInstance}.
|
||||
"""
|
||||
|
||||
protocolInstance = None
|
||||
|
||||
def clientConnectionMade(self, protocol):
|
||||
"""
|
||||
Keep track of the given protocol.
|
||||
"""
|
||||
self.protocolInstance = protocol
|
||||
|
||||
|
||||
class PBConnTestCase(unittest.TestCase):
|
||||
unsafeTracebacks = 0
|
||||
|
||||
def setUp(self):
|
||||
self.serverFactory = SaveProtocolServerFactory(SimpleRoot())
|
||||
self.serverFactory.unsafeTracebacks = self.unsafeTracebacks
|
||||
self.clientFactory = pb.PBClientFactory()
|
||||
(
|
||||
self.connectedServer,
|
||||
self.connectedClient,
|
||||
self.pump,
|
||||
) = connectedServerAndClient(
|
||||
lambda: self.serverFactory.buildProtocol(None),
|
||||
lambda: self.clientFactory.buildProtocol(None),
|
||||
)
|
||||
|
||||
|
||||
class PBFailureTests(PBConnTestCase):
|
||||
compare = unittest.TestCase.assertEqual
|
||||
|
||||
def _exceptionTest(self, method, exceptionType, flush):
|
||||
def eb(err):
|
||||
err.trap(exceptionType)
|
||||
self.compare(err.traceback, "Traceback unavailable\n")
|
||||
if flush:
|
||||
errs = self.flushLoggedErrors(exceptionType)
|
||||
self.assertEqual(len(errs), 1)
|
||||
return (err.type, err.value, err.traceback)
|
||||
|
||||
d = self.clientFactory.getRootObject()
|
||||
|
||||
def gotRootObject(root):
|
||||
d = root.callRemote(method)
|
||||
d.addErrback(eb)
|
||||
return d
|
||||
|
||||
d.addCallback(gotRootObject)
|
||||
self.pump.flush()
|
||||
|
||||
def test_asynchronousException(self):
|
||||
"""
|
||||
Test that a Deferred returned by a remote method which already has a
|
||||
Failure correctly has that error passed back to the calling side.
|
||||
"""
|
||||
return self._exceptionTest("asynchronousException", AsynchronousException, True)
|
||||
|
||||
def test_synchronousException(self):
|
||||
"""
|
||||
Like L{test_asynchronousException}, but for a method which raises an
|
||||
exception synchronously.
|
||||
"""
|
||||
return self._exceptionTest("synchronousException", SynchronousException, True)
|
||||
|
||||
def test_asynchronousError(self):
|
||||
"""
|
||||
Like L{test_asynchronousException}, but for a method which returns a
|
||||
Deferred failing with an L{pb.Error} subclass.
|
||||
"""
|
||||
return self._exceptionTest("asynchronousError", AsynchronousError, False)
|
||||
|
||||
def test_synchronousError(self):
|
||||
"""
|
||||
Like L{test_asynchronousError}, but for a method which synchronously
|
||||
raises a L{pb.Error} subclass.
|
||||
"""
|
||||
return self._exceptionTest("synchronousError", SynchronousError, False)
|
||||
|
||||
def _success(self, result, expectedResult):
|
||||
self.assertEqual(result, expectedResult)
|
||||
return result
|
||||
|
||||
def _addFailingCallbacks(self, remoteCall, expectedResult, eb):
|
||||
remoteCall.addCallbacks(self._success, eb, callbackArgs=(expectedResult,))
|
||||
return remoteCall
|
||||
|
||||
def _testImpl(self, method, expected, eb, exc=None):
|
||||
"""
|
||||
Call the given remote method and attach the given errback to the
|
||||
resulting Deferred. If C{exc} is not None, also assert that one
|
||||
exception of that type was logged.
|
||||
"""
|
||||
rootDeferred = self.clientFactory.getRootObject()
|
||||
|
||||
def gotRootObj(obj):
|
||||
failureDeferred = self._addFailingCallbacks(
|
||||
obj.callRemote(method), expected, eb
|
||||
)
|
||||
if exc is not None:
|
||||
|
||||
def gotFailure(err):
|
||||
self.assertEqual(len(self.flushLoggedErrors(exc)), 1)
|
||||
return err
|
||||
|
||||
failureDeferred.addBoth(gotFailure)
|
||||
return failureDeferred
|
||||
|
||||
rootDeferred.addCallback(gotRootObj)
|
||||
self.pump.flush()
|
||||
|
||||
def test_jellyFailure(self):
|
||||
"""
|
||||
Test that an exception which is a subclass of L{pb.Error} has more
|
||||
information passed across the network to the calling side.
|
||||
"""
|
||||
|
||||
def failureJelly(fail):
|
||||
fail.trap(JellyError)
|
||||
self.assertNotIsInstance(fail.type, str)
|
||||
self.assertIsInstance(fail.value, fail.type)
|
||||
return 43
|
||||
|
||||
return self._testImpl("jelly", 43, failureJelly)
|
||||
|
||||
def test_deferredJellyFailure(self):
|
||||
"""
|
||||
Test that a Deferred which fails with a L{pb.Error} is treated in
|
||||
the same way as a synchronously raised L{pb.Error}.
|
||||
"""
|
||||
|
||||
def failureDeferredJelly(fail):
|
||||
fail.trap(JellyError)
|
||||
self.assertNotIsInstance(fail.type, str)
|
||||
self.assertIsInstance(fail.value, fail.type)
|
||||
return 430
|
||||
|
||||
return self._testImpl("deferredJelly", 430, failureDeferredJelly)
|
||||
|
||||
def test_unjellyableFailure(self):
|
||||
"""
|
||||
A non-jellyable L{pb.Error} subclass raised by a remote method is
|
||||
turned into a Failure with a type set to the FQPN of the exception
|
||||
type.
|
||||
"""
|
||||
|
||||
def failureUnjellyable(fail):
|
||||
self.assertEqual(
|
||||
fail.type, b"twisted.spread.test.test_pbfailure.SynchronousError"
|
||||
)
|
||||
return 431
|
||||
|
||||
return self._testImpl("synchronousError", 431, failureUnjellyable)
|
||||
|
||||
def test_unknownFailure(self):
|
||||
"""
|
||||
Test that an exception which is a subclass of L{pb.Error} but not
|
||||
known on the client side has its type set properly.
|
||||
"""
|
||||
|
||||
def failureUnknown(fail):
|
||||
self.assertEqual(
|
||||
fail.type, b"twisted.spread.test.test_pbfailure.UnknownError"
|
||||
)
|
||||
return 4310
|
||||
|
||||
return self._testImpl("unknownError", 4310, failureUnknown)
|
||||
|
||||
def test_securityFailure(self):
|
||||
"""
|
||||
Test that even if an exception is not explicitly jellyable (by being
|
||||
a L{pb.Jellyable} subclass), as long as it is an L{pb.Error}
|
||||
subclass it receives the same special treatment.
|
||||
"""
|
||||
|
||||
def failureSecurity(fail):
|
||||
fail.trap(SecurityError)
|
||||
self.assertNotIsInstance(fail.type, str)
|
||||
self.assertIsInstance(fail.value, fail.type)
|
||||
return 4300
|
||||
|
||||
return self._testImpl("security", 4300, failureSecurity)
|
||||
|
||||
def test_deferredSecurity(self):
|
||||
"""
|
||||
Test that a Deferred which fails with a L{pb.Error} which is not
|
||||
also a L{pb.Jellyable} is treated in the same way as a synchronously
|
||||
raised exception of the same type.
|
||||
"""
|
||||
|
||||
def failureDeferredSecurity(fail):
|
||||
fail.trap(SecurityError)
|
||||
self.assertNotIsInstance(fail.type, str)
|
||||
self.assertIsInstance(fail.value, fail.type)
|
||||
return 43000
|
||||
|
||||
return self._testImpl("deferredSecurity", 43000, failureDeferredSecurity)
|
||||
|
||||
def test_noSuchMethodFailure(self):
|
||||
"""
|
||||
Test that attempting to call a method which is not defined correctly
|
||||
results in an AttributeError on the calling side.
|
||||
"""
|
||||
|
||||
def failureNoSuch(fail):
|
||||
fail.trap(pb.NoSuchMethod)
|
||||
self.compare(fail.traceback, "Traceback unavailable\n")
|
||||
return 42000
|
||||
|
||||
return self._testImpl("nosuch", 42000, failureNoSuch, AttributeError)
|
||||
|
||||
def test_copiedFailureLogging(self):
|
||||
"""
|
||||
Test that a copied failure received from a PB call can be logged
|
||||
locally.
|
||||
|
||||
Note: this test needs some serious help: all it really tests is that
|
||||
log.err(copiedFailure) doesn't raise an exception.
|
||||
"""
|
||||
d = self.clientFactory.getRootObject()
|
||||
|
||||
def connected(rootObj):
|
||||
return rootObj.callRemote("synchronousException")
|
||||
|
||||
d.addCallback(connected)
|
||||
|
||||
def exception(failure):
|
||||
log.err(failure)
|
||||
errs = self.flushLoggedErrors(SynchronousException)
|
||||
self.assertEqual(len(errs), 2)
|
||||
|
||||
d.addErrback(exception)
|
||||
self.pump.flush()
|
||||
|
||||
def test_throwExceptionIntoGenerator(self):
|
||||
"""
|
||||
L{pb.CopiedFailure.throwExceptionIntoGenerator} will throw a
|
||||
L{RemoteError} into the given paused generator at the point where it
|
||||
last yielded.
|
||||
"""
|
||||
original = pb.CopyableFailure(AttributeError("foo"))
|
||||
copy = jelly.unjelly(jelly.jelly(original, invoker=DummyInvoker()))
|
||||
exception = []
|
||||
|
||||
def generatorFunc():
|
||||
try:
|
||||
yield None
|
||||
except pb.RemoteError as exc:
|
||||
exception.append(exc)
|
||||
else:
|
||||
self.fail("RemoteError not raised")
|
||||
|
||||
gen = generatorFunc()
|
||||
gen.send(None)
|
||||
self.assertRaises(StopIteration, copy.throwExceptionIntoGenerator, gen)
|
||||
self.assertEqual(len(exception), 1)
|
||||
exc = exception[0]
|
||||
self.assertEqual(exc.remoteType, qual(AttributeError).encode("ascii"))
|
||||
self.assertEqual(exc.args, ("foo",))
|
||||
self.assertEqual(exc.remoteTraceback, "Traceback unavailable\n")
|
||||
|
||||
|
||||
class PBFailureUnsafeTests(PBFailureTests):
|
||||
compare = unittest.TestCase.failIfEquals
|
||||
unsafeTracebacks = 1
|
||||
|
||||
|
||||
class DummyInvoker:
|
||||
"""
|
||||
A behaviorless object to be used as the invoker parameter to
|
||||
L{jelly.jelly}.
|
||||
"""
|
||||
|
||||
serializingPerspective = None
|
||||
|
||||
|
||||
class FailureJellyingTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for the interaction of jelly and failures.
|
||||
"""
|
||||
|
||||
def test_unjelliedFailureCheck(self):
|
||||
"""
|
||||
An unjellied L{CopyableFailure} has a check method which behaves the
|
||||
same way as the original L{CopyableFailure}'s check method.
|
||||
"""
|
||||
original = pb.CopyableFailure(ZeroDivisionError())
|
||||
self.assertIs(original.check(ZeroDivisionError), ZeroDivisionError)
|
||||
self.assertIs(original.check(ArithmeticError), ArithmeticError)
|
||||
copied = jelly.unjelly(jelly.jelly(original, invoker=DummyInvoker()))
|
||||
self.assertIs(copied.check(ZeroDivisionError), ZeroDivisionError)
|
||||
self.assertIs(copied.check(ArithmeticError), ArithmeticError)
|
||||
|
||||
def test_twiceUnjelliedFailureCheck(self):
|
||||
"""
|
||||
The object which results from jellying a L{CopyableFailure}, unjellying
|
||||
the result, creating a new L{CopyableFailure} from the result of that,
|
||||
jellying it, and finally unjellying the result of that has a check
|
||||
method which behaves the same way as the original L{CopyableFailure}'s
|
||||
check method.
|
||||
"""
|
||||
original = pb.CopyableFailure(ZeroDivisionError())
|
||||
self.assertIs(original.check(ZeroDivisionError), ZeroDivisionError)
|
||||
self.assertIs(original.check(ArithmeticError), ArithmeticError)
|
||||
copiedOnce = jelly.unjelly(jelly.jelly(original, invoker=DummyInvoker()))
|
||||
derivative = pb.CopyableFailure(copiedOnce)
|
||||
copiedTwice = jelly.unjelly(jelly.jelly(derivative, invoker=DummyInvoker()))
|
||||
self.assertIs(copiedTwice.check(ZeroDivisionError), ZeroDivisionError)
|
||||
self.assertIs(copiedTwice.check(ArithmeticError), ArithmeticError)
|
||||
|
||||
def test_printTracebackIncludesValue(self):
|
||||
"""
|
||||
When L{CopiedFailure.printTraceback} is used to print a copied failure
|
||||
which was unjellied from a L{CopyableFailure} with C{unsafeTracebacks}
|
||||
set to C{False}, the string representation of the exception value is
|
||||
included in the output.
|
||||
"""
|
||||
original = pb.CopyableFailure(Exception("some reason"))
|
||||
copied = jelly.unjelly(jelly.jelly(original, invoker=DummyInvoker()))
|
||||
output = StringIO()
|
||||
copied.printTraceback(output)
|
||||
exception = qual(Exception)
|
||||
expectedOutput = "Traceback from remote host -- " "{}: some reason\n".format(
|
||||
exception
|
||||
)
|
||||
self.assertEqual(expectedOutput, output.getvalue())
|
||||
217
.venv/lib/python3.12/site-packages/twisted/spread/util.py
Normal file
217
.venv/lib/python3.12/site-packages/twisted/spread/util.py
Normal file
@@ -0,0 +1,217 @@
|
||||
# -*- test-case-name: twisted.test.test_pb -*-
|
||||
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Utility classes for spread.
|
||||
"""
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer, interfaces
|
||||
from twisted.protocols import basic
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.spread import pb
|
||||
|
||||
|
||||
class LocalMethod:
|
||||
def __init__(self, local, name):
|
||||
self.local = local
|
||||
self.name = name
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
return self.local.callRemote(self.name, *args, **kw)
|
||||
|
||||
|
||||
class LocalAsRemote:
|
||||
"""
|
||||
A class useful for emulating the effects of remote behavior locally.
|
||||
"""
|
||||
|
||||
reportAllTracebacks = 1
|
||||
|
||||
def callRemote(self, name, *args, **kw):
|
||||
"""
|
||||
Call a specially-designated local method.
|
||||
|
||||
self.callRemote('x') will first try to invoke a method named
|
||||
sync_x and return its result (which should probably be a
|
||||
Deferred). Second, it will look for a method called async_x,
|
||||
which will be called and then have its result (or Failure)
|
||||
automatically wrapped in a Deferred.
|
||||
"""
|
||||
if hasattr(self, "sync_" + name):
|
||||
return getattr(self, "sync_" + name)(*args, **kw)
|
||||
try:
|
||||
method = getattr(self, "async_" + name)
|
||||
return defer.succeed(method(*args, **kw))
|
||||
except BaseException:
|
||||
f = Failure()
|
||||
if self.reportAllTracebacks:
|
||||
f.printTraceback()
|
||||
return defer.fail(f)
|
||||
|
||||
def remoteMethod(self, name):
|
||||
return LocalMethod(self, name)
|
||||
|
||||
|
||||
class LocalAsyncForwarder:
|
||||
"""
|
||||
A class useful for forwarding a locally-defined interface.
|
||||
"""
|
||||
|
||||
def __init__(self, forwarded, interfaceClass, failWhenNotImplemented=0):
|
||||
assert interfaceClass.providedBy(forwarded)
|
||||
self.forwarded = forwarded
|
||||
self.interfaceClass = interfaceClass
|
||||
self.failWhenNotImplemented = failWhenNotImplemented
|
||||
|
||||
def _callMethod(self, method, *args, **kw):
|
||||
return getattr(self.forwarded, method)(*args, **kw)
|
||||
|
||||
def callRemote(self, method, *args, **kw):
|
||||
if self.interfaceClass.queryDescriptionFor(method):
|
||||
result = defer.maybeDeferred(self._callMethod, method, *args, **kw)
|
||||
return result
|
||||
elif self.failWhenNotImplemented:
|
||||
return defer.fail(
|
||||
Failure(NotImplementedError, "No Such Method in Interface: %s" % method)
|
||||
)
|
||||
else:
|
||||
return defer.succeed(None)
|
||||
|
||||
|
||||
class Pager:
|
||||
"""
|
||||
I am an object which pages out information.
|
||||
"""
|
||||
|
||||
def __init__(self, collector, callback=None, *args, **kw):
|
||||
"""
|
||||
Create a pager with a Reference to a remote collector and
|
||||
an optional callable to invoke upon completion.
|
||||
"""
|
||||
if callable(callback):
|
||||
self.callback = callback
|
||||
self.callbackArgs = args
|
||||
self.callbackKeyword = kw
|
||||
else:
|
||||
self.callback = None
|
||||
self._stillPaging = 1
|
||||
self.collector = collector
|
||||
collector.broker.registerPageProducer(self)
|
||||
|
||||
def stillPaging(self):
|
||||
"""
|
||||
(internal) Method called by Broker.
|
||||
"""
|
||||
if not self._stillPaging:
|
||||
self.collector.callRemote("endedPaging", pbanswer=False)
|
||||
if self.callback is not None:
|
||||
self.callback(*self.callbackArgs, **self.callbackKeyword)
|
||||
return self._stillPaging
|
||||
|
||||
def sendNextPage(self):
|
||||
"""
|
||||
(internal) Method called by Broker.
|
||||
"""
|
||||
self.collector.callRemote("gotPage", self.nextPage(), pbanswer=False)
|
||||
|
||||
def nextPage(self):
|
||||
"""
|
||||
Override this to return an object to be sent to my collector.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def stopPaging(self):
|
||||
"""
|
||||
Call this when you're done paging.
|
||||
"""
|
||||
self._stillPaging = 0
|
||||
|
||||
|
||||
class StringPager(Pager):
|
||||
"""
|
||||
A simple pager that splits a string into chunks.
|
||||
"""
|
||||
|
||||
def __init__(self, collector, st, chunkSize=8192, callback=None, *args, **kw):
|
||||
self.string = st
|
||||
self.pointer = 0
|
||||
self.chunkSize = chunkSize
|
||||
Pager.__init__(self, collector, callback, *args, **kw)
|
||||
|
||||
def nextPage(self):
|
||||
val = self.string[self.pointer : self.pointer + self.chunkSize]
|
||||
self.pointer += self.chunkSize
|
||||
if self.pointer >= len(self.string):
|
||||
self.stopPaging()
|
||||
return val
|
||||
|
||||
|
||||
@implementer(interfaces.IConsumer)
|
||||
class FilePager(Pager):
|
||||
"""
|
||||
Reads a file in chunks and sends the chunks as they come.
|
||||
"""
|
||||
|
||||
def __init__(self, collector, fd, callback=None, *args, **kw):
|
||||
self.chunks = []
|
||||
Pager.__init__(self, collector, callback, *args, **kw)
|
||||
self.startProducing(fd)
|
||||
|
||||
def startProducing(self, fd):
|
||||
self.deferred = basic.FileSender().beginFileTransfer(fd, self)
|
||||
self.deferred.addBoth(lambda x: self.stopPaging())
|
||||
|
||||
def registerProducer(self, producer, streaming):
|
||||
self.producer = producer
|
||||
if not streaming:
|
||||
self.producer.resumeProducing()
|
||||
|
||||
def unregisterProducer(self):
|
||||
self.producer = None
|
||||
|
||||
def write(self, chunk):
|
||||
self.chunks.append(chunk)
|
||||
|
||||
def sendNextPage(self):
|
||||
"""
|
||||
Get the first chunk read and send it to collector.
|
||||
"""
|
||||
if not self.chunks:
|
||||
return
|
||||
val = self.chunks.pop(0)
|
||||
self.producer.resumeProducing()
|
||||
self.collector.callRemote("gotPage", val, pbanswer=False)
|
||||
|
||||
|
||||
# Utility paging stuff.
|
||||
class CallbackPageCollector(pb.Referenceable):
|
||||
"""
|
||||
I receive pages from the peer. You may instantiate a Pager with a
|
||||
remote reference to me. I will call the callback with a list of pages
|
||||
once they are all received.
|
||||
"""
|
||||
|
||||
def __init__(self, callback):
|
||||
self.pages = []
|
||||
self.callback = callback
|
||||
|
||||
def remote_gotPage(self, page):
|
||||
self.pages.append(page)
|
||||
|
||||
def remote_endedPaging(self):
|
||||
self.callback(self.pages)
|
||||
|
||||
|
||||
def getAllPages(referenceable, methodName, *args, **kw):
|
||||
"""
|
||||
A utility method that will call a remote method which expects a
|
||||
PageCollector as the first argument.
|
||||
"""
|
||||
d = defer.Deferred()
|
||||
referenceable.callRemote(methodName, CallbackPageCollector(d.callback), *args, **kw)
|
||||
return d
|
||||
Reference in New Issue
Block a user