From 47ae2646b815351cfd865f362920d9acee895485 Mon Sep 17 00:00:00 2001 From: Daniel de Koning Date: Wed, 18 Nov 2020 14:02:06 +0100 Subject: [PATCH] Small fixes 261 (#472) * Catch unexpected KeyError (deleted activity) issues * Inherit different table view for scenario table * Drop SimpleCopy table view class * Drop SimpleCopy models * Add shift toggle to include table header on copy * Ensure correct extension is added to path if not given * Create separate method for extracting 7z archives * Add shortcuts for the different local imports * Simplify run_ecoinvent method --- activity_browser/app/ui/tables/models.py | 16 +---- activity_browser/app/ui/tables/scenarios.py | 4 +- activity_browser/app/ui/tables/views.py | 18 ++--- .../app/ui/tabs/LCA_results_tabs.py | 11 ++- .../app/ui/wizards/db_export_wizard.py | 2 + .../app/ui/wizards/db_import_wizard.py | 67 +++++++++++++------ 6 files changed, 66 insertions(+), 52 deletions(-) diff --git a/activity_browser/app/ui/tables/models.py b/activity_browser/app/ui/tables/models.py index cc158e1a4..83798cac0 100644 --- a/activity_browser/app/ui/tables/models.py +++ b/activity_browser/app/ui/tables/models.py @@ -57,18 +57,11 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): return self._dataframe.index[section] return None - def to_clipboard(self, rows, columns): + def to_clipboard(self, rows, columns, include_header: bool = False): """ Copy the given rows and columns of the dataframe to clipboard """ - self._dataframe.iloc[rows, columns].to_clipboard(index=False) - - -class SimpleCopyPandasModel(PandasModel): - """ Override the to_clipboard method to exclude copying table headers - """ - def to_clipboard(self, rows, columns): self._dataframe.iloc[rows, columns].to_clipboard( - index=False, header=False + index=False, header=include_header ) @@ -97,11 +90,6 @@ def flags(self, index): return super().flags(index) | Qt.ItemIsDragEnabled -class SimpleCopyDragPandasModel(SimpleCopyPandasModel): - def flags(self, index): - return super().flags(index) | Qt.ItemIsDragEnabled - - class EditableDragPandasModel(EditablePandasModel): def flags(self, index): return super().flags(index) | Qt.ItemIsDragEnabled diff --git a/activity_browser/app/ui/tables/scenarios.py b/activity_browser/app/ui/tables/scenarios.py index 2a084e62c..0cbb3a446 100644 --- a/activity_browser/app/ui/tables/scenarios.py +++ b/activity_browser/app/ui/tables/scenarios.py @@ -9,7 +9,7 @@ from ...bwutils.utils import Parameters from ...bwutils import presamples as ps_utils from ...signals import signals -from .views import ABDataFrameSimpleCopy, dataframe_sync +from .views import ABDataFrameView, dataframe_sync class PresamplesList(QComboBox): @@ -51,7 +51,7 @@ def get_package_names() -> List[str]: return ps_utils.find_all_package_names() -class ScenarioTable(ABDataFrameSimpleCopy): +class ScenarioTable(ABDataFrameView): """ Constructs an infinitely (horizontally) expandable table that is used to set specific amount for user-defined parameters. diff --git a/activity_browser/app/ui/tables/views.py b/activity_browser/app/ui/tables/views.py index 441a930f8..c0581d03e 100644 --- a/activity_browser/app/ui/tables/views.py +++ b/activity_browser/app/ui/tables/views.py @@ -11,8 +11,7 @@ from ...settings import ab_settings from .delegates import ViewOnlyDelegate from .models import (DragPandasModel, EditableDragPandasModel, - EditablePandasModel, PandasModel, - SimpleCopyDragPandasModel, SimpleCopyPandasModel) + EditablePandasModel, PandasModel) def dataframe_sync(sync): @@ -145,23 +144,16 @@ def keyPressEvent(self, e): NOTE: by default, the table headers (column names) are also copied. """ - if e.modifiers() and Qt.ControlModifier: + if e.modifiers() & Qt.ControlModifier: + # Should we include headers? + headers = e.modifiers() & Qt.ShiftModifier if e.key() == Qt.Key_C: # copy selection = [self.get_source_index(pindex) for pindex in self.selectedIndexes()] rows = [index.row() for index in selection] columns = [index.column() for index in selection] rows = sorted(set(rows), key=rows.index) columns = sorted(set(columns), key=columns.index) - self.model.to_clipboard(rows, columns) - - -class ABDataFrameSimpleCopy(ABDataFrameView): - """ A view-only class which copies values without including headers - """ - def _select_model(self) -> QAbstractTableModel: - if hasattr(self, 'drag_model'): - return SimpleCopyDragPandasModel(self.dataframe) - return SimpleCopyPandasModel(self.dataframe) + self.model.to_clipboard(rows, columns, headers) class ABDataFrameEdit(ABDataFrameView): diff --git a/activity_browser/app/ui/tabs/LCA_results_tabs.py b/activity_browser/app/ui/tabs/LCA_results_tabs.py index 96312925a..1c767c12a 100644 --- a/activity_browser/app/ui/tabs/LCA_results_tabs.py +++ b/activity_browser/app/ui/tabs/LCA_results_tabs.py @@ -124,8 +124,11 @@ def __init__(self, name: str, presamples=None, parent=None): def do_calculations(self): """Perform the MLCA calculation.""" if self.presamples is None: - self.mlca = MLCA(self.cs_name) - self.contributions = Contributions(self.mlca) + try: + self.mlca = MLCA(self.cs_name) + self.contributions = Contributions(self.mlca) + except KeyError as e: + raise BW2CalcError("LCA Failed", str(e)).with_traceback(e.__traceback__) elif isinstance(self.presamples, str): try: self.mlca = PresamplesMLCA(self.cs_name, self.presamples) @@ -136,6 +139,8 @@ def do_calculations(self): msg = ("Given scenario package refers to non-existent exchanges." " It is suggested to remove or edit this package.") raise BW2CalcError(msg, str(e)).with_traceback(e.__traceback__) + except KeyError as e: + raise BW2CalcError("LCA Failed", str(e)).with_traceback(e.__traceback__) else: try: self.mlca = SuperstructureMLCA(self.cs_name, self.presamples) @@ -150,6 +155,8 @@ def do_calculations(self): "Scenario LCA failed.", "Constructed LCA matrix does not contain any exchanges from the superstructure" ).with_traceback(e.__traceback__) + except KeyError as e: + raise BW2CalcError("LCA Failed", str(e)).with_traceback(e.__traceback__) self.mlca.calculate() self.mc = MonteCarloLCA(self.cs_name) diff --git a/activity_browser/app/ui/wizards/db_export_wizard.py b/activity_browser/app/ui/wizards/db_export_wizard.py index 438cb9533..257116e79 100644 --- a/activity_browser/app/ui/wizards/db_export_wizard.py +++ b/activity_browser/app/ui/wizards/db_export_wizard.py @@ -49,6 +49,8 @@ def perform_export(self) -> None: if ext and not ext == EXTENSIONS[export_as]: ext = EXTENSIONS[export_as] out_path = path + ext + elif not ext: + out_path = path + EXTENSIONS[export_as] EXPORTERS[export_as](db_name, out_path) diff --git a/activity_browser/app/ui/wizards/db_import_wizard.py b/activity_browser/app/ui/wizards/db_import_wizard.py index f168f9acd..b2678842b 100644 --- a/activity_browser/app/ui/wizards/db_import_wizard.py +++ b/activity_browser/app/ui/wizards/db_import_wizard.py @@ -677,32 +677,32 @@ def run(self): import_signals.cancel_sentinel = False if self.use_forwast: self.run_forwast() - elif self.use_local: + elif self.use_local: # excel or bw2package self.run_local_import() + elif self.datasets_path: # ecospold2 files + self.run_import(self.datasets_path) + elif self.archive_path: # 7zip file + self.run_extract_import() else: self.run_ecoinvent() - def run_ecoinvent(self): + def run_ecoinvent(self) -> None: + """Run the ecoinvent downloader from start to finish.""" + self.downloader.outdir = eidl.eidlstorage.eidl_dir + if self.downloader.check_stored(): + import_signals.download_complete.emit() + else: + self.run_download() + with tempfile.TemporaryDirectory() as tempdir: - dataset_dir = self.datasets_path or os.path.join(tempdir, "datasets") - if not os.path.isdir(dataset_dir): - if self.archive_path is None: - self.downloader.outdir = eidl.eidlstorage.eidl_dir - if self.downloader.check_stored(): - import_signals.download_complete.emit() - else: - self.run_download() - else: - self.downloader.out_path = self.archive_path - if not import_signals.cancel_sentinel: - self.run_extract(tempdir) if not import_signals.cancel_sentinel: + self.run_extract(tempdir) + if not import_signals.cancel_sentinel: + dataset_dir = os.path.join(tempdir, "datasets") self.run_import(dataset_dir) - def run_forwast(self): - """ - adapted from pjamesjoyce/lcopt - """ + def run_forwast(self) -> None: + """Adapted from pjamesjoyce/lcopt.""" response = requests.get(self.forwast_url) forwast_zip = zipfile.ZipFile(io.BytesIO(response.content)) import_signals.download_complete.emit() @@ -725,15 +725,40 @@ def run_forwast(self): else: self.delete_canceled_db() - def run_download(self): + def run_download(self) -> None: + """Use the connected ecoinvent downloader.""" self.downloader.download() import_signals.download_complete.emit() - def run_extract(self, temp_dir): + def run_extract(self, temp_dir) -> None: + """Use the connected ecoinvent downloader to extract the downloaded + 7zip file. + """ self.downloader.extract(target_dir=temp_dir) import_signals.unarchive_finished.emit() - def run_import(self, import_dir): + def run_extract_import(self) -> None: + """Combine the extract and import steps when beginning from a selected + 7zip archive. + + By default, look in the 'datasets' folder because this is how ecoinvent + 7zip archives are structured. If this folder is not found, fall back + to using the temporary directory instead. + """ + self.downloader.out_path = self.archive_path + with tempfile.TemporaryDirectory() as tempdir: + self.run_extract(tempdir) + if not import_signals.cancel_sentinel: + # Working with ecoinvent 7z file? look for 'datasets' dir + eco_dir = os.path.join(tempdir, "datasets") + if os.path.exists(eco_dir) and os.path.isdir(eco_dir): + self.run_import(eco_dir) + else: + # Use the temp dir itself instead. + self.run_import(tempdir) + + def run_import(self, import_dir) -> None: + """Use the given dataset path to import the ecospold2 files.""" try: importer = SingleOutputEcospold2Importer( import_dir,