Skip to content

Commit cce8a14

Browse files
committed
Making properties un-redundant (but needing a default value)
There is still a blink, but we would need async data in vuejs. Consider changing how the default is given, currently, not so vuejs: props = ['i'] i = 0 could be the more verbose, more vuejs (check): props = {'val': {'default': 0}}
1 parent 9f6d60d commit cce8a14

File tree

4 files changed

+119
-59
lines changed

4 files changed

+119
-59
lines changed

example-8.html

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
<progress style="width: 500px" max="100" :value="i"></progress>
2121
<my-show :val="i" pre="my-show 1:"></my-show>
2222
<my-show :val="i" pre="my-show 2:"></my-show>
23+
<my-square :val="i" pre="my-square 1:"></my-square>
24+
<my-square :val="i" :pre="'my-square '+i+':'"></my-square>
2325
<my-dummy></my-dummy>
2426
<my-dummy></my-dummy>
25-
<my-square :val="i" pre="my-square 1:"></my-square>
26-
<my-square :val="i" pre="my-square 2:"></my-square>
2727
</div>
2828

2929
<script src="vuejspython.js"></script>
@@ -36,12 +36,9 @@
3636
/* vuejspython component */
3737
vuejspython.component('Dummy', 'my-dummy', {
3838
template: '<div> {{i}} <button @click="incr(1)">+</button> -> {{i**2}} (in the vjspy template)</div>',
39-
beforeCreate: () => {console.log("beforeCreate")},
40-
created: () => {console.log("created")},
41-
data: () => ({i:parseInt(Math.random()*100)})
4239
})
4340
vuejspython.component('Square', 'my-square', {
44-
props: ['val', 'pre'],
41+
props: ['pre'], /* these props are visible only on the js side */
4542
template: '<div> {{pre}} {{val}} -> {{square}} (py-computed value, in the component)</div>',
4643
data: () => ({})
4744
})

example-8.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ def incr(self, d):
2626

2727
@model
2828
class Square:
29-
val = 0 # prop
29+
props = ['val'] # python defined props
30+
val = 0 # need a value for typing reasons, maybe this will change
31+
# TODO? consider a more vuejs syntax like: props = {'val': {'default': 0}}
3032

3133
def computed_square(self):
3234
return self.val ** 2

