Skip to content

Commit

Permalink
Create documentation compatibility for old apps
Browse files Browse the repository at this point in the history
Include add_dynamic_args as deprecated placeholder. Some apps were using
it to prepare a documentation cli. Include it with a deprecation warning
for a more graceful change. It's otherwise a no-op, it's funcationality
subsumed by SnakeBidsApp and the underlying bidsapp

Expose .parser and .config on SnakeBidsApp

Lazily construct app in SnakeBidsApp: Improves compatibility for some
tests
  • Loading branch information
pvandyken committed Apr 17, 2024
1 parent 906760a commit afa8a50
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 24 deletions.
57 changes: 33 additions & 24 deletions snakebids/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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."
Expand All @@ -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
Expand All @@ -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
26 changes: 26 additions & 0 deletions snakebids/cli.py
Original file line number Diff line number Diff line change
@@ -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-<comp> and --wildcards-<comp> 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,
)
38 changes: 38 additions & 0 deletions snakebids/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit afa8a50

Please sign in to comment.