Skip to content

Commit 3ae545c

Browse files
committed
some clean up
1 parent 2483944 commit 3ae545c

8 files changed

Lines changed: 108 additions & 117 deletions

File tree

python/lib/sift_client/_tests/pytest_plugin/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import pytest
1212

13+
_SIFT_ENV_VARS = ("SIFT_API_KEY", "SIFT_GRPC_URI", "SIFT_REST_URI")
14+
1315

1416
@pytest.fixture
1517
def write_plugin_conftest(pytester: pytest.Pytester) -> Callable[[], None]:
@@ -19,3 +21,10 @@ def _write() -> None:
1921
pytester.makeconftest('pytest_plugins = ["sift_client.pytest_plugin"]')
2022

2123
return _write
24+
25+
26+
@pytest.fixture
27+
def clear_sift_env(monkeypatch: pytest.MonkeyPatch) -> None:
28+
"""Unset ``SIFT_API_KEY`` / ``SIFT_GRPC_URI`` / ``SIFT_REST_URI`` for the duration of the test."""
29+
for name in _SIFT_ENV_VARS:
30+
monkeypatch.delenv(name, raising=False)

python/lib/sift_client/_tests/pytest_plugin/test_noop_plugin.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,9 @@ def test_measure_all_outlier(step):
5252
def test_noop_does_not_require_credentials(
5353
self,
5454
pytester: pytest.Pytester,
55-
monkeypatch: pytest.MonkeyPatch,
55+
clear_sift_env: None,
5656
) -> None:
5757
"""The noop plugin never reads SIFT_* env vars; runs cleanly without them."""
58-
for name in ("SIFT_API_KEY", "SIFT_GRPC_URI", "SIFT_REST_URI"):
59-
monkeypatch.delenv(name, raising=False)
6058
pytester.makeconftest('pytest_plugins = ["sift_client.pytest_plugin_noop"]')
6159
pytester.makepyfile("def test_runs(step): step.measure(name='v', value=1.0)")
6260
result = pytester.runpytest()

