Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
98 commits
Select commit Hold shift + click to select a range
1f4d7c2
add custom_hyperparameters
ParthivNaresh May 20, 2021
33df646
comment
ParthivNaresh May 20, 2021
cc2bb32
print
ParthivNaresh May 21, 2021
0ea03de
print
ParthivNaresh May 21, 2021
d3323e0
print
ParthivNaresh May 21, 2021
a45a187
print statements
ParthivNaresh May 21, 2021
8b31f5c
remove custom hyperparameters from pipelines
ParthivNaresh May 22, 2021
f2a2917
more changes
ParthivNaresh May 23, 2021
e749141
print update
ParthivNaresh May 24, 2021
358532c
Set component_parameters to include passed pipeline parameters
ParthivNaresh May 25, 2021
d87aeb3
prints
ParthivNaresh May 25, 2021
38559e8
print
ParthivNaresh May 25, 2021
416af96
update get_hyperparameter_ranges
ParthivNaresh May 25, 2021
c4c3d98
print
ParthivNaresh May 25, 2021
8f8dad8
prevent batch 0 parameter selection from being out of bounds of the h…
ParthivNaresh May 26, 2021
4b93ea6
api updates
ParthivNaresh May 26, 2021
0e968e5
test updates
ParthivNaresh May 27, 2021
27f048d
add changes to pipelines passed in
ParthivNaresh May 27, 2021
7bcec95
practice tests
ParthivNaresh May 27, 2021
a62361c
test updates
ParthivNaresh May 27, 2021
4937680
print update
ParthivNaresh May 27, 2021
4b28e59
print update
ParthivNaresh May 27, 2021
98b4870
pass frozen parameters
ParthivNaresh May 27, 2021
1698b9b
upadte tests
ParthivNaresh May 27, 2021
8f7be8c
test update
ParthivNaresh May 27, 2021
da01d26
test update
ParthivNaresh May 28, 2021
8c84504
test update
ParthivNaresh May 28, 2021
21cc485
print statements
ParthivNaresh May 28, 2021
4adaef7
test update
ParthivNaresh May 29, 2021
b7aae18
test updates
ParthivNaresh May 30, 2021
7ff180c
test updates
ParthivNaresh May 30, 2021
5839642
test update
ParthivNaresh May 30, 2021
7134c33
test updates
ParthivNaresh May 31, 2021
f52668f
test changes
ParthivNaresh May 31, 2021
23aa37b
test update
ParthivNaresh May 31, 2021
0deb613
test updates
ParthivNaresh May 31, 2021
2fd843e
test updates
ParthivNaresh Jun 1, 2021
9fba1a6
Merge branch 'main' into Clean-API-Frozen-And-HyperParameters
ParthivNaresh Jun 1, 2021
81e5ab4
Merge branch 'Clean-API-Frozen-And-HyperParameters' of ssh://github.c…
ParthivNaresh Jun 1, 2021
b0b4c30
Release notes and dask engine test update
ParthivNaresh Jun 1, 2021
7f2f81d
docs update
ParthivNaresh Jun 1, 2021
01dcd81
docs update
ParthivNaresh Jun 1, 2021
52522ad
update version in docs
ParthivNaresh Jun 1, 2021
cd32f60
docs updates
ParthivNaresh Jun 1, 2021
c5e8154
Merge branch 'main' into Clean-API-Frozen-And-HyperParameters
chukarsten Jun 1, 2021
3f12779
remove superfluous code
ParthivNaresh Jun 2, 2021
b24d76b
Merge branch 'main' into Clean-API-Frozen-And-HyperParameters
ParthivNaresh Jun 2, 2021
6a0045c
test update
ParthivNaresh Jun 2, 2021
d345b1e
Merge branch 'main' into Clean-API-Frozen-And-HyperParameters
ParthivNaresh Jun 2, 2021
131cf11
Merge branch 'main' into Clean-API-Frozen-And-HyperParameters
ParthivNaresh Jun 2, 2021
c7471c0
prints
ParthivNaresh Jun 3, 2021
67441c0
clean nitpick
ParthivNaresh Jun 4, 2021
23b4dd3
Reformattings
ParthivNaresh Jun 4, 2021
ac579f2
Merge branch 'main' into Clean-API-Frozen-And-HyperParameters
ParthivNaresh Jun 4, 2021
eabc6bb
Merge branch 'main' into Clean-API-Frozen-And-HyperParameters
ParthivNaresh Jun 4, 2021
88e3c72
lint fixes
ParthivNaresh Jun 4, 2021
62d36a1
lint update
ParthivNaresh Jun 4, 2021
801f8c3
lint update
ParthivNaresh Jun 4, 2021
718f7f5
lint fix
ParthivNaresh Jun 4, 2021
cff865f
lint fix
ParthivNaresh Jun 4, 2021
17a39e5
initial commit
ParthivNaresh Jun 4, 2021
705855c
updates
ParthivNaresh Jun 7, 2021
aa46104
add docstring
ParthivNaresh Jun 8, 2021
84ce2aa
update api ref
ParthivNaresh Jun 8, 2021
5d8ae24
update api ref
ParthivNaresh Jun 8, 2021
5d3aec2
test updates
ParthivNaresh Jun 9, 2021
562648a
test updates
ParthivNaresh Jun 9, 2021
925d123
Merge branch 'main' into Replace-AllowedPipelines-ComponentGraphs
ParthivNaresh Jun 9, 2021
b8d4a63
test updates
ParthivNaresh Jun 10, 2021
e67a3ee
updates
ParthivNaresh Jun 10, 2021
5921047
lint fixes and add check for unique names
ParthivNaresh Jun 10, 2021
fc9bc2a
lint fixes
ParthivNaresh Jun 10, 2021
b58f6cd
lint fixes and update engine tests
ParthivNaresh Jun 10, 2021
2e8401e
docs update
ParthivNaresh Jun 10, 2021
291e188
lint fixes
ParthivNaresh Jun 10, 2021
aa4d8a5
test fixes
ParthivNaresh Jun 10, 2021
1bd99a7
lint fix
ParthivNaresh Jun 10, 2021
3874502
test update
ParthivNaresh Jun 10, 2021
85c296e
docs update
ParthivNaresh Jun 10, 2021
f4bd833
conf test clean up
ParthivNaresh Jun 10, 2021
5fd09c4
test update
ParthivNaresh Jun 10, 2021
e0d54e5
lint and doc fix
ParthivNaresh Jun 10, 2021
5a3d5a3
test updates
ParthivNaresh Jun 14, 2021
a2aa4b0
add component graph check
ParthivNaresh Jun 14, 2021
9cef3a5
lint fix
ParthivNaresh Jun 14, 2021
a4450a1
lint fix
ParthivNaresh Jun 14, 2021
ab05f3f
lint fix
ParthivNaresh Jun 14, 2021
aa9ff36
Merge branch 'main' into Replace-AllowedPipelines-ComponentGraphs
ParthivNaresh Jun 17, 2021
969dffa
Update allowed_component_graphs format, update tests
ParthivNaresh Jun 17, 2021
2e18c65
lint updates
ParthivNaresh Jun 17, 2021
352f8d0
docs update
ParthivNaresh Jun 17, 2021
da5bd00
lint fixes and updates
ParthivNaresh Jun 18, 2021
3674fdb
Merge branch 'main' into Replace-AllowedPipelines-ComponentGraphs
ParthivNaresh Jun 18, 2021
a9e22f6
test fix
ParthivNaresh Jun 18, 2021
99447d5
Merge branch 'Replace-AllowedPipelines-ComponentGraphs' of ssh://gith…
ParthivNaresh Jun 18, 2021
2422aaf
remove unused fixtures
ParthivNaresh Jun 18, 2021
86e788c
docstring fix
ParthivNaresh Jun 18, 2021
81a0067
Merge branch 'main' into Replace-AllowedPipelines-ComponentGraphs
ParthivNaresh Jun 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/source/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Release Notes
* Added change for ``k_neighbors`` parameter in SMOTE Oversamplers to automatically handle small samples :pr:`2375`
* Changed naming for ``Logistic Regression Classifier`` file :pr:`2399`
* Changes
* Replaced `allowed_pipelines` with `allowed_component_graphs` :pr:`2364`
* Removed private method ``_compute_features_during_fit`` from ``PipelineBase`` :pr:`2359`
* Documentation Changes
* Fixed start page code and description dataset naming discrepancy :pr:`2370`
Expand All @@ -23,6 +24,7 @@ Release Notes
.. warning::

