Skip to content

Commit

Permalink
Conditionally skip tests requiring bluepy (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
GianlucaFicarelli committed Dec 7, 2023
1 parent cba1560 commit 41943db
Show file tree
Hide file tree
Showing 13 changed files with 90 additions and 62 deletions.
9 changes: 6 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ dynamic = ["version"]
[project.optional-dependencies]
extra = [
# extra requirements that may be dropped at some point
# "bluepy>=2.5.2",
"fastparquet>=0.8.3,!=2023.1.0", # needed by pandas to read and write parquet files
"orjson", # faster json decoder used by fastparquet
"tables>=3.6.1", # needed by pandas to read and write hdf files
Expand Down Expand Up @@ -117,13 +116,15 @@ source = [
branch = true
parallel = false
omit = [
"src/blueetl/_version.py",
"*/blueetl/_version.py",
"*/blueetl/adapters/bluepy/*.py",
"*/blueetl/external/**/*.py",
]

[tool.coverage.report]
show_missing = true
precision = 0
fail_under = 80
fail_under = 70

[tool.pydocstyle]
# D413: no blank line after last section
Expand All @@ -140,6 +141,8 @@ extension-pkg-allow-list = ["numpy", "lxml", "pydantic"]
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins = ["pylint_pydantic"]
# List of module names for which member attributes should not be checked.
ignored-modules = ["bluepy"]

[tool.pylint.design]
# Maximum number of arguments for function / method.
Expand Down
4 changes: 4 additions & 0 deletions src/blueetl/adapters/bluepy/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
"""Bluepy implementation."""
from blueetl.utils import import_optional_dependency

# Immediately raise an error with a custom message if the submodule is imported
import_optional_dependency("bluepy")
4 changes: 2 additions & 2 deletions src/blueetl/adapters/bluepy/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
from collections.abc import Mapping
from typing import Optional

import bluepy
from bluepy import Circuit

from blueetl.adapters.interfaces.circuit import CircuitInterface, NodePopulationInterface
from blueetl.utils import checksum_json


class CircuitImpl(CircuitInterface[bluepy.Circuit]):
class CircuitImpl(CircuitInterface[Circuit]):
"""Bluepy circuit implementation."""

def checksum(self) -> str:
Expand Down
6 changes: 3 additions & 3 deletions src/blueetl/adapters/bluepy/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from functools import cached_property
from typing import Optional, Union

import bluepy
import pandas as pd
from bluepy import Simulation
from bluepy.exceptions import BluePyError
from bluepy.impl.compartment_report import CompartmentReport, SomaReport
from bluepy.impl.spike_report import SpikeReport
Expand Down Expand Up @@ -53,7 +53,7 @@ def get(self, group=None, t_start=None, t_stop=None, t_step=None) -> pd.DataFram
class ReportCollection(UserDict):
"""Collection of reports as: name -> population -> report."""

def __init__(self, simulation: bluepy.Simulation) -> None:
def __init__(self, simulation: Simulation) -> None:
"""Init the report collection with the specified simulation."""
super().__init__()
self._simulation = simulation
Expand All @@ -68,7 +68,7 @@ def __getitem__(self, name) -> Mapping[Optional[str], PopulationReportInterface]
return self.data[name]


class SimulationImpl(SimulationInterface[bluepy.Simulation]):
class SimulationImpl(SimulationInterface[Simulation]):
"""Bluepy simulation implementation."""

def is_complete(self) -> bool:
Expand Down
4 changes: 2 additions & 2 deletions src/blueetl/adapters/bluepysnap/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
from collections.abc import Mapping
from typing import Optional

import bluepysnap
from bluepysnap import Circuit

from blueetl.adapters.interfaces.circuit import CircuitInterface, NodePopulationInterface
from blueetl.utils import checksum_json


class CircuitImpl(CircuitInterface[bluepysnap.Circuit]):
class CircuitImpl(CircuitInterface[Circuit]):
"""Bluepysnap circuit implementation."""

def checksum(self) -> str:
Expand Down
4 changes: 2 additions & 2 deletions src/blueetl/adapters/bluepysnap/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pathlib import Path
from typing import Optional

import bluepysnap
from bluepysnap import Simulation

from blueetl.adapters.bluepysnap.circuit import CircuitImpl
from blueetl.adapters.interfaces.circuit import CircuitInterface
Expand All @@ -15,7 +15,7 @@
)


class SimulationImpl(SimulationInterface[bluepysnap.Simulation]):
class SimulationImpl(SimulationInterface[Simulation]):
"""Bluepysnap simulation implementation."""

def is_complete(self) -> bool:
Expand Down
8 changes: 2 additions & 6 deletions src/blueetl/adapters/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,9 @@ def _load_impl(self, config: str) -> Optional[CircuitInterface]:
return None
CircuitImpl: type[CircuitInterface]
if config.endswith(".json"):
from bluepysnap import Circuit

from blueetl.adapters.bluepysnap.circuit import CircuitImpl
from blueetl.adapters.bluepysnap.circuit import Circuit, CircuitImpl
else:
from bluepy import Circuit

from blueetl.adapters.bluepy.circuit import CircuitImpl
from blueetl.adapters.bluepy.circuit import Circuit, CircuitImpl
return CircuitImpl(Circuit(config))

def checksum(self) -> str:
Expand Down
8 changes: 2 additions & 6 deletions src/blueetl/adapters/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,9 @@ def _load_impl(self, config: str) -> Optional[SimulationInterface]:
return None
SimulationImpl: type[SimulationInterface]
if config.endswith(".json"):
from bluepysnap import Simulation

from blueetl.adapters.bluepysnap.simulation import SimulationImpl
from blueetl.adapters.bluepysnap.simulation import Simulation, SimulationImpl
else:
from bluepy import Simulation

from blueetl.adapters.bluepy.simulation import SimulationImpl
from blueetl.adapters.bluepy.simulation import Simulation, SimulationImpl
return SimulationImpl(Simulation(config))

def is_complete(self) -> bool:
Expand Down
25 changes: 23 additions & 2 deletions src/blueetl/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Common utilities."""
import hashlib
import importlib
import itertools
import json
import logging
Expand All @@ -8,7 +9,6 @@
from collections.abc import Iterable, Iterator
from contextlib import contextmanager
from functools import cache, cached_property
from importlib import import_module
from pathlib import Path
from typing import Any, Callable, Optional, Union

Expand Down Expand Up @@ -123,7 +123,7 @@ def import_by_string(full_name: str) -> Callable:
The imported function.
"""
module_name, _, func_name = full_name.rpartition(".")
return getattr(import_module(module_name), func_name)
return getattr(importlib.import_module(module_name), func_name)


def resolve_path(*paths: StrOrPath, symlinks: bool = False) -> Path:
Expand Down Expand Up @@ -269,3 +269,24 @@ def all_equal(iterable: Iterable) -> bool:
return False
prev = item
return True


def import_optional_dependency(name: str) -> Any:
"""Import an optional dependency.
If a dependency is missing, an ImportError with a custom message is raised. Based on:
https://github.com/pandas-dev/pandas/blob/0d853e77/pandas/compat/_optional.py#L85
Args:
name: The module name.
Returns:
ModuleType: The imported module, if found.
"""
try:
module = importlib.import_module(name)
except ImportError as ex:
msg = f"Missing optional dependency {name!r}. Use pip to install it."
raise ImportError(msg) from ex

return module
22 changes: 10 additions & 12 deletions tests/unit/adapters/test_circuit.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import pickle

import bluepy
import bluepysnap
import pytest
import bluepysnap.nodes

from blueetl.adapters import circuit as test_module
from blueetl.adapters.base import AdapterError
from tests.unit.utils import TEST_DATA_PATH
import bluepy.cells
from tests.unit.utils import BLUEPY_AVAILABLE, TEST_DATA_PATH, assert_isinstance


@pytest.mark.parametrize(
Expand All @@ -18,8 +15,8 @@
"sonata/circuit_config.json",
"default",
{
"circuit": bluepysnap.Circuit,
"population": bluepysnap.nodes.NodePopulation,
"circuit": "bluepysnap.Circuit",
"population": "bluepysnap.nodes.NodePopulation",
},
id="snap",
)
Expand All @@ -29,10 +26,11 @@
"bbp/CircuitConfig",
None,
{
"circuit": bluepy.Circuit,
"population": bluepy.cells.CellCollection,
"circuit": "bluepy.Circuit",
"population": "bluepy.cells.CellCollection",
},
id="bluepy",
marks=pytest.mark.skipif(not BLUEPY_AVAILABLE, reason="bluepy not available"),
)
),
],
Expand All @@ -42,11 +40,11 @@ def test_circuit_adapter(path, population, expected_classes, monkeypatch):
# enter the circuit dir to resolve relative paths in bluepy
monkeypatch.chdir(path.parent)
obj = test_module.CircuitAdapter(TEST_DATA_PATH / path)
assert isinstance(obj.instance, expected_classes["circuit"])
assert_isinstance(obj.instance, expected_classes["circuit"])

