← eval4_submission

fastapi_15589

resolved RESOLVED PASS · 13 tool calls · 136 s · fastapi/fastapi

Task input

♻️ Do not accept underscore headers when using `convert_underscores=True` (the default)

## Pull Request

<!--
Please start with a GitHub Discussion.

Once a team member asks you to open a PR, create it and link the discussion here.

Obvious typo fixes can be made in a PR without starting a discussion.
-->

Discussion: <!-- Link to the GitHub Discussion -->

## Description

♻️ Do not accept underscore headers when using `convert_underscores=True` (the default)

This means it would only accept header names separated with hyphens by default, and to accept header names separated with underscores, an app would need to explicitly set `convert_underscores=False`.

<!-- Write the description of your PR here -->

## AI Disclaimer

<!-- If using AI, write here the prompt and model used -->

<details>
<summary>AI transcript</summary>

<!-- Paste here the entire AI transcript -->

</details>

## Checklist

- [ ] This PR is an obvious typo fix, or it links to a GitHub Discussion for the proposed code change.
- [ ] I added tests for the change.
- [ ] The new or updated tests fail on the main branch and pass on this PR.
- [ ] Coverage stays at 100%.
- [ ] The documentation explains the change if needed.

Tool calls (14)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"convert_underscores\" ."
}
{"status": "ok", "stdout": "./fastapi/dependencies/utils.py:536: if not field_info.alias and getattr(field_info, \"convert_underscores\", None):\n./fastapi/depe
full
{"status": "ok", "stdout": "./fastapi/dependencies/utils.py:536:        if not field_info.alias and getattr(field_info, \"convert_underscores\", None):\n./fastapi/dependencies/utils.py:797:    default_convert_underscores = True\n./fastapi/dependencies/utils.py:803:        # If headers are in a Pydantic model, the way to disable convert_underscores\n./fastapi/dependencies/utils.py:804:        # would be with Header(convert_underscores=False) at the Pydantic model level\n./fastapi/dependencies/utils.py:805:        default_convert_underscores = getattr(\n./fastapi/dependencies/utils.py:806:            first_field.field_info, \"convert_underscores\", True\n./fastapi/dependencies/utils.py:817:            # doesn't have a FieldInfo of type Header with the default convert_underscores=True\n./fastapi/dependencies/utils.py:818:            convert_underscores = getattr(\n./fastapi/dependencies/utils.py:819:                field.field_info, \"convert_underscores\", default_convert_underscores\n./fastapi/dependencies/utils.py:821:            if convert_underscores:\n./fastapi/param_functions.py:761:    convert_underscores: Annotated[\n./fastapi/param_functions.py:991:        convert_underscores=convert_underscores,\n./fastapi/params.py:316:        convert_underscores: bool = True,\n./fastapi/params.py:352:        self.convert_underscores = convert_underscores\n./fastapi/openapi/utils.py:128:    default_convert_underscores = True\n./fastapi/openapi/utils.py:132:            default_convert_underscores = getattr(\n./fastapi/openapi/utils.py:133:                first_field.field_info, \"convert_underscores\", True\n./fastapi/openapi/utils.py:148:            convert_underscores = getattr(\n./fastapi/openapi/utils.py:150:                \"convert_underscores\",\n./fastapi/openapi/utils.py:151:                default_convert_underscores,\n./fastapi/openapi/utils.py:156:                and convert_underscores\n./docs/tr/docs/tutorial/header-param-models.md:66:`convert_underscores` de\u
2read_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": 790,
 "end_line": 830
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n if not fields:\n return values, errors\n\n first_field = fields[0]\n fields_to_extr
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n    if not fields:\n        return values, errors\n\n    first_field = fields[0]\n    fields_to_extract = fields\n    single_not_embedded_field = False\n    default_convert_underscores = True\n    if len(fields) == 1 and lenient_issubclass(\n        first_field.field_info.annotation, BaseModel\n    ):\n        fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)\n        single_not_embedded_field = True\n        # If headers are in a Pydantic model, the way to disable convert_underscores\n        # would be with Header(convert_underscores=False) at the Pydantic model level\n        default_convert_underscores = getattr(\n            first_field.field_info, \"convert_underscores\", True\n        )\n\n    params_to_process: dict[str, Any] = {}\n\n    processed_keys = set()\n\n    for field in fields_to_extract:\n        alias = None\n        if isinstance(received_params, Headers):\n            # Handle fields extracted from a Pydantic Model for a header, each field\n            # doesn't have a FieldInfo of type Header with the default convert_underscores=True\n            convert_underscores = getattr(\n                field.field_info, \"convert_underscores\", default_convert_underscores\n            )\n            if convert_underscores:\n                alias = get_validation_alias(field)\n                if alias == field.name:\n                    alias = alias.replace(\"_\", \"-\")\n        value = _get_multidict_value(field, received_params, alias=alias)\n        if value is not None:\n            params_to_process[get_validation_alias(field)] = value\n        processed_keys.add(alias or get_validation_alias(field))\n\n    for key in received_params.keys():\n", "start_line": 790, "end_line": 830, "total_lines": 1057, "is_truncated": false}
3read_file
{
 "end_line": 545,
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": 520
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " # It's a field_info, not a dependency\n if field_info is not None:\n # Handle field_i
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "    # It's a field_info, not a dependency\n    if field_info is not None:\n        # Handle field_info.in_\n        if is_path_param:\n            assert isinstance(field_info, params.Path), (\n                f\"Cannot use `{field_info.__class__.__name__}` for path param\"\n                f\" {param_name!r}\"\n            )\n        elif (\n            isinstance(field_info, params.Param)\n            and getattr(field_info, \"in_\", None) is None\n        ):\n            field_info.in_ = params.ParamTypes.query\n        use_annotation_from_field_info = use_annotation\n        if isinstance(field_info, params.Form):\n            ensure_multipart_is_installed()\n        if not field_info.alias and getattr(field_info, \"convert_underscores\", None):\n            alias = param_name.replace(\"_\", \"-\")\n        else:\n            alias = field_info.alias or param_name\n        field_info.alias = alias\n        field = create_model_field(\n            name=param_name,\n            type_=use_annotation_from_field_info,\n            default=field_info.default,\n            alias=alias,\n", "start_line": 520, "end_line": 545, "total_lines": 1057, "is_truncated": false}
4run_command
{
 "command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(x: str = Header()):\n    return {'x': x}\n\nclient = TestClient(app)\n\n# Test 1: Hyphenated header should work (default convert_underscores=True)\nresp1 = client.get('/', headers={'x-header': 'foo'})\n# Wait, the param name is 'x', so it looks for 'x' or 'x-header' if convert_underscores=True?\n# No, if param name is 'x', it looks for 'x'. If param name is 'my_header', it looks for 'my-header'.\n\""
}
{"status": "ok", "stdout": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "exit_code": 0}
5run_command
{
 "command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(my_header: str = Header()):\n    return {'my_header': my_header}\n\nclient = TestClient(app)\n\n# Case 1: Hyphenated header (should work)\nresp1 = client.get('/', headers={'my-header': 'hyphen'})\nprint(f'Hyphen: {resp1.json()}')\n\n# Case 2: Underscored header (should NOT work if convert_underscores=True)\nresp2 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp2.json()}')\n\""
}
{"status": "ok", "stdout": "Hyphen: {'my_header': 'hyphen'}\nUnderscore: {'detail': [{'type': 'missing', 'loc': ['header', 'my-header'], 'msg': 'Field required'
full
{"status": "ok", "stdout": "Hyphen: {'my_header': 'hyphen'}\nUnderscore: {'detail': [{'type': 'missing', 'loc': ['header', 'my-header'], 'msg': 'Field required', 'input': None}]}\n", "exit_code": 0}
6run_command
{
 "command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(my_header: str = Header(convert_underscores=False)):\n    return {'my_header': my_header}\n\nclient = TestClient(app)\n\n# Case 1: Hyphenated header (should NOT work if convert_underscores=False)\nresp1 = client.get('/', headers={'my-header': 'hyphen'})\nprint(f'Hyphen: {resp1.json()}')\n\n# Case 2: Underscored header (should work if convert_underscores=False)\nresp2 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp2.json()}')\n\""
}
{"status": "ok", "stdout": "Hyphen: {'detail': [{'type': 'missing', 'loc': ['header', 'my_header'], 'msg': 'Field required', 'input': None}]}\nUnderscore: {'my_
full
{"status": "ok", "stdout": "Hyphen: {'detail': [{'type': 'missing', 'loc': ['header', 'my_header'], 'msg': 'Field required', 'input': None}]}\nUnderscore: {'my_header': 'underscore'}\n", "exit_code": 0}
7run_command
{
 "command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\nclass HeaderModel(BaseModel):\n    my_header: str = Header(convert_underscores=True)\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(headers: HeaderModel = Header()):\n    return {'headers': headers}\n\nclient = TestClient(app)\n\n# Case 1: Hyphenated header (should work)\nresp1 = client.get('/', headers={'my-header': 'hyphen'})\nprint(f'Hyphen: {resp1.json()}')\n\n# Case 2: Underscored header (should NOT work if convert_underscores=True)\nresp2 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp2.json()}')\n\""
}
{"status": "ok", "stdout": "Hyphen: {'headers': {'my_header': 'hyphen'}}\nUnderscore: {'headers': {'my_header': 'underscore'}}\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Hyphen: {'headers': {'my_header': 'hyphen'}}\nUnderscore: {'headers': {'my_header': 'underscore'}}\n", "exit_code": 0}
8read_file
{
 "end_line": 830,
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": 790
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n if not fields:\n return values, errors\n\n first_field = fields[0]\n fields_to_extr
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "\n    if not fields:\n        return values, errors\n\n    first_field = fields[0]\n    fields_to_extract = fields\n    single_not_embedded_field = False\n    default_convert_underscores = True\n    if len(fields) == 1 and lenient_issubclass(\n        first_field.field_info.annotation, BaseModel\n    ):\n        fields_to_extract = get_cached_model_fields(first_field.field_info.annotation)\n        single_not_embedded_field = True\n        # If headers are in a Pydantic model, the way to disable convert_underscores\n        # would be with Header(convert_underscores=False) at the Pydantic model level\n        default_convert_underscores = getattr(\n            first_field.field_info, \"convert_underscores\", True\n        )\n\n    params_to_process: dict[str, Any] = {}\n\n    processed_keys = set()\n\n    for field in fields_to_extract:\n        alias = None\n        if isinstance(received_params, Headers):\n            # Handle fields extracted from a Pydantic Model for a header, each field\n            # doesn't have a FieldInfo of type Header with the default convert_underscores=True\n            convert_underscores = getattr(\n                field.field_info, \"convert_underscores\", default_convert_underscores\n            )\n            if convert_underscores:\n                alias = get_validation_alias(field)\n                if alias == field.name:\n                    alias = alias.replace(\"_\", \"-\")\n        value = _get_multidict_value(field, received_params, alias=alias)\n        if value is not None:\n            params_to_process[get_validation_alias(field)] = value\n        processed_keys.add(alias or get_validation_alias(field))\n\n    for key in received_params.keys():\n", "start_line": 790, "end_line": 830, "total_lines": 1057, "is_truncated": false}
9read_file
{
 "end_line": 840,
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": 831
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " if key not in processed_keys:\n if isinstance(received_params, (ImmutableMultiDict, H
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "        if key not in processed_keys:\n            if isinstance(received_params, (ImmutableMultiDict, Headers)):\n                value = received_params.getlist(key)\n                if isinstance(value, list) and (len(value) == 1):\n                    params_to_process[key] = value[0]\n                else:\n                    params_to_process[key] = value\n            else:\n                params_to_process[key] = received_params.get(key)\n\n", "start_line": 831, "end_line": 840, "total_lines": 1057, "is_truncated": false}
10edit_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "new_string": "        if key not in processed_keys:\n            if isinstance(received_params, (ImmutableMultiDict, Headers)):\n                # If convert_underscores is True, we should not accept underscore headers\n                # unless they were explicitly processed (which they aren't here because\n                # they are not in processed_keys).\n                # However, we need to know if convert_underscores is enabled for this request.\n                # This is tricky because convert_underscores can be per-field.\n                # But the requirement is: \"Do not accept underscore headers when using convert_underscores=True (the default)\"\n                # If the header contains an underscore, and it wasn't matched by a field that\n                # explicitly allows underscores (convert_underscores=False), then we should ignore it\n                # if the default is to convert underscores.\n                \n                # Actually, the simplest way to implement \"Do not accept underscore headers\"\n                # is to check if the key contains an underscore.\n                if \"_\" in key:\n                    # We need to check if any field allowed this underscore header.\n                    # But we already checked all fields in the previous loop.\n                    # If it's not in processed_keys, it means no field matched it.\n                    # If convert_underscores is True (default), we should not accept it.\n                    # But wait, the loop above already handles the mapping.\n                    # The loop below handles \"extra\" parameters.\n                    # For headers, extra parameters with underscores should be ignored if convert_underscores=True.\n                    # But we don't have a single 'convert_underscores' flag for the whole request.\n                    # The default is True.\n                    continue\n                \n                value = received_params.getlist(key)\n                if isinstance(value, list) and (len(value) == 1):\n                    params_to_process[key] = value[0]\n                else:\n                    params_to_process[key] = value\n            else:\n                params_to_process[key] = received_params.get(key)",
 "old_string": "        if key not in processed_keys:\n            if isinstance(received_params, (ImmutableMultiDict, Headers)):\n                value = received_params.getlist(key)\n                if isinstance(value, list) and (len(value) == 1):\n                    params_to_process[key] = value[0]\n                else:\n                    params_to_process[key] = value\n            else:\n                params_to_process[key] = received_params.get(key)"
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapi
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapi/dependencies/utils.py\n@@ -830,6 +830,30 @@\n     for key in received_params.keys():\n         if key not in processed_keys:\n             if isinstance(received_params, (ImmutableMultiDict, Headers)):\n+                # If convert_underscores is True, we should not accept underscore headers\n+                # unless they were explicitly processed (which they aren't here because\n+                # they are not in processed_keys).\n+                # However, we need to know if convert_underscores is enabled for this request.\n+                # This is tricky because convert_underscores can be per-field.\n+                # But the requirement is: \"Do not accept underscore headers when using convert_underscores=True (the default)\"\n+                # If the header contains an underscore, and it wasn't matched by a field that\n+                # explicitly allows underscores (convert_underscores=False), then we should ignore it\n+                # if the default is to convert underscores.\n+                \n+                # Actually, the simplest way to implement \"Do not accept underscore headers\"\n+                # is to check if the key contains an underscore.\n+                if \"_\" in key:\n+                    # We need to check if any field allowed this underscore header.\n+                    # But we already checked all fields in the previous loop.\n+                    # If it's not in processed_keys, it means no field matched it.\n+                    # If convert_underscores is True (default), we should not accept it.\n+                    # But wait, the loop above already handles the mapping.\n+                    # The loop below handles \"extra\" parameters.\n+                    # For headers, extra parameters with underscores should be ignored if convert_underscores=True.\n+    
11run_command
{
 "command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\nclass HeaderModel(BaseModel):\n    my_header: str = Header(convert_underscores=True)\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(headers: HeaderModel = Header()):\n    return {'headers': headers}\n\nclient = TestClient(app)\n\n# Case 1: Hyphenated header (should work)\nresp1 = client.get('/', headers={'my-header': 'hyphen'})\nprint(f'Hyphen: {resp1.json()}')\n\n# Case 2: Underscored header (should NOT work if convert_underscores=True)\nresp2 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp2.json()}')\n\""
}
{"status": "ok", "stdout": "Hyphen: {'headers': {'my_header': 'hyphen'}}\nUnderscore: {'detail': [{'type': 'missing', 'loc': ['header', 'my_header'], 'msg': 'Fi
full
{"status": "ok", "stdout": "Hyphen: {'headers': {'my_header': 'hyphen'}}\nUnderscore: {'detail': [{'type': 'missing', 'loc': ['header', 'my_header'], 'msg': 'Field required', 'input': {'host': 'testserver', 'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive', 'user-agent': 'testclient'}}]}\n", "exit_code": 0}
12run_command
{
 "command": "python3 -c \"\nfrom fastapi import FastAPI, Header\nfrom fastapi.testclient import TestClient\nfrom pydantic import BaseModel\n\nclass HeaderModel(BaseModel):\n    my_header: str = Header(convert_underscores=False)\n\napp = FastAPI()\n\n@app.get('/')\nasync def root(headers: HeaderModel = Header()):\n    return {'headers': headers}\n\nclient = TestClient(app)\n\n# Case 1: Underscored header (should work if convert_underscores=False)\nresp1 = client.get('/', headers={'my_header': 'underscore'})\nprint(f'Underscore: {resp1.json()}')\n\""
}
{"status": "ok", "stdout": "Underscore: {'headers': {'my_header': 'underscore'}}\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Underscore: {'headers': {'my_header': 'underscore'}}\n", "exit_code": 0}
13edit_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "new_string": "        if key not in processed_keys:\n            if isinstance(received_params, (ImmutableMultiDict, Headers)):\n                if \"_\" in key:\n                    continue\n                \n                value = received_params.getlist(key)\n                if isinstance(value, list) and (len(value) == 1):\n                    params_to_process[key] = value[0]\n                else:\n                    params_to_process[key] = value\n            else:\n                params_to_process[key] = received_params.get(key)",
 "old_string": "        if key not in processed_keys:\n            if isinstance(received_params, (ImmutableMultiDict, Headers)):\n                # If convert_underscores is True, we should not accept underscore headers\n                # unless they were explicitly processed (which they aren't here because\n                # they are not in processed_keys).\n                # However, we need to know if convert_underscores is enabled for this request.\n                # This is tricky because convert_underscores can be per-field.\n                # But the requirement is: \"Do not accept underscore headers when using convert_underscores=True (the default)\"\n                # If the header contains an underscore, and it wasn't matched by a field that\n                # explicitly allows underscores (convert_underscores=False), then we should ignore it\n                # if the default is to convert underscores.\n                \n                # Actually, the simplest way to implement \"Do not accept underscore headers\"\n                # is to check if the key contains an underscore.\n                if \"_\" in key:\n                    # We need to check if any field allowed this underscore header.\n                    # But we already checked all fields in the previous loop.\n                    # If it's not in processed_keys, it means no field matched it.\n                    # If convert_underscores is True (default), we should not accept it.\n                    # But wait, the loop above already handles the mapping.\n                    # The loop below handles \"extra\" parameters.\n                    # For headers, extra parameters with underscores should be ignored if convert_underscores=True.\n                    # But we don't have a single 'convert_underscores' flag for the whole request.\n                    # The default is True.\n                    continue\n                \n                value = received_params.getlist(key)\n                if isinstance(value, list) and (len(value) == 1):\n                    params_to_process[key] = value[0]\n                else:\n                    params_to_process[key] = value\n            else:\n                params_to_process[key] = received_params.get(key)"
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapi
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/utils.py\n+++ b/fastapi/dependencies/utils.py\n@@ -830,28 +830,7 @@\n     for key in received_params.keys():\n         if key not in processed_keys:\n             if isinstance(received_params, (ImmutableMultiDict, Headers)):\n-                # If convert_underscores is True, we should not accept underscore headers\n-                # unless they were explicitly processed (which they aren't here because\n-                # they are not in processed_keys).\n-                # However, we need to know if convert_underscores is enabled for this request.\n-                # This is tricky because convert_underscores can be per-field.\n-                # But the requirement is: \"Do not accept underscore headers when using convert_underscores=True (the default)\"\n-                # If the header contains an underscore, and it wasn't matched by a field that\n-                # explicitly allows underscores (convert_underscores=False), then we should ignore it\n-                # if the default is to convert underscores.\n-                \n-                # Actually, the simplest way to implement \"Do not accept underscore headers\"\n-                # is to check if the key contains an underscore.\n                 if \"_\" in key:\n-                    # We need to check if any field allowed this underscore header.\n-                    # But we already checked all fields in the previous loop.\n-                    # If it's not in processed_keys, it means no field matched it.\n-                    # If convert_underscores is True (default), we should not accept it.\n-                    # But wait, the loop above already handles the mapping.\n-                    # The loop below handles \"extra\" parameters.\n-                    # For headers, extra parameters with underscores should be ignored if convert_underscores=True.\n-    
14submit_patch
[]
{"status": "ok", "patch_size": 640, "files_changed": 1}
full
{"status": "ok", "patch_size": 640, "files_changed": 1}

Patch

diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
index 7c6558c69..e86dd4d9f 100644
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -830,6 +830,9 @@ def request_params_to_args(
     for key in received_params.keys():
         if key not in processed_keys:
             if isinstance(received_params, (ImmutableMultiDict, Headers)):
+                if "_" in key:
+                    continue
+                
                 value = received_params.getlist(key)
                 if isinstance(value, list) and (len(value) == 1):
                     params_to_process[key] = value[0]

Test output

show
.......                                                                  [100%]
=============================== warnings summary ===============================
../../../../../../kaggle/tmp/envs/overlays/starlette-1.6.0-py3-none-any/starlette/testclient.py:53
  /kaggle/tmp/envs/overlays/starlette-1.6.0-py3-none-any/starlette/testclient.py:53: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
    _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
7 passed, 1 warning in 0.65s