snk-cli 0.7.1__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. {snk_cli-0.7.1 → snk_cli-0.8.0}/.github/workflows/publish.yml +1 -1
  2. snk_cli-0.8.0/.github/workflows/tests.yml +66 -0
  3. {snk_cli-0.7.1 → snk_cli-0.8.0}/PKG-INFO +8 -5
  4. {snk_cli-0.7.1 → snk_cli-0.8.0}/pyproject.toml +16 -4
  5. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/__about__.py +1 -1
  6. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/conda.py +12 -4
  7. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/config/config.py +1 -1
  8. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/dynamic_typer.py +16 -6
  9. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/options/utils.py +8 -9
  10. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/subcommands/env.py +12 -3
  11. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/subcommands/run.py +30 -25
  12. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/testing.py +3 -2
  13. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/validate.py +15 -4
  14. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/conftest.py +0 -3
  15. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_cli/test_run.py +1 -1
  16. snk_cli-0.8.0/tests/test_conda_env.py +24 -0
  17. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_dynamic_typer.py +23 -1
  18. snk_cli-0.7.1/.github/workflows/tests.yml +0 -41
  19. snk_cli-0.7.1/tests/test_conda_env.py +0 -11
  20. {snk_cli-0.7.1 → snk_cli-0.8.0}/.gitignore +0 -0
  21. {snk_cli-0.7.1 → snk_cli-0.8.0}/LICENSE.txt +0 -0
  22. {snk_cli-0.7.1 → snk_cli-0.8.0}/README.md +0 -0
  23. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/index.md +0 -0
  24. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/reference/cli.md +0 -0
  25. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/reference/config.md +0 -0
  26. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/reference/dynamic_typer.md +0 -0
  27. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/reference/options.md +0 -0
  28. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/reference/subcommands.md +0 -0
  29. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/reference/testing.md +0 -0
  30. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/reference/utils.md +0 -0
  31. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/reference/validate.md +0 -0
  32. {snk_cli-0.7.1 → snk_cli-0.8.0}/docs/reference/workflow.md +0 -0
  33. {snk_cli-0.7.1 → snk_cli-0.8.0}/mkdocs.yml +0 -0
  34. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/__init__.py +0 -0
  35. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/cli.py +0 -0
  36. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/config/__init__.py +0 -0
  37. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/config/utils.py +0 -0
  38. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/options/__init__.py +0 -0
  39. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/options/option.py +0 -0
  40. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/subcommands/__init__.py +0 -0
  41. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/subcommands/config.py +0 -0
  42. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/subcommands/profile.py +0 -0
  43. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/subcommands/script.py +0 -0
  44. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/utils.py +0 -0
  45. {snk_cli-0.7.1 → snk_cli-0.8.0}/src/snk_cli/workflow.py +0 -0
  46. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/__init__.py +0 -0
  47. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/artic_v4.1.bed +0 -0
  48. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/config.yaml +0 -0
  49. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/cov.fasta +0 -0
  50. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/print_config/Snakefile +0 -0
  51. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/print_config/cli.py +0 -0
  52. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/print_config/config.yaml +0 -0
  53. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/print_config/snk.yaml +0 -0
  54. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/cli.py +0 -0
  55. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/config.yaml +0 -0
  56. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/resources/data.txt +0 -0
  57. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/snk.yaml +0 -0
  58. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/things/__about__.py +0 -0
  59. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/workflow/Snakefile +0 -0
  60. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/workflow/envs/wget.yml +0 -0
  61. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/workflow/profiles/base/config.yaml +0 -0
  62. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/workflow/profiles/slurm/config.yaml +0 -0
  63. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/data/workflow/workflow/scripts/hello.py +0 -0
  64. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_SnkConfig.py +0 -0
  65. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_cli/__init__.py +0 -0
  66. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_cli/test_dynamic_options.py +0 -0
  67. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_cli/test_profile.py +0 -0
  68. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_cli/test_snk_config.py +0 -0
  69. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_cli/test_subcommands.py +0 -0
  70. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_cli/test_validate.py +0 -0
  71. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_cli/test_workflow_cli.py +0 -0
  72. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/test_types.py +0 -0
  73. {snk_cli-0.7.1 → snk_cli-0.8.0}/tests/utils.py +0 -0