# access methods and properties
pop = obj.nodes[population]
assert isinstance(pop, expected_classes["population"])
assert_isinstance(pop, expected_classes["population"])

checksum = obj.checksum()
assert isinstance(checksum, str)
Expand All @@ -56,7 +54,7 @@ def test_circuit_adapter(path, population, expected_classes, monkeypatch):
loaded = pickle.loads(dumped)

assert isinstance(loaded, test_module.CircuitAdapter)
assert isinstance(loaded.instance, expected_classes["circuit"])
assert_isinstance(loaded.instance, expected_classes["circuit"])
# no cached_properties should be loaded after unpickling
assert sorted(loaded.__dict__) == ["_impl"]
assert sorted(loaded._impl.__dict__) == ["_circuit"]
Expand Down
40 changes: 17 additions & 23 deletions tests/unit/adapters/test_simulation.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import pickle

import bluepy.cells
import bluepy.impl.compartment_report
import bluepy.impl.spike_report
import bluepysnap.frame_report
import bluepysnap.nodes
import bluepysnap.spike_report
import pytest

from blueetl.adapters import simulation as test_module
from blueetl.adapters.base import AdapterError
from blueetl.adapters.bluepy.simulation import PopulationReportImpl, PopulationSpikesReportImpl
from tests.unit.utils import TEST_DATA_PATH
from tests.unit.utils import BLUEPY_AVAILABLE, TEST_DATA_PATH, assert_isinstance


