diff --git a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs index 8de5d64d0..a1b44c5f4 100644 --- a/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs +++ b/src/Analysis/Ast/Impl/Analyzer/Evaluation/ExpressionEval.cs @@ -144,6 +144,12 @@ public IMember GetValueFromExpression(Expression expr, LookupOptions options) { case LambdaExpression lambda: m = GetValueFromLambda(lambda); break; + case FString fString: + m = GetValueFromFString(fString); + break; + case FormatSpecifier formatSpecifier: + m = GetValueFromFormatSpecifier(formatSpecifier); + break; default: m = GetValueFromBinaryOp(expr) ?? GetConstantFromLiteral(expr, options); break; @@ -154,6 +160,14 @@ public IMember GetValueFromExpression(Expression expr, LookupOptions options) { return m; } + private IMember GetValueFromFormatSpecifier(FormatSpecifier formatSpecifier) { + return new PythonFString(formatSpecifier.Unparsed, Interpreter, GetLoc(formatSpecifier)); + } + + private IMember GetValueFromFString(FString fString) { + return new PythonFString(fString.Unparsed, Interpreter, GetLoc(fString)); + } + private IMember GetValueFromName(NameExpression expr, LookupOptions options) { if (expr == null || string.IsNullOrEmpty(expr.Name)) { return null; diff --git a/src/Analysis/Ast/Impl/Values/PythonString.cs b/src/Analysis/Ast/Impl/Values/PythonString.cs index c9bbc1f90..c98af8701 100644 --- a/src/Analysis/Ast/Impl/Values/PythonString.cs +++ b/src/Analysis/Ast/Impl/Values/PythonString.cs @@ -13,6 +13,7 @@ // See the Apache Version 2.0 License for specific language governing // permissions and limitations under the License. +using System; using Microsoft.Python.Analysis.Types; using Microsoft.Python.Parsing; @@ -32,4 +33,21 @@ internal sealed class PythonUnicodeString : PythonConstant { public PythonUnicodeString(string s, IPythonInterpreter interpreter, LocationInfo location = null) : base(s, interpreter.GetBuiltinType(interpreter.GetUnicodeTypeId()), location) { } } + + internal sealed class PythonFString : PythonInstance, IEquatable { + public readonly string UnparsedFString; + + public PythonFString(string unparsedFString, IPythonInterpreter interpreter, LocationInfo location = null) + : base(interpreter.GetBuiltinType(interpreter.GetUnicodeTypeId()), location) { + UnparsedFString = unparsedFString; + } + + + public bool Equals(PythonFString other) { + if (!base.Equals(other)) { + return false; + } + return UnparsedFString?.Equals(other?.UnparsedFString) == true; + } + } } diff --git a/src/Analysis/Ast/Test/TypingTests.cs b/src/Analysis/Ast/Test/TypingTests.cs index 5dcad14e0..6c0940fdb 100644 --- a/src/Analysis/Ast/Test/TypingTests.cs +++ b/src/Analysis/Ast/Test/TypingTests.cs @@ -225,6 +225,15 @@ from typing import AnyStr .And.HaveVariable("y").OfType("AnyStr"); } + [TestMethod, Priority(0)] + public async Task FStringIsStringType() { + const string code = @" +x = f'{1}' +"; + var analysis = await GetAnalysisAsync(code); + analysis.Should().HaveVariable("x").OfType(BuiltinTypeId.Str); + } + [TestMethod, Priority(0)] public async Task OptionalNone() { const string code = @" diff --git a/src/LanguageServer/Impl/Sources/HoverSource.cs b/src/LanguageServer/Impl/Sources/HoverSource.cs index 3f9947f52..f8cdcda3c 100644 --- a/src/LanguageServer/Impl/Sources/HoverSource.cs +++ b/src/LanguageServer/Impl/Sources/HoverSource.cs @@ -40,7 +40,8 @@ public Hover GetHover(IDocumentAnalysis analysis, SourceLocation location) { ExpressionLocator.FindExpression(analysis.Ast, location, FindExpressionOptions.Hover, out var node, out var statement, out var scope); - if (node is ConstantExpression || !(node is Expression expr)) { + if (node is ConstantExpression || node is FString || !(node is Expression expr)) { + // node is FString only if it didn't save an f-string subexpression return null; // No hover for literals. } diff --git a/src/LanguageServer/Test/CompletionTests.cs b/src/LanguageServer/Test/CompletionTests.cs index 6d6501c79..1689a4240 100644 --- a/src/LanguageServer/Test/CompletionTests.cs +++ b/src/LanguageServer/Test/CompletionTests.cs @@ -864,6 +864,17 @@ public async Task NoCompletionInOpenString() { result.Should().HaveNoCompletion(); } + [DataRow("f'.")] + [DataRow("f'a.")] + [DataRow("f'a.'")] + [DataTestMethod, Priority(0)] + public async Task NoCompletionInFStringConstant(string openFString) { + var analysis = await GetAnalysisAsync(openFString); + var cs = new CompletionSource(new PlainTextDocumentationSource(), ServerSettings.completion); + var result = cs.GetCompletions(analysis, new SourceLocation(1, 5)); + result.Should().HaveNoCompletion(); + } + [TestMethod, Priority(0)] public async Task NoCompletionBadImportExpression() { var analysis = await GetAnalysisAsync("import os,."); diff --git a/src/LanguageServer/Test/HoverTests.cs b/src/LanguageServer/Test/HoverTests.cs index 414e428f8..b87eb9f79 100644 --- a/src/LanguageServer/Test/HoverTests.cs +++ b/src/LanguageServer/Test/HoverTests.cs @@ -206,6 +206,24 @@ import os.path as PATH AssertHover(hs, analysis, new SourceLocation(2, 20), $"module {name}*", new SourceSpan(2, 19, 2, 23)); } + [TestMethod, Priority(0)] + public async Task FStringExpressions() { + const string code = @" +some = '' +f'{some' +Fr'{some' +f'{some}' +f'hey {some}' +"; + var analysis = await GetAnalysisAsync(code); + var hs = new HoverSource(new PlainTextDocumentationSource()); + AssertHover(hs, analysis, new SourceLocation(3, 4), @"some: str", new SourceSpan(3, 4, 3, 8)); + AssertHover(hs, analysis, new SourceLocation(4, 5), @"some: str", new SourceSpan(4, 5, 4, 9)); + AssertHover(hs, analysis, new SourceLocation(5, 4), @"some: str", new SourceSpan(5, 4, 5, 8)); + hs.GetHover(analysis, new SourceLocation(6, 3)).Should().BeNull(); + AssertHover(hs, analysis, new SourceLocation(6, 8), @"some: str", new SourceSpan(6, 8, 6, 12)); + } + private static void AssertHover(HoverSource hs, IDocumentAnalysis analysis, SourceLocation position, string hoverText, SourceSpan? span = null) { var hover = hs.GetHover(analysis, position); diff --git a/src/Parsing/Impl/Ast/FString.cs b/src/Parsing/Impl/Ast/FString.cs new file mode 100644 index 000000000..55bdd95df --- /dev/null +++ b/src/Parsing/Impl/Ast/FString.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Python.Parsing.Ast { + public class FString : Expression { + private readonly Node[] _children; + private readonly string _openQuotes; + + public FString(Node[] children, string openQuotes, string unparsed) { + _children = children; + _openQuotes = openQuotes; + Unparsed = unparsed; + } + + public override IEnumerable GetChildNodes() { + return _children; + } + + public readonly string Unparsed; + + public override void Walk(PythonWalker walker) { + if (walker.Walk(this)) { + foreach (var child in _children) { + child.Walk(walker); + } + } + walker.PostWalk(this); + } + + public async override Task WalkAsync(PythonWalkerAsync walker, CancellationToken cancellationToken = default) { + if (await walker.WalkAsync(this, cancellationToken)) { + foreach (var child in _children) { + await child.WalkAsync(walker, cancellationToken); + } + } + await walker.PostWalkAsync(this, cancellationToken); + } + + internal override void AppendCodeString(StringBuilder res, PythonAst ast, CodeFormattingOptions format) { + var verbatimPieces = this.GetVerbatimNames(ast); + var verbatimComments = this.GetListWhiteSpace(ast); + if (verbatimPieces != null) { + // string+ / bytes+, such as "abc" "abc", which can spawn multiple lines, and + // have comments in between the peices. + for (var i = 0; i < verbatimPieces.Length; i++) { + if (verbatimComments != null && i < verbatimComments.Length) { + format.ReflowComment(res, verbatimComments[i]); + } + res.Append(verbatimPieces[i]); + } + } else { + format.ReflowComment(res, this.GetPreceedingWhiteSpaceDefaultNull(ast)); + if (this.GetExtraVerbatimText(ast) != null) { + res.Append(this.GetExtraVerbatimText(ast)); + } else { + RecursiveAppendRepr(res, ast, format); + } + } + } + + private void RecursiveAppendRepr(StringBuilder res, PythonAst ast, CodeFormattingOptions format) { + res.Append('f'); + res.Append(_openQuotes); + foreach (var child in _children) { + AppendChild(res, ast, format, child); + } + res.Append(_openQuotes); + } + + private static void AppendChild(StringBuilder res, PythonAst ast, CodeFormattingOptions format, Node child) { + if (child is ConstantExpression expr) { + // Non-Verbatim AppendCodeString for ConstantExpression adds quotes around string + // Remove those quotes + var childStrBuilder = new StringBuilder(); + child.AppendCodeString(childStrBuilder, ast, format); + res.Append(childStrBuilder.ToString().Trim('\'')); + } else { + child.AppendCodeString(res, ast, format); + } + } + } +} diff --git a/src/Parsing/Impl/Ast/FormatSpecifier.cs b/src/Parsing/Impl/Ast/FormatSpecifier.cs new file mode 100644 index 000000000..405cbfcd5 --- /dev/null +++ b/src/Parsing/Impl/Ast/FormatSpecifier.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Python.Parsing.Ast { + public class FormatSpecifier : Expression { + private readonly Node[] _children; + + public FormatSpecifier(Node[] children, string unparsed) { + _children = children; + Unparsed = unparsed; + } + + public override IEnumerable GetChildNodes() { + return _children; + } + + public readonly string Unparsed; + + public override void Walk(PythonWalker walker) { + if (walker.Walk(this)) { + foreach (var child in _children) { + child.Walk(walker); + } + } + walker.PostWalk(this); + } + + public async override Task WalkAsync(PythonWalkerAsync walker, CancellationToken cancellationToken = default) { + if (await walker.WalkAsync(this, cancellationToken)) { + foreach (var child in _children) { + await child.WalkAsync(walker, cancellationToken); + } + } + await walker.PostWalkAsync(this, cancellationToken); + } + + internal override void AppendCodeString(StringBuilder res, PythonAst ast, CodeFormattingOptions format) { + // There is no leading f + foreach (var child in _children) { + AppendChild(res, ast, format, child); + } + } + + private static void AppendChild(StringBuilder res, PythonAst ast, CodeFormattingOptions format, Node child) { + if (child is ConstantExpression expr) { + // Non-Verbatim AppendCodeString for ConstantExpression adds quotes around string + // Remove those quotes + var childStrBuilder = new StringBuilder(); + child.AppendCodeString(childStrBuilder, ast, format); + res.Append(childStrBuilder.ToString().Trim('\'')); + } else { + child.AppendCodeString(res, ast, format); + } + } + } +} diff --git a/src/Parsing/Impl/Ast/FormattedValue.cs b/src/Parsing/Impl/Ast/FormattedValue.cs new file mode 100644 index 000000000..5af84bea7 --- /dev/null +++ b/src/Parsing/Impl/Ast/FormattedValue.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Python.Parsing.Ast { + public class FormattedValue : Node { + public FormattedValue(Expression value, char? conversion, Expression formatSpecifier) { + Value = value; + FormatSpecifier = formatSpecifier; + Conversion = conversion; + } + + public Expression Value { get; } + public Expression FormatSpecifier { get; } + public char? Conversion { get; } + + public override IEnumerable GetChildNodes() { + yield return Value; + if (FormatSpecifier != null) { + yield return FormatSpecifier; + } + } + + public override void Walk(PythonWalker walker) { + if (walker.Walk(this)) { + Value.Walk(walker); + FormatSpecifier?.Walk(walker); + } + walker.PostWalk(this); + } + + public async override Task WalkAsync(PythonWalkerAsync walker, CancellationToken cancellationToken = default) { + if (await walker.WalkAsync(this, cancellationToken)) { + await Value.WalkAsync(walker, cancellationToken); + await FormatSpecifier?.WalkAsync(walker, cancellationToken); + } + await walker.PostWalkAsync(this, cancellationToken); + } + + internal override void AppendCodeString(StringBuilder res, PythonAst ast, CodeFormattingOptions format) { + res.Append('{'); + Value.AppendCodeString(res, ast, format); + if (Conversion.HasValue) { + res.Append('!'); + res.Append(Conversion.Value); + } + if (FormatSpecifier != null) { + res.Append(':'); + FormatSpecifier.AppendCodeString(res, ast, format); + } + res.Append('}'); + } + } +} diff --git a/src/Parsing/Impl/Ast/PythonWalker.Generated.cs b/src/Parsing/Impl/Ast/PythonWalker.Generated.cs index 73df22971..36f1c1b90 100644 --- a/src/Parsing/Impl/Ast/PythonWalker.Generated.cs +++ b/src/Parsing/Impl/Ast/PythonWalker.Generated.cs @@ -283,6 +283,18 @@ public virtual void PostWalk(ErrorStatement node) { } // DecoratorStatement public virtual bool Walk(DecoratorStatement node) { return true; } public virtual void PostWalk(DecoratorStatement node) { } + + // FString + public virtual bool Walk(FString node) { return true; } + public virtual void PostWalk(FString node) { } + + // FormatSpecifier + public virtual bool Walk(FormatSpecifier node) { return true; } + public virtual void PostWalk(FormatSpecifier node) { } + + // FormattedValue + public virtual bool Walk(FormattedValue node) { return true; } + public virtual void PostWalk(FormattedValue node) { } } @@ -794,6 +806,15 @@ private bool Contains(Statement stmt) { // DecoratorStatement public override bool Walk(DecoratorStatement node) { return Contains(node); } + + // FString + public override bool Walk(FString node) { return Location >= node.StartIndex && Location <= node.EndIndex; } + + // FormatSpecifier + public override bool Walk(FormatSpecifier node) { return Location >= node.StartIndex && Location <= node.EndIndex; } + + // FormattedValue + public override bool Walk(FormattedValue node) { return Location >= node.StartIndex && Location <= node.EndIndex; } } } diff --git a/src/Parsing/Impl/Ast/PythonWalkerAsync.Generated.cs b/src/Parsing/Impl/Ast/PythonWalkerAsync.Generated.cs index 8aade0e15..4b8d0864d 100644 --- a/src/Parsing/Impl/Ast/PythonWalkerAsync.Generated.cs +++ b/src/Parsing/Impl/Ast/PythonWalkerAsync.Generated.cs @@ -285,6 +285,18 @@ public class PythonWalkerAsync { // DecoratorStatement public virtual Task WalkAsync(DecoratorStatement node, CancellationToken cancellationToken = default) => Task.FromResult(true); public virtual Task PostWalkAsync(DecoratorStatement node, CancellationToken cancellationToken = default) => Task.CompletedTask; + + // FString + public virtual Task WalkAsync(FString node, CancellationToken cancellationToken = default) => Task.FromResult(true); + public virtual Task PostWalkAsync(FString node, CancellationToken cancellationToken = default) => Task.CompletedTask; + + // FormatSpecifier + public virtual Task WalkAsync(FormatSpecifier node, CancellationToken cancellationToken = default) => Task.FromResult(true); + public virtual Task PostWalkAsync(FormatSpecifier node, CancellationToken cancellationToken = default) => Task.CompletedTask; + + // FormattedValue + public virtual Task WalkAsync(FormattedValue node, CancellationToken cancellationToken = default) => Task.FromResult(true); + public virtual Task PostWalkAsync(FormattedValue node, CancellationToken cancellationToken = default) => Task.CompletedTask; } @@ -862,5 +874,17 @@ public override Task WalkAsync(ErrorStatement node, CancellationToken canc // DecoratorStatement public override Task WalkAsync(DecoratorStatement node, CancellationToken cancellationToken = default) => Task.FromResult(Contains(node)); + + // FString + public override Task WalkAsync(FString node, CancellationToken cancellationToken = default) + => Task.FromResult(Location >= node.StartIndex && Location <= node.EndIndex); + + // FormatSpecifier + public override Task WalkAsync(FormatSpecifier node, CancellationToken cancellationToken = default) + => Task.FromResult(Location >= node.StartIndex && Location <= node.EndIndex); + + // FormattedValue + public override Task WalkAsync(FormattedValue node, CancellationToken cancellationToken = default) + => Task.FromResult(Location >= node.StartIndex && Location <= node.EndIndex); } } diff --git a/src/Parsing/Impl/FStringParser.cs b/src/Parsing/Impl/FStringParser.cs new file mode 100644 index 000000000..020a2de19 --- /dev/null +++ b/src/Parsing/Impl/FStringParser.cs @@ -0,0 +1,451 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using Microsoft.Python.Core; +using Microsoft.Python.Core.Text; +using Microsoft.Python.Parsing.Ast; + +namespace Microsoft.Python.Parsing { + internal class FStringParser { + // Readonly parametrized + private readonly List _fStringChildren; + private readonly string _fString; + private readonly bool _isRaw; + private readonly ErrorSink _errors; + private readonly ParserOptions _options; + private readonly PythonLanguageVersion _langVersion; + private readonly bool _verbatim; + private readonly SourceLocation _start; + + // Nonparametric initialization + private readonly StringBuilder _buffer = new StringBuilder(); + private readonly Stack _nestedParens = new Stack(); + + // State fields + private int _position = 0; + private int _currentLineNumber; + private int _currentColNumber; + private bool _hasErrors = false; + + // Static fields + private static readonly StringSpan DoubleOpen = new StringSpan("{{", 0, 2); + private static readonly StringSpan DoubleClose = new StringSpan("}}", 0, 2); + private static readonly StringSpan NotEqualStringSpan = new StringSpan("!=", 0, 2); + private static readonly StringSpan BackslashN = new StringSpan("\\N", 0, 2); + + internal FStringParser(List fStringChildren, string fString, bool isRaw, + ParserOptions options, PythonLanguageVersion langVersion) { + + _fString = fString; + _isRaw = isRaw; + _fStringChildren = fStringChildren; + _errors = options.ErrorSink ?? ErrorSink.Null; + _options = options; + _langVersion = langVersion; + _verbatim = options.Verbatim; + _start = options.InitialSourceLocation ?? SourceLocation.MinValue; + _currentLineNumber = _start.Line; + _currentColNumber = _start.Column; + } + + public void Parse() { + var bufferStartLoc = CurrentLocation(); + while (!EndOfFString()) { + if (IsNext(DoubleOpen)) { + _buffer.Append(NextChar()); + _buffer.Append(NextChar()); + } else if (IsNext(DoubleClose)) { + _buffer.Append(NextChar()); + _buffer.Append(NextChar()); + } else if (!_isRaw && IsNext(BackslashN)) { + _buffer.Append(NextChar()); + _buffer.Append(NextChar()); + if (CurrentChar() == '{') { + Read('{'); + _buffer.Append('{'); + while (!EndOfFString() && CurrentChar() != '}') { + _buffer.Append(NextChar()); + } + if (Read('}')) { + _buffer.Append('}'); + } + } else { + _buffer.Append(NextChar()); + } + } else if (CurrentChar() == '{') { + AddBufferedSubstring(bufferStartLoc); + ParseInnerExpression(); + bufferStartLoc = CurrentLocation(); + } else if (CurrentChar() == '}') { + ReportSyntaxError(Resources.SingleClosedBraceFStringErrorMsg); + _buffer.Append(NextChar()); + } else { + _buffer.Append(NextChar()); + } + } + AddBufferedSubstring(bufferStartLoc); + } + + private bool IsNext(StringSpan span) + => _fString.Slice(_position, span.Length).Equals(span); + + private void ParseInnerExpression() { + _fStringChildren.Add(ParseFStringExpression()); + } + + // Inspired on CPython's f-string parsing implementation + private Node ParseFStringExpression() { + Debug.Assert(_buffer.Length == 0, "Current buffer is not empty"); + + var startOfFormattedValue = CurrentLocation().Index; + Read('{'); + var initialPosition = _position; + SourceLocation initialSourceLocation = CurrentLocation(); + + BufferInnerExpression(); + Expression fStringExpression = null; + FormattedValue formattedValue; + + if (EndOfFString()) { + if (_nestedParens.Count > 0) { + ReportSyntaxError(Resources.UnmatchedFStringErrorMsg.FormatInvariant(_nestedParens.Peek())); + _nestedParens.Clear(); + } else { + ReportSyntaxError(Resources.ExpectingCharFStringErrorMsg.FormatInvariant('}')); + } + if (_buffer.Length != 0) { + fStringExpression = CreateExpression(_buffer.ToString(), initialSourceLocation); + _buffer.Clear(); + } else { + fStringExpression = Error(initialPosition); + } + formattedValue = new FormattedValue(fStringExpression, null, null); + formattedValue.SetLoc(new IndexSpan(startOfFormattedValue, CurrentLocation().Index - startOfFormattedValue)); + return formattedValue; + } + if (!_hasErrors) { + fStringExpression = CreateExpression(_buffer.ToString(), initialSourceLocation); + _buffer.Clear(); + } else { + // Clear and recover + _buffer.Clear(); + } + + Debug.Assert(CurrentChar() == '}' || CurrentChar() == '!' || CurrentChar() == ':'); + + var conversion = MaybeReadConversionChar(); + var formatSpecifier = MaybeReadFormatSpecifier(); + Read('}'); + + if (fStringExpression == null) { + return Error(initialPosition); + } + formattedValue = new FormattedValue(fStringExpression, conversion, formatSpecifier); + formattedValue.SetLoc(new IndexSpan(startOfFormattedValue, CurrentLocation().Index - startOfFormattedValue)); + + Debug.Assert(_buffer.Length == 0, "Current buffer is not empty"); + return formattedValue; + } + + private SourceLocation CurrentLocation() { + return new SourceLocation(StartIndex() + _position, _currentLineNumber, _currentColNumber); + } + + private Expression MaybeReadFormatSpecifier() { + Debug.Assert(_buffer.Length == 0); + + Expression formatSpecifier = null; + if (!EndOfFString() && CurrentChar() == ':') { + Read(':'); + var position = _position; + /* Ideally we would just call the FStringParser here. But we are relying on + * an already cut of string, so we need to find the end of the format + * specifier. */ + BufferFormatSpecifier(); + + // If we got to the end, there will be an error when we try to read '}' + if (!EndOfFString()) { + var options = _options.Clone(); + options.InitialSourceLocation = new SourceLocation( + StartIndex() + position, + _currentLineNumber, + _currentColNumber + ); + var formatStr = _buffer.ToString(); + _buffer.Clear(); + var formatSpecifierChildren = new List(); + new FStringParser(formatSpecifierChildren, formatStr, _isRaw, options, _langVersion).Parse(); + formatSpecifier = new FormatSpecifier(formatSpecifierChildren.ToArray(), formatStr); + formatSpecifier.SetLoc(new IndexSpan(StartIndex() + position, formatStr.Length)); + } + } + + return formatSpecifier; + } + + private char? MaybeReadConversionChar() { + char? conversion = null; + if (!EndOfFString() && CurrentChar() == '!') { + Read('!'); + if (EndOfFString()) { + return null; + } + conversion = CurrentChar(); + if (conversion == 's' || conversion == 'r' || conversion == 'a') { + NextChar(); + return conversion; + } else if (conversion == '}' || conversion == ':') { + ReportSyntaxError(Resources.InvalidConversionCharacterFStringErrorMsg); + } else { + NextChar(); + ReportSyntaxError(Resources.InvalidConversionCharacterExpectedFStringErrorMsg.FormatInvariant(conversion)); + } + } + return null; + } + + private void BufferInnerExpression() { + Debug.Assert(_nestedParens.Count == 0); + + char? quoteChar = null; + int stringType = 0; + + while (!EndOfFString()) { + var ch = CurrentChar(); + if (!quoteChar.HasValue && _nestedParens.Count == 0 && (ch == '}' || ch == '!' || ch == ':')) { + // check that it's not a != comparison + if (ch != '!' || !IsNext(NotEqualStringSpan)) { + break; + } + } + if (HasBackslash(ch)) { + ReportSyntaxError(Resources.BackslashFStringExpressionErrorMsg); + _buffer.Append(NextChar()); + continue; + } + + if (quoteChar.HasValue) { + HandleInsideString(ref quoteChar, ref stringType); + } else { + HandleInnerExprOutsideString(ref quoteChar, ref stringType); + } + } + } + + private void BufferFormatSpecifier() { + Debug.Assert(_nestedParens.Count == 0); + + char? quoteChar = null; + int stringType = 0; + + while (!EndOfFString()) { + var ch = CurrentChar(); + if (!quoteChar.HasValue && _nestedParens.Count == 0 && (ch == '}')) { + // check that it's not a != comparison + if (ch != '!' || !IsNext(NotEqualStringSpan)) { + break; + } + } + + if (quoteChar.HasValue) { + /* We're inside a string. See if we're at the end. */ + HandleInsideString(ref quoteChar, ref stringType); + } else { + HandleFormatSpecOutsideString(ref quoteChar, ref stringType); + } + } + } + private void HandleFormatSpecOutsideString(ref char? quoteChar, ref int stringType) { + Debug.Assert(!quoteChar.HasValue); + + var ch = CurrentChar(); + if (ch == '\'' || ch == '"') { + /* Is this a triple quoted string? */ + quoteChar = ch; + if (IsNext(new StringSpan($"{ch}{ch}{ch}", 0, 3))) { + stringType = 3; + _buffer.Append(NextChar()); + _buffer.Append(NextChar()); + _buffer.Append(NextChar()); + return; + } else { + /* Start of a normal string. */ + stringType = 1; + } + /* Start looking for the end of the string. */ + } else if ((ch == ')' || ch == '}') && _nestedParens.Count > 0) { + char opening = _nestedParens.Pop(); + if (!IsOpeningOf(opening, ch)) { + ReportSyntaxError(Resources.ClosingParensNotMatchFStringErrorMsg.FormatInvariant(ch, opening)); + } + } else if ((ch == ')' || ch == '}') && _nestedParens.Count == 0) { + ReportSyntaxError(Resources.UnmatchedFStringErrorMsg.FormatInvariant(ch)); + } else if (ch == '(' || ch == '{') { + _nestedParens.Push(ch); + } + + _buffer.Append(NextChar()); + } + + private void HandleInnerExprOutsideString(ref char? quoteChar, ref int stringType) { + Debug.Assert(!quoteChar.HasValue); + + var ch = CurrentChar(); + if (ch == '\'' || ch == '"') { + /* Is this a triple quoted string? */ + quoteChar = ch; + if (IsNext(new StringSpan($"{ch}{ch}{ch}", 0, 3))) { + stringType = 3; + _buffer.Append(NextChar()); + _buffer.Append(NextChar()); + _buffer.Append(NextChar()); + return; + } else { + /* Start of a normal string. */ + stringType = 1; + } + /* Start looking for the end of the string. */ + } else if (ch == '#') { + ReportSyntaxError(Resources.NumberSignFStringExpressionErrorMsg); + } else if ((ch == ')' || ch == '}') && _nestedParens.Count > 0) { + char opening = _nestedParens.Pop(); + if (!IsOpeningOf(opening, ch)) { + ReportSyntaxError(Resources.ClosingParensNotMatchFStringErrorMsg.FormatInvariant(ch, opening)); + } + } else if ((ch == ')' || ch == '}') && _nestedParens.Count == 0) { + ReportSyntaxError(Resources.UnmatchedFStringErrorMsg.FormatInvariant(ch)); + } else if (ch == '(' || ch == '{') { + _nestedParens.Push(ch); + } + + _buffer.Append(NextChar()); + } + + private bool IsOpeningOf(char opening, char ch) { + switch (opening) { + case '(' when ch == ')': + case '{' when ch == '}': + return true; + default: + return false; + } + } + + private void HandleInsideString(ref char? quoteChar, ref int stringType) { + Debug.Assert(quoteChar.HasValue); + + var ch = CurrentChar(); + /* We're inside a string. See if we're at the end. */ + if (ch == quoteChar.Value) { + /* Does this match the string_type (single or triple + quoted)? */ + if (stringType == 3) { + if (IsNext(new StringSpan($"{ch}{ch}{ch}", 0, 3))) { + /* We're at the end of a triple quoted string. */ + _buffer.Append(NextChar()); + _buffer.Append(NextChar()); + _buffer.Append(NextChar()); + stringType = 0; + quoteChar = null; + return; + } + } else { + /* We're at the end of a normal string. */ + quoteChar = null; + stringType = 0; + } + } + _buffer.Append(NextChar()); + } + + private Expression CreateExpression(string subExprStr, SourceLocation initialSourceLocation) { + if (subExprStr.IsNullOrEmpty()) { + ReportSyntaxError(Resources.EmptyExpressionFStringErrorMsg); + return new ErrorExpression(subExprStr, null); + } + var parser = Parser.CreateParser(new StringReader(subExprStr), _langVersion, new ParserOptions() { + ErrorSink = _errors, + InitialSourceLocation = initialSourceLocation, + ParseFStringExpression = true + }); + var expr = parser.ParseFStrSubExpr(); + if (expr is null) { + // Should not happen but just in case + ReportSyntaxError(Resources.InvalidExpressionFStringErrorMsg); + return Error(_position - subExprStr.Length); + } + return expr; + } + + private bool Read(char nextChar) { + if (EndOfFString()) { + ReportSyntaxError(Resources.ExpectingCharFStringErrorMsg.FormatInvariant(nextChar)); + return false; + } + char ch = CurrentChar(); + NextChar(); + + if (ch != nextChar) { + ReportSyntaxError(Resources.ExpectingCharButFoundFStringErrorMsg.FormatInvariant(nextChar, ch)); + return false; + } + return true; + } + + private void AddBufferedSubstring(SourceLocation bufferStartLoc) { + if (_buffer.Length == 0) { + return; + } + var s = _buffer.ToString(); + _buffer.Clear(); + string contents = ""; + try { + contents = LiteralParser.ParseString(s.ToCharArray(), + 0, s.Length, _isRaw, isUni: true, normalizeLineEndings: true, allowTrailingBackslash: true); + } catch (DecoderFallbackException e) { + var span = new SourceSpan(bufferStartLoc, CurrentLocation()); + _errors.Add(e.Message, span, ErrorCodes.SyntaxError, Severity.Error); + } finally { + var expr = new ConstantExpression(contents); + expr.SetLoc(new IndexSpan(bufferStartLoc.Index, s.Length)); + _fStringChildren.Add(expr); + } + } + + private char NextChar() { + var prev = CurrentChar(); + _position++; + _currentColNumber++; + if (IsLineEnding(prev)) { + _currentColNumber = 1; + _currentLineNumber++; + } + return prev; + } + + private int StartIndex() => _start.Index; + + private bool IsLineEnding(char prev) => prev == '\n' || (prev == '\\' && IsNext(new StringSpan("n", 0, 1))); + + private bool HasBackslash(char ch) => ch == '\\'; + + private char CurrentChar() => _fString[_position]; + + private bool EndOfFString() => _position >= _fString.Length; + + private void ReportSyntaxError(string message) { + _hasErrors = true; + var span = new SourceSpan(new SourceLocation(_start.Index + _position, _currentLineNumber, _currentColNumber), + new SourceLocation(StartIndex() + _position + 1, _currentLineNumber, _currentColNumber + 1)); + _errors.Add(message, span, ErrorCodes.SyntaxError, Severity.Error); + } + + private ErrorExpression Error(int startPos, string verbatimImage = null, Expression preceding = null) { + verbatimImage = verbatimImage ?? (_fString.Substring(startPos, _position - startPos)); + var expr = new ErrorExpression(verbatimImage, preceding); + expr.SetLoc(StartIndex() + startPos, StartIndex() + _position); + return expr; + } + } +} diff --git a/src/Parsing/Impl/LiteralParser.cs b/src/Parsing/Impl/LiteralParser.cs index 467761e61..7e85cffb1 100644 --- a/src/Parsing/Impl/LiteralParser.cs +++ b/src/Parsing/Impl/LiteralParser.cs @@ -26,9 +26,10 @@ namespace Microsoft.Python.Parsing { /// Summary description for ConstantValue. /// internal static class LiteralParser { - public static string ParseString(string text, bool isRaw, bool isUni) => ParseString(text.ToCharArray(), 0, text.Length, isRaw, isUni, false); + public static string ParseString(string text, bool isRaw, bool isUni) => ParseString(text.ToCharArray(), 0, text.Length, isRaw, isUni, false, false); - public static string ParseString(char[] text, int start, int length, bool isRaw, bool isUni, bool normalizeLineEndings) { + public static string ParseString(char[] text, int start, int length, bool isRaw, bool isUni, + bool normalizeLineEndings, bool allowTrailingBackslash = false) { if (text == null) { throw new ArgumentNullException("text"); } @@ -49,7 +50,7 @@ public static string ParseString(char[] text, int start, int length, bool isRaw, } if (i >= l) { - if (isRaw) { + if (isRaw || allowTrailingBackslash) { buf.Append('\\'); break; } diff --git a/src/Parsing/Impl/Parser.cs b/src/Parsing/Impl/Parser.cs index 0066ed775..83f546a12 100644 --- a/src/Parsing/Impl/Parser.cs +++ b/src/Parsing/Impl/Parser.cs @@ -102,7 +102,8 @@ public static Parser CreateParser(TextReader reader, PythonLanguageVersion versi version, options.ErrorSink, (options.Verbatim ? TokenizerOptions.Verbatim : TokenizerOptions.None) | TokenizerOptions.GroupingRecovery | - (options.StubFile ? TokenizerOptions.StubFile : 0), + (options.StubFile ? TokenizerOptions.StubFile : 0) | + (options.ParseFStringExpression ? TokenizerOptions.FStringExpression : 0), (span, text) => options.RaiseProcessComment(parser, new CommentEventArgs(span, text))); tokenizer.Initialize(null, reader, options.InitialSourceLocation ?? SourceLocation.MinValue); tokenizer.IndentationInconsistencySeverity = options.IndentationInconsistencySeverity; @@ -196,6 +197,45 @@ public PythonAst ParseInteractiveCode(out ParseResult properties) { } } + public Expression ParseFStrSubExpr() { + _alwaysAllowContextDependentSyntax = true; + StartParsing(); + + // Read empty spaces + while (MaybeEatNewLine() || MaybeEat(TokenKind.Dedent) || MaybeEat(TokenKind.Indent)) { + ; + } + if (PeekToken(TokenKind.EndOfFile)) { + ReportSyntaxError(Resources.EmptyExpressionFStringErrorMsg); + } + // Yield expressions are allowed + + Expression node = null; + if (PeekToken(TokenKind.KeywordYield)) { + Eat(TokenKind.KeywordYield); + node = ParseYieldExpression(); + } else { + node = ParseTestListAsExpr(); + } + + if (node is LambdaExpression lambda) { + _errors.Add( + Resources.LambdaParenthesesFstringErrorMsg, + new SourceSpan(_tokenizer.IndexToLocation(node.StartIndex), _tokenizer.IndexToLocation(node.EndIndex)), + ErrorCodes.SyntaxError, + Severity.Error + ); + } + + if (_errorCode == 0) { + // Detect if there are unexpected tokens + EatEndOfInput(); + } + + _alwaysAllowContextDependentSyntax = false; + return node; + } + private PythonAst CreateAst(Statement ret) { var ast = new PythonAst(ret, _tokenizer.GetLineLocations(), _tokenizer.LanguageVersion, _tokenizer.GetCommentLocations()); ast.HasVerbatim = _verbatim; @@ -3088,25 +3128,16 @@ private Expression ParsePrimary() { } ret.SetLoc(GetStart(), GetEnd()); return ret; + + case TokenKind.FString: case TokenKind.Constant: // literal NextToken(); var start = GetStart(); var cv = t.Value; var cvs = cv as string; - AsciiString bytes; - if (PeekToken() is ConstantValueToken && (cv is string || cv is AsciiString)) { - // string plus - string[] verbatimImages = null, verbatimWhiteSpace = null; - if (cvs != null) { - cv = FinishStringPlus(cvs, t, out verbatimImages, out verbatimWhiteSpace); - } else if ((bytes = cv as AsciiString) != null) { - cv = FinishBytesPlus(bytes, t, out verbatimImages, out verbatimWhiteSpace); - } - ret = new ConstantExpression(cv); - if (_verbatim) { - AddListWhiteSpace(ret, verbatimWhiteSpace); - AddVerbatimNames(ret, verbatimImages); - } + if (IsStringToken(t)) { + // Might read several tokens for string concatanation + ret = ReadString(); } else { ret = new ConstantExpression(cv); if (_verbatim) { @@ -3136,127 +3167,166 @@ private Expression ParsePrimary() { } } - private string FinishStringPlus(string s, Token initialToken, out string[] verbatimImages, out string[] verbatimWhiteSpace) { - List verbatimImagesList = null; - List verbatimWhiteSpaceList = null; - if (_verbatim) { - verbatimWhiteSpaceList = new List(); - verbatimImagesList = new List(); - verbatimWhiteSpaceList.Add(_tokenWhiteSpace); - verbatimImagesList.Add(initialToken.VerbatimImage); + private bool IsStringToken(Token t) { + if (t.Kind == TokenKind.FString) { + return true; + } else if (t is ConstantValueToken && (t.Value is string || t.Value is AsciiString)) { + return true; } + return false; + } - var res = FinishStringPlus(s, verbatimImagesList, verbatimWhiteSpaceList); - if (_verbatim) { - verbatimWhiteSpace = verbatimWhiteSpaceList.ToArray(); - verbatimImages = verbatimImagesList.ToArray(); + private Expression ReadString() { + var verbatimWhiteSpaceList = new List(); + var verbatimImagesList = new List(); + var readTokens = ReadStringTokens(verbatimWhiteSpaceList, verbatimImagesList, out var hasFStrings, + out var hasStrings, out var hasAsciiStrings); + + Expression expr; + if (hasFStrings) { + expr = buildFStringExpr(readTokens); + } else if (hasStrings) { + expr = buildStringExpr(readTokens); } else { - verbatimWhiteSpace = verbatimImages = null; + expr = buildAsciiStringExpr(readTokens); } - return res; + if (_verbatim) { + if (readTokens.Count > 1) { + AddVerbatimNames(expr, verbatimImagesList.ToArray()); + AddListWhiteSpace(expr, verbatimWhiteSpaceList.ToArray()); + } else { + AddExtraVerbatimText(expr, verbatimImagesList.First()); + AddPreceedingWhiteSpace(expr, verbatimWhiteSpaceList.First()); + } + } + return expr; } - private string FinishStringPlus(string s, List verbatimImages, List verbatimWhiteSpace) { - var t = PeekToken(); - while (true) { - if (t is ConstantValueToken) { - AsciiString bytes; - if (t.Value is String cvs) { - s += cvs; - NextToken(); - if (_verbatim) { - verbatimWhiteSpace.Add(_tokenWhiteSpace); - verbatimImages.Add(t.VerbatimImage); - } - t = PeekToken(); - continue; - } else if ((bytes = t.Value as AsciiString) != null) { - if (_langVersion.Is3x()) { - ReportSyntaxError("cannot mix bytes and nonbytes literals"); - } + private List ReadStringTokens(List verbatimWhiteSpaceList, List verbatimImagesList, out bool hasFStrings, + out bool hasStrings, out bool hasAsciiStrings) { + var readTokens = new List(); + hasFStrings = false; + hasStrings = false; + hasAsciiStrings = false; + do { + var token = _token.Token; + Debug.Assert(IsStringToken(token)); - s += bytes.String; + if (token.Kind == TokenKind.FString) { + if (hasAsciiStrings) { + ReportSyntaxError(_token.Span.Start, _token.Span.End, Resources.MixingBytesAndNonBytesErrorMsg); + } + hasFStrings = true; + } else if (token.Value is string str) { + if (hasAsciiStrings && _langVersion.Is3x()) { + ReportSyntaxError(_token.Span.Start, _token.Span.End, Resources.MixingBytesAndNonBytesErrorMsg); + } + hasStrings = true; + } else if (token.Value is AsciiString asciiStr) { + if ((hasStrings && _langVersion.Is3x()) || hasFStrings) { + ReportSyntaxError(_token.Span.Start, _token.Span.End, Resources.MixingBytesAndNonBytesErrorMsg); + } + hasAsciiStrings = true; + } else { + Debug.Fail("Unhandled string token"); + if (IsStringToken(PeekToken())) { NextToken(); - if (_verbatim) { - verbatimWhiteSpace.Add(_tokenWhiteSpace); - verbatimImages.Add(t.VerbatimImage); - } - t = PeekToken(); - continue; - } else { - ReportSyntaxError("invalid syntax"); } + break; } - break; - } - return s; - } - internal static string MakeString(IList bytes) { - var res = new StringBuilder(bytes.Count); - for (var i = 0; i < bytes.Count; i++) { - res.Append((char)bytes[i]); + readTokens.Add(_token); + if (_verbatim) { + verbatimWhiteSpaceList.Add(_tokenWhiteSpace); + verbatimImagesList.Add(token.VerbatimImage); + } + if (IsStringToken(PeekToken())) { + NextToken(); + } else { + break; + } + } while (true); + + if (PeekToken(TokenKind.Constant)) { + // A string was read and then a Constant that is not a string + ReportSyntaxError(_lookahead.Span.Start, _lookahead.Span.End, Resources.InvalidSyntaxErrorMsg); } - return res.ToString(); + + return readTokens; } - private object FinishBytesPlus(AsciiString s, Token initialToken, out string[] verbatimImages, out string[] verbatimWhiteSpace) { - List verbatimImagesList = null; - List verbatimWhiteSpaceList = null; - if (_verbatim) { - verbatimWhiteSpaceList = new List(); - verbatimImagesList = new List(); - verbatimWhiteSpaceList.Add(_tokenWhiteSpace); - verbatimImagesList.Add(initialToken.VerbatimImage); + private Expression buildAsciiStringExpr(IEnumerable readTokens) { + var strBuilder = new StringBuilder(); + var bytes = new List(); + foreach (var tokenWithSpan in readTokens) { + if (tokenWithSpan.Token.Value is AsciiString asciiString) { + strBuilder.Append(asciiString.String); + bytes.AddRange(asciiString.Bytes); + } } - var res = FinishBytesPlus(s, verbatimImagesList, verbatimWhiteSpaceList); + return new ConstantExpression(new AsciiString(bytes.ToArray(), strBuilder.ToString())); + } - if (_verbatim) { - verbatimWhiteSpace = verbatimWhiteSpaceList.ToArray(); - verbatimImages = verbatimImagesList.ToArray(); - } else { - verbatimWhiteSpace = verbatimImages = null; + private Expression buildStringExpr(IEnumerable readTokens) { + var builder = new StringBuilder(); + foreach (var tokenWithSpan in readTokens) { + if (tokenWithSpan.Token.Value is string str) { + builder.Append(str); + } else if (tokenWithSpan.Token.Value is AsciiString asciiString) { + builder.Append(asciiString.String); + } } - return res; + + return new ConstantExpression(builder.ToString()); } - private object FinishBytesPlus(AsciiString s, List verbatimImages, List verbatimWhiteSpace) { - var t = PeekToken(); - while (true) { - if (t is ConstantValueToken) { - string str; - if (t.Value is AsciiString cvs) { - var res = new List(s.Bytes); - res.AddRange(cvs.Bytes); - s = new AsciiString(res.ToArray(), s.String + cvs.String); - NextToken(); - if (_verbatim) { - verbatimWhiteSpace.Add(_tokenWhiteSpace); - verbatimImages.Add(t.VerbatimImage); - } - t = PeekToken(); - continue; - } else if ((str = t.Value as string) != null) { - if (_langVersion.Is3x()) { - ReportSyntaxError("cannot mix bytes and nonbytes literals"); - } + private Expression buildFStringExpr(IEnumerable readTokens) { + var openQuotes = readTokens.Where(t => t.Token.Kind == TokenKind.FString) + .Select(t => ((FStringToken)t.Token).OpenQuotes).DefaultIfEmpty("'").First(); - var final = s.String + str; - NextToken(); - if (_verbatim) { - verbatimWhiteSpace.Add(_tokenWhiteSpace); - verbatimImages.Add(t.VerbatimImage); - } + List fStringChildren = new List(); + StringBuilder unparsedFStringBuilder = new StringBuilder(); - return FinishStringPlus(final, verbatimImages, verbatimWhiteSpace); - } else { - ReportSyntaxError("invalid syntax"); - } + foreach (var tokenWithSpan in readTokens) { + if (tokenWithSpan.Token.Kind == TokenKind.FString) { + var fToken = (FStringToken)tokenWithSpan.Token; + var sourceLoc = _tokenizer.IndexToLocation(tokenWithSpan.Span.Start); + // Account for f and fr/rf + var offset = 1 + (fToken.IsRaw ? 1 : 0); + var options = new ParserOptions() { + ErrorSink = _errors, + Verbatim = _verbatim, + InitialSourceLocation = new SourceLocation( + index: sourceLoc.Index + offset + fToken.OpenQuotes.Length, + line: sourceLoc.Line, + column: sourceLoc.Column + offset + fToken.OpenQuotes.Length + ) + }; + new FStringParser(fStringChildren, fToken.Text, fToken.IsRaw, options, _langVersion).Parse(); + unparsedFStringBuilder.Append(fToken.Text); + } else if (tokenWithSpan.Token.Value is string str) { + var expr = new ConstantExpression(str); + expr.SetLoc(tokenWithSpan.Span.Start, tokenWithSpan.Span.End); + fStringChildren.Append(expr); + unparsedFStringBuilder.Append(str); + } else if (tokenWithSpan.Token.Value is AsciiString asciiString) { + var expr = new ConstantExpression(asciiString.String); + expr.SetLoc(tokenWithSpan.Span.Start, tokenWithSpan.Span.End); + fStringChildren.Append(expr); + unparsedFStringBuilder.Append(asciiString.String); } - break; } - return s; + + return new FString(fStringChildren.ToArray(), openQuotes, unparsedFStringBuilder.ToString()); + } + + internal static string MakeString(IList bytes) { + var res = new StringBuilder(bytes.Count); + for (var i = 0; i < bytes.Count; i++) { + res.Append((char)bytes[i]); + } + return res.ToString(); } private Expression AddTrailers(Expression ret) => AddTrailers(ret, true); diff --git a/src/Parsing/Impl/ParserOptions.cs b/src/Parsing/Impl/ParserOptions.cs index 75dbe9fb7..9a8e06bc0 100644 --- a/src/Parsing/Impl/ParserOptions.cs +++ b/src/Parsing/Impl/ParserOptions.cs @@ -54,6 +54,11 @@ public ParserOptions() { /// public bool StubFile { get; set; } + /// + /// When true, Parser behaves as if parsing an f-string expression + /// + public bool ParseFStringExpression { get; set; } = false; + /// /// An event that is raised for every comment in the source as it is parsed. /// diff --git a/src/Parsing/Impl/Resources.Designer.cs b/src/Parsing/Impl/Resources.Designer.cs index 3c5eeb9cb..5dbaaf505 100644 --- a/src/Parsing/Impl/Resources.Designer.cs +++ b/src/Parsing/Impl/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Microsoft.PythonTools.Core { +namespace Microsoft.Python.Parsing { using System; @@ -39,7 +39,7 @@ internal Resources() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.PythonTools.Core.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Python.Parsing.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -59,5 +59,131 @@ internal Resources() { resourceCulture = value; } } + + /// + /// Looks up a localized string similar to f-string expression part cannot include a backslash. + /// + internal static string BackslashFStringExpressionErrorMsg { + get { + return ResourceManager.GetString("BackslashFStringExpressionErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: closing parenthesis '{0}' does not match opening parenthesis '{1}'. + /// + internal static string ClosingParensNotMatchFStringErrorMsg { + get { + return ResourceManager.GetString("ClosingParensNotMatchFStringErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: empty expression not allowed. + /// + internal static string EmptyExpressionFStringErrorMsg { + get { + return ResourceManager.GetString("EmptyExpressionFStringErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: expecting '{0}' but found '{1}'. + /// + internal static string ExpectingCharButFoundFStringErrorMsg { + get { + return ResourceManager.GetString("ExpectingCharButFoundFStringErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: expecting '{0}'. + /// + internal static string ExpectingCharFStringErrorMsg { + get { + return ResourceManager.GetString("ExpectingCharFStringErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: invalid conversion character: {0} expected 's', 'r', or 'a'. + /// + internal static string InvalidConversionCharacterExpectedFStringErrorMsg { + get { + return ResourceManager.GetString("InvalidConversionCharacterExpectedFStringErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: invalid conversion character: expected 's', 'r', or 'a'. + /// + internal static string InvalidConversionCharacterFStringErrorMsg { + get { + return ResourceManager.GetString("InvalidConversionCharacterFStringErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: invalid expression. + /// + internal static string InvalidExpressionFStringErrorMsg { + get { + return ResourceManager.GetString("InvalidExpressionFStringErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to invalid syntax. + /// + internal static string InvalidSyntaxErrorMsg { + get { + return ResourceManager.GetString("InvalidSyntaxErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: lambda must be inside parentheses. + /// + internal static string LambdaParenthesesFstringErrorMsg { + get { + return ResourceManager.GetString("LambdaParenthesesFstringErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to cannot mix bytes and nonbytes literals. + /// + internal static string MixingBytesAndNonBytesErrorMsg { + get { + return ResourceManager.GetString("MixingBytesAndNonBytesErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string expression part cannot include '#'. + /// + internal static string NumberSignFStringExpressionErrorMsg { + get { + return ResourceManager.GetString("NumberSignFStringExpressionErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: single '}' is not allowed. + /// + internal static string SingleClosedBraceFStringErrorMsg { + get { + return ResourceManager.GetString("SingleClosedBraceFStringErrorMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to f-string: unmatched '{0}'. + /// + internal static string UnmatchedFStringErrorMsg { + get { + return ResourceManager.GetString("UnmatchedFStringErrorMsg", resourceCulture); + } + } } } diff --git a/src/Parsing/Impl/Resources.resx b/src/Parsing/Impl/Resources.resx index 4fdb1b6af..286f18924 100644 --- a/src/Parsing/Impl/Resources.resx +++ b/src/Parsing/Impl/Resources.resx @@ -1,101 +1,162 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 1.3 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + f-string expression part cannot include a backslash + + + f-string: closing parenthesis '{0}' does not match opening parenthesis '{1}' + + + f-string: empty expression not allowed + + + f-string: expecting '{0}' but found '{1}' + + + f-string: expecting '{0}' + + + f-string: invalid conversion character: {0} expected 's', 'r', or 'a' + + + f-string: invalid conversion character: expected 's', 'r', or 'a' + + + f-string: invalid expression + + + invalid syntax + + + f-string: lambda must be inside parentheses + + + cannot mix bytes and nonbytes literals + + + f-string expression part cannot include '#' + + + f-string: single '}' is not allowed + + + f-string: unmatched '{0}' + \ No newline at end of file diff --git a/src/Parsing/Impl/Token.cs b/src/Parsing/Impl/Token.cs index 494f7e3b5..db7d8d1c5 100644 --- a/src/Parsing/Impl/Token.cs +++ b/src/Parsing/Impl/Token.cs @@ -124,6 +124,38 @@ public VerbatimUnicodeStringToken(object value, string verbatim) public override string VerbatimImage { get; } } + public class FStringToken : Token { + public FStringToken(string value, string openQuote, bool isTriple, bool isRaw) + : base(TokenKind.FString) { + Value = value; + OpenQuotes = openQuote; + IsRaw = isRaw; + } + + public override object Value { get; } + + public string OpenQuotes { get; } + + public bool IsRaw { get; } + + public string Text => (string)Value; + + public override string Image { + get { + return Value == null ? "None" : $"f{OpenQuotes}{Value.ToString()}{OpenQuotes}"; + } + } + } + + public sealed class VerbatimFStringToken : FStringToken { + public VerbatimFStringToken(string value, string openQuotes, bool isTriple, bool isRaw, string verbatim) + : base(value, openQuotes, isTriple, isRaw) { + VerbatimImage = verbatim; + } + + public override string VerbatimImage { get; } + } + public sealed class CommentToken : Token { public CommentToken(string comment) : base(TokenKind.Comment) { diff --git a/src/Parsing/Impl/TokenKind.Generated.cs b/src/Parsing/Impl/TokenKind.Generated.cs index 0d18572eb..bb0aed94d 100644 --- a/src/Parsing/Impl/TokenKind.Generated.cs +++ b/src/Parsing/Impl/TokenKind.Generated.cs @@ -28,6 +28,7 @@ public enum TokenKind { Constant = 9, Ellipsis = 10, Arrow = 11, + FString = 12, Dot = 31, @@ -137,7 +138,7 @@ public static class Tokens { public static readonly Token NewLineToken = new VerbatimToken(TokenKind.NewLine, "\n", ""); public static readonly Token NewLineTokenCRLF = new VerbatimToken(TokenKind.NewLine, "\r\n", ""); public static readonly Token NewLineTokenCR = new VerbatimToken(TokenKind.NewLine, "\r", ""); - + public static readonly Token NLToken = new VerbatimToken(TokenKind.NLToken, "\n", ""); // virtual token used for error reporting public static readonly Token NLTokenCRLF = new VerbatimToken(TokenKind.NLToken, "\r\n", ""); // virtual token used for error reporting public static readonly Token NLTokenCR = new VerbatimToken(TokenKind.NLToken, "\r", ""); // virtual token used for error reporting diff --git a/src/Parsing/Impl/Tokenizer.cs b/src/Parsing/Impl/Tokenizer.cs index 6cad4058b..e5f7c6c8e 100644 --- a/src/Parsing/Impl/Tokenizer.cs +++ b/src/Parsing/Impl/Tokenizer.cs @@ -609,7 +609,7 @@ private int SkipWhiteSpace(int ch, bool atBeginning) { BufferBack(); - if (atBeginning && ch != '#' && ch != '\f' && ch != EOF && !IsEoln(ch)) { + if (atBeginning && !_state.FStringExpression && ch != '#' && ch != '\f' && ch != EOF && !IsEoln(ch)) { MarkTokenEnd(); ReportSyntaxError(BufferTokenSpan, "invalid syntax", ErrorCodes.SyntaxError); } @@ -993,10 +993,10 @@ private Token ContinueString(char quote, bool isRaw, bool isUnicode, bool isByte MarkTokenEnd(); - return MakeStringToken(quote, isRaw, isUnicode, isBytes, isTriple, _start + startAdd, TokenLength - startAdd - end_add); + return MakeStringToken(quote, isRaw, isUnicode, isBytes, isTriple, isFormatted, _start + startAdd, TokenLength - startAdd - end_add); } - private Token MakeStringToken(char quote, bool isRaw, bool isUnicode, bool isBytes, bool isTriple, int start, int length) { + private Token MakeStringToken(char quote, bool isRaw, bool isUnicode, bool isBytes, bool isTriple, bool isFormatted, int start, int length) { bool makeUnicode; if (isUnicode) { makeUnicode = true; @@ -1006,19 +1006,32 @@ private Token MakeStringToken(char quote, bool isRaw, bool isUnicode, bool isByt makeUnicode = LanguageVersion.Is3x() || UnicodeLiterals || StubFile; } + if (isFormatted) { + Debug.Assert(LanguageVersion >= PythonLanguageVersion.V36); + + string contents = new string(_buffer, start, length); + string openQuotes = new string(quote, isTriple ? 3 : 1); + if (Verbatim) { + return new VerbatimFStringToken(contents, openQuotes, isTriple, isRaw, GetTokenString()); + } else { + return new FStringToken(contents, openQuotes, isTriple, isRaw); + } + } + if (makeUnicode) { string contents; try { contents = LiteralParser.ParseString(_buffer, start, length, isRaw, true, !_disableLineFeedLineSeparator); } catch (DecoderFallbackException e) { - _errors.Add(e.Message, _newLineLocations.ToArray(), _tokenStartIndex, _tokenEndIndex, ErrorCodes.SyntaxError, Severity.Error); + _errors.Add(e.Message, IndexToLocation(_tokenStartIndex), + IndexToLocation(_tokenEndIndex), ErrorCodes.SyntaxError, Severity.Error); contents = ""; } - if (Verbatim) { return new VerbatimUnicodeStringToken(contents, GetTokenString()); + } else { + return new UnicodeStringToken(contents); } - return new UnicodeStringToken(contents); } else { var data = LiteralParser.ParseBytes(_buffer, start, length, isRaw, !_disableLineFeedLineSeparator); if (data.Count == 0) { @@ -1889,7 +1902,8 @@ private bool Equals(string value) { #endregion } - public int GroupingLevel => _state.ParenLevel + _state.BraceLevel + _state.BracketLevel; + public int GroupingLevel => _state.ParenLevel + _state.BraceLevel + _state.BracketLevel + + (_state.FStringExpression ? 1 : 0); /// /// True if the last characters in the buffer are a backslash followed by a new line indicating @@ -2265,6 +2279,7 @@ struct State : IEquatable { // grouping state public int ParenLevel, BraceLevel, BracketLevel; + public bool FStringExpression; // white space tracking public StringBuilder CurWhiteSpace; @@ -2277,6 +2292,7 @@ public State(State state, bool verbatim) { BracketLevel = state.BraceLevel; ParenLevel = state.ParenLevel; BraceLevel = state.BraceLevel; + FStringExpression = state.FStringExpression; PendingDedents = state.PendingDedents; IndentLevel = state.IndentLevel; IndentFormat = (string[])state.IndentFormat?.Clone(); @@ -2296,6 +2312,7 @@ public State(TokenizerOptions options) { Indent = new int[MaxIndent]; // TODO LastNewLine = true; BracketLevel = ParenLevel = BraceLevel = PendingDedents = IndentLevel = 0; + FStringExpression = (options & TokenizerOptions.FStringExpression) != 0; IndentFormat = null; IncompleteString = null; if ((options & TokenizerOptions.Verbatim) != 0) { diff --git a/src/Parsing/Impl/TokenizerOptions.cs b/src/Parsing/Impl/TokenizerOptions.cs index 534d8f3f6..4a9eab55e 100644 --- a/src/Parsing/Impl/TokenizerOptions.cs +++ b/src/Parsing/Impl/TokenizerOptions.cs @@ -49,5 +49,10 @@ public enum TokenizerOptions { /// of the specified version. /// StubFile = 0x08, + /// + /// Set to true for parsing f-string expressions. These behave as if they were enclosed + /// by implicit parentheses + /// + FStringExpression = 0x10, } } diff --git a/src/Parsing/Test/ParserRoundTripTest.cs b/src/Parsing/Test/ParserRoundTripTest.cs index e96341c69..2b5fca27f 100644 --- a/src/Parsing/Test/ParserRoundTripTest.cs +++ b/src/Parsing/Test/ParserRoundTripTest.cs @@ -856,6 +856,11 @@ public void TestBinaryFiles() { TestOneString(PythonLanguageVersion.V27, originalText); } + [TestMethod, Priority(1)] + public void TestFStringWithoutVerbatim() { + TestOneString(PythonLanguageVersion.V36, "f'''sss {1:5}'''", null, null, true, null, false); + } + [TestMethod, Priority(1)] public void TestErrors() { TestOneString(PythonLanguageVersion.V30, ": ..."); @@ -1690,14 +1695,15 @@ public static void TestOneString( CodeFormattingOptions format = null, string expected = null, bool recurse = true, - string filename = null + string filename = null, + bool verbatim = true ) { bool hadExpected = true; if (expected == null) { expected = originalText; hadExpected = false; } - var parser = Parser.CreateParser(new StringReader(originalText), version, new ParserOptions() { Verbatim = true }); + var parser = Parser.CreateParser(new StringReader(originalText), version, new ParserOptions() { Verbatim = verbatim }); var ast = parser.ParseFile(); string output; diff --git a/src/Parsing/Test/ParserTests.cs b/src/Parsing/Test/ParserTests.cs index fe75e34f7..5c98a4492 100644 --- a/src/Parsing/Test/ParserTests.cs +++ b/src/Parsing/Test/ParserTests.cs @@ -99,6 +99,290 @@ public void Errors35() { ); } + [TestMethod, Priority(0)] + public void FStringErrors() { + ParseErrors("FStringErrors.py", + PythonLanguageVersion.V36, + new ErrorInfo("f-string: expecting '}'", 3, 1, 4, 4, 1, 5), + new ErrorInfo("f-string expression part cannot include a backslash", 9, 2, 4, 10, 2, 5), + new ErrorInfo("unexpected token 'import'", 26, 4, 2, 32, 4, 8), + new ErrorInfo("unexpected token 'def'", 61, 7, 2, 64, 7, 5), + new ErrorInfo("cannot mix bytes and nonbytes literals", 94, 11, 5, 97, 11, 8), + new ErrorInfo("f-string: expecting '}'", 113, 13, 13, 114, 13, 14), + new ErrorInfo("f-string: single '}' is not allowed", 120, 15, 3, 121, 15, 4), + new ErrorInfo("f-string expression part cannot include '#'", 129, 17, 4, 130, 17, 5), + new ErrorInfo("f-string: expecting '}'", 139, 19, 4, 140, 19, 5), + new ErrorInfo("unexpected token 'import'", 147, 21, 4, 153, 21, 10), + new ErrorInfo("f-string: empty expression not allowed", 169, 23, 4, 170, 23, 5), + new ErrorInfo("unexpected token '='", 180, 25, 6, 181, 25, 7), + new ErrorInfo("expected ':'", 200, 27, 12, 200, 27, 12), + new ErrorInfo("f-string: lambda must be inside parentheses", 192, 27, 4, 200, 27, 12), + new ErrorInfo("f-string: expecting '}'", 214, 29, 6, 215, 29, 7), + new ErrorInfo("f-string: invalid conversion character: expected 's', 'r', or 'a'", 224, 31, 6, 225, 31, 7), + new ErrorInfo("f-string: invalid conversion character: k expected 's', 'r', or 'a'", 236, 33, 7, 237, 33, 8), + new ErrorInfo("f-string: unmatched ')'", 245, 35, 4, 246, 35, 5), + new ErrorInfo("f-string: unmatched ')'", 257, 37, 6, 258, 37, 7), + new ErrorInfo("f-string: closing parenthesis '}' does not match opening parenthesis '('", 269, 39, 6, 270, 39, 7) + ); + } + + [TestMethod, Priority(0)] + public void FStrings() { + foreach (var version in V36AndUp) { + var errors = new CollectingErrorSink(); + CheckAst( + ParseFile("FStrings.py", errors, version), + CheckSuite( + CheckAssignment( + CheckNameExpr("some"), + One + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("text") + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + CheckNameExpr("some") + ) + ) + ), + CheckFuncDef("f", NoParameters, CheckSuite( + CheckReturnStmt( + CheckFString( + CheckFormattedValue( + CheckYieldExpr(CheckNameExpr("some")) + ) + ) + ) + )), + CheckExprStmt( + CheckFString( + CheckNodeConstant("result: "), + CheckFormattedValue( + CheckFString( + CheckFormattedValue( + CheckNameExpr("some") + ) + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("{{text "), + CheckFormattedValue( + CheckNameExpr("some") + ), + CheckNodeConstant(" }}") + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + CheckBinaryExpression( + CheckNameExpr("some"), + PythonOperator.Add, + One + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("Has a :") + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + One, + null, + CheckFormatSpecifer( + CheckFormattedValue( + CheckConstant("{") + ), + CheckNodeConstant(">10") + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + CheckNameExpr("some") + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("\n") + ) + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("space between opening braces: "), + CheckFormattedValue( + CheckSetComp( + CheckNameExpr("thing"), + CompFor( + CheckNameExpr("thing"), + CheckTupleExpr( + One, + CheckConstant(2), + CheckConstant(3) + ) + ) + ) + ) + ) + ), + CheckExprStmt( + CheckCallExpression( + CheckNameExpr("print"), + PositionalArg( + CheckFString( + CheckNodeConstant("first: "), + CheckFormattedValue( + CheckFString( + CheckNodeConstant("second "), + CheckFormattedValue( + CheckNameExpr("some") + ) + ) + ) + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + CheckNameExpr("some"), + 'r', + CheckFormatSpecifer( + CheckFormattedValue( + CheckNameExpr("some") + ) + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + CheckNameExpr("some"), + null, + CheckFormatSpecifer( + CheckNodeConstant("#06x") + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("\n"), + CheckFormattedValue( + One + ), + CheckNodeConstant("\n") + ) + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("{{nothing") + ) + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("Hello '"), + CheckFormattedValue( + CheckBinaryExpression( + CheckFString( + CheckFormattedValue( + CheckNameExpr("some") + ) + ), + PythonOperator.Add, + CheckConstant("example") + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + CheckConstant(3.14), + null, + CheckFormatSpecifer( + CheckNodeConstant("!<10.10") + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + CheckBinaryExpression( + One, + PythonOperator.Add, + One + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("\\N{GREEK CAPITAL LETTER DELTA}") + ) + ), + CheckExprStmt( + CheckFString( + CheckNodeConstant("\\"), + CheckFormattedValue( + One + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + CheckNameExpr("a"), + null, + CheckFormatSpecifer( + CheckNodeConstant("{{}}") + ) + ) + ) + ), + CheckExprStmt( + CheckFString( + CheckFormattedValue( + CheckCallExpression( + CheckParenExpr( + CheckLambda( + new[] { CheckParameter("x") }, + CheckBinaryExpression( + CheckNameExpr("x"), + PythonOperator.Add, + One + ) + ) + ), + PositionalArg(One) + ) + ) + ) + ) + ) + ); + + errors.Errors.Should().BeEmpty(); + } + } + [TestMethod, Priority(0)] public void GeneralizedUnpacking() { foreach (var version in V35AndUp) { @@ -4040,6 +4324,13 @@ private Action CheckAsyncWithStmt(Action checkWithStmt) { }; } + private static Action CheckNodeConstant(object value, string expectedRepr = null, PythonLanguageVersion ver = PythonLanguageVersion.V27) { + return node => { + Assert.IsInstanceOfType(node, typeof(Expression)); + CheckConstant(value)((Expression)node); + }; + } + private static Action CheckConstant(object value, string expectedRepr = null, PythonLanguageVersion ver = PythonLanguageVersion.V27) { return expr => { Assert.AreEqual(typeof(ConstantExpression), expr.GetType()); @@ -4228,6 +4519,43 @@ private Action CheckSetLiteral(params Action[] values) { }; } + private static Action CheckFString(params Action[] subExpressions) { + return expr => { + Assert.AreEqual(typeof(FString), expr.GetType()); + var nodes = expr.GetChildNodes().ToArray(); + Assert.AreEqual(nodes.Length, subExpressions.Length, "Wrong amount of nodes in fstring"); + for (var i = 0; i < subExpressions.Length; i++) { + subExpressions[i](nodes[i]); + } + }; + } + + private static Action CheckFormattedValue(Action value, char? conversion = null, Action formatSpecifier = null) { + return node => { + Assert.AreEqual(typeof(FormattedValue), node.GetType()); + var formattedValue = (FormattedValue)node; + + value(formattedValue.Value); + Assert.AreEqual(formattedValue.Conversion, conversion, "formatted value's conversion is not correct"); + if (formatSpecifier == null) { + Assert.AreEqual(formattedValue.FormatSpecifier, null, "format specifier is not null"); + } else { + formatSpecifier(formattedValue.FormatSpecifier); + } + }; + } + + private static Action CheckFormatSpecifer(params Action[] subExpressions) { + return expr => { + Assert.AreEqual(typeof(FormatSpecifier), expr.GetType()); + + var nodes = expr.GetChildNodes().ToArray(); + Assert.AreEqual(nodes.Length, subExpressions.Length, "Wrong amount of nodes in format specifier"); + for (var i = 0; i < subExpressions.Length; i++) { + subExpressions[i](nodes[i]); + } + }; + } private Action CompFor(Action lhs, Action list) { return iter => { diff --git a/src/UnitTests/TestData/Grammar/FStringErrors.py b/src/UnitTests/TestData/Grammar/FStringErrors.py new file mode 100644 index 000000000..43ad00176 --- /dev/null +++ b/src/UnitTests/TestData/Grammar/FStringErrors.py @@ -0,0 +1,39 @@ +f'{' +f'{\n}' +f''' +{import something} +''' +f''' +{def test: + pass} +''' + +f'' b'' + +f'{something' + +f'}' + +f'{#}' + +f"{" + +f'{import random}' + +f'{}' + +f'{a = 1}' + +f'{lambda x: 1}' + +f'\N{' + +f'{1!}' + +f'{1!k}' + +f'{)}' + +f'{1:)}' + +f'{ (} }' diff --git a/src/UnitTests/TestData/Grammar/FStrings.py b/src/UnitTests/TestData/Grammar/FStrings.py new file mode 100644 index 000000000..ee04ea4a3 --- /dev/null +++ b/src/UnitTests/TestData/Grammar/FStrings.py @@ -0,0 +1,48 @@ +some = 1 +f'text' + +f'{some}' + +def f(): + return f'{yield some}' + +f"result: {f'{some}'}" +f'{{text {some} }}' +f'{some + 1}' + +f"Has a :" + +f'{1:{"{"}>10}' + +fr'{some}' +f'\n' + +f'space between opening braces: { {thing for thing in (1, 2, 3)}}' + +print(f'''first: {f'second {some}'}''') + +f'{some!r:{some}}' + +f'{some:#06x}' + +f''' +{ + 1} +''' +f'{{nothing' + +f'Hello \'{f"{some}" + "example"}' + +f'{3.14:!<10.10}' + +f'''{1+ +1 +}''' + +f'\N{GREEK CAPITAL LETTER DELTA}' + +f'\{1}' + +f'{a:{{}}}' + +f'{(lambda x: x + 1)(1)}'