@@ -14,7 +14,7 @@ jobs:
14
14
  - uses: actions/setup-python@v4
15
15
  with:
16
16
  python-version: 3.x
17
- - uses: actions/cache@v2
17
+ - uses: actions/cache@v5
18
18
  with:
19
19
  key: ${{ github.ref }}
20
20
  path: .cache
@@ -0,0 +1,66 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ - main
8
+ pull_request:
9
+ branches:
10
+ - "*"
11
+
12
+ permissions:
13
+ contents: write
14
+
15
+ jobs:
16
+ test:
17
+ runs-on: ubuntu-latest
18
+ strategy:
19
+ matrix:
20
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
21
+ snakemake-version: ["==7.32.4", ">8,<9", ">=9,<10"]
22
+ exclude:
23
+ # Exclude incompatible combinations
24
+ - python-version: "3.10"
25
+ snakemake-version: ">8,<9"
26
+ - python-version: "3.10"
27
+ snakemake-version: ">=9,<10"
28
+ include:
29
+ - python-version: "3.14"
30
+ snakemake-version: ">=9.22,<10"
31
+
32
+ steps:
33
+ - name: Checkout repository
34
+ uses: actions/checkout@v4
35
+ with:
36
+ fetch-depth: 0
37
+
38
+ - name: Set up conda
39
+ uses: conda-incubator/setup-miniconda@v3
40
+ with:
41
+ auto-activate-base: false
42
+ channels: conda-forge,bioconda,defaults
43
+ channel-priority: strict
44
+
45
+ - name: Install pip dependencies
46
+ run: |
47
+ pip install hatch
48
+
49
+ - name: Install system dependencies
50
+ run: |
51
+ sudo apt-get update
52
+ sudo apt-get install -y llvm clang build-essential graphviz
53
+
54
+ - name: Run tests
55
+ env:
56
+ AR: /usr/bin/ar
57
+ shell: bash
58
+ run: |
59
+ hatch python install ${{ matrix.python-version }}
60
+ hatch --env "snakemake.py${{ matrix.python-version }}-${{ matrix.snakemake-version }}" run cov
61
+
62
+ - name: Upload coverage reports to Codecov
63
+ uses: codecov/codecov-action@v3
64
+ with:
65
+ files: ./coverage.xml
66
+ flags: ${{ matrix.python-version }}-snakemake-${{ matrix.snakemake-version }}
@@ -1,11 +1,12 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: snk-cli
3
- Version: 0.7.1
3
+ Version: 0.8.0
4
4
  Project-URL: Documentation, https://github.com/wytamma/snk-cli#readme
5
5
  Project-URL: Issues, https://github.com/wytamma/snk-cli/issues
6
6
  Project-URL: Source, https://github.com/wytamma/snk-cli
7
7
  Author-email: Wytamma Wirth <wytamma.wirth@me.com>
8
- License: MIT
8
+ License-Expression: MIT
9
+ License-File: LICENSE.txt
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Programming Language :: Python
11
12
  Classifier: Programming Language :: Python :: 3.8
@@ -13,16 +14,18 @@ Classifier: Programming Language :: Python :: 3.9
13
14
  Classifier: Programming Language :: Python :: 3.10
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
16
19
  Classifier: Programming Language :: Python :: Implementation :: CPython
17
20
  Classifier: Programming Language :: Python :: Implementation :: PyPy
18
21
  Requires-Python: >=3.8
19
22
  Requires-Dist: art~=5.9
20
- Requires-Dist: datrie>=0.8.2
21
23
  Requires-Dist: makefun~=1.15
22
24
  Requires-Dist: pulp<2.8
