Skip to content

Commit 5cb9638

Browse files
committed
Implement method deletion and renaming updates in calculation setups
1 parent f2129e6 commit 5cb9638

3 files changed

Lines changed: 156 additions & 28 deletions

File tree

activity_browser/actions/method/method_delete.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from os import name
12
from typing import List
23
from logging import getLogger
34

@@ -13,9 +14,17 @@
1314

1415
class MethodDelete(ABAction):
1516
"""
16-
ABAction to remove one or multiple methods. First check whether the method is a node or leaf. If it's a node, also
17-
include all underlying methods. Ask the user for confirmation, and return if canceled. Otherwise, remove all found
18-
methods.
17+
Delete one or more impact assessment methods (impact categories).
18+
19+
Flow:
20+
- Confirm with the user.
21+
- Deregister all selected methods from Brightway.
22+
- Update all Brightway calculation setups by removing the deleted methods from their
23+
'ia' list (if present), and serialize the updated setups.
24+
25+
Notes:
26+
- Calculation setups can store method identifiers either as tuples (recommended) or
27+
sometimes as strings for single-level names; the cleanup accounts for both.
1928
"""
2029

2130
icon = qicons.delete
@@ -44,7 +53,41 @@ def run(methods: List[tuple]):
4453
if warning == QtWidgets.QMessageBox.No:
4554
return
4655

47-
# instruct the controller to delete the selected methods
56+
# collect names for calculation setup cleanup
57+
to_remove = {m.name for m in all_methods}
58+
59+
# delete all methods by deregistering them
4860
for method in all_methods:
4961
method.deregister()
5062
log.info(f"Deleted method {method.name}")
63+
64+
# remove deleted methods from all calculation setups
65+
MethodDelete.remove_methods_from_calculation_setups(to_remove)
66+
67+
@staticmethod
68+
def remove_methods_from_calculation_setups(method_names: set[tuple]) -> None:
69+
"""
70+
Remove given method names from all calculation setups' 'ia' lists and serialize.
71+
"""
72+
try:
73+
changed_any = False
74+
75+
for cs_name, cs in bd.calculation_setups.items():
76+
ia = cs.get("ia", [])
77+
78+
for name in method_names:
79+
if name not in ia:
80+
continue # name not present, skip
81+
82+
ia.remove(name)
83+
changed_any = True
84+
85+
log.info(
86+
f"Updated calculation setup '{cs_name}': removed impact category {name}"
87+
)
88+
89+
90+
if changed_any:
91+
bd.calculation_setups.serialize()
92+
except Exception:
93+
log.exception("Failed to update calculation setups after method rename")

activity_browser/actions/method/method_rename.py

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,19 @@
1414

1515
class MethodRename(ABAction):
1616
"""
17-
Renames an existing method in the Brightway2 data structure.
17+
Rename an existing impact assessment method (impact category).
1818
19-
This method allows renaming a single method by prompting the user for a new name.
20-
It ensures that only one method is renamed at a time, validates the new name, and
21-
updates the method in the Brightway2 database.
22-
23-
Args:
24-
method_name (tuple[str] | list[tuple[str]]): The name of the method to rename.
25-
If a list is provided, it must contain exactly one tuple.
26-
27-
Steps:
28-
- Ensure only one method is being renamed at a time.
29-
- Check if the method exists in the Brightway2 database.
30-
- Open a dialog to prompt the user for the new method name.
31-
- Validate the new name to ensure it is not empty and does not already exist.
19+
Flow:
20+
- Ensure only one method is selected and it exists.
21+
- Prompt the user for the new name and validate it.
3222
- Copy the method to the new name and process it.
33-
- Emit a signal to notify that the method has been renamed.
34-
- Deregister the old method.
23+
- Update all Brightway calculation setups by replacing the old method in 'ia' lists with
24+
the new name and serialize the updates.
25+
- Emit a rename signal and deregister the old method.
3526
3627
Raises:
37-
ValueError: If more than one method is provided for renaming.
38-
RuntimeError: If the method does not exist, the new name is empty, or the new name already exists.
28+
- ValueError: If more than one method is provided for renaming.
29+
- RuntimeError: If the method does not exist, the new name is empty, or the new name already exists.
3930
"""
4031

4132
text = "Rename Impact Category"
@@ -57,14 +48,13 @@ def run(method_name: tuple[str] | list[tuple[str]]):
5748

5849
# open dialog to get new name
5950
dialog = widgets.ABListEditDialog(
60-
method_name,
61-
title="Rename Impact Category",
62-
parent=application.main_window
51+
method_name,
52+
title="Rename Impact Category",
53+
parent=application.main_window,
6354
)
64-
dialog.exec_()
6555

