Skip to content

Commit 14a0881

Browse files
Python: mypy clears kernel.py (microsoft#5689)
### Motivation and Context <!-- Thank you for your contribution to the semantic-kernel repo! Please help reviewers and future users, providing the following information: 1. Why is this change required? 2. What problem does it solve? 3. What scenario does it contribute to? 4. If it fixes an open issue, please link to the issue here. --> Fixed all mypy errors for kernel.py In preparation of extending that to all files in semantic_kernel Also got the pre-commit hook working on a commit, with mypy checks fixes microsoft#5630 ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Moved pre-commit-config to base folder, can now be extended to have specific hooks for other languages fixed things in kernel.py to satisfy mypy used assert for some things, so that they can be skipped when running in -o mode. ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone 😄
1 parent 75089cb commit 14a0881

10 files changed

Lines changed: 462 additions & 433 deletions

File tree

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ repos:
55
rev: 1.1.0
66
hooks:
77
- id: sync_with_poetry
8-
args: [--config=python/.conf/.pre-commit-config.yaml, --db=python/.conf/packages_list.json, python/poetry.lock]
8+
args: [--config=.pre-commit-config.yaml, --db=python/.conf/packages_list.json, python/poetry.lock]
99
- repo: https://github.com/pre-commit/pre-commit-hooks
1010
rev: v4.0.1
1111
hooks:
@@ -23,23 +23,16 @@ repos:
2323
- id: black
2424
files: \.py$
2525
- repo: https://github.com/astral-sh/ruff-pre-commit
26-
rev: v0.3.2
26+
rev: v0.3.4
2727
hooks:
2828
- id: ruff
2929
args: [ --fix, --exit-non-zero-on-fix ]
3030
- repo: local
3131
hooks:
3232
- id: mypy
33+
files: ^python/semantic_kernel/
3334
name: mypy
34-
entry: poetry -C python/ run python -m mypy --no-namespace-packages --config-file=python/mypy.ini
35+
entry: poetry -C python/ run python -m mypy -p semantic_kernel --config-file=python/mypy.ini
3536
language: system
3637
types: [python]
37-
pass_filenames: true
38-
- repo: local
39-
hooks:
40-
- id: tests
41-
name: tests
42-
entry: poetry -C python/ run coverage run -m pytest python/tests/unit
43-
language: system
44-
types: [python]
45-
pass_filenames: true
38+
pass_filenames: false

‎python/.vscode/tasks.json‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
"run",
1212
"pre-commit",
1313
"run",
14-
"-c",
15-
".conf/.pre-commit-config.yaml",
1614
"-a"
1715
],
1816
"problemMatcher": {
@@ -41,8 +39,6 @@
4139
"run",
4240
"pre-commit",
4341
"run",
44-
"-c",
45-
".conf/.pre-commit-config.yaml"
4642
],
4743
"problemMatcher": {
4844
"owner": "python",
@@ -70,8 +66,6 @@
7066
"run",
7167
"pre-commit",
7268
"run",
73-
"-c",
74-
".conf/.pre-commit-config.yaml",
7569
"-a",
7670
"mypy"
7771
],

‎python/mypy.ini‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
python_version = 3.11
44
plugins = pydantic.mypy
55
ignore_missing_imports = true
6-
exclude = "(samples|notebooks|tests)/"
76

87
[pydantic-mypy]
98
init_forbid_extra = true

‎python/poetry.lock‎

Lines changed: 396 additions & 381 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎python/pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pytest-asyncio = "^0.23.5.post1"
7777
snoop = "^0.4.3"
7878
pytest-cov = ">=4.1.0"
7979
mypy = ">=1.9.0"
80+
types-PyYAML = "^6.0.12.20240311"
8081

8182
[tool.poetry.group.unit-tests]
8283
optional = true

‎python/samples/kernel-syntax-examples/chat.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
kernel = sk.Kernel()
2323

