Repository: firebase/firebase-admin-python

Issue:
pathlib and Django v3.1 Compatibility

  * Operating System version: Windows 10 home single language
  * Firebase SDK version: _____
  * Library version: 4.4.0
  * Firebase Product: auth

Recently with Django 3.1 release, the previous os.path library was removed and pathlib is introduced for maintaining the file paths. The class type returned by os.path is str while pathlib returns its own pathlib.Path class type.

This raises ValueError even if the path is correct using pathlib and if we do it using the os.path library it works fine.

ValueError: Invalid certificate argument. Certificate argument must be a file path, or a dict containing th
e parsed file contents.

So, to reproduce this I figured out two ways:

1. Explicit typecasting of the path argument.
```
credentials.Certificate(str(path_to_json))
```

2. Some tweaks into the credentials.py file to accept pathlib object as well.

```
if isinstance(cert, str) or isinstance(cert, pathlib.Path):
            with open(cert) as json_file:
                json_data = json.load(json_file)
        elif isinstance(cert, dict):
            json_data = cert
        else:
            raise ValueError(
                'Invalid certificate argument: "{0}". Certificate argument must be a file path, '
                'or a dict containing the parsed file contents.'.format(cert))
```


Discussion:
hiranya911: As suspected encountering issues with Python 3.5. Instead of implementing an ugly hack, I think a better course of action is to drop Python 3.5 support (which EoL'ed a couple of months ago), and delay this fix until then.

Patch:
diff --git a/firebase_admin/credentials.py b/firebase_admin/credentials.py
index 1f207e4..5477e1c 100644
--- a/firebase_admin/credentials.py
+++ b/firebase_admin/credentials.py
@@ -15,6 +15,7 @@
 """Firebase credentials module."""
 import collections
 import json
+import pathlib
 
 import google.auth
 from google.auth.transport import requests
@@ -78,7 +79,7 @@ class Certificate(Base):
           ValueError: If the specified certificate is invalid.
         """
         super(Certificate, self).__init__()
-        if isinstance(cert, str):
+        if _is_file_path(cert):
             with open(cert) as json_file:
                 json_data = json.load(json_file)
         elif isinstance(cert, dict):
@@ -179,7 +180,7 @@ class RefreshToken(Base):
           ValueError: If the refresh token configuration is invalid.
         """
         super(RefreshToken, self).__init__()
-        if isinstance(refresh_token, str):
+        if _is_file_path(refresh_token):
             with open(refresh_token) as json_file:
                 json_data = json.load(json_file)
         elif isinstance(refresh_token, dict):
@@ -212,3 +213,11 @@ class RefreshToken(Base):
         Returns:
           google.auth.credentials.Credentials: A Google Auth credential instance."""
         return self._g_credential
+
+
+def _is_file_path(path):
+    try:
+        pathlib.Path(path)
+        return True
+    except TypeError:
+        return False


Test patch:
diff --git a/tests/test_credentials.py b/tests/test_credentials.py
index d78ef51..cceb6b6 100644
--- a/tests/test_credentials.py
+++ b/tests/test_credentials.py
@@ -16,6 +16,7 @@
 import datetime
 import json
 import os
+import pathlib
 
 import google.auth
 from google.auth import crypt
@@ -47,6 +48,12 @@ class TestCertificate:
             testutils.resource_filename('service_account.json'))
         self._verify_credential(credential)
 
+    def test_init_from_path_like(self):
+        path = pathlib.Path(testutils.resource_filename('service_account.json'))
+        credential = credentials.Certificate(path)
+        self._verify_credential(credential)
+
+
     def test_init_from_dict(self):
         parsed_json = json.loads(testutils.resource('service_account.json'))
         credential = credentials.Certificate(parsed_json)
@@ -129,6 +136,11 @@ class TestRefreshToken:
             testutils.resource_filename('refresh_token.json'))
         self._verify_credential(credential)
 
+    def test_init_from_path_like(self):
+        path = pathlib.Path(testutils.resource_filename('refresh_token.json'))
+        credential = credentials.RefreshToken(path)
+        self._verify_credential(credential)
+
     def test_init_from_dict(self):
         parsed_json = json.loads(testutils.resource('refresh_token.json'))
         credential = credentials.RefreshToken(parsed_json)