python/lib/sift_client/_tests/pytest_plugin/test_offline.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@ class TestOfflineMode:
2121
def test_offline_runs_without_network(
2222
self,
2323
pytester: pytest.Pytester,
24-
monkeypatch: pytest.MonkeyPatch,
24+
clear_sift_env: None,
2525
write_plugin_conftest: Callable[[], None],
2626
) -> None:
2727
"""Offline mode constructs the client locally and never pings."""
28-
for name in ("SIFT_API_KEY", "SIFT_GRPC_URI", "SIFT_REST_URI"):
29-
monkeypatch.delenv(name, raising=False)
3028
write_plugin_conftest()
3129
pytester.makepyfile(
3230
"""

python/lib/sift_client/_tests/pytest_plugin/test_online.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,10 @@ def sift_client():
4444
def test_missing_env_vars_named_in_error(
4545
self,
4646
pytester: pytest.Pytester,
47-
monkeypatch: pytest.MonkeyPatch,
47+
clear_sift_env: None,
4848
write_plugin_conftest: Callable[[], None],
4949
) -> None:
5050
"""The default ``sift_client`` fixture names missing env vars in its error."""
51-
for name in ("SIFT_API_KEY", "SIFT_GRPC_URI", "SIFT_REST_URI"):
52-
monkeypatch.delenv(name, raising=False)
5351
write_plugin_conftest()
5452
pytester.makepyfile("def test_should_not_run(): pass")
5553
result = pytester.runpytest()

python/lib/sift_client/pytest_plugin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,8 @@ def report_context(
191191
except pytest.UsageError:
192192
raise
193193
except Exception as exc:
194-
grpc_url = getattr(getattr(sift_client, "grpc_client", None), "_config", None)
195-
grpc_url = getattr(grpc_url, "uri", "<unknown>")
194+
grpc_config = getattr(getattr(sift_client, "grpc_client", None), "_config", None)
195+
grpc_url = getattr(grpc_config, "uri", "<unknown>")
196196
raise pytest.UsageError(
197197
f"Sift ping failed against {grpc_url}: {exc}. "
198198
"Pass --sift-offline to run without contacting Sift."

python/lib/sift_client/pytest_plugin_noop.py

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,18 @@
2424
from contextlib import AbstractContextManager
2525
from typing import TYPE_CHECKING, Any, Generator
2626

27+
import numpy as np
2728
import pytest
2829

29-
from sift_client.util.test_results.bounds import value_passes_bounds
30+
from sift_client.util.test_results.bounds import (
31+
all_within_bounds,
32+
to_numpy_array,
33+
value_passes_bounds,
34+
)
3035

3136
if TYPE_CHECKING:
3237
from datetime import datetime
3338

34-
import numpy as np
3539
import pandas as pd
3640
from numpy.typing import NDArray
3741

@@ -96,18 +100,7 @@ def measure_avg(
96100
metadata: dict[str, str | float | bool] | None = None,
97101
channel_names: list[str] | list[Channel] | None = None,
98102
) -> bool:
99-
import numpy as np
100-
import pandas as pd
101-
102-
if isinstance(values, list):
103-
arr = np.array(values)
104-
elif isinstance(values, np.ndarray):
105-
arr = values
106-
elif isinstance(values, pd.Series):
107-
arr = values.to_numpy()
108-
else:
109-
raise ValueError(f"Invalid value type: {type(values)}")
110-
return value_passes_bounds(float(np.mean(arr)), bounds)
103+
return value_passes_bounds(float(np.mean(to_numpy_array(values))), bounds)
111104

112105
def measure_all(
113106
self,
@@ -121,32 +114,7 @@ def measure_all(
121114
metadata: dict[str, str | float | bool] | None = None,
122115
channel_names: list[str] | list[Channel] | None = None,
123116
) -> bool:
124-
import numpy as np
125-
import pandas as pd
126-
127-
from sift_client.sift_types.test_report import NumericBounds as _NumericBounds
128-
129-
if isinstance(values, list):
130-
arr = np.array(values)
131-
elif isinstance(values, np.ndarray):
132-
arr = values
133-
elif isinstance(values, pd.Series):
134-
arr = values.to_numpy()
135-
else:
136-
raise ValueError(f"Invalid value type: {type(values)}")
137-
138-
nb = bounds
139-
if isinstance(nb, dict):
140-
nb = _NumericBounds(min=bounds.get("min"), max=bounds.get("max")) # type: ignore[union-attr]
141-
mask = None
142-
if nb.min is not None:
143-
mask = arr < nb.min
144-
if nb.max is not None:
145-
above = arr > nb.max
146-
mask = mask | above if mask is not None else above
147-
if mask is None:
148-
raise ValueError("No bounds provided")
149-
return bool(arr[mask].size == 0)
117+
return all_within_bounds(to_numpy_array(values), bounds)
150118

151119
def report_outcome(self, name: str, result: bool, reason: str | None = None) -> bool:
152120
return result

python/lib/sift_client/util/test_results/bounds.py

Lines changed: 80 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import annotations
22

3-
from datetime import datetime, timezone
3+
from typing import TYPE_CHECKING
4+
5+
import numpy as np
6+
import pandas as pd
47

58
from sift_client.sift_types.test_report import (
69
NumericBounds,
@@ -10,6 +13,55 @@
1013
TestMeasurementUpdate,
1114
)
1215

16+
if TYPE_CHECKING:
17+
from numpy.typing import NDArray
18+
19+
20+
def to_numpy_array(
21+
values: list[float | int] | NDArray[np.float64] | pd.Series,
22+
) -> NDArray[np.float64]:
23+
"""Normalize a list / ndarray / pandas Series into a numpy array.
24+
25+
Shared by ``measure_avg`` and ``measure_all`` in both the real plugin and
26+
the no-op sibling so the accepted input types stay in sync.
27+
"""
28+
if isinstance(values, list):
29+
return np.array(values)
30+
if isinstance(values, np.ndarray):
31+
return values
32+
if isinstance(values, pd.Series):
33+
return values.to_numpy()
34+
raise ValueError(f"Invalid value type: {type(values)}")
35+
36+
37+
def out_of_bounds_mask(
38+
arr: NDArray[np.float64],
39+
bounds: dict[str, float] | NumericBounds,
40+
) -> NDArray[np.bool_]:
41+
"""Return a boolean mask selecting elements of ``arr`` that violate ``bounds``.
42+
43+
Raises ``ValueError`` when ``bounds`` has neither ``min`` nor ``max`` set.
44+
"""
45+
if isinstance(bounds, dict):
46+
bounds = NumericBounds(min=bounds.get("min"), max=bounds.get("max"))
47+
mask: NDArray[np.bool_] | None = None
48+
if bounds.min is not None:
49+
mask = arr < bounds.min
50+
if bounds.max is not None:
51+
above = arr > bounds.max
52+
mask = mask | above if mask is not None else above
53+
if mask is None:
54+
raise ValueError("No bounds provided")
55+
return mask
56+
57+
58+
def all_within_bounds(
59+
arr: NDArray[np.float64],
60+
bounds: dict[str, float] | NumericBounds,
61+
) -> bool:
62+
"""Return True when every element of ``arr`` is within ``bounds``."""
63+
return bool(arr[out_of_bounds_mask(arr, bounds)].size == 0)
64+
1365

1466
def assign_value_to_measurement(
1567
measurement: TestMeasurement | TestMeasurementCreate | TestMeasurementUpdate,
@@ -43,13 +95,31 @@ def value_passes_bounds(
4395
Used by consumers that need pass/fail semantics matching the real plugin but
4496
do not transmit a measurement (e.g. ``sift_client.pytest_plugin_noop``).
4597
"""
46-
scratch = TestMeasurementCreate(
47-
name="",
48-
test_step_id="",
49-
passed=True,
50-
timestamp=datetime.now(timezone.utc),
51-
)
52-
return evaluate_measurement_bounds(scratch, value, bounds)
98+
if bounds is None:
99+
return True
100+
if isinstance(bounds, dict):
101+
bounds = NumericBounds(min=bounds.get("min"), max=bounds.get("max"))
102+
if isinstance(bounds, bool):
103+
if isinstance(value, str):
104+
return str(value).lower() == str(bounds).lower()
105+
return bool(value) == bounds
106+
if isinstance(bounds, str):
107+
if not (isinstance(value, str) or isinstance(value, bool)):
108+
raise ValueError("Value must be a string if bounds provided is a string")
109+
if isinstance(value, bool):
110+
return str(value).lower() == str(bounds).lower()
111+
return value == bounds
112+
# NumericBounds
113+
try:
114+
if bounds.min is not None and bounds.min > value: # type: ignore[operator]
115+
return False
116+
if bounds.max is not None and bounds.max < value: # type: ignore[operator]
117+
return False
118+
except TypeError:
119+
raise TypeError(
120+
f"Value must be a float or int to evaluate numeric bounds but gave {type(value)}"
121+
) from None
122+
return True
53123

54124

55125
def evaluate_measurement_bounds(
@@ -73,31 +143,10 @@ def evaluate_measurement_bounds(
73143

74144
if isinstance(bounds, dict):
75145
bounds = NumericBounds(min=bounds.get("min"), max=bounds.get("max"))
76-
if isinstance(bounds, bool):
77-
if isinstance(value, str):
78-
measurement.passed = str(value).lower() == str(bounds).lower()
79-
else:
80-
measurement.passed = bool(value) == bounds
81-
return bool(measurement.passed)
82-
elif isinstance(bounds, str):
83-
if not (isinstance(value, str) or isinstance(value, bool)):
84-
raise ValueError("Value must be a string if bounds provided is a string")
146+
if isinstance(bounds, str) and not isinstance(bounds, bool):
85147
measurement.string_expected_value = bounds
86-
if isinstance(value, bool):
87-
measurement.passed = str(value).lower() == str(bounds).lower()
88-
else:
89-
measurement.passed = value == bounds
90148
elif isinstance(bounds, NumericBounds):
91149
measurement.numeric_bounds = bounds
92-
measurement.passed = True
93-
try:
94-
if measurement.numeric_bounds.min is not None:
95-
measurement.passed = measurement.passed and measurement.numeric_bounds.min <= value # type: ignore
96-
if measurement.numeric_bounds.max is not None:
97-
measurement.passed = measurement.passed and measurement.numeric_bounds.max >= value # type: ignore
98-
except TypeError:
99-
raise TypeError(
100-
f"Value must be a float or int to evaluate numeric bounds but gave {type(value)}"
101-
) from None
102150

151+
measurement.passed = value_passes_bounds(value, bounds)
103152
return bool(measurement.passed)

python/lib/sift_client/util/test_results/context_manager.py

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from typing import TYPE_CHECKING
1414

1515
import numpy as np
16-
import pandas as pd
1716

1817
from sift_client.sift_types.test_report import (
1918
ErrorInfo,
@@ -28,9 +27,12 @@
2827
)
2928
from sift_client.util.test_results.bounds import (
3029
evaluate_measurement_bounds,
30+
out_of_bounds_mask,
31+
to_numpy_array,
3132
)
3233

3334
if TYPE_CHECKING:
35+
import pandas as pd
3436
from numpy.typing import NDArray
3537

3638
from sift_client.client import SiftClient
@@ -509,15 +511,7 @@ def measure_avg(
509511
returns: The true if the average of the values is within the bounds, false otherwise.
510512
"""
511513
timestamp = timestamp if timestamp else datetime.now(timezone.utc)
512-
np_array = None
513-
if isinstance(values, list):
514-
np_array = np.array(values)
515-
elif isinstance(values, np.ndarray):
516-
np_array = values
517-
elif isinstance(values, pd.Series):
518-
np_array = values.to_numpy()
519-
else:
520-
raise ValueError(f"Invalid value type: {type(values)}")
514+
np_array = to_numpy_array(values)
521515
avg = float(np.mean(np_array))
522516
result = self.measure(
523517
name=name,
@@ -565,31 +559,8 @@ def measure_all(
565559
returns: The true if all values are within the bounds, false otherwise.
566560
"""
567561
timestamp = timestamp if timestamp else datetime.now(timezone.utc)
568-
np_array = None
569-
if isinstance(values, list):
570-
np_array = np.array(values)
571-
elif isinstance(values, np.ndarray):
572-
np_array = values
573-
elif isinstance(values, pd.Series):
574-
np_array = values.to_numpy()
575-
else:
576-
raise ValueError(f"Invalid value type: {type(values)}")
577-
578-
numeric_bounds = bounds
579-
if isinstance(numeric_bounds, dict):
580-
numeric_bounds = NumericBounds(min=bounds.get("min"), max=bounds.get("max")) # type: ignore
581-
582-
# Construct a mask of the values that are outside the bounds.
583-
mask = None
584-
if numeric_bounds.min is not None:
585-
mask = np_array < numeric_bounds.min
586-
if numeric_bounds.max is not None:
587-
val_above_max = np_array > numeric_bounds.max
588-
mask = mask | val_above_max if mask is not None else val_above_max
589-
if mask is None:
590-
raise ValueError("No bounds provided")
591-
592-
rows_outside_bounds = np_array[mask]
562+
np_array = to_numpy_array(values)
563+
rows_outside_bounds = np_array[out_of_bounds_mask(np_array, bounds)]
593564
for row in rows_outside_bounds:
594565
self.measure(
595566
name=name,

0 commit comments

Comments
 (0)