@@ -419,16 +419,25 @@ class Bad(dj.Computed):
419419 """
420420
421421 def make (self , key ):
422+ # class-form lookup (Diagram.__getitem__ class branch)
422423 try :
423424 self .upstream [Unrelated ]
424425 except DataJointError as exc :
425- captured_errors .append (exc )
426+ captured_errors .append (("class" , exc ))
427+ # string-form lookup (the separate FreeTable/string branch)
428+ try :
429+ self .upstream [Unrelated .full_table_name ]
430+ except DataJointError as exc :
431+ captured_errors .append (("string" , exc ))
426432 # Insert anyway so populate doesn't fail
427433 self .insert1 ({** key , "ok" : 1 })
428434
429435 Bad .populate ()
430- assert len (captured_errors ) == 1
431- assert "not in this trace" in str (captured_errors [0 ]).lower ()
436+ # Both the class-form and string-form lookups must reject the non-ancestor.
437+ forms = {form for form , _ in captured_errors }
438+ assert forms == {"class" , "string" }, f"expected both branches to raise, got { forms } "
439+ class_err = next (exc for form , exc in captured_errors if form == "class" )
440+ assert "not in this trace" in str (class_err ).lower ()
432441
433442
434443def test_upstream_unset_outside_make (prefix , connection_test ):
@@ -458,7 +467,10 @@ def make(self, key):
458467
459468
460469def test_upstream_cleared_after_make (prefix , connection_test ):
461- """After a make() call completes, self.upstream is reset (no stale state)."""
470+ """After make() completes, the SAME instance that ran make() has its
471+ self.upstream cleared. Capturing the populate instance is what gives this
472+ test teeth: it would FAIL if the `finally: self._upstream = None` line were
473+ removed (a fresh-instance probe would pass regardless)."""
462474 schema = dj .Schema (f"{ prefix } _upstream_cleared" , connection = connection_test )
463475
464476 @schema
@@ -468,6 +480,8 @@ class Source(dj.Lookup):
468480 """
469481 contents = [(1 ,)]
470482
483+ captured = []
484+
471485 @schema
472486 class Derived (dj .Computed ):
473487 definition = """
@@ -477,19 +491,61 @@ class Derived(dj.Computed):
477491 """
478492
479493 def make (self , key ):
494+ captured .append (self ) # the actual populate instance
495+ assert self ._upstream is not None # set for the duration of make()
480496 self .insert1 ({** key , "val" : 0 })
481497
482498 Derived .populate ()
483- # The class attribute defaults to None; the per-instance _upstream
484- # set during make() must have been cleared by the finally block.
485- # Probe via the public property — should raise the "outside make" error.
499+ assert captured , "make() did not run"
500+ inst = captured [0 ]
501+ # The finally block must have cleared _upstream on this very instance.
502+ assert inst ._upstream is None
486503 with pytest .raises (DataJointError , match = "only available inside make" ):
487- Derived ().upstream
504+ inst .upstream
505+
506+
507+ def test_upstream_cleared_after_make_raises (prefix , connection_test ):
508+ """The reset lives in `finally` specifically so it survives an exception in
509+ make(). Force make() to raise and assert the populate instance's
510+ self.upstream is still cleared."""
511+ schema = dj .Schema (f"{ prefix } _upstream_exc" , connection = connection_test )
512+
513+ @schema
514+ class Source (dj .Lookup ):
515+ definition = """
516+ source_id : int32
517+ """
518+ contents = [(1 ,)]
519+
520+ captured = []
521+
522+ @schema
523+ class Boom (dj .Computed ):
524+ definition = """
525+ -> Source
526+ ---
527+ val : int32
528+ """
529+
530+ def make (self , key ):
531+ captured .append (self )
532+ assert self ._upstream is not None
533+ raise RuntimeError ("make failed on purpose" )
534+
535+ with pytest .raises (RuntimeError , match = "make failed on purpose" ):
536+ Boom .populate (suppress_errors = False )
537+ assert captured , "make() did not run"
538+ inst = captured [0 ]
539+ # Cleared by the finally block even though make() raised.
540+ assert inst ._upstream is None
541+ with pytest .raises (DataJointError , match = "only available inside make" ):
542+ inst .upstream
488543
489544
490545def test_upstream_seen_across_tripartite_make (prefix , connection_test ):
491- """The tripartite make() invocation pattern sees the same self.upstream
492- across all three phases (fetch / compute / insert)."""
546+ """The tripartite make() sees the SAME self.upstream object across all three
547+ phases (fetch / compute / insert) for a given key — constructed once,
548+ shared. Asserted via object identity, not just a correct result."""
493549 schema = dj .Schema (f"{ prefix } _upstream_tripartite" , connection = connection_test )
494550
495551 @schema
@@ -501,6 +557,8 @@ class Source(dj.Lookup):
501557 """
502558 contents = [(1 , 100 ), (2 , 200 )]
503559
560+ seen = [] # (source_id, phase, id(self._upstream))
561+
504562 @schema
505563 class TriComputed (dj .Computed ):
506564 definition = """
@@ -510,18 +568,30 @@ class TriComputed(dj.Computed):
510568 """
511569
512570 def make_fetch (self , key ):
571+ seen .append ((key ["source_id" ], "fetch" , id (self ._upstream )))
513572 return (self .upstream [Source ].fetch1 ("value" ),)
514573
515574 def make_compute (self , key , value ):
575+ seen .append ((key ["source_id" ], "compute" , id (self ._upstream )))
516576 return (value * 2 ,)
517577
518578 def make_insert (self , key , doubled ):
579+ seen .append ((key ["source_id" ], "insert" , id (self ._upstream )))
519580 self .insert1 ({** key , "result" : doubled })
520581
521582 TriComputed .populate ()
522583 assert (TriComputed & {"source_id" : 1 }).fetch1 ("result" ) == 200
523584 assert (TriComputed & {"source_id" : 2 }).fetch1 ("result" ) == 400
524585
586+ # Every phase that ran for a given key must have observed one and the same
587+ # self.upstream object (not None, not rebuilt per phase).
588+ ids_by_key = {}
589+ for sid , _phase , uid in seen :
590+ ids_by_key .setdefault (sid , set ()).add (uid )
591+ assert ids_by_key , "tripartite make did not run"
592+ for sid , ids in ids_by_key .items ():
593+ assert len (ids ) == 1 , f"source_id={ sid } : self.upstream differed across phases: { ids } "
594+
525595
526596def test_populate_reserve_jobs_respects_restrictions (clean_autopopulate , subject , experiment ):
527597 """Regression test for #1413: populate() with reserve_jobs=True must honour restrictions.
0 commit comments