diff options
Diffstat (limited to 'contrib/python')
32 files changed, 2027 insertions, 125 deletions
diff --git a/contrib/python/allure-pytest/.dist-info/METADATA b/contrib/python/allure-pytest/.dist-info/METADATA index 8da0e7a49e8..834bf01e5c4 100644 --- a/contrib/python/allure-pytest/.dist-info/METADATA +++ b/contrib/python/allure-pytest/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: allure-pytest -Version: 2.14.1 +Version: 2.14.2 Summary: Allure pytest integration Home-page: https://allurereport.org/ Author: Qameta Software Inc., Stanislav Seliverstov @@ -25,7 +25,7 @@ Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Description-Content-Type: text/markdown Requires-Dist: pytest>=4.5.0 -Requires-Dist: allure-python-commons==2.14.1 +Requires-Dist: allure-python-commons==2.14.2 Dynamic: author Dynamic: author-email Dynamic: classifier diff --git a/contrib/python/allure-pytest/ya.make b/contrib/python/allure-pytest/ya.make index cf244ed539c..4b5ffa06a85 100644 --- a/contrib/python/allure-pytest/ya.make +++ b/contrib/python/allure-pytest/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(2.14.1) +VERSION(2.14.2) LICENSE(Apache-2.0) diff --git a/contrib/python/allure-python-commons/.dist-info/METADATA b/contrib/python/allure-python-commons/.dist-info/METADATA index 5a98d829103..65261a79955 100644 --- a/contrib/python/allure-python-commons/.dist-info/METADATA +++ b/contrib/python/allure-python-commons/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: allure-python-commons -Version: 2.14.1 +Version: 2.14.2 Summary: Contains the API for end users as well as helper functions and classes to build Allure adapters for Python test frameworks Home-page: https://allurereport.org/ Author: Qameta Software Inc., Stanislav Seliverstov diff --git a/contrib/python/allure-python-commons/ya.make b/contrib/python/allure-python-commons/ya.make index de11ac76eef..2883e1e72cb 100644 --- a/contrib/python/allure-python-commons/ya.make +++ b/contrib/python/allure-python-commons/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(2.14.1) +VERSION(2.14.2) LICENSE(Apache-2.0) diff --git a/contrib/python/google-auth/py3/.dist-info/METADATA b/contrib/python/google-auth/py3/.dist-info/METADATA index 53da88bdec8..ddba2bddaed 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.39.0 +Version: 2.40.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/_helpers.py b/contrib/python/google-auth/py3/google/auth/_helpers.py index a6c07f7d829..78fe22f7264 100644 --- a/contrib/python/google-auth/py3/google/auth/_helpers.py +++ b/contrib/python/google-auth/py3/google/auth/_helpers.py @@ -18,15 +18,38 @@ import base64 import calendar import datetime from email.message import Message +import hashlib +import json +import logging import sys +from typing import Any, Dict, Mapping, Optional, Union import urllib from google.auth import exceptions + +# _BASE_LOGGER_NAME is the base logger for all google-based loggers. +_BASE_LOGGER_NAME = "google" + +# _LOGGING_INITIALIZED ensures that base logger is only configured once +# (unless already configured by the end-user). +_LOGGING_INITIALIZED = False + + # The smallest MDS cache used by this library stores tokens until 4 minutes from # expiry. REFRESH_THRESHOLD = datetime.timedelta(minutes=3, seconds=45) +# TODO(https://github.com/googleapis/google-auth-library-python/issues/1684): Audit and update the list below. +_SENSITIVE_FIELDS = { + "accessToken", + "access_token", + "id_token", + "client_id", + "refresh_token", + "client_secret", +} + def copy_docstring(source_class): """Decorator that copies a method's docstring from another class. @@ -271,3 +294,220 @@ def is_python_3(): bool: True if the Python interpreter is Python 3 and False otherwise. """ return sys.version_info > (3, 0) + + +def _hash_sensitive_info(data: Union[dict, list]) -> Union[dict, list, str]: + """ + Hashes sensitive information within a dictionary. + + Args: + data: The dictionary containing data to be processed. + + Returns: + A new dictionary with sensitive values replaced by their SHA512 hashes. + If the input is a list, returns a list with each element recursively processed. + If the input is neither a dict nor a list, returns the type of the input as a string. + + """ + if isinstance(data, dict): + hashed_data: Dict[Any, Union[Optional[str], dict, list]] = {} + for key, value in data.items(): + if key in _SENSITIVE_FIELDS and not isinstance(value, (dict, list)): + hashed_data[key] = _hash_value(value, key) + elif isinstance(value, (dict, list)): + hashed_data[key] = _hash_sensitive_info(value) + else: + hashed_data[key] = value + return hashed_data + elif isinstance(data, list): + hashed_list = [] + for val in data: + hashed_list.append(_hash_sensitive_info(val)) + return hashed_list + else: + # TODO(https://github.com/googleapis/google-auth-library-python/issues/1701): + # Investigate and hash sensitive info before logging when the data type is + # not a dict or a list. + return str(type(data)) + + +def _hash_value(value, field_name: str) -> Optional[str]: + """Hashes a value and returns a formatted hash string.""" + if value is None: + return None + encoded_value = str(value).encode("utf-8") + hash_object = hashlib.sha512() + hash_object.update(encoded_value) + hex_digest = hash_object.hexdigest() + return f"hashed_{field_name}-{hex_digest}" + + +def _logger_configured(logger: logging.Logger) -> bool: + """Determines whether `logger` has non-default configuration + + Args: + logger: The logger to check. + + Returns: + bool: Whether the logger has any non-default configuration. + """ + return ( + logger.handlers != [] or logger.level != logging.NOTSET or not logger.propagate + ) + + +def is_logging_enabled(logger: logging.Logger) -> bool: + """ + Checks if debug logging is enabled for the given logger. + + Args: + logger: The logging.Logger instance to check. + + Returns: + True if debug logging is enabled, False otherwise. + """ + # NOTE: Log propagation to the root logger is disabled unless + # the base logger i.e. logging.getLogger("google") is + # explicitly configured by the end user. Ideally this + # needs to happen in the client layer (already does for GAPICs). + # However, this is implemented here to avoid logging + # (if a root logger is configured) when a version of google-auth + # which supports logging is used with: + # - an older version of a GAPIC which does not support logging. + # - Apiary client which does not support logging. + global _LOGGING_INITIALIZED + if not _LOGGING_INITIALIZED: + base_logger = logging.getLogger(_BASE_LOGGER_NAME) + if not _logger_configured(base_logger): + base_logger.propagate = False + _LOGGING_INITIALIZED = True + + return logger.isEnabledFor(logging.DEBUG) + + +def request_log( + logger: logging.Logger, + method: str, + url: str, + body: Optional[bytes], + headers: Optional[Mapping[str, str]], +) -> None: + """ + Logs an HTTP request at the DEBUG level if logging is enabled. + + Args: + logger: The logging.Logger instance to use. + method: The HTTP method (e.g., "GET", "POST"). + url: The URL of the request. + body: The request body (can be None). + headers: The request headers (can be None). + """ + if is_logging_enabled(logger): + content_type = ( + headers["Content-Type"] if headers and "Content-Type" in headers else "" + ) + json_body = _parse_request_body(body, content_type=content_type) + logged_body = _hash_sensitive_info(json_body) + logger.debug( + "Making request...", + extra={ + "httpRequest": { + "method": method, + "url": url, + "body": logged_body, + "headers": headers, + } + }, + ) + + +def _parse_request_body(body: Optional[bytes], content_type: str = "") -> Any: + """ + Parses a request body, handling bytes and string types, and different content types. + + Args: + body (Optional[bytes]): The request body. + content_type (str): The content type of the request body, e.g., "application/json", + "application/x-www-form-urlencoded", or "text/plain". If empty, attempts + to parse as JSON. + + Returns: + Parsed body (dict, str, or None). + - JSON: Decodes if content_type is "application/json" or None (fallback). + - URL-encoded: Parses if content_type is "application/x-www-form-urlencoded". + - Plain text: Returns string if content_type is "text/plain". + - None: Returns if body is None, UTF-8 decode fails, or content_type is unknown. + """ + if body is None: + return None + try: + body_str = body.decode("utf-8") + except (UnicodeDecodeError, AttributeError): + return None + content_type = content_type.lower() + if not content_type or "application/json" in content_type: + try: + return json.loads(body_str) + except (json.JSONDecodeError, TypeError): + return body_str + if "application/x-www-form-urlencoded" in content_type: + parsed_query = urllib.parse.parse_qs(body_str) + result = {k: v[0] for k, v in parsed_query.items()} + return result + if "text/plain" in content_type: + return body_str + return None + + +def _parse_response(response: Any) -> Any: + """ + Parses a response, attempting to decode JSON. + + Args: + response: The response object to parse. This can be any type, but + it is expected to have a `json()` method if it contains JSON. + + Returns: + The parsed response. If the response contains valid JSON, the + decoded JSON object (e.g., a dictionary or list) is returned. + If the response does not have a `json()` method or if the JSON + decoding fails, None is returned. + """ + try: + json_response = response.json() + return json_response + except Exception: + # TODO(https://github.com/googleapis/google-auth-library-python/issues/1744): + # Parse and return response payload as json based on different content types. + return None + + +def _response_log_base(logger: logging.Logger, parsed_response: Any) -> None: + """ + Logs a parsed HTTP response at the DEBUG level. + + This internal helper function takes a parsed response and logs it + using the provided logger. It also applies a hashing function to + potentially sensitive information before logging. + + Args: + logger: The logging.Logger instance to use for logging. + parsed_response: The parsed HTTP response object (e.g., a dictionary, + list, or the original response if parsing failed). + """ + + logged_response = _hash_sensitive_info(parsed_response) + logger.debug("Response received...", extra={"httpResponse": logged_response}) + + +def response_log(logger: logging.Logger, response: Any) -> None: + """ + Logs an HTTP response at the DEBUG level if logging is enabled. + + Args: + logger: The logging.Logger instance to use. + response: The HTTP response object to log. + """ + if is_logging_enabled(logger): + json_response = _parse_response(response) + _response_log_base(logger, json_response) diff --git a/contrib/python/google-auth/py3/google/auth/aio/_helpers.py b/contrib/python/google-auth/py3/google/auth/aio/_helpers.py new file mode 100644 index 00000000000..fd7d37a2f7b --- /dev/null +++ b/contrib/python/google-auth/py3/google/auth/aio/_helpers.py @@ -0,0 +1,57 @@ +# Copyright 2025 Google Inc. +# +# 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. + +"""Helper functions for commonly used utilities.""" + + +import logging +from typing import Any + +from google.auth import _helpers + + +async def _parse_response_async(response: Any) -> Any: + """ + Parses an async response, attempting to decode JSON. + + Args: + response: The response object to parse. This can be any type, but + it is expected to have a `json()` method if it contains JSON. + + Returns: + The parsed response. If the response contains valid JSON, the + decoded JSON object (e.g., a dictionary) is returned. + If the response does not have a `json()` method or if the JSON + decoding fails, None is returned. + """ + try: + json_response = await response.json() + return json_response + except Exception: + # TODO(https://github.com/googleapis/google-auth-library-python/issues/1745): + # Parse and return response payload as json based on different content types. + return None + + +async def response_log_async(logger: logging.Logger, response: Any) -> None: + """ + Logs an Async HTTP response at the DEBUG level if logging is enabled. + + Args: + logger: The logging.Logger instance to use. + response: The HTTP response object to log. + """ + if _helpers.is_logging_enabled(logger): + json_response = await _parse_response_async(response) + _helpers._response_log_base(logger, json_response) diff --git a/contrib/python/google-auth/py3/google/auth/aio/transport/aiohttp.py b/contrib/python/google-auth/py3/google/auth/aio/transport/aiohttp.py index 074d1491c70..67a19f952d2 100644 --- a/contrib/python/google-auth/py3/google/auth/aio/transport/aiohttp.py +++ b/contrib/python/google-auth/py3/google/auth/aio/transport/aiohttp.py @@ -16,6 +16,7 @@ """ import asyncio +import logging from typing import AsyncGenerator, Mapping, Optional try: @@ -27,8 +28,11 @@ except ImportError as caught_exc: # pragma: NO COVER from google.auth import _helpers from google.auth import exceptions +from google.auth.aio import _helpers as _helpers_async from google.auth.aio import transport +_LOGGER = logging.getLogger(__name__) + class Response(transport.Response): """ @@ -155,6 +159,7 @@ class Request(transport.Request): self._session = aiohttp.ClientSession() client_timeout = aiohttp.ClientTimeout(total=timeout) + _helpers.request_log(_LOGGER, method, url, body, headers) response = await self._session.request( method, url, @@ -163,6 +168,7 @@ class Request(transport.Request): timeout=client_timeout, **kwargs, ) + await _helpers_async.response_log_async(_LOGGER, response) return Response(response) except aiohttp.ClientError as caught_exc: diff --git a/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py b/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py index bc4d9dc69af..36366be5108 100644 --- a/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py +++ b/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py @@ -22,14 +22,20 @@ from __future__ import absolute_import import asyncio import functools +import logging import aiohttp # type: ignore import urllib3 # type: ignore +from google.auth import _helpers from google.auth import exceptions from google.auth import transport +from google.auth.aio import _helpers as _helpers_async from google.auth.transport import requests + +_LOGGER = logging.getLogger(__name__) + # Timeout can be re-defined depending on async requirement. Currently made 60s more than # sync timeout. _DEFAULT_TIMEOUT = 180 # in seconds @@ -182,10 +188,11 @@ class Request(transport.Request): self.session = aiohttp.ClientSession( auto_decompress=False ) # pragma: NO COVER - requests._LOGGER.debug("Making request: %s %s", method, url) + _helpers.request_log(_LOGGER, method, url, body, headers) response = await self.session.request( method, url, data=body, headers=headers, timeout=timeout, **kwargs ) + await _helpers_async.response_log_async(_LOGGER, response) return _CombinedResponse(response) except aiohttp.ClientError as caught_exc: diff --git a/contrib/python/google-auth/py3/google/auth/transport/_http_client.py b/contrib/python/google-auth/py3/google/auth/transport/_http_client.py index cec0ab73fb3..898a86519b7 100644 --- a/contrib/python/google-auth/py3/google/auth/transport/_http_client.py +++ b/contrib/python/google-auth/py3/google/auth/transport/_http_client.py @@ -19,6 +19,7 @@ import logging import socket import urllib +from google.auth import _helpers from google.auth import exceptions from google.auth import transport @@ -99,10 +100,11 @@ class Request(transport.Request): connection = http_client.HTTPConnection(parts.netloc, timeout=timeout) try: - _LOGGER.debug("Making request: %s %s", method, url) + _helpers.request_log(_LOGGER, method, url, body, headers) connection.request(method, path, body=body, headers=headers, **kwargs) response = connection.getresponse() + _helpers.response_log(_LOGGER, response) return Response(response) except (http_client.HTTPException, socket.error) as caught_exc: 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 23a69783dc3..0540746f894 100644 --- a/contrib/python/google-auth/py3/google/auth/transport/requests.py +++ b/contrib/python/google-auth/py3/google/auth/transport/requests.py @@ -34,6 +34,7 @@ from requests.packages.urllib3.util.ssl_ import ( # type: ignore create_urllib3_context, ) # pylint: disable=ungrouped-imports +from google.auth import _helpers from google.auth import environment_vars from google.auth import exceptions from google.auth import transport @@ -182,10 +183,11 @@ class Request(transport.Request): google.auth.exceptions.TransportError: If any exception occurred. """ try: - _LOGGER.debug("Making request: %s %s", method, url) + _helpers.request_log(_LOGGER, method, url, body, headers) response = self.session.request( method, url, data=body, headers=headers, timeout=timeout, **kwargs ) + _helpers.response_log(_LOGGER, response) return _Response(response) except requests.exceptions.RequestException as caught_exc: new_exc = exceptions.TransportError(caught_exc) @@ -534,6 +536,7 @@ class AuthorizedSession(requests.Session): remaining_time = guard.remaining_timeout with TimeoutGuard(remaining_time) as guard: + _helpers.request_log(_LOGGER, method, url, data, headers) response = super(AuthorizedSession, self).request( method, url, @@ -542,6 +545,7 @@ class AuthorizedSession(requests.Session): timeout=timeout, **kwargs ) + _helpers.response_log(_LOGGER, response) remaining_time = guard.remaining_timeout # If the response indicated that the credentials needed to be diff --git a/contrib/python/google-auth/py3/google/auth/transport/urllib3.py b/contrib/python/google-auth/py3/google/auth/transport/urllib3.py index db4fa93ff11..03ed75aa2f5 100644 --- a/contrib/python/google-auth/py3/google/auth/transport/urllib3.py +++ b/contrib/python/google-auth/py3/google/auth/transport/urllib3.py @@ -50,6 +50,7 @@ except ImportError as caught_exc: # pragma: NO COVER ) from caught_exc +from google.auth import _helpers from google.auth import environment_vars from google.auth import exceptions from google.auth import transport @@ -144,10 +145,11 @@ class Request(transport.Request): kwargs["timeout"] = timeout try: - _LOGGER.debug("Making request: %s %s", method, url) + _helpers.request_log(_LOGGER, method, url, body, headers) response = self.http.request( method, url, body=body, headers=headers, **kwargs ) + _helpers.response_log(_LOGGER, response) return _Response(response) except urllib3.exceptions.HTTPError as caught_exc: new_exc = exceptions.TransportError(caught_exc) diff --git a/contrib/python/google-auth/py3/google/auth/version.py b/contrib/python/google-auth/py3/google/auth/version.py index 393caa8ad44..d1363c1ef0e 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.39.0" +__version__ = "2.40.0" diff --git a/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py b/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py index 7784e83d0b9..24e984f3d33 100644 --- a/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py +++ b/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py @@ -67,7 +67,7 @@ class GetRequest: extensions: Optional[AuthenticationExtensionsClientInputs] = None def to_json(self) -> str: - req_options: Dict[str, Any] = {"rpid": self.rpid, "challenge": self.challenge} + req_options: Dict[str, Any] = {"rpId": self.rpid, "challenge": self.challenge} if self.timeout_ms: req_options["timeout"] = self.timeout_ms if self.allow_credentials: diff --git a/contrib/python/google-auth/py3/tests/aio/test__helpers.py b/contrib/python/google-auth/py3/tests/aio/test__helpers.py new file mode 100644 index 00000000000..7642431caec --- /dev/null +++ b/contrib/python/google-auth/py3/tests/aio/test__helpers.py @@ -0,0 +1,110 @@ +# Copyright 2025 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 json +import logging + +import pytest # type: ignore + +from google.auth.aio import _helpers + +# _MOCK_BASE_LOGGER_NAME is the base logger namespace used for testing. +_MOCK_BASE_LOGGER_NAME = "foogle" + +# _MOCK_CHILD_LOGGER_NAME is the child logger namespace used for testing. +_MOCK_CHILD_LOGGER_NAME = "foogle.bar" + + +def logger(): + """Returns a child logger for testing.""" + logger = logging.getLogger(_MOCK_CHILD_LOGGER_NAME) + logger.level = logging.NOTSET + logger.handlers = [] + logger.propagate = True + return logger + + +def base_logger(): + """Returns a child logger for testing.""" + logger = logging.getLogger(_MOCK_BASE_LOGGER_NAME) + logger.level = logging.NOTSET + logger.handlers = [] + logger.propagate = True + return logger + + +async def test_response_log_debug_enabled(logger, caplog, base_logger): + caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME) + await _helpers.response_log_async(logger, {"payload": None}) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.message == "Response received..." + assert record.httpResponse == "<class 'NoneType'>" + + +async def test_response_log_debug_disabled(logger, caplog, base_logger): + caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME) + await _helpers.response_log_async(logger, "another_response") + assert "Response received..." not in caplog.text + + +async def test_response_log_debug_enabled_response_json(logger, caplog, base_logger): + class MockResponse: + async def json(self): + return {"key1": "value1", "key2": "value2", "key3": "value3"} + + response = MockResponse() + caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME) + await _helpers.response_log_async(logger, response) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.message == "Response received..." + assert record.httpResponse == {"key1": "value1", "key2": "value2", "key3": "value3"} + + +async def test_parse_response_async_json_valid(): + class MockResponse: + async def json(self): + return {"data": "test"} + + response = MockResponse() + expected = {"data": "test"} + assert await _helpers._parse_response_async(response) == expected + + +async def test_parse_response_async_json_invalid(): + class MockResponse: + def json(self): + raise json.JSONDecodeError("msg", "doc", 0) + + response = MockResponse() + assert await _helpers._parse_response_async(response) is None + + +async def test_parse_response_async_no_json_method(): + response = "plain text" + assert await _helpers._parse_response_async(response) is None + + +async def test_parse_response_async_none(): + assert await _helpers._parse_response_async(None) is None diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py index 454e97cb61d..9fba266da9f 100644 --- a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py +++ b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py @@ -118,7 +118,7 @@ def test_success_get_assertion(os_get_stub, subprocess_run_stub): "type": "get", "origin": "fake_origin", "requestData": { - "rpid": "fake_rpid", + "rpId": "fake_rpid", "challenge": "fake_challenge", "allowCredentials": [{"type": "public-key", "id": "fake_id_1"}], }, diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py index 5231d21896a..bafe5b05060 100644 --- a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py +++ b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py @@ -82,7 +82,7 @@ def test_GetRequest(has_allow_credentials): "type": "get", "origin": "fake_origin", "requestData": { - "rpid": "fake_rpid", + "rpId": "fake_rpid", "timeout": 123, "challenge": "fake_challenge", "userVerification": "preferred", diff --git a/contrib/python/google-auth/py3/tests/test__helpers.py b/contrib/python/google-auth/py3/tests/test__helpers.py index c9a3847ac48..a4337c01608 100644 --- a/contrib/python/google-auth/py3/tests/test__helpers.py +++ b/contrib/python/google-auth/py3/tests/test__helpers.py @@ -13,12 +13,50 @@ # limitations under the License. import datetime +import json +import logging +from unittest import mock import urllib import pytest # type: ignore from google.auth import _helpers +# _MOCK_BASE_LOGGER_NAME is the base logger namespace used for testing. +_MOCK_BASE_LOGGER_NAME = "foogle" + +# _MOCK_CHILD_LOGGER_NAME is the child logger namespace used for testing. +_MOCK_CHILD_LOGGER_NAME = "foogle.bar" + + +def logger(): + """Returns a child logger for testing.""" + logger = logging.getLogger(_MOCK_CHILD_LOGGER_NAME) + logger.level = logging.NOTSET + logger.handlers = [] + logger.propagate = True + return logger + + +def base_logger(): + """Returns a child logger for testing.""" + logger = logging.getLogger(_MOCK_BASE_LOGGER_NAME) + logger.level = logging.NOTSET + logger.handlers = [] + logger.propagate = True + return logger + + [email protected](autouse=True) +def reset_logging_initialized(): + """Resets the global _LOGGING_INITIALIZED variable before each test.""" + original_state = _helpers._LOGGING_INITIALIZED + _helpers._LOGGING_INITIALIZED = False + yield + _helpers._LOGGING_INITIALIZED = original_state + class SourceClass(object): def func(self): # pragma: NO COVER @@ -92,7 +130,7 @@ def test_to_bytes_with_bytes(): def test_to_bytes_with_unicode(): - value = u"string-val" + value = "string-val" encoded_value = b"string-val" assert _helpers.to_bytes(value) == encoded_value @@ -103,13 +141,13 @@ def test_to_bytes_with_nonstring_type(): def test_from_bytes_with_unicode(): - value = u"bytes-val" + value = "bytes-val" assert _helpers.from_bytes(value) == value def test_from_bytes_with_bytes(): value = b"string-val" - decoded_value = u"string-val" + decoded_value = "string-val" assert _helpers.from_bytes(value) == decoded_value @@ -194,3 +232,393 @@ def test_unpadded_urlsafe_b64encode(): for case, expected in cases: assert _helpers.unpadded_urlsafe_b64encode(case) == expected + + +def test_hash_sensitive_info_basic(): + test_data = { + "expires_in": 3599, + "access_token": "access-123", + "scope": "https://www.googleapis.com/auth/test-api", + "token_type": "Bearer", + } + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data["expires_in"] == 3599 + assert hashed_data["scope"] == "https://www.googleapis.com/auth/test-api" + assert hashed_data["access_token"].startswith("hashed_access_token-") + assert hashed_data["token_type"] == "Bearer" + + +def test_hash_sensitive_info_multiple_sensitive(): + test_data = { + "access_token": "some_long_token", + "id_token": "1234-5678-9012-3456", + "expires_in": 3599, + "token_type": "Bearer", + } + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data["expires_in"] == 3599 + assert hashed_data["token_type"] == "Bearer" + assert hashed_data["access_token"].startswith("hashed_access_token-") + assert hashed_data["id_token"].startswith("hashed_id_token-") + + +def test_hash_sensitive_info_none_value(): + test_data = {"username": "user3", "secret": None, "normal_data": "abc"} + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data["secret"] is None + assert hashed_data["normal_data"] == "abc" + + +def test_hash_sensitive_info_non_string_value(): + test_data = {"username": "user4", "access_token": 12345, "normal_data": "def"} + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data["access_token"].startswith("hashed_access_token-") + assert hashed_data["normal_data"] == "def" + + +def test_hash_sensitive_info_list_value(): + test_data = [ + {"name": "Alice", "access_token": "12345"}, + {"name": "Bob", "client_id": "1141"}, + ] + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data[0]["access_token"].startswith("hashed_access_token-") + assert hashed_data[1]["client_id"].startswith("hashed_client_id-") + + +def test_hash_sensitive_info_nested_list_value(): + test_data = [{"names": ["Alice", "Bob"], "tokens": [{"access_token": "1234"}]}] + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data[0]["tokens"][0]["access_token"].startswith( + "hashed_access_token-" + ) + + +def test_hash_sensitive_info_int_value(): + test_data = 123 + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data == "<class 'int'>" + + +def test_hash_sensitive_info_bool_value(): + test_data = True + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data == "<class 'bool'>" + + +def test_hash_sensitive_info_byte_value(): + test_data = b"1243" + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data == "<class 'bytes'>" + + +def test_hash_sensitive_info_empty_dict(): + test_data = {} + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data == {} + + +def test_hash_value_consistent_hashing(): + value = "test_value" + field_name = "test_field" + hash1 = _helpers._hash_value(value, field_name) + hash2 = _helpers._hash_value(value, field_name) + assert hash1 == hash2 + + +def test_hash_value_different_hashing(): + value1 = "test_value1" + value2 = "test_value2" + field_name = "test_field" + hash1 = _helpers._hash_value(value1, field_name) + hash2 = _helpers._hash_value(value2, field_name) + assert hash1 != hash2 + + +def test_hash_value_none(): + assert _helpers._hash_value(None, "test") is None + + +def test_logger_configured_default(logger): + assert not _helpers._logger_configured(logger) + + +def test_logger_configured_with_handler(logger): + mock_handler = logging.NullHandler() + logger.addHandler(mock_handler) + assert _helpers._logger_configured(logger) + + # Cleanup + logger.removeHandler(mock_handler) + + +def test_logger_configured_with_custom_level(logger): + original_level = logger.level + logger.level = logging.INFO + assert _helpers._logger_configured(logger) + + # Cleanup + logging.level = original_level + + +def test_logger_configured_with_propagate(logger): + original_propagate = logger.propagate + logger.propagate = False + assert _helpers._logger_configured(logger) + + # Cleanup + logger.propagate = original_propagate + + +def test_is_logging_enabled_with_no_level_set(logger, base_logger): + with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", "foogle"): + assert _helpers.is_logging_enabled(logger) is False + + +def test_is_logging_enabled_with_debug_disabled(caplog, logger, base_logger): + with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME): + caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME) + assert _helpers.is_logging_enabled(logger) is False + + +def test_is_logging_enabled_with_debug_enabled(caplog, logger, base_logger): + with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME): + caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME) + assert _helpers.is_logging_enabled(logger) + + +def test_is_logging_enabled_with_base_logger_configured_with_info( + caplog, logger, base_logger +): + with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME): + caplog.set_level(logging.INFO, logger=_MOCK_BASE_LOGGER_NAME) + + base_logger = logging.getLogger(_MOCK_BASE_LOGGER_NAME) + assert not _helpers.is_logging_enabled(base_logger) + assert not _helpers.is_logging_enabled(logger) + + +def test_is_logging_enabled_with_base_logger_configured_with_debug( + caplog, logger, base_logger +): + with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME): + caplog.set_level(logging.DEBUG, logger=_MOCK_BASE_LOGGER_NAME) + + assert _helpers.is_logging_enabled(base_logger) + assert _helpers.is_logging_enabled(logger) + + +def test_is_logging_enabled_with_base_logger_info_child_logger_debug( + caplog, logger, base_logger +): + with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME): + caplog.set_level(logging.INFO, logger=_MOCK_BASE_LOGGER_NAME) + caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME) + + assert not _helpers.is_logging_enabled(base_logger) + assert _helpers.is_logging_enabled(logger) + + +def test_is_logging_enabled_with_base_logger_debug_child_logger_info( + caplog, logger, base_logger +): + with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME): + caplog.set_level(logging.DEBUG, logger=_MOCK_BASE_LOGGER_NAME) + caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME) + + assert _helpers.is_logging_enabled(base_logger) + assert not _helpers.is_logging_enabled(logger) + + +def test_request_log_debug_enabled(logger, caplog, base_logger): + caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME) + _helpers.request_log( + logger, + "GET", + "http://example.com", + b'{"key": "value"}', + {"Authorization": "Bearer token"}, + ) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.message == "Making request..." + assert record.httpRequest == { + "method": "GET", + "url": "http://example.com", + "body": {"key": "value"}, + "headers": {"Authorization": "Bearer token"}, + } + + +def test_request_log_plain_text_debug_enabled(logger, caplog, base_logger): + caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME) + _helpers.request_log( + logger, + "GET", + "http://example.com", + b"This is plain text.", + {"Authorization": "Bearer token", "Content-Type": "text/plain"}, + ) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.message == "Making request..." + assert record.httpRequest == { + "method": "GET", + "url": "http://example.com", + "body": "<class 'str'>", + "headers": {"Authorization": "Bearer token", "Content-Type": "text/plain"}, + } + + +def test_request_log_debug_disabled(logger, caplog, base_logger): + caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME) + _helpers.request_log( + logger, + "POST", + "https://api.example.com", + "data", + {"Content-Type": "application/json"}, + ) + assert "Making request: POST https://api.example.com" not in caplog.text + + +def test_response_log_debug_enabled(logger, caplog, base_logger): + caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME) + _helpers.response_log(logger, {"payload": None}) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.message == "Response received..." + assert record.httpResponse == "<class 'NoneType'>" + + +def test_response_log_debug_disabled(logger, caplog): + caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME) + _helpers.response_log(logger, "another_response") + assert "Response received..." not in caplog.text + + +def test_response_log_base_logger_configured(logger, caplog, base_logger): + caplog.set_level(logging.DEBUG, logger=_MOCK_BASE_LOGGER_NAME) + _helpers.response_log(logger, "another_response") + assert "Response received..." in caplog.text + + +def test_response_log_debug_enabled_response_list(logger, caplog, base_logger): + # NOTE: test the response log when response.json() returns a list as per + # https://requests.readthedocs.io/en/latest/api/#requests.Response.json. + class MockResponse: + def json(self): + return ["item1", "item2", "item3"] + + response = MockResponse() + caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME) + _helpers.response_log(logger, response) + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.message == "Response received..." + assert record.httpResponse == ["<class 'str'>", "<class 'str'>", "<class 'str'>"] + + +def test_parse_request_body_bytes_valid(): + body = b"key1=value1&key2=value2" + expected = {"key1": "value1", "key2": "value2"} + assert ( + _helpers._parse_request_body( + body, content_type="application/x-www-form-urlencoded" + ) + == expected + ) + + +def test_parse_request_body_bytes_empty(): + body = b"" + assert _helpers._parse_request_body(body) == "" + + +def test_parse_request_body_bytes_invalid_encoding(): + body = b"\xff\xfe\xfd" # Invalid UTF-8 sequence + assert _helpers._parse_request_body(body) is None + + +def test_parse_request_body_bytes_malformed_query(): + body = b"key1=value1&key2=value2" # missing equals + expected = {"key1": "value1", "key2": "value2"} + assert ( + _helpers._parse_request_body( + body, content_type="application/x-www-form-urlencoded" + ) + == expected + ) + + +def test_parse_request_body_none(): + assert _helpers._parse_request_body(None) is None + + +def test_parse_request_body_bytes_no_content_type(): + body = b'{"key": "value"}' + expected = {"key": "value"} + assert _helpers._parse_request_body(body) == expected + + +def test_parse_request_body_bytes_content_type_json(): + body = b'{"key": "value"}' + expected = {"key": "value"} + assert ( + _helpers._parse_request_body(body, content_type="application/json") == expected + ) + + +def test_parse_request_body_content_type_urlencoded(): + body = b"key=value" + expected = {"key": "value"} + assert ( + _helpers._parse_request_body( + body, content_type="application/x-www-form-urlencoded" + ) + == expected + ) + + +def test_parse_request_body_bytes_content_type_text(): + body = b"This is plain text." + expected = "This is plain text." + assert _helpers._parse_request_body(body, content_type="text/plain") == expected + + +def test_parse_request_body_content_type_invalid(): + body = b'{"key": "value"}' + assert _helpers._parse_request_body(body, content_type="invalid") is None + + +def test_parse_request_body_other_type(): + assert _helpers._parse_request_body(123) is None + assert _helpers._parse_request_body("string") is None + + +def test_parse_response_json_valid(): + class MockResponse: + def json(self): + return {"data": "test"} + + response = MockResponse() + expected = {"data": "test"} + assert _helpers._parse_response(response) == expected + + +def test_parse_response_json_invalid(): + class MockResponse: + def json(self): + raise json.JSONDecodeError("msg", "doc", 0) + + response = MockResponse() + assert _helpers._parse_response(response) is None + + +def test_parse_response_no_json_method(): + response = "plain text" + assert _helpers._parse_response(response) is None + + +def test_parse_response_none(): + assert _helpers._parse_response(None) is None diff --git a/contrib/python/google-auth/py3/ya.make b/contrib/python/google-auth/py3/ya.make index eddecb079e5..7ceec35306e 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.39.0) +VERSION(2.40.0) LICENSE(Apache-2.0) @@ -39,6 +39,7 @@ PY_SRCS( google/auth/_refresh_worker.py google/auth/_service_account_info.py google/auth/aio/__init__.py + google/auth/aio/_helpers.py google/auth/aio/credentials.py google/auth/aio/transport/__init__.py google/auth/aio/transport/aiohttp.py diff --git a/contrib/python/numpy/py2/numpy/random/__init__.py b/contrib/python/numpy/py2/numpy/random/__init__.py index 965ab5ea9b7..1458c8d8f9f 100644 --- a/contrib/python/numpy/py2/numpy/random/__init__.py +++ b/contrib/python/numpy/py2/numpy/random/__init__.py @@ -146,7 +146,7 @@ with warnings.catch_warnings(): ranf = random = sample = random_sample __all__.extend(['ranf', 'random', 'sample']) -def __RandomState_ctor(): +def _RandomState_ctor(): """Return a RandomState instance. This function exists solely to assist (un)pickling. diff --git a/contrib/python/numpy/py2/numpy/random/mtrand/mtrand.pyx b/contrib/python/numpy/py2/numpy/random/mtrand/mtrand.pyx index 08312da552f..7d82b9f8cfa 100644 --- a/contrib/python/numpy/py2/numpy/random/mtrand/mtrand.pyx +++ b/contrib/python/numpy/py2/numpy/random/mtrand/mtrand.pyx @@ -813,7 +813,7 @@ cdef class RandomState: self.set_state(state) def __reduce__(self): - return (np.random.__RandomState_ctor, (), self.get_state()) + return (np.random._RandomState_ctor, (), self.get_state()) # Basic distributions: def random_sample(self, size=None): @@ -990,7 +990,7 @@ cdef class RandomState: raise ValueError("high is out of bounds for %s" % dtype) if ilow >= ihigh and np.prod(size) != 0: raise ValueError("Range cannot be empty (low >= high) unless no samples are taken") - + with self.lock: ret = randfunc(ilow, ihigh - 1, size, self.state_address) @@ -4919,7 +4919,7 @@ cdef class RandomState: return arr arr = np.asarray(x) - + # shuffle has fast-path for 1-d if arr.ndim == 1: # Return a copy if same memory @@ -4932,7 +4932,7 @@ cdef class RandomState: idx = np.arange(arr.shape[0], dtype=np.intp) self.shuffle(idx) return arr[idx] - + _rand = RandomState() seed = _rand.seed diff --git a/contrib/python/numpy/py2/patches/fix_cython_import.patch b/contrib/python/numpy/py2/patches/fix_cython_import.patch new file mode 100644 index 00000000000..ac10dc0eabc --- /dev/null +++ b/contrib/python/numpy/py2/patches/fix_cython_import.patch @@ -0,0 +1,49 @@ +--- contrib/python/numpy/py2/numpy/random/__init__.py (index) ++++ contrib/python/numpy/py2/numpy/random/__init__.py (working tree) +@@ -146,7 +146,7 @@ with warnings.catch_warnings(): + ranf = random = sample = random_sample + __all__.extend(['ranf', 'random', 'sample']) + +-def __RandomState_ctor(): ++def _RandomState_ctor(): + """Return a RandomState instance. + + This function exists solely to assist (un)pickling. +--- contrib/python/numpy/py2/numpy/random/mtrand/mtrand.pyx (index) ++++ contrib/python/numpy/py2/numpy/random/mtrand/mtrand.pyx (working tree) +@@ -813,7 +813,7 @@ cdef class RandomState: + self.set_state(state) + + def __reduce__(self): +- return (np.random.__RandomState_ctor, (), self.get_state()) ++ return (np.random._RandomState_ctor, (), self.get_state()) + + # Basic distributions: + def random_sample(self, size=None): +@@ -990,7 +990,7 @@ cdef class RandomState: + raise ValueError("high is out of bounds for %s" % dtype) + if ilow >= ihigh and np.prod(size) != 0: + raise ValueError("Range cannot be empty (low >= high) unless no samples are taken") +- ++ + with self.lock: + ret = randfunc(ilow, ihigh - 1, size, self.state_address) + +@@ -4919,7 +4919,7 @@ cdef class RandomState: + return arr + + arr = np.asarray(x) +- ++ + # shuffle has fast-path for 1-d + if arr.ndim == 1: + # Return a copy if same memory +@@ -4932,7 +4932,7 @@ cdef class RandomState: + idx = np.arange(arr.shape[0], dtype=np.intp) + self.shuffle(idx) + return arr[idx] +- ++ + + _rand = RandomState() + seed = _rand.seed diff --git a/contrib/python/typing-extensions/py3/.dist-info/METADATA b/contrib/python/typing-extensions/py3/.dist-info/METADATA index f15e2b38773..4b0732f4c7f 100644 --- a/contrib/python/typing-extensions/py3/.dist-info/METADATA +++ b/contrib/python/typing-extensions/py3/.dist-info/METADATA @@ -1,15 +1,15 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: typing_extensions -Version: 4.12.2 +Version: 4.13.2 Summary: Backported and Experimental Type Hints for Python 3.8+ Keywords: annotations,backport,checker,checking,function,hinting,hints,type,typechecking,typehinting,typehints,typing Author-email: "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee" <[email protected]> Requires-Python: >=3.8 Description-Content-Type: text/markdown +License-Expression: PSF-2.0 Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: Python Software Foundation License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only @@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development +License-File: LICENSE Project-URL: Bug Tracker, https://github.com/python/typing_extensions/issues Project-URL: Changes, https://github.com/python/typing_extensions/blob/main/CHANGELOG.md Project-URL: Documentation, https://typing-extensions.readthedocs.io/ diff --git a/contrib/python/typing-extensions/py3/typing_extensions.py b/contrib/python/typing-extensions/py3/typing_extensions.py index dec429ca872..fa89c83efcd 100644 --- a/contrib/python/typing-extensions/py3/typing_extensions.py +++ b/contrib/python/typing-extensions/py3/typing_extensions.py @@ -1,9 +1,12 @@ import abc +import builtins import collections import collections.abc import contextlib +import enum import functools import inspect +import keyword import operator import sys import types as _types @@ -62,8 +65,11 @@ __all__ = [ 'dataclass_transform', 'deprecated', 'Doc', + 'evaluate_forward_ref', 'get_overloads', 'final', + 'Format', + 'get_annotations', 'get_args', 'get_origin', 'get_original_bases', @@ -83,6 +89,7 @@ __all__ = [ 'Text', 'TypeAlias', 'TypeAliasType', + 'TypeForm', 'TypeGuard', 'TypeIs', 'TYPE_CHECKING', @@ -91,6 +98,8 @@ __all__ = [ 'ReadOnly', 'Required', 'NotRequired', + 'NoDefault', + 'NoExtraItems', # Pure aliases, have always been in typing 'AbstractSet', @@ -117,7 +126,6 @@ __all__ = [ 'MutableMapping', 'MutableSequence', 'MutableSet', - 'NoDefault', 'Optional', 'Pattern', 'Reversible', @@ -138,6 +146,9 @@ PEP_560 = True GenericMeta = type _PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta") +# Added with bpo-45166 to 3.10.1+ and some 3.9 versions +_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__ + # The functions below are modified copies of typing internal helpers. # They are needed by _ProtocolMeta and they provide support for PEP 646. @@ -867,6 +878,63 @@ def _ensure_subclassable(mro_entries): return inner +_NEEDS_SINGLETONMETA = ( + not hasattr(typing, "NoDefault") or not hasattr(typing, "NoExtraItems") +) + +if _NEEDS_SINGLETONMETA: + class SingletonMeta(type): + def __setattr__(cls, attr, value): + # TypeError is consistent with the behavior of NoneType + raise TypeError( + f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}" + ) + + +if hasattr(typing, "NoDefault"): + NoDefault = typing.NoDefault +else: + class NoDefaultType(metaclass=SingletonMeta): + """The type of the NoDefault singleton.""" + + __slots__ = () + + def __new__(cls): + return globals().get("NoDefault") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoDefault" + + def __reduce__(self): + return "NoDefault" + + NoDefault = NoDefaultType() + del NoDefaultType + +if hasattr(typing, "NoExtraItems"): + NoExtraItems = typing.NoExtraItems +else: + class NoExtraItemsType(metaclass=SingletonMeta): + """The type of the NoExtraItems singleton.""" + + __slots__ = () + + def __new__(cls): + return globals().get("NoExtraItems") or object.__new__(cls) + + def __repr__(self): + return "typing_extensions.NoExtraItems" + + def __reduce__(self): + return "NoExtraItems" + + NoExtraItems = NoExtraItemsType() + del NoExtraItemsType + +if _NEEDS_SINGLETONMETA: + del SingletonMeta + + # Update this to something like >=3.13.0b1 if and when # PEP 728 is implemented in CPython _PEP_728_IMPLEMENTED = False @@ -913,7 +981,9 @@ else: break class _TypedDictMeta(type): - def __new__(cls, name, bases, ns, *, total=True, closed=False): + + def __new__(cls, name, bases, ns, *, total=True, closed=None, + extra_items=NoExtraItems): """Create new typed dict class object. This method is called when TypedDict is subclassed, @@ -925,6 +995,8 @@ else: if type(base) is not _TypedDictMeta and base is not typing.Generic: raise TypeError('cannot inherit from both a TypedDict type ' 'and a non-TypedDict base class') + if closed is not None and extra_items is not NoExtraItems: + raise TypeError(f"Cannot combine closed={closed!r} and extra_items") if any(issubclass(b, typing.Generic) for b in bases): generic_base = (typing.Generic,) @@ -964,7 +1036,7 @@ else: optional_keys = set() readonly_keys = set() mutable_keys = set() - extra_items_type = None + extra_items_type = extra_items for base in bases: base_dict = base.__dict__ @@ -974,13 +1046,12 @@ else: optional_keys.update(base_dict.get('__optional_keys__', ())) readonly_keys.update(base_dict.get('__readonly_keys__', ())) mutable_keys.update(base_dict.get('__mutable_keys__', ())) - base_extra_items_type = base_dict.get('__extra_items__', None) - if base_extra_items_type is not None: - extra_items_type = base_extra_items_type - if closed and extra_items_type is None: - extra_items_type = Never - if closed and "__extra_items__" in own_annotations: + # This was specified in an earlier version of PEP 728. Support + # is retained for backwards compatibility, but only for Python + # 3.13 and lower. + if (closed and sys.version_info < (3, 14) + and "__extra_items__" in own_annotations): annotation_type = own_annotations.pop("__extra_items__") qualifiers = set(_get_typeddict_qualifiers(annotation_type)) if Required in qualifiers: @@ -1019,8 +1090,7 @@ else: tp_dict.__optional_keys__ = frozenset(optional_keys) tp_dict.__readonly_keys__ = frozenset(readonly_keys) tp_dict.__mutable_keys__ = frozenset(mutable_keys) - if not hasattr(tp_dict, '__total__'): - tp_dict.__total__ = total + tp_dict.__total__ = total tp_dict.__closed__ = closed tp_dict.__extra_items__ = extra_items_type return tp_dict @@ -1036,7 +1106,16 @@ else: _TypedDict = type.__new__(_TypedDictMeta, 'TypedDict', (), {}) @_ensure_subclassable(lambda bases: (_TypedDict,)) - def TypedDict(typename, fields=_marker, /, *, total=True, closed=False, **kwargs): + def TypedDict( + typename, + fields=_marker, + /, + *, + total=True, + closed=None, + extra_items=NoExtraItems, + **kwargs + ): """A simple typed namespace. At runtime it is equivalent to a plain dict. TypedDict creates a dictionary type such that a type checker will expect all @@ -1096,9 +1175,14 @@ else: "using the functional syntax, pass an empty dictionary, e.g. " ) + example + "." warnings.warn(deprecation_msg, DeprecationWarning, stacklevel=2) - if closed is not False and closed is not True: + # Support a field called "closed" + if closed is not False and closed is not True and closed is not None: kwargs["closed"] = closed - closed = False + closed = None + # Or "extra_items" + if extra_items is not NoExtraItems: + kwargs["extra_items"] = extra_items + extra_items = NoExtraItems fields = kwargs elif kwargs: raise TypeError("TypedDict takes either a dict or keyword arguments," @@ -1120,7 +1204,8 @@ else: # Setting correct module is necessary to make typed dict classes pickleable. ns['__module__'] = module - td = _TypedDictMeta(typename, (), ns, total=total, closed=closed) + td = _TypedDictMeta(typename, (), ns, total=total, closed=closed, + extra_items=extra_items) td.__orig_bases__ = (TypedDict,) return td @@ -1232,10 +1317,90 @@ else: # <=3.13 ) else: # 3.8 hint = typing.get_type_hints(obj, globalns=globalns, localns=localns) + if sys.version_info < (3, 11): + _clean_optional(obj, hint, globalns, localns) + if sys.version_info < (3, 9): + # In 3.8 eval_type does not flatten Optional[ForwardRef] correctly + # This will recreate and and cache Unions. + hint = { + k: (t + if get_origin(t) != Union + else Union[t.__args__]) + for k, t in hint.items() + } if include_extras: return hint return {k: _strip_extras(t) for k, t in hint.items()} + _NoneType = type(None) + + def _could_be_inserted_optional(t): + """detects Union[..., None] pattern""" + # 3.8+ compatible checking before _UnionGenericAlias + if get_origin(t) is not Union: + return False + # Assume if last argument is not None they are user defined + if t.__args__[-1] is not _NoneType: + return False + return True + + # < 3.11 + def _clean_optional(obj, hints, globalns=None, localns=None): + # reverts injected Union[..., None] cases from typing.get_type_hints + # when a None default value is used. + # see https://github.com/python/typing_extensions/issues/310 + if not hints or isinstance(obj, type): + return + defaults = typing._get_defaults(obj) # avoid accessing __annotations___ + if not defaults: + return + original_hints = obj.__annotations__ + for name, value in hints.items(): + # Not a Union[..., None] or replacement conditions not fullfilled + if (not _could_be_inserted_optional(value) + or name not in defaults + or defaults[name] is not None + ): + continue + original_value = original_hints[name] + # value=NoneType should have caused a skip above but check for safety + if original_value is None: + original_value = _NoneType + # Forward reference + if isinstance(original_value, str): + if globalns is None: + if isinstance(obj, _types.ModuleType): + globalns = obj.__dict__ + else: + nsobj = obj + # Find globalns for the unwrapped object. + while hasattr(nsobj, '__wrapped__'): + nsobj = nsobj.__wrapped__ + globalns = getattr(nsobj, '__globals__', {}) + if localns is None: + localns = globalns + elif localns is None: + localns = globalns + if sys.version_info < (3, 9): + original_value = ForwardRef(original_value) + else: + original_value = ForwardRef( + original_value, + is_argument=not isinstance(obj, _types.ModuleType) + ) + original_evaluated = typing._eval_type(original_value, globalns, localns) + if sys.version_info < (3, 9) and get_origin(original_evaluated) is Union: + # Union[str, None, "str"] is not reduced to Union[str, None] + original_evaluated = Union[original_evaluated.__args__] + # Compare if values differ. Note that even if equal + # value might be cached by typing._tp_cache contrary to original_evaluated + if original_evaluated != value or ( + # 3.10: ForwardRefs of UnionType might be turned into _UnionGenericAlias + hasattr(_types, "UnionType") + and isinstance(original_evaluated, _types.UnionType) + and not isinstance(value, _types.UnionType) + ): + hints[name] = original_evaluated # Python 3.9+ has PEP 593 (Annotated) if hasattr(typing, 'Annotated'): @@ -1443,34 +1608,6 @@ else: ) -if hasattr(typing, "NoDefault"): - NoDefault = typing.NoDefault -else: - class NoDefaultTypeMeta(type): - def __setattr__(cls, attr, value): - # TypeError is consistent with the behavior of NoneType - raise TypeError( - f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}" - ) - - class NoDefaultType(metaclass=NoDefaultTypeMeta): - """The type of the NoDefault singleton.""" - - __slots__ = () - - def __new__(cls): - return globals().get("NoDefault") or object.__new__(cls) - - def __repr__(self): - return "typing_extensions.NoDefault" - - def __reduce__(self): - return "NoDefault" - - NoDefault = NoDefaultType() - del NoDefaultType, NoDefaultTypeMeta - - def _set_default(type_param, default): type_param.has_default = lambda: default is not NoDefault type_param.__default__ = default @@ -1761,6 +1898,23 @@ else: # 3.8-3.9 if not hasattr(typing, 'Concatenate'): # Inherits from list as a workaround for Callable checks in Python < 3.9.2. + + # 3.9.0-1 + if not hasattr(typing, '_type_convert'): + def _type_convert(arg, module=None, *, allow_special_forms=False): + """For converting None to type(None), and strings to ForwardRef.""" + if arg is None: + return type(None) + if isinstance(arg, str): + if sys.version_info <= (3, 9, 6): + return ForwardRef(arg) + if sys.version_info <= (3, 9, 7): + return ForwardRef(arg, module=module) + return ForwardRef(arg, module=module, is_class=allow_special_forms) + return arg + else: + _type_convert = typing._type_convert + class _ConcatenateGenericAlias(list): # Trick Generic into looking into this for __parameters__. @@ -1792,27 +1946,171 @@ if not hasattr(typing, 'Concatenate'): tp for tp in self.__args__ if isinstance(tp, (typing.TypeVar, ParamSpec)) ) + # 3.8; needed for typing._subst_tvars + # 3.9 used by __getitem__ below + def copy_with(self, params): + if isinstance(params[-1], _ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + elif (not (params[-1] is ... or isinstance(params[-1], ParamSpec))): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return self.__class__(self.__origin__, params) + + # 3.9; accessed during GenericAlias.__getitem__ when substituting + def __getitem__(self, args): + if self.__origin__ in (Generic, Protocol): + # Can't subscript Generic[...] or Protocol[...]. + raise TypeError(f"Cannot subscript already-subscripted {self}") + if not self.__parameters__: + raise TypeError(f"{self} is not a generic class") -# 3.8-3.9 + if not isinstance(args, tuple): + args = (args,) + args = _unpack_args(*(_type_convert(p) for p in args)) + params = self.__parameters__ + for param in params: + prepare = getattr(param, "__typing_prepare_subst__", None) + if prepare is not None: + args = prepare(self, args) + # 3.8 - 3.9 & typing.ParamSpec + elif isinstance(param, ParamSpec): + i = params.index(param) + if ( + i == len(args) + and getattr(param, '__default__', NoDefault) is not NoDefault + ): + args = [*args, param.__default__] + if i >= len(args): + raise TypeError(f"Too few arguments for {self}") + # Special case for Z[[int, str, bool]] == Z[int, str, bool] + if len(params) == 1 and not _is_param_expr(args[0]): + assert i == 0 + args = (args,) + elif ( + isinstance(args[i], list) + # 3.8 - 3.9 + # This class inherits from list do not convert + and not isinstance(args[i], _ConcatenateGenericAlias) + ): + args = (*args[:i], tuple(args[i]), *args[i + 1:]) + + alen = len(args) + plen = len(params) + if alen != plen: + raise TypeError( + f"Too {'many' if alen > plen else 'few'} arguments for {self};" + f" actual {alen}, expected {plen}" + ) + + subst = dict(zip(self.__parameters__, args)) + # determine new args + new_args = [] + for arg in self.__args__: + if isinstance(arg, type): + new_args.append(arg) + continue + if isinstance(arg, TypeVar): + arg = subst[arg] + if ( + (isinstance(arg, typing._GenericAlias) and _is_unpack(arg)) + or ( + hasattr(_types, "GenericAlias") + and isinstance(arg, _types.GenericAlias) + and getattr(arg, "__unpacked__", False) + ) + ): + raise TypeError(f"{arg} is not valid as type argument") + + elif isinstance(arg, + typing._GenericAlias + if not hasattr(_types, "GenericAlias") else + (typing._GenericAlias, _types.GenericAlias) + ): + subparams = arg.__parameters__ + if subparams: + subargs = tuple(subst[x] for x in subparams) + arg = arg[subargs] + new_args.append(arg) + return self.copy_with(tuple(new_args)) + +# 3.10+ +else: + _ConcatenateGenericAlias = typing._ConcatenateGenericAlias + + # 3.10 + if sys.version_info < (3, 11): + + class _ConcatenateGenericAlias(typing._ConcatenateGenericAlias, _root=True): + # needed for checks in collections.abc.Callable to accept this class + __module__ = "typing" + + def copy_with(self, params): + if isinstance(params[-1], (list, tuple)): + return (*params[:-1], *params[-1]) + if isinstance(params[-1], typing._ConcatenateGenericAlias): + params = (*params[:-1], *params[-1].__args__) + elif not (params[-1] is ... or isinstance(params[-1], ParamSpec)): + raise TypeError("The last parameter to Concatenate should be a " + "ParamSpec variable or ellipsis.") + return super(typing._ConcatenateGenericAlias, self).copy_with(params) + + def __getitem__(self, args): + value = super().__getitem__(args) + if isinstance(value, tuple) and any(_is_unpack(t) for t in value): + return tuple(_unpack_args(*(n for n in value))) + return value + + +# 3.8-3.9.2 +class _EllipsisDummy: ... + + +# 3.8-3.10 +def _create_concatenate_alias(origin, parameters): + if parameters[-1] is ... and sys.version_info < (3, 9, 2): + # Hack: Arguments must be types, replace it with one. + parameters = (*parameters[:-1], _EllipsisDummy) + if sys.version_info >= (3, 10, 3): + concatenate = _ConcatenateGenericAlias(origin, parameters, + _typevar_types=(TypeVar, ParamSpec), + _paramspec_tvars=True) + else: + concatenate = _ConcatenateGenericAlias(origin, parameters) + if parameters[-1] is not _EllipsisDummy: + return concatenate + # Remove dummy again + concatenate.__args__ = tuple(p if p is not _EllipsisDummy else ... + for p in concatenate.__args__) + if sys.version_info < (3, 10): + # backport needs __args__ adjustment only + return concatenate + concatenate.__parameters__ = tuple(p for p in concatenate.__parameters__ + if p is not _EllipsisDummy) + return concatenate + + +# 3.8-3.10 @typing._tp_cache def _concatenate_getitem(self, parameters): if parameters == (): raise TypeError("Cannot take a Concatenate of no types.") if not isinstance(parameters, tuple): parameters = (parameters,) - if not isinstance(parameters[-1], ParamSpec): + if not (parameters[-1] is ... or isinstance(parameters[-1], ParamSpec)): raise TypeError("The last parameter to Concatenate should be a " - "ParamSpec variable.") + "ParamSpec variable or ellipsis.") msg = "Concatenate[arg, ...]: each arg must be a type." - parameters = tuple(typing._type_check(p, msg) for p in parameters) - return _ConcatenateGenericAlias(self, parameters) + parameters = (*(typing._type_check(p, msg) for p in parameters[:-1]), + parameters[-1]) + return _create_concatenate_alias(self, parameters) -# 3.10+ -if hasattr(typing, 'Concatenate'): +# 3.11+; Concatenate does not accept ellipsis in 3.10 +if sys.version_info >= (3, 11): Concatenate = typing.Concatenate - _ConcatenateGenericAlias = typing._ConcatenateGenericAlias -# 3.9 +# 3.9-3.10 elif sys.version_info[:2] >= (3, 9): @_ExtensionsSpecialForm def Concatenate(self, parameters): @@ -1976,7 +2274,7 @@ elif sys.version_info[:2] >= (3, 9): 1. The return value is a boolean. 2. If the return value is ``True``, the type of its argument - is the intersection of the type inside ``TypeGuard`` and the argument's + is the intersection of the type inside ``TypeIs`` and the argument's previously known type. For example:: @@ -2024,7 +2322,7 @@ else: 1. The return value is a boolean. 2. If the return value is ``True``, the type of its argument - is the intersection of the type inside ``TypeGuard`` and the argument's + is the intersection of the type inside ``TypeIs`` and the argument's previously known type. For example:: @@ -2042,6 +2340,69 @@ else: PEP 742 (Narrowing types with TypeIs). """) +# 3.14+? +if hasattr(typing, 'TypeForm'): + TypeForm = typing.TypeForm +# 3.9 +elif sys.version_info[:2] >= (3, 9): + class _TypeFormForm(_ExtensionsSpecialForm, _root=True): + # TypeForm(X) is equivalent to X but indicates to the type checker + # that the object is a TypeForm. + def __call__(self, obj, /): + return obj + + @_TypeFormForm + def TypeForm(self, parameters): + """A special form representing the value that results from the evaluation + of a type expression. This value encodes the information supplied in the + type expression, and it represents the type described by that type expression. + + When used in a type expression, TypeForm describes a set of type form objects. + It accepts a single type argument, which must be a valid type expression. + ``TypeForm[T]`` describes the set of all type form objects that represent + the type T or types that are assignable to T. + + Usage: + + def cast[T](typ: TypeForm[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) +# 3.8 +else: + class _TypeFormForm(_ExtensionsSpecialForm, _root=True): + def __getitem__(self, parameters): + item = typing._type_check(parameters, + f'{self._name} accepts only a single type') + return typing._GenericAlias(self, (item,)) + + def __call__(self, obj, /): + return obj + + TypeForm = _TypeFormForm( + 'TypeForm', + doc="""A special form representing the value that results from the evaluation + of a type expression. This value encodes the information supplied in the + type expression, and it represents the type described by that type expression. + + When used in a type expression, TypeForm describes a set of type form objects. + It accepts a single type argument, which must be a valid type expression. + ``TypeForm[T]`` describes the set of all type form objects that represent + the type T or types that are assignable to T. + + Usage: + + def cast[T](typ: TypeForm[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """) + # Vendored from cpython typing._SpecialFrom class _SpecialForm(typing._Final, _root=True): @@ -2344,7 +2705,9 @@ elif sys.version_info[:2] >= (3, 9): # 3.9+ self.__doc__ = _UNPACK_DOC class _UnpackAlias(typing._GenericAlias, _root=True): - __class__ = typing.TypeVar + if sys.version_info < (3, 11): + # needed for compatibility with Generic[Unpack[Ts]] + __class__ = typing.TypeVar @property def __typing_unpacked_tuple_args__(self): @@ -2357,6 +2720,17 @@ elif sys.version_info[:2] >= (3, 9): # 3.9+ return arg.__args__ return None + @property + def __typing_is_unpacked_typevartuple__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + return isinstance(self.__args__[0], TypeVarTuple) + + def __getitem__(self, args): + if self.__typing_is_unpacked_typevartuple__: + return args + return super().__getitem__(args) + @_UnpackSpecialForm def Unpack(self, parameters): item = typing._type_check(parameters, f'{self._name} accepts only a single type.') @@ -2369,6 +2743,28 @@ else: # 3.8 class _UnpackAlias(typing._GenericAlias, _root=True): __class__ = typing.TypeVar + @property + def __typing_unpacked_tuple_args__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + arg, = self.__args__ + if isinstance(arg, typing._GenericAlias): + if arg.__origin__ is not tuple: + raise TypeError("Unpack[...] must be used with a tuple type") + return arg.__args__ + return None + + @property + def __typing_is_unpacked_typevartuple__(self): + assert self.__origin__ is Unpack + assert len(self.__args__) == 1 + return isinstance(self.__args__[0], TypeVarTuple) + + def __getitem__(self, args): + if self.__typing_is_unpacked_typevartuple__: + return args + return super().__getitem__(args) + class _UnpackForm(_ExtensionsSpecialForm, _root=True): def __getitem__(self, parameters): item = typing._type_check(parameters, @@ -2381,21 +2777,22 @@ else: # 3.8 return isinstance(obj, _UnpackAlias) +def _unpack_args(*args): + newargs = [] + for arg in args: + subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) + if subargs is not None and (not (subargs and subargs[-1] is ...)): + newargs.extend(subargs) + else: + newargs.append(arg) + return newargs + + if _PEP_696_IMPLEMENTED: from typing import TypeVarTuple elif hasattr(typing, "TypeVarTuple"): # 3.11+ - def _unpack_args(*args): - newargs = [] - for arg in args: - subargs = getattr(arg, '__typing_unpacked_tuple_args__', None) - if subargs is not None and not (subargs and subargs[-1] is ...): - newargs.extend(subargs) - else: - newargs.append(arg) - return newargs - # Add default parameter - PEP 696 class TypeVarTuple(metaclass=_TypeVarLikeMeta): """Type variable tuple.""" @@ -2726,7 +3123,8 @@ else: # <=3.11 return arg -if hasattr(warnings, "deprecated"): +# Python 3.13.3+ contains a fix for the wrapped __new__ +if sys.version_info >= (3, 13, 3): deprecated = warnings.deprecated else: _T = typing.TypeVar("_T") @@ -2806,7 +3204,7 @@ else: original_new = arg.__new__ @functools.wraps(original_new) - def __new__(cls, *args, **kwargs): + def __new__(cls, /, *args, **kwargs): if cls is arg: warnings.warn(msg, category=category, stacklevel=stacklevel + 1) if original_new is not object.__new__: @@ -2845,13 +3243,21 @@ else: __init_subclass__.__deprecated__ = msg return arg elif callable(arg): + import asyncio.coroutines import functools + import inspect @functools.wraps(arg) def wrapper(*args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel + 1) return arg(*args, **kwargs) + if asyncio.coroutines.iscoroutinefunction(arg): + if sys.version_info >= (3, 12): + wrapper = inspect.markcoroutinefunction(wrapper) + else: + wrapper._is_coroutine = asyncio.coroutines._is_coroutine + arg.__deprecated__ = wrapper.__deprecated__ = msg return wrapper else: @@ -2860,6 +3266,24 @@ else: f"a class or callable, not {arg!r}" ) +if sys.version_info < (3, 10): + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, (tuple, list, ParamSpec, _ConcatenateGenericAlias) + ) +else: + def _is_param_expr(arg): + return arg is ... or isinstance( + arg, + ( + tuple, + list, + ParamSpec, + _ConcatenateGenericAlias, + typing._ConcatenateGenericAlias, + ), + ) + # We have to do some monkey patching to deal with the dual nature of # Unpack/TypeVarTuple: @@ -2874,6 +3298,17 @@ if not hasattr(typing, "TypeVarTuple"): This gives a nice error message in case of count mismatch. """ + # If substituting a single ParamSpec with multiple arguments + # we do not check the count + if (inspect.isclass(cls) and issubclass(cls, typing.Generic) + and len(cls.__parameters__) == 1 + and isinstance(cls.__parameters__[0], ParamSpec) + and parameters + and not _is_param_expr(parameters[0]) + ): + # Generic modifies parameters variable, but here we cannot do this + return + if not elen: raise TypeError(f"{cls} is not a generic class") if elen is _marker: @@ -3007,7 +3442,10 @@ if hasattr(typing, '_collect_type_vars'): for t in types: if _is_unpacked_typevartuple(t): type_var_tuple_encountered = True - elif isinstance(t, typevar_types) and t not in tvars: + elif ( + isinstance(t, typevar_types) and not isinstance(t, _UnpackAlias) + and t not in tvars + ): if enforce_default_ordering: has_default = getattr(t, '__default__', NoDefault) is not NoDefault if has_default: @@ -3022,6 +3460,13 @@ if hasattr(typing, '_collect_type_vars'): tvars.append(t) if _should_collect_from_parameters(t): tvars.extend([t for t in t.__parameters__ if t not in tvars]) + elif isinstance(t, tuple): + # Collect nested type_vars + # tuple wrapped by _prepare_paramspec_params(cls, params) + for x in t: + for collected in _collect_type_vars([x]): + if collected not in tvars: + tvars.append(collected) return tuple(tvars) typing._collect_type_vars = _collect_type_vars @@ -3379,17 +3824,62 @@ else: return typing.Union[other, self] -if hasattr(typing, "TypeAliasType"): +if sys.version_info >= (3, 14): TypeAliasType = typing.TypeAliasType +# 3.8-3.13 else: - def _is_unionable(obj): - """Corresponds to is_unionable() in unionobject.c in CPython.""" - return obj is None or isinstance(obj, ( - type, - _types.GenericAlias, - _types.UnionType, - TypeAliasType, - )) + if sys.version_info >= (3, 12): + # 3.12-3.14 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + typing.TypeAliasType, + TypeAliasType, + )) + else: + # 3.8-3.11 + def _is_unionable(obj): + """Corresponds to is_unionable() in unionobject.c in CPython.""" + return obj is None or isinstance(obj, ( + type, + _types.GenericAlias, + _types.UnionType, + TypeAliasType, + )) + + if sys.version_info < (3, 10): + # Copied and pasted from https://github.com/python/cpython/blob/986a4e1b6fcae7fe7a1d0a26aea446107dd58dd2/Objects/genericaliasobject.c#L568-L582, + # so that we emulate the behaviour of `types.GenericAlias` + # on the latest versions of CPython + _ATTRIBUTE_DELEGATION_EXCLUSIONS = frozenset({ + "__class__", + "__bases__", + "__origin__", + "__args__", + "__unpacked__", + "__parameters__", + "__typing_unpacked_tuple_args__", + "__mro_entries__", + "__reduce_ex__", + "__reduce__", + "__copy__", + "__deepcopy__", + }) + + class _TypeAliasGenericAlias(typing._GenericAlias, _root=True): + def __getattr__(self, attr): + if attr in _ATTRIBUTE_DELEGATION_EXCLUSIONS: + return object.__getattr__(self, attr) + return getattr(self.__origin__, attr) + + if sys.version_info < (3, 9): + def __getitem__(self, item): + result = super().__getitem__(item) + result.__class__ = type(self) + return result class TypeAliasType: """Create named, parameterized type aliases. @@ -3422,11 +3912,29 @@ else: def __init__(self, name: str, value, *, type_params=()): if not isinstance(name, str): raise TypeError("TypeAliasType name must be a string") + if not isinstance(type_params, tuple): + raise TypeError("type_params must be a tuple") self.__value__ = value self.__type_params__ = type_params + default_value_encountered = False parameters = [] for type_param in type_params: + if ( + not isinstance(type_param, (TypeVar, TypeVarTuple, ParamSpec)) + # 3.8-3.11 + # Unpack Backport passes isinstance(type_param, TypeVar) + or _is_unpack(type_param) + ): + raise TypeError(f"Expected a type param, got {type_param!r}") + has_default = ( + getattr(type_param, '__default__', NoDefault) is not NoDefault + ) + if default_value_encountered and not has_default: + raise TypeError(f"non-default type parameter '{type_param!r}'" + " follows default type parameter") + if has_default: + default_value_encountered = True if isinstance(type_param, TypeVarTuple): parameters.extend(type_param) else: @@ -3463,16 +3971,49 @@ else: def __repr__(self) -> str: return self.__name__ + if sys.version_info < (3, 11): + def _check_single_param(self, param, recursion=0): + # Allow [], [int], [int, str], [int, ...], [int, T] + if param is ...: + return ... + if param is None: + return None + # Note in <= 3.9 _ConcatenateGenericAlias inherits from list + if isinstance(param, list) and recursion == 0: + return [self._check_single_param(arg, recursion+1) + for arg in param] + return typing._type_check( + param, f'Subscripting {self.__name__} requires a type.' + ) + + def _check_parameters(self, parameters): + if sys.version_info < (3, 11): + return tuple( + self._check_single_param(item) + for item in parameters + ) + return tuple(typing._type_check( + item, f'Subscripting {self.__name__} requires a type.' + ) + for item in parameters + ) + def __getitem__(self, parameters): + if not self.__type_params__: + raise TypeError("Only generic type aliases are subscriptable") if not isinstance(parameters, tuple): parameters = (parameters,) - parameters = [ - typing._type_check( - item, f'Subscripting {self.__name__} requires a type.' - ) - for item in parameters - ] - return typing._GenericAlias(self, tuple(parameters)) + # Using 3.9 here will create problems with Concatenate + if sys.version_info >= (3, 10): + return _types.GenericAlias(self, parameters) + type_vars = _collect_type_vars(parameters) + parameters = self._check_parameters(parameters) + alias = _TypeAliasGenericAlias(self, parameters) + # alias.__parameters__ is not complete if Concatenate is present + # as it is converted to a list from which no parameters are extracted. + if alias.__parameters__ != type_vars: + alias.__parameters__ = type_vars + return alias def __reduce__(self): return self.__name__ @@ -3599,6 +4140,408 @@ if _CapsuleType is not None: __all__.append("CapsuleType") +# Using this convoluted approach so that this keeps working +# whether we end up using PEP 649 as written, PEP 749, or +# some other variation: in any case, inspect.get_annotations +# will continue to exist and will gain a `format` parameter. +_PEP_649_OR_749_IMPLEMENTED = ( + hasattr(inspect, 'get_annotations') + and inspect.get_annotations.__kwdefaults__ is not None + and "format" in inspect.get_annotations.__kwdefaults__ +) + + +class Format(enum.IntEnum): + VALUE = 1 + FORWARDREF = 2 + STRING = 3 + + +if _PEP_649_OR_749_IMPLEMENTED: + get_annotations = inspect.get_annotations +else: + def get_annotations(obj, *, globals=None, locals=None, eval_str=False, + format=Format.VALUE): + """Compute the annotations dict for an object. + + obj may be a callable, class, or module. + Passing in an object of any other type raises TypeError. + + Returns a dict. get_annotations() returns a new dict every time + it's called; calling it twice on the same object will return two + different but equivalent dicts. + + This is a backport of `inspect.get_annotations`, which has been + in the standard library since Python 3.10. See the standard library + documentation for more: + + https://docs.python.org/3/library/inspect.html#inspect.get_annotations + + This backport adds the *format* argument introduced by PEP 649. The + three formats supported are: + * VALUE: the annotations are returned as-is. This is the default and + it is compatible with the behavior on previous Python versions. + * FORWARDREF: return annotations as-is if possible, but replace any + undefined names with ForwardRef objects. The implementation proposed by + PEP 649 relies on language changes that cannot be backported; the + typing-extensions implementation simply returns the same result as VALUE. + * STRING: return annotations as strings, in a format close to the original + source. Again, this behavior cannot be replicated directly in a backport. + As an approximation, typing-extensions retrieves the annotations under + VALUE semantics and then stringifies them. + + The purpose of this backport is to allow users who would like to use + FORWARDREF or STRING semantics once PEP 649 is implemented, but who also + want to support earlier Python versions, to simply write: + + typing_extensions.get_annotations(obj, format=Format.FORWARDREF) + + """ + format = Format(format) + + if eval_str and format is not Format.VALUE: + raise ValueError("eval_str=True is only supported with format=Format.VALUE") + + if isinstance(obj, type): + # class + obj_dict = getattr(obj, '__dict__', None) + if obj_dict and hasattr(obj_dict, 'get'): + ann = obj_dict.get('__annotations__', None) + if isinstance(ann, _types.GetSetDescriptorType): + ann = None + else: + ann = None + + obj_globals = None + module_name = getattr(obj, '__module__', None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + obj_globals = getattr(module, '__dict__', None) + obj_locals = dict(vars(obj)) + unwrap = obj + elif isinstance(obj, _types.ModuleType): + # module + ann = getattr(obj, '__annotations__', None) + obj_globals = obj.__dict__ + obj_locals = None + unwrap = None + elif callable(obj): + # this includes types.Function, types.BuiltinFunctionType, + # types.BuiltinMethodType, functools.partial, functools.singledispatch, + # "class funclike" from Lib/test/test_inspect... on and on it goes. + ann = getattr(obj, '__annotations__', None) + obj_globals = getattr(obj, '__globals__', None) + obj_locals = None + unwrap = obj + elif hasattr(obj, '__annotations__'): + ann = obj.__annotations__ + obj_globals = obj_locals = unwrap = None + else: + raise TypeError(f"{obj!r} is not a module, class, or callable.") + + if ann is None: + return {} + + if not isinstance(ann, dict): + raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None") + + if not ann: + return {} + + if not eval_str: + if format is Format.STRING: + return { + key: value if isinstance(value, str) else typing._type_repr(value) + for key, value in ann.items() + } + return dict(ann) + + if unwrap is not None: + while True: + if hasattr(unwrap, '__wrapped__'): + unwrap = unwrap.__wrapped__ + continue + if isinstance(unwrap, functools.partial): + unwrap = unwrap.func + continue + break + if hasattr(unwrap, "__globals__"): + obj_globals = unwrap.__globals__ + + if globals is None: + globals = obj_globals + if locals is None: + locals = obj_locals or {} + + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + if type_params := getattr(obj, "__type_params__", ()): + locals = {param.__name__: param for param in type_params} | locals + + return_value = {key: + value if not isinstance(value, str) else eval(value, globals, locals) + for key, value in ann.items() } + return return_value + + +if hasattr(typing, "evaluate_forward_ref"): + evaluate_forward_ref = typing.evaluate_forward_ref +else: + # Implements annotationlib.ForwardRef.evaluate + def _eval_with_owner( + forward_ref, *, owner=None, globals=None, locals=None, type_params=None + ): + if forward_ref.__forward_evaluated__: + return forward_ref.__forward_value__ + if getattr(forward_ref, "__cell__", None) is not None: + try: + value = forward_ref.__cell__.cell_contents + except ValueError: + pass + else: + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + if owner is None: + owner = getattr(forward_ref, "__owner__", None) + + if ( + globals is None + and getattr(forward_ref, "__forward_module__", None) is not None + ): + globals = getattr( + sys.modules.get(forward_ref.__forward_module__, None), "__dict__", None + ) + if globals is None: + globals = getattr(forward_ref, "__globals__", None) + if globals is None: + if isinstance(owner, type): + module_name = getattr(owner, "__module__", None) + if module_name: + module = sys.modules.get(module_name, None) + if module: + globals = getattr(module, "__dict__", None) + elif isinstance(owner, _types.ModuleType): + globals = getattr(owner, "__dict__", None) + elif callable(owner): + globals = getattr(owner, "__globals__", None) + + # If we pass None to eval() below, the globals of this module are used. + if globals is None: + globals = {} + + if locals is None: + locals = {} + if isinstance(owner, type): + locals.update(vars(owner)) + + if type_params is None and owner is not None: + # "Inject" type parameters into the local namespace + # (unless they are shadowed by assignments *in* the local namespace), + # as a way of emulating annotation scopes when calling `eval()` + type_params = getattr(owner, "__type_params__", None) + + # type parameters require some special handling, + # as they exist in their own scope + # but `eval()` does not have a dedicated parameter for that scope. + # For classes, names in type parameter scopes should override + # names in the global scope (which here are called `localns`!), + # but should in turn be overridden by names in the class scope + # (which here are called `globalns`!) + if type_params is not None: + globals = dict(globals) + locals = dict(locals) + for param in type_params: + param_name = param.__name__ + if ( + _FORWARD_REF_HAS_CLASS and not forward_ref.__forward_is_class__ + ) or param_name not in globals: + globals[param_name] = param + locals.pop(param_name, None) + + arg = forward_ref.__forward_arg__ + if arg.isidentifier() and not keyword.iskeyword(arg): + if arg in locals: + value = locals[arg] + elif arg in globals: + value = globals[arg] + elif hasattr(builtins, arg): + return getattr(builtins, arg) + else: + raise NameError(arg) + else: + code = forward_ref.__forward_code__ + value = eval(code, globals, locals) + forward_ref.__forward_evaluated__ = True + forward_ref.__forward_value__ = value + return value + + def _lax_type_check( + value, msg, is_argument=True, *, module=None, allow_special_forms=False + ): + """ + A lax Python 3.11+ like version of typing._type_check + """ + if hasattr(typing, "_type_convert"): + if ( + sys.version_info >= (3, 10, 3) + or (3, 9, 10) < sys.version_info[:3] < (3, 10) + ): + # allow_special_forms introduced later cpython/#30926 (bpo-46539) + type_ = typing._type_convert( + value, + module=module, + allow_special_forms=allow_special_forms, + ) + # module was added with bpo-41249 before is_class (bpo-46539) + elif "__forward_module__" in typing.ForwardRef.__slots__: + type_ = typing._type_convert(value, module=module) + else: + type_ = typing._type_convert(value) + else: + if value is None: + return type(None) + if isinstance(value, str): + return ForwardRef(value) + type_ = value + invalid_generic_forms = (Generic, Protocol) + if not allow_special_forms: + invalid_generic_forms += (ClassVar,) + if is_argument: + invalid_generic_forms += (Final,) + if ( + isinstance(type_, typing._GenericAlias) + and get_origin(type_) in invalid_generic_forms + ): + raise TypeError(f"{type_} is not valid as type argument") from None + if type_ in (Any, LiteralString, NoReturn, Never, Self, TypeAlias): + return type_ + if allow_special_forms and type_ in (ClassVar, Final): + return type_ + if ( + isinstance(type_, (_SpecialForm, typing._SpecialForm)) + or type_ in (Generic, Protocol) + ): + raise TypeError(f"Plain {type_} is not valid as type argument") from None + if type(type_) is tuple: # lax version with tuple instead of callable + raise TypeError(f"{msg} Got {type_!r:.100}.") + return type_ + + def evaluate_forward_ref( + forward_ref, + *, + owner=None, + globals=None, + locals=None, + type_params=None, + format=Format.VALUE, + _recursive_guard=frozenset(), + ): + """Evaluate a forward reference as a type hint. + + This is similar to calling the ForwardRef.evaluate() method, + but unlike that method, evaluate_forward_ref() also: + + * Recursively evaluates forward references nested within the type hint. + * Rejects certain objects that are not valid type hints. + * Replaces type hints that evaluate to None with types.NoneType. + * Supports the *FORWARDREF* and *STRING* formats. + + *forward_ref* must be an instance of ForwardRef. *owner*, if given, + should be the object that holds the annotations that the forward reference + derived from, such as a module, class object, or function. It is used to + infer the namespaces to use for looking up names. *globals* and *locals* + can also be explicitly given to provide the global and local namespaces. + *type_params* is a tuple of type parameters that are in scope when + evaluating the forward reference. This parameter must be provided (though + it may be an empty tuple) if *owner* is not given and the forward reference + does not already have an owner set. *format* specifies the format of the + annotation and is a member of the annotationlib.Format enum. + + """ + if format == Format.STRING: + return forward_ref.__forward_arg__ + if forward_ref.__forward_arg__ in _recursive_guard: + return forward_ref + + # Evaluate the forward reference + try: + value = _eval_with_owner( + forward_ref, + owner=owner, + globals=globals, + locals=locals, + type_params=type_params, + ) + except NameError: + if format == Format.FORWARDREF: + return forward_ref + else: + raise + + msg = "Forward references must evaluate to types." + if not _FORWARD_REF_HAS_CLASS: + allow_special_forms = not forward_ref.__forward_is_argument__ + else: + allow_special_forms = forward_ref.__forward_is_class__ + type_ = _lax_type_check( + value, + msg, + is_argument=forward_ref.__forward_is_argument__, + allow_special_forms=allow_special_forms, + ) + + # Recursively evaluate the type + if isinstance(type_, ForwardRef): + if getattr(type_, "__forward_module__", True) is not None: + globals = None + return evaluate_forward_ref( + type_, + globals=globals, + locals=locals, + type_params=type_params, owner=owner, + _recursive_guard=_recursive_guard, format=format + ) + if sys.version_info < (3, 12, 5) and type_params: + # Make use of type_params + locals = dict(locals) if locals else {} + for tvar in type_params: + if tvar.__name__ not in locals: # lets not overwrite something present + locals[tvar.__name__] = tvar + if sys.version_info < (3, 9): + return typing._eval_type( + type_, + globals, + locals, + ) + if sys.version_info < (3, 12, 5): + return typing._eval_type( + type_, + globals, + locals, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + if sys.version_info < (3, 14): + return typing._eval_type( + type_, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + ) + return typing._eval_type( + type_, + globals, + locals, + type_params, + recursive_guard=_recursive_guard | {forward_ref.__forward_arg__}, + format=format, + owner=owner, + ) + + # Aliases for items that have always been in typing. # Explicitly assign these (rather than using `from typing import *` at the top), # so that we get a CI error if one of these is deleted from typing.py diff --git a/contrib/python/typing-extensions/py3/ya.make b/contrib/python/typing-extensions/py3/ya.make index 69f363f940f..bcd18d9ad38 100644 --- a/contrib/python/typing-extensions/py3/ya.make +++ b/contrib/python/typing-extensions/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(4.12.2) +VERSION(4.13.2) LICENSE(PSF-2.0) diff --git a/contrib/python/ydb/py3/.dist-info/METADATA b/contrib/python/ydb/py3/.dist-info/METADATA index aa485a3f4c0..3299a5e023b 100644 --- a/contrib/python/ydb/py3/.dist-info/METADATA +++ b/contrib/python/ydb/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: ydb -Version: 3.21.1 +Version: 3.21.2 Summary: YDB Python SDK Home-page: http://github.com/ydb-platform/ydb-python-sdk Author: Yandex LLC @@ -65,3 +65,7 @@ Install YDB python sdk: ```sh $ python -m pip install ydb ``` + +## Development + +Instructions on `ydb-python-sdk` development are located in [BUILD.md](BUILD.md). diff --git a/contrib/python/ydb/py3/README.md b/contrib/python/ydb/py3/README.md index 4cde7ff5bca..7842bd1d910 100644 --- a/contrib/python/ydb/py3/README.md +++ b/contrib/python/ydb/py3/README.md @@ -42,3 +42,7 @@ Install YDB python sdk: ```sh $ python -m pip install ydb ``` + +## Development + +Instructions on `ydb-python-sdk` development are located in [BUILD.md](BUILD.md). diff --git a/contrib/python/ydb/py3/ya.make b/contrib/python/ydb/py3/ya.make index 5f37e78726e..bfb67528cff 100644 --- a/contrib/python/ydb/py3/ya.make +++ b/contrib/python/ydb/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(3.21.1) +VERSION(3.21.2) LICENSE(Apache-2.0) diff --git a/contrib/python/ydb/py3/ydb/aio/pool.py b/contrib/python/ydb/py3/ydb/aio/pool.py index c8fbb9047e6..99a3cfdb8da 100644 --- a/contrib/python/ydb/py3/ydb/aio/pool.py +++ b/contrib/python/ydb/py3/ydb/aio/pool.py @@ -199,12 +199,27 @@ class ConnectionPool(IConnectionPool): self._store = ConnectionsCache(driver_config.use_all_nodes) self._grpc_init = Connection(self._driver_config.endpoint, self._driver_config) self._stopped = False - self._discovery = Discovery(self._store, self._driver_config) - self._discovery_task = asyncio.get_event_loop().create_task(self._discovery.run()) + if driver_config.disable_discovery: + # If discovery is disabled, just add the initial endpoint to the store + async def init_connection(): + ready_connection = Connection(self._driver_config.endpoint, self._driver_config) + await ready_connection.connection_ready( + ready_timeout=getattr(self._driver_config, "discovery_request_timeout", 10) + ) + self._store.add(ready_connection) + + # Create and schedule the task to initialize the connection + self._discovery = None + self._discovery_task = asyncio.get_event_loop().create_task(init_connection()) + else: + # Start discovery as usual + self._discovery = Discovery(self._store, self._driver_config) + self._discovery_task = asyncio.get_event_loop().create_task(self._discovery.run()) async def stop(self, timeout=10): - self._discovery.stop() + if self._discovery: + self._discovery.stop() await self._grpc_init.close() try: await asyncio.wait_for(self._discovery_task, timeout=timeout) @@ -215,7 +230,8 @@ class ConnectionPool(IConnectionPool): def _on_disconnected(self, connection): async def __wrapper__(): await connection.close() - self._discovery.notify_disconnected() + if self._discovery: + self._discovery.notify_disconnected() return __wrapper__ @@ -223,7 +239,9 @@ class ConnectionPool(IConnectionPool): await self._store.get(fast_fail=fail_fast, wait_timeout=timeout) def discovery_debug_details(self): - return self._discovery.discovery_debug_details() + if self._discovery: + return self._discovery.discovery_debug_details() + return "Discovery is disabled, using only the initial endpoint" async def __aenter__(self): return self @@ -248,7 +266,8 @@ class ConnectionPool(IConnectionPool): try: connection = await self._store.get(preferred_endpoint, fast_fail=fast_fail, wait_timeout=wait_timeout) except BaseException: - self._discovery.notify_disconnected() + if self._discovery: + self._discovery.notify_disconnected() raise return await connection( diff --git a/contrib/python/ydb/py3/ydb/driver.py b/contrib/python/ydb/py3/ydb/driver.py index 3998aeeef5f..09d531d0ddf 100644 --- a/contrib/python/ydb/py3/ydb/driver.py +++ b/contrib/python/ydb/py3/ydb/driver.py @@ -96,6 +96,7 @@ class DriverConfig(object): "grpc_lb_policy_name", "discovery_request_timeout", "compression", + "disable_discovery", ) def __init__( @@ -120,6 +121,7 @@ class DriverConfig(object): grpc_lb_policy_name="round_robin", discovery_request_timeout=10, compression=None, + disable_discovery=False, ): """ A driver config to initialize a driver instance @@ -140,6 +142,7 @@ class DriverConfig(object): If tracing aio ScopeManager must be ContextVarsScopeManager :param grpc_lb_policy_name: A load balancing policy to be used for discovery channel construction. Default value is `round_round` :param discovery_request_timeout: A default timeout to complete the discovery. The default value is 10 seconds. + :param disable_discovery: If True, endpoint discovery is disabled and only the start endpoint is used for all requests. """ self.endpoint = endpoint @@ -167,6 +170,7 @@ class DriverConfig(object): self.grpc_lb_policy_name = grpc_lb_policy_name self.discovery_request_timeout = discovery_request_timeout self.compression = compression + self.disable_discovery = disable_discovery def set_database(self, database): self.database = database diff --git a/contrib/python/ydb/py3/ydb/pool.py b/contrib/python/ydb/py3/ydb/pool.py index 1e75950ea84..476ea674d9f 100644 --- a/contrib/python/ydb/py3/ydb/pool.py +++ b/contrib/python/ydb/py3/ydb/pool.py @@ -350,8 +350,21 @@ class ConnectionPool(IConnectionPool): self._store = ConnectionsCache(driver_config.use_all_nodes, driver_config.tracer) self.tracer = driver_config.tracer self._grpc_init = connection_impl.Connection(self._driver_config.endpoint, self._driver_config) - self._discovery_thread = Discovery(self._store, self._driver_config) - self._discovery_thread.start() + + if driver_config.disable_discovery: + # If discovery is disabled, just add the initial endpoint to the store + ready_connection = connection_impl.Connection.ready_factory( + self._driver_config.endpoint, + self._driver_config, + ready_timeout=getattr(self._driver_config, "discovery_request_timeout", 10), + ) + self._store.add(ready_connection) + self._discovery_thread = None + else: + # Start discovery thread as usual + self._discovery_thread = Discovery(self._store, self._driver_config) + self._discovery_thread.start() + self._stopped = False self._stop_guard = threading.Lock() @@ -367,9 +380,11 @@ class ConnectionPool(IConnectionPool): return self._stopped = True - self._discovery_thread.stop() + if self._discovery_thread: + self._discovery_thread.stop() self._grpc_init.close() - self._discovery_thread.join(timeout) + if self._discovery_thread: + self._discovery_thread.join(timeout) def async_wait(self, fail_fast=False): """ @@ -404,7 +419,13 @@ class ConnectionPool(IConnectionPool): self._discovery_thread.notify_disconnected() def discovery_debug_details(self): - return self._discovery_thread.discovery_debug_details() + """ + Returns debug string about last errors + :return: str + """ + if self._discovery_thread: + return self._discovery_thread.discovery_debug_details() + return "Discovery is disabled, using only the initial endpoint" @tracing.with_trace() def __call__( diff --git a/contrib/python/ydb/py3/ydb/ydb_version.py b/contrib/python/ydb/py3/ydb/ydb_version.py index 3c62627bc85..62af5284cef 100644 --- a/contrib/python/ydb/py3/ydb/ydb_version.py +++ b/contrib/python/ydb/py3/ydb/ydb_version.py @@ -1 +1 @@ -VERSION = "3.21.1" +VERSION = "3.21.2" |