66-
# if dialog was cancelled, do nothing
67-
if not dialog.result() == QtWidgets.QDialog.Accepted:
56+
# execute the dialog and check for acceptance
57+
if dialog.exec_() != QtWidgets.QDialog.Accepted:
6858
return
6959

7060
new_name = dialog.get_data(as_tuple=True)
@@ -82,9 +72,41 @@ def run(method_name: tuple[str] | list[tuple[str]]):
8272
# copy method to new name and process
8373
method.copy(new_name).process()
8474

75+
# Update any calculation setups that reference this method
76+
MethodRename.rename_method_in_calculation_setups(method_name, new_name)
77+
8578
# this should not happen like this, as the model and therefore signals should be handled declaritavely,
8679
# but since method renaming is not native to bw2data we have to do it manually here
8780
signals.method.renamed.emit(method_name, new_name)
8881

8982
# deregister old method
9083
method.deregister()
84+
85+
@staticmethod
86+
def rename_method_in_calculation_setups(old_name: tuple, new_name: tuple) -> None:
87+
"""Replace occurrences of old_name with new_name in all CS 'ia' lists and serialize.
88+
89+
Handles both tuple and single-string method keys. Best-effort: logs on failure
90+
without blocking the rename flow.
91+
"""
92+
try:
93+
changed_any = False
94+
95+
for cs_name, cs in bd.calculation_setups.items():
96+
ia = cs.get("ia", [])
97+
98+
if old_name not in ia:
99+
continue
100+
101+
i = ia.index(old_name)
102+
ia[i] = new_name
103+
104+
changed_any = True
105+
log.info(
106+
f"Updated calculation setup '{cs_name}': renamed impact category {old_name} -> {new_name}"
107+
)
108+
109+
if changed_any:
110+
bd.calculation_setups.serialize()
111+
except Exception:
112+
log.exception("Failed to update calculation setups after method rename")

tests/actions/test_method_actions.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,66 @@ def test_method_new(monkeypatch, basic_database):
181181
# Verify the method is empty
182182
method_obj = Method(new_method)
183183
assert len(method_obj.load()) == 0
184+
185+
186+
def test_calculation_setups_updated_on_method_delete(monkeypatch, basic_database):
187+
# ensure method exists and is present in calculation setup
188+
method = ("basic_method",)
189+
190+
from bw2data import methods as bw_methods
191+
import bw2data as bd
192+
193+
assert method in bw_methods
194+
# Ensure the calculation setup references the method (arrange precondition)
195+
cs = bd.calculation_setups["basic_calculation_setup"]
196+
cs["ia"] = [method]
197+
bd.calculation_setups["basic_calculation_setup"] = cs
198+
bd.calculation_setups.serialize()
199+
200+
# auto-confirm deletion
201+
monkeypatch.setattr(
202+
QtWidgets.QMessageBox,
203+
"warning",
204+
staticmethod(lambda *args, **kwargs: QtWidgets.QMessageBox.Yes),
205+
)
206+
207+
actions.MethodDelete.run([method])
208+
209+
# method removed
210+
assert method not in bw_methods
211+
# calculation setup no longer references deleted method
212+
cs = bd.calculation_setups["basic_calculation_setup"]
213+
assert method not in cs.get("ia", [])
214+
215+
216+
def test_calculation_setups_updated_on_method_rename(monkeypatch, basic_database):
217+
# prepare rename dialog to accept and return new name
218+
from activity_browser.ui.widgets import ABListEditDialog
219+
import bw2data as bd
220+
221+
old = ("basic_method",)
222+
new = ("renamed_method",)
223+
224+
# Ensure the calculation setup references the old method (arrange precondition)
225+
cs = bd.calculation_setups["basic_calculation_setup"]
226+
cs["ia"] = [old]
227+
bd.calculation_setups["basic_calculation_setup"] = cs
228+
bd.calculation_setups.serialize()
229+
230+
monkeypatch.setattr(
231+
ABListEditDialog,
232+
"exec_",
233+
staticmethod(lambda *args, **kwargs: QtWidgets.QDialog.Accepted),
234+
)
235+
monkeypatch.setattr(
236+
ABListEditDialog,
237+
"get_data",
238+
staticmethod(lambda *args, **kwargs: new),
239+
)
240+
241+
actions.MethodRename.run(old)
242+
243+
# setups reference the new method name
244+
cs = bd.calculation_setups["basic_calculation_setup"]
245+
assert new in cs.get("ia", [])
246+
assert old not in cs.get("ia", [])

0 commit comments

Comments
 (0)