Skip to content

Commit 5301a9d

Browse files
authored
fix tqdm: Fix progress bar to 100% on successful execution (#1570)
Fixes TDQM hook with overrides #845. The h_tqdm.ProgressBar adapter never reached 100% when overrides were passed to .execute() because override nodes are counted in the bar's total but do not trigger run_after_node_execution, leaving the bar short of total on a successful run. On a successful graph execution we top up progress_bar.n to progress_bar.total inside run_after_graph_execution.. On failure we leave the partial count visible.
1 parent a9a4e5f commit 5301a9d

2 files changed

Lines changed: 92 additions & 1 deletion

File tree

hamilton/plugins/h_tqdm.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,18 @@ def run_before_node_execution(
111111
def run_after_node_execution(self, **future_kwargs):
112112
self.progress_bar.update(1)
113113

114-
def run_after_graph_execution(self, **future_kwargs):
114+
def run_after_graph_execution(self, *, success: bool = True, **future_kwargs):
115115
name_part = "Execution Complete!"
116116
if len(name_part) > self.node_name_target_width:
117117
padding = ""
118118
else:
119119
padding = " " * (self.node_name_target_width - len(name_part))
120+
# Overrides are counted in `total` but never trigger run_after_node_execution,
121+
# so on a successful run we top the bar up to 100% rather than leaving a gap.
122+
if success and self.progress_bar.total is not None:
123+
remaining = self.progress_bar.total - self.progress_bar.n
124+
if remaining > 0:
125+
self.progress_bar.update(remaining)
120126
self.progress_bar.set_description_str(f"{self.desc} -> {name_part + padding}")
121127
self.progress_bar.set_postfix({})
122128
self.progress_bar.close()

tests/plugins/test_h_tqdm.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
from hamilton import ad_hoc_utils, driver
19+
from hamilton.plugins.h_tqdm import ProgressBar
20+
21+
22+
def _build_driver(progress_bar: ProgressBar, module_name: str) -> driver.Driver:
23+
def normalized(raw_value: float) -> float:
24+
return raw_value / 100.0
25+
26+
def scaled(normalized: float) -> float:
27+
return normalized * 10.0
28+
29+
def total(scaled: float) -> float:
30+
return scaled + 5.0
31+
32+
module = ad_hoc_utils.create_temporary_module(
33+
normalized, scaled, total, module_name=module_name
34+
)
35+
return driver.Builder().with_adapters(progress_bar).with_modules(module).build()
36+
37+
38+
def test_progress_bar_reaches_total_with_overrides():
39+
"""Regression test for #845: bar must hit 100% when overrides are passed."""
40+
progress_bar = ProgressBar()
41+
dr = _build_driver(progress_bar, "tqdm_pipeline_overrides")
42+
43+
dr.execute(
44+
["total"],
45+
overrides={"scaled": 42.0},
46+
inputs={"raw_value": 200.0},
47+
)
48+
49+
assert progress_bar.progress_bar.n == progress_bar.progress_bar.total
50+
51+
52+
def test_progress_bar_reaches_total_without_overrides():
53+
"""Bar reaches 100% on a normal successful run."""
54+
progress_bar = ProgressBar()
55+
dr = _build_driver(progress_bar, "tqdm_pipeline_no_overrides")
56+
57+
dr.execute(["total"], inputs={"raw_value": 200.0})
58+
59+
assert progress_bar.progress_bar.n == progress_bar.progress_bar.total
60+
61+
62+
def test_progress_bar_does_not_force_completion_on_failure():
63+
"""Bar must not be forced to total when execution fails."""
64+
65+
def step_one(raw_value: float) -> float:
66+
return raw_value
67+
68+
def step_two(step_one: float) -> float:
69+
raise RuntimeError("boom")
70+
71+
def step_three(step_two: float) -> float:
72+
return step_two
73+
74+
module = ad_hoc_utils.create_temporary_module(
75+
step_one, step_two, step_three, module_name="tqdm_pipeline_failure"
76+
)
77+
progress_bar = ProgressBar()
78+
dr = driver.Builder().with_adapters(progress_bar).with_modules(module).build()
79+
80+
try:
81+
dr.execute(["step_three"], inputs={"raw_value": 1.0})
82+
except Exception:
83+
pass
84+
85+
assert progress_bar.progress_bar.n < progress_bar.progress_bar.total

0 commit comments

Comments
 (0)