Repository: OpenMined/PySyft
Pull request: load multiple libs in one call
## Description
Fix #5344
Enable loading multiple libraries in a single `syft.load` call.

## How has this been tested?
`tests/syft/api/load_lib_test.py`

## Checklist
- [x] I have followed the [Contribution Guidelines](https://github.com/OpenMined/.github/blob/master/CONTRIBUTING.md) and [Code of Conduct](https://github.com/OpenMined/.github/blob/master/CODE_OF_CONDUCT.md)
- [x] I have commented my code following the [OpenMined Styleguide](https://github.com/OpenMined/.github/blob/master/STYLEGUIDE.md)
- [ ] I have labeled this PR with the relevant [Type labels](https://github.com/OpenMined/.github/labels?q=Type%3A)
- [x] My changes are covered by tests


Problem statement:
Add ability to load multiple libs in one call
## Description
`sy.load()` should accept any valid sequence of strings, variadic args, a list, tuple or set of strings like:
```
sy.load("pydp", "opacus")
sy.load(["pydp", "opacus"])
sy.load(("pydp", "opacus"))
sy.load({"pydp", "opacus"})
```

## Definition of Done
Loading multiple libs in one go is possible with tests to show it works.

Hints from issue discussion:

Commit message: python linting

Patch to review:
diff --git a/src/syft/lib/__init__.py b/src/syft/lib/__init__.py
index b0ce1f86fe5..73214cb7d6a 100644
--- a/src/syft/lib/__init__.py
+++ b/src/syft/lib/__init__.py
@@ -5,7 +5,10 @@
 from typing import Any
 from typing import Any as TypeAny
 from typing import Dict as TypeDict
+from typing import List as TypeList
 from typing import Optional
+from typing import Set as TypeSet
+from typing import Tuple as TypeTuple
 from typing import Union as TypeUnion
 import warnings
 
@@ -142,20 +145,26 @@ def _load_lib(*, lib: str, options: TypeDict[str, TypeAny] = {}) -> None:
             _regenerate_unions(lib_ast=lib_ast, client=client)
 
 
-def load(lib: str, options: TypeDict[str, TypeAny] = {}) -> None:
+def load(
+    *libs: TypeUnion[TypeList, TypeTuple, TypeSet, str],
+    options: TypeDict[str, TypeAny] = {},
+) -> None:
     """
     Load and Update Node with given library module
 
     Args:
-        lib: name of library to load and update Node with
+        *libs: names of libraries to load and update Node with (can be variadic, tuple, list, set)
         options: external requirements for loading library successfully
     """
-    try:
-        _load_lib(lib=lib, options=options)
-    except VendorLibraryImportException as e:
-        critical(e)
-    except Exception as e:
-        critical(f"Unable to load package support for: {lib}. {e}")
+    if not isinstance(libs[0], str):
+        libs = tuple(libs[0])
+    for lib in libs:
+        try:
+            _load_lib(lib=str(lib), options=options)
+        except VendorLibraryImportException as e:
+            critical(e)
+        except Exception as e:
+            critical(f"Unable to load package support for: {lib}. {e}")
 
 
 def load_lib(lib: str, options: TypeDict[str, TypeAny] = {}) -> None:
@@ -171,7 +180,7 @@ def load_lib(lib: str, options: TypeDict[str, TypeAny] = {}) -> None:
     msg = "load_lib() is deprecated please use load() in the future"
     warning(msg, print=True)
     warnings.warn(msg, DeprecationWarning)
-    load(lib=lib, options=options)
+    load(lib, options=options)
 
 
 # now we need to load the relevant frameworks onto the node
diff --git a/tests/syft/api/load_lib_test.py b/tests/syft/api/load_lib_test.py
new file mode 100644
index 00000000000..f0c733dd7dc
--- /dev/null
+++ b/tests/syft/api/load_lib_test.py
@@ -0,0 +1,9 @@
+# syft absolute
+import syft as sy
+
+
+def test_load_lib() -> None:
+    assert sy.lib.load(["tenseal", "opacus"]) is None
+    assert sy.lib.load("tenseal") is None
+    assert sy.lib.load("tenseal", "opacus") is None
+    assert sy.lib.load(["tenseal", "opacus"]) is None


Code review comments:
diff_hunk: @@ -142,20 +145,26 @@ def _load_lib(*, lib: str, options: TypeDict[str, TypeAny] = {}) -> None:
             _regenerate_unions(lib_ast=lib_ast, client=client)
 
 
-def load(lib: str, options: TypeDict[str, TypeAny] = {}) -> None:
+def load(
+    *libs: TypeUnion[TypeList, TypeTuple, TypeSet, str],
+    options: TypeDict[str, TypeAny] = {},
+) -> None:
     """
     Load and Update Node with given library module
 
     Args:
-        lib: name of library to load and update Node with
+        *libs: names of libraries to load and update Node with (can be variadic, tuple, list, set)
         options: external requirements for loading library successfully
     """
-    try:
-        _load_lib(lib=lib, options=options)
-    except VendorLibraryImportException as e:
-        critical(e)
-    except Exception as e:
-        critical(f"Unable to load package support for: {lib}. {e}")
+    if not isinstance(libs[0], str):
path: src/syft/lib/__init__.py
text: @user1:
We shouldn't convert to string, we need to check all the arguments (even if nested) are strings.

In other words:

```
if isintance(args[0], str):
   ...
elif is_iterable(args[0]):
   # iterate through this and check if the components are strings and load them
else:
   # error

@author:
Done.

diff_hunk: @@ -142,20 +145,26 @@ def _load_lib(*, lib: str, options: TypeDict[str, TypeAny] = {}) -> None:
             _regenerate_unions(lib_ast=lib_ast, client=client)
 
 
-def load(lib: str, options: TypeDict[str, TypeAny] = {}) -> None:
+def load(
+    *libs: TypeUnion[TypeList, TypeTuple, TypeSet, str],
+    options: TypeDict[str, TypeAny] = {},
+) -> None:
     """
     Load and Update Node with given library module
 
     Args:
-        lib: name of library to load and update Node with
+        *libs: names of libraries to load and update Node with (can be variadic, tuple, list, set)
         options: external requirements for loading library successfully
     """
-    try:
-        _load_lib(lib=lib, options=options)
-    except VendorLibraryImportException as e:
-        critical(e)
-    except Exception as e:
-        critical(f"Unable to load package support for: {lib}. {e}")
+    if not isinstance(libs[0], str):
+        libs = tuple(libs[0])
+    for lib in libs:
+        try:
+            _load_lib(lib=str(lib), options=options)
path: src/syft/lib/__init__.py
text: @user1:
Q: Why is the need to convert to string?

@author:
To ensure only string objects get passed.

Now checking using `isinstance` before calling `_load_lib`, to better handle error reporting.