From b2725af28e8c58a30bceee02a5f688d7c7666f4c Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Fri, 13 Jun 2025 16:46:42 -0400 Subject: [PATCH 1/8] chore(deps): Bump minimum supported Python version to 3.9 and add 3.13 to CIs --- .github/workflows/ci.yml | 6 +++--- .github/workflows/nightly.yml | 2 +- .github/workflows/release.yml | 2 +- CONTRIBUTING.md | 2 +- README.md | 4 ++-- integration/conftest.py | 7 ------- requirements.txt | 4 ++-- setup.cfg | 2 ++ setup.py | 7 +++---- 9 files changed, 15 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cc8ec481..bfd29e2cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.9'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.9'] steps: - uses: actions/checkout@v4 @@ -35,10 +35,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Python 3.8 + - name: Set up Python 3.9 uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 282cb1b91..6da6d027d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -36,7 +36,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a7986a5a..7d1d1ebfc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.9 - name: Install dependencies run: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de5934866..72933a24f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ information on using pull requests. ### Initial Setup -You need Python 3.8+ to build and test the code in this repo. +You need Python 3.9+ to build and test the code in this repo. We recommend using [pip](https://pypi.python.org/pypi/pip) for installing the necessary tools and project dependencies. Most recent versions of Python ship with pip. If your development environment diff --git a/README.md b/README.md index 6e3ed6805..146291fa7 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,8 @@ requests, code review feedback, and also pull requests. ## Supported Python Versions -We currently support Python 3.7+. However, Python 3.7 and Python 3.8 support is deprecated, -and developers are strongly advised to use Python 3.9 or higher. Firebase +We currently support Python 3.9+. However, Python 3.9 support is deprecated, +and developers are strongly advised to use Python 3.10 or higher. Firebase Admin Python SDK is also tested on PyPy and [Google App Engine](https://cloud.google.com/appengine/) environments. diff --git a/integration/conftest.py b/integration/conftest.py index efa45932d..169e02d5b 100644 --- a/integration/conftest.py +++ b/integration/conftest.py @@ -16,7 +16,6 @@ import json import pytest -from pytest_asyncio import is_async_test import firebase_admin from firebase_admin import credentials @@ -71,9 +70,3 @@ def api_key(request): 'command-line option.') with open(path) as keyfile: return keyfile.read().strip() - -def pytest_collection_modifyitems(items): - pytest_asyncio_tests = (item for item in items if is_async_test(item)) - session_scope_marker = pytest.mark.asyncio(loop_scope="session") - for async_test in pytest_asyncio_tests: - async_test.add_marker(session_scope_marker, append=False) diff --git a/requirements.txt b/requirements.txt index b5642b549..3d5b68166 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ astroid == 2.3.3 pylint == 2.3.1 -pytest >= 6.2.0 +pytest >= 8.2.2 pytest-cov >= 2.4.0 pytest-localserver >= 0.4.1 -pytest-asyncio >= 0.16.0 +pytest-asyncio >= 0.26.0 pytest-mock >= 3.6.1 respx == 0.22.0 diff --git a/setup.cfg b/setup.cfg index 25c649748..2fff03bfd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,4 @@ [tool:pytest] testpaths = tests +asyncio_default_test_loop_scope = session +asyncio_default_fixture_loop_scope = None diff --git a/setup.py b/setup.py index b9eb11806..25cf12672 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ (major, minor) = (sys.version_info.major, sys.version_info.minor) if major != 3 or minor < 7: - print('firebase_admin requires python >= 3.7', file=sys.stderr) + print('firebase_admin requires python >= 3.9', file=sys.stderr) sys.exit(1) # Read in the package metadata per recommendations from: @@ -60,18 +60,17 @@ keywords='firebase cloud development', install_requires=install_requires, packages=['firebase_admin'], - python_requires='>=3.7', + python_requires='>=3.9', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'License :: OSI Approved :: Apache Software License', ], ) From c6442baacb2152776f9e18f76234bfe3d29b413d Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Fri, 13 Jun 2025 16:49:50 -0400 Subject: [PATCH 2/8] fix deprecation warnings --- .github/workflows/nightly.yml | 3 ++- .github/workflows/release.yml | 3 ++- integration/test_storage.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6da6d027d..3d5420537 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,6 +45,7 @@ jobs: pip install setuptools wheel pip install tensorflow pip install keras + pip install build - name: Run unit tests run: pytest @@ -57,7 +58,7 @@ jobs: # Build the Python Wheel and the source distribution. - name: Package release artifacts - run: python setup.py bdist_wheel sdist + run: python -m build # Attach the packaged artifacts to the workflow output. These can be manually # downloaded for later inspection if necessary. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d1d1ebfc..6cd1d3f07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,6 +56,7 @@ jobs: pip install setuptools wheel pip install tensorflow pip install keras + pip install build - name: Run unit tests run: pytest @@ -68,7 +69,7 @@ jobs: # Build the Python Wheel and the source distribution. - name: Package release artifacts - run: python setup.py bdist_wheel sdist + run: python -m build # Attach the packaged artifacts to the workflow output. These can be manually # downloaded for later inspection if necessary. diff --git a/integration/test_storage.py b/integration/test_storage.py index 729190950..4f0faf76c 100644 --- a/integration/test_storage.py +++ b/integration/test_storage.py @@ -38,7 +38,7 @@ def _verify_bucket(bucket, expected_name): blob.upload_from_string('Hello World') blob = bucket.get_blob(file_name) - assert blob.download_as_string().decode() == 'Hello World' + assert blob.download_as_bytes().decode() == 'Hello World' bucket.delete_blob(file_name) assert not bucket.get_blob(file_name) From c2f2f07e4f2a6a8544dbf1517fd59d3762ee40a2 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Fri, 13 Jun 2025 16:54:53 -0400 Subject: [PATCH 3/8] fix GHA build status svg --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 146291fa7..29303fd4f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/firebase/firebase-admin-python.svg?branch=master)](https://travis-ci.org/firebase/firebase-admin-python) +[![Nightly Builds](https://github.com/firebase/firebase-admin-python/actions/workflows/nightly.yml/badge.svg)](https://github.com/firebase/firebase-admin-python/actions/workflows/nightly.yml) [![Python](https://img.shields.io/pypi/pyversions/firebase-admin.svg)](https://pypi.org/project/firebase-admin/) [![Version](https://img.shields.io/pypi/v/firebase-admin.svg)](https://pypi.org/project/firebase-admin/) From 01af3d2521584577b4744a9e296fd9565470444b Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Mon, 16 Jun 2025 17:00:52 -0400 Subject: [PATCH 4/8] fix: Correctly scope async eventloop --- integration/test_firestore_async.py | 8 ++++---- integration/test_messaging.py | 6 +++--- setup.cfg | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/integration/test_firestore_async.py b/integration/test_firestore_async.py index 8b73dda0f..584ef590a 100644 --- a/integration/test_firestore_async.py +++ b/integration/test_firestore_async.py @@ -34,7 +34,7 @@ } -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_firestore_async(): client = firestore_async.client() expected = _CITY @@ -48,7 +48,7 @@ async def test_firestore_async(): data = await doc.get() assert data.exists is False -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_firestore_async_explicit_database_id(): client = firestore_async.client(database_id='testing-database') expected = _CITY @@ -62,7 +62,7 @@ async def test_firestore_async_explicit_database_id(): data = await doc.get() assert data.exists is False -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_firestore_async_multi_db(): city_client = firestore_async.client() movie_client = firestore_async.client(database_id='testing-database') @@ -98,7 +98,7 @@ async def test_firestore_async_multi_db(): assert data[0].exists is False assert data[1].exists is False -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_server_timestamp(): client = firestore_async.client() expected = { diff --git a/integration/test_messaging.py b/integration/test_messaging.py index 804691962..7ab707c82 100644 --- a/integration/test_messaging.py +++ b/integration/test_messaging.py @@ -157,7 +157,7 @@ def test_unsubscribe(): resp = messaging.unsubscribe_from_topic(_REGISTRATION_TOKEN, 'mock-topic') assert resp.success_count + resp.failure_count == 1 -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_send_each_async(): messages = [ messaging.Message( @@ -189,7 +189,7 @@ async def test_send_each_async(): assert isinstance(response.exception, exceptions.InvalidArgumentError) assert response.message_id is None -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_send_each_async_500(): messages = [] for msg_number in range(500): @@ -206,7 +206,7 @@ async def test_send_each_async_500(): assert response.exception is None assert re.match('^projects/.*/messages/.*$', response.message_id) -@pytest.mark.asyncio +@pytest.mark.asyncio(loop_scope="session") async def test_send_each_for_multicast_async(): multicast = messaging.MulticastMessage( notification=messaging.Notification('Title', 'Body'), diff --git a/setup.cfg b/setup.cfg index 2fff03bfd..32e00676b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ [tool:pytest] testpaths = tests -asyncio_default_test_loop_scope = session +asyncio_default_test_loop_scope = class asyncio_default_fixture_loop_scope = None From 4c219f40d15f9520f84ee0b010b91be4d5fe4005 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Mon, 16 Jun 2025 17:09:45 -0400 Subject: [PATCH 5/8] fix: Bump pylint to v2.7.4 and astroid to v2.5.8 to fix lint issues --- firebase_admin/__init__.py | 5 +++-- firebase_admin/_auth_providers.py | 6 +++--- firebase_admin/_auth_utils.py | 16 ++++++++-------- firebase_admin/_sseclient.py | 2 +- firebase_admin/_token_gen.py | 6 +++--- firebase_admin/_user_import.py | 6 +++--- firebase_admin/_user_mgt.py | 16 ++++++++-------- firebase_admin/app_check.py | 20 ++++++++++---------- firebase_admin/credentials.py | 10 +++++----- firebase_admin/db.py | 2 +- firebase_admin/messaging.py | 5 ++++- firebase_admin/ml.py | 6 +++--- firebase_admin/project_management.py | 8 ++++---- firebase_admin/storage.py | 4 ++-- firebase_admin/tenant_mgt.py | 6 +++--- requirements.txt | 4 ++-- tests/test_db.py | 2 +- tests/test_messaging.py | 23 ++++++++++++----------- tests/test_ml.py | 6 +++--- tests/test_remote_config.py | 2 +- tests/test_sseclient.py | 4 ++-- tests/test_tenant_mgt.py | 6 +++--- tests/testutils.py | 2 +- 23 files changed, 86 insertions(+), 81 deletions(-) diff --git a/firebase_admin/__init__.py b/firebase_admin/__init__.py index 7bb9c59c2..597aaa6b6 100644 --- a/firebase_admin/__init__.py +++ b/firebase_admin/__init__.py @@ -178,11 +178,12 @@ def _load_from_environment(self): with open(config_file, 'r') as json_file: json_str = json_file.read() except Exception as err: - raise ValueError('Unable to read file {}. {}'.format(config_file, err)) + raise ValueError('Unable to read file {}. {}'.format(config_file, err)) from err try: json_data = json.loads(json_str) except Exception as err: - raise ValueError('JSON string "{0}" is not valid json. {1}'.format(json_str, err)) + raise ValueError( + 'JSON string "{0}" is not valid json. {1}'.format(json_str, err)) from err return {k: v for k, v in json_data.items() if k in _CONFIG_VALID_KEYS} diff --git a/firebase_admin/_auth_providers.py b/firebase_admin/_auth_providers.py index 31894a4dc..6512a4f7b 100644 --- a/firebase_admin/_auth_providers.py +++ b/firebase_admin/_auth_providers.py @@ -422,13 +422,13 @@ def _validate_url(url, label): if not parsed.netloc: raise ValueError('Malformed {0}: "{1}".'.format(label, url)) return url - except Exception: - raise ValueError('Malformed {0}: "{1}".'.format(label, url)) + except Exception as exception: + raise ValueError('Malformed {0}: "{1}".'.format(label, url)) from exception def _validate_x509_certificates(x509_certificates): if not isinstance(x509_certificates, list) or not x509_certificates: raise ValueError('x509_certificates must be a non-empty list.') - if not all([isinstance(cert, str) and cert for cert in x509_certificates]): + if not all(isinstance(cert, str) and cert for cert in x509_certificates): raise ValueError('x509_certificates must only contain non-empty strings.') return [{'x509Certificate': cert} for cert in x509_certificates] diff --git a/firebase_admin/_auth_utils.py b/firebase_admin/_auth_utils.py index ac7b322ff..0d56ca7fa 100644 --- a/firebase_admin/_auth_utils.py +++ b/firebase_admin/_auth_utils.py @@ -175,8 +175,8 @@ def validate_photo_url(photo_url, required=False): if not parsed.netloc: raise ValueError('Malformed photo URL: "{0}".'.format(photo_url)) return photo_url - except Exception: - raise ValueError('Malformed photo URL: "{0}".'.format(photo_url)) + except Exception as err: + raise ValueError('Malformed photo URL: "{0}".'.format(photo_url)) from err def validate_timestamp(timestamp, label, required=False): """Validates the given timestamp value. Timestamps must be positive integers.""" @@ -186,8 +186,8 @@ def validate_timestamp(timestamp, label, required=False): raise ValueError('Boolean value specified as timestamp.') try: timestamp_int = int(timestamp) - except TypeError: - raise ValueError('Invalid type for timestamp value: {0}.'.format(timestamp)) + except TypeError as err: + raise ValueError('Invalid type for timestamp value: {0}.'.format(timestamp)) from err else: if timestamp_int != timestamp: raise ValueError('{0} must be a numeric value and a whole number.'.format(label)) @@ -207,8 +207,8 @@ def validate_int(value, label, low=None, high=None): raise ValueError('Invalid type for integer value: {0}.'.format(value)) try: val_int = int(value) - except TypeError: - raise ValueError('Invalid type for integer value: {0}.'.format(value)) + except TypeError as err: + raise ValueError('Invalid type for integer value: {0}.'.format(value)) from err else: if val_int != value: # This will be True for non-numeric values like '2' and non-whole numbers like 2.5. @@ -246,8 +246,8 @@ def validate_custom_claims(custom_claims, required=False): MAX_CLAIMS_PAYLOAD_SIZE)) try: parsed = json.loads(claims_str) - except Exception: - raise ValueError('Failed to parse custom claims string as JSON.') + except Exception as err: + raise ValueError('Failed to parse custom claims string as JSON.') from err if not isinstance(parsed, dict): raise ValueError('Custom claims must be parseable as a JSON object.') diff --git a/firebase_admin/_sseclient.py b/firebase_admin/_sseclient.py index 6585dfc80..ec20cb45c 100644 --- a/firebase_admin/_sseclient.py +++ b/firebase_admin/_sseclient.py @@ -34,7 +34,7 @@ class KeepAuthSession(transport.requests.AuthorizedSession): """A session that does not drop authentication on redirects between domains.""" def __init__(self, credential): - super(KeepAuthSession, self).__init__(credential) + super().__init__(credential) def rebuild_auth(self, prepared_request, response): pass diff --git a/firebase_admin/_token_gen.py b/firebase_admin/_token_gen.py index a2fc725e8..6d82bf7a6 100644 --- a/firebase_admin/_token_gen.py +++ b/firebase_admin/_token_gen.py @@ -158,7 +158,7 @@ def signing_provider(self): 'Failed to determine service account: {0}. Make sure to initialize the SDK ' 'with service account credentials or specify a service account ID with ' 'iam.serviceAccounts.signBlob permission. Please refer to {1} for more ' - 'details on creating custom tokens.'.format(error, url)) + 'details on creating custom tokens.'.format(error, url)) from error return self._signing_provider def create_custom_token(self, uid, developer_claims=None, tenant_id=None): @@ -203,7 +203,7 @@ def create_custom_token(self, uid, developer_claims=None, tenant_id=None): return jwt.encode(signing_provider.signer, payload, header=header) except google.auth.exceptions.TransportError as error: msg = 'Failed to sign custom token. {0}'.format(error) - raise TokenSignError(msg, error) + raise TokenSignError(msg, error) from error def create_session_cookie(self, id_token, expires_in): @@ -403,7 +403,7 @@ def verify(self, token, request, clock_skew_seconds=0): verified_claims['uid'] = verified_claims['sub'] return verified_claims except google.auth.exceptions.TransportError as error: - raise CertificateFetchError(str(error), cause=error) + raise CertificateFetchError(str(error), cause=error) from error except ValueError as error: if 'Token expired' in str(error): raise self._expired_token_error(str(error), cause=error) diff --git a/firebase_admin/_user_import.py b/firebase_admin/_user_import.py index 659a68701..7c7a9e70b 100644 --- a/firebase_admin/_user_import.py +++ b/firebase_admin/_user_import.py @@ -216,10 +216,10 @@ def provider_data(self): def provider_data(self, provider_data): if provider_data is not None: try: - if any([not isinstance(p, UserProvider) for p in provider_data]): + if any(not isinstance(p, UserProvider) for p in provider_data): raise ValueError('One or more provider data instances are invalid.') - except TypeError: - raise ValueError('provider_data must be iterable.') + except TypeError as err: + raise ValueError('provider_data must be iterable.') from err self._provider_data = provider_data @property diff --git a/firebase_admin/_user_mgt.py b/firebase_admin/_user_mgt.py index aa0dfb0a4..957b749a6 100644 --- a/firebase_admin/_user_mgt.py +++ b/firebase_admin/_user_mgt.py @@ -128,7 +128,7 @@ class UserRecord(UserInfo): """Contains metadata associated with a Firebase user account.""" def __init__(self, data): - super(UserRecord, self).__init__() + super().__init__() if not isinstance(data, dict): raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(data)) if not data.get('localId'): @@ -452,7 +452,7 @@ class ProviderUserInfo(UserInfo): """Contains metadata regarding how a user is known by a particular identity provider.""" def __init__(self, data): - super(ProviderUserInfo, self).__init__() + super().__init__() if not isinstance(data, dict): raise ValueError('Invalid data argument: {0}. Must be a dictionary.'.format(data)) if not data.get('rawId'): @@ -518,8 +518,8 @@ def encode_action_code_settings(settings): if not parsed.netloc: raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) parameters['continueUrl'] = settings.url - except Exception: - raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) + except Exception as err: + raise ValueError('Malformed dynamic action links url: "{0}".'.format(settings.url)) from err # handle_code_in_app if settings.handle_code_in_app is not None: @@ -788,13 +788,13 @@ def import_users(self, users, hash_alg=None): raise ValueError( 'Users must be a non-empty list with no more than {0} elements.'.format( MAX_IMPORT_USERS_SIZE)) - if any([not isinstance(u, _user_import.ImportUserRecord) for u in users]): + if any(not isinstance(u, _user_import.ImportUserRecord) for u in users): raise ValueError('One or more user objects are invalid.') - except TypeError: - raise ValueError('users must be iterable') + except TypeError as err: + raise ValueError('users must be iterable') from err payload = {'users': [u.to_dict() for u in users]} - if any(['passwordHash' in u for u in payload['users']]): + if any('passwordHash' in u for u in payload['users']): if not isinstance(hash_alg, _user_import.UserImportHash): raise ValueError('A UserImportHash is required to import users with passwords.') payload.update(hash_alg.to_dict()) diff --git a/firebase_admin/app_check.py b/firebase_admin/app_check.py index 53686db3d..1224f7d80 100644 --- a/firebase_admin/app_check.py +++ b/firebase_admin/app_check.py @@ -84,7 +84,7 @@ def verify_token(self, token: str) -> Dict[str, Any]: except (InvalidTokenError, DecodeError) as exception: raise ValueError( f'Verifying App Check token failed. Error: {exception}' - ) + ) from exception verified_claims['app_id'] = verified_claims.get('sub') return verified_claims @@ -112,28 +112,28 @@ def _decode_and_verify(self, token: str, signing_key: str): algorithms=["RS256"], audience=self._scoped_project_id ) - except InvalidSignatureError: + except InvalidSignatureError as exception: raise ValueError( 'The provided App Check token has an invalid signature.' - ) - except InvalidAudienceError: + ) from exception + except InvalidAudienceError as exception: raise ValueError( 'The provided App Check token has an incorrect "aud" (audience) claim. ' f'Expected payload to include {self._scoped_project_id}.' - ) - except InvalidIssuerError: + ) from exception + except InvalidIssuerError as exception: raise ValueError( 'The provided App Check token has an incorrect "iss" (issuer) claim. ' f'Expected claim to include {self._APP_CHECK_ISSUER}' - ) - except ExpiredSignatureError: + ) from exception + except ExpiredSignatureError as exception: raise ValueError( 'The provided App Check token has expired.' - ) + ) from exception except InvalidTokenError as exception: raise ValueError( f'Decoding App Check token failed. Error: {exception}' - ) + ) from exception audience = payload.get('aud') if not isinstance(audience, list) or self._scoped_project_id not in audience: diff --git a/firebase_admin/credentials.py b/firebase_admin/credentials.py index 750600280..8259c93b4 100644 --- a/firebase_admin/credentials.py +++ b/firebase_admin/credentials.py @@ -63,7 +63,7 @@ class _ExternalCredentials(Base): """A wrapper for google.auth.credentials.Credentials typed credential instances""" def __init__(self, credential: GoogleAuthCredentials): - super(_ExternalCredentials, self).__init__() + super().__init__() self._g_credential = credential def get_credential(self): @@ -92,7 +92,7 @@ def __init__(self, cert): IOError: If the specified certificate file doesn't exist or cannot be read. ValueError: If the specified certificate is invalid. """ - super(Certificate, self).__init__() + super().__init__() if _is_file_path(cert): with open(cert) as json_file: json_data = json.load(json_file) @@ -111,7 +111,7 @@ def __init__(self, cert): json_data, scopes=_scopes) except ValueError as error: raise ValueError('Failed to initialize a certificate credential. ' - 'Caused by: "{0}"'.format(error)) + 'Caused by: "{0}"'.format(error)) from error @property def project_id(self): @@ -142,7 +142,7 @@ def __init__(self): The credentials will be lazily initialized when get_credential() or project_id() is called. See those methods for possible errors raised. """ - super(ApplicationDefault, self).__init__() + super().__init__() self._g_credential = None # Will be lazily-loaded via _load_credential(). def get_credential(self): @@ -193,7 +193,7 @@ def __init__(self, refresh_token): IOError: If the specified file doesn't exist or cannot be read. ValueError: If the refresh token configuration is invalid. """ - super(RefreshToken, self).__init__() + super().__init__() if _is_file_path(refresh_token): with open(refresh_token) as json_file: json_data = json.load(json_file) diff --git a/firebase_admin/db.py b/firebase_admin/db.py index 1dec98653..fc69cbd83 100644 --- a/firebase_admin/db.py +++ b/firebase_admin/db.py @@ -926,7 +926,7 @@ def request(self, method, url, **kwargs): kwargs['params'] = query try: - return super(_Client, self).request(method, url, **kwargs) + return super().request(method, url, **kwargs) except requests.exceptions.RequestException as error: raise _Client.handle_rtdb_error(error) diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 0e3a55f49..5b2e48e80 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -451,7 +451,7 @@ def send_data(data): message_data = [self._message_data(message, dry_run) for message in messages] try: with concurrent.futures.ThreadPoolExecutor(max_workers=len(message_data)) as executor: - responses = [resp for resp in executor.map(send_data, message_data)] + responses = list(executor.map(send_data, message_data)) return BatchResponse(responses) except Exception as error: raise exceptions.UnknownError( @@ -573,6 +573,7 @@ def _build_fcm_error_requests(cls, error, message, error_dict): """Parses an error response from the FCM API and creates a FCM-specific exception if appropriate.""" exc_type = cls._build_fcm_error(error_dict) + # pylint: disable=not-callable return exc_type(message, cause=error, http_response=error.response) if exc_type else None @classmethod @@ -586,8 +587,10 @@ def _build_fcm_error_httpx( appropriate.""" exc_type = cls._build_fcm_error(error_dict) if isinstance(error, httpx.HTTPStatusError): + # pylint: disable=not-callable return exc_type( message, cause=error, http_response=error.response) if exc_type else None + # pylint: disable=not-callable return exc_type(message, cause=error) if exc_type else None @classmethod diff --git a/firebase_admin/ml.py b/firebase_admin/ml.py index 98bdbb56a..27935ca59 100644 --- a/firebase_admin/ml.py +++ b/firebase_admin/ml.py @@ -721,7 +721,7 @@ def __init__(self, current_page): self._current_page = current_page self._index = 0 - def next(self): + def __next__(self): if self._index == len(self._current_page.models): if self._current_page.has_next_page: self._current_page = self._current_page.get_next_page() @@ -732,8 +732,8 @@ def next(self): return result raise StopIteration - def __next__(self): - return self.next() + # def __next__(self): + # return self.next() def __iter__(self): return self diff --git a/firebase_admin/project_management.py b/firebase_admin/project_management.py index ed292b80f..9405c8318 100644 --- a/firebase_admin/project_management.py +++ b/firebase_admin/project_management.py @@ -338,7 +338,7 @@ class AndroidAppMetadata(_AppMetadata): def __init__(self, package_name, name, app_id, display_name, project_id): """Clients should not instantiate this class directly.""" - super(AndroidAppMetadata, self).__init__(name, app_id, display_name, project_id) + super().__init__(name, app_id, display_name, project_id) self._package_name = _check_is_nonempty_string(package_name, 'package_name') @property @@ -347,7 +347,7 @@ def package_name(self): return self._package_name def __eq__(self, other): - return (super(AndroidAppMetadata, self).__eq__(other) and + return (super().__eq__(other) and self.package_name == other.package_name) def __ne__(self, other): @@ -363,7 +363,7 @@ class IOSAppMetadata(_AppMetadata): def __init__(self, bundle_id, name, app_id, display_name, project_id): """Clients should not instantiate this class directly.""" - super(IOSAppMetadata, self).__init__(name, app_id, display_name, project_id) + super().__init__(name, app_id, display_name, project_id) self._bundle_id = _check_is_nonempty_string(bundle_id, 'bundle_id') @property @@ -372,7 +372,7 @@ def bundle_id(self): return self._bundle_id def __eq__(self, other): - return super(IOSAppMetadata, self).__eq__(other) and self.bundle_id == other.bundle_id + return super().__eq__(other) and self.bundle_id == other.bundle_id def __ne__(self, other): return not self.__eq__(other) diff --git a/firebase_admin/storage.py b/firebase_admin/storage.py index b6084842a..567a6abad 100644 --- a/firebase_admin/storage.py +++ b/firebase_admin/storage.py @@ -21,9 +21,9 @@ # pylint: disable=import-error,no-name-in-module try: from google.cloud import storage -except ImportError: +except ImportError as exception: raise ImportError('Failed to import the Cloud Storage library for Python. Make sure ' - 'to install the "google-cloud-storage" module.') + 'to install the "google-cloud-storage" module.') from exception from firebase_admin import _utils diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index 8c53e30a1..a4213c114 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -417,7 +417,7 @@ def __init__(self, current_page): self._current_page = current_page self._index = 0 - def next(self): + def __next__(self): if self._index == len(self._current_page.tenants): if self._current_page.has_next_page: self._current_page = self._current_page.get_next_page() @@ -428,8 +428,8 @@ def next(self): return result raise StopIteration - def __next__(self): - return self.next() + # def __next__(self): + # return self.next() def __iter__(self): return self diff --git a/requirements.txt b/requirements.txt index 3d5b68166..76eeb7582 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -astroid == 2.3.3 -pylint == 2.3.1 +astroid == 2.5.8 +pylint == 2.7.4 pytest >= 8.2.2 pytest-cov >= 2.4.0 pytest-localserver >= 0.4.1 diff --git a/tests/test_db.py b/tests/test_db.py index 00a0077cb..93f4672f1 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -45,7 +45,7 @@ def __init__(self, data, status, recorder, etag=ETAG): def send(self, request, **kwargs): if_match = request.headers.get('if-match') if_none_match = request.headers.get('if-none-match') - resp = super(MockAdapter, self).send(request, **kwargs) + resp = super().send(request, **kwargs) resp.headers = {'ETag': self._etag} if if_match and if_match != MockAdapter.ETAG: resp.status_code = 412 diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 341fd9e07..63b649485 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -1881,8 +1881,8 @@ def test_send_each(self): assert batch_response.failure_count == 0 assert len(batch_response.responses) == 2 assert [r.message_id for r in batch_response.responses] == ['message-id1', 'message-id2'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) @respx.mock @pytest.mark.asyncio @@ -1907,8 +1907,8 @@ async def test_send_each_async(self): assert len(batch_response.responses) == 3 assert [r.message_id for r in batch_response.responses] \ == ['message-id1', 'message-id2', 'message-id3'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) assert route.call_count == 3 @@ -1976,8 +1976,8 @@ async def test_send_each_async_error_401_pass_on_auth_retry(self): assert batch_response.failure_count == 0 assert len(batch_response.responses) == 1 assert [r.message_id for r in batch_response.responses] == ['message-id1'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) @respx.mock @pytest.mark.asyncio @@ -2049,11 +2049,12 @@ async def test_send_each_async_error_500_pass_on_retry_config(self): assert batch_response.failure_count == 0 assert len(batch_response.responses) == 1 assert [r.message_id for r in batch_response.responses] == ['message-id1'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) + - @respx.mock @pytest.mark.asyncio + @respx.mock async def test_send_each_async_request_error(self): responses = httpx.ConnectError("Test request error", request=httpx.Request( 'POST', @@ -2192,8 +2193,8 @@ def test_send_each_for_multicast(self): assert batch_response.failure_count == 0 assert len(batch_response.responses) == 2 assert [r.message_id for r in batch_response.responses] == ['message-id1', 'message-id2'] - assert all([r.success for r in batch_response.responses]) - assert not any([r.exception for r in batch_response.responses]) + assert all(r.success for r in batch_response.responses) + assert not any(r.exception for r in batch_response.responses) @pytest.mark.parametrize('status', HTTP_ERROR_CODES) def test_send_each_for_multicast_detailed_error(self, status): diff --git a/tests/test_ml.py b/tests/test_ml.py index 18a9e2754..4aebdcab6 100644 --- a/tests/test_ml.py +++ b/tests/test_ml.py @@ -1094,7 +1094,7 @@ def test_list_single_page(self): assert models_page.next_page_token == '' assert models_page.has_next_page is False assert models_page.get_next_page() is None - models = [model for model in models_page.iterate_all()] + models = list(models_page.iterate_all()) assert len(models) == 1 def test_list_multiple_pages(self): @@ -1140,7 +1140,7 @@ def test_list_models_stop_iteration(self): assert len(recorder) == 1 assert len(page.models) == 3 iterator = page.iterate_all() - models = [model for model in iterator] + models = list(iterator) assert len(page.models) == 3 with pytest.raises(StopIteration): next(iterator) @@ -1151,5 +1151,5 @@ def test_list_models_no_models(self): page = ml.list_models() assert len(recorder) == 1 assert len(page.models) == 0 - models = [model for model in page.iterate_all()] + models = list(page.iterate_all()) assert len(models) == 0 diff --git a/tests/test_remote_config.py b/tests/test_remote_config.py index 8c6248e18..14b54838f 100644 --- a/tests/test_remote_config.py +++ b/tests/test_remote_config.py @@ -830,7 +830,7 @@ def __init__(self, data, status, recorder, etag=ETAG): self._etag = etag def send(self, request, **kwargs): - resp = super(MockAdapter, self).send(request, **kwargs) + resp = super().send(request, **kwargs) resp.headers = {'etag': self._etag} return resp diff --git a/tests/test_sseclient.py b/tests/test_sseclient.py index 70edcf0d0..2c523e36f 100644 --- a/tests/test_sseclient.py +++ b/tests/test_sseclient.py @@ -25,10 +25,10 @@ class MockSSEClientAdapter(testutils.MockAdapter): def __init__(self, payload, recorder): - super(MockSSEClientAdapter, self).__init__(payload, 200, recorder) + super().__init__(payload, 200, recorder) def send(self, request, **kwargs): - resp = super(MockSSEClientAdapter, self).send(request, **kwargs) + resp = super().send(request, **kwargs) resp.url = request.url resp.status_code = self.status resp.raw = io.BytesIO(self.data.encode()) diff --git a/tests/test_tenant_mgt.py b/tests/test_tenant_mgt.py index 018892e3a..156846343 100644 --- a/tests/test_tenant_mgt.py +++ b/tests/test_tenant_mgt.py @@ -450,7 +450,7 @@ def test_list_single_page(self, tenant_mgt_app): assert page.next_page_token == '' assert page.has_next_page is False assert page.get_next_page() is None - tenants = [tenant for tenant in page.iterate_all()] + tenants = list(page.iterate_all()) assert len(tenants) == 2 self._assert_request(recorder) @@ -514,7 +514,7 @@ def test_list_tenants_stop_iteration(self, tenant_mgt_app): _, recorder = _instrument_tenant_mgt(tenant_mgt_app, 200, LIST_TENANTS_RESPONSE) page = tenant_mgt.list_tenants(app=tenant_mgt_app) iterator = page.iterate_all() - tenants = [tenant for tenant in iterator] + tenants = list(iterator) assert len(tenants) == 2 with pytest.raises(StopIteration): @@ -526,7 +526,7 @@ def test_list_tenants_no_tenants_response(self, tenant_mgt_app): _instrument_tenant_mgt(tenant_mgt_app, 200, json.dumps(response)) page = tenant_mgt.list_tenants(app=tenant_mgt_app) assert len(page.tenants) == 0 - tenants = [tenant for tenant in page.iterate_all()] + tenants = list(page.iterate_all()) assert len(tenants) == 0 def test_list_tenants_with_max_results(self, tenant_mgt_app): diff --git a/tests/testutils.py b/tests/testutils.py index 62f7bd9b5..0505eb6c7 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -183,7 +183,7 @@ def send(self, request, **kwargs): # pylint: disable=arguments-differ class MockAdapter(MockMultiRequestAdapter): """A mock HTTP adapter for the Python requests module.""" def __init__(self, data, status, recorder): - super(MockAdapter, self).__init__([data], [status], recorder) + super().__init__([data], [status], recorder) @property def status(self): From ecbeae235c08e6874a6167972aeed233301b500c Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Mon, 16 Jun 2025 17:53:40 -0400 Subject: [PATCH 6/8] fix ml tests --- integration/test_ml.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/integration/test_ml.py b/integration/test_ml.py index 52cb1bb7e..fd48ccb7e 100644 --- a/integration/test_ml.py +++ b/integration/test_ml.py @@ -317,12 +317,16 @@ def _clean_up_directory(save_dir): @pytest.fixture def keras_model(): assert _TF_ENABLED - x_array = [-1, 0, 1, 2, 3, 4] - y_array = [-3, -1, 1, 3, 5, 7] - model = tf.keras.models.Sequential( - [tf.keras.layers.Dense(units=1, input_shape=[1])]) + x_list = [-1, 0, 1, 2, 3, 4] + y_list = [-3, -1, 1, 3, 5, 7] + x_tensor = tf.convert_to_tensor(x_list, dtype=tf.float32) + y_tensor = tf.convert_to_tensor(y_list, dtype=tf.float32) + model = tf.keras.models.Sequential([ + tf.keras.Input(shape=(1,)), + tf.keras.layers.Dense(units=1) + ]) model.compile(optimizer='sgd', loss='mean_squared_error') - model.fit(x_array, y_array, epochs=3) + model.fit(x_tensor, y_tensor, epochs=3) return model From 3e5931c677b105f5e914052991e84ea0a1b26807 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Mon, 16 Jun 2025 17:56:17 -0400 Subject: [PATCH 7/8] fix lint --- integration/test_ml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test_ml.py b/integration/test_ml.py index fd48ccb7e..f8dd6bb47 100644 --- a/integration/test_ml.py +++ b/integration/test_ml.py @@ -322,7 +322,7 @@ def keras_model(): x_tensor = tf.convert_to_tensor(x_list, dtype=tf.float32) y_tensor = tf.convert_to_tensor(y_list, dtype=tf.float32) model = tf.keras.models.Sequential([ - tf.keras.Input(shape=(1,)), + tf.keras.Input(shape=(1,)), tf.keras.layers.Dense(units=1) ]) model.compile(optimizer='sgd', loss='mean_squared_error') From f9f281cf8fb9166e1006d6ca99c2d5415a022882 Mon Sep 17 00:00:00 2001 From: jonathanedey Date: Tue, 17 Jun 2025 10:19:15 -0400 Subject: [PATCH 8/8] fix: remove commented code --- firebase_admin/ml.py | 3 --- firebase_admin/tenant_mgt.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/firebase_admin/ml.py b/firebase_admin/ml.py index 27935ca59..8cedc8482 100644 --- a/firebase_admin/ml.py +++ b/firebase_admin/ml.py @@ -732,9 +732,6 @@ def __next__(self): return result raise StopIteration - # def __next__(self): - # return self.next() - def __iter__(self): return self diff --git a/firebase_admin/tenant_mgt.py b/firebase_admin/tenant_mgt.py index a4213c114..133e80b45 100644 --- a/firebase_admin/tenant_mgt.py +++ b/firebase_admin/tenant_mgt.py @@ -428,9 +428,6 @@ def __next__(self): return result raise StopIteration - # def __next__(self): - # return self.next() - def __iter__(self): return self