Skip to content

Commit

Permalink
Add tests for spec docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
pvandyken committed Jan 15, 2024
1 parent 8a612a8 commit 131e5ba
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 15 deletions.
1 change: 1 addition & 0 deletions snakebids/paths/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def get_bids_func() -> BidsFunction:


def reset_bids_spec():
specs.__getattr__.cache_clear() # type: ignore
spec = specs.latest()
_config["active_spec"] = spec
_config["bids_func"] = bids_factory(spec, _implicit=True)
Expand Down
21 changes: 13 additions & 8 deletions snakebids/paths/_templates/spec_func.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def {spec}(subject_dir: bool = True, session_dir: bool = True) -> BidsPathSpec:
"""

DEFAULT_DESCRIPTION = """Bids Spec v{version}
DEFAULT_DESCRIPTION = """Bids Spec {version}
Supply this to snakebids.bids_factory to construct a corresponding bids function
"""
Expand Down Expand Up @@ -65,17 +65,22 @@ def compile_example(spec: BidsPathSpec):
return _wrap_template(template[:i] + "..._" + template[i:], 80)


def format_doc(spec: BidsPathSpecFile):
try:
import docstring_parser as docstr
except ImportError:
return DOCSTRING.format(
description=DEFAULT_DESCRIPTION.format(version=spec["version"]).strip()
)
def _import_docstring_parser():
"""Isolated import function that can be mocked in tests."""
import docstring_parser as docstr

return docstr


def format_doc(spec: BidsPathSpecFile):
if (description := spec.get("description")) is None:
description = DEFAULT_DESCRIPTION.format(version=spec["version"])

try:
docstr = _import_docstring_parser()
except ImportError:
return description.strip()

doc = docstr.parse(DOCSTRING.format(description=description.strip()))
if doc.long_description:
doc.long_description = (
Expand Down
2 changes: 2 additions & 0 deletions snakebids/paths/specs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import functools as ft
from typing import TYPE_CHECKING

from snakebids.paths._templates import spec_func
Expand Down Expand Up @@ -33,6 +34,7 @@ def __dir__():
LATEST = "v0_0_0"


@ft.lru_cache
def __getattr__(name: str):
"""Allow dynamic retrieval of latest spec."""
if name == "latest":
Expand Down
71 changes: 64 additions & 7 deletions snakebids/tests/test_paths/test_specs.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
from __future__ import annotations

import warnings
from pathlib import Path

import pytest
from pytest_mock import MockerFixture

import snakebids.paths._templates.spec_func as spec_template
from snakebids.paths import bids_factory, specs
from snakebids.paths._config import reset_bids_spec, set_bids_spec
from snakebids.paths._presets import bids
from snakebids.paths._utils import find_entity
from snakebids.paths._utils import find_entity, get_spec_path, load_spec
from snakebids.paths.specs import v0_0_0


@pytest.fixture(autouse=True)
def _spec_reset(): # type: ignore
reset_bids_spec()


def test_reset_bids_spec_clears_cache(mocker: MockerFixture):
load_spec_spy = mocker.spy(specs, "load_spec")
_ = specs.v0_0_0
load_spec_spy.assert_called_once()
load_spec_spy.reset_mock()
_ = specs.v0_0_0
load_spec_spy.assert_not_called()
reset_bids_spec()
load_spec_spy.reset_mock()
_ = specs.v0_0_0
load_spec_spy.assert_called_once()


def test_all_entries_define_entity():
spec = v0_0_0()
for item in spec:
Expand Down Expand Up @@ -44,42 +65,78 @@ def test_spec_can_be_set_with_obj():


def test_using_include_subject_dir_raises_warning():
reset_bids_spec()
with pytest.warns(UserWarning, match="include_session_dir and include_subject_dir"):
bids(subject="001", include_subject_dir=False)
with pytest.warns(UserWarning, match="include_session_dir and include_subject_dir"):
bids(session="001", include_session_dir=False)


def test_include_subject_dir_can_remove_dir():
reset_bids_spec()
with warnings.catch_warnings():
warnings.simplefilter("ignore")
assert len(Path(bids(subject="001", include_subject_dir=False)).parents) == 1
assert len(Path(bids(session="001", include_session_dir=False)).parents) == 1


class TestSpecDocstrings:
def clean(self, item: str):
return " ".join(item.split())

def specfuncs(self):
for attr in dir(specs):
print(attr)
if attr.startswith("v") or attr == "latest":
yield getattr(specs, attr)

def test_all_specs_have_docstrings(self):
for spec in self.specfuncs():
assert isinstance(spec.__doc__, str)

def test_all_specs_have_format_example(self):
for spec in self.specfuncs():
assert "Formatted as::" in spec.__doc__

def test_spec_has_correct_docstring(self):
spec = load_spec(get_spec_path("v0_0_0"))
assert (
self.clean(specs.v0_0_0.__doc__).startswith(self.clean(spec["description"])) # type: ignore
)

def test_spec_with_description_gets_default_docstring(self, mocker: MockerFixture):
spec = load_spec(get_spec_path("v0_0_0"))
_ = spec.pop("description", None)
mocker.patch.object(specs, "load_spec", return_value=spec)
assert self.clean(specs.v0_0_0.__doc__).startswith( # type: ignore
self.clean(spec_template.DEFAULT_DESCRIPTION.format(version="v0.0.0"))
)

def test_no_format_example_when_no_docstring_parser(self, mocker: MockerFixture):
mocker.patch.object(
spec_template, "_import_docstring_parser", side_effect=ImportError()
)
# Need to reset after mocking because the reset collects the latest spec
reset_bids_spec()
for spec in self.specfuncs():
assert "Formatted as::" not in spec.__doc__, spec


class TestCustomEntityWarnings:
def test_using_custom_entities_with_default_bids_raises_warning(self):
reset_bids_spec()
with pytest.warns(UserWarning, match="spec has not been explicitly declared"):
bids(foo="bar")

def test_no_warning_when_spec_declared(self):
reset_bids_spec()
set_bids_spec("v0_0_0")
with warnings.catch_warnings():
warnings.simplefilter("error")
bids(foo="bar")

def test_no_warning_when_bids_explicitly_generated(self):
reset_bids_spec()
with warnings.catch_warnings():
warnings.simplefilter("error")
bids_factory(specs.v0_0_0())(foo="bar")

def test_no_warning_in_interactive_mode(self, mocker: MockerFixture):
reset_bids_spec()
mocker.patch(
"snakebids.paths._factory.in_interactive_session", return_value=True
)
Expand Down

0 comments on commit 131e5ba

Please sign in to comment.