Initial commit
This commit is contained in:
363
env/lib/python3.10/site-packages/telepath/__init__.py
vendored
Normal file
363
env/lib/python3.10/site-packages/telepath/__init__.py
vendored
Normal file
@@ -0,0 +1,363 @@
|
||||
from django import forms
|
||||
from django.forms import MediaDefiningClass
|
||||
from django.utils.functional import cached_property, Promise
|
||||
|
||||
|
||||
DICT_RESERVED_KEYS = ['_type', '_args', '_dict', '_list', '_val', '_id', '_ref']
|
||||
STRING_REF_MIN_LENGTH = 20 # do not turn strings shorter than this into references
|
||||
|
||||
|
||||
class UnpackableTypeError(TypeError):
|
||||
pass
|
||||
|
||||
|
||||
class Node:
|
||||
"""
|
||||
Intermediate representation of a packed value. Subclasses represent a particular value
|
||||
type, and implement emit_verbose (returns a dict representation of a value that can have
|
||||
an _id attached) and emit_compact (returns a compact representation of the value, in any
|
||||
JSON-serialisable type).
|
||||
|
||||
If this node is assigned an id, emit() will return the verbose representation with the
|
||||
id attached on first call, and a reference on subsequent calls. To disable this behaviour
|
||||
(e.g. for small primitive values where the reference representation adds unwanted overhead),
|
||||
set self.use_id = False.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.id = None
|
||||
self.seen = False
|
||||
self.use_id = True
|
||||
|
||||
def emit(self):
|
||||
if self.use_id and self.seen and self.id is not None:
|
||||
# Have already emitted this value, so emit a reference instead
|
||||
return {'_ref': self.id}
|
||||
else:
|
||||
self.seen = True
|
||||
if self.use_id and self.id is not None:
|
||||
# emit this value in long form including an ID
|
||||
result = self.emit_verbose()
|
||||
result['_id'] = self.id
|
||||
return result
|
||||
else:
|
||||
return self.emit_compact()
|
||||
|
||||
|
||||
class ValueNode(Node):
|
||||
"""Represents a primitive value; int, bool etc"""
|
||||
def __init__(self, value):
|
||||
super().__init__()
|
||||
self.value = value
|
||||
self.use_id = False
|
||||
|
||||
def emit_verbose(self):
|
||||
return {'_val': self.value}
|
||||
|
||||
def emit_compact(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class StringNode(Node):
|
||||
def __init__(self, value):
|
||||
super().__init__()
|
||||
self.value = value
|
||||
self.use_id = len(value) >= STRING_REF_MIN_LENGTH
|
||||
|
||||
def emit_verbose(self):
|
||||
return {'_val': self.value}
|
||||
|
||||
def emit_compact(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class ListNode(Node):
|
||||
def __init__(self, value):
|
||||
super().__init__()
|
||||
self.value = value
|
||||
|
||||
def emit_verbose(self):
|
||||
return {'_list': [item.emit() for item in self.value]}
|
||||
|
||||
def emit_compact(self):
|
||||
return [item.emit() for item in self.value]
|
||||
|
||||
|
||||
class DictNode(Node):
|
||||
def __init__(self, value):
|
||||
super().__init__()
|
||||
self.value = value
|
||||
|
||||
def emit_verbose(self):
|
||||
return {'_dict': {key: val.emit() for key, val in self.value.items()}}
|
||||
|
||||
def emit_compact(self):
|
||||
if any(reserved_key in self.value for reserved_key in DICT_RESERVED_KEYS):
|
||||
# compact representation is not valid as this dict contains reserved keys
|
||||
# that would clash with the verbose representation
|
||||
return self.emit_verbose()
|
||||
else:
|
||||
return {key: val.emit() for key, val in self.value.items()}
|
||||
|
||||
|
||||
class ObjectNode(Node):
|
||||
def __init__(self, constructor, args):
|
||||
super().__init__()
|
||||
self.constructor = constructor
|
||||
self.args = args
|
||||
|
||||
def emit_verbose(self):
|
||||
return {
|
||||
'_type': self.constructor,
|
||||
'_args': [arg.emit() for arg in self.args]
|
||||
}
|
||||
|
||||
def emit_compact(self):
|
||||
# objects always use verbose representation
|
||||
return self.emit_verbose()
|
||||
|
||||
|
||||
class BaseAdapter:
|
||||
"""Handles serialisation of a specific object type"""
|
||||
def build_node(self, obj, context):
|
||||
"""
|
||||
Translates obj into a node that we can call emit() on to obtain the final serialisable
|
||||
form. Any media declarations that will be required for deserialisation of the object should
|
||||
be passed to context.add_media().
|
||||
|
||||
This base implementation handles simple JSON-serialisable values such as integers, and
|
||||
wraps them as a ValueNode.
|
||||
"""
|
||||
return ValueNode(obj)
|
||||
|
||||
|
||||
class StringAdapter(BaseAdapter):
|
||||
def build_node(self, obj, context):
|
||||
return StringNode(obj)
|
||||
|
||||
|
||||
class DictAdapter(BaseAdapter):
|
||||
"""Handles serialisation of dicts"""
|
||||
def build_node(self, obj, context):
|
||||
return DictNode({
|
||||
str(key): context.build_node(val)
|
||||
for key, val in obj.items()
|
||||
})
|
||||
|
||||
|
||||
class Adapter(BaseAdapter, metaclass=MediaDefiningClass):
|
||||
"""
|
||||
Handles serialisation of custom types.
|
||||
Subclasses should define:
|
||||
- js_constructor: namespaced identifier for the JS constructor function that will unpack this
|
||||
object
|
||||
- js_args(obj): returns a list of (telepath-packable) arguments to be passed to the constructor
|
||||
- get_media(obj) or class Media: media definitions necessary for unpacking
|
||||
|
||||
The adapter should then be registered with register(adapter, cls).
|
||||
"""
|
||||
|
||||
def get_media(self, obj):
|
||||
return self.media
|
||||
|
||||
def pack(self, obj, context):
|
||||
context.add_media(self.get_media(obj))
|
||||
return (self.js_constructor, self.js_args(obj))
|
||||
|
||||
def build_node(self, obj, context):
|
||||
constructor, args = self.pack(obj, context)
|
||||
return ObjectNode(
|
||||
constructor, [context.build_node(arg) for arg in args]
|
||||
)
|
||||
|
||||
|
||||
class AutoAdapter(Adapter):
|
||||
"""
|
||||
Adapter for objects that define their own telepath_pack method that we can simply delegate to.
|
||||
"""
|
||||
def pack(self, obj, context):
|
||||
return obj.telepath_pack(context)
|
||||
|
||||
|
||||
class JSContextBase:
|
||||
"""
|
||||
Base class for JSContext classes obtained through AdapterRegistry.js_context_class.
|
||||
Subclasses of this are assigned the following class attributes:
|
||||
registry - points to the associated AdapterRegistry
|
||||
telepath_js_path - path to telepath.js (as per standard Django staticfiles conventions)
|
||||
|
||||
A JSContext handles packing a set of values to be used in the same request; calls to
|
||||
JSContext.pack will return the packed representation and also update the JSContext's media
|
||||
property to include all JS needed to unpack the values seen so far.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.media = self.base_media
|
||||
|
||||
# Keep track of media declarations that have already added to self.media - ones that
|
||||
# exactly match a previous one can be ignored, as they will not affect the result
|
||||
self.media_fragments = set([str(self.media)])
|
||||
|
||||
@property
|
||||
def base_media(self):
|
||||
return forms.Media(js=[self.telepath_js_path])
|
||||
|
||||
def add_media(self, media=None, js=None, css=None):
|
||||
media_objects = []
|
||||
if media:
|
||||
media_objects.append(media)
|
||||
if js or css:
|
||||
if isinstance(js, str):
|
||||
# allow passing a single JS file name as equivalent to a singleton list
|
||||
js = [js]
|
||||
media_objects.append(forms.Media(js=js, css=css))
|
||||
|
||||
for media_obj in media_objects:
|
||||
media_str = str(media_obj)
|
||||
if media_str not in self.media_fragments:
|
||||
self.media += media_obj
|
||||
self.media_fragments.add(media_str)
|
||||
|
||||
def pack(self, obj):
|
||||
return ValueContext(self).build_node(obj).emit()
|
||||
|
||||
|
||||
class AdapterRegistry:
|
||||
"""
|
||||
Manages the mapping of Python types to their corresponding adapter implementations.
|
||||
"""
|
||||
js_context_base_class = JSContextBase
|
||||
|
||||
def __init__(self, telepath_js_path='telepath/js/telepath.js'):
|
||||
self.telepath_js_path = telepath_js_path
|
||||
self.adapters = {
|
||||
# Primitive value types that are unchanged on serialisation
|
||||
type(None): BaseAdapter(),
|
||||
bool: BaseAdapter(),
|
||||
int: BaseAdapter(),
|
||||
float: BaseAdapter(),
|
||||
str: StringAdapter(),
|
||||
|
||||
# Container types to be serialised recursively
|
||||
dict: DictAdapter(),
|
||||
# Iterable types (list, tuple, odict_values...) do not have a reliably recognisable
|
||||
# superclass, so will be handled as a special case
|
||||
}
|
||||
|
||||
def register(self, *args, **kwargs):
|
||||
if len(args) == 2 and not kwargs:
|
||||
# called as register(adapter, cls)
|
||||
adapter, cls = args
|
||||
if not isinstance(adapter, BaseAdapter):
|
||||
raise TypeError("register expected a BaseAdapter instance, got %r" % adapter)
|
||||
|
||||
self.adapters[cls] = adapter
|
||||
|
||||
elif not args:
|
||||
# called as a class decorator: @register() or @register(adapter=MyAdapter()) -
|
||||
# the return value here is the function that will receive the class definition
|
||||
adapter = kwargs.get('adapter') or AutoAdapter()
|
||||
if not isinstance(adapter, BaseAdapter):
|
||||
raise TypeError("register expected a BaseAdapter instance, got %r" % adapter)
|
||||
|
||||
def wrapper(cls):
|
||||
# register the class and return it unchanged
|
||||
self.adapters[cls] = adapter
|
||||
return cls
|
||||
|
||||
return wrapper
|
||||
|
||||
elif len(args) == 1 and isinstance(args[0], type):
|
||||
# called as a class decorator @register without parentheses -
|
||||
# we are passed the class definition here
|
||||
cls = args[0]
|
||||
self.adapters[cls] = AutoAdapter()
|
||||
return cls
|
||||
|
||||
else:
|
||||
raise TypeError(
|
||||
"register must be called as register(adapter, cls) or as a class decorator - "
|
||||
"@register or @register(adapter=MyAdapter())"
|
||||
)
|
||||
|
||||
|
||||
def find_adapter(self, cls):
|
||||
for base in cls.__mro__:
|
||||
adapter = self.adapters.get(base)
|
||||
if adapter is not None:
|
||||
return adapter
|
||||
|
||||
@cached_property
|
||||
def js_context_class(self):
|
||||
return type('JSContext', (self.js_context_base_class,), {
|
||||
'registry': self,
|
||||
'telepath_js_path': self.telepath_js_path
|
||||
})
|
||||
|
||||
|
||||
class ValueContext:
|
||||
"""
|
||||
A context instantiated for each top-level value that JSContext.pack is called on. Results from
|
||||
this context's build_node method will be kept in a lookup table. If, over the course of
|
||||
building the node tree for the top level value, we encounter multiple references to the same
|
||||
value, a reference to the existing node will be generated rather than building it again. Calls
|
||||
to add_media are passed back to the parent context so that multiple calls to pack() will have
|
||||
their media combined in a single bundle.
|
||||
"""
|
||||
def __init__(self, parent_context):
|
||||
self.parent_context = parent_context
|
||||
self.registry = parent_context.registry
|
||||
self.raw_values = {}
|
||||
self.nodes = {}
|
||||
self.next_id = 0
|
||||
|
||||
def add_media(self, *args, **kwargs):
|
||||
self.parent_context.add_media(*args, **kwargs)
|
||||
|
||||
def build_node(self, val):
|
||||
obj_id = id(val)
|
||||
try:
|
||||
existing_node = self.nodes[obj_id]
|
||||
except KeyError:
|
||||
# not seen this value before, so build a new node for it and store in self.nodes
|
||||
node = self._build_new_node(val)
|
||||
self.nodes[obj_id] = node
|
||||
# Also keep a reference to the original value to stop it from getting deallocated
|
||||
# and the ID being recycled
|
||||
self.raw_values[obj_id] = val
|
||||
|
||||
return node
|
||||
|
||||
if existing_node.id is None:
|
||||
# Assign existing_node an ID so that we can create references to it
|
||||
existing_node.id = self.next_id
|
||||
self.next_id += 1
|
||||
|
||||
return existing_node
|
||||
|
||||
def _build_new_node(self, obj):
|
||||
adapter = self.registry.find_adapter(type(obj))
|
||||
if adapter:
|
||||
return adapter.build_node(obj, self)
|
||||
|
||||
# No adapter found; try special-case fallbacks
|
||||
|
||||
if isinstance(obj, Promise):
|
||||
# object is a lazy object (e.g. gettext_lazy result);
|
||||
# handle as a string, translated to the currently active locale
|
||||
return StringNode(str(obj))
|
||||
|
||||
# try handling as an iterable
|
||||
try:
|
||||
items = iter(obj)
|
||||
except TypeError: # obj is not iterable
|
||||
raise UnpackableTypeError("don't know how to pack object: %r" % obj)
|
||||
else:
|
||||
return ListNode([self.build_node(item) for item in items])
|
||||
|
||||
|
||||
# define a default registry of adapters. Typically this will be the only instance of
|
||||
# AdapterRegistry in use, although packages may define their own 'private' registry if they
|
||||
# have a set of adapters customised for their own use (e.g. with a custom JS path).
|
||||
|
||||
registry = AdapterRegistry()
|
||||
JSContext = registry.js_context_class
|
||||
register = registry.register
|
||||
BIN
env/lib/python3.10/site-packages/telepath/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/telepath/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/telepath/__pycache__/test_settings.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/telepath/__pycache__/test_settings.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/telepath/__pycache__/tests.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/telepath/__pycache__/tests.cpython-310.pyc
vendored
Normal file
Binary file not shown.
1
env/lib/python3.10/site-packages/telepath/static/telepath/js/telepath.js
vendored
Normal file
1
env/lib/python3.10/site-packages/telepath/static/telepath/js/telepath.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(()=>{"use strict";var t,r,n={923:t=>{function r(t,o,i){return(r=n()?Reflect.construct:function(t,r,n){var o=[null];o.push.apply(o,r);var i=new(Function.bind.apply(t,o));return n&&e(i,n.prototype),i}).apply(null,arguments)}function n(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(t){return!1}}function e(t,r){return(e=Object.setPrototypeOf||function(t,r){return t.__proto__=r,t})(t,r)}function o(t,r){return function(t){if(Array.isArray(t))return t}(t)||function(t,r){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(t)){var n=[],e=!0,o=!1,i=void 0;try{for(var a,u=t[Symbol.iterator]();!(e=(a=u.next()).done)&&(n.push(a.value),!r||n.length!==r);e=!0);}catch(t){o=!0,i=t}finally{try{e||null==u.return||u.return()}finally{if(o)throw i}}return n}}(t,r)||i(t,r)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function i(t,r){if(t){if("string"==typeof t)return a(t,r);var n=Object.prototype.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?a(t,r):void 0}}function a(t,r){(null==r||r>t.length)&&(r=t.length);for(var n=0,e=new Array(r);n<r;n++)e[n]=t[n];return e}function u(t){return(u="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function c(t,r){for(var n=0;n<r.length;n++){var e=r[n];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(t,e.key,e)}}var f=function(){function t(){!function(t,r){if(!(t instanceof r))throw new TypeError("Cannot call a class as a function")}(this,t),this.constructors={}}var n,e;return n=t,(e=[{key:"register",value:function(t,r){this.constructors[t]=r}},{key:"unpack",value:function(t){var r={};return this.scanForIds(t,r),this.unpackWithRefs(t,r,{})}},{key:"scanForIds",value:function(t,r){var n=this;if(null!==t&&"object"===u(t))if(Array.isArray(t))t.forEach((function(t){return n.scanForIds(t,r)}));else{var e=!1;if("_id"in t&&(e=!0,r[t._id]=t),("_type"in t||"_val"in t||"_ref"in t)&&(e=!0),"_list"in t&&(e=!0,t._list.forEach((function(t){return n.scanForIds(t,r)}))),"_args"in t&&(e=!0,t._args.forEach((function(t){return n.scanForIds(t,r)}))),"_dict"in t){e=!0;for(var i=0,a=Object.entries(t._dict);i<a.length;i++){var c=o(a[i],2),f=(c[0],c[1]);this.scanForIds(f,r)}}if(!e)for(var s=0,l=Object.entries(t);s<l.length;s++){var y=o(l[s],2),p=(y[0],y[1]);this.scanForIds(p,r)}}}},{key:"unpackWithRefs",value:function(t,n,e){var c,f,s=this;if(null===t||"object"!==u(t))return t;if(Array.isArray(t))return t.map((function(t){return s.unpackWithRefs(t,n,e)}));if("_ref"in t)c=t._ref in e?e[t._ref]:this.unpackWithRefs(n[t._ref],n,e);else if("_val"in t)c=t._val;else if("_list"in t)c=t._list.map((function(t){return s.unpackWithRefs(t,n,e)}));else if("_dict"in t){c={};for(var l=0,y=Object.entries(t._dict);l<y.length;l++){var p=o(y[l],2),h=p[0],d=p[1];c[h]=this.unpackWithRefs(d,n,e)}}else{if(!("_type"in t)){if("_id"in t)throw new Error("telepath encountered object with _id but no type specified");c={};for(var b=0,v=Object.entries(t);b<v.length;b++){var _=o(v[b],2),m=_[0],g=_[1];c[m]=this.unpackWithRefs(g,n,e)}return c}var j=t._type;c=r(this.constructors[j],function(t){if(Array.isArray(t))return a(t)}(f=t._args.map((function(t){return s.unpackWithRefs(t,n,e)})))||function(t){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(t))return Array.from(t)}(f)||i(f)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}())}return"_id"in t&&(e[t._id]=c),c}}])&&c(n.prototype,e),t}();t.exports=f}},e={};function o(t){if(e[t])return e[t].exports;var r=e[t]={exports:{}};return n[t](r,r.exports,o),r.exports}o.n=t=>{var r=t&&t.__esModule?()=>t.default:()=>t;return o.d(r,{a:r}),r},o.d=(t,r)=>{for(var n in r)o.o(r,n)&&!o.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o.o=(t,r)=>Object.prototype.hasOwnProperty.call(t,r),t=o(923),r=o.n(t),window.telepath=new(r())})();
|
||||
7
env/lib/python3.10/site-packages/telepath/test_settings.py
vendored
Normal file
7
env/lib/python3.10/site-packages/telepath/test_settings.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
INSTALLED_APPS = [
|
||||
'telepath',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
SECRET_KEY = 'not needed'
|
||||
STATIC_URL = '/static/'
|
||||
334
env/lib/python3.10/site-packages/telepath/tests.py
vendored
Normal file
334
env/lib/python3.10/site-packages/telepath/tests.py
vendored
Normal file
@@ -0,0 +1,334 @@
|
||||
import itertools
|
||||
|
||||
from django.utils.translation import activate, gettext_lazy
|
||||
from unittest import TestCase
|
||||
|
||||
from telepath import Adapter, JSContext, register, StringAdapter
|
||||
|
||||
|
||||
class Artist:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
|
||||
@register
|
||||
class Album:
|
||||
def __init__(self, title, artists):
|
||||
self.title = title
|
||||
self.artists = artists
|
||||
|
||||
def telepath_pack(self, context):
|
||||
context.add_media(js='music_player.js')
|
||||
|
||||
return ('music.Album', [self.title, self.artists])
|
||||
|
||||
|
||||
class ArtistAdapter(Adapter):
|
||||
js_constructor = 'music.Artist'
|
||||
|
||||
def js_args(self, obj):
|
||||
return [obj.name]
|
||||
|
||||
|
||||
register(ArtistAdapter(), Artist)
|
||||
|
||||
|
||||
class TestPacking(TestCase):
|
||||
def test_pack_object(self):
|
||||
beyonce = Artist("Beyoncé")
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(beyonce)
|
||||
|
||||
self.assertEqual(result, {'_type': 'music.Artist', '_args': ["Beyoncé"]})
|
||||
|
||||
def test_pack_list(self):
|
||||
destinys_child = [
|
||||
Artist("Beyoncé"), Artist("Kelly Rowland"), Artist("Michelle Williams")
|
||||
]
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(destinys_child)
|
||||
|
||||
self.assertEqual(result, [
|
||||
{'_type': 'music.Artist', '_args': ["Beyoncé"]},
|
||||
{'_type': 'music.Artist', '_args': ["Kelly Rowland"]},
|
||||
{'_type': 'music.Artist', '_args': ["Michelle Williams"]},
|
||||
])
|
||||
|
||||
def test_pack_dict(self):
|
||||
glastonbury = {
|
||||
'pyramid_stage': Artist("Beyoncé"),
|
||||
'acoustic_stage': Artist("Ed Sheeran"),
|
||||
}
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(glastonbury)
|
||||
self.assertEqual(result, {
|
||||
'pyramid_stage': {'_type': 'music.Artist', '_args': ["Beyoncé"]},
|
||||
'acoustic_stage': {'_type': 'music.Artist', '_args': ["Ed Sheeran"]},
|
||||
})
|
||||
|
||||
def test_dict_reserved_words(self):
|
||||
profile = {
|
||||
'_artist': Artist("Beyoncé"),
|
||||
'_type': 'R&B',
|
||||
}
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(profile)
|
||||
|
||||
self.assertEqual(result, {
|
||||
'_dict': {
|
||||
'_artist': {'_type': 'music.Artist', '_args': ["Beyoncé"]},
|
||||
'_type': 'R&B',
|
||||
}
|
||||
})
|
||||
|
||||
def test_recursive_arg_packing(self):
|
||||
dangerously_in_love = Album("Dangerously in Love", [
|
||||
Artist("Beyoncé"),
|
||||
])
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(dangerously_in_love)
|
||||
|
||||
self.assertEqual(result, {
|
||||
'_type': 'music.Album',
|
||||
'_args': [
|
||||
"Dangerously in Love",
|
||||
[
|
||||
{'_type': 'music.Artist', '_args': ["Beyoncé"]},
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
self.assertIn('music_player.js', str(ctx.media))
|
||||
|
||||
def test_object_references(self):
|
||||
beyonce = Artist("Beyoncé")
|
||||
jay_z = Artist("Jay-Z")
|
||||
discography = [
|
||||
Album("Dangerously in Love", [beyonce]),
|
||||
Album("Everything Is Love", [beyonce, jay_z]),
|
||||
]
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(discography)
|
||||
|
||||
self.assertEqual(result, [
|
||||
{
|
||||
'_type': 'music.Album',
|
||||
'_args': [
|
||||
"Dangerously in Love",
|
||||
[
|
||||
{'_type': 'music.Artist', '_args': ["Beyoncé"], '_id': 0},
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
'_type': 'music.Album',
|
||||
'_args': [
|
||||
"Everything Is Love",
|
||||
[
|
||||
{'_ref': 0},
|
||||
{'_type': 'music.Artist', '_args': ["Jay-Z"]},
|
||||
]
|
||||
]
|
||||
},
|
||||
])
|
||||
|
||||
self.assertIn('music_player.js', str(ctx.media))
|
||||
|
||||
def test_list_references(self):
|
||||
destinys_child = [
|
||||
Artist("Beyoncé"), Artist("Kelly Rowland"), Artist("Michelle Williams")
|
||||
]
|
||||
discography = [
|
||||
Album("Destiny's Child", destinys_child),
|
||||
Album("Survivor", destinys_child),
|
||||
]
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(discography)
|
||||
|
||||
self.assertEqual(result, [
|
||||
{
|
||||
'_type': 'music.Album',
|
||||
'_args': [
|
||||
"Destiny's Child",
|
||||
{
|
||||
'_list': [
|
||||
{'_type': 'music.Artist', '_args': ["Beyoncé"]},
|
||||
{'_type': 'music.Artist', '_args': ["Kelly Rowland"]},
|
||||
{'_type': 'music.Artist', '_args': ["Michelle Williams"]},
|
||||
],
|
||||
'_id': 0,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'_type': 'music.Album',
|
||||
'_args': [
|
||||
"Survivor",
|
||||
{'_ref': 0},
|
||||
]
|
||||
},
|
||||
])
|
||||
|
||||
def test_primitive_value_references(self):
|
||||
beyonce_name = "Beyoncé Giselle Knowles-Carter"
|
||||
beyonce = Artist(beyonce_name)
|
||||
discography = [
|
||||
Album("Dangerously in Love", [beyonce]),
|
||||
Album(beyonce_name, [beyonce]),
|
||||
]
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(discography)
|
||||
|
||||
self.assertEqual(result, [
|
||||
{
|
||||
'_type': 'music.Album',
|
||||
'_args': [
|
||||
"Dangerously in Love",
|
||||
[
|
||||
{
|
||||
'_type': 'music.Artist',
|
||||
'_args': [{'_val': "Beyoncé Giselle Knowles-Carter", '_id': 0}],
|
||||
'_id': 1,
|
||||
},
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
'_type': 'music.Album',
|
||||
'_args': [
|
||||
{'_ref': 0},
|
||||
[
|
||||
{'_ref': 1},
|
||||
]
|
||||
]
|
||||
},
|
||||
])
|
||||
|
||||
def test_avoid_primitive_value_references_for_short_strings(self):
|
||||
beyonce_name = "Beyoncé"
|
||||
beyonce = Artist(beyonce_name)
|
||||
discography = [
|
||||
Album("Dangerously in Love", [beyonce]),
|
||||
Album(beyonce_name, [beyonce]),
|
||||
]
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(discography)
|
||||
|
||||
self.assertEqual(result, [
|
||||
{
|
||||
'_type': 'music.Album',
|
||||
'_args': [
|
||||
"Dangerously in Love",
|
||||
[
|
||||
{
|
||||
'_type': 'music.Artist',
|
||||
'_args': ["Beyoncé"],
|
||||
'_id': 1,
|
||||
},
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
'_type': 'music.Album',
|
||||
'_args': [
|
||||
"Beyoncé",
|
||||
[
|
||||
{'_ref': 1},
|
||||
]
|
||||
]
|
||||
},
|
||||
])
|
||||
|
||||
def test_lazy_translation_objects(self):
|
||||
yes = Artist(gettext_lazy("Yes"))
|
||||
|
||||
activate('en')
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(yes)
|
||||
self.assertEqual(result, {'_type': 'music.Artist', '_args': ["Yes"]})
|
||||
|
||||
activate('fr')
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(yes)
|
||||
self.assertEqual(result, {'_type': 'music.Artist', '_args': ["Oui"]})
|
||||
|
||||
|
||||
class Ark:
|
||||
def __init__(self, animals):
|
||||
self.animals = animals
|
||||
|
||||
def animals_by_type(self):
|
||||
return itertools.groupby(self.animals, lambda animal: animal['type'])
|
||||
|
||||
|
||||
class ArkAdapter(Adapter):
|
||||
js_constructor = 'boats.Ark'
|
||||
|
||||
def js_args(self, obj):
|
||||
return [obj.animals_by_type()]
|
||||
|
||||
|
||||
register(ArkAdapter(), Ark)
|
||||
|
||||
|
||||
class TestIDCollisions(TestCase):
|
||||
def test_grouper_object_collisions(self):
|
||||
"""
|
||||
Certain functions such as itertools.groupby will cause new objects (namely, tuples and
|
||||
custom itertools._grouper iterables) to be created in the course of iterating over the
|
||||
object tree. If we're not careful, these will be released and the memory reallocated to
|
||||
new objects while we're still iterating, leading to ID collisions.
|
||||
"""
|
||||
# create 100 Ark objects all with distinct animals (no object references are re-used)
|
||||
arks = [
|
||||
Ark([
|
||||
{'type': 'lion', 'name': 'Simba %i' % i}, {'type': 'lion', 'name': 'Nala %i' % i},
|
||||
{'type': 'dog', 'name': 'Lady %i' % i}, {'type': 'dog', 'name': 'Tramp %i' % i},
|
||||
])
|
||||
for i in range(0, 100)
|
||||
]
|
||||
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(arks)
|
||||
|
||||
self.assertEqual(len(result), 100)
|
||||
for i, ark in enumerate(result):
|
||||
# each object should be represented in full, with no _id or _ref keys
|
||||
self.assertEqual(ark, {
|
||||
'_type': 'boats.Ark',
|
||||
'_args': [
|
||||
[
|
||||
['lion', [{'type': 'lion', 'name': 'Simba %i' % i}, {'type': 'lion', 'name': 'Nala %i' % i}]],
|
||||
['dog', [{'type': 'dog', 'name': 'Lady %i' % i}, {'type': 'dog', 'name': 'Tramp %i' % i}]],
|
||||
]
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
class StringLike():
|
||||
def __init__(self, val):
|
||||
self.val = val.upper()
|
||||
|
||||
def __str__(self):
|
||||
return self.val
|
||||
|
||||
|
||||
class StringLikeAdapter(StringAdapter):
|
||||
def build_node(self, obj, context):
|
||||
return super().build_node(str(obj), context)
|
||||
|
||||
|
||||
register(StringLikeAdapter(), StringLike)
|
||||
|
||||
|
||||
class TestPackingToString(TestCase):
|
||||
def test_pack_to_string(self):
|
||||
val = [
|
||||
"real string",
|
||||
StringLike("stringlike"),
|
||||
]
|
||||
|
||||
ctx = JSContext()
|
||||
result = ctx.pack(val)
|
||||
|
||||
self.assertEqual(result, ["real string", "STRINGLIKE"])
|
||||
Reference in New Issue
Block a user