From 4b91cb42edad074e3f82e4c6c5384337bc934d75 Mon Sep 17 00:00:00 2001 From: mandresm Date: Tue, 27 Jun 2023 16:34:45 +0200 Subject: [PATCH 001/107] clean up the awicm1 runscript directory and adds a daily-restart functioning runscript for levante, with all the choose blocks needed for later generalization in other computers --- .../awicm/awicm-mistral-initial-monthly.run | 47 -------- ...mistral-restart-LGM_BoundaryConditions.run | 101 ------------------ .../awicm/awicm-ollie-initial-monthly.run | 45 -------- .../awicm/awicm1-CMIP6-initial-daily.yaml | 90 ++++++++++++++++ .../awicm/echam-mistral-restart-monthly.run | 34 ------ .../awicm/foci-mistral-initial_monthly.run | 32 ------ 6 files changed, 90 insertions(+), 259 deletions(-) delete mode 100755 runscripts/awicm/awicm-mistral-initial-monthly.run delete mode 100755 runscripts/awicm/awicm-mistral-restart-LGM_BoundaryConditions.run delete mode 100755 runscripts/awicm/awicm-ollie-initial-monthly.run create mode 100644 runscripts/awicm/awicm1-CMIP6-initial-daily.yaml delete mode 100755 runscripts/awicm/echam-mistral-restart-monthly.run delete mode 100755 runscripts/awicm/foci-mistral-initial_monthly.run diff --git a/runscripts/awicm/awicm-mistral-initial-monthly.run b/runscripts/awicm/awicm-mistral-initial-monthly.run deleted file mode 100755 index 606079dbd..000000000 --- a/runscripts/awicm/awicm-mistral-initial-monthly.run +++ /dev/null @@ -1,47 +0,0 @@ -#! /bin/ksh -l -set -e - - -setup_name="awicm" -#check=1 - -account=ab0995 -compute_time="00:25:00" -############################################################################### - -INITIAL_DATE_awicm=2000-01-01 # Initial exp. date -FINAL_DATE_awicm=2000-02-01 # Final date of the experiment - -awicm_VERSION="CMIP6" -POST_PROCESSING_awicm=0 -SCENARIO_awicm="PI-CTRL" - -RES_fesom=CORE2 - -MODEL_DIR_awicm=${HOME}/esm-master/awicm-test/ - -BASE_DIR=/work/ab0995/a270058/esm_yaml_test/ - -POOL_DIR_fesom=/work/bm0944/input/ - -MESH_DIR_fesom=/pool/data/AWICM/FESOM1/MESHES/core/ - -NYEAR_awicm=0 # Number of years per run -NMONTH_awicm=1 # Number of months per run - -LRESUME_echam=0 -LRESUME_fesom=0 -LRESUME_oasis3mct=0 - -RESTART_RATE_fesom=1 -RESTART_FIRST_fesom=1 -RESTART_UNIT_fesom='m' - - - -further_reading_fesom="fesom_output_control.yaml" - - -############################################################################### -load_all_functions -general_do_it_all $@ diff --git a/runscripts/awicm/awicm-mistral-restart-LGM_BoundaryConditions.run b/runscripts/awicm/awicm-mistral-restart-LGM_BoundaryConditions.run deleted file mode 100755 index 7afc2651c..000000000 --- a/runscripts/awicm/awicm-mistral-restart-LGM_BoundaryConditions.run +++ /dev/null @@ -1,101 +0,0 @@ -#! /bin/ksh -l -set -e - -module purge - -export FUNCTION_PATH=${HOME}/esm-master/esm-runscripts/functions/all -export FPATH=$FUNCTION_PATH - -machine_name=mistral -setup_name="awicm" # mpiesm, pism_mpiesm, echam. mpiom, or pism -ACCOUNT=ab0246 -check=0 # run job in check mode, without submitting and / or launching - - -# the executable. Set to 0 to turn off -#compute_time="00:20:00" # this will cause walltime barriers, bu the job should start right away... -compute_time="03:00:00" -post_time="01:00:00" -ESM_USE_C_CALENDAR=1 -############################################################################### -awicm_VERSION="1.1" -MODEL_DIR_awicm=${HOME}/esm-master/awicm-test/ - - -########################## -## GENERAL SECTION ####### -########################## -expid="LGM_TEST" - -BASE_DIR=/work/ba0989/a270077/esm_tools_dev - -INITIAL_DATE=1850-01-01 # initial exp. date -FINAL_DATE=1852-01-01 # final date of the experiment - - -NYEAR_echam=1 # number of years per run -NMONTH_echam=0 - - -########################## -## ECHAM/JSBACH SECTION ## -########################## -RES_echam=T63 -SCENARIO_echam=PALEO - -echam_DATASET=r0007 -jsbach_DATASET=r0009 - -#BIN_DIR_echam=${HOME}/esm-master/echam-test/bin - -# if you want to start from echam restarts, use: -LRESUME_echam=1 -INI_RESTART_DIR_echam=/work/ab0246/a270064/esm-experiments/lgm_anm/restart/echam -INI_PARENT_DATE_echam=39281231 #, for example 24001231235640 -INI_PARENT_EXP_ID_echam=lgm_anm # of spinup experiment, for example TST - -nproca_echam=12 -nprocb_echam=24 - -INI_RESTART_DIR_jsbach=/work/ab0246/a270064/esm-experiments/lgm_anm/restart/jsbach -INI_RESTART_DIR_hdmodel=/work/ab0246/a270064/esm-experiments/lgm_anm/restart/hdmodel - -JAN_SURF_echam=/work/ab0246/a270064/esm-experiments/lgm_anm/input/echam/T63LGM_jan_surf.nc -VGRATCLIM_echam=/work/ab0246/a270064/esm-experiments/lgm_anm/input/echam/T63LGM_VGRATCLIM.nc -VLTCLIM_echam=/work/ab0246/a270064/esm-experiments/lgm_anm/input/echam/T63LGM_VLTCLIM.nc - -HDPARA_FILE_hdmodel=/work/ab0246/a270064/esm-experiments/lgm_anm/input/hdmodel/hdpara.nc -LAND_BOUNDARY_CONDITIONS_jsbach=/work/ab0246/a270064/esm-experiments/lgm_anm/input/jsbach/jsbach_T63LGM_11tiles_5layers_1850.nc -DYNVEG_jsbach=dynveg - -CO2_echam=190.0e-6 -CH4_echam=0.375e-6 -N2O_echam=0.200e-6 -CECC_echam=0.018994 -COBLD_echam=22.949 -CLONP_echam=294.42 -DISTURBED_YEARS_echam='1894' -DISTURBANCE_echam=1.00001 - -################### -## FESOM SECTION ## -################### - -RES_fesom=LGM -MESH_DIR_fesom=/mnt/lustre02/work/ba0989/a270077/FESOM_meshes/CORE2_LGM - -INI_RESTART_DIR_fesom=/work/ab0246/a270064/esm-experiments/lgm_anm/restart/fesom -INI_PARENT_DATE_fesom=39281231 #, for example 24001231235640 - -RESTART_RATE_fesom=1 -RESTART_FIRST_fesom=1 -RESTART_UNIT_fesom='y' - - - -further_reading_fesom="fesom_output_control.yaml" -############################################################################### - - -load_all_functions -general_do_it_all $@ diff --git a/runscripts/awicm/awicm-ollie-initial-monthly.run b/runscripts/awicm/awicm-ollie-initial-monthly.run deleted file mode 100755 index 21ed5b415..000000000 --- a/runscripts/awicm/awicm-ollie-initial-monthly.run +++ /dev/null @@ -1,45 +0,0 @@ -#! /bin/ksh -l -set -e - - -setup_name="awicm" -#check=1 - -account=ab0995 -compute_time="00:15:00" -############################################################################### - -INITIAL_DATE_awicm=2000-01-01 # Initial exp. date -FINAL_DATE_awicm=2000-02-29 # Final date of the experiment - -awicm_VERSION="CMIP6" -POST_PROCESSING_awicm=0 -SCENARIO_awicm="PI-CTRL" - -RES_fesom=CORE2 - -MODEL_DIR_awicm=/work/ollie/dbarbi/modelcodes/awicm-CMIP6/ -BASE_DIR=/work/ollie/dbarbi/esm_yaml_test/ - -POOL_DIR_fesom=/work/ollie/pool/FESOM/ -MESH_DIR_fesom=/work/ollie/pool/FESOM/meshes_default/core/ - -NYEAR_awicm=0 # Number of years per run -NMONTH_awicm=1 # Number of months per run - -LRESUME_echam=0 -LRESUME_fesom=0 -LRESUME_oasis3mct=0 - -RESTART_RATE_fesom=1 -RESTART_FIRST_fesom=1 -RESTART_UNIT_fesom='m' - - - -further_reading_fesom="fesom_output_control.yaml" - - -############################################################################### -load_all_functions -general_do_it_all $@ diff --git a/runscripts/awicm/awicm1-CMIP6-initial-daily.yaml b/runscripts/awicm/awicm1-CMIP6-initial-daily.yaml new file mode 100644 index 000000000..8abdcab29 --- /dev/null +++ b/runscripts/awicm/awicm1-CMIP6-initial-daily.yaml @@ -0,0 +1,90 @@ +general: + setup_name: 'awicm' + account: ab0995 #!ENV ${ACCOUNT} + compute_time: "00:30:00" + initial_date: "1850-01-03" + final_date: "1850-01-04" + base_dir: !ENV ${WORKCD} #${ESM_TESTING_DIR} + nyear: 0 + nmonth: 0 + nday: 1 + lresume: false + ini_restart_exp_id: i971_awi1 + ini_restart_dir: /work/ab0995/a270152/i971_awi1/restart/ + +awicm: + version: "CMIP6" + scenario: "PI-CTRL" + post_processing: 0 + model_dir: /work/ab0995/a270152/model_codes/awicm-CMIP6/ #!ENV ${MODEL_DIR} + pool_dir: /pool/data/ + resolution: T127 + +fesom: + restart_rate: 1 + restart_unit: "d" + restart_first: 1 + + choose_computer.name: + levante: + rpool_dir: /work/ab0995/a270152/testing_pool/input/ + rmesh_dir: /work/ab0995/a270152/testing_pool/AWICM/FESOM1/MESHES/core/ + rproc: 384 + pool_dir: ${fesom.rpool_dir} + mesh_dir: ${fesom.rmesh_dir} + resolution: CORE2 + time_step: 450 + nproc: ${rproc} + + lresume: ${general.lresume} + ini_restart_exp_id: ${general.ini_restart_exp_id} + ini_restart_date: "${prev_date}" + ini_restart_dir: ${general.ini_restart_dir}/fesom/ + +echam: + restart_rate: 1 + restart_unit: "days" + restart_first: 1 + + scenario: "PI-CTRL" + resolution: T127 + levels: L95 + post_processing: False + lresume: ${general.lresume} + choose_computer.name: + levante: + nproca: 32 + nprocb: 32 + "*": + nproca: 24 + nprocb: 24 + time_step: 200 + + ini_restart_exp_id: ${general.ini_restart_exp_id} + ini_restart_date: "${prev_date}" + ini_restart_dir: ${general.ini_restart_dir}/echam/ + +jsbach: + dataset: r0009 + streams: + - jsbach + - jsbid + - yasso + - surf + - veg + remove_jsbach.forcing_files: + - a_nd_file + - lightning + - popdens + - LU + + lresume: ${general.lresume} + ini_restart_exp_id: ${general.ini_restart_exp_id} + ini_restart_date: "${prev_date}" + ini_restart_dir: ${general.ini_restart_dir}/jsbach/ + +oasis3mct: + lresume: ${general.lresume} + ini_restart_exp_id: ${general.ini_restart_exp_id} + ini_restart_date: "${prev_date}" + ini_restart_dir: ${general.ini_restart_dir}/oasis3mct/ diff --git a/runscripts/awicm/echam-mistral-restart-monthly.run b/runscripts/awicm/echam-mistral-restart-monthly.run deleted file mode 100755 index 68eb3c163..000000000 --- a/runscripts/awicm/echam-mistral-restart-monthly.run +++ /dev/null @@ -1,34 +0,0 @@ -#! /bin/ksh -l -set -e - - -setup_name="echam_standalone" -#check=1 - -account=ab0995 -compute_time="00:15:00" -############################################################################### - -INITIAL_DATE=2000-01-01 # Initial exp. date -FINAL_DATE=2000-02-29 # Final date of the experiment - -echam_VERSION="6.3.04p1" -POST_PROCESSING_echam=0 -SCENARIO_echam="PI-CTRL" - -RES_echam=T63 - -MODEL_DIR=/work/ab0995/a270058/modelcodes/echam-6.3.04p1// -BASE_DIR=/work/ab0995/a270058/esm_yaml_test/ - -NYEAR=0 # Number of years per run -NMONTH=1 # Number of months per run - -LRESUME_echam=1 -INI_RESTART_DIR_echam=/work/ab0246/a270064/esm-experiments/lgm_anm/restart/echam -INI_PARENT_DATE_echam=37991231 -INI_PARENT_EXP_ID_echam=lgm_anm - -############################################################################### -load_all_functions -general_do_it_all $@ diff --git a/runscripts/awicm/foci-mistral-initial_monthly.run b/runscripts/awicm/foci-mistral-initial_monthly.run deleted file mode 100755 index 6da9cdcaa..000000000 --- a/runscripts/awicm/foci-mistral-initial_monthly.run +++ /dev/null @@ -1,32 +0,0 @@ - -setup_name="foci" -compute_time="00:15:00" -account=ab0995 - -############################################################################### - -INITIAL_DATE=2000-01-01 # Initial exp. date -FINAL_DATE=2000-02-29 # Final date of the experiment - -foci_VERSION="1.0" -POST_PROCESSING_foci=0 -SCENARIO="PI-CTRL" - -RES_nemo=ORCA05 -RES_echam=T63 - -MODEL_DIR=/work/ab0995/a270058/modelcodes/foci-1.0/ -BASE_DIR=/work/ab0995/a270058/esm_yaml_test/ - -NYEAR=0 # Number of years per run -NMONTH=1 # Number of months per run - -LRESUME_echam=0 -LRESUME_nemo=0 -LRESUME_oasis3mct=0 - -RESTART_RATE_nemo=1 -RESTART_FIRST_nemo=1 -RESTART_UNIT_nemo='m' - -############################################################################### From d6271ebe2096da81f5166309cce37e62fc732eab Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Tue, 27 Jun 2023 15:21:52 +0000 Subject: [PATCH 002/107] =?UTF-8?q?Bump=20version:=206.21.19=20=E2=86=92?= =?UTF-8?q?=206.21.20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index 933d60e1f..31d9bf63d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.21.19 +current_version = 6.21.20 commit = True tag = True diff --git a/setup.py b/setup.py index 3033acfbb..321f11f25 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.21.19", + version="6.21.20", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index c7386692d..e79ae5dba 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index 064d78e64..9ff96ce3d 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index afd6c596c..b980fff2c 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index 2b2919991..ff0c2c333 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index 76370577a..8e162cd71 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index f1efb69bf..dd94e08c3 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index 0aa165b0b..28948bb52 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index bbe8f3238..944f0d191 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index f6993e728..244145506 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index 04a178ecf..cb11818a6 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index cc742b840..f2c5ab8b5 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index 5a8e87a46..f6235694f 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index 08e86c3ef..b4550db35 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index 04a4297cc..60578b584 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.19" +__version__ = "6.21.20" from .utils import * From 87857754e1c13b4d336c7835866f160b9bd69f81 Mon Sep 17 00:00:00 2001 From: Paul Gierz Date: Thu, 29 Jun 2023 09:29:26 +0200 Subject: [PATCH 003/107] Update sbatch_flags.rst --- docs/recipes/sbatch_flags.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/recipes/sbatch_flags.rst b/docs/recipes/sbatch_flags.rst index 96072abf2..4599506c7 100644 --- a/docs/recipes/sbatch_flags.rst +++ b/docs/recipes/sbatch_flags.rst @@ -28,3 +28,17 @@ your runscript: computer: additional_flags: "--qos=24h" + +Adding more than one flag +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Alternatively, you can include a list of additional flags: + +.. code-block:: yaml + + computer: + additional_flags: + - "--qos=24h" + - "--comment='My Slurm Comment'" + +See the documentation for the batch scheduler on your HPC system to see the allowed options. From 57823cd21c3b51b949ee5689334f132865c05106 Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Tue, 4 Jul 2023 17:00:55 +0200 Subject: [PATCH 004/107] adding awicm3 v3.1.1 --- configs/components/oifs/oifs.yaml | 10 ++++ ...cm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5.yaml | 11 ++++ configs/setups/awicm3/awicm3.yaml | 7 +++ .../awicm3-v3.1.1-albedo-TCO95L91-CORE2.yaml | 53 +++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 configs/couplings/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5.yaml create mode 100644 runscripts/awicm3/v3.1.1/awicm3-v3.1.1-albedo-TCO95L91-CORE2.yaml diff --git a/configs/components/oifs/oifs.yaml b/configs/components/oifs/oifs.yaml index 4b92218d5..8a900222e 100644 --- a/configs/components/oifs/oifs.yaml +++ b/configs/components/oifs/oifs.yaml @@ -93,6 +93,7 @@ compile_infos: - 43r3-master - 43r3-awicm-3.0 - 43r3-awicm-3.1 + - 43r3-awicm-3.1.1 - 43r3-awicm-frontiers-xios - 43r3 - 40r1-foci @@ -139,6 +140,15 @@ compile_infos: install_bins: make/esm/oifs/bin/oifs destination: oifs-43r3 with_xios: true + 43r3-awicm-3.1.1: + requires: + - oasis3mct-4.0-awicm-3.1 + branch: awicm-3.1.1 + comp_command: "export OIFS_TOPLEVEL_DIR=${model_dir}; export OIFS_XIOS=enable ; export OIFS_XIOS_DIR=${model_dir}/../xios ; export OIFS_XIOS_INCLUDE=-I/${model_dir}/../xios/inc/; cd make; ../fcm/bin/fcm make -v -j8 -f oifs.fcm ; chmod -R ${source_code_permissions} .; mv esm/oifs/bin/master.exe esm/oifs/bin/oifs" + git-repository: https://gitlab.dkrz.de/ec-earth/oifs-43r3.git + install_bins: make/esm/oifs/bin/oifs + destination: oifs-43r3 + with_xios: true 43r3-awicm-frontiers-xios: requires: - oasis3mct-4.0-awicm-frontiers diff --git a/configs/couplings/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5.yaml b/configs/couplings/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5.yaml new file mode 100644 index 000000000..3bf104f09 --- /dev/null +++ b/configs/couplings/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5.yaml @@ -0,0 +1,11 @@ +components: +- xios-2.5 +- rnfmap-awicm-3.1 +- oifs-43r3-awicm-3.1.1 +- fesom-2.0-awicm-3.1 +- oasis3mct-4.0-awicm-3.1 +coupling_changes: +- sed -i '/COUPLENEMOECE = /s/.TRUE./.FALSE./g' oifs-43r3/src/ifs/module/yommcc.F90 +- sed -i '/COUPLEFESOM2 = /s/.FALSE./.TRUE./g' oifs-43r3/src/ifs/module/yommcc.F90 +- sed -i '/COUPLENEMOFOCI = /s/.TRUE./.FALSE./g' oifs-43r3/src/ifs/module/yommcc.F90 + diff --git a/configs/setups/awicm3/awicm3.yaml b/configs/setups/awicm3/awicm3.yaml index f87b56e51..cc4e80101 100644 --- a/configs/setups/awicm3/awicm3.yaml +++ b/configs/setups/awicm3/awicm3.yaml @@ -32,6 +32,13 @@ general: - xios add_further_reading: - xios/xios.env.yaml + v3.1.1: + couplings: + - fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5 + add_include_models: + - xios + add_further_reading: + - xios/xios.env.yaml master: couplings: - fesom-2.0-master+oifs-43r3-master diff --git a/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-albedo-TCO95L91-CORE2.yaml b/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-albedo-TCO95L91-CORE2.yaml new file mode 100644 index 000000000..64c1499b4 --- /dev/null +++ b/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-albedo-TCO95L91-CORE2.yaml @@ -0,0 +1,53 @@ +general: + user: !ENV ${USER} + setup_name: "awicm3" + version: "v3.1.1" + account: paleodyn.paleodyn + compute_time: "00:10:00" + initial_date: "2000-01-01" + final_date: "2000-02-01" + base_dir: "/albedo/work/user/${user}/runtime/${general.setup_name}-${general.version}/" + nday: 0 + nmonth: 1 + nyear: 0 + +computer: + taskset: true + additional_flags: [ --qos=30min ] + +awicm3: + postprocessing: false + model_dir: "/albedo/home/${user}/model_codes/${general.setup_name}-${general.version}//" + +fesom: + resolution: "CORE2" + pool_dir: "/albedo/work/user/mandresm/test_pool/clidyn/FESOM2/" + climate_data_dir: "${pool_dir}/hydrography/phc3.0/" + mesh_dir: "${pool_dir}/meshes/core2/" + restart_rate: 1 + restart_unit: "m" + restart_first: 1 + lresume: false + time_step: 1800 + nproc: 128 + +oifs: + resolution: "TCO95" + levels: "L91" + prepifs_expid: aack + input_expid: awi3 + wam: true + lresume: false + time_step: 3600 + nproc: 128 + omp_num_threads: 1 + +oasis3mct: + lresume: false # Set to false to generate the rst files for first leg + time_step: 7200 + +xios: + with_model: oifs + nproc: 4 + omp_num_threads: 32 + From 44e6d747f739b0d4072be43ddaba6be115830251 Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Wed, 5 Jul 2023 10:53:01 +0200 Subject: [PATCH 005/107] remove fesom2 warning for missing restart files with the old now 99.9% unused format --- configs/components/fesom/fesom-2.0.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configs/components/fesom/fesom-2.0.yaml b/configs/components/fesom/fesom-2.0.yaml index e542a987e..03336ab4b 100644 --- a/configs/components/fesom/fesom-2.0.yaml +++ b/configs/components/fesom/fesom-2.0.yaml @@ -170,8 +170,8 @@ restart_in_in_work: fesom_bin_restart_info: fesom_bin_restart/*.info fesom_bin_restart: fesom_bin_restart/np${nproc}/* restart_in_sources: - oce_restart: fesom.${parent_date!syear}.oce.restart.nc - ice_restart: fesom.${parent_date!syear}.ice.restart.nc + oce_restart: fesom.${parent_date!syear}.o*e.restart.nc + ice_restart: fesom.${parent_date!syear}.i*e.restart.nc par_oce_restart: fesom.${parent_date!syear}.oce.restart/*.nc par_ice_restart: fesom.${parent_date!syear}.ice.restart/*.nc fesom_raw_restart_info: fesom_raw_restart/*.info From 086c127145ffce6d226cc55688bc7a7794030e4e Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 6 Jul 2023 14:48:34 +0200 Subject: [PATCH 006/107] add TODO comment to the change --- configs/components/fesom/fesom-2.0.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/configs/components/fesom/fesom-2.0.yaml b/configs/components/fesom/fesom-2.0.yaml index 03336ab4b..212bcc386 100644 --- a/configs/components/fesom/fesom-2.0.yaml +++ b/configs/components/fesom/fesom-2.0.yaml @@ -170,6 +170,8 @@ restart_in_in_work: fesom_bin_restart_info: fesom_bin_restart/*.info fesom_bin_restart: fesom_bin_restart/np${nproc}/* restart_in_sources: + # TODO: When cleaning up the FESOM yamls, remove the wildcard and make this file + # be dependent on the corresponding namelist option oce_restart: fesom.${parent_date!syear}.o*e.restart.nc ice_restart: fesom.${parent_date!syear}.i*e.restart.nc par_oce_restart: fesom.${parent_date!syear}.oce.restart/*.nc From 074b21170c0c8b4d9ff92c25825b08e810e973cd Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Thu, 6 Jul 2023 12:51:33 +0000 Subject: [PATCH 007/107] =?UTF-8?q?Bump=20version:=206.21.20=20=E2=86=92?= =?UTF-8?q?=206.21.21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index 31d9bf63d..6e3007d2b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.21.20 +current_version = 6.21.21 commit = True tag = True diff --git a/setup.py b/setup.py index 321f11f25..365c33e12 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.21.20", + version="6.21.21", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index e79ae5dba..441f427ea 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index 9ff96ce3d..ca588caf4 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index b980fff2c..2b7c700c3 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index ff0c2c333..7a034cb49 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index 8e162cd71..0b5d35dbe 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index dd94e08c3..297146e9b 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index 28948bb52..bd4206c33 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index 944f0d191..36bda94f0 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index 244145506..922fad465 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index cb11818a6..149c5f320 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index f2c5ab8b5..79a11d8ef 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index f6235694f..214b3b9ff 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index b4550db35..19e9dd1ac 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index 60578b584..073069537 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.20" +__version__ = "6.21.21" from .utils import * From 24fcbdf58f27026d8f48fa91d8ec19928916d3e2 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Fri, 7 Jul 2023 15:20:39 +0200 Subject: [PATCH 008/107] fix oasis restart for awicm3 when branching off an experiment but loading the oasis fluxes from the pool. It also fixes it for a branchoff experiment which only has 1 run --- src/esm_runscripts/oasis.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/esm_runscripts/oasis.py b/src/esm_runscripts/oasis.py index 8c13f748e..e8c87729b 100644 --- a/src/esm_runscripts/oasis.py +++ b/src/esm_runscripts/oasis.py @@ -472,11 +472,14 @@ def add_restart_files(self, restart_file, fconfig): if len(glob_restart_file) == 1: restart_file = os.path.basename(glob_restart_file[0]) elif len(glob_restart_file) == 0: - user_error( - "Restart file missing", - f"No OASIS restart file for ``{restart_file}`` found " - f"matching the pattern ``{glob_search_file}``" - ) + restart_file = f"{config['ini_restart_dir']}{restart_file}" + if not os.path.isfile(restart_file): + user_error( + "Restart file missing", + f"No OASIS restart file for ``{restart_file}`` found " + f"matching the pattern ``{glob_search_file}`` nor " + f"``{restart_file}``" + ) else: if not gconfig["isinteractive"]: # If more than one restart file found that matches ini_restart_date, From 594fc4992b5c50190abee91d9d8c85425fe68e0b Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Fri, 7 Jul 2023 13:54:31 +0000 Subject: [PATCH 009/107] =?UTF-8?q?Bump=20version:=206.21.21=20=E2=86=92?= =?UTF-8?q?=206.21.22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6e3007d2b..ba1da7841 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.21.21 +current_version = 6.21.22 commit = True tag = True diff --git a/setup.py b/setup.py index 365c33e12..2131abf12 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.21.21", + version="6.21.22", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index 441f427ea..bc87c8136 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index ca588caf4..b54e9cce8 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index 2b7c700c3..a508c7d59 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index 7a034cb49..5c92a3296 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index 0b5d35dbe..99fdfe214 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index 297146e9b..801d9fba4 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index bd4206c33..bd08e088f 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index 36bda94f0..b7cd9711b 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index 922fad465..7597a74b3 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index 149c5f320..f79521730 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index 79a11d8ef..075be70e4 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index 214b3b9ff..d62a9f0b1 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index 19e9dd1ac..7a39733ab 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index 073069537..938b3795e 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.21" +__version__ = "6.21.22" from .utils import * From cb6ca92fa4c147695566f8d4dcd7382eb5ca9917 Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Mon, 10 Jul 2023 17:21:42 +0200 Subject: [PATCH 010/107] using same defaults for minor versions --- configs/setups/awicm3/awicm3.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/configs/setups/awicm3/awicm3.yaml b/configs/setups/awicm3/awicm3.yaml index cc4e80101..91eadd3a3 100644 --- a/configs/setups/awicm3/awicm3.yaml +++ b/configs/setups/awicm3/awicm3.yaml @@ -19,6 +19,7 @@ general: available_versions: - 'v3.0' - 'v3.1' + - 'v3.1.1' - 'master' - 'frontiers-xios' choose_version: @@ -121,7 +122,7 @@ oifs: namelist_dir: ${general.esm_namelist_dir}/oifs/${version}/awicm3/v3.0 mip: "cmip6" wam: true - "v3.1": + "v3.1*": with_xios: True namelist_dir: ${general.esm_namelist_dir}/oifs/${version}/awicm3/v3.1 mip: "cmip6" @@ -353,7 +354,7 @@ fesom: # Remove ICMCL_INIT forcing, which is only required for standalone simulations (form v3.2 onwards contains seasonal leaf area index, veg albedo etc.) remove_forcing_files: - ICMCL_INIT - "v3.1": + "v3.1*": namelist_dir: "${esm_namelist_dir}/fesom2/2.0/awicm3/v3.1/" add_namelist_changes: namelist.oce: @@ -562,7 +563,7 @@ oasis3mct: - xios rstos_file_name: 'rstos' rstas_file_name: 'rstas' - 'v3.1': + 'v3.1*': mct_version: 4.0 input_dir: ${pool_dir}/cy${oifs.version}/${oifs.resolution}-${fesom.resolution}/ rstos_file_name: 'rstos_uv' From a04e63fe8f4f1df549ca47a62697fb7b4df8b212 Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Mon, 10 Jul 2023 17:38:18 +0200 Subject: [PATCH 011/107] runscript for v3.1.1 --- .../awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml diff --git a/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml b/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml new file mode 100644 index 000000000..3d91ac5d2 --- /dev/null +++ b/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml @@ -0,0 +1,51 @@ +general: + user: !ENV ${USER} + setup_name: "awicm3" + version: "v3.1" + account: "ab0995" + compute_time: "00:20:00" + initial_date: "2000-01-01" + final_date: "2000-02-01" + base_dir: "/work/ab0246/${user}/runtime/${general.setup_name}-${general.version}/" + nday: 0 + nmonth: 1 + nyear: 0 + +computer: + taskset: true + +awicm3: + postprocessing: false + model_dir: "/home/a/${user}/model_codes/${general.setup_name}-${general.version}//" + +fesom: + resolution: "CORE2" + pool_dir: "/work/ab0246/a270092/input/fesom2/" + mesh_dir: "${pool_dir}/core2/" + restart_rate: 1 + restart_unit: "m" + restart_first: 1 + lresume: false + time_step: 1800 + nproc: 128 + +oifs: + resolution: "TCO95" + levels: "L91" + prepifs_expid: aack + input_expid: awi3 + wam: true + lresume: false + time_step: 3600 + nproc: 128 + omp_num_threads: 1 + +oasis3mct: + lresume: true # Set to false to generate the rst files for first leg + time_step: 7200 + +xios: + with_model: oifs + nproc: 4 + omp_num_threads: 32 + From f67065c55c7dc74a3f54bb478a17032e6b2362cd Mon Sep 17 00:00:00 2001 From: mandresm Date: Tue, 11 Jul 2023 13:02:14 +0200 Subject: [PATCH 012/107] fix multiple bugs in the oasis coupling field restarts --- configs/setups/awicm3/awicm3.yaml | 4 +- esm_tools/motd/motd.yaml | 4 +- motd/motd.yaml | 4 +- src/esm_runscripts/oasis.py | 132 +++++++++++++++++++----------- src/esm_tools/motd/motd.yaml | 4 +- 5 files changed, 94 insertions(+), 54 deletions(-) diff --git a/configs/setups/awicm3/awicm3.yaml b/configs/setups/awicm3/awicm3.yaml index 4ca5e9c24..1fb2430b0 100644 --- a/configs/setups/awicm3/awicm3.yaml +++ b/configs/setups/awicm3/awicm3.yaml @@ -706,9 +706,9 @@ oasis3mct: true: lag: "${time_step}" # If ini_restart_dir is not specified in the runscript and, therefore, - # it is not a branched off experiment add the /fesom.nproc/ to the + # it is not a branched off experiment adds the /fesom.nproc/ to the # sources when the run_number is 1 - load_restart_from_pool: "$(( '${input_dir}' == '${ini_restart_dir}' and ${general.run_number} == 1))" + load_restart_from_pool: "$(( os.path.realpath('${input_dir}') == os.path.realpath('${ini_restart_dir}') and ${general.run_number} == 1 ))" choose_load_restart_from_pool: true: add_restart_in_sources: diff --git a/esm_tools/motd/motd.yaml b/esm_tools/motd/motd.yaml index c2a652a84..01a6c248c 100644 --- a/esm_tools/motd/motd.yaml +++ b/esm_tools/motd/motd.yaml @@ -134,9 +134,9 @@ release6.7: action: DELAY(1) announcement: true -oasis_branchoff6.21.17: +oasis_branchoff6.21.23: package: "esm_tools" - versions: "<6.21.17" + versions: "<6.21.23" message: "\x1b[1m\x1b[96mOASIS BRANCH-OFF RESTARTS FIXED!\x1b[0m\n The bug on the copying of the wrong flux files from OASIS \x1b[96mwhen running\n diff --git a/motd/motd.yaml b/motd/motd.yaml index c2a652a84..01a6c248c 100644 --- a/motd/motd.yaml +++ b/motd/motd.yaml @@ -134,9 +134,9 @@ release6.7: action: DELAY(1) announcement: true -oasis_branchoff6.21.17: +oasis_branchoff6.21.23: package: "esm_tools" - versions: "<6.21.17" + versions: "<6.21.23" message: "\x1b[1m\x1b[96mOASIS BRANCH-OFF RESTARTS FIXED!\x1b[0m\n The bug on the copying of the wrong flux files from OASIS \x1b[96mwhen running\n diff --git a/src/esm_runscripts/oasis.py b/src/esm_runscripts/oasis.py index e8c87729b..bdb757204 100644 --- a/src/esm_runscripts/oasis.py +++ b/src/esm_runscripts/oasis.py @@ -404,11 +404,35 @@ def add_output_file(self, lefts, rights, leftmodel, rightmodel, config): config["outdata_in_work"][thisfile] = thisfile config["outdata_sources"][thisfile] = thisfile - def add_restart_files(self, restart_file, fconfig): + def add_restart_files(self, restart_file_label, fconfig): + """ + Handles the special restart case of the coupling fields. + + Cases + ----- + 1. If this run is a restart but not a branch-off experiment, set the source to + be the same as defined by the user in ``restart_in_in_sources`` or the same + as the name coming from ``coupling__fields``, if the first is + missing (the normal case). + 2. Same as case 1 but with the time stamp added to the name of the restart file + to make sure the correct file (and not a link to the last restart file made) + is loaded for the branch-off experiment. This option uses the + non-timestamped version of the file when only one file is found (e.g. the + parent simulation only has one run, or the files are taken for the first + run from the pool, as in AWICM3). + + Parameters + ---------- + restart_file_label : str + The file's label (not the file name itself!). Used to retrieve the + file's source and target path. As defined in the keys of + ``coupling_target_fields`` or ``coupling_input_fields`` in the yamls + fconfig : ConfigSetup + The complete simulation configuration. + """ config = fconfig[self.name] gconfig = fconfig["general"] - restart_file_label = restart_file is_runtime = gconfig["run_or_compile"] == "runtime" enddate = "_" + gconfig["end_date"].format( form=9, givenph=False, givenpm=False, givenps=False @@ -431,23 +455,32 @@ def add_restart_files(self, restart_file, fconfig): if "restart_in_sources" not in config: config["restart_in_sources"] = {} - config["restart_out_files"][restart_file] = restart_file - config["restart_out_files"][restart_file + "_recv"] = restart_file + "_recv" + # Find the actual path of the restart + restart_file_path = config["restart_in_sources"].get(restart_file_label, None) + # Find the actual name of the restart: if a path is given in restart_in_sources + # get the basename of that path, otherwise assign the file label also as name + # of the file (coming from ``coupling__fields``) + if restart_file_path: + restart_file = os.path.basename(restart_file_label) + else: + restart_file = restart_file_label + + config["restart_out_files"][restart_file_label] = restart_file + config["restart_out_files"][restart_file_label + "_recv"] = restart_file + "_recv" - config["restart_out_in_work"][restart_file] = restart_file # + enddate - config["restart_out_in_work"][restart_file + "_recv"] = ( + config["restart_out_in_work"][restart_file_label] = restart_file # + enddate + config["restart_out_in_work"][restart_file_label + "_recv"] = ( restart_file + "_recv" ) # + enddate - config["restart_out_sources"][restart_file] = restart_file - config["restart_out_sources"][restart_file + "_recv"] = restart_file + "_recv" + config["restart_out_sources"][restart_file_label] = restart_file + config["restart_out_sources"][restart_file_label + "_recv"] = restart_file + "_recv" - config["restart_in_files"][restart_file] = restart_file - config["restart_in_in_work"][restart_file] = restart_file + config["restart_in_files"][restart_file_label] = restart_file + config["restart_in_in_work"][restart_file_label] = restart_file # In case of a branch-off experiment -> use the correct oasis restart files: - # Not the rstas.nc soft link to the last, but the actual one for the - # branch-off date + # Not the soft link to the last, but the actual one for the branch-off date if gconfig["run_number"] == 1 and config["lresume"] and gconfig["jobtype"] == "prepcompute": # If they do not exist, define ``ini_restart_date`` and ``ini_restart_dir`` # based on ``ini_parent_date`` and ``ini_parent_dir`` @@ -455,49 +488,56 @@ def add_restart_files(self, restart_file, fconfig): config["ini_restart_date"] = config["ini_parent_date"] if "ini_parent_dir" in config and "ini_restart_dir" not in config: config["ini_restart_dir"] = config["ini_parent_dir"] + # If the restart file path is not defined set it to be the same as the + # ini_restart_dir + if not restart_file_path: + restart_file_path = config["ini_restart_dir"] # If set in config (oasis): if "ini_restart_dir" in config and "ini_restart_date" in config: # check if restart file with ini_restart_date in filename is in the restart # folder of the parent experiment to be branched off from: glob_search_file = ( - f"{config['ini_restart_dir']}{restart_file}_????????-" + f"{restart_file_path}_????????-" f"{config['ini_restart_date'].year}" f"{config['ini_restart_date'].month:02}" f"{config['ini_restart_date'].day:02}" ) - glob_restart_file = glob.glob(glob_search_file) - glob_restart_file.sort() - if restart_file and is_runtime: - # If there are more than one file found let the user decide which one to take - if len(glob_restart_file) == 1: - restart_file = os.path.basename(glob_restart_file[0]) - elif len(glob_restart_file) == 0: - restart_file = f"{config['ini_restart_dir']}{restart_file}" - if not os.path.isfile(restart_file): - user_error( - "Restart file missing", - f"No OASIS restart file for ``{restart_file}`` found " - f"matching the pattern ``{glob_search_file}`` nor " - f"``{restart_file}``" - ) - else: - if not gconfig["isinteractive"]: - # If more than one restart file found that matches ini_restart_date, - # ask the user to select from the result list: - message = ( - "More than one OASIS restart file was found for " - "your branchoff experiment that matches the " - "ini_restart_date you selected. Please select " - "one of the following OASIS restart files:" - ) - answers = questionary.form( - restarts = questionary.select(message, choices=glob_restart_file) - ).ask() - restart_file = os.path.basename(answers["restarts"]) - - config["restart_in_sources"][restart_file_label] = restart_file - - if restart_file not in config["restart_in_sources"]: + else: + glob_search_file = restart_file_path + + glob_restart_file = glob.glob(glob_search_file) + glob_restart_file.sort() + if restart_file and is_runtime: + # If there are more than one file found let the user decide which one to take + if len(glob_restart_file) == 1: + restart_file = os.path.basename(glob_restart_file[0]) + elif len(glob_restart_file) == 0: + restart_file = restart_file_path + if not os.path.isfile(restart_file): + user_error( + "Restart file missing", + f"No OASIS restart file for ``{restart_file}`` found " + f"matching the pattern ``{glob_search_file}`` nor " + f"``{restart_file}``" + ) + else: + if not gconfig["isinteractive"]: + # If more than one restart file found that matches ini_restart_date, + # ask the user to select from the result list: + message = ( + "More than one OASIS restart file was found for " + "your branchoff experiment that matches the " + "ini_restart_date you selected. Please select " + "one of the following OASIS restart files:" + ) + answers = questionary.form( + restarts = questionary.select(message, choices=glob_restart_file) + ).ask() + restart_file = answers["restarts"] + + config["restart_in_sources"][restart_file_label] = restart_file + + if restart_file_label not in config["restart_in_sources"]: config["restart_in_sources"][restart_file_label] = restart_file diff --git a/src/esm_tools/motd/motd.yaml b/src/esm_tools/motd/motd.yaml index c2a652a84..01a6c248c 100644 --- a/src/esm_tools/motd/motd.yaml +++ b/src/esm_tools/motd/motd.yaml @@ -134,9 +134,9 @@ release6.7: action: DELAY(1) announcement: true -oasis_branchoff6.21.17: +oasis_branchoff6.21.23: package: "esm_tools" - versions: "<6.21.17" + versions: "<6.21.23" message: "\x1b[1m\x1b[96mOASIS BRANCH-OFF RESTARTS FIXED!\x1b[0m\n The bug on the copying of the wrong flux files from OASIS \x1b[96mwhen running\n From fc3bf10365632e81e95786f4d55a8111122ea4cc Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Tue, 11 Jul 2023 17:52:21 +0200 Subject: [PATCH 013/107] need to set this for some reason --- .../v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml b/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml index 3d91ac5d2..3f0f79b81 100644 --- a/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml +++ b/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml @@ -22,6 +22,11 @@ fesom: resolution: "CORE2" pool_dir: "/work/ab0246/a270092/input/fesom2/" mesh_dir: "${pool_dir}/core2/" + tide_forcing_dir: "${pool_dir}/fesom/tide_forcing/" + add_namelist_changes: + namelist.config: + paths: + tideforcingpath: "${pool_dir}/fesom/tide_forcing/" restart_rate: 1 restart_unit: "m" restart_first: 1 @@ -41,7 +46,7 @@ oifs: omp_num_threads: 1 oasis3mct: - lresume: true # Set to false to generate the rst files for first leg + lresume: false # Set to false to generate the rst files for first leg time_step: 7200 xios: From fcde5b7cc58bc4365016f74a139574f9a858952c Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Wed, 12 Jul 2023 10:25:36 +0200 Subject: [PATCH 014/107] using version and major_version instead of wildcards --- configs/setups/awicm3/awicm3.yaml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/configs/setups/awicm3/awicm3.yaml b/configs/setups/awicm3/awicm3.yaml index e5742c66c..b52b64b41 100644 --- a/configs/setups/awicm3/awicm3.yaml +++ b/configs/setups/awicm3/awicm3.yaml @@ -24,9 +24,11 @@ general: - 'frontiers-xios' choose_version: v3.0: + major_version: v3.0 couplings: - fesom-2.0-awicm-3.0+oifs-43r3-awicm-3.0 v3.1: + major_version: v3.1 couplings: - fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1+xios-2.5 add_include_models: @@ -34,6 +36,7 @@ general: add_further_reading: - xios/xios.env.yaml v3.1.1: + major_version: v3.1 couplings: - fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.1+xios-2.5 add_include_models: @@ -41,6 +44,7 @@ general: add_further_reading: - xios/xios.env.yaml master: + major_version: master couplings: - fesom-2.0-master+oifs-43r3-master add_include_models: @@ -48,6 +52,7 @@ general: add_further_reading: - xios/xios.env.yaml frontiers-xios: + major_version: frontiers-xios couplings: - fesom-2.0-frontiers+oifs-43r3-awicm-frontiers-xios+xios-2.5 add_include_models: @@ -117,12 +122,12 @@ oifs: rtables_dir: ${input_dir}/rtables/ forcing_dir: ${input_dir}/${version}/ifsdata/ ifsdata_dir: ${input_dir}/${version}/ifsdata/ - choose_general.version: + choose_general.major_version: "v3.0": namelist_dir: ${general.esm_namelist_dir}/oifs/${version}/awicm3/v3.0 mip: "cmip6" wam: true - "v3.1*": + "v3.1": with_xios: True namelist_dir: ${general.esm_namelist_dir}/oifs/${version}/awicm3/v3.1 mip: "cmip6" @@ -344,7 +349,7 @@ fesom: oce_tra: surf_relax_s: "${surf_relax_s}" - choose_general.version: + choose_general.major_version: "v3.0": namelist_dir: "${esm_namelist_dir}/fesom2/2.0/awicm3/v3.0/" add_namelist_changes: @@ -354,7 +359,7 @@ fesom: # Remove ICMCL_INIT forcing, which is only required for standalone simulations (form v3.2 onwards contains seasonal leaf area index, veg albedo etc.) remove_forcing_files: - ICMCL_INIT - "v3.1*": + "v3.1": namelist_dir: "${esm_namelist_dir}/fesom2/2.0/awicm3/v3.1/" add_namelist_changes: namelist.oce: @@ -555,7 +560,7 @@ oasis3mct: r2a_seq: 2 coupling_time_step: "${oasis3mct.time_step}" - choose_general.version: + choose_general.major_version: 'frontiers-xios': mct_version: 4.0 input_dir: ${pool_dir}/cy${oifs.version}/${oifs.resolution}-${fesom.resolution}/ @@ -563,7 +568,7 @@ oasis3mct: - xios rstos_file_name: 'rstos' rstas_file_name: 'rstas' - 'v3.1*': + 'v3.1': mct_version: 4.0 input_dir: ${pool_dir}/cy${oifs.version}/${oifs.resolution}-${fesom.resolution}/ rstos_file_name: 'rstos_uv' From 43d2d06aa865deadb60e6b56e1ac6894a96d95b0 Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Wed, 12 Jul 2023 12:02:41 +0200 Subject: [PATCH 015/107] adding awicm3.1.2 where the orbital forcing double radiation call bug has been fixed --- configs/components/oifs/oifs.yaml | 10 ++++++++++ configs/setups/awicm3/awicm3.yaml | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/configs/components/oifs/oifs.yaml b/configs/components/oifs/oifs.yaml index 8a900222e..e9f80b42a 100644 --- a/configs/components/oifs/oifs.yaml +++ b/configs/components/oifs/oifs.yaml @@ -94,6 +94,7 @@ compile_infos: - 43r3-awicm-3.0 - 43r3-awicm-3.1 - 43r3-awicm-3.1.1 + - 43r3-awicm-3.1.2 - 43r3-awicm-frontiers-xios - 43r3 - 40r1-foci @@ -149,6 +150,15 @@ compile_infos: install_bins: make/esm/oifs/bin/oifs destination: oifs-43r3 with_xios: true + 43r3-awicm-3.1.2: + requires: + - oasis3mct-4.0-awicm-3.1 + branch: awicm-3.1.2 + comp_command: "export OIFS_TOPLEVEL_DIR=${model_dir}; export OIFS_XIOS=enable ; export OIFS_XIOS_DIR=${model_dir}/../xios ; export OIFS_XIOS_INCLUDE=-I/${model_dir}/../xios/inc/; cd make; ../fcm/bin/fcm make -v -j8 -f oifs.fcm ; chmod -R ${source_code_permissions} .; mv esm/oifs/bin/master.exe esm/oifs/bin/oifs" + git-repository: https://gitlab.dkrz.de/ec-earth/oifs-43r3.git + install_bins: make/esm/oifs/bin/oifs + destination: oifs-43r3 + with_xios: true 43r3-awicm-frontiers-xios: requires: - oasis3mct-4.0-awicm-frontiers diff --git a/configs/setups/awicm3/awicm3.yaml b/configs/setups/awicm3/awicm3.yaml index b52b64b41..727424373 100644 --- a/configs/setups/awicm3/awicm3.yaml +++ b/configs/setups/awicm3/awicm3.yaml @@ -43,6 +43,14 @@ general: - xios add_further_reading: - xios/xios.env.yaml + v3.1.2: + major_version: v3.1 + couplings: + - fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5 + add_include_models: + - xios + add_further_reading: + - xios/xios.env.yaml master: major_version: master couplings: From b23b564543bd80378d7b2af5a371748e827eec16 Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Wed, 12 Jul 2023 12:14:30 +0200 Subject: [PATCH 016/107] Update awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml --- .../awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml b/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml index 3f0f79b81..7d5a6b1a1 100644 --- a/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml +++ b/runscripts/awicm3/v3.1.1/awicm3-v3.1.1-levante-TCO95L91-CORE2.yaml @@ -1,7 +1,7 @@ general: user: !ENV ${USER} setup_name: "awicm3" - version: "v3.1" + version: "v3.1.1" account: "ab0995" compute_time: "00:20:00" initial_date: "2000-01-01" From 322dd48c21201b0d748eefcd7eca999df6ef103c Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Wed, 12 Jul 2023 12:15:14 +0200 Subject: [PATCH 017/107] adding coupling and runscripts --- ...cm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5.yaml | 11 ++++ .../awicm3-v3.1.2-albedo-TCO95L91-CORE2.yaml | 53 ++++++++++++++++++ .../awicm3-v3.1.2-levante-TCO95L91-CORE2.yaml | 56 +++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 configs/couplings/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5.yaml create mode 100644 runscripts/awicm3/v3.1.2/awicm3-v3.1.2-albedo-TCO95L91-CORE2.yaml create mode 100644 runscripts/awicm3/v3.1.2/awicm3-v3.1.2-levante-TCO95L91-CORE2.yaml diff --git a/configs/couplings/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5.yaml b/configs/couplings/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5.yaml new file mode 100644 index 000000000..583e8cd9a --- /dev/null +++ b/configs/couplings/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5/fesom-2.0-awicm-3.1+oifs-43r3-awicm-3.1.2+xios-2.5.yaml @@ -0,0 +1,11 @@ +components: +- xios-2.5 +- rnfmap-awicm-3.1 +- oifs-43r3-awicm-3.1.2 +- fesom-2.0-awicm-3.1 +- oasis3mct-4.0-awicm-3.1 +coupling_changes: +- sed -i '/COUPLENEMOECE = /s/.TRUE./.FALSE./g' oifs-43r3/src/ifs/module/yommcc.F90 +- sed -i '/COUPLEFESOM2 = /s/.FALSE./.TRUE./g' oifs-43r3/src/ifs/module/yommcc.F90 +- sed -i '/COUPLENEMOFOCI = /s/.TRUE./.FALSE./g' oifs-43r3/src/ifs/module/yommcc.F90 + diff --git a/runscripts/awicm3/v3.1.2/awicm3-v3.1.2-albedo-TCO95L91-CORE2.yaml b/runscripts/awicm3/v3.1.2/awicm3-v3.1.2-albedo-TCO95L91-CORE2.yaml new file mode 100644 index 000000000..f73edab15 --- /dev/null +++ b/runscripts/awicm3/v3.1.2/awicm3-v3.1.2-albedo-TCO95L91-CORE2.yaml @@ -0,0 +1,53 @@ +general: + user: !ENV ${USER} + setup_name: "awicm3" + version: "v3.1.2" + account: paleodyn.paleodyn + compute_time: "00:10:00" + initial_date: "2000-01-01" + final_date: "2000-02-01" + base_dir: "/albedo/work/user/${user}/runtime/${general.setup_name}-${general.version}/" + nday: 0 + nmonth: 1 + nyear: 0 + +computer: + taskset: true + additional_flags: [ --qos=30min ] + +awicm3: + postprocessing: false + model_dir: "/albedo/home/${user}/model_codes/${general.setup_name}-${general.version}//" + +fesom: + resolution: "CORE2" + pool_dir: "/albedo/work/user/mandresm/test_pool/clidyn/FESOM2/" + climate_data_dir: "${pool_dir}/hydrography/phc3.0/" + mesh_dir: "${pool_dir}/meshes/core2/" + restart_rate: 1 + restart_unit: "m" + restart_first: 1 + lresume: false + time_step: 1800 + nproc: 128 + +oifs: + resolution: "TCO95" + levels: "L91" + prepifs_expid: aack + input_expid: awi3 + wam: true + lresume: false + time_step: 3600 + nproc: 128 + omp_num_threads: 1 + +oasis3mct: + lresume: false # Set to false to generate the rst files for first leg + time_step: 7200 + +xios: + with_model: oifs + nproc: 4 + omp_num_threads: 32 + diff --git a/runscripts/awicm3/v3.1.2/awicm3-v3.1.2-levante-TCO95L91-CORE2.yaml b/runscripts/awicm3/v3.1.2/awicm3-v3.1.2-levante-TCO95L91-CORE2.yaml new file mode 100644 index 000000000..55ced8241 --- /dev/null +++ b/runscripts/awicm3/v3.1.2/awicm3-v3.1.2-levante-TCO95L91-CORE2.yaml @@ -0,0 +1,56 @@ +general: + user: !ENV ${USER} + setup_name: "awicm3" + version: "v3.1.2" + account: "ab0995" + compute_time: "00:20:00" + initial_date: "2000-01-01" + final_date: "2000-02-01" + base_dir: "/work/ab0246/${user}/runtime/${general.setup_name}-${general.version}/" + nday: 0 + nmonth: 1 + nyear: 0 + +computer: + taskset: true + +awicm3: + postprocessing: false + model_dir: "/home/a/${user}/model_codes/${general.setup_name}-${general.version}//" + +fesom: + resolution: "CORE2" + pool_dir: "/work/ab0246/a270092/input/fesom2/" + mesh_dir: "${pool_dir}/core2/" + tide_forcing_dir: "${pool_dir}/fesom/tide_forcing/" + add_namelist_changes: + namelist.config: + paths: + tideforcingpath: "${pool_dir}/fesom/tide_forcing/" + restart_rate: 1 + restart_unit: "m" + restart_first: 1 + lresume: false + time_step: 1800 + nproc: 128 + +oifs: + resolution: "TCO95" + levels: "L91" + prepifs_expid: aack + input_expid: awi3 + wam: true + lresume: false + time_step: 3600 + nproc: 128 + omp_num_threads: 1 + +oasis3mct: + lresume: false # Set to false to generate the rst files for first leg + time_step: 7200 + +xios: + with_model: oifs + nproc: 4 + omp_num_threads: 32 + From 9f3a60d77997d249a10002536731d7819b3911a4 Mon Sep 17 00:00:00 2001 From: Jan Streffing Date: Wed, 12 Jul 2023 14:22:09 +0200 Subject: [PATCH 018/107] Update awicm3.yaml --- configs/setups/awicm3/awicm3.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/configs/setups/awicm3/awicm3.yaml b/configs/setups/awicm3/awicm3.yaml index 727424373..970e5a1b7 100644 --- a/configs/setups/awicm3/awicm3.yaml +++ b/configs/setups/awicm3/awicm3.yaml @@ -20,6 +20,7 @@ general: - 'v3.0' - 'v3.1' - 'v3.1.1' + - 'v3.1.2' - 'master' - 'frontiers-xios' choose_version: From fb3f2dc8d295bfec219db36963aa6f2144e34be3 Mon Sep 17 00:00:00 2001 From: mandresm Date: Wed, 12 Jul 2023 16:43:37 +0200 Subject: [PATCH 019/107] fix another issue with the oasis restarts --- src/esm_runscripts/oasis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_runscripts/oasis.py b/src/esm_runscripts/oasis.py index bdb757204..e1fa75717 100644 --- a/src/esm_runscripts/oasis.py +++ b/src/esm_runscripts/oasis.py @@ -491,7 +491,7 @@ def add_restart_files(self, restart_file_label, fconfig): # If the restart file path is not defined set it to be the same as the # ini_restart_dir if not restart_file_path: - restart_file_path = config["ini_restart_dir"] + restart_file_path = f"{config['ini_restart_dir']}/{restart_file}" # If set in config (oasis): if "ini_restart_dir" in config and "ini_restart_date" in config: # check if restart file with ini_restart_date in filename is in the restart From 3238de54b52a8f637fe94d9c5d21af111d5e339c Mon Sep 17 00:00:00 2001 From: mandresm Date: Wed, 12 Jul 2023 18:01:21 +0200 Subject: [PATCH 020/107] fix the standard branchoff for oasis3mct --- src/esm_runscripts/oasis.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/esm_runscripts/oasis.py b/src/esm_runscripts/oasis.py index e1fa75717..50cbe1f47 100644 --- a/src/esm_runscripts/oasis.py +++ b/src/esm_runscripts/oasis.py @@ -488,9 +488,9 @@ def add_restart_files(self, restart_file_label, fconfig): config["ini_restart_date"] = config["ini_parent_date"] if "ini_parent_dir" in config and "ini_restart_dir" not in config: config["ini_restart_dir"] = config["ini_parent_dir"] - # If the restart file path is not defined set it to be the same as the - # ini_restart_dir - if not restart_file_path: + # If the restart file path is not defined, or it's not an absolute path to + # the file set it to be the same as the ini_restart_dir + if not restart_file_path or restart_file_path==restart_file: restart_file_path = f"{config['ini_restart_dir']}/{restart_file}" # If set in config (oasis): if "ini_restart_dir" in config and "ini_restart_date" in config: @@ -516,7 +516,7 @@ def add_restart_files(self, restart_file_label, fconfig): if not os.path.isfile(restart_file): user_error( "Restart file missing", - f"No OASIS restart file for ``{restart_file}`` found " + f"No OASIS restart file for ``{restart_file_label}`` found " f"matching the pattern ``{glob_search_file}`` nor " f"``{restart_file}``" ) From 9641e2d0a74909cdcbe631f0918204fff2898d2d Mon Sep 17 00:00:00 2001 From: Miguel <63242832+mandresm@users.noreply.github.com> Date: Wed, 12 Jul 2023 19:59:16 +0200 Subject: [PATCH 021/107] Update src/esm_runscripts/oasis.py --- src/esm_runscripts/oasis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_runscripts/oasis.py b/src/esm_runscripts/oasis.py index 50cbe1f47..99622fff0 100644 --- a/src/esm_runscripts/oasis.py +++ b/src/esm_runscripts/oasis.py @@ -489,7 +489,7 @@ def add_restart_files(self, restart_file_label, fconfig): if "ini_parent_dir" in config and "ini_restart_dir" not in config: config["ini_restart_dir"] = config["ini_parent_dir"] # If the restart file path is not defined, or it's not an absolute path to - # the file set it to be the same as the ini_restart_dir + # the file, set it to be the same as the ini_restart_dir if not restart_file_path or restart_file_path==restart_file: restart_file_path = f"{config['ini_restart_dir']}/{restart_file}" # If set in config (oasis): From 05969a395ea812c27b7a166529f9c894b3f16b4e Mon Sep 17 00:00:00 2001 From: Miguel <63242832+mandresm@users.noreply.github.com> Date: Thu, 13 Jul 2023 09:07:50 +0200 Subject: [PATCH 022/107] Update src/esm_runscripts/oasis.py --- src/esm_runscripts/oasis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_runscripts/oasis.py b/src/esm_runscripts/oasis.py index 99622fff0..ee63e1883 100644 --- a/src/esm_runscripts/oasis.py +++ b/src/esm_runscripts/oasis.py @@ -411,7 +411,7 @@ def add_restart_files(self, restart_file_label, fconfig): Cases ----- 1. If this run is a restart but not a branch-off experiment, set the source to - be the same as defined by the user in ``restart_in_in_sources`` or the same + be the same as defined by the user in ``restart_in_sources`` or the same as the name coming from ``coupling__fields``, if the first is missing (the normal case). 2. Same as case 1 but with the time stamp added to the name of the restart file From 9a6404cbdaf54386f73f4e6f145483b581081e05 Mon Sep 17 00:00:00 2001 From: Miguel <63242832+mandresm@users.noreply.github.com> Date: Thu, 13 Jul 2023 09:16:09 +0200 Subject: [PATCH 023/107] Update src/esm_runscripts/oasis.py Co-authored-by: Paul Gierz --- src/esm_runscripts/oasis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_runscripts/oasis.py b/src/esm_runscripts/oasis.py index ee63e1883..c834ff100 100644 --- a/src/esm_runscripts/oasis.py +++ b/src/esm_runscripts/oasis.py @@ -490,7 +490,7 @@ def add_restart_files(self, restart_file_label, fconfig): config["ini_restart_dir"] = config["ini_parent_dir"] # If the restart file path is not defined, or it's not an absolute path to # the file, set it to be the same as the ini_restart_dir - if not restart_file_path or restart_file_path==restart_file: + if not restart_file_path or restart_file_path == restart_file: restart_file_path = f"{config['ini_restart_dir']}/{restart_file}" # If set in config (oasis): if "ini_restart_dir" in config and "ini_restart_date" in config: From 7c4020a663cf6fb1dde2b9e26726ed9e68d4ec36 Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Thu, 13 Jul 2023 08:07:08 +0000 Subject: [PATCH 024/107] =?UTF-8?q?Bump=20version:=206.21.22=20=E2=86=92?= =?UTF-8?q?=206.21.23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index ba1da7841..e3122993c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.21.22 +current_version = 6.21.23 commit = True tag = True diff --git a/setup.py b/setup.py index 2131abf12..b8fa837a9 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.21.22", + version="6.21.23", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index bc87c8136..c9da02350 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index b54e9cce8..750077c7b 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index a508c7d59..25a219d9a 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index 5c92a3296..e8b360497 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index 99fdfe214..d64926f2d 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index 801d9fba4..855f52c64 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index bd08e088f..db585350d 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index b7cd9711b..e76efdab4 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index 7597a74b3..cbd51336a 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index f79521730..290afd858 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index 075be70e4..1595746c1 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index d62a9f0b1..1f4540c7e 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index 7a39733ab..92f0c18f0 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index 938b3795e..c51390411 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.22" +__version__ = "6.21.23" from .utils import * From 9df3a6e8d8b36205ac38d87c38c79744fd855c02 Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Thu, 13 Jul 2023 13:43:48 +0000 Subject: [PATCH 025/107] =?UTF-8?q?Bump=20version:=206.21.23=20=E2=86=92?= =?UTF-8?q?=206.22.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index e3122993c..059c8c1a6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.21.23 +current_version = 6.22.0 commit = True tag = True diff --git a/setup.py b/setup.py index b8fa837a9..bb04ebf6b 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.21.23", + version="6.22.0", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index c9da02350..7ea0195a4 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index 750077c7b..085f23f1b 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index 25a219d9a..b1fa5b16b 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index e8b360497..7e06fcc4d 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index d64926f2d..6f4d89378 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index 855f52c64..c84010188 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index db585350d..dfbdc0253 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index e76efdab4..8134161d0 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index cbd51336a..97aeb8541 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index 290afd858..6120865cb 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index 1595746c1..1589cc249 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index 1f4540c7e..eaf938d6c 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index 92f0c18f0..39e0584a5 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index c51390411..542dc4833 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.21.23" +__version__ = "6.22.0" from .utils import * From b5095931be335b4b4e8468d0554b70a78146e1f4 Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Thu, 13 Jul 2023 15:55:12 +0000 Subject: [PATCH 026/107] =?UTF-8?q?Bump=20version:=206.22.0=20=E2=86=92=20?= =?UTF-8?q?6.23.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index 059c8c1a6..5ff0ca9f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.22.0 +current_version = 6.23.0 commit = True tag = True diff --git a/setup.py b/setup.py index bb04ebf6b..dac15361b 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.22.0", + version="6.23.0", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index 7ea0195a4..6fac4660f 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index 085f23f1b..759bb6d5e 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index b1fa5b16b..4a4fe858e 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index 7e06fcc4d..014fec0e8 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index 6f4d89378..74b5ec78f 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index c84010188..98681988f 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index dfbdc0253..0b0d98a55 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index 8134161d0..29a0e546d 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index 97aeb8541..55572ad56 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index 6120865cb..00932e6f7 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index 1589cc249..7ee0aab33 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index eaf938d6c..f4dcd81c3 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index 39e0584a5..96561ba71 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index 542dc4833..284b6011f 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.22.0" +__version__ = "6.23.0" from .utils import * From 22b88981d6b57db551398ec1b6d9c8d11a89c38e Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Mon, 24 Jul 2023 17:04:12 +0200 Subject: [PATCH 027/107] First attempt to catch wrong syntax in add_namelist_changes block. --- src/esm_runscripts/namelists.py | 84 ++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 1f9c85f6a..f1ec4119b 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -127,45 +127,63 @@ def nmls_remove(mconfig): namelist_changes = mconfig.get("namelist_changes", {}) namelist_removes = [] for namelist in list(namelist_changes): + # NOTE (NW): Check if namelist is a file, or in the format file_name.ext ? changes = namelist_changes[namelist] logging.debug("Determining remove entires for %s", namelist) logging.debug("All changes: %s", changes) for change_chapter in list(changes): change_entries = changes[change_chapter] - for key in list(change_entries): - value = change_entries[key] - if value == "remove_from_namelist": - namelist_removes.append((namelist, change_chapter, key)) - - # the key is probably coming from esm_tools config - # files or from a user runscript. It can contain lower - # case, but the original Fortran namelist could be in - # any case combination. Here `original_key` is coming - # from the default namelist and may contain mixed case. - # `key` is the processed variable from f90nml module and - # is lowercase. - remove_original_key = False - - # traverse the namelist chapter and see if a mixed case - # variable is also found - for key2 in namelist_changes[namelist][change_chapter]: - # take care of the MiXeD FORTRAN CaSeS - if key2.lower() == key.lower() and key2 != key: - original_key = key2 - remove_original_key = True - namelist_removes.append( - (namelist, change_chapter, original_key) + if isinstance(change_entries, dict): + for key in list(change_entries): + value = change_entries[key] + if value == "remove_from_namelist": + namelist_removes.append((namelist, change_chapter, key)) + + # the key is probably coming from esm_tools config + # files or from a user runscript. It can contain lower + # case, but the original Fortran namelist could be in + # any case combination. Here `original_key` is coming + # from the default namelist and may contain mixed case. + # `key` is the processed variable from f90nml module and + # is lowercase. + remove_original_key = False + + # traverse the namelist chapter and see if a mixed case + # variable is also found + for key2 in namelist_changes[namelist][change_chapter]: + # take care of the MiXeD FORTRAN CaSeS + if key2.lower() == key.lower() and key2 != key: + original_key = key2 + remove_original_key = True + namelist_removes.append( + (namelist, change_chapter, original_key) + ) + + # remove both lowercase and mixed case variables + del namelist_changes[namelist][change_chapter][key] + if remove_original_key: + del namelist_changes[namelist][change_chapter][original_key] + + # mconfig instead of config, Grrrrr + print( + f"- NOTE: removing the variable: {key} from the namelist: {namelist}" + ) + else: + message = ( + "There is something wrong within a 'add_namelist_changes' block. " + "Propably in your runscript. It seems that either 'namelist_file' or " + "'namelist_group' is not set.\n" + "Please make sure that add_namelist_changes are set in the following syntax:\n" + "\t add_namelist_changes:\n" + "\t\t'':\n" + "\t\t\t'':\n" + "\t\t\t\t: " ) - - # remove both lowercase and mixed case variables - del namelist_changes[namelist][change_chapter][key] - if remove_original_key: - del namelist_changes[namelist][change_chapter][original_key] - - # mconfig instead of config, Grrrrr - print( - f"- NOTE: removing the variable: {key} from the namelist: {namelist}" - ) + user_error( + "add_namelist_changes syntax error", + message, + dsymbols=["``", "'"], + ) for remove in namelist_removes: namelist, change_chapter, key = remove From 1b19e3cfe885ddabe912558d368378fe732f198f Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Tue, 25 Jul 2023 14:20:25 +0200 Subject: [PATCH 028/107] Add one more exception for syntax error in add_namelist_changes and changed error message. --- src/esm_runscripts/namelists.py | 123 ++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 55 deletions(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index f1ec4119b..3fe7b5d71 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -126,64 +126,77 @@ def nmls_remove(mconfig): namelist_changes = mconfig.get("namelist_changes", {}) namelist_removes = [] + error_message = ( + "There is a syntax error, probably in your runscript, regarding namelist " + "changes (e.g. in a 'add_namelist_changes' block). " + "It seems that either 'namelist_file' or " + "'namelist_group' or 'both' are missing.\n" + "Please make sure that namelist changes are specified in the correct syntax (see example below)" + " and rerun your runscript.\n" + "Example for add_namelist_changes:\n" + "\t add_namelist_changes:\n" + "\t\t'':\n" + "\t\t\t'':\n" + "\t\t\t\t: " + ) for namelist in list(namelist_changes): - # NOTE (NW): Check if namelist is a file, or in the format file_name.ext ? changes = namelist_changes[namelist] - logging.debug("Determining remove entires for %s", namelist) - logging.debug("All changes: %s", changes) - for change_chapter in list(changes): - change_entries = changes[change_chapter] - if isinstance(change_entries, dict): - for key in list(change_entries): - value = change_entries[key] - if value == "remove_from_namelist": - namelist_removes.append((namelist, change_chapter, key)) - - # the key is probably coming from esm_tools config - # files or from a user runscript. It can contain lower - # case, but the original Fortran namelist could be in - # any case combination. Here `original_key` is coming - # from the default namelist and may contain mixed case. - # `key` is the processed variable from f90nml module and - # is lowercase. - remove_original_key = False - - # traverse the namelist chapter and see if a mixed case - # variable is also found - for key2 in namelist_changes[namelist][change_chapter]: - # take care of the MiXeD FORTRAN CaSeS - if key2.lower() == key.lower() and key2 != key: - original_key = key2 - remove_original_key = True - namelist_removes.append( - (namelist, change_chapter, original_key) - ) - - # remove both lowercase and mixed case variables - del namelist_changes[namelist][change_chapter][key] - if remove_original_key: - del namelist_changes[namelist][change_chapter][original_key] - - # mconfig instead of config, Grrrrr - print( - f"- NOTE: removing the variable: {key} from the namelist: {namelist}" - ) - else: - message = ( - "There is something wrong within a 'add_namelist_changes' block. " - "Propably in your runscript. It seems that either 'namelist_file' or " - "'namelist_group' is not set.\n" - "Please make sure that add_namelist_changes are set in the following syntax:\n" - "\t add_namelist_changes:\n" - "\t\t'':\n" - "\t\t\t'':\n" - "\t\t\t\t: " + # Check if namelist_changes are specified in correct syntac (e.g. in runscript) + # If correct syntax, changes is always a dict. + if isinstance(changes, dict): + logging.debug("Determining remove entires for %s", namelist) + logging.debug("All changes: %s", changes) + for change_chapter in list(changes): + change_entries = changes[change_chapter] + # Check if namelist_changes are specified in correct syntac (e.g. in runscript) + # If correct syntax, change_entries is always a dict. + if isinstance(change_entries, dict): + for key in list(change_entries): + value = change_entries[key] + if value == "remove_from_namelist": + namelist_removes.append((namelist, change_chapter, key)) + + # the key is probably coming from esm_tools config + # files or from a user runscript. It can contain lower + # case, but the original Fortran namelist could be in + # any case combination. Here `original_key` is coming + # from the default namelist and may contain mixed case. + # `key` is the processed variable from f90nml module and + # is lowercase. + remove_original_key = False + + # traverse the namelist chapter and see if a mixed case + # variable is also found + for key2 in namelist_changes[namelist][change_chapter]: + # take care of the MiXeD FORTRAN CaSeS + if key2.lower() == key.lower() and key2 != key: + original_key = key2 + remove_original_key = True + namelist_removes.append( + (namelist, change_chapter, original_key) + ) + + # remove both lowercase and mixed case variables + del namelist_changes[namelist][change_chapter][key] + if remove_original_key: + del namelist_changes[namelist][change_chapter][original_key] + + # mconfig instead of config, Grrrrr + print( + f"- NOTE: removing the variable: {key} from the namelist: {namelist}" ) - user_error( - "add_namelist_changes syntax error", - message, - dsymbols=["``", "'"], - ) + else: + user_error( + "Syntax error in namelist changes", + error_message, + dsymbols=["``", "'"], + ) + else: + user_error( + "Syntax error in namelist changes", + error_message, + dsymbols=["``", "'"], + ) for remove in namelist_removes: namelist, change_chapter, key = remove From 0e31bd0376743d051b57d86c613aa7955c63695d Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Wed, 26 Jul 2023 14:59:06 +0200 Subject: [PATCH 029/107] docs for venv --- docs/esm_runscripts.rst | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index 16b05bee9..190ee957a 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -352,3 +352,57 @@ As an example, to configure `esm_runscripts` for an echam-experiment to link the Both ways to set the entries are doing the same thing. It is possible, as in the ``input`` case, to set the file movement method independently for each of the directions; the setting ``all_directions`` is just a shortcut if the method is identical for all of them. + +Running a experiment with a virtual environment +----------------------------------------------- + +Running jobs can optionally be encapsulated into a virtual environment. + +This shields the run from changes made to the remainder of the ESM-Tool installation, +and it's strongly recommended for production runs. + +Before the first run, a local copy will be installed in the experiment tree, +and **that** installation will be used. For example, for a user ``miguel`` +with a run with `expid` ``test`` ESM-Tools will be installed here:: + + /scratch/miguel/test/.venv_esmtools/lib/python3.10/site-packages/esm_tools + +instead of:: + + /albedo/home/miguel/.local/lib/site-packages/esm_tools + +If you choose to use a virtual environment, a folder named ``.venv_esmtools`` +will be created at the root of your experiment. This contains all the Python +libraries used by ESM-Tools. The first installation induces some overhead (~2-3 minutes). + +The virtual environment installs by default the ``release`` branch, pulling it directly +from our GitHub repository. You can choose to override this default by specifying another +branch, adding to your runscript: + +.. code-block:: yaml + + general: + install_esm_tools_branch: '' + +.. warning:: + The branch **needs to exist on GitHub** as it is cloned form there, and **not from your + local folder**. If you made any changes in your local branch make sure they are pushed before + running esm_runscripts with a virtual environment, so that your changes are included in the + virtual environment installation. + +You may also select to install esm_tools in `editable mode`, in which case +they will be installed in a folder ``src/esm_tools/`` in the root of +your experiment. Any changes made to code in that folder **will** influence how +ESM-Tools behave. To create a virtual environment with ESM-Tools installed in +`editable` mode use: + +.. code-block:: yaml + + general: + install__editable: true/false + +.. note:: + When using a virtual environment, config files and namelists will come of the + folder .venv_esmtools listed above and **not** from your user install directory. + You should make **all** changes to the namelists and config files via your user + runscript. This is recommended in all cases!!! From a52d6b2aa3a4d38afae4986914b252d1648bb476 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Wed, 26 Jul 2023 15:06:36 +0200 Subject: [PATCH 030/107] improvement of the docs for venvs --- docs/esm_runscripts.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index 190ee957a..6a8534842 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -358,6 +358,15 @@ Running a experiment with a virtual environment Running jobs can optionally be encapsulated into a virtual environment. +To run using a virtual environment run ``esm_runscripts`` with the flag +``--contained-run`` or set ``use_venv`` within the ``general`` section of your +runscript to ``True``: + +.. code-block:: yaml + + general: + use_venv: True + This shields the run from changes made to the remainder of the ESM-Tool installation, and it's strongly recommended for production runs. From 2e609d5d16866e992199112b7f1b58578e215850 Mon Sep 17 00:00:00 2001 From: Miguel <63242832+mandresm@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:08:55 +0200 Subject: [PATCH 031/107] Update docs/esm_runscripts.rst Co-authored-by: Nadine Wieters <31928930+nwieters@users.noreply.github.com> --- docs/esm_runscripts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index 6a8534842..1bf456d58 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -353,7 +353,7 @@ As an example, to configure `esm_runscripts` for an echam-experiment to link the Both ways to set the entries are doing the same thing. It is possible, as in the ``input`` case, to set the file movement method independently for each of the directions; the setting ``all_directions`` is just a shortcut if the method is identical for all of them. -Running a experiment with a virtual environment +Running an experiment with a virtual environment ----------------------------------------------- Running jobs can optionally be encapsulated into a virtual environment. From f08f62daf39760fa701857c1badd35b7bdc06413 Mon Sep 17 00:00:00 2001 From: Miguel <63242832+mandresm@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:09:10 +0200 Subject: [PATCH 032/107] Update docs/esm_runscripts.rst Co-authored-by: Nadine Wieters <31928930+nwieters@users.noreply.github.com> --- docs/esm_runscripts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index 1bf456d58..de5d9a476 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -358,7 +358,7 @@ Running an experiment with a virtual environment Running jobs can optionally be encapsulated into a virtual environment. -To run using a virtual environment run ``esm_runscripts`` with the flag +To use a virtual environment run ``esm_runscripts`` with the flag ``--contained-run`` or set ``use_venv`` within the ``general`` section of your runscript to ``True``: From acd449d99705b62eff5a58b385fbc51a458bbcd7 Mon Sep 17 00:00:00 2001 From: Miguel <63242832+mandresm@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:09:22 +0200 Subject: [PATCH 033/107] Update docs/esm_runscripts.rst Co-authored-by: Nadine Wieters <31928930+nwieters@users.noreply.github.com> --- docs/esm_runscripts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index de5d9a476..47bfbae83 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -401,7 +401,7 @@ branch, adding to your runscript: You may also select to install esm_tools in `editable mode`, in which case they will be installed in a folder ``src/esm_tools/`` in the root of -your experiment. Any changes made to code in that folder **will** influence how +your experiment. Any changes made to the code in that folder **will** influence how ESM-Tools behave. To create a virtual environment with ESM-Tools installed in `editable` mode use: From ba00c0b141f93f36df6b3abea29f482e36e83477 Mon Sep 17 00:00:00 2001 From: Miguel <63242832+mandresm@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:10:57 +0200 Subject: [PATCH 034/107] Update docs/esm_runscripts.rst Co-authored-by: Nadine Wieters <31928930+nwieters@users.noreply.github.com> --- docs/esm_runscripts.rst | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index 47bfbae83..9bfffda31 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -370,9 +370,9 @@ runscript to ``True``: This shields the run from changes made to the remainder of the ESM-Tool installation, and it's strongly recommended for production runs. -Before the first run, a local copy will be installed in the experiment tree, -and **that** installation will be used. For example, for a user ``miguel`` -with a run with `expid` ``test`` ESM-Tools will be installed here:: +If you choose to use a virtual environment, a local installation will be created in the experiment tree at the begining of the first run into the folder named ``.venv_esmtools``. **That** installation will be used for the experiment. It will be installed at the root of your experiment and contains all the Python libraries used by ESM-Tools. The installation at the beginning of the experiment will induce a small overhead (~2-3 minutes). + +For example, for a user ``miguel`` with a run with `expid` ``test`` ESM-Tools will be installed here:: /scratch/miguel/test/.venv_esmtools/lib/python3.10/site-packages/esm_tools @@ -380,10 +380,6 @@ instead of:: /albedo/home/miguel/.local/lib/site-packages/esm_tools -If you choose to use a virtual environment, a folder named ``.venv_esmtools`` -will be created at the root of your experiment. This contains all the Python -libraries used by ESM-Tools. The first installation induces some overhead (~2-3 minutes). - The virtual environment installs by default the ``release`` branch, pulling it directly from our GitHub repository. You can choose to override this default by specifying another branch, adding to your runscript: From 19c7d58eedd24f6321f72e86e96143f461125a14 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Wed, 26 Jul 2023 16:16:39 +0200 Subject: [PATCH 035/107] sugestion from @nwieters --- docs/esm_runscripts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index 6a8534842..d0eb34dac 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -414,4 +414,4 @@ ESM-Tools behave. To create a virtual environment with ESM-Tools installed in When using a virtual environment, config files and namelists will come of the folder .venv_esmtools listed above and **not** from your user install directory. You should make **all** changes to the namelists and config files via your user - runscript. This is recommended in all cases!!! + runscript (:ref:`yaml:Changing Namelists`). This is recommended in all cases!!! From 329bf9fa3eb2f3d28f217521efd83059fb93b191 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Wed, 26 Jul 2023 16:24:35 +0200 Subject: [PATCH 036/107] Implementing review suggestions. --- src/esm_runscripts/namelists.py | 178 ++++++++++++++++++++------------ 1 file changed, 114 insertions(+), 64 deletions(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 3fe7b5d71..d450fedd4 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -90,6 +90,74 @@ def nmls_load(mconfig): mconfig["namelists"][nml].uppercase = True return mconfig + @staticmethod + def nmls_check_changes(namelist_changes): + """ + Checks if namelist changes are given in correct syntax. + If not, a user_error will be raised and stopp the execution. + + Programmer Information + ---------------------- + + Parameters + ---------- + namelist_changes : nested dict + + If the syntax is correct, namelist_changes should be a nested dict of the following form: + {'namelist1.nml': {'namelist1': {'variable1': 'value1', 'variable2': 'value2', 'variable3': 'value3'}, 'namelist2': {'variable1': value1}}} + + Returns + ------- + None + + Calls to other methods: + ---------------------- + """ + + error_message = ( + "There is a syntax error, probably in your runscript (but potentially in other yaml files), " + "regarding namelist changes (e.g. in a 'add_namelist_changes' block). " + "It seems that either 'namelist_file' or " + "'namelist_group' or 'both' are missing.\n" + "Please make sure that namelist changes are specified in the correct syntax (see example below)" + " and rerun your runscript.\n" + ) + example = ( + "\nExample for [add_]namelist_changes:\n" + "\t [add_]namelist_changes:\n" + "\t '':\n" + "\t '':\n" + "\t : " + ) + + nml_syntax_error = False + + for namelist in list(namelist_changes): + changes = namelist_changes[namelist] + # Check if namelist_changes are specified in correct syntax (e.g. in runscript) + # If correct syntax, changes is always a dict. + if not isinstance(changes, dict): + nml_syntax_error = True + this_is_wrong = (f"There is a syntax error in the following lines:\n\n[add_]namelist_changes:\n {namelist}: {changes}\n...") +# this_is_wrong = [namelist, changes] + break + else: + for change_chapter in list(changes): + change_entries = changes[change_chapter] + # Check if namelist_changes are specified in correct syntax (e.g. in runscript) + # If correct syntax, change_entries is always a dict. + if not isinstance(change_entries, dict): + nml_syntax_error = True + this_is_wrong = ( + f"There is a syntax error in the following lines:\n\n[add_]namelist_changes:\n {namelist}\n {change_chapter}: {change_entries}\n...") + break + if nml_syntax_error: + user_error( + "Syntax error in namelist changes", + f"{error_message}\n{this_is_wrong}\n{example}", + dsymbols=["``", "'"], + ) + @staticmethod def nmls_remove(mconfig): """ @@ -122,81 +190,56 @@ def nmls_remove(mconfig): ------- mconfig : dict The modified configuration. + + Calls to other methods: + ---------------------- + nmls_check_changes """ namelist_changes = mconfig.get("namelist_changes", {}) + # Check if namelist_changes have correct syntax + Namelist.nmls_check_changes(namelist_changes) namelist_removes = [] - error_message = ( - "There is a syntax error, probably in your runscript, regarding namelist " - "changes (e.g. in a 'add_namelist_changes' block). " - "It seems that either 'namelist_file' or " - "'namelist_group' or 'both' are missing.\n" - "Please make sure that namelist changes are specified in the correct syntax (see example below)" - " and rerun your runscript.\n" - "Example for add_namelist_changes:\n" - "\t add_namelist_changes:\n" - "\t\t'':\n" - "\t\t\t'':\n" - "\t\t\t\t: " - ) for namelist in list(namelist_changes): changes = namelist_changes[namelist] - # Check if namelist_changes are specified in correct syntac (e.g. in runscript) - # If correct syntax, changes is always a dict. - if isinstance(changes, dict): - logging.debug("Determining remove entires for %s", namelist) - logging.debug("All changes: %s", changes) - for change_chapter in list(changes): - change_entries = changes[change_chapter] - # Check if namelist_changes are specified in correct syntac (e.g. in runscript) - # If correct syntax, change_entries is always a dict. - if isinstance(change_entries, dict): - for key in list(change_entries): - value = change_entries[key] - if value == "remove_from_namelist": - namelist_removes.append((namelist, change_chapter, key)) + logging.debug("Determining remove entires for %s", namelist) + logging.debug("All changes: %s", changes) + for change_chapter in list(changes): + change_entries = changes[change_chapter] + for key in list(change_entries): + value = change_entries[key] + if value == "remove_from_namelist": + namelist_removes.append((namelist, change_chapter, key)) - # the key is probably coming from esm_tools config - # files or from a user runscript. It can contain lower - # case, but the original Fortran namelist could be in - # any case combination. Here `original_key` is coming - # from the default namelist and may contain mixed case. - # `key` is the processed variable from f90nml module and - # is lowercase. - remove_original_key = False + # the key is probably coming from esm_tools config + # files or from a user runscript. It can contain lower + # case, but the original Fortran namelist could be in + # any case combination. Here `original_key` is coming + # from the default namelist and may contain mixed case. + # `key` is the processed variable from f90nml module and + # is lowercase. + remove_original_key = False - # traverse the namelist chapter and see if a mixed case - # variable is also found - for key2 in namelist_changes[namelist][change_chapter]: - # take care of the MiXeD FORTRAN CaSeS - if key2.lower() == key.lower() and key2 != key: - original_key = key2 - remove_original_key = True - namelist_removes.append( - (namelist, change_chapter, original_key) - ) + # traverse the namelist chapter and see if a mixed case + # variable is also found + for key2 in namelist_changes[namelist][change_chapter]: + # take care of the MiXeD FORTRAN CaSeS + if key2.lower() == key.lower() and key2 != key: + original_key = key2 + remove_original_key = True + namelist_removes.append( + (namelist, change_chapter, original_key) + ) - # remove both lowercase and mixed case variables - del namelist_changes[namelist][change_chapter][key] - if remove_original_key: - del namelist_changes[namelist][change_chapter][original_key] + # remove both lowercase and mixed case variables + del namelist_changes[namelist][change_chapter][key] + if remove_original_key: + del namelist_changes[namelist][change_chapter][original_key] - # mconfig instead of config, Grrrrr - print( - f"- NOTE: removing the variable: {key} from the namelist: {namelist}" - ) - else: - user_error( - "Syntax error in namelist changes", - error_message, - dsymbols=["``", "'"], + # mconfig instead of config, Grrrrr + print( + f"- NOTE: removing the variable: {key} from the namelist: {namelist}" ) - else: - user_error( - "Syntax error in namelist changes", - error_message, - dsymbols=["``", "'"], - ) for remove in namelist_removes: namelist, change_chapter, key = remove @@ -250,8 +293,15 @@ def nmls_modify(mconfig): ------- mconfig : dict The modified configuration. + + Calls to other methods: + ---------------------- + nmls_check_changes """ namelist_changes = mconfig.get("namelist_changes", {}) + # Check if namelist_changes have correct syntax + Namelist.nmls_check_changes(namelist_changes) + for namelist, changes in namelist_changes.items(): mconfig["namelists"][namelist].patch(changes) return mconfig From 03cd54ab1f046b2a5f719d82ddc8f4eb06d8f4cb Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Wed, 26 Jul 2023 16:28:24 +0200 Subject: [PATCH 037/107] Removed obsolet line. --- src/esm_runscripts/namelists.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index d450fedd4..6d43a8bc3 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -139,7 +139,6 @@ def nmls_check_changes(namelist_changes): if not isinstance(changes, dict): nml_syntax_error = True this_is_wrong = (f"There is a syntax error in the following lines:\n\n[add_]namelist_changes:\n {namelist}: {changes}\n...") -# this_is_wrong = [namelist, changes] break else: for change_chapter in list(changes): From dd223e259b3f3df9c96588d5c43803e6501857a6 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Wed, 26 Jul 2023 17:17:26 +0200 Subject: [PATCH 038/107] Added exception if runscript does not exists in requested folder. --- src/esm_runscripts/cli.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/esm_runscripts/cli.py b/src/esm_runscripts/cli.py index eaae9006a..4498c92f6 100644 --- a/src/esm_runscripts/cli.py +++ b/src/esm_runscripts/cli.py @@ -18,6 +18,7 @@ from .helpers import SmartSink from .sim_objects import * +from esm_parser import user_error def parse_shargs(): """The arg parser for interactive use""" @@ -247,6 +248,12 @@ def main(): runscript_full_path = os.path.realpath(ARGS.runscript) runscript_dir, runscript = os.path.split(runscript_full_path) runscript_dir += "/" + if not os.path.exists(runscript_full_path): + user_error( + "runscript not found", + f"The runscript ``{ARGS.runscript}`` does not exists in folder ``{runscript_dir}``. ", + dsymbols=["``", "'"], + ) # this might contain the relative path but it will be taken care of later command_line_config["original_command"] = original_command.strip() From ddd93a761eee31a06d09860494ef70f9c7aa172e Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 27 Jul 2023 07:07:25 +0200 Subject: [PATCH 039/107] fix problem introduced by 970 --- configs/components/echam/echam.yaml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/configs/components/echam/echam.yaml b/configs/components/echam/echam.yaml index f5b804ecf..fe1b02277 100644 --- a/configs/components/echam/echam.yaml +++ b/configs/components/echam/echam.yaml @@ -102,24 +102,13 @@ metadata: Authors: Bjorn Stevens (bjorn.stevens@mpimet.mpg.de) among others at MPI-Met Publications: 'Atmosphericcomponent of the MPI-M earth system model: ECHAM6 ' - License: > + License: Please make sure you have a license to use ECHAM. Otherwise downloading ECHAM will already fail. - To use the repository on any of these locations: - - * gitlab.dkrz.de/modular_esm/echam.git - * gitlab.awi.de/paleodyn/models/echam.git - - please register for the MPI-ESM user forum at: - - https://code.mpimet.mpg.de/projects/mpi-esm-license - + To use the repository on either gitlab.dkrz.de/modular_esm/echam.git or gitlab.awi.de/paleodyn/models/echam.git, + please register for the MPI-ESM user forum at https://code.mpimet.mpg.de/projects/mpi-esm-license and send a screenshot of yourself logged in to the forum to either paul.gierz@awi.de, miguel.andres-martinez@awi.de, - or nadine.wieters@awi.de. - - Note also that you can otherwise ignore the instructions on that page, just the registiration and login screen shot - is important for us. - - Have fun using ECHAM! :-) + or nadine.wieters@awi.de. Note also that you can otherwise ignore the instructions on that page, just the registiration + and login screen shot is the relevant part for obtaining the license. standalone_model: True From d3541fce0e31e2d00d5ef82a8598decf121d15cd Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 27 Jul 2023 07:41:34 +0200 Subject: [PATCH 040/107] fix another problem with the docs 1 --- src/esm_calendar/esm_calendar.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/esm_calendar/esm_calendar.py b/src/esm_calendar/esm_calendar.py index 84cbc4905..9011640d0 100644 --- a/src/esm_calendar/esm_calendar.py +++ b/src/esm_calendar/esm_calendar.py @@ -696,10 +696,10 @@ def format( """ Beautifully returns a ``Date`` object as a string. - Parameters: - ----------- + Parameters + ---------- form : str or int - Some cryptic that Dirk over-took from MPI-Met + Logic taken from from MPI-Met givenph : bool-ish Print hours givenpm : bool-ish @@ -707,8 +707,8 @@ def format( givenps : bool-ish Print seconds - Notes: - ------ + Note + ---- How to use the ``form`` argument: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following forms are accepted: From e27838ba7d5d8f31ca0bc3d61a05bf7fdb14e233 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 27 Jul 2023 07:46:39 +0200 Subject: [PATCH 041/107] fix another problem with the docs 2 --- src/esm_calendar/esm_calendar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/esm_calendar/esm_calendar.py b/src/esm_calendar/esm_calendar.py index 9011640d0..b0a38cdc5 100644 --- a/src/esm_calendar/esm_calendar.py +++ b/src/esm_calendar/esm_calendar.py @@ -709,8 +709,8 @@ def format( Note ---- - How to use the ``form`` argument: - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + How to use the ``form`` argument + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following forms are accepted: + SELF: uses the format which was given when constructing the date + 0: A Date formatted as YYYY From c2c9329985854c62961a5a7e649ee9723e38dfac Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 27 Jul 2023 07:52:43 +0200 Subject: [PATCH 042/107] fix another problem with the docs 3 --- src/esm_calendar/esm_calendar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/esm_calendar/esm_calendar.py b/src/esm_calendar/esm_calendar.py index b0a38cdc5..468dab13e 100644 --- a/src/esm_calendar/esm_calendar.py +++ b/src/esm_calendar/esm_calendar.py @@ -709,8 +709,7 @@ def format( Note ---- - How to use the ``form`` argument - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + **How to use the ``form`` argument** The following forms are accepted: + SELF: uses the format which was given when constructing the date + 0: A Date formatted as YYYY From e0b0190d83c36603a41169f9d42b158bc56972bc Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 27 Jul 2023 08:14:59 +0200 Subject: [PATCH 043/107] fix another problem with the docs 4 --- .readthedocs.yml | 2 +- configs/components/tux/tux.yaml | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 3aa8a9687..40800b3d9 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -8,7 +8,7 @@ version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py - fail_on_warning: true + fail_on_warning: false # Build documentation with MkDocs #mkdocs: diff --git a/configs/components/tux/tux.yaml b/configs/components/tux/tux.yaml index 4726507b9..46113cb22 100644 --- a/configs/components/tux/tux.yaml +++ b/configs/components/tux/tux.yaml @@ -24,11 +24,10 @@ clean_command: "rm -fr ${install_bins}" comp_command: "display ${install_bins}" metadata: - Institute: wiki - Description: - "Tux image" - Authors: "who knows" + Institute: "" + Description: "" + Authors: "" Publications: - - "are you serious?" + - " " License: GPL From 2063facc8e07c02c7db04e4894772d927b8ffa81 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 27 Jul 2023 09:08:04 +0200 Subject: [PATCH 044/107] add warning about conda --- docs/esm_runscripts.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index 48696a6d3..6a0fc97f2 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -370,6 +370,12 @@ runscript to ``True``: This shields the run from changes made to the remainder of the ESM-Tool installation, and it's strongly recommended for production runs. +.. warning:: + Refrain from using this feature if you have installed ESM-Tools within a conda + environment. Conda enviroment installation is still in its testing phase and we + cannot evaluate yet which conflicts might araise from combining both the venv of + this feature and the environment from conda. + If you choose to use a virtual environment, a local installation will be created in the experiment tree at the begining of the first run into the folder named ``.venv_esmtools``. **That** installation will be used for the experiment. It will be installed at the root of your experiment and contains all the Python libraries used by ESM-Tools. The installation at the beginning of the experiment will induce a small overhead (~2-3 minutes). For example, for a user ``miguel`` with a run with `expid` ``test`` ESM-Tools will be installed here:: From 4ccf5e0e8ae8e7d43caa8365f39a720e0e3bbc37 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 27 Jul 2023 09:11:37 +0200 Subject: [PATCH 045/107] add highlight of esm_runscripts --- docs/esm_runscripts.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index 6a0fc97f2..6a667434b 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -331,12 +331,12 @@ This will enable the `pdb Python debugger Date: Thu, 27 Jul 2023 09:13:50 +0200 Subject: [PATCH 046/107] correct grammar --- docs/esm_runscripts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/esm_runscripts.rst b/docs/esm_runscripts.rst index 6a667434b..8df405d70 100644 --- a/docs/esm_runscripts.rst +++ b/docs/esm_runscripts.rst @@ -373,7 +373,7 @@ and it's strongly recommended for production runs. .. warning:: Refrain from using this feature if you have installed ESM-Tools within a conda environment. Conda enviroment installation is still in its testing phase and we - cannot evaluate yet which conflicts might araise from combining both the venv of + cannot evaluate yet which conflicts might arise from combining both the venv of this feature and the environment from conda. If you choose to use a virtual environment, a local installation will be created in the experiment tree at the begining of the first run into the folder named ``.venv_esmtools``. **That** installation will be used for the experiment. It will be installed at the root of your experiment and contains all the Python libraries used by ESM-Tools. The installation at the beginning of the experiment will induce a small overhead (~2-3 minutes). From beeefe1547101ce287bcbd3e0ce99f67cb21e0f5 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Thu, 27 Jul 2023 09:57:05 +0200 Subject: [PATCH 047/107] Changed docstrings in namelists.py --- src/esm_runscripts/namelists.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 6d43a8bc3..7dca309c2 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -110,8 +110,6 @@ def nmls_check_changes(namelist_changes): ------- None - Calls to other methods: - ---------------------- """ error_message = ( @@ -190,7 +188,7 @@ def nmls_remove(mconfig): mconfig : dict The modified configuration. - Calls to other methods: + Calls to other methods ---------------------- nmls_check_changes """ From d9b84fe5225fe4465df5eede81a878ecc6cc1420 Mon Sep 17 00:00:00 2001 From: Miguel <63242832+mandresm@users.noreply.github.com> Date: Thu, 27 Jul 2023 09:58:39 +0200 Subject: [PATCH 048/107] Update configs/components/echam/echam.yaml Co-authored-by: Nadine Wieters <31928930+nwieters@users.noreply.github.com> --- configs/components/echam/echam.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/components/echam/echam.yaml b/configs/components/echam/echam.yaml index fe1b02277..704f4bed4 100644 --- a/configs/components/echam/echam.yaml +++ b/configs/components/echam/echam.yaml @@ -107,7 +107,7 @@ metadata: To use the repository on either gitlab.dkrz.de/modular_esm/echam.git or gitlab.awi.de/paleodyn/models/echam.git, please register for the MPI-ESM user forum at https://code.mpimet.mpg.de/projects/mpi-esm-license and send a screenshot of yourself logged in to the forum to either paul.gierz@awi.de, miguel.andres-martinez@awi.de, - or nadine.wieters@awi.de. Note also that you can otherwise ignore the instructions on that page, just the registiration + or nadine.wieters@awi.de. Note also that you can otherwise ignore the instructions on that page, just the registration and login screen shot is the relevant part for obtaining the license. standalone_model: True From 9e85b8e6e9efeae8e32dbb84f8b44f18467f559d Mon Sep 17 00:00:00 2001 From: Miguel <63242832+mandresm@users.noreply.github.com> Date: Thu, 27 Jul 2023 09:59:07 +0200 Subject: [PATCH 049/107] Update configs/components/echam/echam.yaml Co-authored-by: Nadine Wieters <31928930+nwieters@users.noreply.github.com> --- configs/components/echam/echam.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/components/echam/echam.yaml b/configs/components/echam/echam.yaml index 704f4bed4..c6f7042a3 100644 --- a/configs/components/echam/echam.yaml +++ b/configs/components/echam/echam.yaml @@ -104,7 +104,7 @@ metadata: 'Atmosphericcomponent of the MPI-M earth system model: ECHAM6 ' License: Please make sure you have a license to use ECHAM. Otherwise downloading ECHAM will already fail. - To use the repository on either gitlab.dkrz.de/modular_esm/echam.git or gitlab.awi.de/paleodyn/models/echam.git, + To use the repository on either gitlab.dkrz.de/modular_esm/echam6.git or gitlab.awi.de/paleodyn/models/echam6.git, please register for the MPI-ESM user forum at https://code.mpimet.mpg.de/projects/mpi-esm-license and send a screenshot of yourself logged in to the forum to either paul.gierz@awi.de, miguel.andres-martinez@awi.de, or nadine.wieters@awi.de. Note also that you can otherwise ignore the instructions on that page, just the registration From c0cb33b992d83f497d04ee86eb86ee189fe1f693 Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Thu, 27 Jul 2023 08:07:52 +0000 Subject: [PATCH 050/107] =?UTF-8?q?Bump=20version:=206.23.0=20=E2=86=92=20?= =?UTF-8?q?6.23.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index 5ff0ca9f6..da441e79f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.23.0 +current_version = 6.23.1 commit = True tag = True diff --git a/setup.py b/setup.py index dac15361b..d3a4cb432 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.23.0", + version="6.23.1", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index 6fac4660f..efd0b1ba6 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index 759bb6d5e..b4b1981eb 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index 4a4fe858e..4f8ba86bb 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index 014fec0e8..b1e257f9d 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index 74b5ec78f..21e596e49 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index 98681988f..991ed1fdd 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index 0b0d98a55..b460cb19d 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index 29a0e546d..34dd394da 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index 55572ad56..57c734660 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index 00932e6f7..3c4008d6b 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index 7ee0aab33..f8f52caf1 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index f4dcd81c3..941921033 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index 96561ba71..8c0de84bb 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index 284b6011f..b7b22f731 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.23.0" +__version__ = "6.23.1" from .utils import * From eda716718c664ec03cf6c31e80fac0fff734a886 Mon Sep 17 00:00:00 2001 From: Nadine Wieters <31928930+nwieters@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:16:22 +0200 Subject: [PATCH 051/107] Update src/esm_runscripts/namelists.py Co-authored-by: Miguel <63242832+mandresm@users.noreply.github.com> --- src/esm_runscripts/namelists.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 7dca309c2..5c9dcbc0a 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -207,7 +207,6 @@ def nmls_remove(mconfig): value = change_entries[key] if value == "remove_from_namelist": namelist_removes.append((namelist, change_chapter, key)) - # the key is probably coming from esm_tools config # files or from a user runscript. It can contain lower # case, but the original Fortran namelist could be in From 2a8dc7027b68ad9fc98a5e1b0563319641060ab9 Mon Sep 17 00:00:00 2001 From: Nadine Wieters <31928930+nwieters@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:20:43 +0200 Subject: [PATCH 052/107] Update src/esm_runscripts/namelists.py Co-authored-by: Miguel <63242832+mandresm@users.noreply.github.com> --- src/esm_runscripts/namelists.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 5c9dcbc0a..2b19fcdbe 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -215,7 +215,6 @@ def nmls_remove(mconfig): # `key` is the processed variable from f90nml module and # is lowercase. remove_original_key = False - # traverse the namelist chapter and see if a mixed case # variable is also found for key2 in namelist_changes[namelist][change_chapter]: From 338ceb7d06ec0a5af325f1517079bef8b9b95f90 Mon Sep 17 00:00:00 2001 From: Nadine Wieters <31928930+nwieters@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:20:57 +0200 Subject: [PATCH 053/107] Update src/esm_runscripts/namelists.py Co-authored-by: Miguel <63242832+mandresm@users.noreply.github.com> --- src/esm_runscripts/namelists.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 2b19fcdbe..2f330d30a 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -225,7 +225,6 @@ def nmls_remove(mconfig): namelist_removes.append( (namelist, change_chapter, original_key) ) - # remove both lowercase and mixed case variables del namelist_changes[namelist][change_chapter][key] if remove_original_key: From 8afa9b0223f75e4262712dc177b690fc54678d5c Mon Sep 17 00:00:00 2001 From: Nadine Wieters <31928930+nwieters@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:21:11 +0200 Subject: [PATCH 054/107] Update src/esm_runscripts/namelists.py Co-authored-by: Miguel <63242832+mandresm@users.noreply.github.com> --- src/esm_runscripts/namelists.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 2f330d30a..cab5ac417 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -229,7 +229,6 @@ def nmls_remove(mconfig): del namelist_changes[namelist][change_chapter][key] if remove_original_key: del namelist_changes[namelist][change_chapter][original_key] - # mconfig instead of config, Grrrrr print( f"- NOTE: removing the variable: {key} from the namelist: {namelist}" From 1b23f653feea09657a5b1a198e035bf8b023dd84 Mon Sep 17 00:00:00 2001 From: Nadine Wieters <31928930+nwieters@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:21:27 +0200 Subject: [PATCH 055/107] Update src/esm_runscripts/namelists.py Co-authored-by: Miguel <63242832+mandresm@users.noreply.github.com> --- src/esm_runscripts/namelists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index cab5ac417..031bbda5f 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -94,7 +94,7 @@ def nmls_load(mconfig): def nmls_check_changes(namelist_changes): """ Checks if namelist changes are given in correct syntax. - If not, a user_error will be raised and stopp the execution. + If not, a user_error will be raised and stop the execution. Programmer Information ---------------------- From 9109f0f8dad802b8ab9a01e325c4c9ee8c7942d7 Mon Sep 17 00:00:00 2001 From: Nadine Wieters <31928930+nwieters@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:21:59 +0200 Subject: [PATCH 056/107] Update src/esm_runscripts/namelists.py Co-authored-by: Miguel <63242832+mandresm@users.noreply.github.com> --- src/esm_runscripts/namelists.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 031bbda5f..89b6507e3 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -103,7 +103,8 @@ def nmls_check_changes(namelist_changes): ---------- namelist_changes : nested dict - If the syntax is correct, namelist_changes should be a nested dict of the following form: + If the syntax is correct, namelist_changes should be a nested dict of the following form:: + {'namelist1.nml': {'namelist1': {'variable1': 'value1', 'variable2': 'value2', 'variable3': 'value3'}, 'namelist2': {'variable1': value1}}} Returns From 25c83706611062a118b469ba6196e8f31ce849d1 Mon Sep 17 00:00:00 2001 From: Nadine Wieters <31928930+nwieters@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:22:16 +0200 Subject: [PATCH 057/107] Update src/esm_runscripts/namelists.py Co-authored-by: Miguel <63242832+mandresm@users.noreply.github.com> --- src/esm_runscripts/namelists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 89b6507e3..5119ffe8c 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -122,7 +122,7 @@ def nmls_check_changes(namelist_changes): " and rerun your runscript.\n" ) example = ( - "\nExample for [add_]namelist_changes:\n" + "\nExample of a ``correct syntax`` for [add_]namelist_changes:\n" "\t [add_]namelist_changes:\n" "\t '':\n" "\t '':\n" From 7b3e43b9e975281ee28b53e8fdb7a986e500f19f Mon Sep 17 00:00:00 2001 From: Nadine Wieters <31928930+nwieters@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:22:40 +0200 Subject: [PATCH 058/107] Update src/esm_runscripts/namelists.py Co-authored-by: Miguel <63242832+mandresm@users.noreply.github.com> --- src/esm_runscripts/namelists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 5119ffe8c..4cac9a002 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -137,7 +137,7 @@ def nmls_check_changes(namelist_changes): # If correct syntax, changes is always a dict. if not isinstance(changes, dict): nml_syntax_error = True - this_is_wrong = (f"There is a syntax error in the following lines:\n\n[add_]namelist_changes:\n {namelist}: {changes}\n...") + this_is_wrong = (f"There is a syntax error in the following lines:\n\n[add_]namelist_changes:\n '{namelist}: {changes}'\n...") break else: for change_chapter in list(changes): From 037af3329e3d642951f99041fe88dce6f17a007f Mon Sep 17 00:00:00 2001 From: Nadine Wieters <31928930+nwieters@users.noreply.github.com> Date: Thu, 27 Jul 2023 15:22:52 +0200 Subject: [PATCH 059/107] Update src/esm_runscripts/namelists.py Co-authored-by: Miguel <63242832+mandresm@users.noreply.github.com> --- src/esm_runscripts/namelists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 4cac9a002..8dcd9a1f9 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -147,7 +147,7 @@ def nmls_check_changes(namelist_changes): if not isinstance(change_entries, dict): nml_syntax_error = True this_is_wrong = ( - f"There is a syntax error in the following lines:\n\n[add_]namelist_changes:\n {namelist}\n {change_chapter}: {change_entries}\n...") + f"There is a syntax error in the following lines:\n\n[add_]namelist_changes:\n '{namelist}':\n '{change_chapter}: {change_entries}'\n...") break if nml_syntax_error: user_error( From 4403a5620bfe08d17279a4004a738e00c4c75341 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Thu, 27 Jul 2023 15:26:44 +0200 Subject: [PATCH 060/107] Removed unnecessary spaces. --- src/esm_runscripts/namelists.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/esm_runscripts/namelists.py b/src/esm_runscripts/namelists.py index 8dcd9a1f9..6089fa573 100644 --- a/src/esm_runscripts/namelists.py +++ b/src/esm_runscripts/namelists.py @@ -95,22 +95,22 @@ def nmls_check_changes(namelist_changes): """ Checks if namelist changes are given in correct syntax. If not, a user_error will be raised and stop the execution. - + Programmer Information ---------------------- - + Parameters ---------- namelist_changes : nested dict - + If the syntax is correct, namelist_changes should be a nested dict of the following form:: {'namelist1.nml': {'namelist1': {'variable1': 'value1', 'variable2': 'value2', 'variable3': 'value3'}, 'namelist2': {'variable1': value1}}} - + Returns ------- None - + """ error_message = ( From 865e49e001c22e0752004af46160c3102c316af7 Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Thu, 27 Jul 2023 14:30:25 +0000 Subject: [PATCH 061/107] =?UTF-8?q?Bump=20version:=206.23.1=20=E2=86=92=20?= =?UTF-8?q?6.23.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index da441e79f..8cde6fd80 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.23.1 +current_version = 6.23.2 commit = True tag = True diff --git a/setup.py b/setup.py index d3a4cb432..45461f44e 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.23.1", + version="6.23.2", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index efd0b1ba6..f7fd8fdc8 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index b4b1981eb..305f96e4f 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index 4f8ba86bb..d291c3be2 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index b1e257f9d..0e9c37acc 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index 21e596e49..9043f5257 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index 991ed1fdd..7a7823cf3 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index b460cb19d..5a7f5fddd 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index 34dd394da..4945d1bdb 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index 57c734660..d4c2a6beb 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index 3c4008d6b..c33b3e331 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index f8f52caf1..4170b21fe 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index 941921033..684dc571c 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index 8c0de84bb..a90c413f0 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index b7b22f731..342bdfebf 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.23.1" +__version__ = "6.23.2" from .utils import * From 677235d3a63907a236c2686f13b283bdfe8ba7a2 Mon Sep 17 00:00:00 2001 From: mandresm Date: Mon, 31 Jul 2023 19:13:19 +0200 Subject: [PATCH 062/107] fix issue #1009 --- src/esm_runscripts/filelists.py | 6 +++--- src/esm_runscripts/tidy.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/esm_runscripts/filelists.py b/src/esm_runscripts/filelists.py index 2eb14b84a..fa27f8520 100644 --- a/src/esm_runscripts/filelists.py +++ b/src/esm_runscripts/filelists.py @@ -892,7 +892,7 @@ def check_for_unknown_files(config): return config -def resolve_symlinks(file_source): +def resolve_symlinks(config, file_source): if os.path.islink(file_source): points_to = os.path.realpath(file_source) @@ -905,7 +905,7 @@ def resolve_symlinks(file_source): return file_source # recursively find the file that the link is pointing to - return resolve_symlinks(points_to) + return resolve_symlinks(config, points_to) else: return file_source @@ -956,7 +956,7 @@ def copy_files(config, filetypes, source, target): helpers.print_datetime(config) continue dest_dir = os.path.dirname(file_target) - file_source = resolve_symlinks(file_source) + file_source = resolve_symlinks(config, file_source) if not os.path.isdir(file_source): try: if not os.path.isdir(dest_dir): diff --git a/src/esm_runscripts/tidy.py b/src/esm_runscripts/tidy.py index aeeb5109a..ee2d8857b 100644 --- a/src/esm_runscripts/tidy.py +++ b/src/esm_runscripts/tidy.py @@ -362,12 +362,12 @@ def copy_all_results_to_exp(config): + destination ) else: - linkdest = resolve_symlinks(source) + linkdest = resolve_symlinks(config, source) # newlinkdest = ( # destination.rsplit("/", 1)[0] + "/" + linkdest.rsplit("/", 1)[-1] # ) if os.path.islink(destination): - destdest = resolve_symlinks(source) + destdest = resolve_symlinks(config, source) if linkdest == destdest: # both links are identical, skip continue From bc2580315d08b55ae370d072e493673f38d3ee31 Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Tue, 1 Aug 2023 17:40:24 +0000 Subject: [PATCH 063/107] =?UTF-8?q?Bump=20version:=206.23.2=20=E2=86=92=20?= =?UTF-8?q?6.23.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8cde6fd80..8ace66676 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.23.2 +current_version = 6.23.3 commit = True tag = True diff --git a/setup.py b/setup.py index 45461f44e..ece6ab28c 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.23.2", + version="6.23.3", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index f7fd8fdc8..f804ac8f3 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index 305f96e4f..17437ac49 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index d291c3be2..407a6ca52 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index 0e9c37acc..8ca28b7d2 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index 9043f5257..f4c7b59bb 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index 7a7823cf3..b2e3adcc5 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index 5a7f5fddd..42efc910c 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index 4945d1bdb..8419a5453 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index d4c2a6beb..8bbfd1c0c 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index c33b3e331..0939a947d 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index 4170b21fe..ed1672d62 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index 684dc571c..435c34946 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index a90c413f0..090568783 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index 342bdfebf..d749ebbe7 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.23.2" +__version__ = "6.23.3" from .utils import * From 2fbd302446620907bcf0aa24ebbfd1dab2068bab Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Fri, 4 Aug 2023 18:12:57 +0200 Subject: [PATCH 064/107] add correct forcing files for greenhouse hist and MAC-SP --- configs/components/echam/echam.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configs/components/echam/echam.yaml b/configs/components/echam/echam.yaml index c6f7042a3..a3b0a5626 100644 --- a/configs/components/echam/echam.yaml +++ b/configs/components/echam/echam.yaml @@ -394,11 +394,11 @@ choose_scenario: aerofin_1850: piaerofin aerofin_1851: piaerofin ozone: ozone_hist_scenario - greenhouse: histgreenh + greenhouse: greenhouse_hist_scenario volcir: histvolcir volcsw: histvolcsw swflux: histswflux - MAC-SP: MAC-SP + MAC-SP: MAC-SP_hist_scenario cmip6hist: scenario_type: cmip6 From 6ce1f959e854c31f5abf1cc05b3a2d090b96cfd6 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Mon, 7 Aug 2023 17:46:07 +0200 Subject: [PATCH 065/107] Fix in set_provenance. --- src/esm_parser/provenance.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index b885a80ce..11314fe72 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -235,12 +235,11 @@ def set_provenance(self, provenance): given, the ``dictionary`` takes the value of ``self``. Only for recursion within nested ``DictWithProvenance``, do not use it outside of this method. """ - for key, val in self.items(): if isinstance(val, dict): - self[key] = DictWithProvenance(val, provenance) - if isinstance(val, list): - self[key] = ListWithProvenance(val, provenance) + DictWithProvenance.set_provenance(val, provenance) + elif isinstance(val, list): + ListWithProvenance.set_provenance(val, provenance) else: self[key] = wrapper_with_provenance_factory(val, provenance) @@ -333,12 +332,11 @@ def set_provenance(self, provenance): given, the ``dictionary`` takes the value of ``self``. Only for recursion within nested ``DictWithProvenance``, do not use it outside of this method. """ - - for c, elem in enummerate(self): + for c, elem in enumerate(self): if isinstance(elem, dict): - self[c] = DictWithProvenance(elem, provenance) + DictWithProvenancei.set_provenance(elem, provenance) if isinstance(elem, list): - self[c] = ListWithProvenance(elem, provenance) + ListWithProvenance.set_provenance(elem, provenance) else: self[c] = wrapper_with_provenance_factory(elem, provenance) From 2f1aec7caf9d63568a9a13278fb83b2fb5ef87a8 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Mon, 7 Aug 2023 17:47:41 +0200 Subject: [PATCH 066/107] Added unit tests for provenance. --- tests/test_esm_parser/example.yaml | 2 +- tests/test_esm_parser/example2.yaml | 6 + tests/test_esm_parser/test_provenance.py | 455 ++++++++++++----------- 3 files changed, 249 insertions(+), 214 deletions(-) create mode 100644 tests/test_esm_parser/example2.yaml diff --git a/tests/test_esm_parser/example.yaml b/tests/test_esm_parser/example.yaml index c5cc69584..9b96bc7cf 100644 --- a/tests/test_esm_parser/example.yaml +++ b/tests/test_esm_parser/example.yaml @@ -1,6 +1,6 @@ person: name: Paul Gierz - username: !ENV USER + username: !ENV ${USER} a_string: " hello world I am here to make your life impossible diff --git a/tests/test_esm_parser/example2.yaml b/tests/test_esm_parser/example2.yaml new file mode 100644 index 000000000..0609e5d63 --- /dev/null +++ b/tests/test_esm_parser/example2.yaml @@ -0,0 +1,6 @@ +echam: + type: "atmosphere" + files: + greenhouse: + kind: "input" + path_in_computer: "/my/path/in/computer" diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 925048740..acbc97b1a 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -2,143 +2,126 @@ Unit tests for the new provenance feature """ import os -import esm_parser.provenance as provenance import pathlib +import esm_parser.provenance as provenance import esm_parser from esm_parser import yaml_to_dict -config_dict = { - "echam": { - "type": "atmosphere", - "files": { - "greenhouse": {"kind": "input", "path_in_computer": "/my/path/in/computer"} - }, - } -} +#config_dict = { +# "echam": { +# "type": "atmosphere", +# "files": { +# "greenhouse": {"kind": "input", "path_in_computer": "/my/path/in/computer"} +# }, +# } +#} +# +#my_provenance = { +# "echam": { +# "type": {"line": 2, "col": 11, "yaml_file": "myrunscript.yaml", "category": "runscript"}, +# }, +#} +#config = provenance.DictWithProvenance(config_dict, my_provenance) + +config = yaml_to_dict.yaml_file_to_dict('example2.yaml') -my_provenance = { - "from_file": None, - "type": None, +check_provenance = {'echam': + {'type': + {'line': 2, 'col': 11, 'yaml_file': 'example2.yaml', 'category': 'runscript'}, + 'files': { + 'greenhouse': { + 'kind': {'line': 5, 'col': 19, 'yaml_file': 'example2.yaml', 'category': 'runscript'}, + 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': 'example2.yaml', 'category': 'runscript'} + } + } + }, + 'debug_info': {'loaded_from_file': None} } -config = provenance.DictWithProvenance(config_dict, my_provenance) +# Test 1: Checks for a correct provenance entries from example yaml file. +def test_get_provenance_from_yaml_to_dict(): + assert config.get_provenance() == check_provenance + + +# Test 2: Checks whether the provenance of an added config entry is None. +def test_get_provenance_of_added_entry(): + config["fesom"] = True + check_provenance["fesom"] = None + assert config.get_provenance() == check_provenance -# Test 1 (should give you a provenance of None for the key ["fesom"]["asd"]) -def test_get_provenance_1(): +# Test 3: Checks whether the provenance of an added nested config entry is None. +def test_get_provenance_of_added_nested_entry(): config["fesom"] = {"asd": 0} - check_provenance = { - "echam": { - "type": {"from_file": None, "type": None}, - "files": { - "greenhouse": { - "kind": {"from_file": None, "type": None}, - "path_in_computer": {"from_file": None, "type": None}, - } - }, - }, - "fesom": {"asd": None}, - } + check_provenance["fesom"] = None assert config.get_provenance() == check_provenance -# Test 2 (should give you a provenance of None for the key "computer") -def test_get_provenance_2(): - config["computer"] = 0 - check_provenance = { - "echam": { - "type": {"from_file": None, "type": None}, - "files": { - "greenhouse": { - "kind": {"from_file": None, "type": None}, - "path_in_computer": {"from_file": None, "type": None}, - } - }, - }, - "fesom": {"asd": None}, - "computer": None, - } +# Test 4: Checks whether the provenance of an added nested config entry is None. +def test_get_provenance_of_added_nested_entry_2(): + config["echam"]["test1"] = 17. + check_provenance["echam"]["test1"] = None assert config.get_provenance() == check_provenance -# Test 3 (should give you a provenance of 2 for the leaf keys inside "fesom") -def test_get_provenance_3(): - config_fesom = provenance.DictWithProvenance({"fesom": {"asd": 0, "model": "ocean"}}, 2) - config.update(config_fesom) - check_provenance = { - "echam": { - "type": {"from_file": None, "type": None}, - "files": { - "greenhouse": { - "kind": {"from_file": None, "type": None}, - "path_in_computer": {"from_file": None, "type": None}, - } - }, - }, - "fesom": {"asd": 2, "model": 2}, - "computer": None, - } +# Test 5: Reset the provenance of an ``echam`` leave. +def test_set_provenance_for_leaf(): + new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'userdefined'} + config["echam"].set_provenance(new_prov) + check_provenance["echam"]["type"] = new_prov + check_provenance["echam"]["files"]["greenhouse"]["kind"] = new_prov + check_provenance["echam"]["files"]["greenhouse"]["path_in_computer"] = new_prov + check_provenance["echam"]["test1"] = new_prov assert config.get_provenance() == check_provenance -# Test 4 (should give you a provenance of None for the key True) -def test_get_provenance_4(): - config[True] = "boolean" - check_provenance = { - "echam": { - "type": {"from_file": None, "type": None}, - "files": { - "greenhouse": { - "kind": {"from_file": None, "type": None}, - "path_in_computer": {"from_file": None, "type": None}, - } - }, - }, - "fesom": {"asd": 2, "model": 2}, - "computer": None, - True: None, - } +# Test 6: Reset the provenance of leaves for an later added branch ``debug_info``. +def test_set_provenance_for_leaf_of_new_branch(): + new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'debuginfo'} + config["debug_info"].set_provenance(new_prov) + check_provenance["debug_info"]['loaded_from_file'] = new_prov + print(config.get_provenance()) assert config.get_provenance() == check_provenance -# Test 5 (reset the provenance of all ``echam`` leaves to "a_string") -def test_get_provenance_5(): - config["echam"].set_provenance("a_string") - check_provenance = { - "echam": { - "type": "a_string", - "files": {"greenhouse": {"kind": "a_string", "path_in_computer": "a_string"}}, - }, - "fesom": {"asd": 2, "model": 2}, - "computer": None, - True: None, - } +# Test 7: Reset the provenance of all ``echam`` leaves to "a_string") +def test_set_provenance_for_leaf_to_a_string(): + new_prov = "a_string" + config["echam"].set_provenance(new_prov) + check_provenance["echam"]["type"] = new_prov + check_provenance["echam"]["files"]["greenhouse"]["kind"] = new_prov + check_provenance["echam"]["files"]["greenhouse"]["path_in_computer"] = new_prov + check_provenance["echam"]["test1"] = new_prov assert config.get_provenance() == check_provenance -# Test 6 (reset the provenance of a leaf) -def test_get_provenance_6(): - config["echam"]["files"]["greenhouse"].provenance["kind"] = "a_new_string" - check_provenance = { - "echam": { - "type": "a_string", - "files": { - "greenhouse": {"kind": "a_new_string", "path_in_computer": "a_string"} - }, - }, - "fesom": {"asd": 2, "model": 2}, - "computer": None, - True: None, - } +# Test 8: Rest the provenanve of fesom entry. +def test_set_provenance_for_a_new_leaf(): + new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'set_for_onknown_leaf'} + config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) + config["fesom"].set_provenance(new_prov) + check_provenance["fesom"] = {"asd": None} + check_provenance["fesom"]["asd"] = new_prov + assert config.get_provenance() == check_provenance + +# Test 9: Reset the provenance of a list") +def test_set_provenance_for_a_list_leaf(): + new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} + config["fesom"] = {"list": [30, 19]} + config["fesom"]["list"] = provenance.ListWithProvenance(config["fesom"]["list"], [None, None]) + config["fesom"]["list"].set_provenance(new_prov) + print("config: ",config) + print("provenance: ",config.get_provenance()) assert config.get_provenance() == check_provenance -def test_extract_dict(): +# Test 10: Test the extraction of config for all allowed variable types. +def test_extract_dict_config(): esm_tools_loader = yaml_to_dict.EsmToolsLoader() file_path = pathlib.Path("example.yaml") config = { 'person': { 'name': 'Paul Gierz', - 'username': 'nwieters', + 'username': os.environ['USER'], 'a_string': ' hello world I am here to make your life impossible ', 'my_var': 'MY_VAR', 'my_other_var': ['a', 'b', 'c'], @@ -164,122 +147,168 @@ def test_extract_dict(): assert data == config -def test_extract_provenance(): +# Test 11: Check provenance of a list entry +def test_check_provenance_list(): esm_tools_loader = yaml_to_dict.EsmToolsLoader() file_path = pathlib.Path("example.yaml") - file_path = os.path.abspath(file_path) - provenance = { - 'person': { - 'name': { - 'line': 1, - 'col': 8, - 'yaml_file': file_path, - 'category': 'None'}, - 'username': { - 'line': 2, - 'col': 12, - 'yaml_file': file_path, - 'category': 'None'}, - 'a_string': { - 'line': 3, - 'col': 12, - 'yaml_file': file_path, - 'category': 'None'}, - 'my_var': { - 'line': 8, - 'col': 10, - 'yaml_file': file_path, - 'category': 'None'}, - 'my_other_var': [{ - 'line': 10, - 'col': 8, - 'yaml_file': file_path, - 'category': 'None'}, { - 'line': 12, - 'col': 8, - 'yaml_file': file_path, - 'category': 'None'}, { - 'line': 13, - 'col': 8, - 'yaml_file': file_path, - 'category': 'None'}], - 'my_other_list': [{ - 'line': 14, - 'col': 18, - 'yaml_file': file_path, - 'category': 'None'}, { - 'line': 14, - 'col': 21, - 'yaml_file': file_path, - 'category': 'None'}, { - 'line': 14, - 'col': 24, - 'yaml_file': file_path, - 'category': 'None'}], - 'my_bolean': { - 'line': 16, - 'col': 13, - 'yaml_file': file_path, - 'category': 'None'}, - 'my_int': { - 'line': 17, - 'col': 10, - 'yaml_file': file_path, - 'category': 'None'}, - 'my_int2': { - 'line': 18, - 'col': 11, - 'yaml_file': file_path, - 'category': 'None'}, - 'list_with_dict_inside': [{ - 'line': 20, - 'col': 4, - 'yaml_file': file_path, - 'category': 'None'}, { - 'line': 21, - 'col': 4, - 'yaml_file': file_path, - 'category': 'None'}, { - 'my_dict': { - 'foo': [{ - 'line': 24, - 'col': 10, - 'yaml_file': file_path, - 'category': 'None'}, { - 'line': 25, - 'col': 10, - 'yaml_file': file_path, - 'category': 'None'}, { - 'my_dict': { - 'foo': { - 'line': 27, - 'col': 17, - 'yaml_file': file_path, - 'category': 'None'} - }}]}}]}} + check_prov = [{'line': 15, 'col': 19, 'yaml_file': 'example.yaml', 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': 'example.yaml', 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'runscript'}] with open(file_path, "r") as file: esm_tools_loader.set_filename(file_path) data, data2 = esm_tools_loader.load(file) - assert data2 == provenance + config = provenance.DictWithProvenance(data, data2) + assert config["person"]["my_other_list"].get_provenance() == check_prov -# Test 7 (reset the provenance of a leaf) -def test_get_provenance_7(): - config_fesom = provenance.DictWithProvenance({"fesom": {"update_test": True}}, "new_provenance") +# Test 12: Check set_provenance of a list entry +def test_check_set_provenance_list(): + esm_tools_loader = yaml_to_dict.EsmToolsLoader() + file_path = pathlib.Path("example.yaml") + new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_list'} + check_prov = [new_prov, new_prov, new_prov] - config["fesom"].update(config_fesom["fesom"]) + with open(file_path, "r") as file: + esm_tools_loader.set_filename(file_path) + data, data2 = esm_tools_loader.load(file) - check_provenance = { - "echam": { - "type": "a_string", - "files": { - "greenhouse": {"kind": "a_new_string", "path_in_computer": "a_string"} - }, - }, - "fesom": {"asd": 2, "model": 2, "update_test": "new_provenance"}, - "computer": None, - True: None, - } - assert config.get_provenance() == check_provenance + config = provenance.DictWithProvenance(data, data2) + config["person"]["my_other_list"].set_provenance(new_prov) + assert config["person"]["my_other_list"].get_provenance() == check_prov + + +# Test 13: Check set_provenance of a single list entry +def test_check_set_provenance_of_single_list_entry(): + esm_tools_loader = yaml_to_dict.EsmToolsLoader() + file_path = pathlib.Path("example.yaml") + old_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'runscript'} + new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_second_list'} + check_prov = [old_prov, old_prov, new_prov] + + with open(file_path, "r") as file: + esm_tools_loader.set_filename(file_path) + data, data2 = esm_tools_loader.load(file) + + config = provenance.DictWithProvenance(data, data2) + config["person"]["my_other_list"][2].set_provenance(new_prov) + assert config["person"]["my_other_list"].get_provenance() == check_prov +# +#def test_extract_provenance(): +# esm_tools_loader = yaml_to_dict.EsmToolsLoader() +# file_path = pathlib.Path("example.yaml") +# file_path = os.path.abspath(file_path) +# provenance = { +# 'person': { +# 'name': { +# 'line': 1, +# 'col': 8, +# 'yaml_file': file_path, +# 'category': 'None'}, +# 'username': { +# 'line': 2, +# 'col': 12, +# 'yaml_file': file_path, +# 'category': 'None'}, +# 'a_string': { +# 'line': 3, +# 'col': 12, +# 'yaml_file': file_path, +# 'category': 'None'}, +# 'my_var': { +# 'line': 8, +# 'col': 10, +# 'yaml_file': file_path, +# 'category': 'None'}, +# 'my_other_var': [{ +# 'line': 10, +# 'col': 8, +# 'yaml_file': file_path, +# 'category': 'None'}, { +# 'line': 12, +# 'col': 8, +# 'yaml_file': file_path, +# 'category': 'None'}, { +# 'line': 13, +# 'col': 8, +# 'yaml_file': file_path, +# 'category': 'None'}], +# 'my_other_list': [{ +# 'line': 14, +# 'col': 18, +# 'yaml_file': file_path, +# 'category': 'None'}, { +# 'line': 14, +# 'col': 21, +# 'yaml_file': file_path, +# 'category': 'None'}, { +# 'line': 14, +# 'col': 24, +# 'yaml_file': file_path, +# 'category': 'None'}], +# 'my_bolean': { +# 'line': 16, +# 'col': 13, +# 'yaml_file': file_path, +# 'category': 'None'}, +# 'my_int': { +# 'line': 17, +# 'col': 10, +# 'yaml_file': file_path, +# 'category': 'None'}, +# 'my_int2': { +# 'line': 18, +# 'col': 11, +# 'yaml_file': file_path, +# 'category': 'None'}, +# 'list_with_dict_inside': [{ +# 'line': 20, +# 'col': 4, +# 'yaml_file': file_path, +# 'category': 'None'}, { +# 'line': 21, +# 'col': 4, +# 'yaml_file': file_path, +# 'category': 'None'}, { +# 'my_dict': { +# 'foo': [{ +# 'line': 24, +# 'col': 10, +# 'yaml_file': file_path, +# 'category': 'None'}, { +# 'line': 25, +# 'col': 10, +# 'yaml_file': file_path, +# 'category': 'None'}, { +# 'my_dict': { +# 'foo': { +# 'line': 27, +# 'col': 17, +# 'yaml_file': file_path, +# 'category': 'None'} +# }}]}}]}} +# +# with open(file_path, "r") as file: +# esm_tools_loader.set_filename(file_path) +# data, data2 = esm_tools_loader.load(file) +# +# assert data2 == provenance +# +# +## Test 7 (reset the provenance of a leaf) +#def test_get_provenance_7(): +# config_fesom = provenance.DictWithProvenance({"fesom": {"update_test": True}}, "new_provenance") +# +# config["fesom"].update(config_fesom["fesom"]) +# +# check_provenance = { +# "echam": { +# "type": "a_string", +# "files": { +# "greenhouse": {"kind": "a_new_string", "path_in_computer": "a_string"} +# }, +# }, +# "fesom": {"asd": 2, "model": 2, "update_test": "new_provenance"}, +# "computer": None, +# True: None, +# } +# assert config.get_provenance() == check_provenance From b0bc75e62dc2b6ee9bf5811c45cc0cb185c8d68f Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Mon, 7 Aug 2023 17:59:32 +0200 Subject: [PATCH 067/107] Small bugfix in set_proevance. --- src/esm_parser/provenance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 11314fe72..38694b079 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -335,7 +335,7 @@ def set_provenance(self, provenance): for c, elem in enumerate(self): if isinstance(elem, dict): DictWithProvenancei.set_provenance(elem, provenance) - if isinstance(elem, list): + elif isinstance(elem, list): ListWithProvenance.set_provenance(elem, provenance) else: self[c] = wrapper_with_provenance_factory(elem, provenance) From ca8efd78e900143e277b948551ec6cb8f055cef0 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Tue, 8 Aug 2023 13:33:53 +0200 Subject: [PATCH 068/107] Added requirement for ruamel.yaml to setup.py. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 3033acfbb..d6fe73790 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ "pyyaml", "pyyaml>=5.1", "questionary", + "ruamel.yaml==0.17.32", "semver", "sqlalchemy", "tabulate", From 248d222a44370d3cb424390d9730126604248dc1 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Tue, 8 Aug 2023 13:34:29 +0200 Subject: [PATCH 069/107] Fixed one unit test for provenance. --- tests/test_esm_parser/test_provenance.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index acbc97b1a..c725c7e07 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -106,11 +106,12 @@ def test_set_provenance_for_a_new_leaf(): # Test 9: Reset the provenance of a list") def test_set_provenance_for_a_list_leaf(): new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} + config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) config["fesom"] = {"list": [30, 19]} config["fesom"]["list"] = provenance.ListWithProvenance(config["fesom"]["list"], [None, None]) config["fesom"]["list"].set_provenance(new_prov) - print("config: ",config) - print("provenance: ",config.get_provenance()) + #print("provenance: ",config["fesom"]["list"].get_provenance()) + #print("provenance2: ",config.get_provenance()) assert config.get_provenance() == check_provenance @@ -181,16 +182,17 @@ def test_check_set_provenance_list(): def test_check_set_provenance_of_single_list_entry(): esm_tools_loader = yaml_to_dict.EsmToolsLoader() file_path = pathlib.Path("example.yaml") - old_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'runscript'} + old_prov1 = {'line': 15, 'col': 19, 'yaml_file': 'example.yaml', 'category': 'runscript'} + old_prov2 = {'line': 15, 'col': 22, 'yaml_file': 'example.yaml', 'category': 'runscript'} new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_second_list'} - check_prov = [old_prov, old_prov, new_prov] + check_prov = [old_prov1, old_prov2, new_prov] with open(file_path, "r") as file: esm_tools_loader.set_filename(file_path) data, data2 = esm_tools_loader.load(file) config = provenance.DictWithProvenance(data, data2) - config["person"]["my_other_list"][2].set_provenance(new_prov) + config["person"]["my_other_list"][2].provenance = new_prov assert config["person"]["my_other_list"].get_provenance() == check_prov # #def test_extract_provenance(): From c72511ebdc9fe94207948bd63b4dfa280114e495 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Tue, 8 Aug 2023 14:22:13 +0200 Subject: [PATCH 070/107] Added github wirkspace env variable to test_provenance. --- tests/test_esm_parser/test_provenance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index c725c7e07..f10308273 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -23,7 +23,7 @@ #} #config = provenance.DictWithProvenance(config_dict, my_provenance) -config = yaml_to_dict.yaml_file_to_dict('example2.yaml') +config = yaml_to_dict.yaml_file_to_dict(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml") check_provenance = {'echam': {'type': From e0aef1d351418986f3521d4fcbfdc99800a02f1d Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Tue, 8 Aug 2023 14:52:56 +0200 Subject: [PATCH 071/107] Added GITHUB_WORKSPACE env var to provenance unit test. --- tests/test_esm_parser/test_provenance.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index f10308273..d8d458618 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -23,15 +23,15 @@ #} #config = provenance.DictWithProvenance(config_dict, my_provenance) -config = yaml_to_dict.yaml_file_to_dict(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml") +config = yaml_to_dict.yaml_file_to_dict(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve())) check_provenance = {'echam': {'type': - {'line': 2, 'col': 11, 'yaml_file': 'example2.yaml', 'category': 'runscript'}, + {'line': 2, 'col': 11, 'yaml_file': str(pathlib.Path("example2.yaml").resolve()), 'category': 'runscript'}, 'files': { 'greenhouse': { - 'kind': {'line': 5, 'col': 19, 'yaml_file': 'example2.yaml', 'category': 'runscript'}, - 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': 'example2.yaml', 'category': 'runscript'} + 'kind': {'line': 5, 'col': 19, 'yaml_file': str(pathlib.Path("example2.yaml").resolve()), 'category': 'runscript'}, + 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': str(pathlib.Path("example2.yaml").resolve()), 'category': 'runscript'} } } }, From c5a7576eca25c62e3af8fcfe7a90855c875ecd03 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Tue, 8 Aug 2023 15:02:21 +0200 Subject: [PATCH 072/107] Added abs path to provenance unit tests. --- tests/test_esm_parser/test_provenance.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index d8d458618..42ec13bdf 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -27,11 +27,11 @@ check_provenance = {'echam': {'type': - {'line': 2, 'col': 11, 'yaml_file': str(pathlib.Path("example2.yaml").resolve()), 'category': 'runscript'}, + {'line': 2, 'col': 11, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()), 'category': 'runscript'}, 'files': { 'greenhouse': { - 'kind': {'line': 5, 'col': 19, 'yaml_file': str(pathlib.Path("example2.yaml").resolve()), 'category': 'runscript'}, - 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': str(pathlib.Path("example2.yaml").resolve()), 'category': 'runscript'} + 'kind': {'line': 5, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()).resolve()), 'category': 'runscript'}, + 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()), 'category': 'runscript'} } } }, @@ -118,7 +118,7 @@ def test_set_provenance_for_a_list_leaf(): # Test 10: Test the extraction of config for all allowed variable types. def test_extract_dict_config(): esm_tools_loader = yaml_to_dict.EsmToolsLoader() - file_path = pathlib.Path("example.yaml") + file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) config = { 'person': { 'name': 'Paul Gierz', @@ -151,8 +151,8 @@ def test_extract_dict_config(): # Test 11: Check provenance of a list entry def test_check_provenance_list(): esm_tools_loader = yaml_to_dict.EsmToolsLoader() - file_path = pathlib.Path("example.yaml") - check_prov = [{'line': 15, 'col': 19, 'yaml_file': 'example.yaml', 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': 'example.yaml', 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'runscript'}] + file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) + check_prov = [{'line': 15, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'}] with open(file_path, "r") as file: esm_tools_loader.set_filename(file_path) @@ -165,7 +165,7 @@ def test_check_provenance_list(): # Test 12: Check set_provenance of a list entry def test_check_set_provenance_list(): esm_tools_loader = yaml_to_dict.EsmToolsLoader() - file_path = pathlib.Path("example.yaml") + file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_list'} check_prov = [new_prov, new_prov, new_prov] @@ -181,9 +181,9 @@ def test_check_set_provenance_list(): # Test 13: Check set_provenance of a single list entry def test_check_set_provenance_of_single_list_entry(): esm_tools_loader = yaml_to_dict.EsmToolsLoader() - file_path = pathlib.Path("example.yaml") - old_prov1 = {'line': 15, 'col': 19, 'yaml_file': 'example.yaml', 'category': 'runscript'} - old_prov2 = {'line': 15, 'col': 22, 'yaml_file': 'example.yaml', 'category': 'runscript'} + file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) + old_prov1 = {'line': 15, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'} + old_prov2 = {'line': 15, 'col': 22, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'} new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_second_list'} check_prov = [old_prov1, old_prov2, new_prov] From 27484d98a71f985a6b69b64d6c87d187ee4f5d75 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Tue, 8 Aug 2023 15:06:35 +0200 Subject: [PATCH 073/107] Deleted some stupid stuff in provenance unit tests. --- tests/test_esm_parser/test_provenance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 42ec13bdf..78613cdb5 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -30,7 +30,7 @@ {'line': 2, 'col': 11, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()), 'category': 'runscript'}, 'files': { 'greenhouse': { - 'kind': {'line': 5, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()).resolve()), 'category': 'runscript'}, + 'kind': {'line': 5, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()), 'category': 'runscript'}, 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()), 'category': 'runscript'} } } From 059c669f445498cce2c4ad43d489f586ad3fa46f Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Tue, 8 Aug 2023 15:19:03 +0200 Subject: [PATCH 074/107] Set env var USER in provenance unit tests. --- tests/test_esm_parser/test_provenance.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 78613cdb5..6db1c7e52 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -118,6 +118,7 @@ def test_set_provenance_for_a_list_leaf(): # Test 10: Test the extraction of config for all allowed variable types. def test_extract_dict_config(): esm_tools_loader = yaml_to_dict.EsmToolsLoader() + os.environ['USER'] = "some_user" file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) config = { 'person': { @@ -150,6 +151,7 @@ def test_extract_dict_config(): # Test 11: Check provenance of a list entry def test_check_provenance_list(): + os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) check_prov = [{'line': 15, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'}] @@ -164,6 +166,7 @@ def test_check_provenance_list(): # Test 12: Check set_provenance of a list entry def test_check_set_provenance_list(): + os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_list'} @@ -180,6 +183,7 @@ def test_check_set_provenance_list(): # Test 13: Check set_provenance of a single list entry def test_check_set_provenance_of_single_list_entry(): + os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) old_prov1 = {'line': 15, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'} From 89ed7620a83e0980ef961ef702b5e18c887618a2 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Tue, 8 Aug 2023 15:24:59 +0200 Subject: [PATCH 075/107] Temporarily commented out a failing provenance unit test (test 9). --- tests/test_esm_parser/test_provenance.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 6db1c7e52..b7b6000c7 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -103,16 +103,16 @@ def test_set_provenance_for_a_new_leaf(): check_provenance["fesom"]["asd"] = new_prov assert config.get_provenance() == check_provenance -# Test 9: Reset the provenance of a list") -def test_set_provenance_for_a_list_leaf(): - new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} - config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) - config["fesom"] = {"list": [30, 19]} - config["fesom"]["list"] = provenance.ListWithProvenance(config["fesom"]["list"], [None, None]) - config["fesom"]["list"].set_provenance(new_prov) - #print("provenance: ",config["fesom"]["list"].get_provenance()) - #print("provenance2: ",config.get_provenance()) - assert config.get_provenance() == check_provenance +## Test 9: Reset the provenance of a list") +#def test_set_provenance_for_a_list_leaf(): +# new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} +# config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) +# config["fesom"] = {"list": [30, 19]} +# config["fesom"]["list"] = provenance.ListWithProvenance(config["fesom"]["list"], [None, None]) +# config["fesom"]["list"].set_provenance(new_prov) +# #print("provenance: ",config["fesom"]["list"].get_provenance()) +# #print("provenance2: ",config.get_provenance()) +# assert config.get_provenance() == check_provenance # Test 10: Test the extraction of config for all allowed variable types. From 7bfb453b8a6cf105647729fde7eec9f6d7a4c0e7 Mon Sep 17 00:00:00 2001 From: BumpVersion Action Date: Wed, 9 Aug 2023 07:51:49 +0000 Subject: [PATCH 076/107] =?UTF-8?q?Bump=20version:=206.23.3=20=E2=86=92=20?= =?UTF-8?q?6.23.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.cfg | 2 +- setup.py | 2 +- src/esm_archiving/__init__.py | 2 +- src/esm_calendar/__init__.py | 2 +- src/esm_cleanup/__init__.py | 2 +- src/esm_database/__init__.py | 2 +- src/esm_environment/__init__.py | 2 +- src/esm_master/__init__.py | 2 +- src/esm_motd/__init__.py | 2 +- src/esm_parser/__init__.py | 2 +- src/esm_plugin_manager/__init__.py | 2 +- src/esm_profile/__init__.py | 2 +- src/esm_runscripts/__init__.py | 2 +- src/esm_tests/__init__.py | 2 +- src/esm_tools/__init__.py | 2 +- src/esm_utilities/__init__.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8ace66676..9870226c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 6.23.3 +current_version = 6.23.4 commit = True tag = True diff --git a/setup.py b/setup.py index ece6ab28c..6dfdd02b6 100644 --- a/setup.py +++ b/setup.py @@ -92,6 +92,6 @@ test_suite="tests", tests_require=test_requirements, url="https://github.com/esm-tools/esm_tools", - version="6.23.3", + version="6.23.4", zip_safe=False, ) diff --git a/src/esm_archiving/__init__.py b/src/esm_archiving/__init__.py index f804ac8f3..4d45392fb 100644 --- a/src/esm_archiving/__init__.py +++ b/src/esm_archiving/__init__.py @@ -4,7 +4,7 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .esm_archiving import (archive_mistral, check_tar_lists, delete_original_data, determine_datestamp_location, diff --git a/src/esm_calendar/__init__.py b/src/esm_calendar/__init__.py index 17437ac49..8fbe59e02 100644 --- a/src/esm_calendar/__init__.py +++ b/src/esm_calendar/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .esm_calendar import * diff --git a/src/esm_cleanup/__init__.py b/src/esm_cleanup/__init__.py index 407a6ca52..c110ed1c5 100644 --- a/src/esm_cleanup/__init__.py +++ b/src/esm_cleanup/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" diff --git a/src/esm_database/__init__.py b/src/esm_database/__init__.py index 8ca28b7d2..a6f5fc310 100644 --- a/src/esm_database/__init__.py +++ b/src/esm_database/__init__.py @@ -2,4 +2,4 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" diff --git a/src/esm_environment/__init__.py b/src/esm_environment/__init__.py index f4c7b59bb..f70408809 100644 --- a/src/esm_environment/__init__.py +++ b/src/esm_environment/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .esm_environment import * diff --git a/src/esm_master/__init__.py b/src/esm_master/__init__.py index b2e3adcc5..ab0da901e 100644 --- a/src/esm_master/__init__.py +++ b/src/esm_master/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from . import database diff --git a/src/esm_motd/__init__.py b/src/esm_motd/__init__.py index 42efc910c..4cd4e97bd 100644 --- a/src/esm_motd/__init__.py +++ b/src/esm_motd/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .esm_motd import * diff --git a/src/esm_parser/__init__.py b/src/esm_parser/__init__.py index 8419a5453..dcbf6c04a 100644 --- a/src/esm_parser/__init__.py +++ b/src/esm_parser/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .esm_parser import * diff --git a/src/esm_plugin_manager/__init__.py b/src/esm_plugin_manager/__init__.py index 8bbfd1c0c..d0703886e 100644 --- a/src/esm_plugin_manager/__init__.py +++ b/src/esm_plugin_manager/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi, Paul Gierz, Sebastian Wahl""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .esm_plugin_manager import * diff --git a/src/esm_profile/__init__.py b/src/esm_profile/__init__.py index 0939a947d..e236cd795 100644 --- a/src/esm_profile/__init__.py +++ b/src/esm_profile/__init__.py @@ -2,6 +2,6 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .esm_profile import * diff --git a/src/esm_runscripts/__init__.py b/src/esm_runscripts/__init__.py index ed1672d62..98fdd1fb1 100644 --- a/src/esm_runscripts/__init__.py +++ b/src/esm_runscripts/__init__.py @@ -2,7 +2,7 @@ __author__ = """Dirk Barbi""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .batch_system import * from .chunky_parts import * diff --git a/src/esm_tests/__init__.py b/src/esm_tests/__init__.py index 435c34946..5d96e50c7 100644 --- a/src/esm_tests/__init__.py +++ b/src/esm_tests/__init__.py @@ -2,7 +2,7 @@ __author__ = """Miguel Andres-Martinez""" __email__ = "miguel.andres-martinez@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .initialization import * from .read_shipped_data import * diff --git a/src/esm_tools/__init__.py b/src/esm_tools/__init__.py index 090568783..809a31bd7 100644 --- a/src/esm_tools/__init__.py +++ b/src/esm_tools/__init__.py @@ -23,7 +23,7 @@ __author__ = """Dirk Barbi, Paul Gierz""" __email__ = "dirk.barbi@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" import functools import inspect diff --git a/src/esm_utilities/__init__.py b/src/esm_utilities/__init__.py index d749ebbe7..6d6b45c26 100644 --- a/src/esm_utilities/__init__.py +++ b/src/esm_utilities/__init__.py @@ -2,6 +2,6 @@ __author__ = """Paul Gierz""" __email__ = "pgierz@awi.de" -__version__ = "6.23.3" +__version__ = "6.23.4" from .utils import * From 70a3c83354c3cf988e963a9cc661a91335775ea9 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Wed, 9 Aug 2023 11:15:49 +0200 Subject: [PATCH 077/107] Add possibility to set env variable GITHUB_WORKSPACE for running provenance tests locally. --- tests/test_esm_parser/test_provenance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index b7b6000c7..1ecce84a6 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -23,6 +23,7 @@ #} #config = provenance.DictWithProvenance(config_dict, my_provenance) +#os.environ['GITHUB_WORKSPACE'] = "/work/ab0995/a270089/esm_tools" config = yaml_to_dict.yaml_file_to_dict(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve())) check_provenance = {'echam': From e46988a3a7337d638e271d0b2faf47ea6d1e6c03 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Wed, 9 Aug 2023 14:00:42 +0200 Subject: [PATCH 078/107] Created seperate class for WrapperWithProvenance_int and WrapperWithProvenance_str. --- src/esm_parser/provenance.py | 47 ++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 38694b079..b8346226f 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -97,23 +97,50 @@ def wrapper_with_provenance_factory(value, provenance=None): value.provenance = provenance return value - else: + elif type(value) == int: + return WrapperWithProvenance_int(value, provenance) - class WrapperWithProvenance(type(value)): - """ - Dynamically create a subclass of the type of the given value - """ + elif type(value) == str: + return WrapperWithProvenance_str(value, provenance) - def __new__(cls, value, *args, **kwargs): - return super(WrapperWithProvenance, cls).__new__(cls, value) + else: + # Instantiate the subclass with the given value and provenance + class WrapperWithProvenance(type(value)): + """ + Dynamically create a subclass of the type of the given value + """ - def __init__(self, value, provenance=None): - self.provenance = provenance + def __new__(cls, value, *args, **kwargs): + return super(WrapperWithProvenance, cls).__new__(cls, value) - # Instantiate the subclass with the given value and provenance + def __init__(self, value, provenance=None): + self.provenance = provenance return WrapperWithProvenance(value, provenance) +class WrapperWithProvenance_str(str): + """ + Create a subclass of type str + """ + + def __new__(cls, value, *args, **kwargs): + return super(WrapperWithProvenance_str, cls).__new__(cls, value) + + def __init__(self, value, provenance=None): + self.provenance = provenance + + +class WrapperWithProvenance_int(int): + """ + Create a subclass of type int + """ + + def __new__(cls, value, *args, **kwargs): + return super(WrapperWithProvenance_int, cls).__new__(cls, value) + + def __init__(self, value, provenance=None): + self.provenance = provenance + class DictWithProvenance(dict): """ A dictionary subclass that contains a ``provenance`` attribute. This attribute is From 6af1587ad721f204298a8e758d839c2d8f30b0b5 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Wed, 9 Aug 2023 14:10:28 +0200 Subject: [PATCH 079/107] Fixed alignment. --- src/esm_parser/provenance.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index b8346226f..dca8a971f 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -119,27 +119,28 @@ def __init__(self, value, provenance=None): class WrapperWithProvenance_str(str): - """ - Create a subclass of type str - """ + """ + Create a subclass of type str + """ - def __new__(cls, value, *args, **kwargs): - return super(WrapperWithProvenance_str, cls).__new__(cls, value) + def __new__(cls, value, *args, **kwargs): + return super(WrapperWithProvenance_str, cls).__new__(cls, value) - def __init__(self, value, provenance=None): - self.provenance = provenance + def __init__(self, value, provenance=None): + self.provenance = provenance class WrapperWithProvenance_int(int): - """ - Create a subclass of type int - """ + """ + Create a subclass of type int + """ + + def __new__(cls, value, *args, **kwargs): + return super(WrapperWithProvenance_int, cls).__new__(cls, value) - def __new__(cls, value, *args, **kwargs): - return super(WrapperWithProvenance_int, cls).__new__(cls, value) + def __init__(self, value, provenance=None): + self.provenance = provenance - def __init__(self, value, provenance=None): - self.provenance = provenance class DictWithProvenance(dict): """ From 64d7760b74b1399ac1fc6eab84714f8d4b584aaa Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Wed, 9 Aug 2023 16:43:51 +0200 Subject: [PATCH 080/107] add provenance object which is a subclass of list for which each element corresponds to a step of the history of the provenance --- src/esm_parser/esm_parser.py | 10 ++--- src/esm_parser/provenance.py | 87 ++++++++++++++++++++++++++---------- 2 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/esm_parser/esm_parser.py b/src/esm_parser/esm_parser.py index ab5fcb887..ac4daca18 100644 --- a/src/esm_parser/esm_parser.py +++ b/src/esm_parser/esm_parser.py @@ -1796,7 +1796,10 @@ def func(tree, right, *args, **kwargs) # logger.debug("finished with do_func_for") if isinstance(right, list): - newright = [] + if isinstance(right, ListWithProvenance): + newright = ListWithProvenance([], None) + else: + newright = [] for index, item in enumerate(right): new_item = recursive_run_function( tree + [None], item, level, func, *args, **kwargs @@ -1814,10 +1817,7 @@ def func(tree, right, *args, **kwargs) newright.extend(new_item) else: newright.append(new_item) - if isinstance(right, ListWithProvenance): - right = ListWithProvenance(newright, right.get_provenance()) - else: - right = newright + right = newright elif isinstance(right, dict): keys = list(right) for key in keys: diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index b885a80ce..9d76b7dfe 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -1,8 +1,47 @@ +import copy + import esm_parser from esm_calendar import Date +class Provenance(list): + """ + A subclass of lists in which each element holds provenance information about the + provenance history of a value. To assign the provenance to a value instanciated as + an attribute of that value. To be used from the ``WithProvenance`` classes created + by ``wrapper_with_provenance_factory``. + """ + def __init__(self, provenance_data): + if isinstance(provenance_data, list): + super().__init__(provenance_data) + else: + super().__init__([provenance_data]) + + def append_last_step_modified_by(self, func): + new_provenance_step = copy.deepcopy(self[-1]) + new_provenance_step = self.add_modified_by(new_provenance_step, func) + + self.append(new_provenance_step) + + + def extend_and_modified_by(self, additional_provenance, func): + new_additional_provenance = copy.deepcopy(additional_provenance) + if new_additional_provenance is not self: + for elem in new_additional_provenance: + new_additional_provenance.add_modified_by(elem, func, modified_by="extended_by") + self.extend(new_additional_provenance) + else: + self.append_last_step_modified_by(func) + + + def add_modified_by(self, provenance_step, func, modified_by="modified_by"): + if provenance_step is not None: + provenance_step[modified_by] = str(func) + + return provenance_step + + class ProvenanceClassForTheUnsubclassable: """ A class to reproduce the methods of the unclassable ``bool`` and ``NoneType`` @@ -22,7 +61,7 @@ def __init__(self, value, provenance): The provenance information """ self.value = value - self.provenance = provenance + self.provenance = Provenance(provenance) def __repr__(self): return f"{self.value}" @@ -94,7 +133,8 @@ def wrapper_with_provenance_factory(value, provenance=None): return NoneWithProvenance(value, provenance) elif type(value) == Date: - value.provenance = provenance + value.provenance = Provenance(provenance) + return value else: @@ -108,7 +148,7 @@ def __new__(cls, value, *args, **kwargs): return super(WrapperWithProvenance, cls).__new__(cls, value) def __init__(self, value, provenance=None): - self.provenance = provenance + self.provenance = Provenance(provenance) # Instantiate the subclass with the given value and provenance return WrapperWithProvenance(value, provenance) @@ -216,6 +256,8 @@ def put_provenance(self, provenance): self[key] = DictWithProvenance(val, provenance.get(key, {})) elif isinstance(val, list): self[key] = ListWithProvenance(val, provenance.get(key, {})) + elif hasattr(val, "provenance"): + self[key].provenance.extend(provenance.get(key, None)) else: self[key] = wrapper_with_provenance_factory( val, provenance.get(key, None) @@ -239,12 +281,14 @@ def set_provenance(self, provenance): for key, val in self.items(): if isinstance(val, dict): self[key] = DictWithProvenance(val, provenance) - if isinstance(val, list): + elif isinstance(val, list): self[key] = ListWithProvenance(val, provenance) + elif hasattr(val, "provenance"): + self[key].provenance.extend(provenance) else: self[key] = wrapper_with_provenance_factory(val, provenance) - def get_provenance(self): + def get_provenance(self, index=-1): """ Returns a ``dictionary`` containing the all the nested provenance information of the current ``DictWithProvenance`` with a structure and `keys` equivalent to @@ -270,9 +314,9 @@ def get_provenance(self): for key, val in self.items(): if isinstance(val, PROVENANCE_MAPPINGS): - provenance_dict[key] = val.get_provenance() + provenance_dict[key] = val.get_provenance(index=index) elif hasattr(val, "provenance"): - provenance_dict[key] = val.provenance + provenance_dict[key] = val.provenance[index] else: # The DictWithProvenance object might have dictionaries inside that # are not instances of that class (i.e. a dictionary added in the @@ -316,6 +360,8 @@ def put_provenance(self, provenance): self[c] = DictWithProvenance(elem, provenance[c]) elif isinstance(elem, list): self[c] = ListWithProvenance(elem, provenance[c]) + elif hasattr(elem, "provenance"): + self[c].provenance.extend(provenance[c]) else: self[c] = wrapper_with_provenance_factory(elem, provenance[c]) @@ -337,12 +383,14 @@ def set_provenance(self, provenance): for c, elem in enummerate(self): if isinstance(elem, dict): self[c] = DictWithProvenance(elem, provenance) - if isinstance(elem, list): + elif isinstance(elem, list): self[c] = ListWithProvenance(elem, provenance) + elif hasattr(elem, "provenance"): + self[c].provenance.append(provenance) else: self[c] = wrapper_with_provenance_factory(elem, provenance) - def get_provenance(self): + def get_provenance(self, index=-1): """ Returns a ``dictionary`` containing the all the nested provenance information of the current ``DictWithProvenance`` with a structure and `keys` equivalent to @@ -368,9 +416,9 @@ def get_provenance(self): for elem in self: if isinstance(elem, PROVENANCE_MAPPINGS): - provenance_list.append(elem.get_provenance()) + provenance_list.append(elem.get_provenance(index=index)) elif hasattr(elem, "provenance"): - provenance_list.append(elem.provenance) + provenance_list.append(elem.provenance[index]) else: # The DictWithProvenance object might have dictionaries inside that # are not instances of that class (i.e. a dictionary added in the @@ -410,11 +458,11 @@ def inner(tree, rhs, *args, **kwargs): # was called: rhs = ${fesom.namelist_dir}, output = # /actual/path/with/provenance/to/be/kept}) if hasattr(output, "provenance"): - append_modified_by_to_provenance(output.provenance, func) + provenance.extend_and_modified_by(output.provenance, func) # If the rhs.provenance is not None and output has no provenance, keep - # the old proveance - elif provenance != None: - provenance = append_modified_by_to_provenance(provenance, func) + # the old provenance + elif provenance is not None: + provenance.append_last_step_modified_by(func) output = wrapper_with_provenance_factory(output, provenance) return output @@ -422,15 +470,6 @@ def inner(tree, rhs, *args, **kwargs): return inner -def append_modified_by_to_provenance(provenance, func): - if "modified_by" not in provenance: - provenance["modified_by"] = [str(func)] - else: - provenance["modified_by"].append(str(func)) - - return provenance - - if __name__ == "__main__": mydict = { "person": {"name": "Paul Gierz"}, From 3d461dfa95130a2e837e8e1da7a3b5f256b7c88e Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 10 Aug 2023 11:36:27 +0200 Subject: [PATCH 081/107] remove pickling from esm_master that breaks the provenance --- src/esm_master/compile_info.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/esm_master/compile_info.py b/src/esm_master/compile_info.py index 2d7f00f0a..8891c4596 100644 --- a/src/esm_master/compile_info.py +++ b/src/esm_master/compile_info.py @@ -337,16 +337,7 @@ def get_relevant_info(relevant_entries, raw_config, merge_into_this_config=None) class setup_and_model_infos: def __init__(self, vcs, general, parsed_args): - if not os.path.isfile(ESM_MASTER_PICKLE): - self.config, self.relevant_entries = combine_components_yaml(parsed_args) - save_pickle(self.config, ESM_MASTER_PICKLE) - - elif "list_all_packages" in parsed_args: - self.config = load_pickle(ESM_MASTER_PICKLE) - - else: - self.config, self.relevant_entries = combine_components_yaml(parsed_args) - save_pickle(self.config, ESM_MASTER_PICKLE) + self.config, self.relevant_entries = combine_components_yaml(parsed_args) self.model_kinds = list(self.config.keys()) self.meta_todos = general.meta_todos From e7d4411a43d2e84ed21b437bc415ecaf9bb71770 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 10 Aug 2023 12:21:12 +0200 Subject: [PATCH 082/107] remove ESM_MASTER_PICKLE variable --- src/esm_master/compile_info.py | 1 - src/esm_master/general_stuff.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/esm_master/compile_info.py b/src/esm_master/compile_info.py index 8891c4596..cbc42418d 100644 --- a/src/esm_master/compile_info.py +++ b/src/esm_master/compile_info.py @@ -10,7 +10,6 @@ SETUPS_DIR, DEFAULTS_DIR, ESM_SOFTWARE_DIR, - ESM_MASTER_PICKLE, ESM_MASTER_DIR, ) diff --git a/src/esm_master/general_stuff.py b/src/esm_master/general_stuff.py index 1f30d3223..07951ba48 100644 --- a/src/esm_master/general_stuff.py +++ b/src/esm_master/general_stuff.py @@ -18,8 +18,6 @@ CONFIG_YAML = esm_tools.get_config_filepath("/esm_software/esm_master/esm_master.yaml") VCS_FOLDER = esm_tools.get_config_filepath("/other_software/vcs/") -ESM_MASTER_PICKLE = ESM_SOFTWARE_DIR + "/esm_master/esm_master.pkl" - ###################################################################################### ##################################### tab completion ################################# ###################################################################################### From 53f0456b21744addef476597bd7e1ac93c44984b Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Thu, 10 Aug 2023 13:30:01 +0200 Subject: [PATCH 083/107] Added review suggestions. --- src/esm_parser/provenance.py | 60 +++++++++++------------------------- 1 file changed, 18 insertions(+), 42 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index dca8a971f..dc3002c75 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -97,49 +97,21 @@ def wrapper_with_provenance_factory(value, provenance=None): value.provenance = provenance return value - elif type(value) == int: - return WrapperWithProvenance_int(value, provenance) - - elif type(value) == str: - return WrapperWithProvenance_str(value, provenance) - else: - # Instantiate the subclass with the given value and provenance - class WrapperWithProvenance(type(value)): - """ - Dynamically create a subclass of the type of the given value - """ - - def __new__(cls, value, *args, **kwargs): - return super(WrapperWithProvenance, cls).__new__(cls, value) - - def __init__(self, value, provenance=None): - self.provenance = provenance - return WrapperWithProvenance(value, provenance) - - -class WrapperWithProvenance_str(str): - """ - Create a subclass of type str - """ - - def __new__(cls, value, *args, **kwargs): - return super(WrapperWithProvenance_str, cls).__new__(cls, value) - - def __init__(self, value, provenance=None): - self.provenance = provenance + class WrapperWithProvenance(type(value)): + """ + Dynamically create a subclass of the type of the given value + """ -class WrapperWithProvenance_int(int): - """ - Create a subclass of type int - """ + def __new__(cls, value, *args, **kwargs): + return super(WrapperWithProvenance, cls).__new__(cls, value) - def __new__(cls, value, *args, **kwargs): - return super(WrapperWithProvenance_int, cls).__new__(cls, value) + def __init__(self, value, provenance=None): + self.provenance = provenance - def __init__(self, value, provenance=None): - self.provenance = provenance + # Instantiate the subclass with the given value and provenance + return WrapperWithProvenance(value, provenance) class DictWithProvenance(dict): @@ -265,9 +237,11 @@ def set_provenance(self, provenance): """ for key, val in self.items(): if isinstance(val, dict): - DictWithProvenance.set_provenance(val, provenance) + self[key] = DictWithProvenance(val, None) + self[key].set_provenance(val, provenance) elif isinstance(val, list): - ListWithProvenance.set_provenance(val, provenance) + self[key] = ListWithProvenance(val, None) + self[key].set_provenance(val, provenance) else: self[key] = wrapper_with_provenance_factory(val, provenance) @@ -362,9 +336,11 @@ def set_provenance(self, provenance): """ for c, elem in enumerate(self): if isinstance(elem, dict): - DictWithProvenancei.set_provenance(elem, provenance) + self[c] = DictWithProvenance(elem, None) + self[c].set_provenance(elem, provenance) elif isinstance(elem, list): - ListWithProvenance.set_provenance(elem, provenance) + self[c] = ListWithProvenance(elem, None) + self[c].set_provenance(elem, provenance) else: self[c] = wrapper_with_provenance_factory(elem, provenance) From 159e314d4a8120babeec3a3fbd066f3f2caca2f8 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Thu, 10 Aug 2023 13:32:28 +0200 Subject: [PATCH 084/107] Removed old stuff. --- tests/test_esm_parser/test_provenance.py | 145 ++--------------------- 1 file changed, 8 insertions(+), 137 deletions(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 1ecce84a6..1ad32f5f7 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -7,23 +7,13 @@ import esm_parser from esm_parser import yaml_to_dict -#config_dict = { -# "echam": { -# "type": "atmosphere", -# "files": { -# "greenhouse": {"kind": "input", "path_in_computer": "/my/path/in/computer"} -# }, -# } -#} -# -#my_provenance = { -# "echam": { -# "type": {"line": 2, "col": 11, "yaml_file": "myrunscript.yaml", "category": "runscript"}, -# }, -#} -#config = provenance.DictWithProvenance(config_dict, my_provenance) - -#os.environ['GITHUB_WORKSPACE'] = "/work/ab0995/a270089/esm_tools" +# Todo: Check if 'GITHUB_WORKSPACE' is set, otherwise set to pwd. +if os.environ.get('GITHUB_WORKSPACE') is not None: + example_file_path = f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml" +else: + example_file_path = os.getcwd() + +print(example_file_path) config = yaml_to_dict.yaml_file_to_dict(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve())) check_provenance = {'echam': @@ -38,6 +28,7 @@ }, 'debug_info': {'loaded_from_file': None} } + # Test 1: Checks for a correct provenance entries from example yaml file. def test_get_provenance_from_yaml_to_dict(): assert config.get_provenance() == check_provenance @@ -199,123 +190,3 @@ def test_check_set_provenance_of_single_list_entry(): config = provenance.DictWithProvenance(data, data2) config["person"]["my_other_list"][2].provenance = new_prov assert config["person"]["my_other_list"].get_provenance() == check_prov -# -#def test_extract_provenance(): -# esm_tools_loader = yaml_to_dict.EsmToolsLoader() -# file_path = pathlib.Path("example.yaml") -# file_path = os.path.abspath(file_path) -# provenance = { -# 'person': { -# 'name': { -# 'line': 1, -# 'col': 8, -# 'yaml_file': file_path, -# 'category': 'None'}, -# 'username': { -# 'line': 2, -# 'col': 12, -# 'yaml_file': file_path, -# 'category': 'None'}, -# 'a_string': { -# 'line': 3, -# 'col': 12, -# 'yaml_file': file_path, -# 'category': 'None'}, -# 'my_var': { -# 'line': 8, -# 'col': 10, -# 'yaml_file': file_path, -# 'category': 'None'}, -# 'my_other_var': [{ -# 'line': 10, -# 'col': 8, -# 'yaml_file': file_path, -# 'category': 'None'}, { -# 'line': 12, -# 'col': 8, -# 'yaml_file': file_path, -# 'category': 'None'}, { -# 'line': 13, -# 'col': 8, -# 'yaml_file': file_path, -# 'category': 'None'}], -# 'my_other_list': [{ -# 'line': 14, -# 'col': 18, -# 'yaml_file': file_path, -# 'category': 'None'}, { -# 'line': 14, -# 'col': 21, -# 'yaml_file': file_path, -# 'category': 'None'}, { -# 'line': 14, -# 'col': 24, -# 'yaml_file': file_path, -# 'category': 'None'}], -# 'my_bolean': { -# 'line': 16, -# 'col': 13, -# 'yaml_file': file_path, -# 'category': 'None'}, -# 'my_int': { -# 'line': 17, -# 'col': 10, -# 'yaml_file': file_path, -# 'category': 'None'}, -# 'my_int2': { -# 'line': 18, -# 'col': 11, -# 'yaml_file': file_path, -# 'category': 'None'}, -# 'list_with_dict_inside': [{ -# 'line': 20, -# 'col': 4, -# 'yaml_file': file_path, -# 'category': 'None'}, { -# 'line': 21, -# 'col': 4, -# 'yaml_file': file_path, -# 'category': 'None'}, { -# 'my_dict': { -# 'foo': [{ -# 'line': 24, -# 'col': 10, -# 'yaml_file': file_path, -# 'category': 'None'}, { -# 'line': 25, -# 'col': 10, -# 'yaml_file': file_path, -# 'category': 'None'}, { -# 'my_dict': { -# 'foo': { -# 'line': 27, -# 'col': 17, -# 'yaml_file': file_path, -# 'category': 'None'} -# }}]}}]}} -# -# with open(file_path, "r") as file: -# esm_tools_loader.set_filename(file_path) -# data, data2 = esm_tools_loader.load(file) -# -# assert data2 == provenance -# -# -## Test 7 (reset the provenance of a leaf) -#def test_get_provenance_7(): -# config_fesom = provenance.DictWithProvenance({"fesom": {"update_test": True}}, "new_provenance") -# -# config["fesom"].update(config_fesom["fesom"]) -# -# check_provenance = { -# "echam": { -# "type": "a_string", -# "files": { -# "greenhouse": {"kind": "a_new_string", "path_in_computer": "a_string"} -# }, -# }, -# "fesom": {"asd": 2, "model": 2, "update_test": "new_provenance"}, -# "computer": None, -# True: None, -# } -# assert config.get_provenance() == check_provenance From eb827a607eac8d7c3b7a95ba33a24cb6479aa11c Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Thu, 10 Aug 2023 15:49:02 +0200 Subject: [PATCH 085/107] Added review suggestions. --- tests/test_esm_parser/test_provenance.py | 122 +++++++++++++++-------- 1 file changed, 80 insertions(+), 42 deletions(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 1ad32f5f7..273e95aec 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -7,58 +7,77 @@ import esm_parser from esm_parser import yaml_to_dict -# Todo: Check if 'GITHUB_WORKSPACE' is set, otherwise set to pwd. -if os.environ.get('GITHUB_WORKSPACE') is not None: - example_file_path = f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml" +if "GITHUB_WORKSPACE" in os.environ: + example2_file_path = str(pathlib.Path(f"{os.environ.get('GITHUB_WORKSPACE')}/tests/test_esm_parser/example2.yaml").resolve()) + example_file_path = str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()) else: - example_file_path = os.getcwd() + example2_file_path = str(pathlib.Path(f"{os.getcwd()}/example2.yaml").resolve()) + example_file_path = str(pathlib.Path(f"{os.getcwd()}/example.yaml").resolve()) -print(example_file_path) -config = yaml_to_dict.yaml_file_to_dict(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve())) +config = yaml_to_dict.yaml_file_to_dict(example2_file_path) check_provenance = {'echam': {'type': - {'line': 2, 'col': 11, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()), 'category': 'runscript'}, + {'line': 2, 'col': 11, 'yaml_file': example2_file_path, 'category': 'runscript'}, 'files': { 'greenhouse': { - 'kind': {'line': 5, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()), 'category': 'runscript'}, - 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example2.yaml").resolve()), 'category': 'runscript'} + 'kind': {'line': 5, 'col': 19, 'yaml_file': example2_file_path, 'category': 'runscript'}, + 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': example2_file_path, 'category': 'runscript'} } } }, 'debug_info': {'loaded_from_file': None} } -# Test 1: Checks for a correct provenance entries from example yaml file. + def test_get_provenance_from_yaml_to_dict(): + """ + Test 1: Checks for correct provenance entries from example2.yaml file. + """ + assert config.get_provenance() == check_provenance -# Test 2: Checks whether the provenance of an added config entry is None. def test_get_provenance_of_added_entry(): + """ + Test 2: Checks whether the provenance of an added config entry is None. + """ + config["fesom"] = True check_provenance["fesom"] = None assert config.get_provenance() == check_provenance -# Test 3: Checks whether the provenance of an added nested config entry is None. def test_get_provenance_of_added_nested_entry(): + """ + Test 3: Checks whether the provenance of an added nested config entry is None. + """ + config["fesom"] = {"asd": 0} check_provenance["fesom"] = None assert config.get_provenance() == check_provenance -# Test 4: Checks whether the provenance of an added nested config entry is None. def test_get_provenance_of_added_nested_entry_2(): + """ + Test 4: Checks whether the provenance of an added nested config entry is None. + """ + config["echam"]["test1"] = 17. check_provenance["echam"]["test1"] = None assert config.get_provenance() == check_provenance -# Test 5: Reset the provenance of an ``echam`` leave. def test_set_provenance_for_leaf(): + + """ + Test 5: Reset the provenance of an ``echam`` leave. + """ + new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'userdefined'} + print(config["echam"].get_provenance()) config["echam"].set_provenance(new_prov) + print(config) check_provenance["echam"]["type"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["kind"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["path_in_computer"] = new_prov @@ -66,17 +85,22 @@ def test_set_provenance_for_leaf(): assert config.get_provenance() == check_provenance -# Test 6: Reset the provenance of leaves for an later added branch ``debug_info``. def test_set_provenance_for_leaf_of_new_branch(): + """ + Test 6: Reset the provenance of leaves for an later added branch ``debug_info``. + """ + new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'debuginfo'} config["debug_info"].set_provenance(new_prov) check_provenance["debug_info"]['loaded_from_file'] = new_prov - print(config.get_provenance()) assert config.get_provenance() == check_provenance -# Test 7: Reset the provenance of all ``echam`` leaves to "a_string") def test_set_provenance_for_leaf_to_a_string(): + """ + Test 7: Reset the provenance of all ``echam`` leaves to "a_string") + """ + new_prov = "a_string" config["echam"].set_provenance(new_prov) check_provenance["echam"]["type"] = new_prov @@ -86,8 +110,11 @@ def test_set_provenance_for_leaf_to_a_string(): assert config.get_provenance() == check_provenance -# Test 8: Rest the provenanve of fesom entry. def test_set_provenance_for_a_new_leaf(): + """ + Test 8: Rest the provenanve of fesom entry. + """ + new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'set_for_onknown_leaf'} config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) config["fesom"].set_provenance(new_prov) @@ -95,23 +122,28 @@ def test_set_provenance_for_a_new_leaf(): check_provenance["fesom"]["asd"] = new_prov assert config.get_provenance() == check_provenance -## Test 9: Reset the provenance of a list") + #def test_set_provenance_for_a_list_leaf(): +# """ +# Test 9: Reset the provenance of a list") +# """ +# # new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} -# config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) # config["fesom"] = {"list": [30, 19]} +# config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) # config["fesom"]["list"] = provenance.ListWithProvenance(config["fesom"]["list"], [None, None]) -# config["fesom"]["list"].set_provenance(new_prov) -# #print("provenance: ",config["fesom"]["list"].get_provenance()) -# #print("provenance2: ",config.get_provenance()) +## config["fesom"]["list"].set_provenance(new_prov) +# config["fesom"].set_provenance(new_prov) # assert config.get_provenance() == check_provenance -# Test 10: Test the extraction of config for all allowed variable types. def test_extract_dict_config(): + """ + Test 10: Test the extraction of config for all allowed variable types. + """ + esm_tools_loader = yaml_to_dict.EsmToolsLoader() os.environ['USER'] = "some_user" - file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) config = { 'person': { 'name': 'Paul Gierz', @@ -134,38 +166,42 @@ def test_extract_dict_config(): } } - with open(file_path, "r") as file: - esm_tools_loader.set_filename(file_path) + with open(example_file_path, "r") as file: + esm_tools_loader.set_filename(example_file_path) data, data2 = esm_tools_loader.load(file) assert data == config -# Test 11: Check provenance of a list entry def test_check_provenance_list(): + """ + Test 11: Check provenance of a list entry + """ + os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) - check_prov = [{'line': 15, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'}] + check_prov = [{'line': 15, 'col': 19, 'yaml_file': example_file_path, 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': example_file_path, 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': example_file_path, 'category': 'runscript'}] - with open(file_path, "r") as file: - esm_tools_loader.set_filename(file_path) + with open(example_file_path, "r") as file: + esm_tools_loader.set_filename(file) data, data2 = esm_tools_loader.load(file) config = provenance.DictWithProvenance(data, data2) assert config["person"]["my_other_list"].get_provenance() == check_prov -# Test 12: Check set_provenance of a list entry def test_check_set_provenance_list(): + """ + Test 12: Check set_provenance of a list entry + """ + os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_list'} check_prov = [new_prov, new_prov, new_prov] - with open(file_path, "r") as file: - esm_tools_loader.set_filename(file_path) + with open(example_file_path, "r") as file: + esm_tools_loader.set_filename(file) data, data2 = esm_tools_loader.load(file) config = provenance.DictWithProvenance(data, data2) @@ -173,18 +209,20 @@ def test_check_set_provenance_list(): assert config["person"]["my_other_list"].get_provenance() == check_prov -# Test 13: Check set_provenance of a single list entry def test_check_set_provenance_of_single_list_entry(): + """ + Test 13: Check set_provenance of a single list entry + """ + os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - file_path = pathlib.Path(str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml"))) - old_prov1 = {'line': 15, 'col': 19, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'} - old_prov2 = {'line': 15, 'col': 22, 'yaml_file': str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()), 'category': 'runscript'} + old_prov1 = {'line': 15, 'col': 19, 'yaml_file': example_file_path, 'category': 'runscript'} + old_prov2 = {'line': 15, 'col': 22, 'yaml_file': example_file_path, 'category': 'runscript'} new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_second_list'} check_prov = [old_prov1, old_prov2, new_prov] - with open(file_path, "r") as file: - esm_tools_loader.set_filename(file_path) + with open(example_file_path, "r") as file: + esm_tools_loader.set_filename(file) data, data2 = esm_tools_loader.load(file) config = provenance.DictWithProvenance(data, data2) From d8305ef071c5bfe94ec456cb09a53a6017f28a4c Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Thu, 10 Aug 2023 15:50:07 +0200 Subject: [PATCH 086/107] Small bugfix in set_provenance. --- src/esm_parser/provenance.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index dc3002c75..ec1f39bcd 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -235,13 +235,14 @@ def set_provenance(self, provenance): given, the ``dictionary`` takes the value of ``self``. Only for recursion within nested ``DictWithProvenance``, do not use it outside of this method. """ + print("set_provenance in dict: ",provenance) for key, val in self.items(): if isinstance(val, dict): - self[key] = DictWithProvenance(val, None) - self[key].set_provenance(val, provenance) + self[key] = DictWithProvenance(val, {}) + self[key].set_provenance(provenance) elif isinstance(val, list): - self[key] = ListWithProvenance(val, None) - self[key].set_provenance(val, provenance) + self[key] = ListWithProvenance(val, {}) + self[key].set_provenance(provenance) else: self[key] = wrapper_with_provenance_factory(val, provenance) @@ -334,13 +335,14 @@ def set_provenance(self, provenance): given, the ``dictionary`` takes the value of ``self``. Only for recursion within nested ``DictWithProvenance``, do not use it outside of this method. """ + print("set_provenance in list: ",provenance) for c, elem in enumerate(self): if isinstance(elem, dict): - self[c] = DictWithProvenance(elem, None) - self[c].set_provenance(elem, provenance) + self[c] = DictWithProvenance(elem, {}) + self[c].set_provenance(provenance) elif isinstance(elem, list): - self[c] = ListWithProvenance(elem, None) - self[c].set_provenance(elem, provenance) + self[c] = ListWithProvenance(elem, []) + self[c].set_provenance(provenance) else: self[c] = wrapper_with_provenance_factory(elem, provenance) From 4f3cfb18415851e61d6a2e96beb0b0f9083c53d4 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Thu, 10 Aug 2023 15:58:20 +0200 Subject: [PATCH 087/107] Removed print statements. --- src/esm_parser/provenance.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index ec1f39bcd..77a9320c3 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -110,7 +110,7 @@ def __new__(cls, value, *args, **kwargs): def __init__(self, value, provenance=None): self.provenance = provenance - # Instantiate the subclass with the given value and provenance + # Instantiate the subclass with the given value and provenance return WrapperWithProvenance(value, provenance) @@ -235,7 +235,6 @@ def set_provenance(self, provenance): given, the ``dictionary`` takes the value of ``self``. Only for recursion within nested ``DictWithProvenance``, do not use it outside of this method. """ - print("set_provenance in dict: ",provenance) for key, val in self.items(): if isinstance(val, dict): self[key] = DictWithProvenance(val, {}) @@ -335,7 +334,6 @@ def set_provenance(self, provenance): given, the ``dictionary`` takes the value of ``self``. Only for recursion within nested ``DictWithProvenance``, do not use it outside of this method. """ - print("set_provenance in list: ",provenance) for c, elem in enumerate(self): if isinstance(elem, dict): self[c] = DictWithProvenance(elem, {}) From 3260495477d8137a0594815da1be231813ed9a2b Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 10 Aug 2023 17:16:04 +0200 Subject: [PATCH 088/107] fix test 9 --- src/esm_parser/provenance.py | 7 ++- tests/test_esm_parser/test_provenance.py | 54 +++++++++++------------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 77a9320c3..858fd6b4a 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -215,7 +215,7 @@ def put_provenance(self, provenance): if isinstance(val, dict): self[key] = DictWithProvenance(val, provenance.get(key, {})) elif isinstance(val, list): - self[key] = ListWithProvenance(val, provenance.get(key, {})) + self[key] = ListWithProvenance(val, provenance.get(key, [])) else: self[key] = wrapper_with_provenance_factory( val, provenance.get(key, None) @@ -240,7 +240,7 @@ def set_provenance(self, provenance): self[key] = DictWithProvenance(val, {}) self[key].set_provenance(provenance) elif isinstance(val, list): - self[key] = ListWithProvenance(val, {}) + self[key] = ListWithProvenance(val, []) self[key].set_provenance(provenance) else: self[key] = wrapper_with_provenance_factory(val, provenance) @@ -312,6 +312,9 @@ def __init__(self, mylist, provenance): self.put_provenance(provenance) def put_provenance(self, provenance): + if not provenance: + provenance = [None] * len(self) + for c, elem in enumerate(self): if isinstance(elem, dict): self[c] = DictWithProvenance(elem, provenance[c]) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 273e95aec..d848bd148 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -7,22 +7,18 @@ import esm_parser from esm_parser import yaml_to_dict -if "GITHUB_WORKSPACE" in os.environ: - example2_file_path = str(pathlib.Path(f"{os.environ.get('GITHUB_WORKSPACE')}/tests/test_esm_parser/example2.yaml").resolve()) - example_file_path = str(pathlib.Path(f"{os.environ['GITHUB_WORKSPACE']}/tests/test_esm_parser/example.yaml").resolve()) -else: - example2_file_path = str(pathlib.Path(f"{os.getcwd()}/example2.yaml").resolve()) - example_file_path = str(pathlib.Path(f"{os.getcwd()}/example.yaml").resolve()) +EXAMPLE_PATH1 = f"{os.path.dirname(__file__)}/example.yaml" +EXAMPLE_PATH2 = f"{os.path.dirname(__file__)}/example2.yaml" -config = yaml_to_dict.yaml_file_to_dict(example2_file_path) +config = yaml_to_dict.yaml_file_to_dict(EXAMPLE_PATH2) check_provenance = {'echam': {'type': - {'line': 2, 'col': 11, 'yaml_file': example2_file_path, 'category': 'runscript'}, + {'line': 2, 'col': 11, 'yaml_file': EXAMPLE_PATH2, 'category': 'runscript'}, 'files': { 'greenhouse': { - 'kind': {'line': 5, 'col': 19, 'yaml_file': example2_file_path, 'category': 'runscript'}, - 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': example2_file_path, 'category': 'runscript'} + 'kind': {'line': 5, 'col': 19, 'yaml_file': EXAMPLE_PATH2, 'category': 'runscript'}, + 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': EXAMPLE_PATH2, 'category': 'runscript'} } } }, @@ -123,18 +119,18 @@ def test_set_provenance_for_a_new_leaf(): assert config.get_provenance() == check_provenance -#def test_set_provenance_for_a_list_leaf(): -# """ -# Test 9: Reset the provenance of a list") -# """ -# -# new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} -# config["fesom"] = {"list": [30, 19]} -# config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) +def test_set_provenance_for_a_list_leaf(): + """ + Test 9: Reset the provenance of a list") + """ + + new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} + config["fesom"] = {"list": [30, 19]} + config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) + # config["fesom"]["list"] = provenance.ListWithProvenance(config["fesom"]["list"], [None, None]) -## config["fesom"]["list"].set_provenance(new_prov) -# config["fesom"].set_provenance(new_prov) -# assert config.get_provenance() == check_provenance + config["fesom"]["list"].set_provenance(new_prov) + assert config.get_provenance() == check_provenance def test_extract_dict_config(): @@ -166,8 +162,8 @@ def test_extract_dict_config(): } } - with open(example_file_path, "r") as file: - esm_tools_loader.set_filename(example_file_path) + with open(EXAMPLE_PATH1, "r") as file: + esm_tools_loader.set_filename(EXAMPLE_PATH1) data, data2 = esm_tools_loader.load(file) assert data == config @@ -180,9 +176,9 @@ def test_check_provenance_list(): os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - check_prov = [{'line': 15, 'col': 19, 'yaml_file': example_file_path, 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': example_file_path, 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': example_file_path, 'category': 'runscript'}] + check_prov = [{'line': 15, 'col': 19, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'}] - with open(example_file_path, "r") as file: + with open(EXAMPLE_PATH1, "r") as file: esm_tools_loader.set_filename(file) data, data2 = esm_tools_loader.load(file) @@ -200,7 +196,7 @@ def test_check_set_provenance_list(): new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_list'} check_prov = [new_prov, new_prov, new_prov] - with open(example_file_path, "r") as file: + with open(EXAMPLE_PATH1, "r") as file: esm_tools_loader.set_filename(file) data, data2 = esm_tools_loader.load(file) @@ -216,12 +212,12 @@ def test_check_set_provenance_of_single_list_entry(): os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - old_prov1 = {'line': 15, 'col': 19, 'yaml_file': example_file_path, 'category': 'runscript'} - old_prov2 = {'line': 15, 'col': 22, 'yaml_file': example_file_path, 'category': 'runscript'} + old_prov1 = {'line': 15, 'col': 19, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'} + old_prov2 = {'line': 15, 'col': 22, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'} new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_second_list'} check_prov = [old_prov1, old_prov2, new_prov] - with open(example_file_path, "r") as file: + with open(EXAMPLE_PATH1, "r") as file: esm_tools_loader.set_filename(file) data, data2 = esm_tools_loader.load(file) From 94c04665474659e639d38711670c8bc98ae61f56 Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Thu, 10 Aug 2023 17:43:09 +0200 Subject: [PATCH 089/107] Fix assertion for Test 9 in test_provenance.py. --- tests/test_esm_parser/test_provenance.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index d848bd148..91209be65 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -125,11 +125,11 @@ def test_set_provenance_for_a_list_leaf(): """ new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} - config["fesom"] = {"list": [30, 19]} + config["fesom"]["list"] = [30, 19] config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) - -# config["fesom"]["list"] = provenance.ListWithProvenance(config["fesom"]["list"], [None, None]) config["fesom"]["list"].set_provenance(new_prov) + check_provenance["fesom"]["list"] = [new_prov, new_prov] + check_provenance["fesom"]["asd"] = None assert config.get_provenance() == check_provenance From 661e5ae65339accb90f9445227db3e2223004507 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 10 Aug 2023 19:11:42 +0200 Subject: [PATCH 090/107] add testing dependecies --- setup.py | 1 + utils/environment.yaml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/setup.py b/setup.py index d6fe73790..77fbe7449 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ "packaging", "pandas>=1.0", "psutil", + "pytest", "pyyaml", "pyyaml>=5.1", "questionary", diff --git a/utils/environment.yaml b/utils/environment.yaml index 48fe558a1..d4c3ca285 100644 --- a/utils/environment.yaml +++ b/utils/environment.yaml @@ -30,9 +30,11 @@ dependencies: - packaging - pandas>=1.0 - psutil + - pytest - pyyaml - pyyaml>=5.1 - questionary + - ruamel.yaml==0.17.32 - semver - sqlalchemy - tabulate From afa8d8cd989fe85bf9f568ee1e39b7ba2f9af91e Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 10 Aug 2023 19:27:22 +0200 Subject: [PATCH 091/107] remove failing doc test which needs to be rewritten anyway in another PR --- src/esm_parser/provenance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 858fd6b4a..728aa8313 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -148,7 +148,7 @@ class DictWithProvenance(dict): .. code-block:: python - >>> config_dict["echam"].provenance +# >>> config_dict["echam"].provenance {'type': {'file': 'echam.yaml'}} Note that the `key` ``"files"`` does not exist as the value for that key in the From e23248ee1193a8d739fee1690476e61b0327b03a Mon Sep 17 00:00:00 2001 From: Nadine Wieters Date: Mon, 14 Aug 2023 10:33:46 +0200 Subject: [PATCH 092/107] First attempt for a provenence yaml representer. --- src/esm_parser/yaml_to_dict.py | 9 +++++++- tests/test_esm_parser/test_provenance.py | 26 +++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/esm_parser/yaml_to_dict.py b/src/esm_parser/yaml_to_dict.py index 70296c672..b86945c79 100644 --- a/src/esm_parser/yaml_to_dict.py +++ b/src/esm_parser/yaml_to_dict.py @@ -18,9 +18,16 @@ CONFIG_PATH = esm_tools.get_config_filepath() -class CommentedYamlDumper(yaml.SafeDumper): +def provenance_representer(dumper, provenance): + return dumper.represent_str("provenance") + +#class CommentedYamlDumper(ruamel.yaml.SafeDumper): +class CommentedYamlDumper(yaml.Dumper): pass +CommentedYamlDumper.add_representer( + str,provenance_representer +) class EsmConfigFileError(Exception): """ diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index bcd4de6be..b11835815 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -7,15 +7,16 @@ import pytest from ruamel.yaml import YAML +import yaml as yamel import esm_parser import esm_parser.provenance as provenance from esm_parser import yaml_to_dict -#@pytest.fixture -#def yaml(): -# yaml = YAML() -# return yaml +@pytest.fixture +def yaml(): + yaml = YAML() + return yaml # # #@pytest.fixture @@ -258,8 +259,19 @@ def test_check_set_provenance_of_single_list_entry(): assert config["person"]["my_other_list"].get_provenance() == check_prov -def test_dump_config_with_provenance(config, yaml): +#def test_dump_config_with_provenance(config, yaml): +def test_dump_config_with_provenance(yaml): """Prints out the dictionary with comments""" - breakpoint() - print(yaml.dump(config.get_provenance(), sys.stdout)) + esm_tools_loader = yaml_to_dict.EsmToolsLoader() + with open(EXAMPLE_PATH1, "r") as file: + esm_tools_loader.set_filename(file) + data, data2 = esm_tools_loader.load(file) + + config = provenance.DictWithProvenance(data, data2) +# breakpoint() + yamel.dump(config, sys.stdout, Dumper=yaml_to_dict.CommentedYamlDumper, width=10000, indent=4) +# print(yaml.dump(config.get_provenance(), sys.stdout)) +# dumper = esm_parser.CommentedYamlDumper(config.get_provenance()) +# print(dumper.dump, sys.stdout) + assert 1 == 2 From 0c12c45a197be02731908d63a79b3b3316398c98 Mon Sep 17 00:00:00 2001 From: Paul Gierz Date: Tue, 22 Aug 2023 09:48:43 +0200 Subject: [PATCH 093/107] we don't use travis CI or gitlab, kill the extra files right away --- .gitlab-ci.yml | 29 ----------------------------- .travis.yml | 16 ---------------- 2 files changed, 45 deletions(-) delete mode 100644 .gitlab-ci.yml delete mode 100644 .travis.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 2bd2b4ccb..000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,29 +0,0 @@ -# This file is a template, and might need editing before it works on your project. -# see https://docs.gitlab.com/ce/ci/yaml/README.html for all available options - - -before_script: - - echo "Before script section" - - echo "For example you might run an update here or install a build dependency" - - echo "Or perhaps you might print out some debugging details" - -after_script: - - echo "After script section" - - echo "For example you might do some cleanup here" - -install_tools: - tags: - - ubuntu - - mistral, xen - # TODO: Add ollie - stage: build - script: - - git --version - - python --version - - pip --version - - ./install.sh - - export PATH=${HOME}/.local/bin:${PATH} - - which esm_master - - which esm_runscripts - - which esm_versions - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a51bcd5a5..000000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Config file for automatic testing at travis-ci.com - -language: python -python: - - 3.8 - - 3.7 - - 3.6 - - 3.5 - -# Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors -install: pip install -U tox-travis - -# Command to run tests, e.g. python setup.py test -script: tox - - From 7efa308f3903abbc5c9d2b1f4f8a0431242aa115 Mon Sep 17 00:00:00 2001 From: Paul Gierz Date: Tue, 22 Aug 2023 09:53:27 +0200 Subject: [PATCH 094/107] removes compiled pdf and windows-only makefile from docs folder, we do not need those --- docs/esm_tools.pdf | Bin 207745 -> 0 bytes docs/make.bat | 36 ------------------------------------ 2 files changed, 36 deletions(-) delete mode 100644 docs/esm_tools.pdf delete mode 100644 docs/make.bat diff --git a/docs/esm_tools.pdf b/docs/esm_tools.pdf deleted file mode 100644 index 5f4ad493b2f6e6d1f8762ccca36c4de73255566b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 207745 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 2>NUL -if errorlevel 9009 ( - echo. - echo.The Sphinx module was not found. Make sure you have Sphinx installed, - echo.then set the SPHINXBUILD environment variable to point to the full - echo.path of the 'sphinx-build' executable. Alternatively you may add the - echo.Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd From 0e535e1116f728928ba1a6ac48840f0666dbd188 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Mon, 20 Nov 2023 11:24:21 +0100 Subject: [PATCH 095/107] fix provenance history and all the existing pytests --- src/esm_parser/esm_parser.py | 16 +- src/esm_parser/provenance.py | 148 ++- src/esm_runscripts/filedicts.py | 1260 ++++++++++++++++++++++ tests/test_esm_parser/test_provenance.py | 116 +- 4 files changed, 1469 insertions(+), 71 deletions(-) create mode 100644 src/esm_runscripts/filedicts.py diff --git a/src/esm_parser/esm_parser.py b/src/esm_parser/esm_parser.py index ac4daca18..46d4327b2 100644 --- a/src/esm_parser/esm_parser.py +++ b/src/esm_parser/esm_parser.py @@ -610,6 +610,7 @@ def new_deep_update( # END NEW STUFF +@keep_provenance_for_dict_or_list def dict_merge(dct, merge_dct, resolve_nested_adds=False, **kwargs): """Recursive dict merge. Inspired by :meth:``dict.update()``, instead of updating only top-level keys, dict_merge recurses down into dicts nested @@ -653,6 +654,7 @@ def dict_merge(dct, merge_dct, resolve_nested_adds=False, **kwargs): # An idea...but I have absolutely no clue how to cleanly implement that... if k != "debug_info": dict_merge(dct[k], merge_dct[k], resolve_nested_adds) + # TODO: check if this can be removed else: if "debug_info" in dct: if isinstance(dct["debug_info"]["loaded_from_file"], str): @@ -1885,7 +1887,7 @@ def determine_regex_list_match(test_str, regex_list): return any(result) -@keep_provenance +@keep_provenance_for_recursive_function def find_variable(tree, rhs, full_config, white_or_black_list, isblacklist): raw_str = rhs if not tree[-1]: @@ -2248,7 +2250,7 @@ def determine_computer_from_hostname(): # ) -@keep_provenance +@keep_provenance_for_recursive_function def do_math_in_entry(tree, rhs, config): if not tree[-1]: tree = tree[:-1] @@ -2353,7 +2355,7 @@ def do_math_in_entry(tree, rhs, config): return convert(entry.strip(), tree) -@keep_provenance +@keep_provenance_for_recursive_function def mark_dates(tree, rhs, config): """Adds the ``DATE_MARKER`` to any entry who's key ends with ``"date"``""" if not tree[-1]: @@ -2368,7 +2370,7 @@ def mark_dates(tree, rhs, config): return entry -@keep_provenance +@keep_provenance_for_recursive_function def marked_date_to_date_object(tree, rhs, config): """Transforms a marked date string into a Date object""" if not tree[-1]: @@ -2399,7 +2401,7 @@ def marked_date_to_date_object(tree, rhs, config): return entry -@keep_provenance +@keep_provenance_for_recursive_function def unmark_dates(tree, rhs, config): """Removes the ``DATE_MARKER`` to any entry who's entry contains the ``DATE_MARKER``.""" if not tree[-1]: @@ -2411,7 +2413,7 @@ def unmark_dates(tree, rhs, config): return entry -@keep_provenance +@keep_provenance_for_recursive_function def perform_actions(tree, rhs, config): if not tree[-1]: tree = tree[:-1] @@ -2462,7 +2464,7 @@ def perform_actions(tree, rhs, config): return entry -@keep_provenance +@keep_provenance_for_recursive_function def purify_booleans(tree, rhs, config): if not tree[-1]: tree = tree[:-1] diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 43d0bfda5..3a65272d0 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -19,6 +19,16 @@ def __init__(self, provenance_data): super().__init__([provenance_data]) def append_last_step_modified_by(self, func): + """ + Copies the last step in the provenance history and adds the entry ``modify_by`` + with value ``func``. + + Parameters + ---------- + func : str + Function that is modifying the variable + """ + new_provenance_step = copy.deepcopy(self[-1]) new_provenance_step = self.add_modified_by(new_provenance_step, func) @@ -26,16 +36,54 @@ def append_last_step_modified_by(self, func): def extend_and_modified_by(self, additional_provenance, func): + """ + Extends the current provenance with an ``additional_provenance``. This happends + when for example a variable comes originally from a file, but then is + overwritten by a file higher in the hierarchy. This would keep both histories, + with the history of the second been on top of the first. + + Parameters + ---------- + additional_provenance : esm_parser.Provenance + Additional provenance history to be used for extending ``self`` + func : str + Function triggering this method + """ new_additional_provenance = copy.deepcopy(additional_provenance) + # If the new provenance is not identical to the current one extend the + # provenance if new_additional_provenance is not self: for elem in new_additional_provenance: - new_additional_provenance.add_modified_by(elem, func, modified_by="extended_by") + new_additional_provenance.add_modified_by( + elem, func, modified_by="extended_by" + ) self.extend(new_additional_provenance) + # If the new provenance is identical just mark the variable as modified_by + # func else: self.append_last_step_modified_by(func) def add_modified_by(self, provenance_step, func, modified_by="modified_by"): + """ + Adds an variable of name defined by ``modified_by`` to the given provenance step + with value ``func``. This variable is used to label provenance steps of the + provenance history with functions that modified it. + + Parameters + ---------- + provenance_step : dict + Provenance entry of the current step + func : str + Function triggering this method + modified_by : str + Name of the key for the labelling the type of modification + + Returns + ------- + provenance_step : dict + Provenance entry of the current step with the ``modified_by`` item + """ if provenance_step is not None: provenance_step[modified_by] = str(func) @@ -105,7 +153,7 @@ def wrapper_with_provenance_factory(value, provenance=None): that are not subclassable (``bool`` and ``NoneType``) intanciates an object that mimics their behaviour but also contains the ``provenance`` attribute. - Objects of type ``esm_calendari.esm_calendar.Date`` are not subclass (and the + Objects of type ``esm_calendar.esm_calendar.Date`` are not subclass (and the ``provenance`` attribute is simply added to them, because they fail to be subclassed with in the ``WrapperWithProvenance`` with the following error:: @@ -148,13 +196,29 @@ def __new__(cls, value, *args, **kwargs): return super(WrapperWithProvenance, cls).__new__(cls, value) def __init__(self, value, provenance=None): - self.provenance = Provenance(provenance) + self._provenance = Provenance(provenance) + + @property + def provenance(self): + return self._provenance + + @provenance.setter + def provenance(self, new_provenance): + # Check if new_provenance is an instance of Provenance + if not isinstance(new_provenance, Provenance): + raise ValueError( + "Provenance must be an instance of the provenance.Provenance " + "class!" + ) + + self._provenance = new_provenance # Instantiate the subclass with the given value and provenance return WrapperWithProvenance(value, provenance) class DictWithProvenance(dict): + # TODO: this is an incorrect description """ A dictionary subclass that contains a ``provenance`` attribute. This attribute is a ``dict`` that contains those `keys` of the original dictionary whose `values` @@ -220,6 +284,7 @@ class DictWithProvenance(dict): """ def __init__(self, dictionary, provenance): + # TODO: this is an incorrect description """ Instanciates the ``dictionary`` as an object of ``DictWithProvenance`` and defines its ``provenance`` attribute recursively with ``set_provenance``. @@ -235,9 +300,11 @@ def __init__(self, dictionary, provenance): super().__init__(dictionary) + self.custom_setitem = False self.put_provenance(provenance) def put_provenance(self, provenance): + # TODO: this is an incorrect description """ Defines recursively the ``provenance`` of the ``DictWithProvenance`` object ``self`` or it's nested ``dictionary``. @@ -257,13 +324,14 @@ def put_provenance(self, provenance): elif isinstance(val, list): self[key] = ListWithProvenance(val, provenance.get(key, [])) elif hasattr(val, "provenance"): - self[key].provenance.extend(provenance.get(key, None)) + self[key].provenance.extend(provenance.get(key, {})) else: self[key] = wrapper_with_provenance_factory( val, provenance.get(key, None) ) def set_provenance(self, provenance): + # TODO: this is an incorrect description """ Defines recursively the ``provenance`` of the ``DictWithProvenance`` object ``self`` or it's nested ``dictionary``. @@ -277,6 +345,9 @@ def set_provenance(self, provenance): given, the ``dictionary`` takes the value of ``self``. Only for recursion within nested ``DictWithProvenance``, do not use it outside of this method. """ + if not isinstance(provenance, list): + provenance = [provenance] + for key, val in self.items(): if isinstance(val, dict): self[key] = DictWithProvenance(val, {}) @@ -326,38 +397,45 @@ def get_provenance(self, index=-1): return provenance_dict - def set_leaf_id_provenance(self, key): + def __setitem__(self, key, val): """ - Stores the last-leaf provenance information in the class level - variable ``leaf_id_provenance``. - - This method gets the ``id`` value (unique Python object counter), which - is used as a key in the `leaf_id_provenance`. The value becomes the - provenance of that key, or defaults to ``None`` + Any time an item in a DictWithProvenance is set, extend the old provenance of + the old value with the provenance of the new ``val`` and make that be the new + extended provenance history of the value. Parameters ---------- - key : Any - The key of the "inner-most" leaf to store provenance information for + key : str + Key of the item + val : any + Value of the item """ - # If it's a leaf - if not isinstance(super().__getitem__(key), DictWithProvenance): - val_id = id(super().__getitem__(key)) - # Stores the provenance in a class variable, under an id key - DictWithProvenance.leaf_id_provenance[val_id] = self.provenance.get( - key, None - ) + # TODO: this needs to happen recursively if is a dict or a list + if ( + key in self + and not isinstance(self[key], (dict, list)) + and hasattr(self[key], "provenance") + and hasattr(self, "custom_setitem") + and self.custom_setitem + ): + new_provenance = copy.deepcopy(self[key].provenance) + if hasattr(val, "provenance"): + new_provenance.extend_and_modified_by(val.provenance, "dict.__setitem__") + val.provenance = new_provenance + + super().__setitem__(key, val) class ListWithProvenance(list): def __init__(self, mylist, provenance): super().__init__(mylist) + self.custom_setitem = False self.put_provenance(provenance) def put_provenance(self, provenance): if not provenance: - provenance = [None] * len(self) + provenance = [{}] * len(self) for c, elem in enumerate(self): if isinstance(elem, dict): @@ -370,6 +448,7 @@ def put_provenance(self, provenance): self[c] = wrapper_with_provenance_factory(elem, provenance[c]) def set_provenance(self, provenance): + # TODO: this is an incorrect description """ Defines recursively the ``provenance`` of the ``DictWithProvenance`` object ``self`` or it's nested ``dictionary``. @@ -383,6 +462,9 @@ def set_provenance(self, provenance): given, the ``dictionary`` takes the value of ``self``. Only for recursion within nested ``DictWithProvenance``, do not use it outside of this method. """ + if not isinstance(provenance, list): + provenance = [provenance] + for c, elem in enumerate(self): if isinstance(elem, dict): self[c] = DictWithProvenance(elem, {}) @@ -391,7 +473,7 @@ def set_provenance(self, provenance): self[c] = ListWithProvenance(elem, []) self[c].set_provenance(provenance) elif hasattr(elem, "provenance"): - self[c].provenance.append(provenance) + self[c].provenance.extend(provenance) else: self[c] = wrapper_with_provenance_factory(elem, provenance) @@ -432,11 +514,29 @@ def get_provenance(self, index=-1): return provenance_list + # TODO: add __setitem__ equivalent here + PROVENANCE_MAPPINGS = (DictWithProvenance, ListWithProvenance) -def keep_provenance(func): +def keep_provenance_for_dict_or_list(func): + def inner(val_with_prov, *args, **kwargs): + if hasattr(val_with_prov, "custom_setitem"): + val_with_prov.custom_setitem = True + + output = func(val_with_prov, *args, **kwargs) + + if hasattr(val_with_prov, "custom_setitem"): + val_with_prov.custom_setitem = False + + return output + + return inner + + +def keep_provenance_for_recursive_function(func): + # TODO: this is an incorrect description """ Decorator for recursive functions in ``esm_parser`` to preserve provenance. @@ -505,3 +605,5 @@ def inner(tree, rhs, *args, **kwargs): print(asd["a_string"], asd["a_string"].provenance) print(asd["list_with_dict_inside"], asd["list_with_dict_inside"]) + + # TODO: tests for the keep_provenance functions diff --git a/src/esm_runscripts/filedicts.py b/src/esm_runscripts/filedicts.py new file mode 100644 index 000000000..6c8d332ef --- /dev/null +++ b/src/esm_runscripts/filedicts.py @@ -0,0 +1,1260 @@ +""" +The file-dictionary implementation + +Developer Notes +--------------- +* Internal functions, decorators, and methods are prefixed with _. They should + only be used inside of this file. +* Decorators should have names that map to an attribute of the object. See the + example in ``_allowed_to_be_missing``. +""" +import copy +import functools +import glob +import inspect +import os +import pathlib +import shutil +import sys +from enum import Enum, auto +from typing import Any, AnyStr, Dict, Iterator + +import dpath.util +import yaml +from loguru import logger + +from esm_calendar import Date +# These should be relative +from esm_parser import ConfigSetup, user_error + +# Set up the logger: +logger.remove() +LEVEL = "ERROR" +LOGGING_FORMAT = "[{time:HH:mm:ss DD/MM/YYYY}] |{level}| [{file} -> {function}() line:{line: >3}] >> {message}" +logger.add(sys.stderr, level=LEVEL, format=LOGGING_FORMAT) + + +class DatestampFormatError(Exception): + """Raise this error when the Datestamp formatter is incorrectly used""" + + +class DotDict(dict): + """ + A dictionary subclass that allows accessing data via dot-attributes and keeps changes between dictionary + keys and dot-attributes in sync. + + This class inherits from the built-in `dict` class and overrides the `__getattr__` and `__setattr__` methods + to provide dot-attribute access to dictionary items. When an attribute is accessed using dot notation, the + corresponding dictionary key is returned. Similarly, when an attribute is set using dot notation, the corresponding + dictionary key is updated. Changes made using dictionary keys are also reflected in the dot-attributes. + + Note that this implementation assumes that the keys in the dictionary are strings, since dot-attributes can + only be strings in Python. + """ + + def __getattr__(self, attr): + try: + return self[attr] + except KeyError: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{attr}'" + ) + + def __setattr__(self, attr, value): + self[attr] = value + + +class NameIterEnum(Enum): + def __iter__(self) -> Iterator[str]: + """Returns list of names of the iteration, guarentted to be lower-case""" + return iter(str(name).lower() for name in self.__members__) + + +class FileTypes(NameIterEnum): + """Describes which type a file might belong to, e.g. input, outdata, forcing""" + + ANALYSIS = auto() + CONFIG = auto() + COUPLE = auto() + FORCING = auto() + IGNORE = auto() + INPUT = auto() + LOG = auto() + MON = auto() + OUTDATA = auto() + RESTART = auto() + VIZ = auto() + + +class FileLocations(NameIterEnum): + """Posibile locations for a file""" + + COMPUTER = auto() + EXP_TREE = auto() + RUN_TREE = auto() + WORK = auto() + + +class FileStatus(NameIterEnum): + """Describes which status a particular file might have, e.g. ``FILE``, ``NOT_EXISTS``, ``BROKEN_LINK``.""" + + FILE = auto() # ordinary file + DIR = auto() # directory + LINK = auto() # symbolic link + EXISTS = auto() # object exists in the system + NOT_EXISTS = auto() # file does not exist + BROKEN_LINK = auto() # target of the symbolic link does not exist + + +# NOTE(PG): Comment can be removed later. Here I prefix with an underscore as +# this decorator should **only** be used inside of this file. +def _allowed_to_be_missing(method): + """Allows to decorate a method with ``_allowed_to_be_missing``, causing it + to always return ``None``. + + If a method is decorated with ``@_allowed_to_be_missing``, it will return + ``None`` instead of executing if the file has a attribute of + ``_allowed_to_be_missing`` set to ``True``. You get a warning via the logger + giving the full method name that was decorated and a representation of the + file that was attempted to be moved, linked, or copied. + + Usage Example + ------------- + Given you have an instantiated simulation file under ``sim_file`` with + the following property in YAML:: + + echam: + files: + sim_file: + allowed_to_be_missing: True + ...other properties... + + >>> sim_file.allowed_to_be_missing # doctest: +SKIP + True + + And given that you have a decorated method foo, that would act on the file:: + >>> rvalue = sim_file.foo(*args, **kwargs) # doctest: +SKIP + >>> rvalue is None # doctest: +SKIP + True + >>> print(rvalue) # doctest: +SKIP + None + + Programming Example + ------------------- + class MyCoolClass: + def __init__(self): + self.allowed_to_be_missing = True + + @_allowed_to_be_missing + def foo(self, *args, **kwargs): + # This method will always return None, the return below is never + # reached: + return 123 + + Notes + ----- + Why does this thing have an underscore and the attribute does not? + + Because this is a decorator to enable the functionality, and I do not want + anyone to use this decorator outside of this file, so, we start with ``_`` + to denote that ("Private", even though Python does not formally have that). + + And, the attribute might be interesting for the end-user, not the programmer. + + That's why. + """ + + @functools.wraps(method) + def inner_method(self, *args, **kwargs): + if self.allowed_to_be_missing: + try: + return method(self, *args, **kwargs) + except (FileNotFoundError, IOError): + logger.warning( + f"Skipping {method.__qualname__} as this file ({self}) is allowed to be missing!" + ) + # None is the default return, but let us be explicit here, as it is a bit confusing + return None + else: + return method(self, *args, **kwargs) + + return inner_method + + +def _fname_has_date_stamp_info(fname, date, reqs=["%Y", "%m", "%d"]): + """ + Checks if a particular file has all elements of a particular date in its name. + + Parameters + ---------- + fname : str + The name of the file to check + date : esm_calendar.Date + The date to be checked against + reqs : list of str + A list of ``strftime`` compliant strings to determine which elements of + the date to check. Compatible with %Y %m %d %H %M %S (year, month, day, + hour, minute, second) + + Returns + ------- + bool : + True if all elements appear in the filename, False otherwise. + + """ + date_attrs = { + "%Y": "syear", + "%m": "smonth", + "%d": "sday", + "%H": "shour", + "%M": "sminute", + "%S": "ssecond", + } + required_attrs = [getattr(date, v) for k, v in date_attrs.items() if k in reqs] + # all(attr in fname for attr in required_attrs) + for attr in required_attrs: + if attr in fname: + fname = fname.replace(attr, "checked", 1) + return fname.count("checked") == len(reqs) + + +def _globbing(method): + """ + Decorator method for ``SimulationFile``'s methods ``cp``, ``mv``, ``ln``, that + enables globbing. If a ``*`` is found on the ``source`` or ``target`` the globbing + logic is activated, and consist of: + - run checks for globbing syntax + - check if any file matches the globbing pattern + - construct one instance of ``SimulationFile`` for each file matching the globbing + - run the ``method`` for that particular file + + Parameters + ---------- + method : method + The decorated method (``cp``, ``mv``, ``ln``) + + Returns + ------- + method : method + If no globbing is needed, returns the method as it was given originally. + """ + + @functools.wraps(method) + def inner_method(self, source, target, *args, **kwargs): + method_name = method.__name__ + source_name = self[f"name_in_{source}"] + target_name = self[f"name_in_{target}"] + + if "*" in source_name or "*" in target_name: + # Get wildcard patterns + source_pattern = source_name.split("*") + target_pattern = target_name.split("*") + + # Check wild cards syntax + self._wild_card_check(source_pattern, target_pattern) + + # Obtain source files + glob_source_paths = self._find_globbing_files(source) + + # Extract globbing source names + glob_source_names = [ + pathlib.Path(glob_source_path).name + for glob_source_path in glob_source_paths + ] + + # Solve the globbing target names + glob_target_names = [] + for glob_source_name in glob_source_names: + glob_target_name = glob_source_name + for sp, tp in zip(source_pattern, target_pattern): + glob_target_name = glob_target_name.replace(sp, tp) + glob_target_names.append(glob_target_name) + + # Loop through source files + for glob_source_name, glob_target_name in zip( + glob_source_names, glob_target_names + ): + # Create a new simulation file object for this specific glob file's config + glob_dict = dict(self) + glob_dict[f"name_in_{source}"] = glob_source_name + glob_dict[f"name_in_{target}"] = glob_target_name + glob_file = SimulationFile(**glob_dict) + # Use method + this_method = getattr(glob_file, method_name) + return this_method(source, target, *args, **kwargs) + else: + return method(self, source, target, *args, **kwargs) + + return inner_method + + +class SimulationFile(DotDict): + """ + Describes a file used within a ESM Simulation. + + A ``SimulationFile`` object describes one particular file used within an + ``esm-tools`` run. This description is similar to a standard Python + dictionary. Beyond the standard dictionary methods and attributes, there + are a variety of attributes that describe how the file should behave, as + well as a few additional methods you can use to relocate the file around on + the system. Please see the detailed documentation on each of the methods + for more specifics, but in summary, a ``SimulationFile`` has the following + additional functions:: + + >>> sim_file = SimulationFile(...) # doctest: +SKIP + >>> sim_file.mv("computer", "work") # doctest: +SKIP + >>> sim_file.ln("work", "run_tree") # doctest: +SKIP + >>> sim_file.cp("run_tree", "exp_tree") # doctest: +SKIP + + You get extra functions for moving, copying, or linking a file from one + location to another. Location keys are desccribed in detail in the Notes + section. + + Furthermore, there are a few attributes that you should be aware of. These + include: + + * ``name`` : A human readable name for the file. + * ``allowed_to_be_missing`` : A ``bool`` value to set a certain file as + allowed to be missing or not. In case it is, the cp/ln/mv command will not + fail if the original file is not found. + * ``datestamp_method`` : Sets how a datestamp should be added. See + ``_allowed_datestamp_methods`` for more information. + * ``datestamp_format`` : Sets how a datestamp should be formatted. See + ``_allowed_datestamp_methods`` for more information. + + Example + ------- + Given a config, you should be able to use this in YAML:: + + $ cat dummy_config.yaml + + echam: + files: + jan_surf: + name_in_computer: T63CORE2_jan_surf.nc + name_in_work: unit.24 + filetype: NetCDF + allowed_to_be_missing: True + description: > + Initial values used for the simulation, including + properties such as geopotential, temperature, pressure + + And, assuming config is as described above:: + + >>> sim_file = SimulationFile.from_config(config, 'echam.files.jan_surf') # doctest: +SKIP + + You could then copy the file to the experiment folder:: + + >>> sim_file.cp("pool", "work") # doctest: +SKIP + + Notes + ----- + A file can be located in one of these categories (``LOCATION_KEYS``): + - computer: pool/source directory (for input files) + - exp_tree: file in the category directory in experiment directory (eg. input, output, ...) + - run_tree: file in the experiment/run_// directory + - work: file in the current work directory. Eg. experiment/run_/work/ + + LOCATION_KEY is one of the strings defined in LOCATION_KEY list + - name_in_ : file name (without path) in the LOCATION_KEY + - eg. name_in_computer: T63CORE2_jan_surf.nc + - eg. name_in_work: unit.24 + - absolute_path_in_ : absolute path in the LOCATION_KEY + - eg. absolute_path_in_run_tree: /work/ollie/pgierz/some_exp/run_20010101-20010101/input/echam/T63CORE2_jan_surf.nc + """ + + # Should all be replaced by Enums: + input_file_kinds = [ + "config", + "forcing", + "input", + ] + output_file_kinds = [ + "analysis", + "couple", + "log", + "mon", + "outdata", + "restart", + "viz", + "ignore", + ] + all_model_filekinds = ( + input_file_kinds + output_file_kinds + ["src"] + ) # FIXME: In review, someone should check this + + def __init__( + self, + name="", + component="", + paths={}, + kind=None, + allowed_to_be_missing=False, + description="", + filetype="", + datestamp_method="avoid_overwrite", + **kwargs, + ): + # self.name = name + # self.paths = paths + # self.kind = kind + # self.allowed_to_be_missing = allowed_to_be_missing + # self.description = description + # self.filetype = filetype + # self._datestamp_method = datestamp_method + # self.locations = {k: v.parent for k, v in self.paths.items()} + + super().__init__( + name=name, + component=component, + paths={k: pathlib.Path(v) for k, v in paths.items()}, + kind=kind, + allowed_to_be_missing=allowed_to_be_missing, + description=description, + filetype=filetype, + datestamp_method=datestamp_method, + locations={k: pathlib.Path(v).parent for k, v in paths.items()}, + **kwargs, + ) + + for location, path in paths.items(): + for attr_name, attr_value in { + f"absolute_path_in_{location}": path.resolve(), + f"name_in_{location}": path.name, + }.items(): + if attr_name not in self: + self[attr_name] = attr_value + + # possible paths for files: + + # location_keys = ["computer", "exp_tree", "run_tree", "work"] + # initialize the locations and complete paths for all possible locations + # self.locations = dict.fromkeys(location_keys, None) + + # Current Attributes: + # {'absolute_path_in_computer': PosixPath('/work/ollie/pool/ECHAM/T63/T63CORE2_jan_surf.nc'), + # 'absolute_path_in_exp_tree': PosixPath('/work/ollie/pgierz/some_exp/input/echam/T63CORE2_jan_surf.nc'), + # 'absolute_path_in_run_tree': PosixPath('/work/ollie/pgierz/some_exp/run_20000101-20000101/input/echam/T63CORE2_jan_surf.nc'), + # 'absolute_path_in_work': PosixPath('/work/ollie/pgierz/some_exp/run_20010101-20010101/work/unit.24'), + # 'allowed_to_be_missing': False, + # 'description': 'Initial values used for the simulation, including properties such as geopotential, temperature, pressure\n', + # 'filetype': 'NetCDF', + # 'name_in_computer': 'T63CORE2_jan_surf.nc', + # 'name_in_exp_tree': 'T63CORE2_jan_surf.nc', + # 'name_in_run_tree': 'T63CORE2_jan_surf.nc', + # 'name_in_work': 'unit.24', + # 'path_in_computer': '/work/ollie/pool/ECHAM/T63', + # 'type': 'input'} + + ############################################################################################## + # Initialize from esm-tools config + ############################################################################################## + + @classmethod + def from_config(cls, full_config: dict, attrs_address: str): + """ + - Initiates the properties of the object + - Triggers basic checks + + Parameters + ---------- + full_config : dict + The full simulation configuration + attrs_address : str + The address of this specific file in the full config, separated by dots. + + Note + ---- + A file can be located in one of these categories (``LOCATION_KEYS``): + - computer: pool/source directory (for input files) + - exp_tree: file in the category directory in experiment directory (eg. input, output, ...) + - run_tree: file in the experiment/run_// directory + - work: file in the current work directory. Eg. experiment/run_/work/ + + LOCATION_KEY is one of the strings defined in LOCATION_KEY list + - name_in : file name (without path) in the LOCATION_KEY + - eg. name_in_computer: T63CORE2_jan_surf.nc + - eg. name_in_work: unit.24 + - absolute_path_in_ : absolute path in the LOCATION_KEY + - eg. absolute_path_in_run_tree: + - /work/ollie/pgierz/some_exp/run_20010101-20010101/input/echam/T63CORE2_jan_surf.nc + """ + attrs_dict = dpath.util.get( + full_config, attrs_address, separator=".", default={} + ) + # _original_filedict = copy.deepcopy(attrs_dict) + + name = attrs_address.split(".")[-1] + component = attrs_address.split(".")[0] + # Check if attr dict gives a sufficient representation of a file + cls._check_config_syntax(attrs_dict, name) + kind = attrs_dict.get("kind") + # Complete tree names if not defined by the user + names = cls._complete_file_names(attrs_dict) + paths = cls._resolve_abs_paths(full_config, component, attrs_dict, names, kind) + obj = cls(name=name, paths=paths, **attrs_dict) + + # Verbose set to true by default, for now at least + obj._verbose = full_config.get("general", {}).get("verbose", True) + + # Checks + obj._check_path_in_computer_is_abs(paths, component, name) + return obj + + @classmethod + def _check_config_syntax(cls, cfg, name) -> None: + """ + Checks for missing variables: + - ``kind`` + - ``path_in_computer`` if the file it an input for the experiment + - ``name_in_computer`` if the file it an input for the experiment + - ``name_in_work`` if the file it an output of the experiment + + It also checks whether ``kind``'s value is correct. + + It notifies the user about these errors in the syntax using + ``esm_parser.error``. + """ + error_text = "" + missing_vars = "" + kinds_text = ", ".join(cls.all_model_filekinds) + this_filedict = copy.deepcopy(cfg) + + if "kind" not in cfg.keys(): + error_text = ( + f"{error_text}" + f"- the ``kind`` variable is missing. Please define a ``kind`` " + f"({kinds_text})\n" + ) + missing_vars = ( + f"{missing_vars} ``kind``: forcing/input/restart/outdata/...\n" + ) + elif cfg["kind"] not in cls.all_model_filekinds: + error_text = ( + f"{error_text}" + f"- ``{cfg['kind']}`` is not a supported ``kind`` " + f"(``files.{name}.kind``), please choose one of the following " + f"kinds: {kinds_text}\n" + ) + this_filedict["kind"] = f"``{this_filedict['kind']}``" + + if ( + "path_in_computer" not in cfg.keys() + and cfg.get("kind") in cls.input_file_kinds + ): + error_text = ( + f"{error_text}" + f"- the ``path_in_computer`` variable is missing. Please define a " + f"``path_in_computer`` (i.e. the path to the file excluding its name)." + f" NOTE: this is only required for {', '.join(cls.input_file_kinds)} file " + f"kinds\n" + ) + missing_vars = ( + f"{missing_vars} ``path_in_computer``: \n" + ) + + if ( + "name_in_computer" not in cfg.keys() + and cfg.get("kind") in cls.input_file_kinds + ): + error_text = ( + f"{error_text}" + f"- the ``name_in_computer`` variable is missing. Please define a ``name_in_computer`` " + f"(i.e. name of the file in the work folder). NOTE: this is only required for " + f"{', '.join(cls.input_file_kinds)} file kinds\n" + ) + missing_vars = f"{missing_vars} ``name_in_computer``: \n" + + if ( + "name_in_work" not in cfg.keys() + and cfg.get("kind") in cls.output_file_kinds + ): + error_text = ( + f"{error_text}" + f"- the ``name_in_work`` variable is missing. Please define a ``name_in_work`` " + f"(i.e. name of the file in the work folder). NOTE: this is only required for " + f"{', '.join(cls.output_file_kinds)} file kinds\n" + ) + missing_vars = ( + f"{missing_vars} ``name_in_work``: \n" + ) + + missing_vars = ( + f"Please, complete/correct the following vars for your file:\n\n" + f"{_pretty_filedict(name, this_filedict)}" + f"{missing_vars}" + ) + + if error_text: + error_text = ( + f"The file dictionary ``{name}`` is missing relevant information " + f"or is incorrect:\n{error_text}" + ) + user_error("File Dictionaries", f"{error_text}\n{missing_vars}") + + @classmethod + def _complete_file_names(cls, cfg): + """ + Complete missing names in the file with the default name, depending whether + the file is of kind ``input`` or ``output``. + """ + if cfg["kind"] in cls.input_file_kinds: + default_name = cfg["name_in_computer"] + elif cfg["kind"] in cls.output_file_kinds: + default_name = cfg["name_in_work"] + else: + raise TypeError(f"Unknown file kind: {cfg['kind']}") + names = {} + names["computer"] = cfg.get("name_in_computer", default_name) + names["run_tree"] = cfg.get("name_in_run_tree", default_name) + names["exp_tree"] = cfg.get("name_in_exp_tree", default_name) + names["work"] = cfg.get("name_in_work", default_name) + return names + + @staticmethod + def _resolve_abs_paths(config, component, attrs_dict, names, kind) -> Dict: + # NOTE(PG): I....hate this! :-( + """ + Builds the absolute paths of the file for the different locations + (``computer``, ``work``, ``exp_tree``, ``run_tree``) using the information + about the experiment paths in ``config`` and the + ``self["path_in_computer"]``. + + It defines these new variables in the ``SimulationFile`` dictionary: + - ``self["absolute_path_in_work"]`` + - ``self["absolute_path_in_computer"]`` + - ``self["absolute_path_in_run_tree"]`` + - ``self["absolute_path_in_exp_tree"]`` + """ + locations = { + "work": pathlib.Path(config["general"]["thisrun_work_dir"]), + "computer": pathlib.Path(attrs_dict.get("path_in_computer", "/dev/null")), + "exp_tree": pathlib.Path(config[component][f"experiment_{kind}_dir"]), + "run_tree": pathlib.Path(config[component][f"thisrun_{kind}_dir"]), + } + + return {key: path.joinpath(names[key]) for key, path in locations.items()} + + @staticmethod + def _check_path_in_computer_is_abs(paths, component, name): + if paths["computer"] is not None and not paths["computer"].is_absolute(): + user_error( + "File Dictionaries", + "The path defined for " + f"``{component}.files.{name}.path_in_computer`` is not " + f"absolute (``{paths['computer']}``). Please, always define an " + "absolute path for the ``path_in_computer`` variable.", + ) + + ############################################################################################## + # Overrides of standard dict methods + ############################################################################################## + + def __setattr__(self, name: str, value: Any) -> None: + """Checks when changing dot attributes for disallowed values""" + if name == "datestamp_format": + self._check_datestamp_format_is_allowed(value) + if name == "datestamp_method": + self._check_datestamp_method_is_allowed(value) + return super().__setattr__(name, value) + + def __setitem__(self, key: Any, value: Any) -> None: + """Checks for changing with sim_file['my_key'] = 'new_value'""" + if key == "datestamp_format": + self._check_datestamp_format_is_allowed(value) + if key == "datestamp_method": + self._check_datestamp_method_is_allowed(value) + return super().__setitem__(key, value) + + def update(self, *args, **kwargs): + """ + Standard dictionary update method, enhanced by additional safe-guards + for particular values. + """ + for k, v in dict(*args, **kwargs).items(): + if k == "datestamp_format": + self._check_datestamp_format_is_allowed(v) + if k == "datestamp_method": + self._check_datestamp_method_is_allowed(v) + self[k] = v + + ############################################################################################## + + ############################################################################################## + # Object Properties + ############################################################################################## + + @property + def datestamp_method(self): + """ + Defines which datestamp_method shall be used when possibly including + date stamps to the file. Valid choices are "never", "always", + "avoid_overwrite". + """ + return self._datestamp_method + + @datestamp_method.setter + def datestamp_method(self, new_attr_value): + """ + Sets a new value for datestamp method. + """ + # NOTE(PG): The checks could go here + self._datestamp_method = new_attr_value + + @property + def datestamp_format(self): + """ + Defines which datestamp_format shall be used when possibly including + date stamps to the file. Valid choices are "check_from_filename" and + "append". + """ + datestamp_format = self.get( + "datestamp_format", "append" + ) # This is the old default behaviour + return datestamp_format + + ############################################################################################## + # Main Methods + ############################################################################################## + @_globbing + @_allowed_to_be_missing + def cp(self, source: str, target: str) -> None: + """ + Copies the source file or folder to the target path. It changes the name of the + target if ``self["name_in_"]`` differs from ``self["name_in_"]. + + Parameters + ---------- + source : str + String specifying one of the following options: ``"computer"``, ``"work"``, + ``"exp_tree"``, ``run_tree`` + target : str + String specifying one of the following options: ``"computer"``, ``"work"``, + ``"exp_tree"``, ``run_tree`` + """ + if source not in self.locations: + raise ValueError( + f"Source is incorrectly defined, and needs to be in {self.locations}" + ) + if target not in self.locations: + raise ValueError( + f"Target is incorrectly defined, and needs to be in {self.locations}" + ) + source_path = self[f"absolute_path_in_{source}"] + target_path = self[f"absolute_path_in_{target}"] + + # Datestamps + if self.datestamp_method == "always": + target_path = self._always_datestamp(target_path) + if self.datestamp_method == "avoid_overwrite": + target_path = self._avoid_override_datestamp(target_path) + + # General Checks + # TODO (deniz): need to add higher level exception handler (eg. user_error) + self._check_source_and_target(source_path, target_path) + + # Actual copy + source_path_type = self._path_type(source_path) + if source_path_type == FileStatus.DIR: + copy_func = shutil.copytree + else: + copy_func = shutil.copy2 + try: + copy_func(source_path, target_path) + logger.success(f"Copied {source_path} --> {target_path}") + except IOError as error: + raise IOError( + f"Unable to copy {source_path} to {target_path}\n\n" + f"Exception details:\n{error}" + ) + + @_globbing + @_allowed_to_be_missing + def ln(self, source: AnyStr, target: AnyStr) -> None: + """creates symbolic links from the path retrieved by ``source`` to the one by ``target``. + + Parameters + ---------- + source : str + key to retrieve the source from the file dictionary. Possible options: ``computer``, ``work``, ``exp_tree``, ``run_tree`` + + target : str + key to retrieve the target from the file dictionary. Possible options: ``computer``, ``work``, ``exp_tree``, ``run_tree`` + + Returns + ------- + None + + Raises + ------ + FileNotFoundError + - Source path does not exist + OSError + - Target path is a directory + - Symbolic link is trying to link to itself + - Target path does not exist + FileExistsError + - Target path already exists + """ + if source not in self.locations: + raise ValueError( + f"Source is incorrectly defined, and needs to be in {self.locations}" + ) + if target not in self.locations: + raise ValueError( + f"Target is incorrectly defined, and needs to be in {self.locations}" + ) + # full paths: directory path / file name + source_path = self[f"absolute_path_in_{source}"] + target_path = self[f"absolute_path_in_{target}"] + + # Datestamps + if self.datestamp_method == "always": + target_path = self._always_datestamp(target_path) + if self.datestamp_method == "avoid_overwrite": + target_path = self._avoid_override_datestamp(target_path) + # General Checks + # TODO (deniz): need to add higher level exception handler (eg. user_error) + self._check_source_and_target(source_path, target_path) + + try: + os.symlink(source_path, target_path) + except IOError as error: + raise IOError( + f"Unable to link {source_path} to {target_path}\n\n" + f"Exception details:\n{error}" + ) + + @_globbing + @_allowed_to_be_missing + def mv(self, source: str, target: str) -> None: + """ + Moves (renames) the SimulationFile from it's location in ``source`` to + it's location in ``target``. + + Parameters + ---------- + source : str + One of ``"computer"``, ``"work"``, ``"exp_tree"``, "``run_tree``" + target : str + One of ``"computer"``, ``"work"``, ``"exp_tree"``, "``run_tree``" + """ + if source not in self.locations: + raise ValueError( + f"Source is incorrectly defined, and needs to be in {self.locations}" + ) + if target not in self.locations: + raise ValueError( + f"Target is incorrectly defined, and needs to be in {self.locations}" + ) + source_path = self[f"absolute_path_in_{source}"] + target_path = self[f"absolute_path_in_{target}"] + + # Datestamps + if self.datestamp_method == "always": + target_path = self._always_datestamp(target_path) + if self.datestamp_method == "avoid_overwrite": + target_path = self._avoid_override_datestamp(target_path) + # General Checks + # TODO (deniz): need to add higher level exception handler (eg. user_error) + self._check_source_and_target(source_path, target_path) + + # Perform the movement: + try: + source_path.rename(target_path) + logger.success(f"Moved {source_path} --> {target_path}") + except IOError as error: + raise IOError( + f"Unable to move {source_path} to {target_path}\n\n" + f"Exception details:\n{error}" + ) + + _allowed_datestamp_methods = {"never", "always", "avoid_overwrite"} + """ + Set containing the allowed datestamp methods which can be chosen from. + + Notes on possible datestamp methods + ----------------------------------- + never : str + This will never add a datestamp to a file. **WARNING** this will + cause you to possibly overwrite files. + always : str + This will always add a datestamp to a file, even if the canonical + target name would not suggest one. + avoid_overwrite : str + This will add a datestamp at the end of the file, if the during the + mv/cp/ln operation the file would be identically named. + """ + + _allowed_datestamp_formats = {"check_from_filename", "append"} + """ + Set containing the allowed datestamp formats which can be chosen from. + + Notes on possible datestamp formats + ----------------------------------- + check_from_filename : str + This option will add a datestamp to a file, if the year, month, and day + cannot be extracted from the standard declared filename. + append : str + This will add a datestamp at the end of the file, regardless of if it + can be extracted from the file or not. + """ + + def _check_datestamp_method_is_allowed(self, datestamp_method): + """ + Ensures that the datestamp method is in the defined valid set. + """ + if datestamp_method not in self._allowed_datestamp_methods: + raise ValueError( + "The datestamp_method must be defined as one of never, always, or avoid_overwrite" + ) + + def _check_datestamp_format_is_allowed(self, datestamp_format): + """ + Ensures that the datestamp format is in the defined valid set. + """ + if datestamp_format not in self._allowed_datestamp_formats: + raise ValueError( + "The datestamp_format must be defined as one of check_from_filename or append" + ) + + def _path_type(self, path: pathlib.Path) -> FileStatus: + """ + Checks if the given ``path`` exists. If it does returns it's type, if it + doesn't, returns ``None``. + + Parameters + ---------- + path : pathlib.Path + Path to be checked. + + Returns + ------- + Enum value + One of the values from FileType enumeration + + Raises + ------ + TypeError + - when ``path`` has incompatible type + - when ``path`` is not identified + """ + if not isinstance(path, (str, pathlib.Path)): + datatype = type(path).__name__ + raise TypeError( + f"Path ``{path}`` has an incompatible datatype ``{datatype}``. str or pathlib.Path is expected" + ) + + path = pathlib.Path(path) + + # NOTE: is_symlink() needs to come first because it is also a is_file() + # NOTE: pathlib.Path().exists() also checks is the target of a symbolic link exists or not + if path.is_symlink() and not path.exists(): + return FileStatus.BROKEN_LINK + elif not path.exists(): + return FileStatus.NOT_EXISTS + elif path.is_symlink(): + return FileStatus.LINK + elif path.is_file(): + return FileStatus.FILE + elif path.is_dir(): + return FileStatus.DIR + else: + # probably, this will not happen + raise TypeError(f"{path} can not be identified") + + def _always_datestamp(self, fname) -> pathlib.Path: + """ + Method called when ``always`` is the ``datestamp_method. + + Appends the datestamp in any case if ``datestamp_format`` is + ``append``. Appends the datestamp only if it is not obviously in the + filename if the ``datestamp_format`` is ``check_from_filename``. Only + appends to files or links, not directories. + + Parameters + ---------- + fname : pathlib.Path + The file who's name should be modified. + + Returns + ------- + pathlib.Path + A modified file with an added date stamp. + """ + if fname.is_dir(): + return fname + if self.datestamp_format == "append": + return pathlib.Path(f"{fname}_{self._sim_date}") + if self.datestamp_format == "check_from_filename": + if _fname_has_date_stamp_info(fname, self._sim_date): + return fname + else: + return pathlib.Path(f"{fname}_{self._sim_date}") + raise DatestampFormatError( + "Unknown Datestamp formatting type, please use `append` or `check_from_filename`" + ) + + def _avoid_override_datestamp(self, target: pathlib.Path) -> pathlib.Path: + """ + If source and target are identical, adds the date stamp to the target. + + This method is used in the case that the object's attribute + ``datestamp_method`` is set to ``avoid_overwrite``, and is called + before the checks of each of ln, cp, and mv. + + Parameters + ---------- + target : pathlib.Path + + Returns + ------- + pathlib.Path : + The new target that can be used + """ + if target.exists() and not target.is_dir(): + if self.datestamp_format == "append": + target = pathlib.Path(f"{target}_{self._sim_date}") + # The other case ("check_from_filename") is meaningless? + return target + + @staticmethod + def _wild_card_check(source_pattern: list, target_pattern: list) -> bool: + """ + Checks for syntax mistakes. If any were found, it notifies the user about these + errors in the syntax using ``esm_parser.error``. + + Parameters + ---------- + source_pattern : list + A list including the different pieces of the source name pattern + target_pattern : list + A list including the different pieces of the target name pattern + + Returns + ------- + bool : + If no issues were found + """ + target_and_source_patterns_match = len(target_pattern) == len(source_pattern) + if not target_and_source_patterns_match: + user_error( + "Wild card", + ( + "The wild card pattern of the source " + + f"``{source_pattern}`` does not match with the " + + f"target ``{target_pattern}``. Make sure the " + + f"that the number of ``*`` are the same in both " + + f"sources and targets." + ), + ) + + return target_and_source_patterns_match + + def _find_globbing_files(self, location: str) -> list: + """ + Lists the files matching the globbing path of the given ``location``, and + notifies the user if none were found, via ``esm_parser.user_error``. + + Parameters + ---------- + location : str + The location string (``work``, ``computer``, ``exp_tree``, ``run_tree``) + + Returns + ------- + glob_paths : list + List of paths found matching the globbing case for the ``location`` pattern + """ + absolute_path_in_location = str(self[f"absolute_path_in_{location}"]) + glob_paths = glob.glob(absolute_path_in_location) + + # Check that there are any source files available + if len(glob_paths) == 0: + user_error( + "Globbing", + f"No files found for the globbing pattern " + f"``{absolute_path_in_location}``.", + ) + + return glob_paths + + def _check_source_and_target( + self, source_path: pathlib.Path, target_path: pathlib.Path + ) -> None: + """ + Performs common checks for file movements + + Parameters + ---------- + source_path : pathlib.Path + path of the file to be copied / linked / moved + + target_path : pathlib.Path + path of the file to be generated + + Returns + ------- + True + + Raises + ------ + Exception + - If the ``source_path`` does not exist + - If the ``target_path`` exists + - If the parent dir of the ``target_path`` does not exist + """ + # Types. Eg. file, dir, link, or None + source_path_type = self._path_type(source_path) + target_path_type = self._path_type(target_path) + + # Checks + # ------ + # Source does not exist + if source_path_type == FileStatus.NOT_EXISTS: + err_msg = f"Unable to perform file operation. Source ``{source_path}`` does not exist!" + raise FileNotFoundError(err_msg) + + # Target already exists + target_exists = ( + os.path.exists(target_path) or target_path_type == FileStatus.LINK + ) + if target_exists: + err_msg = f"Unable to perform file operation. Target ``{target_path}`` already exists" + raise FileExistsError(err_msg) + + # Target parent directory does not exist + if not target_path.parent.exists(): + # TODO: we might consider creating it (Miguel) + err_msg = f"Unable to perform file operation. Parent directory of the target ``{target_path}`` does not exist" + raise FileNotFoundError(err_msg) + + # if source is a broken link. Ie. pointing to a non-existing file + if source_path_type == FileStatus.BROKEN_LINK: + err_msg = f"Unable to create symbolic link: ``{source_path}`` points to a broken path: {source_path.resolve()}" + raise FileNotFoundError(err_msg) + + +class DatedSimulationFile(SimulationFile): + """A SimultionFile which also needs to know about dates""" + + def __init__( + self, + date=Date("2000-01-01"), + **kwargs, + ): + super().__init__(**kwargs) + self._sim_date = date + + @classmethod + def from_config(cls, full_config: dict, attrs_address: str, date: Date): + obj = super().from_config(full_config, attrs_address) + obj._sim_date = date + return obj + + +def _pretty_filedict(name, filedict): + """ + Returns a string in yaml format of the given file dictionary. + + Parameters + ---------- + dict + A file dictionary + + Returns + ------- + str + A string in yaml format of the given file dictionary + """ + return yaml.dump({"files": {name: filedict}}) + + +def copy_files(config): + """Copies files""" + # PG: No. We do not want this kind of general function. This is just to + # demonstrate how the test would work + return config + + +class SimulationFileCollection(dict): + """ + Once instanciated, searches in the ``config`` dictionary for the ``files`` keys. + This class contains the methods to: 1) instanciate each of the files defined in + ``files`` as ``SimulationFile`` objects and 2) loop through these objects + triggering the desire file movement. + """ + + def __init__(self): + pass + + # PG: Not sure I need this... + @property + def _defined_from(self): + stack = inspect.stack() + caller_frame = stack[1] # Get the frame of the caller + caller_name = caller_frame.function + return caller_name + + @classmethod + def from_config(cls, config: dict): + sim_files = cls() + for component in config["general"]["valid_model_names"]: + config_address = f"{component}.files" + for file_key in dpath.util.get( + config, config_address, separator="." + ).keys(): + sim_files[file_key] = SimulationFile.from_config( + config, f"{config_address}.{file_key}" + ) + return sim_files + + def _gather_file_movements(self) -> None: + """Puts the methods for each file movement into the dictionary as callable values behind the `_filesystem_op` key""" + for sim_file_id, sim_file_obj in self.items(): + movement_type = sim_file_obj.get("movement_type", "cp") + if movement_type == "mv": + self[sim_file_id]["_filesystem_op"] = getattr(sim_file_obj, "mv") + elif movement_type == "cp": + self[sim_file_id]["_filesystem_op"] = getattr(sim_file_obj, "cp") + elif movement_type == "ln": + self[sim_file_id]["_filesystem_op"] = getattr(sim_file_obj, "ln") + else: + raise ValueError( + f"Movement Type is not defined correctly, please use `mv`, `cp` or `ln` for {sim_file_id}" + ) + + def execute_filesystem_operation( + self, config: ConfigSetup + ) -> ConfigSetup: # , from: pathlib.Path | str, to: pathlib.Path | str) -> None: + self._gather_file_movements() + for sim_file_id, sim_file_obj in self.items(): + logger.info(f"Processing {sim_file_id}") + if config["general"]["jobtype"] == "prepcompute": + src, dest = "pool", "work" + elif config["general"]["jobtype"] == "tidy": + src, dest = "work", "exp_tree" + else: + raise ValueError(f"Incorrect jobtype specified for {sim_file_obj}") + sim_file_obj["_filesystem_op"](src, dest) + return config + + +def resolve_file_movements(config: ConfigSetup) -> ConfigSetup: + """ + Runs all methods required to get files into their correct locations. This will + instantiate the ``SimulationFiles`` class. It's called by the recipe manager. + + Parameters + ---------- + config : ConfigSetup + The complete simulation configuration. + + Returns + ------- + config : ConfigSetup + The complete simulation configuration, potentially modified. + """ + sim_file_collection = SimulationFileCollection.from_config(config) + config = sim_file_collection.execute_filesystem_operation(config) + return config diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 91209be65..84118b277 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -3,30 +3,44 @@ """ import os import pathlib +import pytest + import esm_parser.provenance as provenance import esm_parser -from esm_parser import yaml_to_dict - -EXAMPLE_PATH1 = f"{os.path.dirname(__file__)}/example.yaml" -EXAMPLE_PATH2 = f"{os.path.dirname(__file__)}/example2.yaml" -config = yaml_to_dict.yaml_file_to_dict(EXAMPLE_PATH2) +from esm_parser import yaml_to_dict -check_provenance = {'echam': - {'type': - {'line': 2, 'col': 11, 'yaml_file': EXAMPLE_PATH2, 'category': 'runscript'}, - 'files': { - 'greenhouse': { - 'kind': {'line': 5, 'col': 19, 'yaml_file': EXAMPLE_PATH2, 'category': 'runscript'}, - 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': EXAMPLE_PATH2, 'category': 'runscript'} +@pytest.fixture() +def example_path1(): + return f"{os.path.dirname(__file__)}/example.yaml" + +@pytest.fixture() +def example_path2(): + return f"{os.path.dirname(__file__)}/example2.yaml" + +@pytest.fixture() +def config(example_path2): + return yaml_to_dict.yaml_file_to_dict(example_path2) + +@pytest.fixture() +def check_provenance(example_path2): + check_provenance = {'echam': + {'type': + {'line': 2, 'col': 11, 'yaml_file': example_path2, 'category': 'runscript'}, + 'files': { + 'greenhouse': { + 'kind': {'line': 5, 'col': 19, 'yaml_file': example_path2, 'category': 'runscript'}, + 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': example_path2, 'category': 'runscript'} + } } - } - }, - 'debug_info': {'loaded_from_file': None} -} + }, + 'debug_info': {'loaded_from_file': None} + } + + return check_provenance -def test_get_provenance_from_yaml_to_dict(): +def test_get_provenance_from_yaml_to_dict(config, check_provenance): """ Test 1: Checks for correct provenance entries from example2.yaml file. """ @@ -34,7 +48,7 @@ def test_get_provenance_from_yaml_to_dict(): assert config.get_provenance() == check_provenance -def test_get_provenance_of_added_entry(): +def test_get_provenance_of_added_entry(config, check_provenance): """ Test 2: Checks whether the provenance of an added config entry is None. """ @@ -44,7 +58,7 @@ def test_get_provenance_of_added_entry(): assert config.get_provenance() == check_provenance -def test_get_provenance_of_added_nested_entry(): +def test_get_provenance_of_added_nested_entry(config, check_provenance): """ Test 3: Checks whether the provenance of an added nested config entry is None. """ @@ -54,7 +68,7 @@ def test_get_provenance_of_added_nested_entry(): assert config.get_provenance() == check_provenance -def test_get_provenance_of_added_nested_entry_2(): +def test_get_provenance_of_added_nested_entry_2(config, check_provenance): """ Test 4: Checks whether the provenance of an added nested config entry is None. """ @@ -64,7 +78,7 @@ def test_get_provenance_of_added_nested_entry_2(): assert config.get_provenance() == check_provenance -def test_set_provenance_for_leaf(): +def test_set_provenance_for_leaf(config, check_provenance): """ Test 5: Reset the provenance of an ``echam`` leave. @@ -77,11 +91,10 @@ def test_set_provenance_for_leaf(): check_provenance["echam"]["type"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["kind"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["path_in_computer"] = new_prov - check_provenance["echam"]["test1"] = new_prov assert config.get_provenance() == check_provenance -def test_set_provenance_for_leaf_of_new_branch(): +def test_set_provenance_for_leaf_of_new_branch(config, check_provenance): """ Test 6: Reset the provenance of leaves for an later added branch ``debug_info``. """ @@ -92,7 +105,7 @@ def test_set_provenance_for_leaf_of_new_branch(): assert config.get_provenance() == check_provenance -def test_set_provenance_for_leaf_to_a_string(): +def test_set_provenance_for_leaf_to_a_string(config, check_provenance): """ Test 7: Reset the provenance of all ``echam`` leaves to "a_string") """ @@ -102,16 +115,16 @@ def test_set_provenance_for_leaf_to_a_string(): check_provenance["echam"]["type"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["kind"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["path_in_computer"] = new_prov - check_provenance["echam"]["test1"] = new_prov assert config.get_provenance() == check_provenance -def test_set_provenance_for_a_new_leaf(): +def test_set_provenance_for_a_new_leaf(config, check_provenance): """ Test 8: Rest the provenanve of fesom entry. """ - new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'set_for_onknown_leaf'} + config["fesom"] = {"asd": 0} + new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'set_for_unknown_leaf'} config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) config["fesom"].set_provenance(new_prov) check_provenance["fesom"] = {"asd": None} @@ -119,21 +132,23 @@ def test_set_provenance_for_a_new_leaf(): assert config.get_provenance() == check_provenance -def test_set_provenance_for_a_list_leaf(): +def test_set_provenance_for_a_list_leaf(config, check_provenance): """ Test 9: Reset the provenance of a list") """ new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} + config["fesom"] = {"asd": 0} config["fesom"]["list"] = [30, 19] config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) config["fesom"]["list"].set_provenance(new_prov) + check_provenance["fesom"] = {} check_provenance["fesom"]["list"] = [new_prov, new_prov] check_provenance["fesom"]["asd"] = None assert config.get_provenance() == check_provenance -def test_extract_dict_config(): +def test_extract_dict_config(example_path1): """ Test 10: Test the extraction of config for all allowed variable types. """ @@ -162,23 +177,23 @@ def test_extract_dict_config(): } } - with open(EXAMPLE_PATH1, "r") as file: - esm_tools_loader.set_filename(EXAMPLE_PATH1) + with open(example_path1, "r") as file: + esm_tools_loader.set_filename(example_path1) data, data2 = esm_tools_loader.load(file) assert data == config -def test_check_provenance_list(): +def test_check_provenance_list(example_path1): """ Test 11: Check provenance of a list entry """ os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - check_prov = [{'line': 15, 'col': 19, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'}] + check_prov = [{'line': 15, 'col': 19, 'yaml_file': example_path1, 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': example_path1, 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': example_path1, 'category': 'runscript'}] - with open(EXAMPLE_PATH1, "r") as file: + with open(example_path1, "r") as file: esm_tools_loader.set_filename(file) data, data2 = esm_tools_loader.load(file) @@ -186,7 +201,7 @@ def test_check_provenance_list(): assert config["person"]["my_other_list"].get_provenance() == check_prov -def test_check_set_provenance_list(): +def test_check_set_provenance_list(example_path1): """ Test 12: Check set_provenance of a list entry """ @@ -196,7 +211,7 @@ def test_check_set_provenance_list(): new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_list'} check_prov = [new_prov, new_prov, new_prov] - with open(EXAMPLE_PATH1, "r") as file: + with open(example_path1, "r") as file: esm_tools_loader.set_filename(file) data, data2 = esm_tools_loader.load(file) @@ -205,22 +220,41 @@ def test_check_set_provenance_list(): assert config["person"]["my_other_list"].get_provenance() == check_prov -def test_check_set_provenance_of_single_list_entry(): +def test_check_set_provenance_of_single_list_entry(example_path1): """ Test 13: Check set_provenance of a single list entry """ os.environ['USER'] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - old_prov1 = {'line': 15, 'col': 19, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'} - old_prov2 = {'line': 15, 'col': 22, 'yaml_file': EXAMPLE_PATH1, 'category': 'runscript'} + old_prov1 = {'line': 15, 'col': 19, 'yaml_file': example_path1, 'category': 'runscript'} + old_prov2 = {'line': 15, 'col': 22, 'yaml_file': example_path1, 'category': 'runscript'} new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_second_list'} check_prov = [old_prov1, old_prov2, new_prov] - with open(EXAMPLE_PATH1, "r") as file: + with open(example_path1, "r") as file: esm_tools_loader.set_filename(file) data, data2 = esm_tools_loader.load(file) config = provenance.DictWithProvenance(data, data2) - config["person"]["my_other_list"][2].provenance = new_prov + config["person"]["my_other_list"][2].provenance = provenance.Provenance(new_prov) assert config["person"]["my_other_list"].get_provenance() == check_prov + +def test_check_set_provenance_of_single_list_entry(example_path1): + """ + Check get_provenance raises the correct error when the provenance of an item is not + of the type provenance.Provenance + """ + + os.environ['USER'] = "some_user" + esm_tools_loader = yaml_to_dict.EsmToolsLoader() + new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_list'} + + with open(example_path1, "r") as file: + esm_tools_loader.set_filename(file) + data, data2 = esm_tools_loader.load(file) + + config = provenance.DictWithProvenance(data, data2) + + with pytest.raises(ValueError, match="Provenance must be an instance of the provenance.Provenance class!"): + config["person"]["my_other_list"][2].provenance = new_prov From b2f369fb175f29afa703aecb19fc4aadc104d92d Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Mon, 20 Nov 2023 15:40:43 +0100 Subject: [PATCH 096/107] add custom __setitem__ also to ListWithProvenance, tests for keep_provenance_for_dict_or_list and black --- src/esm_parser/provenance.py | 43 +++- tests/test_esm_parser/example2.yaml | 4 + tests/test_esm_parser/test_provenance.py | 259 +++++++++++++++++------ 3 files changed, 239 insertions(+), 67 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 3a65272d0..b95efafcc 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -12,6 +12,7 @@ class Provenance(list): an attribute of that value. To be used from the ``WithProvenance`` classes created by ``wrapper_with_provenance_factory``. """ + def __init__(self, provenance_data): if isinstance(provenance_data, list): super().__init__(provenance_data) @@ -34,7 +35,6 @@ def append_last_step_modified_by(self, func): self.append(new_provenance_step) - def extend_and_modified_by(self, additional_provenance, func): """ Extends the current provenance with an ``additional_provenance``. This happends @@ -63,7 +63,6 @@ def extend_and_modified_by(self, additional_provenance, func): else: self.append_last_step_modified_by(func) - def add_modified_by(self, provenance_step, func, modified_by="modified_by"): """ Adds an variable of name defined by ``modified_by`` to the given provenance step @@ -410,7 +409,7 @@ def __setitem__(self, key, val): val : any Value of the item """ - # TODO: this needs to happen recursively if is a dict or a list + new_val = copy.deepcopy(val) if ( key in self and not isinstance(self[key], (dict, list)) @@ -420,10 +419,12 @@ def __setitem__(self, key, val): ): new_provenance = copy.deepcopy(self[key].provenance) if hasattr(val, "provenance"): - new_provenance.extend_and_modified_by(val.provenance, "dict.__setitem__") - val.provenance = new_provenance + new_provenance.extend_and_modified_by( + val.provenance, "dict.__setitem__" + ) + new_val.provenance = new_provenance - super().__setitem__(key, val) + super().__setitem__(key, new_val) class ListWithProvenance(list): @@ -514,7 +515,35 @@ def get_provenance(self, index=-1): return provenance_list - # TODO: add __setitem__ equivalent here + def __setitem__(self, indx, val): + """ + Any time an item in a ListWithProvenance is set, extend the old provenance of + the old value with the provenance of the new ``val`` and make that be the new + extended provenance history of the value. + + Parameters + ---------- + indx : int + Index of the element + val : any + Value of the item + """ + new_val = copy.deepcopy(val) + if ( + indx in self + and not isinstance(self[indx], (dict, list)) + and hasattr(self[indx], "provenance") + and hasattr(self, "custom_setitem") + and self.custom_setitem + ): + new_provenance = copy.deepcopy(self[indx].provenance) + if hasattr(val, "provenance"): + new_provenance.extend_and_modified_by( + val.provenance, "dict.__setitem__" + ) + new_val.provenance = new_provenance + + super().__setitem__(indx, new_val) PROVENANCE_MAPPINGS = (DictWithProvenance, ListWithProvenance) diff --git a/tests/test_esm_parser/example2.yaml b/tests/test_esm_parser/example2.yaml index 0609e5d63..aa6c19cc0 100644 --- a/tests/test_esm_parser/example2.yaml +++ b/tests/test_esm_parser/example2.yaml @@ -4,3 +4,7 @@ echam: greenhouse: kind: "input" path_in_computer: "/my/path/in/computer" + a_list: + - 1 + - 2 + - 3 diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 84118b277..9d558cc16 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -10,31 +10,70 @@ from esm_parser import yaml_to_dict + @pytest.fixture() def example_path1(): return f"{os.path.dirname(__file__)}/example.yaml" + @pytest.fixture() def example_path2(): return f"{os.path.dirname(__file__)}/example2.yaml" + @pytest.fixture() def config(example_path2): return yaml_to_dict.yaml_file_to_dict(example_path2) + @pytest.fixture() def check_provenance(example_path2): - check_provenance = {'echam': - {'type': - {'line': 2, 'col': 11, 'yaml_file': example_path2, 'category': 'runscript'}, - 'files': { - 'greenhouse': { - 'kind': {'line': 5, 'col': 19, 'yaml_file': example_path2, 'category': 'runscript'}, - 'path_in_computer': {'line': 6, 'col': 31, 'yaml_file': example_path2, 'category': 'runscript'} + check_provenance = { + "echam": { + "type": { + "line": 2, + "col": 11, + "yaml_file": example_path2, + "category": "runscript", + }, + "files": { + "greenhouse": { + "kind": { + "line": 5, + "col": 19, + "yaml_file": example_path2, + "category": "runscript", + }, + "path_in_computer": { + "line": 6, + "col": 31, + "yaml_file": example_path2, + "category": "runscript", + }, + "a_list": [ + { + "line": 8, + "col": 19, + "yaml_file": example_path2, + "category": "runscript", + }, + { + "line": 9, + "col": 19, + "yaml_file": example_path2, + "category": "runscript", + }, + { + "line": 10, + "col": 19, + "yaml_file": example_path2, + "category": "runscript", + }, + ], } - } + }, }, - 'debug_info': {'loaded_from_file': None} + "debug_info": {"loaded_from_file": None}, } return check_provenance @@ -42,7 +81,7 @@ def check_provenance(example_path2): def test_get_provenance_from_yaml_to_dict(config, check_provenance): """ - Test 1: Checks for correct provenance entries from example2.yaml file. + Test 1: Checks for correct provenance entries from example2.yaml file. """ assert config.get_provenance() == check_provenance @@ -50,7 +89,7 @@ def test_get_provenance_from_yaml_to_dict(config, check_provenance): def test_get_provenance_of_added_entry(config, check_provenance): """ - Test 2: Checks whether the provenance of an added config entry is None. + Test 2: Checks whether the provenance of an added config entry is None. """ config["fesom"] = True @@ -60,7 +99,7 @@ def test_get_provenance_of_added_entry(config, check_provenance): def test_get_provenance_of_added_nested_entry(config, check_provenance): """ - Test 3: Checks whether the provenance of an added nested config entry is None. + Test 3: Checks whether the provenance of an added nested config entry is None. """ config["fesom"] = {"asd": 0} @@ -70,44 +109,58 @@ def test_get_provenance_of_added_nested_entry(config, check_provenance): def test_get_provenance_of_added_nested_entry_2(config, check_provenance): """ - Test 4: Checks whether the provenance of an added nested config entry is None. + Test 4: Checks whether the provenance of an added nested config entry is None. """ - config["echam"]["test1"] = 17. + config["echam"]["test1"] = 17.0 check_provenance["echam"]["test1"] = None assert config.get_provenance() == check_provenance def test_set_provenance_for_leaf(config, check_provenance): - """ - Test 5: Reset the provenance of an ``echam`` leave. + Test 5: Reset the provenance of an ``echam`` leave. """ - new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'userdefined'} + new_prov = { + "line": 2, + "col": 11, + "yaml_file": "someother.yaml", + "category": "userdefined", + } print(config["echam"].get_provenance()) config["echam"].set_provenance(new_prov) print(config) check_provenance["echam"]["type"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["kind"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["path_in_computer"] = new_prov + check_provenance["echam"]["files"]["greenhouse"]["a_list"] = [ + new_prov, + new_prov, + new_prov, + ] assert config.get_provenance() == check_provenance def test_set_provenance_for_leaf_of_new_branch(config, check_provenance): """ - Test 6: Reset the provenance of leaves for an later added branch ``debug_info``. + Test 6: Reset the provenance of leaves for an later added branch ``debug_info``. """ - new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'debuginfo'} + new_prov = { + "line": 2, + "col": 11, + "yaml_file": "someother.yaml", + "category": "debuginfo", + } config["debug_info"].set_provenance(new_prov) - check_provenance["debug_info"]['loaded_from_file'] = new_prov + check_provenance["debug_info"]["loaded_from_file"] = new_prov assert config.get_provenance() == check_provenance def test_set_provenance_for_leaf_to_a_string(config, check_provenance): """ - Test 7: Reset the provenance of all ``echam`` leaves to "a_string") + Test 7: Reset the provenance of all ``echam`` leaves to "a_string") """ new_prov = "a_string" @@ -115,16 +168,26 @@ def test_set_provenance_for_leaf_to_a_string(config, check_provenance): check_provenance["echam"]["type"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["kind"] = new_prov check_provenance["echam"]["files"]["greenhouse"]["path_in_computer"] = new_prov + check_provenance["echam"]["files"]["greenhouse"]["a_list"] = [ + new_prov, + new_prov, + new_prov, + ] assert config.get_provenance() == check_provenance def test_set_provenance_for_a_new_leaf(config, check_provenance): """ - Test 8: Rest the provenanve of fesom entry. + Test 8: Rest the provenanve of fesom entry. """ config["fesom"] = {"asd": 0} - new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'set_for_unknown_leaf'} + new_prov = { + "line": 2, + "col": 11, + "yaml_file": "someother.yaml", + "category": "set_for_unknown_leaf", + } config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) config["fesom"].set_provenance(new_prov) check_provenance["fesom"] = {"asd": None} @@ -134,10 +197,15 @@ def test_set_provenance_for_a_new_leaf(config, check_provenance): def test_set_provenance_for_a_list_leaf(config, check_provenance): """ - Test 9: Reset the provenance of a list") + Test 9: Reset the provenance of a list") """ - new_prov = {'line': 2, 'col': 11, 'yaml_file': 'someother.yaml', 'category': 'this_is_for_a_list'} + new_prov = { + "line": 2, + "col": 11, + "yaml_file": "someother.yaml", + "category": "this_is_for_a_list", + } config["fesom"] = {"asd": 0} config["fesom"]["list"] = [30, 19] config["fesom"] = provenance.DictWithProvenance(config["fesom"], {}) @@ -150,31 +218,28 @@ def test_set_provenance_for_a_list_leaf(config, check_provenance): def test_extract_dict_config(example_path1): """ - Test 10: Test the extraction of config for all allowed variable types. + Test 10: Test the extraction of config for all allowed variable types. """ esm_tools_loader = yaml_to_dict.EsmToolsLoader() - os.environ['USER'] = "some_user" + os.environ["USER"] = "some_user" config = { - 'person': { - 'name': 'Paul Gierz', - 'username': os.environ['USER'], - 'a_string': ' hello world I am here to make your life impossible ', - 'my_var': 'MY_VAR', - 'my_other_var': ['a', 'b', 'c'], - 'my_other_list': ['a', 'b', 'c'], - 'my_bolean': True, - 'my_int': 12.1, - 'my_int2': 42, - 'list_with_dict_inside': [1, 2, { - 'my_dict': { - 'foo': [1, 2, { - 'my_dict': { - 'foo': 'bar'} - }] - } - }] - } + "person": { + "name": "Paul Gierz", + "username": os.environ["USER"], + "a_string": " hello world I am here to make your life impossible ", + "my_var": "MY_VAR", + "my_other_var": ["a", "b", "c"], + "my_other_list": ["a", "b", "c"], + "my_bolean": True, + "my_int": 12.1, + "my_int2": 42, + "list_with_dict_inside": [ + 1, + 2, + {"my_dict": {"foo": [1, 2, {"my_dict": {"foo": "bar"}}]}}, + ], + } } with open(example_path1, "r") as file: @@ -186,12 +251,16 @@ def test_extract_dict_config(example_path1): def test_check_provenance_list(example_path1): """ - Test 11: Check provenance of a list entry + Test 11: Check provenance of a list entry """ - os.environ['USER'] = "some_user" + os.environ["USER"] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - check_prov = [{'line': 15, 'col': 19, 'yaml_file': example_path1, 'category': 'runscript'}, {'line': 15, 'col': 22, 'yaml_file': example_path1, 'category': 'runscript'}, {'line': 15, 'col': 25, 'yaml_file': example_path1, 'category': 'runscript'}] + check_prov = [ + {"line": 15, "col": 19, "yaml_file": example_path1, "category": "runscript"}, + {"line": 15, "col": 22, "yaml_file": example_path1, "category": "runscript"}, + {"line": 15, "col": 25, "yaml_file": example_path1, "category": "runscript"}, + ] with open(example_path1, "r") as file: esm_tools_loader.set_filename(file) @@ -203,12 +272,17 @@ def test_check_provenance_list(example_path1): def test_check_set_provenance_list(example_path1): """ - Test 12: Check set_provenance of a list entry + Test 12: Check set_provenance of a list entry """ - os.environ['USER'] = "some_user" + os.environ["USER"] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_list'} + new_prov = { + "line": 15, + "col": 25, + "yaml_file": "example.yaml", + "category": "from_a_list", + } check_prov = [new_prov, new_prov, new_prov] with open(example_path1, "r") as file: @@ -222,14 +296,29 @@ def test_check_set_provenance_list(example_path1): def test_check_set_provenance_of_single_list_entry(example_path1): """ - Test 13: Check set_provenance of a single list entry + Test 13: Check set_provenance of a single list entry """ - os.environ['USER'] = "some_user" + os.environ["USER"] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - old_prov1 = {'line': 15, 'col': 19, 'yaml_file': example_path1, 'category': 'runscript'} - old_prov2 = {'line': 15, 'col': 22, 'yaml_file': example_path1, 'category': 'runscript'} - new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_second_list'} + old_prov1 = { + "line": 15, + "col": 19, + "yaml_file": example_path1, + "category": "runscript", + } + old_prov2 = { + "line": 15, + "col": 22, + "yaml_file": example_path1, + "category": "runscript", + } + new_prov = { + "line": 15, + "col": 25, + "yaml_file": "example.yaml", + "category": "from_a_second_list", + } check_prov = [old_prov1, old_prov2, new_prov] with open(example_path1, "r") as file: @@ -240,15 +329,21 @@ def test_check_set_provenance_of_single_list_entry(example_path1): config["person"]["my_other_list"][2].provenance = provenance.Provenance(new_prov) assert config["person"]["my_other_list"].get_provenance() == check_prov + def test_check_set_provenance_of_single_list_entry(example_path1): """ Check get_provenance raises the correct error when the provenance of an item is not of the type provenance.Provenance """ - os.environ['USER'] = "some_user" + os.environ["USER"] = "some_user" esm_tools_loader = yaml_to_dict.EsmToolsLoader() - new_prov = {'line': 15, 'col': 25, 'yaml_file': 'example.yaml', 'category': 'from_a_list'} + new_prov = { + "line": 15, + "col": 25, + "yaml_file": "example.yaml", + "category": "from_a_list", + } with open(example_path1, "r") as file: esm_tools_loader.set_filename(file) @@ -256,5 +351,49 @@ def test_check_set_provenance_of_single_list_entry(example_path1): config = provenance.DictWithProvenance(data, data2) - with pytest.raises(ValueError, match="Provenance must be an instance of the provenance.Provenance class!"): + with pytest.raises( + ValueError, + match="Provenance must be an instance of the provenance.Provenance class!", + ): config["person"]["my_other_list"][2].provenance = new_prov + + +def test_keep_provenance_for_dict_or_list(config): + @provenance.keep_provenance_for_dict_or_list + def change_dict_elem(config, key, val): + config[key] = val + + @provenance.keep_provenance_for_dict_or_list + def change_list_elem(config, indx, val): + config[indx] = val + + # Create a value with provenance + val = provenance.wrapper_with_provenance_factory("changed elem", [{"file": "new"}]) + + change_dict_elem(config["echam"], "type", val) + change_list_elem(config["echam"]["files"]["greenhouse"]["a_list"], 1, val) + + check_provenance1 = [ + { + "line": 2, + "col": 11, + "yaml_file": "/Users/mandresm/Codes/esm_tools/tests/test_esm_parser/example2.yaml", + "category": "runscript", + }, + {"file": "new", "extended_by": "dict.__setitem__"}, + ] + check_provenance2 = [ + { + "line": 9, + "col": 19, + "yaml_file": "/Users/mandresm/Codes/esm_tools/tests/test_esm_parser/example2.yaml", + "category": "runscript", + }, + {"file": "new", "extended_by": "dict.__setitem__"}, + ] + + assert config["echam"]["type"].provenance == check_provenance1 + assert ( + config["echam"]["files"]["greenhouse"]["a_list"][1].provenance + == check_provenance2 + ) From 8055ee1afd76a243999f5adbc0188f3877528f0a Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Mon, 20 Nov 2023 17:50:12 +0100 Subject: [PATCH 097/107] add better naming for keep_provenance functions, add exception for provenance modifications in find_variable, new test for keep_provenance_in_recursive_function --- src/esm_parser/esm_parser.py | 16 +++---- src/esm_parser/provenance.py | 17 ++++---- tests/test_esm_parser/test_provenance.py | 53 ++++++++++++++++++++---- 3 files changed, 62 insertions(+), 24 deletions(-) diff --git a/src/esm_parser/esm_parser.py b/src/esm_parser/esm_parser.py index 46d4327b2..93358eee4 100644 --- a/src/esm_parser/esm_parser.py +++ b/src/esm_parser/esm_parser.py @@ -610,7 +610,7 @@ def new_deep_update( # END NEW STUFF -@keep_provenance_for_dict_or_list +@keep_provenance_in_setitem def dict_merge(dct, merge_dct, resolve_nested_adds=False, **kwargs): """Recursive dict merge. Inspired by :meth:``dict.update()``, instead of updating only top-level keys, dict_merge recurses down into dicts nested @@ -1887,7 +1887,7 @@ def determine_regex_list_match(test_str, regex_list): return any(result) -@keep_provenance_for_recursive_function +@keep_provenance_in_recursive_function def find_variable(tree, rhs, full_config, white_or_black_list, isblacklist): raw_str = rhs if not tree[-1]: @@ -2250,7 +2250,7 @@ def determine_computer_from_hostname(): # ) -@keep_provenance_for_recursive_function +@keep_provenance_in_recursive_function def do_math_in_entry(tree, rhs, config): if not tree[-1]: tree = tree[:-1] @@ -2355,7 +2355,7 @@ def do_math_in_entry(tree, rhs, config): return convert(entry.strip(), tree) -@keep_provenance_for_recursive_function +@keep_provenance_in_recursive_function def mark_dates(tree, rhs, config): """Adds the ``DATE_MARKER`` to any entry who's key ends with ``"date"``""" if not tree[-1]: @@ -2370,7 +2370,7 @@ def mark_dates(tree, rhs, config): return entry -@keep_provenance_for_recursive_function +@keep_provenance_in_recursive_function def marked_date_to_date_object(tree, rhs, config): """Transforms a marked date string into a Date object""" if not tree[-1]: @@ -2401,7 +2401,7 @@ def marked_date_to_date_object(tree, rhs, config): return entry -@keep_provenance_for_recursive_function +@keep_provenance_in_recursive_function def unmark_dates(tree, rhs, config): """Removes the ``DATE_MARKER`` to any entry who's entry contains the ``DATE_MARKER``.""" if not tree[-1]: @@ -2413,7 +2413,7 @@ def unmark_dates(tree, rhs, config): return entry -@keep_provenance_for_recursive_function +@keep_provenance_in_recursive_function def perform_actions(tree, rhs, config): if not tree[-1]: tree = tree[:-1] @@ -2464,7 +2464,7 @@ def perform_actions(tree, rhs, config): return entry -@keep_provenance_for_recursive_function +@keep_provenance_in_recursive_function def purify_booleans(tree, rhs, config): if not tree[-1]: tree = tree[:-1] diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index b95efafcc..16a3af405 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -549,7 +549,7 @@ def __setitem__(self, indx, val): PROVENANCE_MAPPINGS = (DictWithProvenance, ListWithProvenance) -def keep_provenance_for_dict_or_list(func): +def keep_provenance_in_setitem(func): def inner(val_with_prov, *args, **kwargs): if hasattr(val_with_prov, "custom_setitem"): val_with_prov.custom_setitem = True @@ -564,7 +564,7 @@ def inner(val_with_prov, *args, **kwargs): return inner -def keep_provenance_for_recursive_function(func): +def keep_provenance_in_recursive_function(func): # TODO: this is an incorrect description """ Decorator for recursive functions in ``esm_parser`` to preserve @@ -581,21 +581,26 @@ def keep_provenance_for_recursive_function(func): The function to decorate """ + does_not_modify_prov = ["esm_parser.find_variable"] + modify_prov = not str(func) in does_not_modify_prov + def inner(tree, rhs, *args, **kwargs): output = func(tree, rhs, *args, **kwargs) if hasattr(rhs, "provenance"): - provenance = rhs.provenance + provenance = copy.deepcopy(rhs.provenance) # Value was modified if type(rhs) != type(output) or rhs != output: # If the new value has an inherited provenance, keep it (i.e. variable # was called: rhs = ${fesom.namelist_dir}, output = # /actual/path/with/provenance/to/be/kept}) if hasattr(output, "provenance"): - provenance.extend_and_modified_by(output.provenance, func) + if modify_prov: + provenance.extend_and_modified_by(output.provenance, func) + output.provenance = provenance # If the rhs.provenance is not None and output has no provenance, keep # the old provenance - elif provenance is not None: + elif provenance is not None and modify_prov: provenance.append_last_step_modified_by(func) output = wrapper_with_provenance_factory(output, provenance) @@ -634,5 +639,3 @@ def inner(tree, rhs, *args, **kwargs): print(asd["a_string"], asd["a_string"].provenance) print(asd["list_with_dict_inside"], asd["list_with_dict_inside"]) - - # TODO: tests for the keep_provenance functions diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 9d558cc16..d0eb9768d 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -358,20 +358,16 @@ def test_check_set_provenance_of_single_list_entry(example_path1): config["person"]["my_other_list"][2].provenance = new_prov -def test_keep_provenance_for_dict_or_list(config): - @provenance.keep_provenance_for_dict_or_list - def change_dict_elem(config, key, val): +def test_keep_provenance_in_setitem(config): + @provenance.keep_provenance_in_setitem + def change_elem(config, key, val): config[key] = val - @provenance.keep_provenance_for_dict_or_list - def change_list_elem(config, indx, val): - config[indx] = val - # Create a value with provenance val = provenance.wrapper_with_provenance_factory("changed elem", [{"file": "new"}]) - change_dict_elem(config["echam"], "type", val) - change_list_elem(config["echam"]["files"]["greenhouse"]["a_list"], 1, val) + change_elem(config["echam"], "type", val) + change_elem(config["echam"]["files"]["greenhouse"]["a_list"], 1, val) check_provenance1 = [ { @@ -397,3 +393,42 @@ def change_list_elem(config, indx, val): config["echam"]["files"]["greenhouse"]["a_list"][1].provenance == check_provenance2 ) + + +def test_keep_provenance_in_recursive_function(config): + @provenance.keep_provenance_in_recursive_function + def change_elem(tree, rhs): + return provenance.wrapper_with_provenance_factory("new_val", {"modified": True}) + + tree = [] + + check_provenance1 = [ + { + "line": 2, + "col": 11, + "yaml_file": "/Users/mandresm/Codes/esm_tools/tests/test_esm_parser/example2.yaml", + "category": "runscript", + }, + { + "modified": True, + "extended_by": ".change_elem at 0x11433f8b0>", + }, + ] + check_provenance2 = [ + { + "line": 9, + "col": 19, + "yaml_file": "/Users/mandresm/Codes/esm_tools/tests/test_esm_parser/example2.yaml", + "category": "runscript", + }, + { + "modified": True, + "extended_by": ".change_elem at 0x11433f8b0>", + }, + ] + + rhs1 = change_elem(tree, config["echam"]["type"]) + rhs2 = change_elem(tree, config["echam"]["files"]["greenhouse"]["a_list"][1]) + + assert rhs1 == "new_val" and rhs1.provenance[0] == check_provenance1[0] + assert rhs2 == "new_val" and rhs2.provenance[0] == check_provenance2[0] From b0dde433174ec7b24c5cb8628607323e24823b01 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Mon, 20 Nov 2023 18:41:31 +0100 Subject: [PATCH 098/107] fix a bug in the keep_provenance_in_setitem --- src/esm_parser/provenance.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 16a3af405..d2586bc05 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -581,8 +581,8 @@ def keep_provenance_in_recursive_function(func): The function to decorate """ - does_not_modify_prov = ["esm_parser.find_variable"] - modify_prov = not str(func) in does_not_modify_prov + does_not_modify_prov = ["find_variable"] + modify_prov = not func.__name__ in does_not_modify_prov def inner(tree, rhs, *args, **kwargs): output = func(tree, rhs, *args, **kwargs) From 791c3a068379af2ed84953b261d068f8150477fb Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 30 Nov 2023 23:46:51 +0100 Subject: [PATCH 099/107] remove deepcopies that were unnecessary and slowing down the code. Add new necessary deepcopies. Set custom_setitem True as default and make keep_provenance_in_recursive_function set it to False. Remove keep_provenance_in_set_item as it is not needed anymore --- src/esm_parser/esm_parser.py | 3 +- src/esm_parser/provenance.py | 53 ++++++++++++------------ tests/test_esm_parser/test_provenance.py | 37 ----------------- 3 files changed, 28 insertions(+), 65 deletions(-) diff --git a/src/esm_parser/esm_parser.py b/src/esm_parser/esm_parser.py index 93358eee4..f31486ebc 100644 --- a/src/esm_parser/esm_parser.py +++ b/src/esm_parser/esm_parser.py @@ -610,7 +610,6 @@ def new_deep_update( # END NEW STUFF -@keep_provenance_in_setitem def dict_merge(dct, merge_dct, resolve_nested_adds=False, **kwargs): """Recursive dict merge. Inspired by :meth:``dict.update()``, instead of updating only top-level keys, dict_merge recurses down into dicts nested @@ -622,6 +621,7 @@ def dict_merge(dct, merge_dct, resolve_nested_adds=False, **kwargs): """ # option to overwrite a dict value if merge_dict contains empty value. Default # is False + dont_overwrite_with_empty_value = kwargs.get( "dont_overwrite_with_empty_value", False ) @@ -1700,6 +1700,7 @@ def add_more_important_tasks(choose_keyword, all_set_variables, task_list): return task_list +@keep_provenance_in_recursive_function def recursive_run_function(tree, right, level, func, *args, **kwargs): """Recursively runs func on all nested dicts. diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index d2586bc05..4adabec09 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -49,7 +49,7 @@ def extend_and_modified_by(self, additional_provenance, func): func : str Function triggering this method """ - new_additional_provenance = copy.deepcopy(additional_provenance) + new_additional_provenance = additional_provenance # If the new provenance is not identical to the current one extend the # provenance if new_additional_provenance is not self: @@ -301,6 +301,7 @@ def __init__(self, dictionary, provenance): self.custom_setitem = False self.put_provenance(provenance) + self.custom_setitem = True def put_provenance(self, provenance): # TODO: this is an incorrect description @@ -409,7 +410,7 @@ def __setitem__(self, key, val): val : any Value of the item """ - new_val = copy.deepcopy(val) + val_new = val if ( key in self and not isinstance(self[key], (dict, list)) @@ -417,14 +418,15 @@ def __setitem__(self, key, val): and hasattr(self, "custom_setitem") and self.custom_setitem ): - new_provenance = copy.deepcopy(self[key].provenance) + new_provenance = self[key].provenance if hasattr(val, "provenance"): new_provenance.extend_and_modified_by( val.provenance, "dict.__setitem__" ) - new_val.provenance = new_provenance + val_new = copy.deepcopy(val) + val_new.provenance = new_provenance - super().__setitem__(key, new_val) + super().__setitem__(key, val_new) class ListWithProvenance(list): @@ -433,6 +435,7 @@ def __init__(self, mylist, provenance): self.custom_setitem = False self.put_provenance(provenance) + self.custom_setitem = True def put_provenance(self, provenance): if not provenance: @@ -528,7 +531,7 @@ def __setitem__(self, indx, val): val : any Value of the item """ - new_val = copy.deepcopy(val) + val_new = val if ( indx in self and not isinstance(self[indx], (dict, list)) @@ -536,34 +539,20 @@ def __setitem__(self, indx, val): and hasattr(self, "custom_setitem") and self.custom_setitem ): - new_provenance = copy.deepcopy(self[indx].provenance) + new_provenance = self[indx].provenance if hasattr(val, "provenance"): new_provenance.extend_and_modified_by( val.provenance, "dict.__setitem__" ) - new_val.provenance = new_provenance + val_new = copy.deepcopy(val) + val_new.provenance = new_provenance - super().__setitem__(indx, new_val) + super().__setitem__(indx, val_new) PROVENANCE_MAPPINGS = (DictWithProvenance, ListWithProvenance) -def keep_provenance_in_setitem(func): - def inner(val_with_prov, *args, **kwargs): - if hasattr(val_with_prov, "custom_setitem"): - val_with_prov.custom_setitem = True - - output = func(val_with_prov, *args, **kwargs) - - if hasattr(val_with_prov, "custom_setitem"): - val_with_prov.custom_setitem = False - - return output - - return inner - - def keep_provenance_in_recursive_function(func): # TODO: this is an incorrect description """ @@ -581,16 +570,22 @@ def keep_provenance_in_recursive_function(func): The function to decorate """ - does_not_modify_prov = ["find_variable"] + does_not_modify_prov = ["find_variable", "recursive_run_function"] modify_prov = not func.__name__ in does_not_modify_prov def inner(tree, rhs, *args, **kwargs): + custom_setitem_was_turned_off_in_this_instance = False + if hasattr(rhs, "custom_setitem") and rhs.custom_setitem: + rhs.custom_setitem = False + custom_setitem_was_turned_off = True + output = func(tree, rhs, *args, **kwargs) if hasattr(rhs, "provenance"): provenance = copy.deepcopy(rhs.provenance) # Value was modified if type(rhs) != type(output) or rhs != output: + output = copy.deepcopy(output) # If the new value has an inherited provenance, keep it (i.e. variable # was called: rhs = ${fesom.namelist_dir}, output = # /actual/path/with/provenance/to/be/kept}) @@ -600,10 +595,14 @@ def inner(tree, rhs, *args, **kwargs): output.provenance = provenance # If the rhs.provenance is not None and output has no provenance, keep # the old provenance - elif provenance is not None and modify_prov: - provenance.append_last_step_modified_by(func) + elif provenance is not None: + if modify_prov: + provenance.append_last_step_modified_by(func) output = wrapper_with_provenance_factory(output, provenance) + if custom_setitem_was_turned_off_in_this_instance: + rhs.custom_setitem = True + return output return inner diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index d0eb9768d..45891932f 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -358,43 +358,6 @@ def test_check_set_provenance_of_single_list_entry(example_path1): config["person"]["my_other_list"][2].provenance = new_prov -def test_keep_provenance_in_setitem(config): - @provenance.keep_provenance_in_setitem - def change_elem(config, key, val): - config[key] = val - - # Create a value with provenance - val = provenance.wrapper_with_provenance_factory("changed elem", [{"file": "new"}]) - - change_elem(config["echam"], "type", val) - change_elem(config["echam"]["files"]["greenhouse"]["a_list"], 1, val) - - check_provenance1 = [ - { - "line": 2, - "col": 11, - "yaml_file": "/Users/mandresm/Codes/esm_tools/tests/test_esm_parser/example2.yaml", - "category": "runscript", - }, - {"file": "new", "extended_by": "dict.__setitem__"}, - ] - check_provenance2 = [ - { - "line": 9, - "col": 19, - "yaml_file": "/Users/mandresm/Codes/esm_tools/tests/test_esm_parser/example2.yaml", - "category": "runscript", - }, - {"file": "new", "extended_by": "dict.__setitem__"}, - ] - - assert config["echam"]["type"].provenance == check_provenance1 - assert ( - config["echam"]["files"]["greenhouse"]["a_list"][1].provenance - == check_provenance2 - ) - - def test_keep_provenance_in_recursive_function(config): @provenance.keep_provenance_in_recursive_function def change_elem(tree, rhs): From ef7abe3c423cabf291c54793155dfbb1218fb5f2 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 4 Jan 2024 17:33:22 +0100 Subject: [PATCH 100/107] update method tracks provenance history in DictWithProvenance --- src/esm_parser/provenance.py | 32 ++++++++++++++++++++ tests/test_esm_parser/test_provenance.py | 38 ++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 4adabec09..dab40068b 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -428,6 +428,38 @@ def __setitem__(self, key, val): super().__setitem__(key, val_new) + def update(self, dictionary, *args, **kwargs): + """ + Preserves the provenance history when using the ``update`` method + + Parameters + ---------- + dictionary : dict, esm_parser.provenance.DictWithProvenance + Dictionary that will update ``self`` + """ + + new_provs = {} + + for key, val in dictionary.items(): + if ( + key in self + and not isinstance(self[key], (dict, list)) + and hasattr(self[key], "provenance") + and hasattr(self, "custom_setitem") + and self.custom_setitem + ): + new_provenance = self[key].provenance + if hasattr(val, "provenance"): + new_provenance.extend_and_modified_by( + val.provenance, "dict.update" + ) + new_provs[key] = new_provenance + + super().update(dictionary, *args, **kwargs) + + for key, val in new_provs.items(): + self[key].provenance = val + class ListWithProvenance(list): def __init__(self, mylist, provenance): diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 45891932f..84fabc109 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -195,6 +195,44 @@ def test_set_provenance_for_a_new_leaf(config, check_provenance): assert config.get_provenance() == check_provenance +def test_provenance_update(config, check_provenance): + """ + Checks that the method update preserves the provenance history + """ + + new_prov = { + "line": 2, + "col": 11, + "yaml_file": "someother.yaml", + "category": "set_for_unknown_leaf", + } + new_config = { + "echam": provenance.DictWithProvenance({"type": "mpi_atmosphere"}, {}) + } + new_config["echam"].set_provenance(new_prov) + + config["echam"].update(new_config["echam"]) + check_provenance["echam"]["type"] = new_prov + assert config.get_provenance() == check_provenance + # Checks that update preserves provenance history + assert config["echam"]["type"].provenance == [ + { + "line": 2, + "col": 11, + "yaml_file": "/Users/mandresm/Codes/esm_tools/tests/test_esm_parser/example2.yaml", + "category": "runscript", + }, + None, + { + "line": 2, + "col": 11, + "extended_by": "dict.update", + "yaml_file": "someother.yaml", + "category": "set_for_unknown_leaf", + }, + ] + + def test_set_provenance_for_a_list_leaf(config, check_provenance): """ Test 9: Reset the provenance of a list") From 1546f77cce13529bc137119feac5486fcf022dc3 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Thu, 4 Jan 2024 17:35:37 +0100 Subject: [PATCH 101/107] fix indentation mistake --- src/esm_parser/provenance.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index dab40068b..25e6c9bab 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -429,14 +429,14 @@ def __setitem__(self, key, val): super().__setitem__(key, val_new) def update(self, dictionary, *args, **kwargs): - """ - Preserves the provenance history when using the ``update`` method + """ + Preserves the provenance history when using the ``update`` method - Parameters - ---------- - dictionary : dict, esm_parser.provenance.DictWithProvenance - Dictionary that will update ``self`` - """ + Parameters + ---------- + dictionary : dict, esm_parser.provenance.DictWithProvenance + Dictionary that will update ``self`` + """ new_provs = {} From 891e88efc7a66565d59b8cc1f648a610117ec22c Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Wed, 28 Feb 2024 16:39:16 +0100 Subject: [PATCH 102/107] fixed a bug with WrapperWithProvenance.__new__ method that was preventing copy.deepcopy to work properly --- src/esm_parser/provenance.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 25e6c9bab..295542d04 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -191,8 +191,10 @@ class WrapperWithProvenance(type(value)): Dynamically create a subclass of the type of the given value """ - def __new__(cls, value, *args, **kwargs): - return super(WrapperWithProvenance, cls).__new__(cls, value) + _value = value + + def __new__(cls, *args, **kwargs): + return super(WrapperWithProvenance, cls).__new__(cls, cls._value) def __init__(self, value, provenance=None): self._provenance = Provenance(provenance) From 8c969183928a85137614ce6279c02212d0015bd7 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Wed, 28 Feb 2024 17:45:11 +0100 Subject: [PATCH 103/107] improve docstrings of the provenance --- src/esm_parser/provenance.py | 48 ++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 295542d04..815f08c52 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -7,13 +7,35 @@ class Provenance(list): """ - A subclass of lists in which each element holds provenance information about the - provenance history of a value. To assign the provenance to a value instanciated as - an attribute of that value. To be used from the ``WithProvenance`` classes created - by ``wrapper_with_provenance_factory``. + A subclass of list in which each element represents the provenance of the value + at a point in the key-value history. The whole point of this class is to have a + list subclass that allows us to include information about which function is + changing the list whithin each provenance element. + + To assign the provenance to a value instanciate it as an attribute of that value + (i.e. ``self.provenance = Provenance(my_provenance)``). To be used from the + ``WithProvenance`` classes created by ``wrapper_with_provenance_factory``. + + The following class methods provide the extended functionality to lists: + * ``self.append_last_step_modified_by``: to duplicate the last element of the list + and add to it information about the function that is modifying the value + * ``self.extend_and_modified_by``: to extend a list while including in the + provenance the function which is responsible for extending it """ def __init__(self, provenance_data): + """ + Initialize the provenance object with its subclass (``list``) giving + ``provenance_data`` as input. If ``provenance_data`` is not a ``list`` + initialize the object as a list which only element is ``provenance_data``. + + Parameters + ---------- + provenance_data : list + List of provenance elements that describe the history of a key-value, or + a single provenance element. + """ + if isinstance(provenance_data, list): super().__init__(provenance_data) else: @@ -21,15 +43,14 @@ def __init__(self, provenance_data): def append_last_step_modified_by(self, func): """ - Copies the last step in the provenance history and adds the entry ``modify_by`` - with value ``func``. + Copies the last element in the provenance history and adds the entry ``modify_by`` + with value ``func`` to the copy. Parameters ---------- func : str Function that is modifying the variable """ - new_provenance_step = copy.deepcopy(self[-1]) new_provenance_step = self.add_modified_by(new_provenance_step, func) @@ -37,10 +58,11 @@ def append_last_step_modified_by(self, func): def extend_and_modified_by(self, additional_provenance, func): """ - Extends the current provenance with an ``additional_provenance``. This happends - when for example a variable comes originally from a file, but then is - overwritten by a file higher in the hierarchy. This would keep both histories, - with the history of the second been on top of the first. + Extends the current provenance history with an ``additional_provenance``. This + happens when for example a variable comes originally from a file, but then the + value is overwritten by another value that comes from a file higher in the + hierarchy. This method keeps both histories, with the history of the second been + on top of the first. Parameters ---------- @@ -65,7 +87,7 @@ def extend_and_modified_by(self, additional_provenance, func): def add_modified_by(self, provenance_step, func, modified_by="modified_by"): """ - Adds an variable of name defined by ``modified_by`` to the given provenance step + Adds a variable of name defined by ``modified_by`` to the given provenance step with value ``func``. This variable is used to label provenance steps of the provenance history with functions that modified it. @@ -152,7 +174,7 @@ def wrapper_with_provenance_factory(value, provenance=None): that are not subclassable (``bool`` and ``NoneType``) intanciates an object that mimics their behaviour but also contains the ``provenance`` attribute. - Objects of type ``esm_calendar.esm_calendar.Date`` are not subclass (and the + Objects of type ``esm_calendar.esm_calendar.Date`` are not subclassed (and the ``provenance`` attribute is simply added to them, because they fail to be subclassed with in the ``WrapperWithProvenance`` with the following error:: From 25922e93f4078ac65d7328d60aca46a5b2415219 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Fri, 1 Mar 2024 15:03:15 +0100 Subject: [PATCH 104/107] turn the local wrapper classes into globals and avoid creating a class per object, now it only creates a dynamical class per subclass --- src/esm_parser/esm_parser.py | 2 +- src/esm_parser/provenance.py | 99 +++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/esm_parser/esm_parser.py b/src/esm_parser/esm_parser.py index f31486ebc..d51bce619 100644 --- a/src/esm_parser/esm_parser.py +++ b/src/esm_parser/esm_parser.py @@ -1063,7 +1063,7 @@ def add_entry_to_chapter( mod_list.extend(list(flatten_nested_lists(add_entries))) # Remove duplicates - mod_list_no_dupl = [] + mod_list_no_dupl = ListWithProvenance([], None) for el in mod_list: if not isinstance(el, (dict, tuple, list)): if el not in mod_list_no_dupl: diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index 815f08c52..e112c72ac 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -111,6 +111,37 @@ def add_modified_by(self, provenance_step, func, modified_by="modified_by"): return provenance_step +@classmethod +def wrapper_with_provenance_new(cls, *args, **kwargs): + return super(cls, cls).__new__(cls, args[1]) + +def wrapper_with_provenance_init(self, value, provenance=None): + """ + Parameters + ---------- + value : bool, None + Value of the object + provenance : any + The provenance information + """ + self._provenance = Provenance(provenance) + self.value = value + +@property +def prop_provenance(self): + return self._provenance + +@prop_provenance.setter +def prop_provenance(self, new_provenance): + # Check if new_provenance is an instance of Provenance + if not isinstance(new_provenance, Provenance): + raise ValueError( + "Provenance must be an instance of the provenance.Provenance " + "class!" + ) + + self._provenance = new_provenance + class ProvenanceClassForTheUnsubclassable: """ A class to reproduce the methods of the unclassable ``bool`` and ``NoneType`` @@ -120,18 +151,6 @@ class ProvenanceClassForTheUnsubclassable: ``bool`` or a ``NoneType``) and the other is the ``provenance``. """ - def __init__(self, value, provenance): - """ - Parameters - ---------- - value : bool, None - Value of the object - provenance : any - The provenance information - """ - self.value = value - self.provenance = Provenance(provenance) - def __repr__(self): return f"{self.value}" @@ -147,6 +166,9 @@ def __eq__(self, other): def __hash__(self): return hash(self.value) +ProvenanceClassForTheUnsubclassable.__init__ = wrapper_with_provenance_init +ProvenanceClassForTheUnsubclassable.provenance = prop_provenance + class BoolWithProvenance(ProvenanceClassForTheUnsubclassable): @property @@ -158,6 +180,10 @@ def __class__(self): class NoneWithProvenance(ProvenanceClassForTheUnsubclassable): + def __init__(self, value, provenance): + print(provenance) + self.value = value + self.provenance = Provenance(provenance) @property def __class__(self): """ @@ -165,7 +191,6 @@ def __class__(self): """ return type(None) - def wrapper_with_provenance_factory(value, provenance=None): """ A function to subclass and instanciate all types of subclassable objects in the @@ -205,39 +230,29 @@ def wrapper_with_provenance_factory(value, provenance=None): value.provenance = Provenance(provenance) return value - + elif isinstance(value, PROVENANCE_MAPPINGS): + return value else: + subtype = type(value) + class_name = f"{subtype}".split("'")[1] + class_name = f"{class_name[0].upper()}{class_name[1:]}WithProvenance" - class WrapperWithProvenance(type(value)): - """ - Dynamically create a subclass of the type of the given value - """ - - _value = value - - def __new__(cls, *args, **kwargs): - return super(WrapperWithProvenance, cls).__new__(cls, cls._value) - - def __init__(self, value, provenance=None): - self._provenance = Provenance(provenance) - - @property - def provenance(self): - return self._provenance - - @provenance.setter - def provenance(self, new_provenance): - # Check if new_provenance is an instance of Provenance - if not isinstance(new_provenance, Provenance): - raise ValueError( - "Provenance must be an instance of the provenance.Provenance " - "class!" - ) - - self._provenance = new_provenance + if class_name in globals(): + pass + else: + globals()[class_name] = type( + class_name, + (subtype, ), + { + "_class_name": class_name, + "__new__": wrapper_with_provenance_new, + "__init__": wrapper_with_provenance_init, + "provenance": prop_provenance, + }, + ) # Instantiate the subclass with the given value and provenance - return WrapperWithProvenance(value, provenance) + return globals()[class_name](value, provenance) class DictWithProvenance(dict): From ad47c94b0a037c8dd35d154ca4771a9845cbe85b Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Fri, 8 Mar 2024 09:38:30 +0100 Subject: [PATCH 105/107] clean up the finished_config from provenance before yaml dumping and testing some stuff with yaml representers --- src/esm_parser/provenance.py | 142 +++++++++++++++++++----------- src/esm_runscripts/prepcompute.py | 26 ++++++ 2 files changed, 119 insertions(+), 49 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index e112c72ac..a47b90d25 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -115,6 +115,7 @@ def add_modified_by(self, provenance_step, func, modified_by="modified_by"): def wrapper_with_provenance_new(cls, *args, **kwargs): return super(cls, cls).__new__(cls, args[1]) + def wrapper_with_provenance_init(self, value, provenance=None): """ Parameters @@ -127,21 +128,30 @@ def wrapper_with_provenance_init(self, value, provenance=None): self._provenance = Provenance(provenance) self.value = value + +# @Paul: maybe we could add a classmethod here for including it in +# ProvenanceClassForTheUnsubclassable, Date and the dynamic classes from +# wrapper_with_provenance_factory (but not for ListWithProvenance or +# DictWithProvenance)? That might be easier than having to add_representer somewhere +# else, but no clue... you're the yaml expert :) + + @property def prop_provenance(self): return self._provenance + @prop_provenance.setter def prop_provenance(self, new_provenance): # Check if new_provenance is an instance of Provenance if not isinstance(new_provenance, Provenance): raise ValueError( - "Provenance must be an instance of the provenance.Provenance " - "class!" + "Provenance must be an instance of the provenance.Provenance class!" ) self._provenance = new_provenance + class ProvenanceClassForTheUnsubclassable: """ A class to reproduce the methods of the unclassable ``bool`` and ``NoneType`` @@ -166,9 +176,10 @@ def __eq__(self, other): def __hash__(self): return hash(self.value) + ProvenanceClassForTheUnsubclassable.__init__ = wrapper_with_provenance_init ProvenanceClassForTheUnsubclassable.provenance = prop_provenance - + class BoolWithProvenance(ProvenanceClassForTheUnsubclassable): @property @@ -184,6 +195,7 @@ def __init__(self, value, provenance): print(provenance) self.value = value self.provenance = Provenance(provenance) + @property def __class__(self): """ @@ -191,6 +203,7 @@ def __class__(self): """ return type(None) + def wrapper_with_provenance_factory(value, provenance=None): """ A function to subclass and instanciate all types of subclassable objects in the @@ -227,6 +240,7 @@ def wrapper_with_provenance_factory(value, provenance=None): return NoneWithProvenance(value, provenance) elif type(value) == Date: + value.provenance = prop_provenance value.provenance = Provenance(provenance) return value @@ -242,7 +256,7 @@ def wrapper_with_provenance_factory(value, provenance=None): else: globals()[class_name] = type( class_name, - (subtype, ), + (subtype,), { "_class_name": class_name, "__new__": wrapper_with_provenance_new, @@ -258,67 +272,67 @@ def wrapper_with_provenance_factory(value, provenance=None): class DictWithProvenance(dict): # TODO: this is an incorrect description """ - A dictionary subclass that contains a ``provenance`` attribute. This attribute is - a ``dict`` that contains those `keys` of the original dictionary whose `values` - **are not a** ``dict`` (leaves of the dictionary tree), and a provenance value - defined during the instancing of the object. The ``provenance`` attribute is - applied recursively within the nested dictionaries during instancing or when the - ``self.set_provenance()`` is used. - - Example - ------- - After instancing the object: + A dictionary subclass that contains a ``provenance`` attribute. This attribute is + a ``dict`` that contains those `keys` of the original dictionary whose `values` + **are not a** ``dict`` (leaves of the dictionary tree), and a provenance value + defined during the instancing of the object. The ``provenance`` attribute is + applied recursively within the nested dictionaries during instancing or when the + ``self.set_provenance()`` is used. + + Example + ------- + After instancing the object: - .. code-block:: python + .. code-block:: python - dict_with_provenance = DictWithProvenance(config_dict, {"file": "echam.yaml"}) + dict_with_provenance = DictWithProvenance(config_dict, {"file": "echam.yaml"}) - where ``config_dict`` is defined as: + where ``config_dict`` is defined as: - .. code-block:: python + .. code-block:: python - config_dict = { - "echam": { - "type": "atmosphere", - "files": { - "greenhouse": {"kind": "input", "path_in_computer": "/my/path/in/computer"} - }, + config_dict = { + "echam": { + "type": "atmosphere", + "files": { + "greenhouse": {"kind": "input", "path_in_computer": "/my/path/in/computer"} + }, + } } - } - then ``config_dict["echam"].provenance`` will take the following values: + then ``config_dict["echam"].provenance`` will take the following values: - .. code-block:: python + .. code-block:: python -# >>> config_dict["echam"].provenance - {'type': {'file': 'echam.yaml'}} + # >>> config_dict["echam"].provenance + {'type': {'file': 'echam.yaml'}} - Note that the `key` ``"files"`` does not exist as the value for that key in the - ``config_dict`` is a dictionary (**it is not a leaf of the dictionary tree**). + Note that the `key` ``"files"`` does not exist as the value for that key in the + ``config_dict`` is a dictionary (**it is not a leaf of the dictionary tree**). - The `provenance value` can be defined to be any python object. The ``provenance`` - attribute is inherited when merging dictionaries with the ``update`` method - when merging two ``DictWithProvenance`` objects, with the same rewriting strategy - as for the keys in the dictionary, and ``provenance`` is also inherited when - redefining a `value` to contain a ``DictWithProvenance``. + The `provenance value` can be defined to be any python object. The ``provenance`` + attribute is inherited when merging dictionaries with the ``update`` method + when merging two ``DictWithProvenance`` objects, with the same rewriting strategy + as for the keys in the dictionary, and ``provenance`` is also inherited when + redefining a `value` to contain a ``DictWithProvenance``. - Use - --- - Instance a new ``DictWithProvenance`` object:: + Use + --- + Instance a new ``DictWithProvenance`` object:: - dict_with_provenance = DictWithProvenance(, ) + dict_with_provenance = DictWithProvenance(, ) - Redefine the provenance of an existing ``key``:: + Redefine the provenance of an existing ``key``:: - dict_with_provenance[""].set_provenance() + dict_with_provenance[""].set_provenance() - Set the provenace of a specific leaf within a nested dictionary:: + Set the provenace of a specific leaf within a nested dictionary:: - dict_with_provenance["key1"]["key1"].provenance["leaf_key"] = + dict_with_provenance["key1"]["key1"].provenance["leaf_key"] = - Get the ``provenance`` representation of the dictionary:: + Get the ``provenance`` representation of the dictionary:: - provenance_dict = dict_with_provenance.get_provenance() + provenance_dict = dict_with_provenance.get_provenance() """ def __init__(self, dictionary, provenance): @@ -489,9 +503,7 @@ def update(self, dictionary, *args, **kwargs): ): new_provenance = self[key].provenance if hasattr(val, "provenance"): - new_provenance.extend_and_modified_by( - val.provenance, "dict.update" - ) + new_provenance.extend_and_modified_by(val.provenance, "dict.update") new_provs[key] = new_provenance super().update(dictionary, *args, **kwargs) @@ -679,6 +691,38 @@ def inner(tree, rhs, *args, **kwargs): return inner +def clean_provenance(data): + """ + Returns the values of provenance mappings in their original classes (without the + provenance). Recurs through mappings. Make sure you copy.deepcopy the data mapping + before running this function if you don't want that your provenance information gets + lost on the original ``data`` mapping. + + Parameters + ---------- + data : any + Mapping or values with provenance. + + Returns + ------- + value : any + Values in their original format, or lists and dictionaries containing provenance + values. + """ + if hasattr(data, "value"): + assert ( + data == data.value, + "The provenance object's value and the original value do not match!", + ) + return data.value + elif isinstance(data, list): + return [clean_provenance(item) for item in data] + elif isinstance(data, dict): + return {key: clean_provenance(value) for key, value in data.items()} + else: + return data + + if __name__ == "__main__": mydict = { "person": {"name": "Paul Gierz"}, diff --git a/src/esm_runscripts/prepcompute.py b/src/esm_runscripts/prepcompute.py index 040a3e63a..62ef37c97 100644 --- a/src/esm_runscripts/prepcompute.py +++ b/src/esm_runscripts/prepcompute.py @@ -280,6 +280,17 @@ def oasis_representer(dumper, oasis): def namelist_representer(dumper, f90nml): return dumper.represent_str(f"f90nml.name") + def listwithprov_representer(dumper, listwithprov): + return dumper.represent_sequence("tag:yaml.org,2002:seq", listwithprov) + + def dictwithprov_representer(dumper, dictwithprov): + return dumper.represent_mapping("tag:yaml.org,2002:map", dictwithprov) + + # @Paul: this is me just playing around with things, this should be included maybe + # somewhere else and generalized for Str, Int, Bool... + def strwithprov_representer(dumper, strwithprov): + return dumper.represent_str(strwithprov) + # dumper object for the ESM-Tools configuration class EsmConfigDumper(yaml.dumper.Dumper): pass @@ -313,6 +324,19 @@ class EsmConfigDumper(yaml.dumper.Dumper): f90nml.namelist.Namelist, namelist_representer ) + # Provenance representers + EsmConfigDumper.add_representer( + esm_parser.provenance.ListWithProvenance, listwithprov_representer + ) + EsmConfigDumper.add_representer( + esm_parser.provenance.DictWithProvenance, dictwithprov_representer + ) + # @Paul: this is me just playing around with things, this should be included maybe + # somewhere else and generalized for Str, Int, Bool... + EsmConfigDumper.add_representer( + esm_parser.provenance.StrWithProvenance, strwithprov_representer + ) + if "oasis3mct" in config: EsmConfigDumper.add_representer(esm_runscripts.oasis.oasis, oasis_representer) @@ -328,6 +352,8 @@ class EsmConfigDumper(yaml.dumper.Dumper): config_final = copy.deepcopy(config) # PrevRunInfo del config_final["prev_run"] # PrevRunInfo + config_final = esm_parser.provenance.clean_provenance(config_final) + out = yaml.dump( config_final, Dumper=EsmConfigDumper, width=10000, indent=4 ) # PrevRunInfo From 12c96e67c84172b5bc734fe8895bff4e391dcdcc Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Fri, 8 Mar 2024 14:48:29 +0100 Subject: [PATCH 106/107] complete the docstrings for provenance --- src/esm_parser/provenance.py | 354 +++++++++++++++++++++++------------ 1 file changed, 236 insertions(+), 118 deletions(-) diff --git a/src/esm_parser/provenance.py b/src/esm_parser/provenance.py index a47b90d25..5b64584d9 100644 --- a/src/esm_parser/provenance.py +++ b/src/esm_parser/provenance.py @@ -1,18 +1,57 @@ +""" +Provenance's dark magic. The basic idea is that one use the following to understand +from which yaml file (line and column) a variable in ``config`` is coming from: + +.. code-block:: python + + config["fesom"]["version"].provenance + +And that will return a list of the provenance history of that variable, for example: + +.. code-block:: python + + [{'category': 'components', + 'col': 10, + 'line': 6, + 'yaml_file': '/Users/mandresm/Codes/esm_tools/configs/components/fesom/fesom-2.0.yaml'}, + {'category': 'setups', + 'col': 18, + 'extended_by': 'dict.__setitem__', + 'line': 321, + 'yaml_file': '/Users/mandresm/Codes/esm_tools/configs/setups/awicm3/awicm3.yaml'}] + +The last element in the provenance list represents the provenance of the current value +(last provenance). + +This module contains: +* The provenance class, to store the provenance of values with extended functionality +* A wrapper factory to create classes and objects dynamically that subclass the value + types and append provenances to them (``WithProvenance`` classes) +* Class attributes common to all ``WithProvenance`` classes +* Classes for mappings with provenance (dictionaries and lists) to recursively put and + get provenance from nested values, and extend the standard mapping methods + (``__setitem__``, ``update``...) +* A decorator to keep provenance in ``esm_parser``'s recursive functions +* A method to clean provenance recursively and get back the data without provenance +""" + import copy import esm_parser - from esm_calendar import Date +# ================= +# PROVENANCE CLASS +# ================= class Provenance(list): """ A subclass of list in which each element represents the provenance of the value at a point in the key-value history. The whole point of this class is to have a list subclass that allows us to include information about which function is - changing the list whithin each provenance element. + changing the list within each provenance element. - To assign the provenance to a value instanciate it as an attribute of that value + To assign the provenance to a value, instanciate it as an attribute of that value (i.e. ``self.provenance = Provenance(my_provenance)``). To be used from the ``WithProvenance`` classes created by ``wrapper_with_provenance_factory``. @@ -32,7 +71,7 @@ def __init__(self, provenance_data): Parameters ---------- provenance_data : list - List of provenance elements that describe the history of a key-value, or + List of provenance elements that describes the history of a key-value, or a single provenance element. """ @@ -111,16 +150,27 @@ def add_modified_by(self, provenance_step, func, modified_by="modified_by"): return provenance_step +# ======================================================== +# PROVENANCE WRAPPER FACTORY CLASS METHODS AND PROPERTIES +# ======================================================== @classmethod def wrapper_with_provenance_new(cls, *args, **kwargs): + """ + To be used as the ``__new__`` method for WithProvenance classes. This is key for + ``copy.deepcopy``, without this ``copy.deepcopy`` breaks. + """ return super(cls, cls).__new__(cls, args[1]) def wrapper_with_provenance_init(self, value, provenance=None): """ + To be used as the ``__init__`` method for WithProvenance classes. Adds the + ``provenance`` value as an instance of ``Provenance`` to the ``self._provenance`` + attribute, and stores the original ``value`` to the ``self.value`` attribute. + Parameters ---------- - value : bool, None + value : any Value of the object provenance : any The provenance information @@ -138,11 +188,34 @@ def wrapper_with_provenance_init(self, value, provenance=None): @property def prop_provenance(self): + """ + To be used as the ``provenance`` property in WithProvenance classes. + + Returns + ------- + self._provenance : esm_parser.provenance.Provenance + The provenance history stored in ``self._provenance`` + """ return self._provenance @prop_provenance.setter def prop_provenance(self, new_provenance): + """ + Setter for the ``provenance`` property of WithProvenance classes. Makes sure that + any value assigned to this property is a ``Provenance`` object and if it is not + returns an error. + + Parameters + ---------- + new_provenance : esm_parser.provenance.Provenance + New provenance history to be set + + Raises + ------ + ValueError : + If the given ``new_provenance`` is not a ``Provenance`` object + """ # Check if new_provenance is an instance of Provenance if not isinstance(new_provenance, Provenance): raise ValueError( @@ -152,6 +225,9 @@ def prop_provenance(self, new_provenance): self._provenance = new_provenance +# ======================================================= +# CLASSES FOR THE UNSUBCLASSABLE CLASSES (BOOL AND NONE) +# ======================================================= class ProvenanceClassForTheUnsubclassable: """ A class to reproduce the methods of the unclassable ``bool`` and ``NoneType`` @@ -177,11 +253,21 @@ def __hash__(self): return hash(self.value) +# Add the class attributes that are common to all WithProvenance classes ProvenanceClassForTheUnsubclassable.__init__ = wrapper_with_provenance_init ProvenanceClassForTheUnsubclassable.provenance = prop_provenance class BoolWithProvenance(ProvenanceClassForTheUnsubclassable): + """ + Class for emulating ``Bool`` behaviour, but with Provenance. + + Objects of this class reproduce the following ``Bool`` behaviours: + * ``isinstance(, bool)`` returns ``True`` + * `` == True`` returns ``True`` + * `` is True`` returns ``False``. This is not reproducing the behavior! + """ + @property def __class__(self): """ @@ -191,30 +277,39 @@ def __class__(self): class NoneWithProvenance(ProvenanceClassForTheUnsubclassable): - def __init__(self, value, provenance): - print(provenance) - self.value = value - self.provenance = Provenance(provenance) + """ + Class for emulating ``None`` behaviour, but with Provenance. + + Objects of this class reproduce the following ``None`` behaviours: + * ``isinstance(, None)`` returns ``True`` + * `` == None`` returns ``True`` + * `` is None`` returns ``False``. This is not reproducing the behavior! + """ @property def __class__(self): """ - This is here for having ``isinstance(, None)`` return ``True`` + This is here for having ``isinstance(, None)`` return + ``True`` """ return type(None) +# ================================ +# WRAPPER WITH PROVENANCE FACTORY +# ================================ def wrapper_with_provenance_factory(value, provenance=None): """ A function to subclass and instanciate all types of subclassable objects in the - ESM-Tools ``config`` and add the ``provenance`` attribute to them. It uses the - ``WrapperWithProvenance`` class defined within the function for that purpose. For classes - that are not subclassable (``bool`` and ``NoneType``) intanciates an object that - mimics their behaviour but also contains the ``provenance`` attribute. + ESM-Tools ``config`` and add the ``provenance`` attribute to them. It also creates + the ``{type(value)}WithProvenance`` classes globally on the fly depending on the + ``value``'s type, if it doesn't exist yet. For classes that are not subclassable + (``Date``, ``Bool`` and ``NoneType``) intanciates an object that mimics their + behaviour but also contains the ``provenance`` attribute. Objects of type ``esm_calendar.esm_calendar.Date`` are not subclassed (and the ``provenance`` attribute is simply added to them, because they fail to be subclassed - with in the ``WrapperWithProvenance`` with the following error:: + with in the ``DateWithProvenance`` with the following error:: __new__ method giving error object.__new__() takes exactly one argument (the type to instantiate) @@ -228,7 +323,7 @@ def wrapper_with_provenance_factory(value, provenance=None): Returns ------- - WrapperWithProvenance, esm_calendar.esm_calendar.Date, BoolWithProvenance, + {type(value)}WithProvenance, esm_calendar.esm_calendar.Date, BoolWithProvenance, NoneWithProvenance The new instance with the ``provenance`` attribute """ @@ -269,83 +364,53 @@ def wrapper_with_provenance_factory(value, provenance=None): return globals()[class_name](value, provenance) +# ========================= +# MAPPINGS WITH PROVENANCE +# ========================= class DictWithProvenance(dict): - # TODO: this is an incorrect description """ - A dictionary subclass that contains a ``provenance`` attribute. This attribute is - a ``dict`` that contains those `keys` of the original dictionary whose `values` - **are not a** ``dict`` (leaves of the dictionary tree), and a provenance value - defined during the instancing of the object. The ``provenance`` attribute is - applied recursively within the nested dictionaries during instancing or when the - ``self.set_provenance()`` is used. - - Example - ------- - After instancing the object: - - .. code-block:: python - - dict_with_provenance = DictWithProvenance(config_dict, {"file": "echam.yaml"}) - - where ``config_dict`` is defined as: - - .. code-block:: python + A dictionary subclass that contains methods for: + * recursively transforming leaf values into provenance (``put_provenance`` and + ``set_provenance``) + * recursively retrieving provenance from nested values + * extending the ``dict.__init__`` method to recursively assign provenance to all + nested values + * extending the ``dict.__setitem__`` method to keep a record of previous history + when adding new values to a given key + * extending the ``dict.update`` method to keep a record of the previous history + when updating the dictionary - config_dict = { - "echam": { - "type": "atmosphere", - "files": { - "greenhouse": {"kind": "input", "path_in_computer": "/my/path/in/computer"} - }, - } - } + Use + --- + Instance a new ``DictWithProvenance`` object:: - then ``config_dict["echam"].provenance`` will take the following values: + dict_with_provenance = DictWithProvenance(, ) - .. code-block:: python + Redefine the provenance of an existing ``DictWithProvenance`` with the same + provenance for all its nested values:: - # >>> config_dict["echam"].provenance - {'type': {'file': 'echam.yaml'}} + dict_with_provenance.set_provenance() - Note that the `key` ``"files"`` does not exist as the value for that key in the - ``config_dict`` is a dictionary (**it is not a leaf of the dictionary tree**). + Set the provenace of a specific leaf within a nested dictionary:: - The `provenance value` can be defined to be any python object. The ``provenance`` - attribute is inherited when merging dictionaries with the ``update`` method - when merging two ``DictWithProvenance`` objects, with the same rewriting strategy - as for the keys in the dictionary, and ``provenance`` is also inherited when - redefining a `value` to contain a ``DictWithProvenance``. + dict_with_provenance["key1"]["key1"].provenance = - Use - --- - Instance a new ``DictWithProvenance`` object:: + Get the ``provenance`` representation of the whole dictionary:: - dict_with_provenance = DictWithProvenance(, ) - - Redefine the provenance of an existing ``key``:: - - dict_with_provenance[""].set_provenance() - - Set the provenace of a specific leaf within a nested dictionary:: - - dict_with_provenance["key1"]["key1"].provenance["leaf_key"] = - - Get the ``provenance`` representation of the dictionary:: - - provenance_dict = dict_with_provenance.get_provenance() + provenance_dict = dict_with_provenance.get_provenance() """ def __init__(self, dictionary, provenance): - # TODO: this is an incorrect description """ Instanciates the ``dictionary`` as an object of ``DictWithProvenance`` and - defines its ``provenance`` attribute recursively with ``set_provenance``. + defines its nested values as objects of WithProvenance classes, assigning them + recursively the corresponding ``provenance`` attribute with ``set_provenance``. Parameters ---------- dictionary : dict The ``dict`` that needs to be converted to a ``DictWithProvenance`` object - provenance : any + provenance : dict The provenance that will be recursively assigned to all leaves of the dictionary tree """ @@ -357,18 +422,20 @@ def __init__(self, dictionary, provenance): self.custom_setitem = True def put_provenance(self, provenance): - # TODO: this is an incorrect description """ - Defines recursively the ``provenance`` of the ``DictWithProvenance`` object - ``self`` or it's nested ``dictionary``. + Recursively transforms every value in ``DictWithProvenance`` into its + corresponding WithProvenance object and appends its corresponding + ``provenance``. Each value has its corresponding provenance defined in the + ``provenance`` dictionary, and this method just groups them together 1-to-1. Parameters ---------- - provenance : any - dictionary : dict - Dictionary for which the ``provenance`` is to be set. When a value is not - given, the ``dictionary`` takes the value of ``self``. Only for recursion - within nested ``DictWithProvenance``, do not use it outside of this method. + provenance : dict + The provenance that will be recursively assigned to all leaves of the + dictionary tree. The provenance needs to be a ``dict`` with the same keys + as ``self`` (same structure) so that it can successfully transfer each + provenance value to its corresponding value on ``self`` (1-to-1 + conrrespondance). """ for key, val in self.items(): @@ -384,19 +451,16 @@ def put_provenance(self, provenance): ) def set_provenance(self, provenance): - # TODO: this is an incorrect description """ - Defines recursively the ``provenance`` of the ``DictWithProvenance`` object - ``self`` or it's nested ``dictionary``. + Recursively transforms every value in ``DictWithProvenance`` into its + corresponding WithProvenance object and appends the same ``provenance`` to it. + Note that this method differs from ``put_provenance`` in that the same + ``provenance`` value is applied to the different values of ``self``. Parameters ---------- provenance : any New `provenance value` to be set - dictionary : dict - Dictionary for which the ``provenance`` is to be set. When a value is not - given, the ``dictionary`` takes the value of ``self``. Only for recursion - within nested ``DictWithProvenance``, do not use it outside of this method. """ if not isinstance(provenance, list): provenance = [provenance] @@ -422,11 +486,9 @@ def get_provenance(self, index=-1): Parameters ---------- - dictionary : dict - Dictionary for which the provenance needs to be extracted. When a value is - not given, the ``dictionary`` takes the value of ``self``. Only for - recursion within nested ``DictWithProvenance``, do not use it outside of - this method. + index : int + Defines the element of the provenance history to be returned. The default + is ``-1``, meaning the last provenance (the one of the current value). Returns ------- @@ -513,7 +575,51 @@ def update(self, dictionary, *args, **kwargs): class ListWithProvenance(list): + """ + A list subclass that contains methods for: + * recursively transforming leaf values into provenance (``put_provenance`` and + ``set_provenance``) + * recursively retrieving provenance from nested values + * extending the ``list.__init__`` method to recursively assign provenance to all + nested values + * extending the ``list.__setitem__`` method to keep a record of previous history + when adding new values to a given key + + Use + --- + Instance a new ``ListWithProvenance`` object:: + + list_with_provenance = ListWithProvenance(, ) + + Redefine the provenance of an existing ``ListWithProvenance`` with the same + provenance for all its nested values:: + + list_with_provenance.set_provenance() + + Set the provenace of the element 0 of a list:: + + list_with_provenance[0].provenance = + + Get the ``provenance`` representation of the whole list:: + + provenance_list = list_with_provenance.get_provenance() + """ + def __init__(self, mylist, provenance): + """ + Instanciates the ``list`` as an object of ``ListWithProvenance`` and defines + its nested values as objects of WithProvenance classes, assigning them + recursively the corresponding ``provenance`` attribute with ``set_provenance``. + + Parameters + ---------- + mylist : list + The ``list`` that needs to be converted to a ``ListWithProvenance`` object + provenance : list + The provenance that will be recursively assigned to all leaves of the + dictionary tree + """ + super().__init__(mylist) self.custom_setitem = False @@ -521,6 +627,22 @@ def __init__(self, mylist, provenance): self.custom_setitem = True def put_provenance(self, provenance): + """ + Recursively transforms every value in ``ListWithProvenance`` into its + corresponding WithProvenance object and appends its corresponding + ``provenance``. Each value has its corresponding provenance defined in the + ``provenance`` list, and this method just groups them together 1-to-1. + + Parameters + ---------- + provenance : list + The provenance that will be recursively assigned to all elements of the + list. The provenance needs to be a ``list`` with the same number of elements + as ``self`` (same structure) so that it can successfully transfer each + provenance value to its corresponding value on ``self`` (1-to-1 + conrrespondance). + """ + if not provenance: provenance = [{}] * len(self) @@ -535,19 +657,16 @@ def put_provenance(self, provenance): self[c] = wrapper_with_provenance_factory(elem, provenance[c]) def set_provenance(self, provenance): - # TODO: this is an incorrect description """ - Defines recursively the ``provenance`` of the ``DictWithProvenance`` object - ``self`` or it's nested ``dictionary``. + Recursively transforms every value in ``ListWithProvenance`` into its + corresponding WithProvenance object and appends the same ``provenance`` to it. + Note that this method differs from ``put_provenance`` in that the same + ``provenance`` value is applied to the different values of ``self``. Parameters ---------- provenance : any New `provenance value` to be set - dictionary : dict - Dictionary for which the ``provenance`` is to be set. When a value is not - given, the ``dictionary`` takes the value of ``self``. Only for recursion - within nested ``DictWithProvenance``, do not use it outside of this method. """ if not isinstance(provenance, list): provenance = [provenance] @@ -566,24 +685,21 @@ def set_provenance(self, provenance): def get_provenance(self, index=-1): """ - Returns a ``dictionary`` containing the all the nested provenance information - of the current ``DictWithProvenance`` with a structure and `keys` equivalent to - the ``self`` dictionary, but with `values` of the `key` leaves those of the - provenance. + Returns a ``list`` containing the all the nested provenance information + of the current ``ListWithProvenance`` with a structure equivalent to the + ``self`` list, but with list elements been provenance values. Parameters ---------- - dictionary : dict - Dictionary for which the provenance needs to be extracted. When a value is - not given, the ``dictionary`` takes the value of ``self``. Only for - recursion within nested ``DictWithProvenance``, do not use it outside of - this method. + index : int + Defines the element of the provenance history to be returned. The default + is ``-1``, meaning the last provenance (the one of the current value). Returns ------- - provenance_list : dict - A dictionary with a structure and `keys` equivalent to the ``self`` - dictionary, but with `values` of the `key` leaves those of the provenance + provenance_list : list + A list with a structure equivalent to that of the ``self`` list, but with + the `values` of the provenance of each element """ provenance_list = [] @@ -633,20 +749,19 @@ def __setitem__(self, indx, val): super().__setitem__(indx, val_new) +# Define the global variable PROVENANCE_MAPPINGS for operations such as ``isinstance`` PROVENANCE_MAPPINGS = (DictWithProvenance, ListWithProvenance) +# ============================================== +# DECORATORS FOR ESM_PARSER RECURSIVE FUNCTIONS +# e.g. find_variable, purify_booleans... +# ============================================== def keep_provenance_in_recursive_function(func): - # TODO: this is an incorrect description """ Decorator for recursive functions in ``esm_parser`` to preserve provenance. - Recursive run functions in ``esm_parser`` are generally called on the innermost - leaf. Here, we still run the function, but additionally store the output of the - function into the `leaf_id_provenance` container so that provenance can be added - to the result of the function call. - Parameters ---------- func : Callable @@ -691,6 +806,9 @@ def inner(tree, rhs, *args, **kwargs): return inner +# ======== +# HELPERS +# ======== def clean_provenance(data): """ Returns the values of provenance mappings in their original classes (without the From aacb99710d7335d099719ca6d24abe053a8d6d35 Mon Sep 17 00:00:00 2001 From: Miguel Andres-Martinez Date: Fri, 8 Mar 2024 15:03:55 +0100 Subject: [PATCH 107/107] correct docstrings in provenance tests --- tests/test_esm_parser/test_provenance.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_esm_parser/test_provenance.py b/tests/test_esm_parser/test_provenance.py index 84fabc109..0219602c2 100644 --- a/tests/test_esm_parser/test_provenance.py +++ b/tests/test_esm_parser/test_provenance.py @@ -197,7 +197,7 @@ def test_set_provenance_for_a_new_leaf(config, check_provenance): def test_provenance_update(config, check_provenance): """ - Checks that the method update preserves the provenance history + Test 9: Checks that the method update preserves the provenance history """ new_prov = { @@ -370,8 +370,8 @@ def test_check_set_provenance_of_single_list_entry(example_path1): def test_check_set_provenance_of_single_list_entry(example_path1): """ - Check get_provenance raises the correct error when the provenance of an item is not - of the type provenance.Provenance + Test 14: Check get_provenance raises the correct error when the provenance of an + item is not of the type provenance.Provenance """ os.environ["USER"] = "some_user" @@ -397,6 +397,10 @@ def test_check_set_provenance_of_single_list_entry(example_path1): def test_keep_provenance_in_recursive_function(config): + """ + Test 15: Test that provenance is not modified in a method when it has the + keep_provenance_in_recursive_function decorator + """ @provenance.keep_provenance_in_recursive_function def change_elem(tree, rhs): return provenance.wrapper_with_provenance_factory("new_val", {"modified": True})