diff --git a/snakebids/app.py b/snakebids/app.py index b130f6a0..ec040edd 100644 --- a/snakebids/app.py +++ b/snakebids/app.py @@ -20,16 +20,17 @@ from pathlib import Path from typing import Any, Callable -import attr +import attrs import boutiques.creator as bc # type: ignore from snakebids import bidsapp from snakebids import plugins as sb_plugins +from snakebids.bidsapp.run import _Runner logger = logging.Logger(__name__) -@attr.define(slots=False) +@attrs.define class SnakeBidsApp: """Snakebids app with config and arguments. @@ -69,23 +70,36 @@ class SnakeBidsApp: """ snakemake_dir: Path - plugins: list[Callable[[SnakeBidsApp], None | SnakeBidsApp]] = attr.Factory(list) + plugins: list[Callable[[SnakeBidsApp], None | SnakeBidsApp]] = attrs.Factory(list) skip_parse_args: bool = False - parser: Any = None + _parser: Any = attrs.field(default=None, alias="parser") configfile_path: Path | None = None snakefile_path: Path | None = None - config: Any = None + _config: Any = attrs.field(default=None, alias="config") version: str | None = None args: Any = None + _app_holder: _Runner | None = attrs.field(default=None, init=False) + + @property + def _app(self): + if self._app_holder is None: + self._check_deprecations() + + self._app_holder = bidsapp.app( + [sb_plugins.SnakemakeBidsApp(**self._get_args()), *self.plugins], + description="Snakebids helps build BIDS Apps with Snakemake", + ) + return self._app_holder + def _check_deprecations(self): - if self.parser is not None: + if self._parser is not None: msg = ( "`SnakeBidsApp.parser` is deprecated and no longer has any effect. To " "modify the parser, use the new `bidsapp` module." ) warnings.warn(msg, stacklevel=3) - if self.config is not None: + if self._config is not None: msg = ( "`SnakeBidsApp.config` is deprecated and no longer has any effect. To " "modify the config, use the new `bidsapp` module." @@ -105,6 +119,16 @@ def _check_deprecations(self): ) warnings.warn(msg, stacklevel=3) + @property + def config(self): + """Get config dict (before arguments are parsed).""" + return self._app.build_parser().config + + @property + def parser(self): + """Get parser.""" + return self._app.build_parser().parser + def _get_args(self): args: dict[str, Any] = {} args["snakemake_dir"] = self.snakemake_dir @@ -116,26 +140,11 @@ def _get_args(self): def run_snakemake(self) -> None: """Run snakemake with the given config, after applying plugins.""" - self._check_deprecations() - - bidsapp.app( - [sb_plugins.SnakemakeBidsApp(**self._get_args()), *self.plugins], - description="Snakebids helps build BIDS Apps with Snakemake", - ).run() + self._app.run() def create_descriptor(self, out_file: PathLike[str] | str) -> None: """Generate a boutiques descriptor for this Snakebids app.""" - self._check_deprecations() - - parser = ( - bidsapp.app( - [sb_plugins.SnakemakeBidsApp(**self._get_args()), *self.plugins], - description="Snakebids helps build BIDS Apps with Snakemake", - ) - .build_parser() - .parser - ) new_descriptor = bc.CreateDescriptor( # type: ignore - parser, execname="run.py" + self.parser, execname="run.py" ) new_descriptor.save(out_file) # type: ignore diff --git a/snakebids/cli.py b/snakebids/cli.py new file mode 100644 index 00000000..abbc4152 --- /dev/null +++ b/snakebids/cli.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import argparse +import warnings +from typing import Any, Mapping + +from snakebids.types import InputsConfig + + +def add_dynamic_args( + parser: argparse.ArgumentParser, + parse_args: Mapping[str, Any], + pybids_inputs: InputsConfig, +) -> None: + """Do nothing. + + Originally added --filter- and --wildcards- argumets to the CLI. Kept + as a placeholder for apps that relied on it for generating documentation. This + functionality is now native to `SnakeBidsApp`. + """ + warnings.warn( + "add_dynamic_args() is deprecated and no longer has any effect. Its function " + "is now provided natively by `SnakeBidsApp`. It will be removed in an upcoming " + "release", + stacklevel=2, + ) diff --git a/snakebids/tests/test_app.py b/snakebids/tests/test_app.py index 9754162b..5e8f79c1 100644 --- a/snakebids/tests/test_app.py +++ b/snakebids/tests/test_app.py @@ -3,11 +3,15 @@ import argparse import json from pathlib import Path +from typing import Any import pytest +from hypothesis import given from pytest_mock import MockerFixture from snakebids.app import SnakeBidsApp +from snakebids.cli import add_dynamic_args +from snakebids.tests import strategies as sb_st class TestDeprecations: @@ -70,6 +74,40 @@ def test_plugins_carried_forward(mocker: MockerFixture): assert len(plugins) == 3 # type: ignore # noqa: PLR2004 +def test_parser_can_be_directly_accessed(tmpdir: Path): + config_path = Path(tmpdir) / "config.json" + config_path.write_text("{}") + app = SnakeBidsApp( + Path(), + configfile_path=config_path, + snakefile_path=Path("Snakefile"), + ) + assert app.parser is app._app.parser + + +def test_config_can_be_directly_accessed(tmpdir: Path): + config_path = Path(tmpdir) / "config.json" + config_path.write_text("{}") + app = SnakeBidsApp( + Path(), + configfile_path=config_path, + snakefile_path=Path("Snakefile"), + ) + assert app.config is app._app.config + + +@given( + parser=sb_st.everything(), + parse_args=sb_st.everything(), + pybids_inputs=sb_st.everything(), +) +def test_add_dynamic_args_raises_warning( + parser: Any, parse_args: Any, pybids_inputs: Any +): + with pytest.warns(UserWarning, match="is deprecated and no longer has any effect"): + add_dynamic_args(parser, parse_args, pybids_inputs) + + class TestGenBoutiques: def test_boutiques_descriptor(self, tmp_path: Path): configpth = tmp_path / "config.json"