Repository navigation
Python: fix: CodeTokenizer: handle spaces inside quoted named-arg values - #14478
Wu Shuwen (dajiaohuang) wants to merge 3 commits into
Conversation
When parsing templates like {{func key='hello world'}}, the tokenizer
incorrectly included spaces inside quoted named-arg values because
the VALUE state did not check for a closing delimiter when a space
was encountered.
Two changes:
1. A space in VALUE mode now only ends the value when followed by a
non-alphanumeric, non-$-, non-quote character (i.e., a genuine
token separator, not part of the quoted text).
2. The closing delimiter check now additionally verifies that the
character after the quote is not alphanumeric, which correctly
handles quoted values adjacent to punctuation like }}.
Fixes microsoft#14460.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The tokenizer does not enter named-argument value handling, so the reported case remains unresolved.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
Fixes CodeTokenizer handling of spaces in quoted named-argument values.
Changes:
- Adds quoted-value space handling.
- Adds closing-quote lookahead logic.
| File | Summary |
|---|---|
python/semantic_kernel/template_engine/code_tokenizer.py |
Adjusts tokenizer state and quoted-value parsing. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Eduard van Valkenburg (eavanvalkenburg)
left a comment
There was a problem hiding this comment.
Review finding (blocking): Quoted named-argument values with spaces are still split
The advertised {{func key='hello world'}} case still starts in the FUNCTION_ID state, and the tokenizer never enters the new named-argument value branch while scanning the equals sign and quote. The space after hello therefore still takes the generic separator path and raises NamedArgBlockSyntaxError. Please enter a quoted named-argument state when = is followed by a quote, and add a regression test for this exact template.
The previous fix had two issues: 1. It added a broken space-handling branch that incorrectly terminated ValBlocks on spaces, causing tokens after quoted values to be consumed into the ValBlock. 2. The closing-delimiter check used text[index+2] (off-by-one), preventing legitimate closing quotes (e.g. in escape sequences like 'f\\'xy') from being detected. 3. It never added the state transition from FUNCTION_ID to NAMED_ARG when the opening quote appears mid-token after '=', which is the root cause of the original bug. The root cause was that when parsing 'key='hello world'', the opening quote appears after '=' (still mid-FUNCTION_ID token), not after a space separator. The code never switched into quoted-value mode, so the space inside 'hello world' incorrectly terminated the FUNCTION_ID token. Fix: - Remove the broken space-handling and incorrect index+2 check. - When in FUNCTION_ID mode and the current char is a quote and '=' is already present in the token content, switch to NAMED_ARG mode (setting text_value_delimiter) so that spaces inside the quoted value are preserved and the closing quote ends the NamedArgBlock. - When closing a quoted block, create NamedArgBlock (not ValBlock) if in NAMED_ARG mode. - Handle NAMED_ARG in the end-of-loop final-capture. - Add regression test for issue microsoft#14460.
Fixed: Completed the fix for spaces in quoted named-arg valuesThe original PR had the right goal but the implementation had two issues that caused the 39 test failures: Root cause of the failures
What this fix does
Test results
The fix correctly tokenizes:
|
|
The tokenizer state-machine finding is fixed and the full template-engine suite passes. CI still fails spelling on the existing test identifier "fo.o" at test_code_tokenizer.py:60, which typos reports as "fo". Please rename that test identifier (for example to "foo.o") so the spelling check and merge gate can pass. |
PRABHU KIRAN VANDRANKI (VANDRANKI)
left a comment
There was a problem hiding this comment.
I applied gh pr diff onto main (df1cb33) in a detached worktree. It applied cleanly. I ran everything below with Python 3.13.3 in python/.
End to end. A native plugin function p.echo(key, other) that returns [key|other], rendered with KernelPromptTemplate (main, then PR):
| template | main | PR |
|---|---|---|
{{p.echo key='hello world'}} |
TemplateSyntaxError |
[hello world|] |
{{p.echo key="hello world"}} |
TemplateSyntaxError |
[hello world|] |
{{p.echo key='x y' }} (two spaces) |
TemplateSyntaxError |
[x y|] |
{{p.echo key='a b' other='c d'}} |
TemplateSyntaxError |
[a b|c d] |
{{p.echo key='a b' other=$x}} with x=XX |
TemplateSyntaxError |
[a b|XX] |
{{p.echo other='c d' key=$x}} |
TemplateSyntaxError |
[XX|c d] |
{{p.echo key='a=b c'}} |
TemplateSyntaxError |
[a=b c|] |
{{p.echo key='a b'}}text{{p.echo key='c d'}} |
TemplateSyntaxError |
[a b|]text[c d|] |
{{p.echo key='a'}} and {{p.echo key='b'}} |
[a|] and [b|] |
same |
Regression check on the tokenizer. I built 56 code strings (2 function names, 28 argument shapes: none, positional, variables, named args with and without spaces, escapes, mixes) and ran CodeTokenizer.tokenize on both trees. 18 give identical results. 38 differ. In every one of the 38, main raised an error. 34 of them tokenize cleanly on the PR with the expected NAMED_ARG blocks. The other 4 raise on both trees with a different exception class (key='it's' and key='a'b, each with two function names). No string that tokenized on main changed on the PR.
Tests:
tests/unit/template_engineplustests/unit/prompt_templatewith the PR applied: 690 passed. Main gives 689 passed.- The PR's
test_code_tokenizer.pyagainst unpatched main source: 1 failed (test_named_args_with_spaces_in_quoted_value), 19 passed.
Limits that exist on main and stay with the PR, so not blockers:
- A named-arg value with an escaped quote still fails:
{{p.echo key='it\'s'}}givesTemplateSyntaxErroron both. The quote-handling branch drops the backslash, so the block content becomeskey='it's'. A positional{{p.echo 'it\'s a'}}works on both. - A named-arg value that holds the other quote type still fails on both:
key="a'b"andkey='a"b'. Positional values like"a 'b' c"work.
One note on the description. It talks about a space check in VALUE mode and a closing-delimiter check. The code in the diff does something else: it switches to the NAMED_ARG state when a quote follows = inside a function-id token, and closes the block on the matching quote. The behavior above is what I tested. The description could be updated to match.
No blockers from me. Approving.

Description
When parsing templates like
{{func key='hello world'}}, the CodeTokenizer incorrectly included spaces inside quoted named-arg values, causingNamedArgBlockSyntaxError.Root cause
The VALUE state in the tokenizer's main loop appends every character to
current_token_contentwithout checking whether a space is a token separator. When a space inside a quoted value is followed by an alphanumeric character (the continuation of the quoted text), the space is incorrectly treated as a token boundary.Two changes fix this:
Space handling in VALUE mode: A space now ends the quoted value only when the following character is not a valid quoted-value continuation (alphanumeric,
_,-,$, quote,VAR_PREFIX).Closing delimiter check: The closing quote is recognized as a block boundary only when the character after it is not alphanumeric. This correctly handles quoted values adjacent to punctuation like
}}.Reproduction
Fixes #14460.
Maintainer follow-up (57b6ac1): renamed the test identifier fo.o to foo.o as requested. Diff whitespace check passed. The project-pinned typos 1.44.0 with .github/_typos.toml passed on the changed test file. Python tests have not been run at this head; pytest and project dependencies are not installed in this environment.