Skip to content

Commit eed7d17

Browse files
gegnewzbjornson
authored andcommitted
bug(experiment): Comp types not handled correctly
1 parent 786aa82 commit eed7d17

File tree

4 files changed

+124
-49
lines changed

4 files changed

+124
-49
lines changed

cellengine/__init__.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,41 @@
11
# flake8: noqa
22
__version__ = "0.1.0"
33

4-
from cellengine.utils.api_client.APIClient import APIClient
4+
try:
5+
from typing import Literal
6+
except ImportError:
7+
from typing_extensions import Literal
8+
59
from cellengine.resources.attachment import Attachment
610
from cellengine.resources.compensation import Compensation
711
from cellengine.resources.experiment import Experiment
812
from cellengine.resources.fcs_file import FcsFile
913
from cellengine.resources.gate import (
14+
EllipseGate,
1015
Gate,
11-
RectangleGate,
1216
PolygonGate,
13-
EllipseGate,
14-
SplitGate,
1517
QuadrantGate,
1618
RangeGate,
19+
RectangleGate,
20+
SplitGate,
1721
)
1822
from cellengine.resources.plot import Plot
1923
from cellengine.resources.population import Population
2024
from cellengine.resources.scaleset import ScaleSet
25+
from cellengine.utils.api_client.APIClient import APIClient
2126
from cellengine.utils.complex_population_builder import ComplexPopulationBuilder
27+
28+
UNCOMPENSATED: int = 0
29+
"""Apply no compensation."""
30+
31+
FILE_INTERNAL: int = -1
32+
"""
33+
Use the file's internal compensation matrix, if available. If not available, an
34+
error will be returned from API requests.
35+
"""
36+
37+
PER_FILE: int = -2
38+
"""
39+
Use the compensation assigned to each individual FCS file. Not a valid value for
40+
`FcsFile.compensation`.
41+
"""

cellengine/resources/experiment.py

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,46 @@
11
from __future__ import annotations
2-
from datetime import datetime
3-
4-
from marshmallow import fields
5-
from pandas.core.frame import DataFrame
6-
7-
from cellengine.utils.dataclass_mixin import DataClassMixin, ReadOnly
82
from dataclasses import dataclass, field
3+
from datetime import datetime
94
import inspect
105
from math import pi
11-
from custom_inherit import doc_inherit
12-
from typing import Any, Optional, Dict, Union, List
6+
from typing import Any, Dict, List, Optional, Union
137

8+
from custom_inherit import doc_inherit
149
from dataclasses_json.cfg import config
10+
from marshmallow import fields
11+
from pandas.core.frame import DataFrame
1512

1613
import cellengine as ce
17-
from cellengine.resources.population import Population
18-
from cellengine.resources.scaleset import ScaleSet
19-
from cellengine.resources.fcs_file import FcsFile
20-
from cellengine.resources.compensation import Compensation
14+
from cellengine.payloads.gate_utils import (
15+
format_ellipse_gate,
16+
format_polygon_gate,
17+
format_quadrant_gate,
18+
format_range_gate,
19+
format_rectangle_gate,
20+
format_split_gate,
21+
)
2122
from cellengine.resources.attachment import Attachment
23+
from cellengine.resources.compensation import Compensation
24+
from cellengine.resources.fcs_file import FcsFile
2225
from cellengine.resources.gate import (
26+
EllipseGate,
2327
Gate,
24-
RectangleGate,
2528
PolygonGate,
29+
QuadrantGate,
2630
RangeGate,
31+
RectangleGate,
2732
SplitGate,
28-
EllipseGate,
29-
QuadrantGate,
3033
)
34+
from cellengine.resources.population import Population
35+
from cellengine.resources.scaleset import ScaleSet
36+
from cellengine.utils.dataclass_mixin import DataClassMixin, ReadOnly
3137
from cellengine.utils.helpers import (
3238
CommentList,
3339
datetime_to_timestamp,
40+
is_valid_id,
3441
timestamp_to_datetime,
3542
)
3643

37-
from cellengine.payloads.gate_utils import (
38-
format_rectangle_gate,
39-
format_polygon_gate,
40-
format_ellipse_gate,
41-
format_range_gate,
42-
format_split_gate,
43-
format_quadrant_gate,
44-
)
45-
4644

