Repository: alteryx/evalml (python)

Issue:
Update PipelineBase.load() to accept both a path and a file-like object
Currently, PipelineBase.load() accepts just a file path.  I have some scenarios in which I have a BytesIO object that I would like to pass into the function.

Proposed update: 
1. Allow str or BytesIO as the argument
2. If BytesIO, pass directly to cloudpickle, which already handles BytesIO
3. If str, business as usual

Pull request description:
Added BytesIO support to PipelineBase.load()
### Pull Request Description
Added support for BytesIO input to PipelineBase.load(), primarily to support VFS

Fixes #4178 




Patch:
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index 3b532b4a..681dad25 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -2,6 +2,7 @@ Release Notes
 -------------
 **Future Releases**
     * Enhancements
+        * Added BytesIO support to PipelinBase.load() :pr:`4179`
     * Fixes
         * Capped numpy at <=1.23.5 as a temporary measure for SHAP :pr:`4172`
         * Updated our readthedocs recipe to reenable builds :pr:`4177`
diff --git a/evalml/pipelines/pipeline_base.py b/evalml/pipelines/pipeline_base.py
index faee4866..4b726b1e 100644
--- a/evalml/pipelines/pipeline_base.py
+++ b/evalml/pipelines/pipeline_base.py
@@ -7,6 +7,8 @@ import sys
 import traceback
 from abc import ABC, abstractmethod
 from collections import OrderedDict
+from io import BytesIO
+from typing import Union
 
 import cloudpickle
 import numpy as np
@@ -628,15 +630,18 @@ class PipelineBase(ABC, metaclass=PipelineBaseMeta):
             cloudpickle.dump(self, f, protocol=pickle_protocol)
 
     @staticmethod
-    def load(file_path):
+    def load(file_path: Union[str, BytesIO]):
         """Loads pipeline at file path.
 
         Args:
-            file_path (str): Location to load file.
+            file_path (str|BytesIO): load filepath or a BytesIO object.
 
         Returns:
             PipelineBase object
         """
+        if isinstance(file_path, BytesIO):
+            return cloudpickle.load(file_path)
+
         with open(file_path, "rb") as f:
             return cloudpickle.load(f)
 


Test patch:
diff --git a/evalml/tests/pipeline_tests/test_pipelines.py b/evalml/tests/pipeline_tests/test_pipelines.py
index fb519c16..2477acab 100644
--- a/evalml/tests/pipeline_tests/test_pipelines.py
+++ b/evalml/tests/pipeline_tests/test_pipelines.py
@@ -1,6 +1,8 @@
+import io
 import os
 import pickle
 import re
+from typing import Union
 from unittest.mock import patch
 
 import cloudpickle
@@ -127,13 +129,26 @@ def test_required_fields():
         TestPipelineWithoutComponentGraph(parameters={})
 
 
-def test_serialization(X_y_binary, tmpdir, logistic_regression_binary_pipeline):
+@pytest.mark.parametrize("filetype", ["str", "BytesIO"])
+def test_serialization(
+    X_y_binary, tmpdir, logistic_regression_binary_pipeline, filetype
+):
     X, y = X_y_binary
     path = os.path.join(str(tmpdir), "pipe.pkl")
     pipeline = logistic_regression_binary_pipeline
     pipeline.fit(X, y)
+
     pipeline.save(path)
-    assert pipeline.score(X, y, ["precision"]) == PipelineBase.load(path).score(
+    data_path: Union[str, io.BytesIO]
+
+    data_path = path
+
+    if filetype == "BytesIO":
+        with open(path, "rb") as f:
+            file_contents = f.read()
+        data_path = io.BytesIO(file_contents)
+
+    assert pipeline.score(X, y, ["precision"]) == PipelineBase.load(data_path).score(
         X,
         y,
         ["precision"],