2424
api_key, org_id = sk.openai_settings_from_dot_env()
25-
service_id = "chat-gpt"
25+
service_id = "chat"
2626
kernel.add_service(
2727
sk_oai.OpenAIChatCompletion(service_id=service_id, ai_model_id="gpt-3.5-turbo-1106", api_key=api_key, org_id=org_id)
2828
)

‎python/semantic_kernel/kernel.py‎

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
import logging
88
import os
99
from copy import copy
10-
from typing import Any, AsyncIterable, Callable, Literal, Type, TypeVar
10+
from types import MethodType
11+
from typing import TYPE_CHECKING, Any, AsyncIterable, Callable, ItemsView, Literal, Type, TypeVar, Union
1112

1213
import yaml
1314
from pydantic import Field, field_validator
1415

1516
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
16-
from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin
1717
from semantic_kernel.events import FunctionInvokedEventArgs, FunctionInvokingEventArgs
1818
from semantic_kernel.exceptions import (
1919
FunctionInitializationError,
@@ -47,9 +47,15 @@
4747
from semantic_kernel.services.ai_service_selector import AIServiceSelector
4848
from semantic_kernel.utils.validation import validate_plugin_name
4949

50+
if TYPE_CHECKING:
51+
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
52+
from semantic_kernel.connectors.ai.embeddings.embedding_generator_base import EmbeddingGeneratorBase
53+
from semantic_kernel.connectors.ai.text_completion_client_base import TextCompletionClientBase
54+
from semantic_kernel.contents.streaming_content_mixin import StreamingContentMixin
55+
5056
T = TypeVar("T")
5157

52-
ALL_SERVICE_TYPES = "TextCompletionClientBase | ChatCompletionClientBase | EmbeddingGeneratorBase"
58+
ALL_SERVICE_TYPES = Union["TextCompletionClientBase", "ChatCompletionClientBase", "EmbeddingGeneratorBase"]
5359

5460
logger: logging.Logger = logging.getLogger(__name__)
5561

@@ -139,7 +145,7 @@ async def invoke_stream(
139145
plugin_name: str | None = None,
140146
return_function_results: bool | None = False,
141147
**kwargs: Any,
142-
) -> AsyncIterable[list["StreamingContentMixin"] | list[FunctionResult]]:
148+
) -> AsyncIterable[list["StreamingContentMixin"] | FunctionResult | list[FunctionResult]]:
143149
"""Execute one or more stream functions.
144150
145151
This will execute the functions in the order they are provided, if a list of functions is provided.
@@ -204,25 +210,33 @@ async def invoke_stream(
204210
return
205211
# TODO: decide how to put results into kernelarguments,
206212
# might need to be done as part of the invoked_handler
207-
function_result = []
213+
function_result: FunctionResult | list[list["StreamingContentMixin"] | Any] = []
208214
exception = None
209215

210216
async for stream_message in stream_function.invoke_stream(self, arguments):
211217
if isinstance(stream_message, FunctionResult):
212-
exception = stream_message.metadata.get("exception", None)
213-
if exception:
214-
break
218+
function_result = stream_message
219+
break
220+
assert isinstance(function_result, list)
215221
function_result.append(stream_message)
216222
yield stream_message
217223

218-
output_function_result = []
219-
for result in function_result:
220-
for choice in result:
221-
if len(output_function_result) <= choice.choice_index:
222-
output_function_result.append(copy(choice))
223-
else:
224-
output_function_result[choice.choice_index] += choice
225-
func_result = FunctionResult(function=stream_function.metadata, value=output_function_result)
224+
if isinstance(function_result, FunctionResult):
225+
func_result = function_result
226+
exception = func_result.metadata.get("exception", None)
227+
else:
228+
output_function_result: list["StreamingContentMixin"] = []
229+
assert isinstance(function_result, list)
230+
for result in function_result:
231+
assert isinstance(result, list)
232+
for choice in result:
233+
if isinstance(choice, FunctionResult):
234+
continue
235+
if len(output_function_result) <= choice.choice_index:
236+
output_function_result.append(copy(choice))
237+
else:
238+
output_function_result[choice.choice_index] += choice
239+
func_result = FunctionResult(function=stream_function.metadata, value=output_function_result)
226240
function_invoked_args = self.on_function_invoked(
227241
stream_function.metadata,
228242
arguments,
@@ -235,7 +249,7 @@ async def invoke_stream(
235249
f"During function invocation:'{stream_function.plugin_name}.{stream_function.name}'. "
236250
f"Error description: '{str(function_invoked_args.exception)}'"
237251
) from function_invoked_args.exception
238-
if return_function_results:
252+
if return_function_results and function_invoked_args.function_result:
239253
results.append(function_invoked_args.function_result)
240254
if function_invoked_args.is_cancel_requested:
241255
logger.info(
@@ -286,7 +300,7 @@ async def invoke(
286300
"""
287301
if arguments is None:
288302
arguments = KernelArguments(**kwargs)
289-
results = []
303+
results: list[FunctionResult] = []
290304
pipeline_step = 0
291305
if not functions:
292306
if not function_name or not plugin_name:
@@ -332,7 +346,10 @@ async def invoke(
332346

333347
# this allows a hook to alter the results before adding.
334348
function_invoked_args = self.on_function_invoked(func.metadata, arguments, function_result, exception)
335-
results.append(function_invoked_args.function_result)
349+
if function_invoked_args.function_result:
350+
results.append(function_invoked_args.function_result)
351+
else:
352+
results.append(FunctionResult(function=func.metadata, value=None, metadata={}))
336353

337354
if function_invoked_args.exception:
338355
raise KernelInvokeException(
@@ -343,7 +360,7 @@ async def invoke(
343360
f"Execution was cancelled on function invoked event of pipeline step "
344361
f"{pipeline_step}: {func.plugin_name}.{func.name}."
345362
)
346-
return results if results else None
363+
return results if results else FunctionResult(function=func.metadata, value=None, metadata={})
347364
if function_invoked_args.updated_arguments:
348365
logger.info(
349366
f"Arguments updated by function_invoked_handler in pipeline step: "
@@ -491,6 +508,7 @@ def import_plugin_from_object(self, plugin_instance: Any | dict[str, Any], plugi
491508
logger.debug(f"Importing plugin {plugin_name}")
492509

493510
functions: dict[str, KernelFunction] = {}
511+
candidates: list[tuple[str, MethodType]] | ItemsView[str, Any] = []
494512

495513
if isinstance(plugin_instance, dict):
496514
candidates = plugin_instance.items()
@@ -515,7 +533,9 @@ def import_plugin_from_object(self, plugin_instance: Any | dict[str, Any], plugi
515533

516534
return plugin
517535

518-
def import_native_plugin_from_directory(self, parent_directory: str, plugin_directory_name: str) -> KernelPlugin:
536+
def import_native_plugin_from_directory(
537+
self, parent_directory: str, plugin_directory_name: str
538+
) -> KernelPlugin | None:
519539
MODULE_NAME = "native_function"
520540

521541
validate_plugin_name(plugin_directory_name)
@@ -529,7 +549,10 @@ def import_native_plugin_from_directory(self, parent_directory: str, plugin_dire
529549
plugin_name = os.path.basename(plugin_directory)
530550

531551
spec = importlib.util.spec_from_file_location(MODULE_NAME, native_py_file_path)
552+
if not spec:
553+
raise PluginInitializationError(f"Failed to load plugin: {plugin_name}")
532554
module = importlib.util.module_from_spec(spec)
555+
assert spec.loader
533556
spec.loader.exec_module(module)
534557

535558
class_name = next(
@@ -616,7 +639,7 @@ def import_plugin_from_prompt_directory(self, parent_directory: str, plugin_dire
616639
prompt = prompt_file.read()
617640
prompt_template_config.template = prompt
618641

619-
prompt_template = TEMPLATE_FORMAT_MAP[prompt_template_config.template_format](
642+
prompt_template = TEMPLATE_FORMAT_MAP[prompt_template_config.template_format]( # type: ignore
620643
prompt_template_config=prompt_template_config
621644
)
622645

@@ -806,7 +829,7 @@ def get_service(
806829
self,
807830
service_id: str | None = None,
808831
type: Type[ALL_SERVICE_TYPES] | None = None,
809-
) -> ALL_SERVICE_TYPES:
832+
) -> "AIServiceClientBase":
810833
"""Get a service by service_id and type.
811834
812835
Type is optional and when not supplied, no checks are done.
@@ -830,6 +853,7 @@ def get_service(
830853
ValueError: If no service is found that matches the type.
831854
832855
"""
856+
service: "AIServiceClientBase | None" = None
833857
if not service_id or service_id == "default":
834858
if not type:
835859
if default_service := self.services.get("default"):
@@ -848,11 +872,11 @@ def get_service(
848872
raise ServiceInvalidTypeError(f"Service with service_id '{service_id}' is not of type {type}")
849873
return service
850874

851-
def get_services_by_type(self, type: Type[T]) -> dict[str, T]:
875+
def get_services_by_type(self, type: Type[ALL_SERVICE_TYPES]) -> dict[str, "AIServiceClientBase"]:
852876
return {service.service_id: service for service in self.services.values() if isinstance(service, type)}
853877

854878
def get_prompt_execution_settings_from_service_id(
855-
self, service_id: str, type: Type[T] | None = None
879+
self, service_id: str, type: Type[ALL_SERVICE_TYPES] | None = None
856880
) -> PromptExecutionSettings:
857881
"""Get the specific request settings from the service, instantiated with the service_id and ai_model_id."""
858882
service = self.get_service(service_id, type=type)
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
# Copyright (c) Microsoft. All rights reserved.
22

3-
from typing import Literal, Union
3+
from typing import Literal, get_args
44

5-
KERNEL_TEMPLATE_FORMAT_NAME: Literal["semantic-kernel"] = "semantic-kernel"
6-
HANDLEBARS_TEMPLATE_FORMAT_NAME: Literal["handlebars"] = "handlebars"
7-
JINJA2_TEMPLATE_FORMAT_NAME: Literal["jinja2"] = "jinja2"
5+
KERNEL_TEMPLATE_FORMAT_NAME_TYPE = Literal["semantic-kernel"]
6+
KERNEL_TEMPLATE_FORMAT_NAME = get_args(KERNEL_TEMPLATE_FORMAT_NAME_TYPE)[0]
7+
HANDLEBARS_TEMPLATE_FORMAT_NAME_TYPE = Literal["handlebars"]
8+
HANDLEBARS_TEMPLATE_FORMAT_NAME = get_args(HANDLEBARS_TEMPLATE_FORMAT_NAME_TYPE)[0]
9+
JINJA2_TEMPLATE_FORMAT_NAME_TYPE = Literal["jinja2"]
10+
JINJA2_TEMPLATE_FORMAT_NAME = get_args(JINJA2_TEMPLATE_FORMAT_NAME_TYPE)[0]
811

9-
TEMPLATE_FORMAT_TYPES = Union[
10-
type(KERNEL_TEMPLATE_FORMAT_NAME), type(HANDLEBARS_TEMPLATE_FORMAT_NAME), type(JINJA2_TEMPLATE_FORMAT_NAME)
12+
TEMPLATE_FORMAT_TYPES = Literal[
13+
KERNEL_TEMPLATE_FORMAT_NAME_TYPE, HANDLEBARS_TEMPLATE_FORMAT_NAME_TYPE, JINJA2_TEMPLATE_FORMAT_NAME_TYPE
1114
]

‎python/semantic_kernel/prompt_template/prompt_template_config.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818

1919
class PromptTemplateConfig(KernelBaseModel):
20-
name: Optional[str] = ""
20+
name: str = ""
2121
description: Optional[str] = ""
2222
template: Optional[str] = None
2323
template_format: TEMPLATE_FORMAT_TYPES = KERNEL_TEMPLATE_FORMAT_NAME

‎python/semantic_kernel/py.typed‎

Whitespace-only changes.

0 commit comments

Comments
 (0)