diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4cc8ec48..bfd29e2c 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 282cb1b9..3d542053 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: | @@ -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 7a7986a5..6cd1d3f0 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: | @@ -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/CONTRIBUTING.md b/CONTRIBUTING.md index de593486..72933a24 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 6e3ed680..29303fd4 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/) @@ -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/firebase_admin/__init__.py b/firebase_admin/__init__.py index 7bb9c59c..597aaa6b 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 31894a4d..6512a4f7 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 ac7b322f..0d56ca7f 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 6585dfc8..ec20cb45 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 a2fc725e..6d82bf7a 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 659a6870..7c7a9e70 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 aa0dfb0a..957b749a 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 53686db3..1224f7d8 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 75060028..8259c93b 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 1dec9865..fc69cbd8 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 0e3a55f4..5b2e48e8 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 98bdbb56..8cedc848 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,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/project_management.py b/firebase_admin/project_management.py index ed292b80..9405c831 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 b6084842..567a6aba 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 8c53e30a..133e80b4 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,9 +428,6 @@ def next(self): return result raise StopIteration - def __next__(self): - return self.next() - def __iter__(self): return self diff --git a/integration/conftest.py b/integration/conftest.py index efa45932..169e02d5 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/integration/test_firestore_async.py b/integration/test_firestore_async.py index 8b73dda0..584ef590 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 80469196..7ab707c8 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/integration/test_ml.py b/integration/test_ml.py index 52cb1bb7..f8dd6bb4 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 diff --git a/integration/test_storage.py b/integration/test_storage.py index 72919095..4f0faf76 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) diff --git a/requirements.txt b/requirements.txt index b5642b54..76eeb758 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ -astroid == 2.3.3 -pylint == 2.3.1 -pytest >= 6.2.0 +astroid == 2.5.8 +pylint == 2.7.4 +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 25c64974..32e00676 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,4 @@ [tool:pytest] testpaths = tests +asyncio_default_test_loop_scope = class +asyncio_default_fixture_loop_scope = None diff --git a/setup.py b/setup.py index b9eb1180..25cf1267 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', ], ) diff --git a/tests/test_db.py b/tests/test_db.py index 00a0077c..93f4672f 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 341fd9e0..63b64948 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 18a9e275..4aebdcab 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 8c6248e1..14b54838 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 70edcf0d..2c523e36 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 018892e3..15684634 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 62f7bd9b..0505eb6c 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):