@pytest.mark.parametrize(
Expand All @@ -23,11 +16,11 @@
"default",
["soma_report", "section_report"],
{
"simulation": bluepysnap.Simulation,
"population": bluepysnap.nodes.NodePopulation,
"spikes": bluepysnap.spike_report.PopulationSpikeReport,
"soma_report": bluepysnap.frame_report.PopulationSomaReport,
"section_report": bluepysnap.frame_report.PopulationCompartmentReport,
"simulation": "bluepysnap.Simulation",
"population": "bluepysnap.nodes.NodePopulation",
"spikes": "bluepysnap.spike_report.PopulationSpikeReport",
"soma_report": "bluepysnap.frame_report.PopulationSomaReport",
"section_report": "bluepysnap.frame_report.PopulationCompartmentReport",
},
id="snap",
)
Expand All @@ -38,13 +31,14 @@
None,
["soma", "AllCompartments"],
{
"simulation": bluepy.Simulation,
"population": bluepy.cells.CellCollection,
"spikes": PopulationSpikesReportImpl,
"soma": PopulationReportImpl,
"AllCompartments": PopulationReportImpl,
"simulation": "bluepy.Simulation",
"population": "bluepy.cells.CellCollection",
"spikes": "blueetl.adapters.bluepy.simulation.PopulationSpikesReportImpl",
"soma": "blueetl.adapters.bluepy.simulation.PopulationReportImpl",
"AllCompartments": "blueetl.adapters.bluepy.simulation.PopulationReportImpl",
},
id="bluepy",
marks=pytest.mark.skipif(not BLUEPY_AVAILABLE, reason="bluepy not available"),
)
),
],
Expand All @@ -54,28 +48,28 @@ def test_simulation_adapter(path, population, reports, expected_classes, monkeyp
# enter the circuit dir to resolve relative paths in bluepy
monkeypatch.chdir(path.parent)
obj = test_module.SimulationAdapter(TEST_DATA_PATH / path)
assert isinstance(obj.instance, expected_classes["simulation"])
assert_isinstance(obj.instance, expected_classes["simulation"])

assert obj.exists() is True
assert obj.is_complete() is True

# access methods and properties
pop = obj.circuit.nodes[population]
assert isinstance(pop, expected_classes["population"])
assert_isinstance(pop, expected_classes["population"])

spikes = obj.spikes[population]
assert isinstance(spikes, expected_classes["spikes"])
assert_isinstance(spikes, expected_classes["spikes"])

for report_name in reports:
report = obj.reports[report_name][population]
assert isinstance(report, expected_classes[report_name])
assert_isinstance(report, expected_classes[report_name])

# test pickle roundtrip
dumped = pickle.dumps(obj)
loaded = pickle.loads(dumped)

assert isinstance(loaded, test_module.SimulationAdapter)
assert isinstance(loaded.instance, expected_classes["simulation"])
assert_isinstance(loaded.instance, expected_classes["simulation"])
# no cached_properties should be loaded after unpickling
assert sorted(loaded.__dict__) == ["_impl"]
assert sorted(loaded._impl.__dict__) == ["_simulation"]
Expand Down
17 changes: 16 additions & 1 deletion tests/unit/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import importlib
from pathlib import Path

import pandas as pd
import pydantic
import xarray as xr

try:
import bluepy

BLUEPY_AVAILABLE = True
except ImportError:
BLUEPY_AVAILABLE = False

TEST_DATA_PATH = Path(__file__).parent / "data"


Expand Down Expand Up @@ -34,9 +42,16 @@ def iterallvalues(obj):


def assert_not_duplicates(obj):
# verify that obj doesn't contain duplicate instances
"""Verify that obj doesn't contain duplicate instances."""
ids = set()
for v in iterallvalues(obj):
if isinstance(v, (dict, list, tuple)):
assert id(v) not in ids, f"Duplicate {type(v).__name__}: {v}"
ids.add(id(v))


def assert_isinstance(instance, class_name):
"""Verify that instance is an instance of class_name (given as string)."""
module_name, _, class_name = class_name.rpartition(".")
cls = getattr(importlib.import_module(module_name), class_name)
assert isinstance(instance, cls)
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ deps =
{[base]testdeps}
brion;platform_system=='Linux'
elephant>=0.10.0,<0.13.0 # CPDF output changed in 0.13.0
bluepy>=2.5.2
commands = python -m pytest -vs tests/functional {posargs}

[testenv:check-packaging]
Expand Down

0 comments on commit 41943db

Please sign in to comment.