4745
@dataclass
4846
class Experiment(DataClassMixin):
@@ -74,7 +72,7 @@ class Experiment(DataClassMixin):
7472
),
7573
default=ReadOnly(),
7674
) # type: ignore
77-
_active_comp: Optional[Union[int, str, Compensation]] = field(
75+
_active_comp: Optional[Union[int, str]] = field(
7876
default=None, metadata=config(field_name="activeCompensation")
7977
)
8078
data: Optional[Dict[Any, Any]] = field(default=None)
@@ -213,28 +211,43 @@ def undelete(self):
213211
del self.deleted
214212

215213
@property
216-
def active_compensation(self) -> Optional[Union[Compensation, int]]:
217-
active_comp = self._active_comp
218-
if type(active_comp) is str:
219-
return ce.APIClient().get_compensation(self._id, active_comp)
220-
elif type(active_comp) is int:
221-
return active_comp
222-
elif active_comp is None:
223-
return None
224-
raise ValueError(
225-
f"Value '{active_comp}' is not a valid for activeCompensation."
226-
)
214+
def active_compensation(self) -> Optional[Union[str, int]]:
215+
"""The most recently used compensation.
216+
217+
Accepted values are:
218+
219+
- A Compensation object (value will be set to its `_id`)
220+
- A Compensation ID
221+
- A `cellengine` Compensation constant:
222+
[`UNCOMPENSATED`][cellengine.UNCOMPENSATED],
223+
[`FILE_INTERNAL`][cellengine.FILE_INTERNAL] or
224+
[`PER_FILE`][cellengine.PER_FILE].
225+
226+
Example:
227+
```python
228+
import cellengine
229+
exp = cellengine.get_experiment(name='my experiment')
230+
experiment.active_compensation = cellengine.UNCOMPENSATED
231+
```
232+
"""
233+
return self._active_comp
227234

228235
@active_compensation.setter
229-
def active_compensation(self, compensation: Union[Compensation, int]):
230-
if type(compensation) is Compensation:
236+
def active_compensation(self, compensation: Union[Compensation, int, str]):
237+
if isinstance(compensation, Compensation):
231238
self._active_comp = compensation._id
232-
elif (
233-
compensation == "UNCOMPENSATED"
234-
or compensation == "FILE_INTERNAL"
235-
or compensation == "PER_FILE"
236-
):
239+
elif isinstance(compensation, str) and is_valid_id(compensation):
240+
self._active_comp = compensation
241+
elif isinstance(compensation, int) and compensation in [
242+
ce.UNCOMPENSATED,
243+
ce.PER_FILE,
244+
ce.FILE_INTERNAL,
245+
]:
237246
self._active_comp = compensation
247+
else:
248+
raise ValueError(
249+
f"Value '{compensation}' can not be set as the active compensation."
250+
)
238251

239252
@property
240253
def attachments(self) -> List[Attachment]:

docs/compensations.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ CellEngine object. Properties are the snake_case equivalent of those documented
1515
on the [CellEngine API](https://docs.cellengine.com/api/#compensations) unless
1616
otherwise noted.
1717

18+
## Special Constants
19+
20+
::: cellengine.UNCOMPENSATED
21+
22+
::: cellengine.FILE_INTERNAL
23+
24+
::: cellengine.PER_FILE
25+
1826
## Methods
1927

2028
::: cellengine.resources.compensation.Compensation

tests/unit/resources/test_experiment.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import json
22
import pytest
33
import responses
4+
from cellengine import FILE_INTERNAL, PER_FILE, UNCOMPENSATED
45

5-
from cellengine.utils import helpers
6+
from cellengine.utils import converter, helpers
67
from cellengine.resources.experiment import Experiment
78
from cellengine.resources.population import Population
89
from cellengine.resources.fcs_file import FcsFile
@@ -20,7 +21,14 @@
2021
STATISTICS_ID = "5d64abe2ca9df61349ed8e79"
2122

2223

23-
def test_all_experiment_properties(client, ENDPOINT_BASE, experiment):
24+
@pytest.fixture(scope="function")
25+
def compensation(client, ENDPOINT_BASE, compensations):
26+
comp = compensations[0]
27+
comp.update({"experimentId": EXP_ID})
28+
return converter.structure(comp, Compensation)
29+
30+
31+
def test_all_experiment_properties(ENDPOINT_BASE, experiment):
2432
assert experiment._id == "5d38a6f79fae87499999a74b"
2533
assert experiment.name == "pytest_experiment"
2634
assert experiment.comments == [{"insert": "\xa0\xa0\xa0First 12 of 96 files\n\n"}]
@@ -208,3 +216,29 @@ def test_should_clone_experiment(client, ENDPOINT_BASE, experiment, experiments)
208216
assert json.loads(responses.calls[0].request.body) == {
209217
"name": "my cloned experiment",
210218
}
219+
220+
221+
def test_sets_active_compensation_correctly(experiment, compensation):
222+
experiment.active_compensation = 0
223+
assert experiment.active_compensation == 0
224+
225+
experiment.active_compensation = -1
226+
assert experiment.active_compensation == -1
227+
228+
experiment.active_compensation = -2
229+
assert experiment.active_compensation == -2
230+
231+
experiment.active_compensation = UNCOMPENSATED
232+
assert experiment.active_compensation == 0
233+
234+
experiment.active_compensation = FILE_INTERNAL
235+
assert experiment.active_compensation == -1
236+
237+
experiment.active_compensation = PER_FILE
238+
assert experiment.active_compensation == -2
239+
240+
experiment.active_compensation = compensation
241+
assert experiment.active_compensation == compensation._id
242+
243+
experiment.active_compensation = compensation._id
244+
assert experiment.active_compensation == compensation._id

0 commit comments

Comments
 (0)