Files
semantic-kernel/python/semantic_kernel/template_engine/code_tokenizer.py
T
Eduard van Valkenburg 9e99933a27 Python: new pre-commit actions and pre-commit as GHA (#6376)
### 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.
-->

- Added static code checking to pre-commit (check-ast and
nbqa-check-ast)
- Added Bandit security checking to pre-commit
- Added pre-commit step to python-lint workflow, if it works, can delete
seperate mypy, ruff and black tests

### Description

<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

### 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 😄
2024-05-28 15:28:04 +00:00

156 lines
6.5 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import logging
from semantic_kernel.exceptions import CodeBlockSyntaxError
from semantic_kernel.template_engine.blocks.block import Block
from semantic_kernel.template_engine.blocks.block_types import BlockTypes
from semantic_kernel.template_engine.blocks.function_id_block import FunctionIdBlock
from semantic_kernel.template_engine.blocks.named_arg_block import NamedArgBlock
from semantic_kernel.template_engine.blocks.symbols import Symbols
from semantic_kernel.template_engine.blocks.val_block import ValBlock
from semantic_kernel.template_engine.blocks.var_block import VarBlock
logger: logging.Logger = logging.getLogger(__name__)
# BNF parsed by CodeTokenizer:
# [template] ::= "" | [variable] " " [template]
# | [value] " " [template]
# | [function-call] " " [template]
# [variable] ::= "$" [valid-name]
# [value] ::= "'" [text] "'" | '"' [text] '"'
# [function-call] ::= [function-id] | [function-id] [parameter]
# [parameter] ::= [variable] | [value]
class CodeTokenizer:
@staticmethod
def tokenize(text: str) -> list[Block]:
"""Tokenize the code text into blocks."""
# Remove spaces, which are ignored anyway
text = text.strip() if text else ""
# Render None/empty to []
if not text:
return []
# 1 char only edge case, var and val blocks are invalid with one char, so it must be a function id block
if len(text) == 1:
return [FunctionIdBlock(content=text)]
# Track what type of token we're reading
current_token_type = None
# Track the content of the current token
current_token_content: list[str] = []
# Other state we need to track
text_value_delimiter = None
space_separator_found = False
skip_next_char = False
next_char = ""
blocks: list[Block] = []
for index, current_char in enumerate(text[:-1]):
next_char = text[index + 1]
if skip_next_char:
skip_next_char = False
continue
# First char is easy
if index == 0:
if current_char == Symbols.VAR_PREFIX:
current_token_type = BlockTypes.VARIABLE
elif current_char in (Symbols.DBL_QUOTE, Symbols.SGL_QUOTE):
current_token_type = BlockTypes.VALUE
text_value_delimiter = current_char
else:
current_token_type = BlockTypes.FUNCTION_ID
current_token_content.append(current_char)
continue
# While reading values between quotes
if current_token_type in (BlockTypes.VALUE, BlockTypes.NAMED_ARG):
# If the current char is escaping the next special char we:
# - skip the current char (escape char)
# - add the next char (special char)
# - jump to the one after (to handle "\\" properly)
if current_char == Symbols.ESCAPE_CHAR and next_char in (
Symbols.DBL_QUOTE,
Symbols.SGL_QUOTE,
Symbols.ESCAPE_CHAR,
):
current_token_content.append(next_char)
skip_next_char = True
continue
current_token_content.append(current_char)
# When we reach the end of the value, we add the block
if current_char == text_value_delimiter:
blocks.append(ValBlock(content="".join(current_token_content)))
current_token_content.clear()
current_token_type = None
space_separator_found = False
continue
# If we're not between quotes, a space signals the end of the current token
# Note: there might be multiple consecutive spaces
if current_char in (
Symbols.SPACE,
Symbols.NEW_LINE,
Symbols.CARRIAGE_RETURN,
Symbols.TAB,
):
if current_token_type == BlockTypes.VARIABLE:
blocks.append(VarBlock(content="".join(current_token_content)))
current_token_content.clear()
elif current_token_type == BlockTypes.FUNCTION_ID:
if Symbols.NAMED_ARG_BLOCK_SEPARATOR.value in current_token_content:
blocks.append(NamedArgBlock(content="".join(current_token_content)))
else:
blocks.append(FunctionIdBlock(content="".join(current_token_content)))
current_token_content.clear()
space_separator_found = True
current_token_type = None
continue
# If we're not inside a quoted value, and we're not processing a space
current_token_content.append(current_char)
if current_token_type is None:
if not space_separator_found:
raise CodeBlockSyntaxError("Tokens must be separated by one space least")
if current_char in (Symbols.DBL_QUOTE, Symbols.SGL_QUOTE):
# A quoted value starts here
current_token_type = BlockTypes.VALUE
text_value_delimiter = current_char
elif current_char == Symbols.VAR_PREFIX:
# A variable starts here
current_token_type = BlockTypes.VARIABLE
else:
# A function id starts here
current_token_type = BlockTypes.FUNCTION_ID
# end of main for loop
# Capture last token
current_token_content.append(next_char)
if current_token_type == BlockTypes.VALUE:
blocks.append(ValBlock(content="".join(current_token_content)))
elif current_token_type == BlockTypes.VARIABLE:
blocks.append(VarBlock(content="".join(current_token_content)))
elif current_token_type == BlockTypes.FUNCTION_ID:
if Symbols.NAMED_ARG_BLOCK_SEPARATOR.value in current_token_content:
blocks.append(NamedArgBlock(content="".join(current_token_content)))
else:
blocks.append(FunctionIdBlock(content="".join(current_token_content)))
else:
raise CodeBlockSyntaxError("Tokens must be separated by one space least")
return blocks