23
25
  Requires-Dist: rich>=10.11.0
24
26
  Requires-Dist: shellingham>=1.3.0
25
- Requires-Dist: snakemake>=7
27
+ Requires-Dist: snakemake>=7; python_version < '3.14'
28
+ Requires-Dist: snakemake>=9.22; python_version >= '3.14'
26
29
  Requires-Dist: typer~=0.9
27
30
  Description-Content-Type: text/markdown
28
31
 
@@ -21,18 +21,20 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.10",
22
22
  "Programming Language :: Python :: 3.11",
23
23
  "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
24
26
  "Programming Language :: Python :: Implementation :: CPython",
25
27
  "Programming Language :: Python :: Implementation :: PyPy",
26
28
  ]
27
29
  dependencies = [
28
- "snakemake>=7",
30
+ "snakemake>=7; python_version < '3.14'",
31
+ "snakemake>=9.22; python_version >= '3.14'",
29
32
  "typer~=0.9",
30
33
  "shellingham >=1.3.0",
31
34
  "rich >=10.11.0",
32
35
  "pulp<2.8", # Pin pulp <2.8 for snakemake: https://github.com/snakemake/snakemake/issues/2607
33
36
  "art~=5.9",
34
37
  "makefun~=1.15",
35
- "datrie>=0.8.2",
36
38
  ]
37
39
 
38
40
  [project.urls]
@@ -45,7 +47,16 @@ path = "src/snk_cli/__about__.py"
45
47
 
46
48
 
47
49
  [[tool.hatch.envs.snakemake.matrix]]
48
- snakemake = ["==7.32.4", ">8"]
50
+ snakemake = ["==7.32.4"] # Compatible with Python 3.10–3.13
51
+ python = ["3.10", "3.11", "3.12", "3.13"]
52
+
53
+ [[tool.hatch.envs.snakemake.matrix]]
54
+ snakemake = [">8,<9", ">=9,<10"]
55
+ python = ["3.11", "3.12", "3.13"]
56
+
57
+ [[tool.hatch.envs.snakemake.matrix]]
58
+ snakemake = [">=9.22,<10"] # Python 3.14 support starts with Snakemake 9.22
59
+ python = ["3.14"]
49
60
 
50
61
  [tool.hatch.envs.default]
