3
3
import websockets
4
4
import json
5
5
from observablecollections .observablelist import ObservableList
6
+ import traceback
6
7
7
8
g_components = {}
8
9
g_instances = {}
9
10
10
11
PREFIX = {
11
- 'IN' : '🢀║ ' ,
12
- 'OUT' : ' ║🢂' ,
13
- 'END' : '╚╩╝' ,
14
- 'ERR' : ' │ ⚠⚠⚠' ,
15
- 'def' : ' │ ►►►' ,
12
+ 'IN' : '🢀║ IN' ,
13
+ 'OUT' : ' ║🢂 OUT' ,
14
+ 'END' : '╚╩╝' ,
15
+ 'ENDx' : ' ..' ,
16
+ 'ENDe' : ' ⚠⚠' ,
17
+ 'ERR' : ' │ ⚠⚠⚠⚠⚠⚠' ,
18
+ 'def' : ' │ ►►►%s►►►' ,
16
19
}
17
- infos = ('DEPS' + ' IN OUT END ERR' ).split ()
20
+ # list of enabled logging
21
+ infos = ('DEPS ENDx ENDe' + ' IN OUT END ERR' ).split ()
18
22
def info (k , * args , ** kwargs ):
19
23
if k in infos :
20
- pre = PREFIX ['def' ]
21
- if k in PREFIX .keys (): pre = PREFIX [k ]
22
- print (' ' , pre , k , * args , ** kwargs )
24
+ if k in PREFIX .keys ():
25
+ pre = PREFIX [k ]
26
+ else :
27
+ pre = PREFIX ['def' ]% (k ,)
28
+ print (' ' , pre , * args , ** kwargs )
29
+
30
+ def info_exception (k , pre = '' ):
31
+ for l in traceback .format_exc ().split ('\n ' ):
32
+ info (k , pre + l )
33
+
23
34
24
35
def is_ndarray (a ):
25
36
try :
@@ -40,18 +51,19 @@ def cache_stale(cache, k, v):
40
51
return (v != cache [k ]).any ()
41
52
return cache [k ] != v
42
53
43
- def make_prop (k ):
54
+ def make_prop (k , no_broadcast ):
44
55
f = '_' + k
45
56
def get (o ):
46
57
if hasattr (o , '_v_currently_computing' ) and o ._v_currently_computing != []: # may be triggered before "start" (in __init__)
47
58
o ._v_deps [o ._v_currently_computing [- 1 ]].append (k )
48
- info (" DEPS" , o ._v_deps )
59
+ info (' DEPS' , o ._v_deps )
49
60
return getattr (o , f )
50
61
def set (o , v ):
51
62
def trigger_on_change (* args ):
52
63
call_watcher (o , k )
53
64
update_computed_depending_on (o , k )
54
- broadcast (o , k )
65
+ if not no_broadcast :
66
+ broadcast (o , k )
55
67
if type (v ) == list :
56
68
v = ObservableList (v )
57
69
v .attach (trigger_on_change )
@@ -102,32 +114,34 @@ def recompute_computed(o, k):
102
114
103
115
def field_should_be_synced (cls ):
104
116
novue = cls ._v_novue if hasattr (cls , '_v_novue' ) else []
105
- return lambda k : k [0 ] != '_' and k not in novue
117
+ if hasattr (cls , 'props' ):
118
+ novue .append ('props' )
119
+ return lambda k : k [0 ] != '_' and not k .startswith ('computed_' ) and k not in novue
106
120
107
121
# class annotation
108
122
def model (cls ):
123
+ if not hasattr (cls , 'props' ): setattr (cls , 'props' , [])
109
124
g_components [cls .__name__ ] = cls
110
125
prefix = 'computed_'
111
126
novue = cls ._v_novue if hasattr (cls , '_v_novue' ) else []
112
127
cls ._v_nobroadcast = cls ._v_nobroadcast if hasattr (cls , '_v_nobroadcast' ) else []
113
128
computed = [k [len (prefix ):] for k in dir (cls ) if k .startswith (prefix )]
114
129
cls ._v_computed = {}
130
+ cls ._v_just_schedule = True
115
131
for k in computed :
116
132
cls ._v_computed [k ] = getattr (cls , prefix + k )
117
133
setattr (cls , k , make_computed_prop (k ))
118
134
for k in filter (field_should_be_synced (cls ), dir (cls )):
119
135
if not callable (getattr (cls , k )):
120
136
v = getattr (cls , k )
121
137
setattr (cls , '_' + k , v )
122
- setattr (cls , k , make_prop (k ))
138
+ setattr (cls , k , make_prop (k , k in cls . props ))
123
139
return cls
124
140
125
141
126
142
def broadcast (self , k ):
127
- print ("BCAST" , hasattr (self , '__id' ), k )
128
143
if not hasattr (self , '__id' ): return # no id yet, still building
129
144
if k in self ._v_nobroadcast : return
130
- print (" " , self .__id , k , getattr (self , k ))
131
145
asyncio .ensure_future (broadcast_update (self .__id , k , getattr (self , k )))
132
146
133
147
def call_watcher (o , k ):
@@ -158,18 +172,19 @@ def next_instance_id():
158
172
159
173
async def handleClient (websocket , path ):
160
174
# TODO: these all should be per-id? to avoid unncessary calls?
175
+ # TODO: cleanup g_instances (on socket disconnect at least)
161
176
all .append (websocket )
162
177
try :
163
178
while True :
164
179
comm = await websocket .recv ()
165
- print ("COMM" , comm )
166
- if comm == 'INIT' :
180
+ if comm == 'INIT' or comm == 'INFO' :
167
181
clss_name = await websocket .recv ()
168
- print ( "CLASS NAME" , clss_name )
182
+ info ( 'IN' , comm , clss_name )
169
183
if clss_name == 'ROOT' :
170
184
id = clss_name
171
185
o = g_instances [id ]
172
186
clss = type (o )
187
+ o ._v_just_schedule = False
173
188
elif clss_name not in g_components :
174
189
info ('ERR' , 'Component type ' + clss_name + ' not found (missing @model?).' )
175
190
else :
@@ -178,33 +193,43 @@ async def handleClient(websocket, path):
178
193
id = next_instance_id ()
179
194
setattr (o , '__id' , id )
180
195
setup_model_object_infra (o )
181
- data = await websocket .recv ()
182
- info ('IN' , 'PROPS' , data )
183
- data = json .loads (data )
184
- for k in data :
185
- setattr (o , k , '' )
186
- setup_model_object_infra (o )
187
196
g_instances [id ] = o
197
+ if comm == 'INIT' :
198
+ prop_values = await websocket .recv ()
199
+ prop_values = json .loads (prop_values )
200
+ info ('IN' , prop_values )
201
+ for k in prop_values .keys ():
202
+ setattr (o , k , prop_values [k ])
203
+ o ._v_just_schedule = False
204
+ recompute_scheduled_computed (o )
205
+ else :
206
+ o ._v_just_schedule = False
207
+ # we do it as for computed, we have no reasonable default...
208
+ # as they might depend on the properties (and thus their type is unknown)
209
+ # it would be called anyway on state[k] = getattr(o, k) below
210
+ # and for now we don't know what to put as a default value (that will be temporary present on the js side)
211
+ recompute_scheduled_computed (o )
188
212
state = {}
213
+ props = o .props if hasattr (o , 'props' ) else []
189
214
methods = []
190
215
for k in filter (field_should_be_synced (clss ), dir (clss )):
191
- if callable (getattr (o , k )):
216
+ if k in props : continue
217
+ if callable (getattr (clss , k )):
192
218
methods .append (k )
193
219
else :
194
220
state [k ] = getattr (o , k )
195
221
state [k ] = sanitize (state [k ])
196
222
to_send = {
197
223
'id' : id ,
224
+ 'props' : props ,
198
225
'state' : state ,
199
226
'methods' : methods
200
227
}
201
- info ('OUT' , 'INIT' , clss . __name__ , id , list ( state . keys ()) )
202
- await websocket .send ('INIT ' + json .dumps (to_send ))
228
+ info ('OUT' , comm , to_send )
229
+ await websocket .send (comm + ' ' + json .dumps (to_send ))
203
230
elif comm == 'CALL' :
204
231
id = await websocket .recv ()
205
- print (type (id ), id )
206
232
o = g_instances [id ]
207
- print (o )
208
233
meth = await websocket .recv ()
209
234
info ('IN' , 'METH' , meth )
210
235
data = await websocket .recv ()
@@ -213,7 +238,8 @@ async def handleClient(websocket, path):
213
238
###############
214
239
res = getattr (o , meth )(* json .loads (data ))
215
240
except Exception as inst :
216
- info ('ERR' , '... exception while calling method:' , inst )
241
+ info ('ERR' , 'Exception while calling method:' , inst )
242
+ info_exception ('ERR' , ' ' )
217
243
elif comm == 'UPDATE' :
218
244
id = await websocket .recv ()
219
245
o = g_instances [id ]
@@ -225,11 +251,18 @@ async def handleClient(websocket, path):
225
251
call_watcher (o , k )
226
252
except Exception as e :
227
253
info ('ERR' , 'Not a JSON value (or watcher error) for key' , k , '->' , v , '//' , e )
228
- import traceback
229
- traceback .print_exc ()
230
- except :
231
- info ('END' , 'websocket disconnected' )
232
- pass # disconnected
254
+ info_exception ('ERR' , ' ' )
255
+ except websockets .ConnectionClosed as e :
256
+ if e .code == 1001 :
257
+ info ('END' , 'disconnected' )
258
+ elif e .code == 1005 :
259
+ info ('END' , 'closed' )
260
+ else :
261
+ info ('END' , e )
262
+ info_exception ('ENDx' )
263
+ except Exception as e :
264
+ info ('END' , e )
265
+ info_exception ('ENDe' )
233
266
return handleClient
234
267
235
268
# decorator
@@ -245,7 +278,6 @@ def setup_model_object_infra(o):
245
278
cls = o .__class__
246
279
o ._v_cache = {}
247
280
o ._v_currently_computing = []
248
- o ._v_just_schedule = False
249
281
o ._v_schedule_recomputing = []
250
282
o ._v_deps = {}
251
283
# Set all attributes, so they get wrapped (e.g., observable) if necessary
0 commit comments