vuejspython.js

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ vuejspython.start = function(wsurl, opt={}) {
2222
})
2323
ws.addEventListener('message', function(a) {
2424
a = a.data
25-
console.log("rcv root", a)
2625
if (a.startsWith('INIT ')) {
2726
a = JSON.parse(a.substr('INIT '.length))
2827
let computed = {...opt.computed}
@@ -89,37 +88,32 @@ vuejspython.component = function(pyClass, name, opt={}) {
8988

9089
// later, consider refactoring if the two are really similar
9190
if (opt.props === undefined) opt.props = []
91+
if (opt.data === undefined) opt.data = ()=>({})
9292

9393
let created = opt.created || (()=>{})
94-
for (let k of ['created']) delete opt[k]
95-
96-
// TODO make a first call to know what is in data (what is reactive)
97-
// same for python-computed I guess
98-
// ... and this will make all this registration async, so async component
99-
// currently, we are forced to declare it also in js
94+
let props = opt.props
95+
let optdata = opt.data
96+
for (let k of ['created', 'data', 'props']) delete opt[k]
10097

101-
// TODO watch properties below to send notif
102-
Vue.component(name, {
98+
let description = (pyState) => ({
10399
created: function() {
100+
let vm = this
101+
vm.__id = 'NOT-SET-YET'
104102
let wsurl = vuejspython.wsurl
105103
let ws = new WebSocket(wsurl)
106104
let valuesWhere = {}
107105
ws.addEventListener('open', function(a) {
108106
ws.send('INIT')
109107
ws.send(pyClass)
110-
ws.send(JSON.stringify(opt.props))
108+
ws.send(JSON.stringify(vm.$props))
111109
})
112110

113-
let vm = this
114-
vm.__id = 'NOT-SET-YET'
115111
ws.addEventListener('message', function(a) {
116112
a = a.data
117113
vm.__ws = ws
118-
console.log("rcv", name, a)
119114
if (a.startsWith('INIT ')) {
120115
a = JSON.parse(a.substr('INIT '.length))
121116
vm.__id = a.id
122-
console.log("RECEIVED INIT ID", pyClass, a.id, Object.keys(a.state))
123117

124118
for (let k in a.state) {
125119
vm.$set(vm, k, a.state[k])
@@ -133,6 +127,17 @@ vuejspython.component = function(pyClass, name, opt={}) {
133127
ws.send(JSON.stringify(v))
134128
})
135129
}
130+
for (let k of a.props) {
131+
vm.$watch(k, function (v, old) {
132+
if (this.__id === 'NOT-SET-YET') return
133+
if (valuesWhere[k] == v) return
134+
delete valuesWhere[k]
135+
ws.send('UPDATE')
136+
ws.send(vm.__id)
137+
ws.send(k)
138+
ws.send(JSON.stringify(v))
139+
}, {immediate: true})
140+
}
136141
for (let k of a.methods) {
137142
vm[k] = function(...args) {
138143
ws.send('CALL')
@@ -147,7 +152,6 @@ vuejspython.component = function(pyClass, name, opt={}) {
147152
let k = parts[2]
148153
if (upid === vm.__id) {
149154
let v = a.substr(parts.join(' ').length)
150-
console.log(v)
151155
v = JSON.parse(v)
152156
valuesWhere[k] = v
153157
vm.$set(vm, k, v)
@@ -157,6 +161,31 @@ vuejspython.component = function(pyClass, name, opt={}) {
157161

158162
created.bind(this)() // not sure when it is best to call it, or whether we should accept it at all
159163
},
164+
data: () => ({
165+
...pyState,
166+
...optdata()
167+
}),
168+
props,
160169
...opt
161170
})
171+
172+
173+
Vue.component(name, function (resolve, reject) {
174+
let wsurl = vuejspython.wsurl
175+
let wsMeta = new WebSocket(wsurl)
176+
wsMeta.addEventListener('open', function(a) {
177+
wsMeta.send('INFO')
178+
wsMeta.send(pyClass)
179+
})
180+
wsMeta.addEventListener('message', function(a) {
181+
a = a.data
182+
if (a.startsWith('INFO ')) {
183+
a = JSON.parse(a.substr('INFO '.length))
184+
props = [...props, ...a.props]
185+
let desc = description(a.state)
186+
resolve(desc)
187+
wsMeta.close()
188+
}
189+
})
190+
})
162191
}

vuejspython.py

Lines changed: 69 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,34 @@
33
import websockets
44
import json
55
from observablecollections.observablelist import ObservableList
6+
import traceback
67

78
g_components = {}
89
g_instances = {}
910

1011
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►►►',
1619
}
17-
infos = ('DEPS'+' IN OUT END ERR').split()
20+
# list of enabled logging
21+
infos = ('DEPS ENDx ENDe'+' IN OUT END ERR').split()
1822
def info(k, *args, **kwargs):
1923
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+
2334

2435
def is_ndarray(a):
2536
try:
@@ -40,18 +51,19 @@ def cache_stale(cache, k, v):
4051
return (v != cache[k]).any()
4152
return cache[k] != v
4253

43-
def make_prop(k):
54+
def make_prop(k, no_broadcast):
4455
f = '_'+k
4556
def get(o):
4657
if hasattr(o, '_v_currently_computing') and o._v_currently_computing != []: # may be triggered before "start" (in __init__)
4758
o._v_deps[o._v_currently_computing[-1]].append(k)
48-
info("DEPS", o._v_deps)
59+
info('DEPS', o._v_deps)
4960
return getattr(o, f)
5061
def set(o, v):
5162
def trigger_on_change(*args):
5263
call_watcher(o, k)
5364
update_computed_depending_on(o, k)
54-
broadcast(o, k)
65+
if not no_broadcast:
66+
broadcast(o, k)
5567
if type(v) == list:
5668
v = ObservableList(v)
5769
v.attach(trigger_on_change)
@@ -102,32 +114,34 @@ def recompute_computed(o, k):
102114

103115
def field_should_be_synced(cls):
104116
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
106120

107121
# class annotation
108122
def model(cls):
123+
if not hasattr(cls, 'props'): setattr(cls, 'props', [])
109124
g_components[cls.__name__] = cls
110125
prefix = 'computed_'
111126
novue = cls._v_novue if hasattr(cls, '_v_novue') else []
112127
cls._v_nobroadcast = cls._v_nobroadcast if hasattr(cls, '_v_nobroadcast') else []
113128
computed = [k[len(prefix):] for k in dir(cls) if k.startswith(prefix)]
114129
cls._v_computed = {}
130+
cls._v_just_schedule = True
115131
for k in computed:
116132
cls._v_computed[k] = getattr(cls, prefix+k)
117133
setattr(cls, k, make_computed_prop(k))
118134
for k in filter(field_should_be_synced(cls), dir(cls)):
119135
if not callable(getattr(cls, k)):
120136
v = getattr(cls, k)
121137
setattr(cls, '_'+k, v)
122-
setattr(cls, k, make_prop(k))
138+
setattr(cls, k, make_prop(k, k in cls.props))
123139
return cls
124140

125141

126142
def broadcast(self, k):
127-
print("BCAST", hasattr(self, '__id'), k)
128143
if not hasattr(self, '__id'): return # no id yet, still building
129144
if k in self._v_nobroadcast: return
130-
print(" ", self.__id, k, getattr(self, k))
131145
asyncio.ensure_future(broadcast_update(self.__id, k, getattr(self, k)))
132146

133147
def call_watcher(o, k):
@@ -158,18 +172,19 @@ def next_instance_id():
158172

159173
async def handleClient(websocket, path):
160174
# TODO: these all should be per-id? to avoid unncessary calls?
175+
# TODO: cleanup g_instances (on socket disconnect at least)
161176
all.append(websocket)
162177
try:
163178
while True:
164179
comm = await websocket.recv()
165-
print("COMM", comm)
166-
if comm == 'INIT':
180+
if comm == 'INIT' or comm == 'INFO':
167181
clss_name = await websocket.recv()
168-
print("CLASS NAME", clss_name)
182+
info('IN', comm, clss_name)
169183
if clss_name == 'ROOT':
170184
id = clss_name
171185
o = g_instances[id]
172186
clss = type(o)
187+
o._v_just_schedule = False
173188
elif clss_name not in g_components:
174189
info('ERR', 'Component type ' + clss_name + ' not found (missing @model?).')
175190
else:
@@ -178,33 +193,43 @@ async def handleClient(websocket, path):
178193
id = next_instance_id()
179194
setattr(o, '__id', id)
180195
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)
187196
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)
188212
state = {}
213+
props = o.props if hasattr(o, 'props') else []
189214
methods = []
190215
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)):
192218
methods.append(k)
193219
else:
194220
state[k] = getattr(o, k)
195221
state[k] = sanitize(state[k])
196222
to_send = {
197223
'id': id,
224+
'props': props,
198225
'state': state,
199226
'methods': methods
200227
}
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))
203230
elif comm == 'CALL':
204231
id = await websocket.recv()
205-
print(type(id), id)
206232
o = g_instances[id]
207-
print(o)
208233
meth = await websocket.recv()
209234
info('IN', 'METH', meth)
210235
data = await websocket.recv()
@@ -213,7 +238,8 @@ async def handleClient(websocket, path):
213238
###############
214239
res = getattr(o, meth)(*json.loads(data))
215240
except Exception as inst:
216-
info('ERR', '... exception while calling method:', inst)
241+
info('ERR', 'Exception while calling method:', inst)
242+
info_exception('ERR', ' ')
217243
elif comm == 'UPDATE':
218244
id = await websocket.recv()
219245
o = g_instances[id]
@@ -225,11 +251,18 @@ async def handleClient(websocket, path):
225251
call_watcher(o, k)
226252
except Exception as e:
227253
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')
233266
return handleClient
234267

235268
# decorator
@@ -245,7 +278,6 @@ def setup_model_object_infra(o):
245278
cls = o.__class__
246279
o._v_cache = {}
247280
o._v_currently_computing = []
248-
o._v_just_schedule = False
249281
o._v_schedule_recomputing = []
250282
o._v_deps = {}
251283
# Set all attributes, so they get wrapped (e.g., observable) if necessary

0 commit comments

Comments
 (0)