**Breaking Changes**
* `AutoMLSearch` will accept `allowed_component_graphs` instead of `allowed_pipelines` :pr:`2364`


**v0.26.0 Jun. 08, 2021**
Expand Down
5 changes: 3 additions & 2 deletions docs/source/user_guide/automl.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@
"source": [
"### Using custom pipelines\n",
"\n",
"EvalML's AutoML algorithm generates a set of pipelines to search with. To provide a custom set instead, set allowed_pipelines to a list of custom pipeline instances. Note: this will prevent AutoML from generating other pipelines to search over."
"EvalML's AutoML algorithm generates a set of pipelines to search with. To provide a custom set instead, set allowed_component_graphs to a dictionary of custom component graphs. `AutoMLSearch` will use these to generate `Pipeline` instances. Note: this will prevent AutoML from generating other pipelines to search over."
]
},
{
Expand All @@ -235,7 +235,8 @@
"automl_custom = evalml.automl.AutoMLSearch(X_train=X_train,\n",
" y_train=y_train,\n",
" problem_type='multiclass',\n",
" allowed_pipelines=[MulticlassClassificationPipeline(component_graph=['Simple Imputer', 'Random Forest Classifier'])])"
" allowed_component_graphs={\"My_pipeline\": ['Simple Imputer', 'Random Forest Classifier'],\n",
" \"My_other_pipeline\": ['One Hot Encoder', 'Random Forest Classifier']})"
]
},
{
Expand Down
16 changes: 3 additions & 13 deletions evalml/automl/automl_algorithm/iterative_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ def __init__(
text_in_ensembling=False,
pipeline_params=None,
custom_hyperparameters=None,
_frozen_pipeline_parameters=None,
_estimator_family_order=None,
):
"""An automl algorithm which first fits a base round of pipelines with default parameters, then does a round of parameter tuning on each pipeline in order of performance.
Expand All @@ -54,7 +53,6 @@ def __init__(
text_in_ensembling (boolean): If True and ensembling is True, then n_jobs will be set to 1 to avoid downstream sklearn stacking issues related to nltk.
pipeline_params (dict or None): Pipeline-level parameters that should be passed to the proposed pipelines.
custom_hyperparameters (dict or None): Custom hyperparameter ranges specified for pipelines to iterate over.
_frozen_pipeline_parameters (dict or None): Pipeline-level parameters are frozen and used in the proposed pipelines.
_estimator_family_order (list(ModelFamily) or None): specify the sort order for the first batch. Defaults to _ESTIMATOR_FAMILY_ORDER.
"""
self._estimator_family_order = (
Expand Down Expand Up @@ -95,7 +93,6 @@ def __init__(
self.text_in_ensembling = text_in_ensembling
self._pipeline_params = pipeline_params or {}
self._custom_hyperparameters = custom_hyperparameters or {}
self._frozen_pipeline_parameters = _frozen_pipeline_parameters or {}

if custom_hyperparameters and not isinstance(custom_hyperparameters, dict):
raise ValueError(
Expand Down Expand Up @@ -136,7 +133,7 @@ def next_batch(self):
if self._batch_number == 0:
next_batch = [
pipeline.new(
parameters=self._combine_parameters(pipeline, {}),
parameters=self._transform_parameters(pipeline, {}),
random_seed=self.random_seed,
)
for pipeline in self.allowed_pipelines
Expand All @@ -152,7 +149,7 @@ def next_batch(self):
for pipeline_dict in self._best_pipeline_info.values():
pipeline = pipeline_dict["pipeline"]
pipeline_params = pipeline_dict["parameters"]
parameters = self._combine_parameters(pipeline, pipeline_params)
parameters = self._transform_parameters(pipeline, pipeline_params)
input_pipelines.append(
pipeline.new(parameters=parameters, random_seed=self.random_seed)
)
Expand All @@ -175,21 +172,14 @@ def next_batch(self):
pipeline = self._first_batch_results[idx][1]
for i in range(self.pipelines_per_batch):
proposed_parameters = self._tuners[pipeline.name].propose()
parameters = self._combine_parameters(pipeline, proposed_parameters)
parameters = self._transform_parameters(pipeline, proposed_parameters)
next_batch.append(
pipeline.new(parameters=parameters, random_seed=self.random_seed)
)
self._pipeline_number += len(next_batch)
self._batch_number += 1
return next_batch

def _combine_parameters(self, pipeline, proposed_parameters):
"""Helper function for logic to transform proposed parameters and frozen parameters."""
return {
**self._transform_parameters(pipeline, proposed_parameters),
**self._frozen_pipeline_parameters,
}

def add_result(self, score_to_minimize, pipeline, trained_pipeline_results):
"""Register results from evaluating a pipeline

Expand Down
70 changes: 31 additions & 39 deletions evalml/automl/automl_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
check_all_pipeline_names_unique,
get_best_sampler_for_data,
get_default_primary_search_objective,
get_pipelines_from_component_graphs,
make_data_splitter,
)
from evalml.data_checks import DefaultDataChecks
Expand All @@ -36,8 +37,8 @@
)
from evalml.pipelines import (
BinaryClassificationPipeline,
ComponentGraph,
MulticlassClassificationPipeline,
PipelineBase,
RegressionPipeline,
TimeSeriesBinaryClassificationPipeline,
TimeSeriesMulticlassClassificationPipeline,
Expand Down Expand Up @@ -137,7 +138,7 @@ def __init__(
patience=None,
tolerance=None,
data_splitter=None,
allowed_pipelines=None,
allowed_component_graphs=None,
allowed_model_families=None,
start_iteration_callback=None,
add_result_callback=None,
Expand Down Expand Up @@ -190,10 +191,14 @@ def __init__(
tolerance (float): Minimum percentage difference to qualify as score improvement for early stopping.
Only applicable if patience is not None. Defaults to None.

allowed_pipelines (list(class)): A list of PipelineBase subclasses indicating the pipelines allowed in the search.
The default of None indicates all pipelines for this problem type are allowed. Setting this field will cause
allowed_component_graphs (dict): A dictionary of lists or ComponentGraphs indicating the component graphs allowed in the search.
The format should follow { "Name_0": [list_of_components], "Name_1": [ComponentGraph(...)] }

The default of None indicates all pipeline component graphs for this problem type are allowed. Setting this field will cause
allowed_model_families to be ignored.

e.g. allowed_component_graphs = { "My_Graph": ["Imputer", "One Hot Encoder", "Random Forest Classifier"] }

allowed_model_families (list(str, ModelFamily)): The model families to search. The default of None searches over all
model families. Run evalml.pipelines.components.utils.allowed_model_families("binary") to see options. Change `binary`
to `multiclass` or `regression` depending on the problem type. Note that if allowed_pipelines is provided,
Expand Down Expand Up @@ -399,18 +404,17 @@ def __init__(
logger.warning(
"Unable to import plotly; skipping pipeline search plotting\n"
)

if allowed_pipelines is not None and not isinstance(allowed_pipelines, list):
raise ValueError(
"Parameter allowed_pipelines must be either None or a list!"
)
if allowed_pipelines is not None and not all(
isinstance(p, PipelineBase) for p in allowed_pipelines
):
raise ValueError(
"Every element of allowed_pipelines must an instance of PipelineBase!"
)
self.allowed_pipelines = allowed_pipelines
if allowed_component_graphs is not None:
if not isinstance(allowed_component_graphs, dict):
raise ValueError(
"Parameter allowed_component_graphs must be either None or a dictionary!"
)
for graph_name, graph in allowed_component_graphs.items():
if not isinstance(graph, (list, dict, ComponentGraph)):
raise ValueError(
"Every component graph passed must be of type list, dictionary, or ComponentGraph!"
)
self.allowed_component_graphs = allowed_component_graphs
self.allowed_model_families = allowed_model_families
self._automl_algorithm = None
self._start = 0.0
Expand Down Expand Up @@ -442,15 +446,11 @@ def __init__(
self.custom_hyperparameters = custom_hyperparameters or {}
self.search_iteration_plot = None
self._interrupted = False
self._frozen_pipeline_parameters = {}

parameters = copy.copy(self.pipeline_parameters)

if self.problem_configuration:
parameters.update({"pipeline": self.problem_configuration})
self._frozen_pipeline_parameters.update(
{"pipeline": self.problem_configuration}
)

self.sampler_method = sampler_method
self.sampler_balanced_ratio = sampler_balanced_ratio
Expand All @@ -473,11 +473,8 @@ def __init__(
parameters[self._sampler_name].update(
{"sampling_ratio": self.sampler_balanced_ratio}
)
self._frozen_pipeline_parameters[self._sampler_name] = parameters[
self._sampler_name
]

if self.allowed_pipelines is None:
if self.allowed_component_graphs is None:
logger.info("Generating pipelines to search over...")
allowed_estimators = get_estimators(
self.problem_type, self.allowed_model_families
Expand All @@ -492,26 +489,30 @@ def __init__(
)
index_columns = list(self.X_train.ww.select("index").columns)
if len(index_columns) > 0 and drop_columns is None:
self._frozen_pipeline_parameters["Drop Columns Transformer"] = {
"columns": index_columns
}
parameters["Drop Columns Transformer"] = {"columns": index_columns}
self.allowed_pipelines = [
make_pipeline(
self.X_train,
self.y_train,
estimator,
self.problem_type,
parameters=self._frozen_pipeline_parameters,
parameters=parameters,
sampler_name=self._sampler_name,
)
for estimator in allowed_estimators
]
else:
self.allowed_pipelines = get_pipelines_from_component_graphs(
self.allowed_component_graphs,
self.problem_type,
parameters,
self.random_seed,
)

if self.allowed_pipelines == []:
raise ValueError("No allowed pipelines to search")

logger.info(f"{len(self.allowed_pipelines)} pipelines ready for search.")
check_all_pipeline_names_unique(self.allowed_pipelines)

run_ensembling = self.ensembling
text_in_ensembling = len(self.X_train.ww.select("natural_language").columns) > 0
Expand Down Expand Up @@ -605,8 +606,7 @@ def __init__(
ensembling=run_ensembling,
text_in_ensembling=text_in_ensembling,
pipeline_params=parameters,
custom_hyperparameters=self.custom_hyperparameters,
_frozen_pipeline_parameters=self._frozen_pipeline_parameters,
custom_hyperparameters=custom_hyperparameters,
)

def _get_batch_number(self):
Expand Down Expand Up @@ -937,14 +937,6 @@ def _validate_problem_type(self):
)
)

for pipeline in self.allowed_pipelines or []:
if pipeline.problem_type != self.problem_type:
raise ValueError(
"Given pipeline {} is not compatible with problem_type {}.".format(
pipeline.name, self.problem_type.value
)
)

def _get_baseline_pipeline(self):
"""Creates a baseline pipeline instance."""
if self.problem_type == ProblemTypes.BINARY:
Expand Down
46 changes: 45 additions & 1 deletion evalml/automl/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@
from sklearn.model_selection import KFold, StratifiedKFold

from evalml.objectives import get_objective
from evalml.pipelines import ComponentGraph
from evalml.pipelines import (
BinaryClassificationPipeline,
ComponentGraph,
MulticlassClassificationPipeline,
RegressionPipeline,
TimeSeriesBinaryClassificationPipeline,
TimeSeriesMulticlassClassificationPipeline,
TimeSeriesRegressionPipeline,
)
from evalml.preprocessing.data_splitters import (
TimeSeriesSplit,
TrainingValidationSplit,
Expand Down Expand Up @@ -220,3 +228,39 @@ def get_hyperparameter_ranges(component_graph, custom_hyperparameters):
component_hyperparameters.update(custom_hyperparameters[component_name])
hyperparameter_ranges[component_name] = component_hyperparameters
return hyperparameter_ranges


def get_pipelines_from_component_graphs(
component_graphs_dict, problem_type, parameters=None, random_seed=0
):
"""
Returns created pipelines from passed component graphs based on the specified problem type.

Arguments:
component_graphs_dict (dict): The dict of component graphs.
problem_type (str or ProblemType): The problem type for which pipelines will be created.
parameters (dict or None): Pipeline-level parameters that should be passed to the proposed pipelines.
random_seed (int): Random seed.

Returns:
list: List of pipelines made from the passed component graphs.
"""
pipeline_class = {
ProblemTypes.BINARY: BinaryClassificationPipeline,
ProblemTypes.MULTICLASS: MulticlassClassificationPipeline,
ProblemTypes.REGRESSION: RegressionPipeline,
ProblemTypes.TIME_SERIES_BINARY: TimeSeriesBinaryClassificationPipeline,
ProblemTypes.TIME_SERIES_MULTICLASS: TimeSeriesMulticlassClassificationPipeline,
ProblemTypes.TIME_SERIES_REGRESSION: TimeSeriesRegressionPipeline,
}[handle_problem_types(problem_type)]
created_pipelines = []
for graph_name, component_graph in component_graphs_dict.items():
created_pipelines.append(
pipeline_class(
component_graph=component_graph,
parameters=parameters,
custom_name=graph_name,
random_seed=random_seed,
)
)
return created_pipelines
2 changes: 1 addition & 1 deletion evalml/pipelines/time_series_regression_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(
Pipeline(parameters={"pipeline": {"date_index": "Date", "max_delay": 4, "gap": 2}}).
random_seed (int): Seed for the random number generator. Defaults to 0.
"""
if "pipeline" not in parameters:
if not parameters or "pipeline" not in parameters:
raise ValueError(
"date_index, gap, and max_delay parameters cannot be omitted from the parameters dict. "
"Please specify them as a dictionary with the key 'pipeline'."
Expand Down
22 changes: 19 additions & 3 deletions evalml/tests/automl_tests/dask_tests/test_automl_dask.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dask.distributed import Client, LocalCluster

from evalml.automl import AutoMLSearch
from evalml.automl.automl_algorithm import IterativeAlgorithm
from evalml.automl.callbacks import raise_error_callback
from evalml.automl.engine import DaskEngine, SequentialEngine
from evalml.tests.automl_tests.dask_test_utils import (
Expand All @@ -11,6 +12,7 @@
TestPipelineWithFitError,
TestPipelineWithScoreError,
)
from evalml.tuners import SKOptTuner


@pytest.fixture
Expand Down Expand Up @@ -107,8 +109,9 @@ def test_automl_train_dask_error_callback(X_y_binary_cls, cluster, caplog):
problem_type="binary",
engine=parallel_engine,
max_iterations=2,
allowed_pipelines=pipelines,
)
automl.allowed_pipelines = pipelines

automl.train_pipelines(pipelines)
assert "Train error for PipelineWithError: Yikes" in caplog.text

Expand All @@ -127,8 +130,9 @@ def test_automl_score_dask_error_callback(X_y_binary_cls, cluster, caplog):
problem_type="binary",
engine=parallel_engine,
max_iterations=2,
allowed_pipelines=pipelines,
)
automl.allowed_pipelines = pipelines

automl.score_pipelines(
pipelines, X, y, objectives=["Log Loss Binary", "F1", "AUC"]
)
Expand All @@ -153,10 +157,22 @@ def test_automl_immediate_quit(X_y_binary_cls, cluster, caplog):
problem_type="binary",
engine=parallel_engine,
max_iterations=4,
allowed_pipelines=pipelines,
error_callback=raise_error_callback,
optimize_thresholds=False,
)
automl._automl_algorithm = IterativeAlgorithm(
max_iterations=4,
allowed_pipelines=pipelines,
tuner_class=SKOptTuner,
random_seed=0,
n_jobs=-1,
number_features=X.shape[1],
pipelines_per_batch=5,
ensembling=False,
text_in_ensembling=False,
pipeline_params={},
custom_hyperparameters=None,
)

# Ensure the broken pipeline raises the error
with pytest.raises(Exception, match="Yikes"):
Expand Down
Loading