Repository: AzureAD/microsoft-authentication-library-for-python
Issue category: Performance Issue

Issue:
MSAL can consider using lazy import for `request`, `jwt`
**Describe the bug**

Importing MSAL is can cost ~300ms on Windows due to some heavy libraries like `request` and `jwt`: 

```
python -X importtime -c "import msal"  2>perf.log; tuna .\perf.log
```

![image](https://user-images.githubusercontent.com/4003950/137872368-a85b9753-f46b-4ffe-acdd-323815486c1b.png)

MSAL can consider using lazy import for these libraries so that importing MSAL can be lightweight. (Azure CLI lazily imports MSAL: https://github.com/Azure/azure-cli/pull/19898)


Hints:

Patch:
diff --git a/msal/application.py b/msal/application.py
index 5d1406af..a06df303 100644
--- a/msal/application.py
+++ b/msal/application.py
@@ -11,8 +11,6 @@
 from threading import Lock
 import os
 
-import requests
-
 from .oauth2cli import Client, JwtAssertionCreator
 from .oauth2cli.oidc import decode_part
 from .authority import Authority
@@ -425,6 +423,8 @@ def __init__(
         if http_client:
             self.http_client = http_client
         else:
+            import requests  # Lazy load
+
             self.http_client = requests.Session()
             self.http_client.verify = verify
             self.http_client.proxies = proxies
diff --git a/msal/authority.py b/msal/authority.py
index 145ce3d9..ecf6b777 100644
--- a/msal/authority.py
+++ b/msal/authority.py
@@ -5,11 +5,6 @@
     from urlparse import urlparse
 import logging
 
-# Historically some customers patched this module-wide requests instance.
-# We keep it here for now. They will be removed in next major release.
-import requests
-import requests as _requests
-
 from .exceptions import MsalServiceError
 
 
@@ -59,9 +54,10 @@ class Authority(object):
     _domains_without_user_realm_discovery = set([])
 
     @property
-    def http_client(self):  # Obsolete. We will remove this in next major release.
-        # A workaround: if module-wide requests is patched, we honor it.
-        return self._http_client if requests is _requests else requests
+    def http_client(self):  # Obsolete. We will remove this eventually
+        warnings.warn(
+            "authority.http_client might be removed in MSAL Python 1.21+", DeprecationWarning)
+        return self._http_client
 
     def __init__(self, authority_url, http_client, validate_authority=True):
         """Creates an authority instance, and also validates it.
@@ -84,7 +80,7 @@ def __init__(self, authority_url, http_client, validate_authority=True):
             payload = instance_discovery(
                 "https://{}{}/oauth2/v2.0/authorize".format(
                     self.instance, authority.path),
-                self.http_client)
+                self._http_client)
             if payload.get("error") == "invalid_instance":
                 raise ValueError(
                     "invalid_instance: "
@@ -104,7 +100,7 @@ def __init__(self, authority_url, http_client, validate_authority=True):
         try:
             openid_config = tenant_discovery(
                 tenant_discovery_endpoint,
-                self.http_client)
+                self._http_client)
         except ValueError:
             raise ValueError(
                 "Unable to get authority configuration for {}. "
@@ -124,7 +120,7 @@ def user_realm_discovery(self, username, correlation_id=None, response=None):
         # "federation_protocol", "cloud_audience_urn",
         # "federation_metadata_url", "federation_active_auth_url", etc.
         if self.instance not in self.__class__._domains_without_user_realm_discovery:
-            resp = response or self.http_client.get(
+            resp = response or self._http_client.get(
                 "https://{netloc}/common/userrealm/{username}?api-version=1.0".format(
                     netloc=self.instance, username=username),
                 headers={'Accept': 'application/json',
diff --git a/msal/oauth2cli/assertion.py b/msal/oauth2cli/assertion.py
index 0cf58799..855bd16b 100644
--- a/msal/oauth2cli/assertion.py
+++ b/msal/oauth2cli/assertion.py
@@ -4,8 +4,6 @@
 import uuid
 import logging
 
-import jwt
-
 
 logger = logging.getLogger(__name__)
 
@@ -99,6 +97,7 @@ def create_normal_assertion(
         Parameters are defined in https://tools.ietf.org/html/rfc7523#section-3
         Key-value pairs in additional_claims will be added into payload as-is.
         """
+        import jwt  # Lazy loading
         now = time.time()
         payload = {
             'aud': audience,
diff --git a/msal/oauth2cli/oauth2.py b/msal/oauth2cli/oauth2.py
index e092b3dd..54708004 100644
--- a/msal/oauth2cli/oauth2.py
+++ b/msal/oauth2cli/oauth2.py
@@ -17,8 +17,6 @@
 import string
 import hashlib
 
-import requests
-
 from .authcode import AuthCodeReceiver as _AuthCodeReceiver
 
 try:
@@ -159,6 +157,8 @@ def __init__(
                     "when http_client is in use")
             self._http_client = http_client
         else:
+            import requests  # Lazy loading
+
             self._http_client = requests.Session()
             self._http_client.verify = True if verify is None else verify
             self._http_client.proxies = proxies


Test patch:
diff --git a/tests/test_authority_patch.py b/tests/test_authority_patch.py
deleted file mode 100644
index 1feca62d..00000000
--- a/tests/test_authority_patch.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import unittest
-
-import msal
-from tests.http_client import MinimalHttpClient
-
-
-class DummyHttpClient(object):
-    def get(self, url, **kwargs):
-        raise RuntimeError("just for testing purpose")
-
-
-class TestAuthorityHonorsPatchedRequests(unittest.TestCase):
-    """This is only a workaround for an undocumented behavior."""
-    def test_authority_honors_a_patched_requests(self):
-        # First, we test that the original, unmodified authority is working
-        a = msal.authority.Authority(
-            "https://login.microsoftonline.com/common", MinimalHttpClient())
-        self.assertEqual(
-            a.authorization_endpoint,
-            'https://login.microsoftonline.com/common/oauth2/v2.0/authorize')
-
-        original = msal.authority.requests
-        try:
-            # Now we mimic a (discouraged) practice of patching authority.requests
-            msal.authority.requests = DummyHttpClient()
-            # msal.authority is expected to honor that patch.
-            with self.assertRaises(RuntimeError):
-                a = msal.authority.Authority(
-                    "https://login.microsoftonline.com/common", MinimalHttpClient())
-        finally:  # Tricky:
-            # Unpatch is necessary otherwise other test cases would be affected
-            msal.authority.requests = original