Repository: haddocking/haddock3

Issue:
parameter `--restart` likely not functioning properly - investigate

Comments:

Patch:
diff --git a/src/haddock/clis/cli.py b/src/haddock/clis/cli.py
index 36932a86..bcb48ac3 100755
--- a/src/haddock/clis/cli.py
+++ b/src/haddock/clis/cli.py
@@ -6,7 +6,8 @@ from argparse import ArgumentTypeError
 from functools import partial
 
 from haddock.version import CURRENT_VERSION
-from haddock.libs.libutil import file_exists, non_negative_int
+from haddock.libs.libutil import file_exists
+from haddock.gear.restart_run import add_restart_arg
 
 
 # Command line interface parser
@@ -22,17 +23,7 @@ ap.add_argument(
     help="The input recipe file path",
     )
 
-_arg_pos_int = partial(
-    non_negative_int,
-    exception=ArgumentTypeError,
-    emsg="Minimum value is 0, {!r} given.",
-    )
-ap.add_argument(
-    "--restart",
-    type=_arg_pos_int,
-    default=0,
-    help="Restart the recipe from this course",
-    )
+add_restart_arg(ap)
 
 ap.add_argument(
     "--setup",
@@ -75,7 +66,7 @@ def maincli():
 
 def main(
         recipe,
-        restart=0,
+        restart=None,
         setup_only=False,
         log_level="INFO",
         ):
@@ -112,7 +103,7 @@ def main(
     logging.info(get_initial_greeting())
 
     try:
-        params, other_params = setup_run(recipe)
+        params, other_params = setup_run(recipe, restart_from=restart)
 
     except ConfigurationError as err:
         logging.error(err)
diff --git a/src/haddock/gear/prepare_run.py b/src/haddock/gear/prepare_run.py
index 2f6ffbb5..5e2e5bf6 100644
--- a/src/haddock/gear/prepare_run.py
+++ b/src/haddock/gear/prepare_run.py
@@ -11,6 +11,7 @@ from haddock import haddock3_source_path
 from haddock.modules import modules_category
 from haddock.error import ConfigurationError
 from haddock.gear.parameters import config_mandatory_general_parameters
+from haddock.gear.restart_run import remove_folders_after_number
 from haddock.libs.libutil import (
     copy_files_to_dir,
     make_list_if_string,
@@ -41,7 +42,7 @@ def with_config_error(func):
     return wrapper
 
 
-def setup_run(workflow_path):
+def setup_run(workflow_path, restart_from=None):
     """
     Setup HADDOCK3 run.
 
@@ -55,6 +56,15 @@ def setup_run(workflow_path):
     #6 : create the needed folders/files to start the run
     #7 : copy additional files to run folder
 
+    Parameters
+    ----------
+    workflow_path : str or pathlib.Path
+        The path to the configuration file.
+
+    erase_previous : bool
+        Whether to erase the previous run folder and reprare from
+        scratch. Defaults to `True`.
+
     Returns
     -------
     tuple of two dicts
@@ -77,12 +87,16 @@ def setup_run(workflow_path):
         )
     validate_modules_params(modules_params)
 
-    # prepares the run folders
-    remove_folder(params['run_dir'])
-    begin_dir, _ = create_begin_files(params)
+    if restart_from is None:
+        # prepares the run folders
+        remove_folder(params['run_dir'])
+        begin_dir, _ = create_begin_files(params)
+
+        # prepare other files
+        copy_ambig_files(modules_params, begin_dir)
 
-    # prepare other files
-    copy_ambig_files(modules_params, begin_dir)
+    else:
+        remove_folders_after_number(params['run_dir'], restart_from)
 
     # return the modules' parameters and other parameters that may serve
     # the workflow, the "other parameters" can be expanded in the future
diff --git a/src/haddock/gear/restart_run.py b/src/haddock/gear/restart_run.py
new file mode 100644
index 00000000..8028ff85
--- /dev/null
+++ b/src/haddock/gear/restart_run.py
@@ -0,0 +1,59 @@
+"""Features to allow run restart from a given step."""
+from argparse import ArgumentTypeError
+from functools import partial
+
+from haddock.libs.libutil import non_negative_int, remove_folder
+
+
+_help_cli = """Restart the run from a given step. Previous folders from
+the selected step onward will be deleted."""
+
+
+_arg_non_neg_int = partial(
+    non_negative_int,
+    exception=ArgumentTypeError,
+    emsg="Minimum value is 0, {!r} given.",
+    )
+
+
+def add_restart_arg(parser):
+    """Adds `--restart` option to argument parser."""
+    parser.add_argument(
+        "--restart",
+        type=_arg_non_neg_int,
+        default=None,
+        help=_help_cli,
+        )
+
+
+def remove_folders_after_number(run_dir, num):
+    """
+    Remove calculation folder after (included) a given number.
+
+    Example
+    -------
+    If the following step folders exist:
+
+        00_topoaa
+        01_rigidbody
+        02_mdref
+        03_flexref
+
+    and the number `2` is given, folders `02_` and `03_` will be
+    deleted.
+
+    Parameters
+    ----------
+    run_dir : pathlib.Path
+        The run directory.
+
+    num : int
+        The number of the folder from which to delete calculation step
+        folders. `num` must be non-negative integer, or equivalent
+        representation.
+    """
+    num = _arg_non_neg_int(num)
+    previous = sorted(list(run_dir.resolve().glob('[0-9][0-9]*/')))
+    for folder in previous[num:]:
+        remove_folder(folder)
+    return
diff --git a/src/haddock/libs/libutil.py b/src/haddock/libs/libutil.py
index dcd42376..cac05d66 100644
--- a/src/haddock/libs/libutil.py
+++ b/src/haddock/libs/libutil.py
@@ -111,7 +111,7 @@ def parse_ncores(n=None, njobs=None, max_cpus=None):
         raise SetupError(_msg) from err
 
     if n < 1:
-        _msg = "`n` is not positive, this is not possible."
+        _msg = f"`n` is not positive, this is not possible: {n!r}"
         raise SetupError(_msg)
 
     if njobs:
diff --git a/src/haddock/modules/__init__.py b/src/haddock/modules/__init__.py
index 5a5d2529..e7e1ffcf 100644
--- a/src/haddock/modules/__init__.py
+++ b/src/haddock/modules/__init__.py
@@ -102,7 +102,7 @@ class BaseHaddockModule(ABC):
     def previous_path(self):
         previous = sorted(list(self.path.resolve().parent.glob('[0-9][0-9]*/')))
         try:
-            return previous[-2]
+            return previous[self.order - 1]
         except IndexError:
             return self.path
 


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 85070308..fb4a6aba 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -31,26 +31,6 @@ def test_ap_setup_false():
     assert cmd.setup_only == False
 
 
-@pytest.mark.parametrize(
-    'n',
-    (0, 1, 10, 1230, 50000),
-    )
-def test_ap_restart(n):
-    cmd = ap.parse_args(f'{recipe} --restart {n}'.split())
-    assert cmd.restart == n
-
-
-@pytest.mark.parametrize(
-    'n',
-    (-1, -10, -1230, -50000),
-    )
-def test_ap_restart_error(n):
-    with pytest.raises(SystemExit) as exit:
-        cmd = ap.parse_args(f'{recipe} --restart {n}'.split())
-    assert exit.type == SystemExit
-    assert exit.value.code == 2
-
-
 def test_ap_version():
     with pytest.raises(SystemExit) as exit:
         ap.parse_args('-v'.split())
diff --git a/tests/test_gear_restart.py b/tests/test_gear_restart.py
new file mode 100644
index 00000000..99462fe0
--- /dev/null
+++ b/tests/test_gear_restart.py
@@ -0,0 +1,65 @@
+"""Test gear.restart_run."""
+import argparse
+
+import pytest
+
+from haddock.gear import restart_run
+
+
+def test_has_help():
+    """Assert module has _help_cli variable."""
+    assert restart_run._help_cli
+
+
+@pytest.mark.parametrize(
+    'i,expected',
+    [
+        ('0', 0),
+        ('1', 1),
+        ('57', 57),
+        (100, 100),
+        ]
+    )
+def test_non_neg_arg(i, expected):
+    """Test non negative arg type."""
+    r = restart_run._arg_non_neg_int(i)
+    assert r == expected
+
+
+@pytest.mark.parametrize(
+    'i,expected',
+    [
+        ('0', 0),
+        ('1', 1),
+        ('57', 57),
+        (100, 100),
+        ]
+    )
+def test_restart_cli(i, expected):
+    """Test non negative arg type."""
+    ap = argparse.ArgumentParser()
+    restart_run.add_restart_arg(ap)
+    cmd = ap.parse_args(f'--restart {i}'.split())
+    assert cmd.restart == expected
+
+
+@pytest.mark.parametrize(
+    'n',
+    (-1, -10, '-1230', -50000),
+    )
+def test_arg_non_neg_error(n):
+    with pytest.raises(argparse.ArgumentTypeError) as exit:
+        restart_run._arg_non_neg_int(n)
+
+
+@pytest.mark.parametrize(
+    'n',
+    (-1, -10, '-1230', -50000),
+    )
+def test_restart_cli_error(n):
+    ap = argparse.ArgumentParser()
+    restart_run.add_restart_arg(ap)
+    with pytest.raises(SystemExit) as exit:
+        cmd = ap.parse_args(f'--restart {n}'.split())
+    assert exit.type == SystemExit
+    assert exit.value.code == 2