51
62
  dependencies = [
@@ -64,9 +75,10 @@ cov = [
64
75
  "test-cov",
65
76
  "cov-report",
66
77
  ]
78
+ list-deps = "pip list"
67
79
 
68
80
  [[tool.hatch.envs.all.matrix]]
69
- python = ["3.9", "3.10", "3.11", "3.12"]
81
+ python = ["3.8", "3.9", "3.10", "3.11", "3.12"]
70
82
 
71
83
  [tool.hatch.envs.types]
72
84
  dependencies = [
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Wytamma Wirth <wytamma.wirth@me.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.7.1"
4
+ __version__ = "0.8.0"
@@ -1,21 +1,22 @@
1
1
  # This file contains functions to create and manage conda environments for snakemake workflows
2
- # it needs to work with v 7 and 8 of snakemake
2
+ # it needs to work with v 7, 8 and 9 of snakemake
3
3
  #
4
4
  from pathlib import Path
5
5
  from packaging import version
6
6
  from dataclasses import dataclass
7
+ import inspect
7
8
  import os
8
9
 
9
10
  from snk_cli.utils import check_command_available
10
11
  from snakemake.deployment.conda import Env
11
- from snakemake.persistence import Persistence
12
12
  import snakemake
13
13
 
14
14
  snakemake_version = version.parse(snakemake.__version__)
15
15
  is_snakemake_version_8_or_above = snakemake_version >= version.parse('8')
16
+ is_snakemake_version_9_or_above = snakemake_version >= version.parse('9')
16
17
 
17
18
  @dataclass
18
- class PersistenceMock(Persistence):
19
+ class PersistenceMock:
19
20
  """
20
21
  Mock for workflow.persistence
21
22
  """
@@ -27,9 +28,14 @@ class PersistenceMock(Persistence):
27
28
  container_img_path: Path = None
28
29
  aux_path: Path = None
29
30
 
31
+ def __post_init__(self):
32
+ for path in (self.conda_env_path, self.conda_env_archive_path):
33
+ if path:
34
+ Path(path).mkdir(parents=True, exist_ok=True)
35
+
30
36
 
31
37
  def get_frontend():
32
- if check_command_available("mamba"):
38
+ if check_command_available("mamba") and not is_snakemake_version_9_or_above:
33
39
  conda_frontend = "mamba"
34
40
  else:
35
41
  conda_frontend = "conda"
@@ -70,6 +76,7 @@ def create_workflow_v8(
70
76
  WorkflowSettings,
71
77
  StorageSettings,
72
78
  )
79
+ workflow_kwargs = {"logger_manager": None} if "logger_manager" in inspect.signature(Workflow).parameters else {}
73
80
  conda_frontend = get_frontend()
74
81
  workflow = Workflow(
75
82
  config_settings=ConfigSettings(),
@@ -80,6 +87,7 @@ def create_workflow_v8(
80
87
  conda_frontend=conda_frontend,
81
88
  conda_prefix=conda_prefix
82
89
  ),
90
+ **workflow_kwargs,
83
91
  )
84
92
  persistence = PersistenceMock(
85
93
  conda_env_path=Path(conda_prefix).resolve() if conda_prefix else None,
@@ -117,7 +117,7 @@ class SnkConfig:
117
117
  invalid_config_keys = set(snk_config_dict.keys()) - fields
118
118
  if invalid_config_keys:
119
119
  import warnings
120
- warnings.warn(f"invalid keys in `snk.yaml` file: {invalid_config_keys}.")
120
+ warnings.warn(f"""invalid keys '{" ".join(invalid_config_keys)}' in {snk_config_path}.""")
121
121
  # filer out any invalid keys
122
122
  snk_config_dict = {k: v for k, v in snk_config_dict.items() if k in fields}
123
123
  snk_config = cls(**snk_config_dict)
@@ -1,6 +1,5 @@
1
1
  import typer
2
- from click import Tuple
3
- from typing import List, Callable, get_origin
2
+ from typing import List, Callable, get_args, get_origin
4
3
  from inspect import signature, Parameter
5
4
  from makefun import with_signature
6
5
  from enum import Enum
@@ -159,10 +158,7 @@ class DynamicTyper:
159
158
  annotation_type = Enum(f'{option.name}', {str(e): annotation_type(e) for e in option.choices})
160
159
  click_type = None
161
160
  if get_origin(annotation_type) is dict or annotation_type is dict:
162
- click_type = Tuple([str, str])
163
- if hasattr(annotation_type, '__args__') and len(annotation_type.__args__) == 2:
164
- click_type = Tuple([str, annotation_type.__args__[1]])
165
- annotation_type = List[Tuple]
161
+ annotation_type, click_type = self._get_dict_cli_types(annotation_type)
166
162
  if type(default) is dict:
167
163
  default = [[k, v] for k, v in default.items()]
168
164
  return Parameter(
@@ -179,6 +175,20 @@ class DynamicTyper:
179
175
  annotation=annotation_type,
180
176
  )
181
177
 
178
+ @staticmethod
179
+ def _get_dict_cli_types(dict_type):
180
+ """
181
+ Return the annotation and parser type for a repeatable key-value option.
182
+
183
+ Typer uses the outer List to make an option repeatable, but does not
184
+ support parameterized tuples inside a List. The unparameterized tuple
185
+ accurately describes each parsed value, while click_type supplies the
186
+ concrete key and value parsers.
187
+ """
188
+ type_args = get_args(dict_type)
189
+ value_type = type_args[1] if len(type_args) == 2 else str
190
+ return List[tuple], (str, value_type)
191
+
182
192
  def check_if_option_passed_via_command_line(self, option: Option):
183
193
  """
184
194
  Check if an option is passed via the command line.
@@ -1,11 +1,10 @@
1
- from typing import List, Tuple, get_origin
1
+ from typing import Dict, List, Tuple, get_origin
2
2
  from ..config.config import SnkConfig
3
3
  from ..utils import get_default_type, flatten
4
4
  from .option import Option
5
5
  from pathlib import Path
6
- from enum import Enum
7
6
 
8
- types = {
7
+ allowed_option_types = {
9
8
  "int": int,
10
9
  "integer": int,
11
10
  "float": float,
@@ -21,8 +20,8 @@ types = {
21
20
  "list[float]": List[float],
22
21
  "pair": Tuple[str, str],
23
22
  "dict": dict,
24
- "dict[str, str]": dict[str, str],
25
- "dict[str, int]": dict[str, int],
23
+ "dict[str,str]": Dict[str, str],
24
+ "dict[str,int]": Dict[str, int],
26
25
  }
27
26
 
28
27
  # Define the basic types for the combinations
@@ -31,7 +30,7 @@ basic_types = [int, str, bool, float]
31
30
  # Add the combinations of the basic types to the `types` dictionary
32
31
  for t1 in basic_types:
33
32
  for t2 in basic_types:
34
- types[f"pair[{t1.__name__}, {t2.__name__}]"] = Tuple[t1, t2]
33
+ allowed_option_types[f"pair[{t1.__name__},{t2.__name__}]"] = Tuple[t1, t2]
35
34
 
36
35
  def get_keys_from_annotation(annotations):
37
36
  # Get the unique keys from the annotations
@@ -68,10 +67,10 @@ def create_option_from_annotation(
68
67
  updated = True
69
68
  annotation_type = annotation_values.get(f"{annotation_key}:type", None)
70
69
  if annotation_type is not None:
71
- annotation_type = annotation_type.lower()
72
- assert annotation_type in types, f"Type '{annotation_type}' not supported."
70
+ annotation_type = annotation_type.lower().replace(" ", "")
71
+ assert annotation_type in allowed_option_types, f"Type '{annotation_type}' for '{annotation_key}' is not supported."
73
72
  annotation_type = (annotation_type or get_default_type(default)).lower()
74
- annotation_type = types.get(
73
+ annotation_type = allowed_option_types.get(
75
74
  annotation_type, List[str] if "list" in annotation_type else str
76
75
  )
77
76
  name = annotation_values.get(
@@ -12,6 +12,10 @@ from ..workflow import Workflow
12
12
  from rich.console import Console
13
13
  from rich.syntax import Syntax
14
14
  from snakemake.deployment.conda import Conda, CreateCondaEnvironmentException
15
+ try:
16
+ from snakemake_interface_common.exceptions import WorkflowError
17
+ except ImportError:
18
+ from snakemake.exceptions import WorkflowError
15
19
 
16
20
  from concurrent.futures import ProcessPoolExecutor
17
21
 
@@ -30,7 +34,7 @@ def create_conda_environment_wrapper(args):
30
34
  env_path = Path(env_path_str).resolve()
31
35
  try:
32
36
  conda_environment_factory(env_path, conda_prefix_dir).create()
33
- except CreateCondaEnvironmentException as e:
37
+ except (CreateCondaEnvironmentException, WorkflowError) as e:
34
38
  typer.secho(str(e), fg="red", err=True)
35
39
  return 1
36
40
  return 0
@@ -160,8 +164,13 @@ class EnvApp(DynamicTyper):
160
164
  )
161
165
  for path in env_paths
162
166
  ]
163
- with ProcessPoolExecutor(max_workers=max_workers) as executor:
164
- status_codes = executor.map(create_conda_environment_wrapper, env_args)
167
+ if max_workers == 1:
168
+ status_codes = [
169
+ create_conda_environment_wrapper(args) for args in env_args
170
+ ]
171
+ else:
172
+ with ProcessPoolExecutor(max_workers=max_workers) as executor:
173
+ status_codes = list(executor.map(create_conda_environment_wrapper, env_args))
165
174
  if any(status_codes):
166
175
  self.error("Failed to create all conda environments!")
167
176
  if names:
@@ -5,7 +5,10 @@ from contextlib import contextmanager
5
5
 
6
6
  from snk_cli.dynamic_typer import DynamicTyper
7
7
  from snk_cli.options.option import Option
8
- from snk_cli.conda import is_snakemake_version_8_or_above
8
+ from snk_cli.conda import (
9
+ is_snakemake_version_8_or_above,
10
+ is_snakemake_version_9_or_above,
11
+ )
9
12
  from ..workflow import Workflow
10
13
  from snk_cli.utils import (
11
14
  parse_config_args,
@@ -219,21 +222,24 @@ class RunApp(DynamicTyper):
219
222
  )
220
223
 
221
224
  if conda_found and self.snk_config.conda and not no_conda:
222
- args.extend(
223
- [
224
- "--use-conda",
225
- f"--conda-prefix={self.conda_prefix_dir}",
226
- ]
227
- )
228
- if not check_command_available("mamba"):
229
- if verbose:
230
- self.log(
231
- "Could not find mamba, using conda instead...",
232
- color=typer.colors.MAGENTA,
233
- )
234
- args.append("--conda-frontend=conda")
225
+ if is_snakemake_version_9_or_above: # support for mamba deprecated since v8.20.6(#3121)
226
+ args.extend(["--software-deployment-method", "conda"])
235
227
  else:
236
- args.append("--conda-frontend=mamba")
228
+ args.extend(
229
+ [
230
+ "--use-conda",
231
+ f"--conda-prefix={self.conda_prefix_dir}",
232
+ ]
233
+ )
234
+ if not check_command_available("mamba"):
235
+ if verbose:
236
+ self.log(
237
+ "Could not find mamba, using conda instead...",
238
+ color=typer.colors.MAGENTA,
239
+ )
240
+ args.append("--conda-frontend=conda")
241
+ else:
242
+ args.append("--conda-frontend=mamba")
237
243
 
238
244
  if verbose:
239
245
  args.insert(0, "--verbose")
@@ -395,16 +401,15 @@ class RunApp(DynamicTyper):
395
401
  )
396
402
  yield
397
403
  finally:
398
- if not cleanup:
399
- return
400
- for copied_resource in copied_resources:
401
- if copied_resource.exists():
402
- if self.verbose:
403
- self.log(
404
- f"Deleting '{copied_resource.name}' resource...",
405
- color=typer.colors.MAGENTA,
406
- )
407
- remove_resource(copied_resource)
404
+ if cleanup:
405
+ for copied_resource in copied_resources:
406
+ if copied_resource.exists():
407
+ if self.verbose:
408
+ self.log(
409
+ f"Deleting '{copied_resource.name}' resource...",
410
+ color=typer.colors.MAGENTA,
411
+ )
412
+ remove_resource(copied_resource)
408
413
 
409
414
  def execute_snakemake(args):
410
415
  """
@@ -17,11 +17,12 @@ class SnkCliRunner:
17
17
  """
18
18
 
19
19
  cli: CLI
20
- runner = CliRunner(mix_stderr=False)
20
+ runner = CliRunner()
21
21
 
22
22
  def invoke(self, args: List[str]) -> Result:
23
+ args = [str(arg) for arg in args]
23
24
  old_argv = sys.argv
24
25
  sys.argv = ['cli'] + args # ensure that the CLI is invoked with the correct arguments
25
26
  result = self.runner.invoke(self.cli.app, args)
26
27
  sys.argv = old_argv
27
- return result
28
+ return result
@@ -1,7 +1,7 @@
1
1
  from pathlib import Path
2
2
  from typing import Any, Dict, Union, get_origin
3
3
  from .config import SnkConfig
4
- from .options.utils import types
4
+ from .options.utils import allowed_option_types
5
5
  import inspect
6
6
 
7
7
  class ValidationError(Exception):
@@ -52,24 +52,35 @@ def validate_and_transform_in_place(config: Dict[str, Any], validation: Validati
52
52
  val_info = validation[key]
53
53
  if isinstance(val_info, dict) and 'type' in val_info:
54
54
  # Direct type validation
55
- val_type = types.get(val_info["type"].lower(), None)
55
+ val_type = allowed_option_types.get(val_info["type"].lower().replace(" ", ""), None)
56
56
  if val_type is None:
57
57
  raise ValueError(f"Unknown type '{val_info['type']}'")
58
58
  try:
59
59
  if getattr(val_type, "__origin__", None) is list:
60
+ # List e.g. List[str]
60
61
  val_type = val_type.__args__[0]
61
62
  if not isinstance(value, list):
62
63
  raise ValueError(f"Expected a list for key '{key}'")
63
64
  config[key] = [val_type(v) for v in value]
64
65
  elif get_origin(val_type) is tuple:
65
- assert len(value) == 2, f"Expected a list of length 2 for key '{key}'"
66
+ # Tuple e.g. Tuple[str, int]
67
+ assert len(value) == 2, f"Tuple type should have 2 elements for key '{key}'"
66
68
  key_type = val_type.__args__[0]
67
69
  val_type = val_type.__args__[1]
68
70
  config[key] = [key_type(value[0]), val_type(value[1])]
71
+ elif get_origin(val_type) is dict:
72
+ # Dict e.g. Dict[str, int]
73
+ if val_type.__args__ is None:
74
+ config[key] = value
75
+ continue
76
+ assert len(val_type.__args__) == 2, f"Dict type should have 2 arguments for key '{key}'"
77
+ val_type = val_type.__args__[1]
78
+ config[key] = {k: val_type(v) for k, v in value.items()}
69
79
  else:
80
+ # basic
70
81
  config[key] = val_type(value)
71
82
  except (ValueError, TypeError) as e:
72
- raise ValueError(f"Type conversion error for key '{key}': {e}")
83
+ raise ValueError(f"Type conversion error for key '{key}' ({val_info['type']}). Could not convert value '{value}' to type '{val_type}'.") from e
73
84
  elif isinstance(value, dict):
74
85
  # Nested dictionary validation
75
86
  validate_and_transform_in_place(value, val_info)
@@ -1,10 +1,7 @@
1
- from typing import Tuple
2
1
  import pytest
3
2
  from pathlib import Path
4
3
  from .utils import SnkCliRunner
5
- from snk_cli.config import SnkConfig
6
4
  from snk_cli import CLI
7
- import yaml
8
5
 
9
6
  @pytest.fixture()
10
7
  def example_config():
@@ -47,4 +47,4 @@ def test_snakemake_help(local_runner: SnkCliRunner):
47
47
  def test_snakemake_version(local_runner: SnkCliRunner):
48
48
  res = local_runner(["run", "--snake-v"])
49
49
  assert res.exit_code == 0, res.stderr
50
- assert res.stdout.startswith("7.32.4") or res.stdout.startswith("8.")
50
+ assert res.stdout.startswith("7.32.4") or res.stdout.startswith("8.") or res.stdout.startswith("9.")
@@ -0,0 +1,24 @@
1
+ from snk_cli.conda import conda_environment_factory
2
+ from snakemake.deployment.conda import Env
3
+ from pathlib import Path
4
+
5
+
6
+ def test_conda_env(tmp_path):
7
+ env = conda_environment_factory("tests/data/workflow/workflow/envs/wget.yml", tmp_path)
8
+ assert isinstance(env, Env)
9
+ assert not Path(env.address).exists()
10
+ env.create()
11
+ assert Path(env.address).exists()
12
+
13
+
14
+ def test_conda_env_content(tmp_path):
15
+ env_file = Path("tests/data/workflow/workflow/envs/wget.yml").resolve()
16
+ env = conda_environment_factory(env_file, tmp_path)
17
+ assert env.content == env_file.read_bytes()
18
+
19
+
20
+ def test_conda_env_factory_creates_prefix(tmp_path):
21
+ env_file = Path("tests/data/workflow/workflow/envs/wget.yml").resolve()
22
+ conda_prefix = tmp_path / "conda"
23
+ conda_environment_factory(env_file, conda_prefix)
24
+ assert conda_prefix.exists()
@@ -4,6 +4,7 @@ from snk_cli.dynamic_typer import DynamicTyper
4
4
  from snk_cli.options import Option
5
5
  from inspect import signature, Parameter
6
6
  import typer
7
+ from typing import Dict, List
7
8
 
8
9
  class SubApp(DynamicTyper):
9
10
  def __init__(self):
@@ -94,6 +95,27 @@ def test_create_cli_parameter(dynamic_typer):
94
95
  assert param.kind == Parameter.POSITIONAL_OR_KEYWORD
95
96
 
96
97
 
98
+ def test_create_dict_cli_parameter(dynamic_typer):
99
+ option = Option(
100
+ name="labels",
101
+ type=Dict[str, int],
102
+ required=False,
103
+ default={"first": 1},
104
+ help="Labels",
105
+ short=None,
106
+ updated=False,
107
+ original_key="labels",
108
+ flag="--labels",
109
+ short_flag=None,
110
+ )
111
+
112
+ param = dynamic_typer._create_cli_parameter(option)
113
+
114
+ assert param.annotation == List[tuple]
115
+ assert param.default.click_type == (str, int)
116
+ assert param.default.default == [["first", 1]]
117
+
118
+
97
119
 
98
120
  def test_add_dynamic_options(dynamic_typer):
99
121
  def func(name: str):
@@ -145,4 +167,4 @@ def test_calling_cli_produces_help(dynamic_app: DynamicTyper, capsys):
145
167
  except SystemExit:
146
168
  pass
147
169
  captured = capsys.readouterr()
148
- assert "Usage" in captured.out
170
+ assert "Usage" in captured.out
@@ -1,41 +0,0 @@
1
-
2
- name: tests
3
- on:
4
- push:
5
- branches:
6
- - master
7
- - main
8
- permissions:
9
- contents: write
10
- jobs:
11
- test:
12
- runs-on: ubuntu-latest
13
- steps:
14
- - uses: actions/checkout@v4
15
- with:
16
- fetch-depth: 0
17
- - uses: "conda-incubator/setup-miniconda@v2"
18
- with:
19
- python-version: 3.11
20
- auto-activate-base: false
21
- miniforge-variant: Mambaforge
22
- channels: conda-forge,bioconda,defaults
23
- channel-priority: strict
24
- auto-update-conda: true
25
- - uses: actions/setup-python@v5
26
- with:
27
- python-version: '3.11'
28
- - uses: actions/cache@v2
29
- with:
30
- key: ${{ github.ref }}
31
- path: .cache
32
- - run: pip install hatch
33
- - run: sudo apt-get update && sudo apt-get install -y llvm
34
- - run: sudo apt-get -y install graphviz
35
- - name: Run tests
36
- shell: bash
37
- run: hatch run snakemake:cov
38
- - name: Upload coverage reports to Codecov
39
- uses: codecov/codecov-action@v3
40
-
41
-
@@ -1,11 +0,0 @@
1
- from snk_cli.conda import conda_environment_factory
2
- from snakemake.deployment.conda import Env
3
- from pathlib import Path
4
-
5
-
6
- def test_conda_env(tmp_path):
7
- env = conda_environment_factory("tests/data/workflow/workflow/envs/wget.yml", tmp_path)
8
- assert isinstance(env, Env)
9
- assert not Path(env.address).exists()
10
- env.create()
11
- assert Path(env.address).exists()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes