diff options
author | robot-piglet <robot-piglet@yandex-team.com> | 2024-08-22 10:43:37 +0300 |
---|---|---|
committer | robot-piglet <robot-piglet@yandex-team.com> | 2024-08-22 10:52:34 +0300 |
commit | 1fbd27b4e37aecbce5bc29b1084ebc08d49c44ab (patch) | |
tree | dc2e6502cd69163a7309a5a2b5ee7bc0f7b1d736 | |
parent | 09b7cd61fa6d98c03d6612f2130641e209f61a06 (diff) | |
download | ydb-1fbd27b4e37aecbce5bc29b1084ebc08d49c44ab.tar.gz |
Intermediate changes
30 files changed, 750 insertions, 158 deletions
diff --git a/contrib/python/google-auth/py3/.dist-info/METADATA b/contrib/python/google-auth/py3/.dist-info/METADATA index 1814862af6..cdbc683396 100644 --- a/contrib/python/google-auth/py3/.dist-info/METADATA +++ b/contrib/python/google-auth/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: google-auth -Version: 2.32.0 +Version: 2.33.0 Summary: Google Authentication Library Home-page: https://github.com/googleapis/google-auth-library-python Author: Google Cloud Platform diff --git a/contrib/python/google-auth/py3/google/auth/_credentials_base.py b/contrib/python/google-auth/py3/google/auth/_credentials_base.py new file mode 100644 index 0000000000..64d5ce34b9 --- /dev/null +++ b/contrib/python/google-auth/py3/google/auth/_credentials_base.py @@ -0,0 +1,75 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Interface for base credentials.""" + +import abc + +from google.auth import _helpers + + +class _BaseCredentials(metaclass=abc.ABCMeta): + """Base class for all credentials. + + All credentials have a :attr:`token` that is used for authentication and + may also optionally set an :attr:`expiry` to indicate when the token will + no longer be valid. + + Most credentials will be :attr:`invalid` until :meth:`refresh` is called. + Credentials can do this automatically before the first HTTP request in + :meth:`before_request`. + + Although the token and expiration will change as the credentials are + :meth:`refreshed <refresh>` and used, credentials should be considered + immutable. Various credentials will accept configuration such as private + keys, scopes, and other options. These options are not changeable after + construction. Some classes will provide mechanisms to copy the credentials + with modifications such as :meth:`ScopedCredentials.with_scopes`. + + Attributes: + token (Optional[str]): The bearer token that can be used in HTTP headers to make + authenticated requests. + """ + + def __init__(self): + self.token = None + + @abc.abstractmethod + def refresh(self, request): + """Refreshes the access token. + + Args: + request (google.auth.transport.Request): The object used to make + HTTP requests. + + Raises: + google.auth.exceptions.RefreshError: If the credentials could + not be refreshed. + """ + # pylint: disable=missing-raises-doc + # (pylint doesn't recognize that this is abstract) + raise NotImplementedError("Refresh must be implemented") + + def _apply(self, headers, token=None): + """Apply the token to the authentication header. + + Args: + headers (Mapping): The HTTP request headers. + token (Optional[str]): If specified, overrides the current access + token. + """ + headers["authorization"] = "Bearer {}".format( + _helpers.from_bytes(token or self.token) + ) diff --git a/contrib/python/google-auth/py3/google/auth/_exponential_backoff.py b/contrib/python/google-auth/py3/google/auth/_exponential_backoff.py index 0dd621a949..04f9f97641 100644 --- a/contrib/python/google-auth/py3/google/auth/_exponential_backoff.py +++ b/contrib/python/google-auth/py3/google/auth/_exponential_backoff.py @@ -15,6 +15,8 @@ import random import time +from google.auth import exceptions + # The default amount of retry attempts _DEFAULT_RETRY_TOTAL_ATTEMPTS = 3 @@ -68,6 +70,11 @@ class ExponentialBackoff: randomization_factor=_DEFAULT_RANDOMIZATION_FACTOR, multiplier=_DEFAULT_MULTIPLIER, ): + if total_attempts < 1: + raise exceptions.InvalidValue( + f"total_attempts must be greater than or equal to 1 but was {total_attempts}" + ) + self._total_attempts = total_attempts self._initial_wait_seconds = initial_wait_seconds @@ -87,6 +94,9 @@ class ExponentialBackoff: raise StopIteration self._backoff_count += 1 + if self._backoff_count <= 1: + return self._backoff_count + jitter_variance = self._current_wait_in_seconds * self._randomization_factor jitter = random.uniform( self._current_wait_in_seconds - jitter_variance, diff --git a/contrib/python/google-auth/py3/google/auth/aio/__init__.py b/contrib/python/google-auth/py3/google/auth/aio/__init__.py new file mode 100644 index 0000000000..331708cba6 --- /dev/null +++ b/contrib/python/google-auth/py3/google/auth/aio/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Google Auth AIO Library for Python.""" + +import logging + +from google.auth import version as google_auth_version + + +__version__ = google_auth_version.__version__ + +# Set default logging handler to avoid "No handler found" warnings. +logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/contrib/python/google-auth/py3/google/auth/aio/credentials.py b/contrib/python/google-auth/py3/google/auth/aio/credentials.py new file mode 100644 index 0000000000..3bc6a5a676 --- /dev/null +++ b/contrib/python/google-auth/py3/google/auth/aio/credentials.py @@ -0,0 +1,143 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Interfaces for asynchronous credentials.""" + + +from google.auth import _helpers +from google.auth import exceptions +from google.auth._credentials_base import _BaseCredentials + + +class Credentials(_BaseCredentials): + """Base class for all asynchronous credentials. + + All credentials have a :attr:`token` that is used for authentication and + may also optionally set an :attr:`expiry` to indicate when the token will + no longer be valid. + + Most credentials will be :attr:`invalid` until :meth:`refresh` is called. + Credentials can do this automatically before the first HTTP request in + :meth:`before_request`. + + Although the token and expiration will change as the credentials are + :meth:`refreshed <refresh>` and used, credentials should be considered + immutable. Various credentials will accept configuration such as private + keys, scopes, and other options. These options are not changeable after + construction. Some classes will provide mechanisms to copy the credentials + with modifications such as :meth:`ScopedCredentials.with_scopes`. + """ + + def __init__(self): + super(Credentials, self).__init__() + + async def apply(self, headers, token=None): + """Apply the token to the authentication header. + + Args: + headers (Mapping): The HTTP request headers. + token (Optional[str]): If specified, overrides the current access + token. + """ + self._apply(headers, token=token) + + async def refresh(self, request): + """Refreshes the access token. + + Args: + request (google.auth.aio.transport.Request): The object used to make + HTTP requests. + + Raises: + google.auth.exceptions.RefreshError: If the credentials could + not be refreshed. + """ + raise NotImplementedError("Refresh must be implemented") + + async def before_request(self, request, method, url, headers): + """Performs credential-specific before request logic. + + Refreshes the credentials if necessary, then calls :meth:`apply` to + apply the token to the authentication header. + + Args: + request (google.auth.aio.transport.Request): The object used to make + HTTP requests. + method (str): The request's HTTP method or the RPC method being + invoked. + url (str): The request's URI or the RPC service's URI. + headers (Mapping): The request's headers. + """ + await self.apply(headers) + + +class StaticCredentials(Credentials): + """Asynchronous Credentials representing an immutable access token. + + The credentials are considered immutable except the tokens which can be + configured in the constructor :: + + credentials = StaticCredentials(token="token123") + + StaticCredentials does not support :meth `refresh` and assumes that the configured + token is valid and not expired. StaticCredentials will never attempt to + refresh the token. + """ + + def __init__(self, token): + """ + Args: + token (str): The access token. + """ + super(StaticCredentials, self).__init__() + self.token = token + + @_helpers.copy_docstring(Credentials) + async def refresh(self, request): + raise exceptions.InvalidOperation("Static credentials cannot be refreshed.") + + # Note: before_request should never try to refresh access tokens. + # StaticCredentials intentionally does not support it. + @_helpers.copy_docstring(Credentials) + async def before_request(self, request, method, url, headers): + await self.apply(headers) + + +class AnonymousCredentials(Credentials): + """Asynchronous Credentials that do not provide any authentication information. + + These are useful in the case of services that support anonymous access or + local service emulators that do not use credentials. + """ + + async def refresh(self, request): + """Raises :class:``InvalidOperation``, anonymous credentials cannot be + refreshed.""" + raise exceptions.InvalidOperation("Anonymous credentials cannot be refreshed.") + + async def apply(self, headers, token=None): + """Anonymous credentials do nothing to the request. + + The optional ``token`` argument is not supported. + + Raises: + google.auth.exceptions.InvalidValue: If a token was specified. + """ + if token is not None: + raise exceptions.InvalidValue("Anonymous credentials don't support tokens.") + + async def before_request(self, request, method, url, headers): + """Anonymous credentials do nothing to the request.""" + pass diff --git a/contrib/python/google-auth/py3/google/auth/compute_engine/_metadata.py b/contrib/python/google-auth/py3/google/auth/compute_engine/_metadata.py index e597365851..69b7b52458 100644 --- a/contrib/python/google-auth/py3/google/auth/compute_engine/_metadata.py +++ b/contrib/python/google-auth/py3/google/auth/compute_engine/_metadata.py @@ -28,11 +28,12 @@ from google.auth import _helpers from google.auth import environment_vars from google.auth import exceptions from google.auth import metrics +from google.auth._exponential_backoff import ExponentialBackoff _LOGGER = logging.getLogger(__name__) # Environment variable GCE_METADATA_HOST is originally named -# GCE_METADATA_ROOT. For compatiblity reasons, here it checks +# GCE_METADATA_ROOT. For compatibility reasons, here it checks # the new variable first; if not set, the system falls back # to the old variable. _GCE_METADATA_HOST = os.getenv(environment_vars.GCE_METADATA_HOST, None) @@ -119,11 +120,12 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3): # could lead to false negatives in the event that we are on GCE, but # the metadata resolution was particularly slow. The latter case is # "unlikely". - retries = 0 headers = _METADATA_HEADERS.copy() headers[metrics.API_CLIENT_HEADER] = metrics.mds_ping() - while retries < retry_count: + backoff = ExponentialBackoff(total_attempts=retry_count) + + for attempt in backoff: try: response = request( url=_METADATA_IP_ROOT, method="GET", headers=headers, timeout=timeout @@ -139,11 +141,10 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3): _LOGGER.warning( "Compute Engine Metadata server unavailable on " "attempt %s of %s. Reason: %s", - retries + 1, + attempt, retry_count, e, ) - retries += 1 return False @@ -179,7 +180,7 @@ def get( Returns: Union[Mapping, str]: If the metadata server returns JSON, a mapping of - the decoded JSON is return. Otherwise, the response content is + the decoded JSON is returned. Otherwise, the response content is returned as a string. Raises: @@ -198,8 +199,9 @@ def get( url = _helpers.update_query(base_url, query_params) - retries = 0 - while retries < retry_count: + backoff = ExponentialBackoff(total_attempts=retry_count) + + for attempt in backoff: try: response = request(url=url, method="GET", headers=headers_to_use) break @@ -208,11 +210,10 @@ def get( _LOGGER.warning( "Compute Engine Metadata server unavailable on " "attempt %s of %s. Reason: %s", - retries + 1, + attempt, retry_count, e, ) - retries += 1 else: raise exceptions.TransportError( "Failed to retrieve {} from the Google Compute Engine " diff --git a/contrib/python/google-auth/py3/google/auth/credentials.py b/contrib/python/google-auth/py3/google/auth/credentials.py index 27abd443dc..e31930311b 100644 --- a/contrib/python/google-auth/py3/google/auth/credentials.py +++ b/contrib/python/google-auth/py3/google/auth/credentials.py @@ -22,12 +22,13 @@ import os from google.auth import _helpers, environment_vars from google.auth import exceptions from google.auth import metrics +from google.auth._credentials_base import _BaseCredentials from google.auth._refresh_worker import RefreshThreadManager DEFAULT_UNIVERSE_DOMAIN = "googleapis.com" -class Credentials(metaclass=abc.ABCMeta): +class Credentials(_BaseCredentials): """Base class for all credentials. All credentials have a :attr:`token` that is used for authentication and @@ -47,9 +48,8 @@ class Credentials(metaclass=abc.ABCMeta): """ def __init__(self): - self.token = None - """str: The bearer token that can be used in HTTP headers to make - authenticated requests.""" + super(Credentials, self).__init__() + self.expiry = None """Optional[datetime]: When the token expires and is no longer valid. If this is None, the token is assumed to never expire.""" @@ -167,9 +167,7 @@ class Credentials(metaclass=abc.ABCMeta): token (Optional[str]): If specified, overrides the current access token. """ - headers["authorization"] = "Bearer {}".format( - _helpers.from_bytes(token or self.token) - ) + self._apply(headers, token=token) """Trust boundary value will be a cached value from global lookup. The response of trust boundary will be a list of regions and a hex diff --git a/contrib/python/google-auth/py3/google/auth/transport/_requests_base.py b/contrib/python/google-auth/py3/google/auth/transport/_requests_base.py new file mode 100644 index 0000000000..ec718d909a --- /dev/null +++ b/contrib/python/google-auth/py3/google/auth/transport/_requests_base.py @@ -0,0 +1,52 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Transport adapter for Base Requests.""" + + +import abc + + +_DEFAULT_TIMEOUT = 120 # in second + + +class _BaseAuthorizedSession(metaclass=abc.ABCMeta): + """Base class for a Request Session with credentials. This class is intended to capture + the common logic between synchronous and asynchronous request sessions and is not intended to + be instantiated directly. + + Args: + credentials (google.auth._credentials_base.BaseCredentials): The credentials to + add to the request. + """ + + def __init__(self, credentials): + self.credentials = credentials + + @abc.abstractmethod + def request( + self, + method, + url, + data=None, + headers=None, + max_allowed_time=None, + timeout=_DEFAULT_TIMEOUT, + **kwargs + ): + raise NotImplementedError("Request must be implemented") + + @abc.abstractmethod + def close(self): + raise NotImplementedError("Close must be implemented") diff --git a/contrib/python/google-auth/py3/google/auth/transport/requests.py b/contrib/python/google-auth/py3/google/auth/transport/requests.py index 23a69783dc..68f67c59bd 100644 --- a/contrib/python/google-auth/py3/google/auth/transport/requests.py +++ b/contrib/python/google-auth/py3/google/auth/transport/requests.py @@ -38,6 +38,7 @@ from google.auth import environment_vars from google.auth import exceptions from google.auth import transport import google.auth.transport._mtls_helper +from google.auth.transport._requests_base import _BaseAuthorizedSession from google.oauth2 import service_account _LOGGER = logging.getLogger(__name__) @@ -292,7 +293,7 @@ class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter): return super(_MutualTlsOffloadAdapter, self).proxy_manager_for(*args, **kwargs) -class AuthorizedSession(requests.Session): +class AuthorizedSession(requests.Session, _BaseAuthorizedSession): """A Requests Session class with credentials. This class is used to perform requests to API endpoints that require @@ -389,7 +390,7 @@ class AuthorizedSession(requests.Session): default_host=None, ): super(AuthorizedSession, self).__init__() - self.credentials = credentials + _BaseAuthorizedSession.__init__(self, credentials) self._refresh_status_codes = refresh_status_codes self._max_refresh_attempts = max_refresh_attempts self._refresh_timeout = refresh_timeout diff --git a/contrib/python/google-auth/py3/google/auth/version.py b/contrib/python/google-auth/py3/google/auth/version.py index 51f7f62acd..c41f877658 100644 --- a/contrib/python/google-auth/py3/google/auth/version.py +++ b/contrib/python/google-auth/py3/google/auth/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.32.0" +__version__ = "2.33.0" diff --git a/contrib/python/google-auth/py3/google/oauth2/_client.py b/contrib/python/google-auth/py3/google/oauth2/_client.py index bce797b88b..68e13ddc73 100644 --- a/contrib/python/google-auth/py3/google/oauth2/_client.py +++ b/contrib/python/google-auth/py3/google/oauth2/_client.py @@ -183,7 +183,11 @@ def _token_endpoint_request_no_throw( if headers: headers_to_use.update(headers) - def _perform_request(): + response_data = {} + retryable_error = False + + retries = _exponential_backoff.ExponentialBackoff() + for _ in retries: response = request( method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs ) @@ -192,7 +196,7 @@ def _token_endpoint_request_no_throw( if hasattr(response.data, "decode") else response.data ) - response_data = "" + try: # response_body should be a JSON response_data = json.loads(response_body) @@ -206,18 +210,8 @@ def _token_endpoint_request_no_throw( status_code=response.status, response_data=response_data ) - return False, response_data, retryable_error - - request_succeeded, response_data, retryable_error = _perform_request() - - if request_succeeded or not retryable_error or not can_retry: - return request_succeeded, response_data, retryable_error - - retries = _exponential_backoff.ExponentialBackoff() - for _ in retries: - request_succeeded, response_data, retryable_error = _perform_request() - if request_succeeded or not retryable_error: - return request_succeeded, response_data, retryable_error + if not can_retry or not retryable_error: + return False, response_data, retryable_error return False, response_data, retryable_error diff --git a/contrib/python/google-auth/py3/google/oauth2/_client_async.py b/contrib/python/google-auth/py3/google/oauth2/_client_async.py index 2858d862b0..8867f0a527 100644 --- a/contrib/python/google-auth/py3/google/oauth2/_client_async.py +++ b/contrib/python/google-auth/py3/google/oauth2/_client_async.py @@ -67,7 +67,11 @@ async def _token_endpoint_request_no_throw( if access_token: headers["Authorization"] = "Bearer {}".format(access_token) - async def _perform_request(): + response_data = {} + retryable_error = False + + retries = _exponential_backoff.ExponentialBackoff() + for _ in retries: response = await request( method="POST", url=token_uri, headers=headers, body=body ) @@ -93,18 +97,8 @@ async def _token_endpoint_request_no_throw( status_code=response.status, response_data=response_data ) - return False, response_data, retryable_error - - request_succeeded, response_data, retryable_error = await _perform_request() - - if request_succeeded or not retryable_error or not can_retry: - return request_succeeded, response_data, retryable_error - - retries = _exponential_backoff.ExponentialBackoff() - for _ in retries: - request_succeeded, response_data, retryable_error = await _perform_request() - if request_succeeded or not retryable_error: - return request_succeeded, response_data, retryable_error + if not can_retry or not retryable_error: + return False, response_data, retryable_error return False, response_data, retryable_error diff --git a/contrib/python/google-auth/py3/tests/compute_engine/test__metadata.py b/contrib/python/google-auth/py3/tests/compute_engine/test__metadata.py index 35e3c089f9..352342f150 100644 --- a/contrib/python/google-auth/py3/tests/compute_engine/test__metadata.py +++ b/contrib/python/google-auth/py3/tests/compute_engine/test__metadata.py @@ -127,13 +127,15 @@ def test_ping_success_retry(mock_metrics_header_value): assert request.call_count == 2 -def test_ping_failure_bad_flavor(): +@mock.patch("time.sleep", return_value=None) +def test_ping_failure_bad_flavor(mock_sleep): request = make_request("", headers={_metadata._METADATA_FLAVOR_HEADER: "meep"}) assert not _metadata.ping(request) -def test_ping_failure_connection_failed(): +@mock.patch("time.sleep", return_value=None) +def test_ping_failure_connection_failed(mock_sleep): request = make_request("") request.side_effect = exceptions.TransportError() @@ -196,7 +198,8 @@ def test_get_success_json_content_type_charset(): assert result[key] == value -def test_get_success_retry(): +@mock.patch("time.sleep", return_value=None) +def test_get_success_retry(mock_sleep): key, value = "foo", "bar" data = json.dumps({key: value}) @@ -312,7 +315,8 @@ def _test_get_success_custom_root_old_variable(): ) -def test_get_failure(): +@mock.patch("time.sleep", return_value=None) +def test_get_failure(mock_sleep): request = make_request("Metadata error", status=http_client.NOT_FOUND) with pytest.raises(exceptions.TransportError) as excinfo: @@ -339,7 +343,8 @@ def test_get_return_none_for_not_found_error(): ) -def test_get_failure_connection_failed(): +@mock.patch("time.sleep", return_value=None) +def test_get_failure_connection_failed(mock_sleep): request = make_request("") request.side_effect = exceptions.TransportError() diff --git a/contrib/python/google-auth/py3/tests/oauth2/test__client.py b/contrib/python/google-auth/py3/tests/oauth2/test__client.py index f9a2d3aff4..8736a4e27b 100644 --- a/contrib/python/google-auth/py3/tests/oauth2/test__client.py +++ b/contrib/python/google-auth/py3/tests/oauth2/test__client.py @@ -195,8 +195,8 @@ def test__token_endpoint_request_internal_failure_error(): _client._token_endpoint_request( request, "http://example.com", {"error_description": "internal_failure"} ) - # request should be called once and then with 3 retries - assert request.call_count == 4 + # request with 2 retries + assert request.call_count == 3 request = make_request( {"error": "internal_failure"}, status=http_client.BAD_REQUEST @@ -206,8 +206,8 @@ def test__token_endpoint_request_internal_failure_error(): _client._token_endpoint_request( request, "http://example.com", {"error": "internal_failure"} ) - # request should be called once and then with 3 retries - assert request.call_count == 4 + # request with 2 retries + assert request.call_count == 3 def test__token_endpoint_request_internal_failure_and_retry_failure_error(): @@ -626,6 +626,6 @@ def test__token_endpoint_request_no_throw_with_retry(can_retry): ) if can_retry: - assert mock_request.call_count == 4 + assert mock_request.call_count == 3 else: assert mock_request.call_count == 1 diff --git a/contrib/python/google-auth/py3/tests/test__exponential_backoff.py b/contrib/python/google-auth/py3/tests/test__exponential_backoff.py index 06a54527e6..95422502b0 100644 --- a/contrib/python/google-auth/py3/tests/test__exponential_backoff.py +++ b/contrib/python/google-auth/py3/tests/test__exponential_backoff.py @@ -13,8 +13,10 @@ # limitations under the License. import mock +import pytest # type: ignore from google.auth import _exponential_backoff +from google.auth import exceptions @mock.patch("time.sleep", return_value=None) @@ -24,18 +26,31 @@ def test_exponential_backoff(mock_time): iteration_count = 0 for attempt in eb: - backoff_interval = mock_time.call_args[0][0] - jitter = curr_wait * eb._randomization_factor - - assert (curr_wait - jitter) <= backoff_interval <= (curr_wait + jitter) - assert attempt == iteration_count + 1 - assert eb.backoff_count == iteration_count + 1 - assert eb._current_wait_in_seconds == eb._multiplier ** (iteration_count + 1) - - curr_wait = eb._current_wait_in_seconds + if attempt == 1: + assert mock_time.call_count == 0 + else: + backoff_interval = mock_time.call_args[0][0] + jitter = curr_wait * eb._randomization_factor + + assert (curr_wait - jitter) <= backoff_interval <= (curr_wait + jitter) + assert attempt == iteration_count + 1 + assert eb.backoff_count == iteration_count + 1 + assert eb._current_wait_in_seconds == eb._multiplier ** iteration_count + + curr_wait = eb._current_wait_in_seconds iteration_count += 1 assert eb.total_attempts == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS assert eb.backoff_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS assert iteration_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS - assert mock_time.call_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS + assert ( + mock_time.call_count == _exponential_backoff._DEFAULT_RETRY_TOTAL_ATTEMPTS - 1 + ) + + +def test_minimum_total_attempts(): + with pytest.raises(exceptions.InvalidValue): + _exponential_backoff.ExponentialBackoff(total_attempts=0) + with pytest.raises(exceptions.InvalidValue): + _exponential_backoff.ExponentialBackoff(total_attempts=-1) + _exponential_backoff.ExponentialBackoff(total_attempts=1) diff --git a/contrib/python/google-auth/py3/tests/test_credentials_async.py b/contrib/python/google-auth/py3/tests/test_credentials_async.py new file mode 100644 index 0000000000..51e4f0611c --- /dev/null +++ b/contrib/python/google-auth/py3/tests/test_credentials_async.py @@ -0,0 +1,136 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest # type: ignore + +from google.auth import exceptions +from google.auth.aio import credentials + + +class CredentialsImpl(credentials.Credentials): + pass + + +def test_credentials_constructor(): + credentials = CredentialsImpl() + assert not credentials.token + + +@pytest.mark.asyncio +async def test_before_request(): + credentials = CredentialsImpl() + request = "water" + headers = {} + credentials.token = "orchid" + + # before_request should not affect the value of the token. + await credentials.before_request(request, "http://example.com", "GET", headers) + assert credentials.token == "orchid" + assert headers["authorization"] == "Bearer orchid" + assert "x-allowed-locations" not in headers + + request = "earth" + headers = {} + + # Second call shouldn't affect token or headers. + await credentials.before_request(request, "http://example.com", "GET", headers) + assert credentials.token == "orchid" + assert headers["authorization"] == "Bearer orchid" + assert "x-allowed-locations" not in headers + + +@pytest.mark.asyncio +async def test_static_credentials_ctor(): + static_creds = credentials.StaticCredentials(token="orchid") + assert static_creds.token == "orchid" + + +@pytest.mark.asyncio +async def test_static_credentials_apply_default(): + static_creds = credentials.StaticCredentials(token="earth") + headers = {} + + await static_creds.apply(headers) + assert headers["authorization"] == "Bearer earth" + + await static_creds.apply(headers, token="orchid") + assert headers["authorization"] == "Bearer orchid" + + +@pytest.mark.asyncio +async def test_static_credentials_before_request(): + static_creds = credentials.StaticCredentials(token="orchid") + request = "water" + headers = {} + + # before_request should not affect the value of the token. + await static_creds.before_request(request, "http://example.com", "GET", headers) + assert static_creds.token == "orchid" + assert headers["authorization"] == "Bearer orchid" + assert "x-allowed-locations" not in headers + + request = "earth" + headers = {} + + # Second call shouldn't affect token or headers. + await static_creds.before_request(request, "http://example.com", "GET", headers) + assert static_creds.token == "orchid" + assert headers["authorization"] == "Bearer orchid" + assert "x-allowed-locations" not in headers + + +@pytest.mark.asyncio +async def test_static_credentials_refresh(): + static_creds = credentials.StaticCredentials(token="orchid") + request = "earth" + + with pytest.raises(exceptions.InvalidOperation) as exc: + await static_creds.refresh(request) + assert exc.match("Static credentials cannot be refreshed.") + + +@pytest.mark.asyncio +async def test_anonymous_credentials_ctor(): + anon = credentials.AnonymousCredentials() + assert anon.token is None + + +@pytest.mark.asyncio +async def test_anonymous_credentials_refresh(): + anon = credentials.AnonymousCredentials() + request = object() + with pytest.raises(exceptions.InvalidOperation) as exc: + await anon.refresh(request) + assert exc.match("Anonymous credentials cannot be refreshed.") + + +@pytest.mark.asyncio +async def test_anonymous_credentials_apply_default(): + anon = credentials.AnonymousCredentials() + headers = {} + await anon.apply(headers) + assert headers == {} + with pytest.raises(ValueError): + await anon.apply(headers, token="orchid") + + +@pytest.mark.asyncio +async def test_anonymous_credentials_before_request(): + anon = credentials.AnonymousCredentials() + request = object() + method = "GET" + url = "https://example.com/api/endpoint" + headers = {} + await anon.before_request(request, method, url, headers) + assert headers == {} diff --git a/contrib/python/google-auth/py3/ya.make b/contrib/python/google-auth/py3/ya.make index 4ea57aefcc..caefae5db6 100644 --- a/contrib/python/google-auth/py3/ya.make +++ b/contrib/python/google-auth/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(2.32.0) +VERSION(2.33.0) LICENSE(Apache-2.0) @@ -28,6 +28,7 @@ PY_SRCS( google/auth/__init__.py google/auth/_cloud_sdk.py google/auth/_credentials_async.py + google/auth/_credentials_base.py google/auth/_default.py google/auth/_default_async.py google/auth/_exponential_backoff.py @@ -36,6 +37,8 @@ PY_SRCS( google/auth/_oauth2client.py google/auth/_refresh_worker.py google/auth/_service_account_info.py + google/auth/aio/__init__.py + google/auth/aio/credentials.py google/auth/api_key.py google/auth/app_engine.py google/auth/aws.py @@ -66,6 +69,7 @@ PY_SRCS( google/auth/transport/_custom_tls_signer.py google/auth/transport/_http_client.py google/auth/transport/_mtls_helper.py + google/auth/transport/_requests_base.py google/auth/transport/grpc.py google/auth/transport/mtls.py google/auth/transport/requests.py diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA index e48da7dc0b..9a38b4132f 100644 --- a/contrib/python/hypothesis/py3/.dist-info/METADATA +++ b/contrib/python/hypothesis/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: hypothesis -Version: 6.108.10 +Version: 6.110.0 Summary: A library for property-based testing Home-page: https://hypothesis.works Author: David R. MacIver and Zac Hatfield-Dodds diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/cathetus.py b/contrib/python/hypothesis/py3/hypothesis/internal/cathetus.py index 30e0d214f1..1f8f2fe82b 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/cathetus.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/cathetus.py @@ -27,7 +27,7 @@ def cathetus(h, a): may be inaccurate up to a relative error of (around) floating-point epsilon. - Based on the C99 implementation https://github.com/jjgreen/cathetus + Based on the C99 implementation https://gitlab.com/jjg/cathetus """ if isnan(h): return nan diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py index 72bc4ba980..40aad2e850 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py @@ -679,6 +679,12 @@ class Examples: i += n return Example(self, i) + # not strictly necessary as we have len/getitem, but required for mypy. + # https://github.com/python/mypy/issues/9737 + def __iter__(self) -> Iterator[Example]: + for i in range(len(self)): + yield self[i] + @dataclass_transform() @attr.s(slots=True, frozen=True) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py index 4465f59e5c..39382637db 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/junkdrawer.py @@ -19,12 +19,14 @@ import time import warnings from random import Random from typing import ( + Any, Callable, Dict, Generic, Iterable, Iterator, List, + Literal, Optional, Sequence, Tuple, @@ -109,10 +111,10 @@ class IntList(Sequence[int]): def count(self, value: int) -> int: return self.__underlying.count(value) - def __repr__(self): + def __repr__(self) -> str: return f"IntList({list(self.__underlying)!r})" - def __len__(self): + def __len__(self) -> int: return len(self.__underlying) @overload @@ -305,7 +307,7 @@ class ensure_free_stackframes: a reasonable value of N). """ - def __enter__(self): + def __enter__(self) -> None: cur_depth = stack_depth_of_caller() self.old_maxdepth = sys.getrecursionlimit() # The default CPython recursionlimit is 1000, but pytest seems to bump @@ -418,8 +420,8 @@ class SelfOrganisingList(Generic[T]): _gc_initialized = False -_gc_start = 0 -_gc_cumulative_time = 0 +_gc_start: float = 0 +_gc_cumulative_time: float = 0 # Since gc_callback potentially runs in test context, and perf_counter # might be monkeypatched, we store a reference to the real one. @@ -431,7 +433,9 @@ def gc_cumulative_time() -> float: if not _gc_initialized: if hasattr(gc, "callbacks"): # CPython - def gc_callback(phase, info): + def gc_callback( + phase: Literal["start", "stop"], info: Dict[str, int] + ) -> None: global _gc_start, _gc_cumulative_time try: now = _perf_counter() @@ -453,7 +457,7 @@ def gc_cumulative_time() -> float: gc.callbacks.insert(0, gc_callback) elif hasattr(gc, "hooks"): # pragma: no cover # pypy only # PyPy - def hook(stats): + def hook(stats: Any) -> None: global _gc_cumulative_time try: _gc_cumulative_time += stats.duration diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/optimiser.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/optimiser.py index 2f8f761223..a8f4478453 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/optimiser.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/optimiser.py @@ -8,9 +8,11 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. +from typing import Union + from hypothesis.internal.compat import int_from_bytes, int_to_bytes -from hypothesis.internal.conjecture.data import Status -from hypothesis.internal.conjecture.engine import BUFFER_SIZE +from hypothesis.internal.conjecture.data import ConjectureResult, Status, _Overrun +from hypothesis.internal.conjecture.engine import BUFFER_SIZE, ConjectureRunner from hypothesis.internal.conjecture.junkdrawer import find_integer from hypothesis.internal.conjecture.pareto import NO_SCORE @@ -31,7 +33,13 @@ class Optimiser: Software Testing and Analysis. ACM, 2017. """ - def __init__(self, engine, data, target, max_improvements=100): + def __init__( + self, + engine: ConjectureRunner, + data: ConjectureResult, + target: str, + max_improvements: int = 100, + ) -> None: """Optimise ``target`` starting from ``data``. Will stop either when we seem to have found a local maximum or when the target score has been improved ``max_improvements`` times. This limit is in place to @@ -42,21 +50,22 @@ class Optimiser: self.max_improvements = max_improvements self.improvements = 0 - def run(self): + def run(self) -> None: self.hill_climb() - def score_function(self, data): + def score_function(self, data: ConjectureResult) -> float: return data.target_observations.get(self.target, NO_SCORE) @property - def current_score(self): + def current_score(self) -> float: return self.score_function(self.current_data) - def consider_new_test_data(self, data): + def consider_new_data(self, data: Union[ConjectureResult, _Overrun]) -> bool: """Consider a new data object as a candidate target. If it is better than the current one, return True.""" if data.status < Status.VALID: return False + assert isinstance(data, ConjectureResult) score = self.score_function(data) if score < self.current_score: return False @@ -73,7 +82,7 @@ class Optimiser: return True return False - def hill_climb(self): + def hill_climb(self) -> None: """The main hill climbing loop where we actually do the work: Take data, and attempt to improve its score for target. select_example takes a data object and returns an index to an example where we should focus @@ -104,7 +113,7 @@ class Optimiser: if existing_as_int == max_int_value: continue - def attempt_replace(v): + def attempt_replace(v: int) -> bool: """Try replacing the current block in the current best test case with an integer of value i. Note that we use the *current* best and not the one we started with. This helps ensure that @@ -126,12 +135,14 @@ class Optimiser: + bytes(BUFFER_SIZE), ) - if self.consider_new_test_data(attempt): + if self.consider_new_data(attempt): return True - if attempt.status < Status.INVALID or len(attempt.buffer) == len( - self.current_data.buffer - ): + if attempt.status == Status.OVERRUN: + return False + + assert isinstance(attempt, ConjectureResult) + if len(attempt.buffer) == len(self.current_data.buffer): return False for i, ex in enumerate(self.current_data.examples): @@ -143,7 +154,7 @@ class Optimiser: if ex.length == ex_attempt.length: continue # pragma: no cover replacement = attempt.buffer[ex_attempt.start : ex_attempt.end] - if self.consider_new_test_data( + if self.consider_new_data( self.engine.cached_test_function( prefix + replacement diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py index a004338372..d441821787 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py @@ -9,7 +9,7 @@ # obtain one at https://mozilla.org/MPL/2.0/. from collections import defaultdict -from typing import TYPE_CHECKING, Callable, Dict, Optional, Union +from typing import TYPE_CHECKING, Callable, Dict, Optional, Tuple, TypeVar, Union import attr @@ -38,10 +38,14 @@ from hypothesis.internal.conjecture.shrinking import ( ) if TYPE_CHECKING: + from random import Random + from hypothesis.internal.conjecture.engine import ConjectureRunner +SortKeyT = TypeVar("SortKeyT", str, bytes) + -def sort_key(buffer): +def sort_key(buffer: SortKeyT) -> Tuple[int, SortKeyT]: """Returns a sort key such that "simpler" buffers are smaller than "more complicated" ones. @@ -357,16 +361,16 @@ class Shrinker: return self.passes_by_name[name] @property - def calls(self): + def calls(self) -> int: """Return the number of calls that have been made to the underlying test function.""" return self.engine.call_count @property - def misaligned(self): + def misaligned(self) -> int: return self.engine.misaligned_count - def check_calls(self): + def check_calls(self) -> None: if self.calls - self.calls_at_last_shrink >= self.max_stall: raise StopShrinking @@ -449,11 +453,11 @@ class Shrinker: self.check_calls() return result - def debug(self, msg): + def debug(self, msg: str) -> None: self.engine.debug(msg) @property - def random(self): + def random(self) -> "Random": return self.engine.random def shrink(self): @@ -1068,10 +1072,11 @@ class Shrinker: if node.was_forced: return False # pragma: no cover - if node.ir_type == "string": + if node.ir_type in {"string", "bytes"}: + size_kwarg = "min_size" if node.ir_type == "string" else "size" # if the size *increased*, we would have to guess what to pad with # in order to try fixing up this attempt. Just give up. - if node.kwargs["min_size"] <= attempt_kwargs["min_size"]: + if node.kwargs[size_kwarg] <= attempt_kwargs[size_kwarg]: return False # the size decreased in our attempt. Try again, but replace with # the min_size that we would have gotten, and truncate the value @@ -1082,22 +1087,7 @@ class Shrinker: initial_attempt[node.index].copy( with_kwargs=attempt_kwargs, with_value=initial_attempt[node.index].value[ - : attempt_kwargs["min_size"] - ], - ) - ] - + initial_attempt[node.index :] - ) - if node.ir_type == "bytes": - if node.kwargs["size"] <= attempt_kwargs["size"]: - return False - return self.consider_new_tree( - initial_attempt[: node.index] - + [ - initial_attempt[node.index].copy( - with_kwargs=attempt_kwargs, - with_value=initial_attempt[node.index].value[ - : attempt_kwargs["size"] + : attempt_kwargs[size_kwarg] ], ) ] diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/common.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/common.py index 1de89bd18b..b0c5ec8694 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/common.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinking/common.py @@ -38,10 +38,10 @@ class Shrinker: self.debugging_enabled = debug @property - def calls(self): + def calls(self) -> int: return len(self.__seen) - def __repr__(self): + def __repr__(self) -> str: return "{}({}initial={!r}, current={!r})".format( type(self).__name__, "" if self.name is None else f"{self.name!r}, ", @@ -75,7 +75,7 @@ class Shrinker: return other_class.shrink(initial, predicate, **kwargs) - def debug(self, *args): + def debug(self, *args: object) -> None: if self.debugging_enabled: print("DEBUG", self, *args) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/floats.py b/contrib/python/hypothesis/py3/hypothesis/internal/floats.py index 6c4210e997..79e6433dca 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/floats.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/floats.py @@ -11,22 +11,50 @@ import math import struct from sys import float_info -from typing import Callable, Optional, SupportsFloat +from typing import ( + TYPE_CHECKING, + Callable, + Dict, + Literal, + Optional, + SupportsFloat, + Tuple, + Union, +) + +if TYPE_CHECKING: + from typing import TypeAlias +else: + TypeAlias = object + +SignedIntFormat: "TypeAlias" = Literal["!h", "!i", "!q"] +UnsignedIntFormat: "TypeAlias" = Literal["!H", "!I", "!Q"] +IntFormat: "TypeAlias" = Union[SignedIntFormat, UnsignedIntFormat] +FloatFormat: "TypeAlias" = Literal["!e", "!f", "!d"] +Width: "TypeAlias" = Literal[16, 32, 64] # Format codes for (int, float) sized types, used for byte-wise casts. # See https://docs.python.org/3/library/struct.html#format-characters -STRUCT_FORMATS = { +STRUCT_FORMATS: Dict[int, Tuple[UnsignedIntFormat, FloatFormat]] = { 16: ("!H", "!e"), 32: ("!I", "!f"), 64: ("!Q", "!d"), } +TO_SIGNED_FORMAT: Dict[UnsignedIntFormat, SignedIntFormat] = { + "!H": "!h", + "!I": "!i", + "!Q": "!q", +} + -def reinterpret_bits(x, from_, to): - return struct.unpack(to, struct.pack(from_, x))[0] +def reinterpret_bits(x: float, from_: str, to: str) -> float: + x = struct.unpack(to, struct.pack(from_, x))[0] + assert isinstance(x, (float, int)) + return x -def float_of(x, width): +def float_of(x: SupportsFloat, width: Width) -> float: assert width in (16, 32, 64) if width == 64: return float(x) @@ -45,7 +73,7 @@ def is_negative(x: SupportsFloat) -> bool: ) from None -def count_between_floats(x, y, width=64): +def count_between_floats(x: float, y: float, width: int = 64) -> int: assert x <= y if is_negative(x): if is_negative(y): @@ -59,17 +87,19 @@ def count_between_floats(x, y, width=64): return float_to_int(y, width) - float_to_int(x, width) + 1 -def float_to_int(value, width=64): +def float_to_int(value: float, width: int = 64) -> int: fmt_int, fmt_flt = STRUCT_FORMATS[width] - return reinterpret_bits(value, fmt_flt, fmt_int) + x = reinterpret_bits(value, fmt_flt, fmt_int) + assert isinstance(x, int) + return x -def int_to_float(value, width=64): +def int_to_float(value: int, width: int = 64) -> float: fmt_int, fmt_flt = STRUCT_FORMATS[width] return reinterpret_bits(value, fmt_int, fmt_flt) -def next_up(value, width=64): +def next_up(value: float, width: int = 64) -> float: """Return the first float larger than finite `val` - IEEE 754's `nextUp`. From https://stackoverflow.com/a/10426033, with thanks to Mark Dickinson. @@ -81,34 +111,34 @@ def next_up(value, width=64): return 0.0 fmt_int, fmt_flt = STRUCT_FORMATS[width] # Note: n is signed; float_to_int returns unsigned - fmt_int = fmt_int.lower() - n = reinterpret_bits(value, fmt_flt, fmt_int) + fmt_int_signed = TO_SIGNED_FORMAT[fmt_int] + n = reinterpret_bits(value, fmt_flt, fmt_int_signed) if n >= 0: n += 1 else: n -= 1 - return reinterpret_bits(n, fmt_int, fmt_flt) + return reinterpret_bits(n, fmt_int_signed, fmt_flt) -def next_down(value, width=64): +def next_down(value: float, width: int = 64) -> float: return -next_up(-value, width) -def next_down_normal(value, width, allow_subnormal): +def next_down_normal(value: float, width: int, *, allow_subnormal: bool) -> float: value = next_down(value, width) if (not allow_subnormal) and 0 < abs(value) < width_smallest_normals[width]: return 0.0 if value > 0 else -width_smallest_normals[width] return value -def next_up_normal(value, width, allow_subnormal): - return -next_down_normal(-value, width, allow_subnormal) +def next_up_normal(value: float, width: int, *, allow_subnormal: bool) -> float: + return -next_down_normal(-value, width, allow_subnormal=allow_subnormal) # Smallest positive non-zero numbers that is fully representable by an # IEEE-754 float, calculated with the width's associated minimum exponent. # Values from https://en.wikipedia.org/wiki/IEEE_754#Basic_and_interchange_formats -width_smallest_normals = { +width_smallest_normals: Dict[int, float] = { 16: 2 ** -(2 ** (5 - 1) - 2), 32: 2 ** -(2 ** (8 - 1) - 2), 64: 2 ** -(2 ** (11 - 1) - 2), diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py index 62dcecde20..7fdeedb497 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py @@ -1295,6 +1295,12 @@ def _from_type(thing: Type[Ex]) -> SearchStrategy[Ex]: if types.is_a_union(thing): args = sorted(thing.__args__, key=types.type_sorting_key) return one_of([_from_type(t) for t in args]) + if thing in types.LiteralStringTypes: # pragma: no cover + # We can't really cover this because it needs either + # typing-extensions or python3.11+ typing. + # `LiteralString` from runtime's point of view is just a string. + # Fallback to regular text. + return text() # We also have a special case for TypeVars. # They are represented as instances like `~T` when they come here. # We need to work with their type instead. @@ -1340,27 +1346,68 @@ def _from_type(thing: Type[Ex]) -> SearchStrategy[Ex]: or hasattr(types.typing_extensions, "_TypedDictMeta") # type: ignore and type(thing) is types.typing_extensions._TypedDictMeta # type: ignore ): # pragma: no cover + + def _get_annotation_arg(key, annotation_type): + try: + return get_args(annotation_type)[0] + except IndexError: + raise InvalidArgument( + f"`{key}: {annotation_type.__name__}` is not a valid type annotation" + ) from None + + # Taken from `Lib/typing.py` and modified: + def _get_typeddict_qualifiers(key, annotation_type): + qualifiers = [] + while True: + annotation_origin = types.extended_get_origin(annotation_type) + if annotation_origin in types.AnnotatedTypes: + if annotation_args := get_args(annotation_type): + annotation_type = annotation_args[0] + else: + break + elif annotation_origin in types.RequiredTypes: + qualifiers.append(types.RequiredTypes) + annotation_type = _get_annotation_arg(key, annotation_type) + elif annotation_origin in types.NotRequiredTypes: + qualifiers.append(types.NotRequiredTypes) + annotation_type = _get_annotation_arg(key, annotation_type) + elif annotation_origin in types.ReadOnlyTypes: + qualifiers.append(types.ReadOnlyTypes) + annotation_type = _get_annotation_arg(key, annotation_type) + else: + break + return set(qualifiers), annotation_type + # The __optional_keys__ attribute may or may not be present, but if there's no # way to tell and we just have to assume that everything is required. # See https://github.com/python/cpython/pull/17214 for details. optional = set(getattr(thing, "__optional_keys__", ())) + required = set( + getattr(thing, "__required_keys__", get_type_hints(thing).keys()) + ) anns = {} for k, v in get_type_hints(thing).items(): - origin = get_origin(v) - if origin in types.RequiredTypes + types.NotRequiredTypes: - if origin in types.NotRequiredTypes: - optional.add(k) - else: - optional.discard(k) - try: - v = v.__args__[0] - except IndexError: - raise InvalidArgument( - f"`{k}: {v.__name__}` is not a valid type annotation" - ) from None + qualifiers, v = _get_typeddict_qualifiers(k, v) + # We ignore `ReadOnly` type for now, only unwrap it. + if types.RequiredTypes in qualifiers: + optional.discard(k) + required.add(k) + if types.NotRequiredTypes in qualifiers: + optional.add(k) + required.discard(k) + anns[k] = from_type_guarded(v) if anns[k] is ...: anns[k] = _from_type_deferred(v) + + if not required.isdisjoint(optional): # pragma: no cover + # It is impossible to cover, because `typing.py` or `typing-extensions` + # won't allow creating incorrect TypedDicts, + # this is just a sanity check from our side. + raise InvalidArgument( + f"Required keys overlap with optional keys in a TypedDict:" + f" {required=}, {optional=}" + ) if ( (not anns) and thing.__annotations__ @@ -1368,7 +1415,7 @@ def _from_type(thing: Type[Ex]) -> SearchStrategy[Ex]: ): raise InvalidArgument("Failed to retrieve type annotations for local type") return fixed_dictionaries( # type: ignore - mapping={k: v for k, v in anns.items() if k not in optional}, + mapping={k: v for k, v in anns.items() if k in required}, optional={k: v for k, v in anns.items() if k in optional}, ) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py index 507cf879d6..033577f2c8 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py @@ -380,22 +380,30 @@ def floats( if min_value is not None and ( exclude_min or (min_arg is not None and min_value < min_arg) ): - min_value = next_up_normal(min_value, width, assumed_allow_subnormal) + min_value = next_up_normal( + min_value, width, allow_subnormal=assumed_allow_subnormal + ) if min_value == min_arg: assert min_value == min_arg == 0 assert is_negative(min_arg) assert not is_negative(min_value) - min_value = next_up_normal(min_value, width, assumed_allow_subnormal) + min_value = next_up_normal( + min_value, width, allow_subnormal=assumed_allow_subnormal + ) assert min_value > min_arg # type: ignore if max_value is not None and ( exclude_max or (max_arg is not None and max_value > max_arg) ): - max_value = next_down_normal(max_value, width, assumed_allow_subnormal) + max_value = next_down_normal( + max_value, width, allow_subnormal=assumed_allow_subnormal + ) if max_value == max_arg: assert max_value == max_arg == 0 assert is_negative(max_value) assert not is_negative(max_arg) - max_value = next_down_normal(max_value, width, assumed_allow_subnormal) + max_value = next_down_normal( + max_value, width, allow_subnormal=assumed_allow_subnormal + ) assert max_value < max_arg # type: ignore if min_value == -math.inf: diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py index 8753bfb784..d5fef48aad 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py @@ -147,6 +147,49 @@ except AttributeError: # pragma: no cover pass # `typing_extensions` might not be installed +ReadOnlyTypes: tuple = () +try: + ReadOnlyTypes += (typing.ReadOnly,) # type: ignore +except AttributeError: # pragma: no cover + pass # Is missing for `python<3.13` +try: + ReadOnlyTypes += (typing_extensions.ReadOnly,) +except AttributeError: # pragma: no cover + pass # `typing_extensions` might not be installed + + +AnnotatedTypes: tuple = () +try: + AnnotatedTypes += (typing.Annotated,) +except AttributeError: # pragma: no cover + pass # Is missing for `python<3.9` +try: + AnnotatedTypes += (typing_extensions.Annotated,) +except AttributeError: # pragma: no cover + pass # `typing_extensions` might not be installed + + +LiteralStringTypes: tuple = () +try: + LiteralStringTypes += (typing.LiteralString,) # type: ignore +except AttributeError: # pragma: no cover + pass # Is missing for `python<3.11` +try: + LiteralStringTypes += (typing_extensions.LiteralString,) +except AttributeError: # pragma: no cover + pass # `typing_extensions` might not be installed + + +# We need this function to use `get_origin` on 3.8 for types added later: +# in typing-extensions, so we prefer this function over regular `get_origin` +# when unwrapping `TypedDict`'s annotations. +try: + extended_get_origin = typing_extensions.get_origin +except AttributeError: # pragma: no cover + # `typing_extensions` might not be installed, in this case - fallback: + extended_get_origin = get_origin # type: ignore + + # We use this variable to be sure that we are working with a type from `typing`: typing_root_type = (typing._Final, typing._GenericAlias) # type: ignore @@ -169,10 +212,10 @@ for name in ( "Self", "Required", "NotRequired", + "ReadOnly", "Never", "TypeVarTuple", "Unpack", - "LiteralString", ): try: NON_RUNTIME_TYPES += (getattr(typing, name),) diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py index c8a8221c54..007af37ae6 100644 --- a/contrib/python/hypothesis/py3/hypothesis/version.py +++ b/contrib/python/hypothesis/py3/hypothesis/version.py @@ -8,5 +8,5 @@ # v. 2.0. If a copy of the MPL was not distributed with this file, You can # obtain one at https://mozilla.org/MPL/2.0/. -__version_info__ = (6, 108, 10) +__version_info__ = (6, 110, 0) __version__ = ".".join(map(str, __version_info__)) diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make index 8a025cf359..7e33dc4497 100644 --- a/contrib/python/hypothesis/py3/ya.make +++ b/contrib/python/hypothesis/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(6.108.10) +VERSION(6.110.0) LICENSE(MPL-2.0) |