1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
|
# Copyright 2005-2009 Nanorex, Inc. See LICENSE file for details.
"""
changes.py - utilities for tracking changes, usage, nested events, etc.
@author: Bruce
@version: $Id$
@copyright: 2005-2009 Nanorex, Inc. See LICENSE file for details.
History:
original features were stubs and have mostly been removed.
bruce 050803 new features to help with graphics updates when
preferences are changed.
bruce 061022 soon some of this will be used in the new exprs module.
Later it will need optimization for that use.
...
bruce 071106 split changedicts.py out of changes.py
"""
from utilities.debug import print_compact_traceback, print_compact_stack
import foundation.env as env
from utilities.constants import noop
from utilities import debug_flags
from utilities.Comparison import same_vals #bruce 060306
# == Usage tracking.
_print_all_subs = False # can be set to True by debuggers or around bits of code being debugged
class OneTimeSubsList: #bruce 050804; as of 061022, looks ok for use in new exprs module (and being used there); doc revised 061119
"""
This object corresponds to (one momentary value of) some variable or aspect whose uses (as inputs to
other computations) can be tracked (causing a ref to this object to get added to a set of used things).
When the user of the value corresponding to this object finds this object in the list of all such things
it used during some computation, it can subscribe some function (eg an invalidator for the result of that
computation, which (result) might be another variable, or a side effect like the effect of drawing something)
to the event of that value becoming invalid. [See SubUsageTrackingMixin and usage_tracker_obj for how this is done.]
That subscription will be fulfilled (by self) at most once, ASAP after the corresponding value is known to be invalid.
Exceptions in fulfilling it might be debug-reported but will cause no harm to this object (since it's important
that they don't prevent this object from fulfilling its other subscriptions).
The value-user can also remove that subscription before it gets fulfilled
(or even after? not sure, esp re duplicate funcs provided).
Note: this object's implem seems general enough for any one-time-only subslist,
even though this docstring is only about its application to usage-tracking and inval-subscription.
[###e what to do about that? Is it really reusable??]
[###k Does it permit resubs while fulfilling them (as in changed_members)??]
[Does SelfUsageTrackingMixin make a new one each time it fulfills old one? Yes [verified 061119].]
"""
def __init__(self, debug_name = None):
self.debug_name = debug_name #061118
self._subs = {}
# map from id(func) to list of (zero or more identical elements) func (which are of course strongrefs to func).
# (We need the dict for efficient removal, and multiple copies of func in case duplicate funcs are provided
# (which is quite possible), but that doesn't need to be optimized for, so using a list of copies seems simplest.
# Implem note if we ever do this in C: could replace id(func) with hash(id(func)), i.e. not distinguish between
# equal funcs and funcs with same hashcode.)
def __repr__(self): #061118
return "<%s%s at %#x>" % (self.__class__.__name__, self.debug_name and ("(%s)" % self.debug_name) or '', id(self))
def subscribe(self, func):
try:
subs = self._subs
except AttributeError:
# our event already occurred (as indicated by fulfill_all removing this attribute).
# [not sure if this ever happens in initial uses of this class]
# (note: if subscribe could come before __init__, e.g. due to some sort of bug
# in which this obj got unpickled, this could also happen.)
if debug_flags.atom_debug:
#e Remove this debug print if this non-error happens routinely (and turns out not to reveal a bug).
# It never happened enough to notice until circa 061118 in exprs module; I don't yet know why it happens there,
# but it started after an LvalUnset exception was added and some exception-ignorers were changed to pass that on;
# it is also associated (but not always) with the same invals happening twice.
print_compact_stack( "atom_debug: fyi: %r's event already occurred, fulfilling new subs %r immediately: " % (self, func))
self._fulfill1(func)
else:
lis = subs.setdefault( id(func), [])
lis.append(func)
return #e return a unique "removal code" for this subs?? or None if we just fulfilled it now.
def fulfill_all(self, debug = False):
"""
Fulfill all our subscriptions now (and arrange to immediately
fulfill any subscriptions that come in later).
You must only call this once.
"""
subs = self._subs
del self._subs # (this would expose the class's definition of _subs, if there is one; presently there's not)
# Does this make it illegal to subscribe to this object ever again? No!
# It causes such a subs to be immediately fulfilled.
for sublis in subs.values(): #bruce 060405 precaution: itervalues -> values (didn't analyze whether needed)
for sub1 in sublis:
# note: fulfilling at most one elt might be acceptable if we redefined API to permit that
# (since all elts are identical),
# but it wouldn't much simplify the code, since the list length can legally be zero.
self._fulfill1(sub1, debug = debug)
pass
return
def _list_of_subs(self): #bruce 070109
"""
For debugging: return a newly made list of our subscriptions
(not removing duplicates), without changing or fulfilling them.
"""
res = []
subs = self._subs
for sublis in subs.itervalues():
res.extend(sublis)
return res
def remove_all_subs(self): #bruce 070109 experimental (for trying to fix a bug in exprs module), might become normal
"""
[private while experimental]
WARNING: I'm not sure makes sense except on an owning obj
since we are a 'one time' sublist
"""
try:
self._subs.clear() # does self._subs always exist when this is called? I think so but I'm not sure, so check for this.
except AttributeError:
print "no _subs in %r so nothing to clear in remove_all_subs" % (self,)
return
def _fulfill1(self, sub1, debug = False):
# note: the only use of self is in the debug msg.
try:
if debug or _print_all_subs:
print "%r: fulfilling sub1 %r" % (self, sub1)
sub1() #e would an arg of self be useful?
except:
# We have no choice but to ignore the exception, even if it's always a bug (as explained in docstring).
# The convention should be to make sure sub1 won't raise an exception (to make bugs more noticable),
# so this is always a likely bug, so we print it; but only when atom_debug, in case it might get printed
# a lot in some circumstances. [revised, see below]
if True or debug or debug_flags.atom_debug:
#bruce 070816 included True in that condition, to avoid silently discarding exceptions indicating real bugs.
print_compact_traceback("bug: exception in subs %r ignored by %r: " % (sub1, self) )
print_compact_stack(" note: here is where that exception occurred: ", skip_innermost_n = 1) #bruce 080917 revised
return
def remove_subs(self, func): # note: this has never been used as of long before 061022, and looks potentially unsafe (see below)
"""
Make sure (one subscribed instance of) func will never be fulfilled.
WARNING: calling this on a subs (a specific instance of func) that was already fulfilled is an UNDETECTED ERROR.
But it's ok to subscribe the same func eg 5 times, let 2 of those be fulfilled, and remove the other 3.
"""
# optimize by assuming it's there -- if not, various exceptions are possible.
# [Note, bruce 061022: this assumption looks like a bug, in light of the use of remove_all_instances.
# At least, it would be an error to use both of them on the same sublis within self,
# which means in practice that it would be hard to safely use both of them on the same OneTimeSubsList object.]
self._subs[id(func)].pop()
# (this can create a 0-length list which remains in the dict. seems ok provided self is not recycled.)
return
def remove_all_instances(self, func):
"""
#doc; legal even if no instances, but only if an instance
once existed (but this might not be checked for).
"""
try:
del self._subs[id(func)]
except KeyError:
pass # not sure this ever happens, but it's legal (if we call this multiple times on one func)
except AttributeError:
if 0 and debug_flags.atom_debug:
print "atom_debug: fyi: %r's event already occurred, in remove_all_instances( %r)" % (self, func)
pass # this happens routinely after fulfill_all removes self._subs,
# since our recipient is too lazy to only remove its other subs when one gets fulfilled --
# it just removes all the subs it had, including the one that got fulfilled.
except:
# this detected the following bug during development (subsequently fixed):
# bug: self._subs is None: exceptions.TypeError: object does not support item deletion
print_compact_traceback("bug: self._subs is %r: " % (self._subs,) )
return
pass # end of class OneTimeSubsList
# ==
class SelfUsageTrackingMixin: #bruce 050804; docstring revised 090212
"""
You can mix this into client classes which need to let all other code
track uses and changes (or invalidations) of their "main value".
What "main value" means is up to the client class. Usually it's the value
returned by accessing some "get method" (in the client class instance
itself, or if that class implements a "high-level slot", then in an instance
of the slot's client), but it can instead be an externally stored value
or implicit value, or even something like the side effects that a
certain method would perform, or the cumulative side effects that would
have occurred over all calls of a certain method if it was called again
now (e.g. a method to keep something up to date). In the more complex cases
of what value is being tracked, there is still usually a method that must
be called by external code to indicate a use of that value (and sometimes
to return a related quantity such as a change indicator), which can be
considered to be a kind of "get method" even though it doesn't actually
return the tracked value.
If a client class needs to permit tracking of more than one value or aspect,
it should use more than one instance of this class or a client class,
one for each tracked value. (Our subclass UsageTracker can be used as a
"stand-alone instance of this class".)
To accomplish the tracking, the client class instances must notice all
uses and changes of the "main value" they want to track, and call the
following methods whenever these occur:
* for uses, call self.track_use() (must be called on every use, not just
the first use after a change, in case the using entity is different);
* for changes or invalidations, call self.track_change() or
self.track_inval() (these are presently synonyms but might not always be).
For more info see the docstrings of the specific methods.
See also code comments warning about an issue in correctly using this
in conjunction with class Formula, and after client objects are killed.
"""
# note: as of 061022 this was used only in class Chunk and
# (via UsageTracker) in preferences.py; more uses were added later.
### REVIEW the status of these old comments, and clarify them:
# WARNING: The way Formula uses track_change only works when the call of
# track_change comes after the new value is available,
# but when using track_change to report an invalidation, that's not
# possible in general! ###@@@ this design flaw needs to be corrected
# somehow. See also comments near one of preferences.py's uses of
# track_change.
# note about whether to call track_change after something is "killed":
# If something is "killed" or in some other way becomes unusable,
# it only needs to call track_change if a use after that is theoretically
# possible (which is usually the case, since bugs in callers can cause
# that), and if a use after that would have a different effect because it
# had been killed. (I think it's always safe to call it then, and not even
# an anti-optimization, but I'm not 100% sure.) (Typically, other changes,
# which themselves call track_change(), occur when something is killed,
# so whether it's called directly doesn't matter anyway.)
def track_use(self):
"""
This must be called whenever the "get method" for the value we're
tracking is called, or more generally whenever that value is "used"
(see class docstring for background).
@note: This must be called on every use, not just the first use after
a change, in case the using entity is different, since all users
need to know about the next change or invalidation.
This works by telling cooperating users (clients of SubUsageTrackingMixin ###VERIFY)
what they used, so they can subscribe to invalidations or changes of all
values they used.
[some callers might inline this method]
"""
if getattr(self, '_changes__debug_print', False):
####REVIEW: can we give _changes__debug_print a default value
# as an optim? Note that since we're a mixin class, it would end up
# defined in arbitrary client classes, but its mangled-like name
# ought to make that ok. [bruce 090212 comment]
print_compact_stack( "\n_changes__debug_print: track_use of %r: " % self )
try:
subslist = self.__subslist
except AttributeError:
# note: this is the only way self.__subslist gets created;
# it happens on the first call of track_use (on self),
# and on the first call after each call of track_change/track_inval
debug_name = debug_flags.atom_debug and ("%r" % self) or None #061118; TODO: optimize
subslist = self.__subslist = OneTimeSubsList(debug_name)
# (more generally, some sort of unique name for self's current value era)
# (design note: Should we now return subslist.subscribe(func)? No --
# that's what the value-user should do *after* subslist gets entered
# here into its list of used objects, and value-user later finds it
# there.)
env.track( subslist)
###@@@ REVIEW:
# - is it right that the name of this function doesn't imply it's specific to usage-tracking?
# can it be used for tracking other things too?? [guess: no and no -- rename it ###@@@.]
# - (Wouldn't it be faster to save self, not self.__subslist, and then at the end to create and use the subslist?
# Yes, but it would be wrong, since one self has several subslists in succession as its value changes!
# [addendum 061022: i'm suspicious of that reasoning -- doesn't it happen too soon for the subslist to change??])
# - Maybe some callers can inline this method using essentially only a line like this:
# env.used_value_sublists[id(subslist)] = subslist
return
def track_change(self):
"""
This method (which implements both track_change and track_inval)
must be called whenever something changes or invalidates the value
self is tracking (see class docstring for background).
It tells everything which tracked a use of self's value that
self's value is no longer valid (unless this would be redundant
since it already told them this since they last subscribed).
(What the recipients do with that message is up to them,
but typically includes propogating further invalidations to
their own subscribers, directly marking their own value as invalid
(using another instance of this class or anything analogous to it),
and/or adding themselves to a set of objects that need later updates.)
Note that in typical use cases, there are several distinct
reasons to call this method -- all of them which apply must be done,
which typically means two or three kinds of calls of this method
can occur for each instance of self:
* the tracked value is explicitly set
* something used to compute the tracked value has changed (or been
invalidated), so the tracked value needs to be invalidated
(to ensure it will be recomputed when next needed).
(This differs from the tracked value changing by being set,
since we don't yet know the new value, or even (in general) whether
it's definitely different than the old value. But this class doesn't
yet behave differently in those cases -- in general, it's rare for
there to be a simple correct way to take advantage of knowing the new
value, except for comparing it to an old value and avoiding
propogating an invalidation if they're equivalent, but in all cases
where that can be done simply and correctly, it can be done by our
client before calling this method. [###todo: refer to external docs
which elaborate on this point, discussing why this comparison-optim
can't be propogated in general in any simple way.])
This kind of call has two subcases:
* done manually by the client, since it knows some value
used in that computation has changed (this is the only way to handle
it for values it uses which are not automatically tracked
by the system used by this class);
* done automatically, since something the client usage-tracked
(via the system used by this class)
while last recomputing that value has changed -- this case
is called "propogating an invalidation" (propogating it
from the other value usage-tracked by our client during its
recomputation, to the value directly tracked by our client).
"""
### REVIEW: rename to track_inval?? see class docstring and other
# comments for discussion
# review: add args? e.g. "why" (for debugging), nature of change
# (for optims), etc...
debug = getattr(self, '_changes__debug_print', False)
if debug:
print_compact_stack( "\n_changes__debug_print: track_change of %r: " % self )
try:
subslist = self.__subslist
except AttributeError:
# this is common [TODO: optimize by replacing with boolean test]
return # optimization (there were no uses of the current value, or,
# they were already invalidated)
del self.__subslist
subslist.fulfill_all(debug = debug)
return
track_inval = track_change
#bruce 061022 added track_inval to API, though I don't know if it ever
# needs to have a different implem. ### REVIEW: does each client use the
# appropriate one of track_inval vs. track_change?
pass # end of class SelfUsageTrackingMixin
# ==
class UsageTracker( SelfUsageTrackingMixin): #bruce 050804 #e rename?
"""
Ownable version of SelfUsageTrackingMixin, for owners that have
more than one aspect whose usage can be tracked, or which want to use
a cooperating class rather than a mixin class for clarity.
"""
# note: as of 061022 this is used only in _tracker_for_pkey (preferences.py);
# this or its superclass might soon be used in exprs/lvals.py
pass
# ===
class begin_end_matcher: #bruce 050804
"""
Maintain a stack of objects (whose constructor is a parameter of this object)
which are created/pushed and popped by calls of our begin/end methods, which must occur in
properly nested matching pairs. Try to detect and handle the error of nonmatching begin/end calls.
(This only works if the constructor promises to return unique objects, since we use their id()
to detect matching.)
"""
active = False # flag to warn outside callers that we're processing a perhaps-nonreentrant method [bruce 050909]
def __init__(self, constructor, stack_changed_func = None):
"""
Constructor is given all args (including keyword args) to self.begin(),
and from them should construct objects with begin(), end(), and error(text) methods
which get stored on self.stack while they're active.
Or constructor can be None, which means begin should always receive one arg, which is the object to use.
Stack_changed_func (or noop if that is None or not supplied)
is called when we're created and after every change to our stack,
with arg1 self, and arg2 a dict of info (keyword:value pairs) about the nature of the stack change
(see dict() calls herein for its keywords, including popped, pushed, errmsg).
"""
self.constructor = constructor or (lambda arg: arg)
self.stack_changed = stack_changed_func or noop
self.stack = []
self.active_apply( self.stack_changed, (self, dict()) ) # self.stack_changed(self, dict())
# callers depend on stack_changed being called when we're first created
#k is active_apply needed here??
def active_apply(self, func, args, kws = {}):
self.active = True
try:
return apply(func, args, kws)
finally:
self.active = False
def begin(self, *args, **kws):
"""
Construct a new object using our constructor;
activate it by calling its .begin() method [#k needed??] [before it's pushed onto the stack];
push it on self.stack (and inform observers that self.stack was modified);
return a currently-unique match_checking_code which must be passed to the matching self.end() call.
"""
self.active = True
try:
newobj = self.constructor(*args, **kws) # exceptions in this mean no change to our stack, which is fine
newobj.begin()
self.stack.append(newobj)
self.stack_changed(self, dict(pushed = newobj))
# for debugging, we could record compact_stack in newobj, and print that on error...
# but newobj __init__ can do this if it wants to, without any help from this code.
return id(newobj) # match_checking_code
finally:
self.active = False
def end(self, match_checking_code):
"""
This must be passed the return value from the matching self.begin() call.
Verify begin/end matches, pop and deactivate (using .end(), after it's popped) the matching object, return it.
Error handling:
- Objects which had begin but no end recieve .error(errmsg_text) before their .end().
- Ends with no matching begin (presumably a much rarer mistake -- only likely if some code
with begin/end in a loop reuses a localvar holding a match_checking_code from an outer begin/end)
just print a debug message and return None.
"""
stack = self.stack
if stack and id(stack[-1]) == match_checking_code:
# usual case, no error, be fast
doneobj = stack.pop()
self.active = True
try:
self.stack_changed(self, dict(popped = doneobj))
doneobj.end() # finalize doneobj
finally:
self.active = False
return doneobj
# some kind of error
ids = map( id, stack )
if match_checking_code in ids:
# some subroutines did begin and no end, but the present end has a begin -- we can recover from this error
doneobj = stack.pop()
while id(doneobj) != match_checking_code:
self.active = True
try:
errmsg = "begin, with no matching end by the time some outer begin got its matching end"
self.stack_changed(self, dict(popped = doneobj, errmsg = errmsg))
# stack_change is needed here in case doneobj.end() looks at something this updates from self.stack
doneobj.error(errmsg)
doneobj.end() # might be needed -- in fact, it might look at stack and send messages to its top
finally:
self.active = False
doneobj = stack.pop()
# now doneobj is correct
self.active = True
try:
self.stack_changed(self, dict(popped = doneobj))
doneobj.end()
finally:
self.active = False
return doneobj
# otherwise the error is that this end doesn't match any begin, so we can't safely pop anything from the stack
print_compact_stack( "bug, ignored for now: end(%r) with no matching begin, in %r: " % (match_checking_code, self) )
return None # might cause further errors as caller tries to use the returned object
pass # end of class begin_end_matcher
# ==
def default_track(thing): #bruce 050804; see also the default definition of track in env module
"""
Default implementation -- will be replaced at runtime whenever usage of certain things is being tracked.
"""
## if debug_flags.atom_debug:
## print "atom_debug: fyi (from changes module): something asked to be tracked, but nothing is tracking: ", thing
## # if this happens and is not an error, then we'll zap the message.
return
def _usage_tracker_stack_changed( usage_tracker, infodict): #bruce 050804
"""
[private]
called when usage_tracker's begin/end stack is created,
and after every time it changes
"""
# getting usage_tracker arg is necessary (and makes better sense anyway),
# since when we're first called, the global usage_tracker is not yet defined
# (since its rhs, in the assignment below, is still being evaluated).
del infodict
stack = usage_tracker.stack
if stack:
env.track = stack[-1].track
else:
env.track = default_track
return
usage_tracker = begin_end_matcher( None, _usage_tracker_stack_changed )
# constructor None means obj must be passed to each begin
usage_tracker._do_after_current_tracked_usage_ends = {} #070108
def after_current_tracked_usage_ends(func):# new feature 070108 [tested and works, but not currently used]
"""
Do func at least once after the current usage-tracked computation ends.
WARNING: if func invalidates something used during that computation, it will cause
an immediate inval of the entire computation, which in most cases would lead to
excessive useless recomputation. For an example of incorrect usage of this kind,
see Lval_which_recomputes_every_time in exprs/lvals.py (deprecated).
(There may be legitimate uses which do something other than a direct inval,
such as making a note to compare something's current and new value at some later time,
for possible inval just before it's next used in a tracked computation.
So this function itself is not deprecated. But it's been tested only in the deprecated
Lval_which_recomputes_every_time.)
"""
assert usage_tracker.stack, "error: there is no current usage-tracked computation"
# we use a dict to optim for lots of equal funcs (reducing RAM usage, at least)
# (note that in the case where this comes up, Lval_which_recomputes_every_time in exprs/lvals.py,
# they might be equivalent but non-identical bound methods)
usage_tracker._do_after_current_tracked_usage_ends[ func] = func
return
class SubUsageTrackingMixin: #bruce 050804; as of 061022 this is used only in class Chunk, class GLPane, class Formula
# [note, 060926: this doesn't use self at all [later: except for debug messages].
# Does it need to be a mixin?? addendum 061022: maybe for inval propogation??]
"""
###doc - for doing usagetracking in whatever code we call when we
remake a value, and handling results of that;
see class usage_tracker_obj for a related docstring
@note: this is often used in conjunction with SelfUsageTrackingMixin,
in which case (for a client class which inherits this class and
that one together) the invalidator passed to self.end_tracking_usage
can often be self.track_inval or a client method that calls it.
"""
def begin_tracking_usage(self): #e there's a docstring for this in an outtakes file, if one is needed
debug_name = debug_flags.atom_debug and ("%r" % self) or None #061118
obj = usage_tracker_obj(debug_name)
match_checking_code = usage_tracker.begin( obj)
# don't store match_checking_code in self -- that would defeat the error-checking
# for bugs in how self's other code matches up these begin and end methods
# [later, 060926: would it be good to combine id(self) into the match-checking code,
# thereby enforcing that the same object calls the matching begin & end methods?
# I doubt it's needed, since the match_checking_code is already unique,
# and it's hard to see how the correct one could be passed by accident
# (except by code written to use some bad kluge, which might need that kluge).
# And it might be legitimate. So don't add a restriction like that.]
return match_checking_code
def end_tracking_usage(self, match_checking_code, invalidator, debug = False):
"""
#doc;
returns the usage_tracker_obj
"""
# new feature 070109, mainly for debugging-related uses
obj = usage_tracker.end( match_checking_code)
obj.standard_end( invalidator, debug = debug)
##e or we could pass our own invalidator which wraps that one,
# so it can destroy us, and/or do more invals, like one inside the other mixin class if that exists
# support after_current_tracked_usage_ends [070108]
if not usage_tracker.stack:
dict1 = usage_tracker._do_after_current_tracked_usage_ends
if dict1: # condition is optim
usage_tracker._do_after_current_tracked_usage_ends = {}
for func in dict1.itervalues():
try:
func()
except:
print_compact_traceback(
"after_current_tracked_usage_ends: error: exception in call of %r (ignored): " % (func,))
pass
continue
pass
pass
return obj
pass # end of class SubUsageTrackingMixin
class usage_tracker_obj: #bruce 050804; docstring added 060927
"""
###doc [private to SubUsageTrackingMixin, mostly]
This object corresponds to one formula being evaluated,
or to one occurrence of some other action which needs to know what it uses
during a defined period of time while it calculates something.
At the start of that action, the client should create this object and make it accessible within env
so that when a trackable thing is used, its inval-subslist is passed to self.track (self being this object),
which will store it in self.
[This can be done by calling SubUsageTrackingMixin.begin_tracking_usage().]
At the end of that action, the client should make this object inaccessible
(restoring whatever tracker was active previously, if any),
then call self.standard_end(invalidator) with a function it wants called
the next time one of the things it used becomes invalid.
[This can be done by calling SubUsageTrackingMixin.end_tracking_usage() with appropriate args.]
Self.standard_end will store invalidator in self, and subscribe self.standard_inval to the invalidation
of each trackable thing that was used.
This usage_tracker_obj lives on as a record of the set of those subscriptions, and as the recipient
of invalidation-signals from them (via self.standard_inval), which removes the other subscriptions
before calling invalidator. (Being able to remove them is the only reason this object needs to live on,
or that the inval signal needs to be something other than invalidator itself. Perhaps this could be fixed
in the future, using weak pointers somehow. #e)
"""
# [note, 060926: for some purposes, namely optim of recompute when inputs don't change [#doc - need ref to explanation of why],
# we might need to record the order of first seeing the used things,
# and perhaps their values or value-version-counters ###e]
# 061022 review re possible use in exprs/lvals.py:
# - a needed decision is whether inval propogation is the responsibility of each invalidator, or this general system.
# - it seems like SubUsageTrackingMixin might need self to tie it to SelfUsageTrackingMixin for inval propogation. (guess)
# - which might mean we need variants of that, perhaps varying in this class, usage_tracker_obj:
# - a variant to propogate invals;
# - a variant to track in order (see below);
# - a variant to immediately signal an error (by raising an exception) if any usage gets tracked; used to assert none does.
# - as mentioned above, one variant will need to keep an order of addition of new items, in self.track.
# - and self.track is called extremely often and eventually needs to be optimized (and perhaps recoded in C).
# Other than that, it can probably be used directly, with the invalidator (from client code) responsible for inval propogation.
def __init__(self, debug_name = None):
self.debug_name = debug_name #061118
def __repr__(self): #061118
return "<%s%s at %#x>" % (self.__class__.__name__, self.debug_name and ("(%s)" % self.debug_name) or '', id(self))
def begin(self):
self.data = {}
def track(self, subslist):
"""
This gets called (via env.track) by everything that wants to record one use of its value.
The argument subslist is a subscription-list object which permits subscribing to future changes
(or invalidations) to the value whose use now is being tracked.
[This method, self.track, gets installed as env.track, which is called often, so it needs to be fast.]
"""
# For now, just store subslist, at most one copy. Later we'll subscribe just once to each one stored.
self.data[id(subslist)] = subslist
def error(self, text):
print "bug: error in usage_tracker_obj:", text #e stub
def end(self):
pass # let the caller think about self.data.values() (eg filter or compress them) before subscribing to them
def standard_end(self, invalidator, debug = False):
"""
some callers will find this useful to call, shortly after
self.end gets called; see the class docstring for more info
"""
self.invalidator = invalidator # this will be called only by our own standard_inval
whatweused = self.whatweused = self.data.values() # this list is saved for use in other methods called later
self.last_sub_invalidator = inval = self.standard_inval
# save the exact copy of this bound method object which we use now, so comparison with 'is' will work for unsubscribes
# (this might not be needed if they compare using '==' [###k find out])
# note: that's a self-referential value, which would cause a memory leak, except that it gets deleted
# in standard_inval (so it's ok). [060927 comment]
if debug:
print "changes debug: %r.standard_end subscribing %r to each of %r" % (self, inval, whatweused) #bruce 070816
for subslist in whatweused:
subslist.subscribe( inval ) #e could save retvals to help with unsub, if we wanted
self.data = 222 # make further calls of self.track() illegal [#e do this in self.end()??]
return
def standard_inval(self):
"""
This is used to receive the invalidation signal,
and call self.invalidator after some bookkeeping.
See class docstring for more info.
It also removes all cyclic or large attrs of self,
to prevent memory leaks.
"""
already = self.unsubscribe_to_invals('standard_inval')
if already:
pass # error message was already printed
else:
invalidator = self.invalidator
del self.invalidator
invalidator()
# calling this twice might cause a bug if "something called standard_inval twice",
# depending on the client object which receives it,
# so we don't, and the error message above should be retained [070110 comment]
return
def unsubscribe_to_invals(self, why): #070110 split this out and revised caller (leaving it equivalent) and added a caller
"""
if we already did this, print an error message mentioning why
we did it before and return 1, else do it and return 0
"""
# this needs to remove every subs except the one which is being fulfilled by calling it.
# But it doesn't know which subs that is! So it has to remove them all, even that one,
# so it has to call a remove method which is ok to call even for a subs that was already fulfilled.
# The only way for that method to always work ok is for its semantics to be that it removes all current subs (0 or more)
# which have the same fulfillment function (like subslist.remove_all_instances does).
# That is only ok since our subs (self.standard_inval) is unique to this object, and this object
# makes sure to give only one copy of it to one thing. (Another reason it could be ok is if it doesn't
# matter how many times self.invalidator is called, once it's called once. This is true in all current uses [061119]
# but perhaps not guaranteed.)
inval = self.last_sub_invalidator
self.last_sub_invalidator = 'hmm' # this value is also tested in another method
# 061119 do this rather than using del, to avoid exception when we're called twice; either way avoids a memory leak
self.last_sub_invalidator_why = why
if inval == 'hmm':
# we already did this
if self.last_sub_invalidator_why == 'standard_inval' and why == 'standard_inval':
# Something called standard_inval twice. This can happen (for reasons not yet clear to me) when some tracked state
# is set and later used, all during the same computation -- normally, state used by a computation should never be set
# during it, only before it (so as to trigger it, if the same computation used the same state last time around).
# Maybe the problem is that sets of default values, within "initialization on demand", should be considered pure uses
# but are being considered sets? Not sure yet -- this debug code only shows me a later event. ###k [061121 comment]
# ... update 061207: this is now happening all the time when I drag rects using exprs.test.testexpr_19c,
# so until it's debugged I need to lower the verbosity, so I'm putting it under control of flags I can set from other code.
if _debug_standard_inval_twice: ## was debug_flags.atom_debug:
msg = "debug: fyi: something called standard_inval twice (not illegal but weird -- bug hint?) in %r" % self
if _debug_standard_inval_twice_stack:
print_compact_stack(msg + ": ",
## frame_repr = _std_frame_repr, #bruce 061120, might or might not be temporary, not normally seen
linesep = '\n')
else:
print msg
else:
print "likely bug: %r unsubscribe_to_invals twice, whys are %r and %r" % (self, self.last_sub_invalidator_why, why)
#e print stack?
return 1
elif _debug_standard_inval_nottwice_stack:
print_compact_stack("debug: fyi: something called standard_inval once (totally normal) in %r: " % self)
# the actual unsubscribe:
whatweused = self.whatweused
self.whatweused = 444 # not a sequence (cause bug if we call this again)
for subslist in whatweused:
## can't use subslist.remove( self.last_sub_invalidator ), as explained above
subslist.remove_all_instances( inval )
####e should add debug code to make sure this actually removes some, except in at most one call;
# in fact, I need code to somehow verify they get removed by comparing lengths
# since a bug in removing them properly at all is a possibility [070110 comment]
return 0
def got_inval(self):#070110
"""
Did we get an inval yet (or make them illegal already)?
(if we did, we no longer subscribe to any others, in theory --
known to be not always true in practice)
"""
return (self.last_sub_invalidator == 'hmm')
def make_invals_illegal(self, obj_for_errmsgs = None):#070110, experimental -- clients ought to call it before another begin_tracking_usage... ##e
# if not self.got_inval():
# if it still subscribes to any invals:
# + remove those subs
# + in case that fails, arrange to complain if they are ever fulfilled,
# or better yet, prevent them from being fulfilled, by changing the invalidator
# - print a message (if atom_debug) since it might indicate a bug (or did, anyway, before we removed the subs)
# else:
# - it supposedly doesn't subscribe to any invals, but what if it gets one anyway?
# tell it to not call our invalidator in that case (change it?);
# no need to print a message unless it would have called it
if not self.got_inval():
if 1: ## self.whatweused:
if _debug_old_invalsubs:
# not necessarily a bug if they would never arrive -- I don't really know yet --
# it might (in fact it does routinely) happen in GLPane for reasons I'm not sure about...
# I should find out if it happens in other subusagetracking objects. ###e
# What I do know -- it's large and normal (for glpane) as if inval unsubs never happened; it's never 0;
# but the zapping fixes my continuous redraw bug in exprs module!
# [bruce 070110; more info and cleanup to follow when situation is better understood --
# could it be a bug in the removal of subs which makes it never happen at all?!? ###k]
print "fyi: %r still has %d subs of its invalidator to things it used; zapping/disabling them" % \
(obj_for_errmsgs or self, len(self.whatweused))
already = self.unsubscribe_to_invals('standard_inval')
if already:
# error message was already printed -- but, I don't think this can ever happen, so print *that*
print "(should never happen in make_invals_illegal)"
else:
# if an inval comes (to self.standard_inval), we'll find out, since it'll complain (I think)
pass
return
pass # end of class usage_tracker_obj
_debug_old_invalsubs = False #070110
_debug_standard_inval_twice = debug_flags.atom_debug # whether to warn about this at all
_debug_standard_inval_twice_stack = False # whether to print_compact_stack in that warning [untested since revised by bruce 061207]
_debug_standard_inval_nottwice_stack = False # whether to print_compact_stack in an inval that *doesn't* give that warning [untested]
# ==
class begin_disallowing_usage_tracking(SubUsageTrackingMixin):
"""
Publicly, this class is just a helper function, used like this:
mc = begin_disallowing_usage_tracking(whosays) # arg is for use in debug prints and exception text
try:
... do stuff in which usage tracking would be an error or indicate a bug
finally:
end_disallowing_usage_tracking(mc)
pass
"""
def __init__(self, whosays, noprint = False):
self.whosays = "%r" % (whosays,) #k %r??
self.noprint = noprint
self.mc = self.begin_tracking_usage()
###e SHOULD do that in a nonstandard way so we notice each usage that occurs and complain, w/ exception;
#e and we should not actually subscribe
def _end_disallowing_usage_tracking(self):
self.end_tracking_usage(self.mc, self.inval) # this shouldn't subscribe us to anything, once implem is finished properly
# now warn if we actually subscribed to anything -- done in our overridden version of that method
# someday, self.destroy(), but for now [until implem is done], stick around for things to call our inval.
return
def inval(self):
#e don't use noprint, since this implies a bug in self, tho for now, that's a known bug, always there, til implem is done
print "bug (some time ago): something that %r should not have subscribed to (but did - also a bug) has changed" % self
def __repr__(self):
return "<%s at %#x for %r>" % (self.__class__.__name__, id(self), self.whosays)
def end_tracking_usage(self, match_checking_code, invalidator):
"""
[THIS OVERRIDES the method from SubUsageTrackingMixin]
"""
obj = usage_tracker.end( match_checking_code) # same as in mixin
obj.standard_end( invalidator) # same as in mixin, but we'll remove this later, so don't just call the mixin version here
if obj.whatweused: # a private attr of obj (a usage_tracker_obj), that we happen to know about, being a friend of it
msg = "begin_disallowing_usage_tracking for %s sees some things were used: %r" % (self.whosays, obj.whatweused,)
if not self.noprint:
print msg
assert 0, msg ##e should be a private exception so clients can catch it specifically; until then, noprint is not useful
return obj #070110 be compatible with new superclass API
pass
def end_disallowing_usage_tracking(mc):
mc._end_disallowing_usage_tracking()
return
# ==
def _std_frame_repr(frame): #bruce 061120 #e refile into debug.py? warning: dup code with lvals.py and [g4?] changes.py
"""
return a string for use in print_compact_stack
"""
# older eg: frame_repr = lambda frame: " %s" % (frame.f_locals.keys(),), linesep = '\n'
locals = frame.f_locals
dfr = locals.get('__debug_frame_repr__')
if dfr:
return ' ' + dfr(locals)
co_name = frame.f_code.co_name
res = ' ' + co_name
self = locals.get('self')
if self is not None:
res +=', self = %r' % (self,)
return res
# locals.keys() ##e sorted? limit to 25? include funcname of code object? (f_name?)
# note: an example of dir(frame) is:
['__class__', '__delattr__', '__doc__', '__getattribute__', '__hash__',
'__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__str__', 'f_back', 'f_builtins', 'f_code',
'f_exc_traceback', 'f_exc_type', 'f_exc_value', 'f_globals', 'f_lasti',
'f_lineno', 'f_locals', 'f_restricted', 'f_trace']
# and of dir(frame.f_code) is:
['__class__', '__cmp__', '__delattr__', '__doc__', '__getattribute__',
'__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__',
'__repr__', '__setattr__', '__str__', 'co_argcount', 'co_cellvars',
'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags',
'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals',
'co_stacksize', 'co_varnames']
# ==
class Formula( SubUsageTrackingMixin): #bruce 050805 [not related to class Expr in exprs/Exprs.py, informally called formulae]
"""
"""
killed = False
def __init__( self, value_lambda, action = None, not_when_value_same = False, debug = False ):
"""
Create a formula which tracks changes to the value of value_lambda (by tracking what's used to recompute it),
and when created and after each change occurs, calls action with the new value_lambda return value as sole argument.
This only works if whatever value_lambda uses, which might change and whose changes should trigger recomputation,
uses SelfUsageTrackingMixin or the equivalent to track its usage and changes, AND (not always true!) if those things
only report track_change after the new value is available, not just when the old value is known to be invalid.
[About that issue, see comments in docstring of class which defines track_change. ####@@@@]
If action is not supplied, it's a noop.
Whether or not it's supplied, it can be changed later using self.set_action.
Note that anything whose usage is tracked by calling value_lambda can cause action to be called,
even if that thing does not contribute to the return value from value_lambda.
If this bothers you, consider passing not_when_value_same = True, so that repeated calls
only occur when the return value is not equal to what it was before.
(The old return value is not even kept in this object unless not_when_value_same is true.)
[WARNING: the code for the not_when_value_same = True option is untested as of 060306, since nothing uses it yet.]
"""
self.debug = debug #bruce 070816
self.value_lambda = value_lambda
self.set_action( action)
self.not_when_value_same = not_when_value_same
self.first_time = True # prevents looking for self.oldval
self.recompute()
return
def destroy(self):
self.killed = True
self.value_lambda = self.action = self.oldval = None
#e anything else?
return
def set_action(self, action):
if action is None:
action = noop
self.action = action
def recompute(self):
debug = self.debug
print_errors = True or debug or debug_flags.atom_debug
#bruce 070816 included True in that condition, since prior code could
# silently discard exceptions which indicated real bugs.
if debug:
print "\n_changes__debug_print: %r.recompute() calling %r" % \
( self, self.value_lambda )
error = False
match_checking_code = self.begin_tracking_usage()
try:
newval = self.value_lambda()
except:
error = True
newval = None
if print_errors:
print_compact_traceback( "bug: exception in %r value_lambda %r: " % (self, self.value_lambda) )
self.end_tracking_usage( match_checking_code, self.need_to_recompute, debug = debug )
if not error and (not self.not_when_value_same or self.first_time or not self.values_same( self.oldval, newval) ):
# do the action
try:
self.action( newval)
except:
error = True
if print_errors:
print_compact_traceback( "bug: exception in %r action %r: " % (self, self.action) )
self.first_time = False
if not error and self.not_when_value_same:
# save self.oldval for the comparison we might need to do next time
self.oldval = newval
if error:
self.destroy()
if print_errors:
print_compact_stack( "note: destroyed %r due to bug reported above: " % self )
return
def need_to_recompute(self):
if not self.killed:
self.recompute()
return
def values_same(self, val1, val2): #bruce 060306; untested, since self.not_when_value_same is apparently never True ###@@@
"""
Determine whether two values are the same, for purposes of the option 'not_when_value_same'.
Override this in a subclass that needs a different value comparison, such as 'not !='
(or change the code to let the caller pass a comparison function).
WARNING: The obvious naive comparison (a == b) is buggy for Numeric arrays,
and the fix for that, (not (a != b)), is buggy for Python container classes
(like list or tuple) containing Numeric arrays. The current implem uses a slower and stricter comparison,
state_utils.same_vals, which might be too strict for some purposes.
"""
return same_vals(val1, val2)
pass # end of class Formula
# ==
# As of 050803, many of the facilities after this point are used only as stubs,
# though some might be used soon, e.g. as part of recent files menu,
# custom jigs, Undo, or other things.
# [as of 060330 pairmatcher is used in Undo; maybe keep_forever is used somewhere;
# a lot of Undo helpers occur below too]
class pairmatcher:
"""
Keep two forever-growing lists,
and call a specified function on each pair of items in these lists,
in a deterministic order,
as this becomes possible due to the lists growing.
Normally that specified function should return None;
otherwise it should return special codes which
control this object's behavior (see the code for details).
"""
def __init__(self, func, typearg, debug_name = None):
self.d1s = []
self.d2s = []
self.func = func #e will this func ever need to be changed, after we're made?
self.typearg = typearg # public, thus visible to each call of func, since we now pass self [bruce 060330 new feature]
self.debug_name = debug_name or str(typearg) or "?" #revised 060330
def another_dim2(self, d2):
## print self,'getsg5h6 d2',d2
for d1 in self.d1s:
self.doit(d1,d2) # doit for existing d1s
self.d2s.append(d2) # and for future d1s
def another_dim1(self, d1):
## print self,'getsg5h6 d1',d1
for d2 in self.d2s:
self.doit(d1,d2) # doit for existing d2s
self.d1s.append(d1) # and for future d2s
def doit(self, d1, d2):
try:
retcode = self.func(d1,d2,self) #bruce 060330 new feature -- pass self [##k should we catch exception in func??]
if retcode:
if retcode == "remove d1":
# note: we might or might not be iterating over d1s right now!
# if we are iterating over d2, we might have already removed d1!
# we should probably stop that loop in that case, but that feature is nim.
# at least don't mind if we did already remove it...
# [060330 temp kluge -- really we ought to stop the loop ###@@@]
self.d1s = list(self.d1s) # in case we are, modify a fresh copy
try:
self.d1s.remove(d1)
except ValueError:
pass
elif retcode == "remove d2":
self.d2s = list(self.d2s)
try:
self.d2s.remove(d2)
except ValueError:
pass
else:
print_compact_stack( "bug (ignored): unrecognized return code %r in pairmatcher %r: " % \
(retcode, self.debug_name))
#e any other use for retval??
except:
print_compact_traceback( "exception in %r ignored: " % self)#revised 060330
return
def __repr__(self): #060330, untested
return "<%s at %#x, debug_name = %r>" % (self.__class__.__name__, id(self), self.debug_name)
pass
class MakerDict:
"""
A read-only dict with a function for constructing missing elements.
"""
def __init__( self, func):
self.data = {}
self.func = func
def __getitem__(self, key): # (no need for __setitem__ or other dict methods)
try:
return self.data[key]
except KeyError:
self.data[key] = self.func(key)
return self.data[key]
pass
# A place for objects of one kind to register themselves under some name,
# so that objects of another kind can meet all of them using a pairmatcher (#doc better?)
def postinit_func( d1, d2, matcher): #bruce 060330 add matcher arg
"""
After d1 is inited, tell it about d2.
(This is meant to be called for every d1 of one kind,
and every d2 of another kind,
registered below under the same name.)
"""
if 1:
#bruce 060330 kluge, but might be reasonable -- let method name be encoded in typename for type of postinit object
#e (if speed of this ever matters, we might memoize the following in an our-private attr of matcher)
typename = matcher.typearg
if typename.startswith('_'):
###e should assert it's legit as attrname?
methodname = typename
else:
methodname = 'postinit_item' # compatibility with old code, still used as of 060330 for "Jig menu items" or so
try:
method = getattr(d1, methodname)
method(d2)
except:
# blame d1, for error in either statement above
print_compact_traceback( "exception in calling (or finding method for) d1.%s(d2) ignored; removing d1: " % (methodname,))
#e objnames? safe_repr(obj)?
return "remove d1" # special code recognized by the pairmatcher
postinit_pairmatchers = MakerDict( lambda typename: pairmatcher( postinit_func, typename ) )
# the public functions:
# (for main window Jig menu items we'll use the typename "Jigs menu items" since maybe other things will have Jigs menus)
#e [later, 060330: this API suffers from the methodname postinit_item being fixed, rather than depending on typename,
# which matters if this is used for lots of purposes and the same object might participate in more than one purpose.]
def register_postinit_object( typename, object):
"""
Cause object to receive the method-call object.postinit_item(item)
for every postinit item registered under the same typename,
in the order of their registration,
whether item is already registered or will be registered in the future.
"""
pairmatcher = postinit_pairmatchers[ typename]
pairmatcher.another_dim1( object)
def register_postinit_item( typename, item):
"""
Cause every object registered with register_postinit_object
under the same typename (whether registered already or in the future,
and in their order of registration)
to receive the method call object.postinit_item(item) for this item.
"""
pairmatcher = postinit_pairmatchers[ typename]
pairmatcher.another_dim2( item)
# ==
_keep_these_forever = {}
def keep_forever(thing):
"""
a place to put stuff if you need to make sure it's never deallocated by Python
"""
_keep_these_forever[id(thing)] = thing
# ==
_op_id = 0
debug_begin_ops = False #bruce 051018 changed from debug_flags.atom_debug
class op_run:
"""
Track one run of one operation or suboperation, as reported to
env.begin_op and env.end_op in nested pairs
"""
def __init__(self, op_type = None, in_event_loop = False, typeflag = ''): #bruce 060321 added typeflag
#bruce 060127 adding in_event_loop for Undo
"""
[this gets all the args passed to env.begin_op()]
"""
self.op_type = op_type # this might be almost anything, mainly meant for humans to see
self.in_event_loop = in_event_loop
self.typeflag = typeflag # this is one of a small set of constants which control how this is treated by undo (at least)
global _op_id
_op_id += 1
self.op_id = _op_id
def begin(self):
if debug_begin_ops:
self.printmsg( "%sbegin op_id %r, op_type %r" % (self.indent(), self.op_id, self.op_type) )
pass # not sure it's good that begin_end_matcher requires us to define this
def end(self):
if debug_begin_ops:
self.printmsg( "%send op_id %r, op_type %r" % (self.indent(), self.op_id, self.op_type) )
pass
def error(self, errmsg_text):
"""
called for begin_op with no matching end_op, just before our .end() and the next outer end_op is called
"""
if debug_begin_ops: #
self.printmsg( "%serror op_id %r, op_type %r, errmsg %r" % (self.indent(), self.op_id, self.op_type, errmsg_text) )
pass
def printmsg(self, text):
if debug_begin_ops:
# print "atom_debug: fyi: %s" % text
env.history.message( "debug: " + text ) # might be recursive call of history.message; ok in theory but untested ###@@@
def indent(self):
"""
return an indent string based on the stack length; we assume the stack does not include this object
"""
#e (If the stack did include this object, we should subtract 1 from its length. But for now, it never does.)
return "| " * len(op_tracker.stack)
pass
_in_event_loop = True #bruce 060127; also keep a copy of this in env; probably that will become the only copy #e
env._in_event_loop = _in_event_loop
def _op_tracker_stack_changed( tracker, infodict ): #bruce 050908 for Undo
"""
[private]
called when op_tracker's begin/end stack is created, and after every time it changes
"""
#e we might modify some sort of env.prefs object, or env.history (to filter out history messages)...
#e and we might figure out when outer-level ops happen, as part of undo system
#e and we might install something to receive reports about possible missing begin_op or end_op calls
#
#bruce 060127 new code:
new_in_event_loop = True # when nothing is on this op_run stack, we're in Qt's event loop
if tracker.stack:
new_in_event_loop = tracker.stack[-1].in_event_loop
global _in_event_loop, _last_typeflag
changed = False # will be set to whether in_event_loop changed
if _in_event_loop != new_in_event_loop:
# time for a checkpoint
## if _in_event_loop:
## print "begin command segment"
## else:
## print "end command segment"
changed = True
beginflag = _in_event_loop
_in_event_loop = new_in_event_loop
env._in_event_loop = _in_event_loop
if changed:
for sub in env.command_segment_subscribers[:]: # this global list might be changed during this loop
unsub = False
try:
#e ideally we should prevent any calls into op_tracker here...
# infodict is info about the nature of the stack change, passed from the tracker [bruce 060321 for bug 1440 et al]
unsub = sub( beginflag, infodict, tracker ) # (we can't always pass tracker.stack[-1] -- it might not exist!)
except:
print_compact_traceback("bug in some element of env.command_segment_subscribers (see below for more): ")
#e discard it?? nah. (we'd do so by unsub = True)
""" note: during Quit, we got this, when we tried to update the menu items no longer present (enable a QAction);
this could be related to the crashes on Quit reported recently;
so we should try to get the assy to unsubscribe (clear and deinit) when we're about to quit. [bruce 060127]
bug in some element of env.command_segment_subscribers: exceptions.RuntimeError:
underlying C/C++ object has been deleted
[changes.py:607] [undo_manager.py:115] [undo_manager.py:154] [undo_manager.py:128] [undo_manager.py:238]
"""
print_compact_stack(" the call that led to that bug: ", skip_innermost_n = 1) # bruce 080917
if unsub:
try:
env.command_segment_subscribers.remove(sub)
except ValueError:
pass
pass
pass
return
op_tracker = begin_end_matcher( op_run, _op_tracker_stack_changed )
def env_begin_op(*args, **kws):
return op_tracker.begin(*args, **kws)
def env_end_op(mc):
return op_tracker.end(mc) #e more args?
env.begin_op = env_begin_op
env.end_op = env_end_op
global_mc = None
_in_op_recursing = False
def env_in_op(*args, **kws): # (disabled, separately, bruce 060127)
# [note 060320: it's still called directly just below, in env_begin_recursive_event_processing; does it do anything then??]
"""
This gets called by various code which might indicate that an operation is ongoing,
to detect ops in legacy code which don't yet call env.begin_op when they start.
The resulting "artificial op" will continue until the next GLPane repaint event
(other than one known to occur inside recursive event processing,
which should itself be wrapped by begin_op/end_op [with a special flag ###doc? or just in order to mask the artificial op?]),
which will end it.
This system is subject to bugs if recursive event processing is not wrapped,
and some op outside of that has begin but no matching end -- then a redraw during
the unwrapped recursive event processing might terminate this artificial op too early,
and subsequent ends will have no begin (since their begin got terminated early as if it had no end)
and (presently) print debug messages, or (in future, perhaps) result in improperly nested ops.
"""
global global_mc, _in_op_recursing
if not op_tracker.stack and not _in_op_recursing and not op_tracker.active:
_in_op_recursing = True # needed when env_begin_op (before pushing to stack) calls env.history.message calls this func
try:
assert global_mc is None
global_mc = env_begin_op(*args, **kws)
finally:
_in_op_recursing = False
return
def env_after_op(): # (disabled, separately, bruce 060127)
"""
This gets called at the start of GLPane repaint events
[#e or at other times not usually inside user-event handling methods],
which don't occur during an "op" unless there is recursive Qt event processing.
"""
global global_mc
if global_mc is not None and len(op_tracker.stack) == 1:
#e might want to check whether we're inside recursive event processing (if so, topmost op on stack should be it).
# for now, assume no nonmatch errors, and either we're inside it and nothing else was wrapped
# (impossible if the wrapper for it first calls env_in_op),
# or we're not, so stack must hold an artificial op.
mc = global_mc
global_mc = None #k even if following has an error??
env_end_op(mc)
return
# disable these, bruce 060127
##env.in_op = env_in_op
##env.after_op = env_after_op
def env_begin_recursive_event_processing():
"""
call this just before calling qApp.processEvents()
"""
env_in_op('(env_begin_recursive_event_processing)')
return env_begin_op('(recursive_event_processing)', in_event_loop = True, typeflag = 'beginrec') #bruce 060321 added typeflag
#bruce 060127 added in_event_loop = True
def env_end_recursive_event_processing(mc):
"""
call this just after calling qApp.processEvents()
"""
return env_end_op(mc) #bruce 060321: no typeflag needed (or allowed), gets it from matching begin_op
env.begin_recursive_event_processing = env_begin_recursive_event_processing
env.end_recursive_event_processing = env_end_recursive_event_processing
#e desired improvements:
# - begin_op could look at existing stack frames, see which ones are new, sometimes do artificial begin_ops on those,
# with the end_ops happening either from __del__ methods, or if that doesn't work (due to saved tracebacks),
# from every later stack-scan realizing those frames are missing and doing something about it
# (like finding the innermost still-valid stack-frame-op and then the next inner one and ending its stack-suffix now).
# (If we can run any code when tracebacks are created, that would help too. I don't know if we can.)
# This should give you valid begin/end on every python call which contained any begin_op call which did this
# (so it might be important for only some of them to do this, or for the results to often be discarded).
# It matters most for history message emission (to understand py stack context of that). [050909]
# end
|