Skip to content

Commit

Permalink
Soy to Python CL #8. Design Doc: []
Browse files Browse the repository at this point in the history
Support for IfNodes as traditional conditionals and ternary expressions where possible.
-------------
Created by MOE: http://code.google.com/p/moe-java
MOE_MIGRATED_REVID=86653446
  • Loading branch information
DavidCPhillips authored and Brendan Linn committed Feb 20, 2015
1 parent 5d3a12e commit 52647e7
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.google.template.soy.internal.base.Pair;
import com.google.template.soy.internal.i18n.SoyBidiUtils;
import com.google.template.soy.pysrc.internal.GenPyExprsVisitor.GenPyExprsVisitorFactory;
import com.google.template.soy.pysrc.internal.TranslateToPyExprVisitor.TranslateToPyExprVisitorFactory;
import com.google.template.soy.pysrc.restricted.PyExpr;
import com.google.template.soy.pysrc.restricted.PyExprUtils;
import com.google.template.soy.shared.internal.FindCalleesNotInFileVisitor;
Expand All @@ -30,6 +31,9 @@
import com.google.template.soy.shared.restricted.ApiCallScopeBindingAnnotations.TranslationPyModuleName;
import com.google.template.soy.sharedpasses.ShouldEnsureDataIsDefinedVisitor;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.IfCondNode;
import com.google.template.soy.soytree.IfElseNode;
import com.google.template.soy.soytree.IfNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.SoyFileNode;
import com.google.template.soy.soytree.SoyFileSetNode;
Expand Down Expand Up @@ -75,10 +79,10 @@ final class GenPyCodeVisitor extends AbstractSoyNodeVisitor<List<String>> {

@VisibleForTesting protected GenPyExprsVisitor genPyExprsVisitor;

private final TranslateToPyExprVisitorFactory translateToPyExprVisitorFactory;

/**
* @param runtimePath The module path for the runtime libraries.
* @param isComputableAsPyExprVisitor The IsComputableAsPyExprVisitor to use.
* @param genPyExprsVisitorFactory Factory for creating an instance of GenPyExprsVisitor.
* @param translationPyModuleName Python module name used in python runtime to instantiate
* translation.
*/
Expand All @@ -87,12 +91,14 @@ final class GenPyCodeVisitor extends AbstractSoyNodeVisitor<List<String>> {
@BidiIsRtlFn String bidiIsRtlFn,
@TranslationPyModuleName String translationPyModuleName,
IsComputableAsPyExprVisitor isComputableAsPyExprVisitor,
GenPyExprsVisitorFactory genPyExprsVisitorFactory) {
GenPyExprsVisitorFactory genPyExprsVisitorFactory,
TranslateToPyExprVisitorFactory translateToPyExprVisitorFactory) {
this.runtimePath = runtimePath;
this.bidiIsRtlFn = bidiIsRtlFn;
this.translationPyModuleName = translationPyModuleName;
this.isComputableAsPyExprVisitor = isComputableAsPyExprVisitor;
this.genPyExprsVisitorFactory = genPyExprsVisitorFactory;
this.translateToPyExprVisitorFactory = translateToPyExprVisitorFactory;
}


Expand Down Expand Up @@ -246,6 +252,56 @@ final class GenPyCodeVisitor extends AbstractSoyNodeVisitor<List<String>> {
pyCodeBuilder.addToOutputVar(genPyExprsVisitor.exec(node));
}

/**
* Visit an IfNode and generate a full conditional statement, or an inline ternary conditional
* expression if all the children are computable as expressions.
*
* <p>Example:
* <pre>
* {if $boo > 0}
* ...
* {/if}
* </pre>
* might generate
* <pre>
* if opt_data.get('boo') > 0:
* ...
* </pre>
*/
@Override protected void visitIfNode(IfNode node) {
if (isComputableAsPyExprVisitor.exec(node)) {
pyCodeBuilder.addToOutputVar(genPyExprsVisitor.exec(node));
return;
}

// Not computable as Python expressions, so generate full code.
TranslateToPyExprVisitor translator = translateToPyExprVisitorFactory.create();
for (SoyNode child : node.getChildren()) {
if (child instanceof IfCondNode) {
IfCondNode icn = (IfCondNode) child;
PyExpr condPyExpr = translator.exec(icn.getExprUnion().getExpr());

if (icn.getCommandName().equals("if")) {
pyCodeBuilder.appendLine("if ", condPyExpr.getText(), ":");
} else {
pyCodeBuilder.appendLine("elif ", condPyExpr.getText(), ":");
}

pyCodeBuilder.increaseIndent();
visit(icn);
pyCodeBuilder.decreaseIndent();

} else if (child instanceof IfElseNode) {
pyCodeBuilder.appendLine("else:");
pyCodeBuilder.increaseIndent();
visit(child);
pyCodeBuilder.decreaseIndent();
} else {
throw new AssertionError("Unexpected if child node type. Child: " + child);
}
}
}


// -----------------------------------------------------------------------------------------------
// Fallback implementation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
import com.google.template.soy.pysrc.restricted.PyStringExpr;
import com.google.template.soy.pysrc.restricted.SoyPySrcPrintDirective;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
import com.google.template.soy.soytree.IfCondNode;
import com.google.template.soy.soytree.IfElseNode;
import com.google.template.soy.soytree.IfNode;
import com.google.template.soy.soytree.MsgFallbackGroupNode;
import com.google.template.soy.soytree.MsgNode;
import com.google.template.soy.soytree.PrintDirectiveNode;
Expand Down Expand Up @@ -207,4 +210,62 @@ public static interface GenPyExprsVisitorFactory {
MsgFuncGenerator msgFuncGenerator = msgFuncGeneratorFactory.create(node);
pyExprs.add(msgFuncGenerator.getPyExpr());
}

/**
* If all the children are computable as expressions, the IfNode can be written as a ternary
* conditional expression.
*/
@Override protected void visitIfNode(IfNode node) {
// Create another instance of this visitor for generating Python expressions from children.
GenPyExprsVisitor genPyExprsVisitor = genPyExprsVisitorFactory.create();
TranslateToPyExprVisitor translator = translateToPyExprVisitorFactory.create();

StringBuilder pyExprTextSb = new StringBuilder();

boolean hasElse = false;
for (SoyNode child : node.getChildren()) {

if (child instanceof IfCondNode) {
IfCondNode icn = (IfCondNode) child;

// Python ternary conditional expressions modify the order of the conditional from
// <conditional> ? <true> : <false> to
// <true> if <conditional> else <false>
PyExpr condBlock = PyExprUtils.concatPyExprs(genPyExprsVisitor.exec(icn)).toPyString();
condBlock = PyExprUtils.maybeProtect(condBlock,
PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL));
pyExprTextSb.append(condBlock.getText());

// Append the conditional and if/else syntax.
PyExpr condPyExpr = translator.exec(icn.getExprUnion().getExpr());
pyExprTextSb.append(" if ").append(condPyExpr.getText()).append(" else ");

} else if (child instanceof IfElseNode) {
hasElse = true;
IfElseNode ien = (IfElseNode) child;

PyExpr elseBlock = PyExprUtils.concatPyExprs(genPyExprsVisitor.exec(ien)).toPyString();
pyExprTextSb.append(elseBlock.getText());
} else {
throw new AssertionError("Unexpected if child node type. Child: " + child);
}
}

if (!hasElse) {
pyExprTextSb.append("''");
}

// By their nature, inline'd conditionals can only contain output strings, so they can be
// treated as a string type with a conditional precedence.
pyExprs.add(new PyStringExpr(pyExprTextSb.toString(),
PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL)));
}

@Override protected void visitIfCondNode(IfCondNode node) {
visitChildren(node);
}

@Override protected void visitIfElseNode(IfElseNode node) {
visitChildren(node);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ private String visitNullSafeNodeRecurse(ExprNode node, StringBuilder nullSafetyP

default: {
PyExpr value = visit(node);
return genMaybeProtect(value, Integer.MAX_VALUE);
return PyExprUtils.maybeProtect(value, Integer.MAX_VALUE).getText();
}
}
}
Expand Down Expand Up @@ -260,6 +260,7 @@ private String visitNullSafeNodeRecurse(ExprNode node, StringBuilder nullSafetyP
@Override protected PyExpr visitConditionalOpNode(ConditionalOpNode node) {
// Retrieve the operands.
Operator op = Operator.CONDITIONAL;
int conditionalPrecedence = PyExprUtils.pyPrecedenceForOperator(op);
List<SyntaxElement> syntax = op.getSyntax();
List<PyExpr> operandExprs = visitChildren(node);

Expand All @@ -273,13 +274,13 @@ private String visitNullSafeNodeRecurse(ExprNode node, StringBuilder nullSafetyP
// Python's ternary operator switches the order from <conditional> ? <true> : <false> to
// <true> if <conditional> else <false>.
StringBuilder exprSb = new StringBuilder();
exprSb.append(genMaybeProtect(trueExpr, PyExprUtils.pyPrecedenceForOperator(op)));
exprSb.append(PyExprUtils.maybeProtect(trueExpr, conditionalPrecedence).getText());
exprSb.append(" if ");
exprSb.append(genMaybeProtect(conditionalExpr, PyExprUtils.pyPrecedenceForOperator(op)));
exprSb.append(PyExprUtils.maybeProtect(conditionalExpr, conditionalPrecedence).getText());
exprSb.append(" else ");
exprSb.append(genMaybeProtect(falseExpr, PyExprUtils.pyPrecedenceForOperator(op)));
exprSb.append(PyExprUtils.maybeProtect(falseExpr, conditionalPrecedence).getText());

return new PyExpr(exprSb.toString(), PyExprUtils.pyPrecedenceForOperator(op));
return new PyExpr(exprSb.toString(), conditionalPrecedence);
}

@Override protected PyExpr visitFunctionNode(FunctionNode node) {
Expand Down Expand Up @@ -367,8 +368,4 @@ private PyExpr genPyExprUsingSoySyntax(OperatorNode opNode) {

return new PyExpr(newExpr, PyExprUtils.pyPrecedenceForOperator(opNode.getOperator()));
}

private static String genMaybeProtect(PyExpr expr, int minSafePrecedence) {
return (expr.getPrecedence() > minSafePrecedence) ? expr.getText() : "(" + expr.getText() + ")";
}
}
18 changes: 18 additions & 0 deletions java/src/com/google/template/soy/pysrc/restricted/PyExprUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,24 @@ public static PyExpr genPyNotNullCheck(PyExpr pyExpr) {
return new PyExpr(conditionalExpr, PyExprUtils.pyPrecedenceForOperator(Operator.NOT_EQUAL));
}

/**
* Wraps an expression with parenthesis if it's not above the minimum safe precedence.
*
* <p>NOTE: For the sake of brevity, this implementation loses typing information in the
* expressions.
*
* @param expr The expression to wrap.
* @param minSafePrecedence The minimum safe precedence (not inclusive).
* @return The PyExpr potentially wrapped in parenthesis.
*/
public static PyExpr maybeProtect(PyExpr expr, int minSafePrecedence) {
if (expr.getPrecedence() > minSafePrecedence) {
return expr;
} else {
return new PyExpr("(" + expr.getText() + ")", Integer.MAX_VALUE);
}
}

/**
* Wraps an expression with the proper SanitizedContent constructor if contentKind is non-null.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
/**
* Unit tests for GenPyCodeVisitor.
*
* <p>TODO(dcphillips): Add non-inlined 'if' test after adding LetNode support.
*
*/
public final class GenPyCodeVisitorTest extends TestCase {

Expand Down Expand Up @@ -68,6 +70,7 @@ public final class GenPyCodeVisitorTest extends TestCase {

private GenPyCodeVisitor genPyCodeVisitor;


@Override protected void setUp() {
// Default to empty values for the bidi and translation configs.
setupGenPyCodeVisitor("", "");
Expand Down Expand Up @@ -173,6 +176,7 @@ public void testMsg() {
// -----------------------------------------------------------------------------------------------
// Test Utilities.


private void assertGeneratedPyFile(String soyCode, String expectedPyCode) {
assertThat(getGeneratedPyFile(soyCode)).isEqualTo(expectedPyCode);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2015 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.template.soy.pysrc.internal;

import static com.google.template.soy.pysrc.internal.PyCodeSubject.assertThatSoyCode;

import com.google.common.collect.ImmutableList;
import com.google.template.soy.exprtree.Operator;
import com.google.template.soy.pysrc.restricted.PyExpr;
import com.google.template.soy.pysrc.restricted.PyExprUtils;

import junit.framework.TestCase;

/**
* Unit tests for GenPyExprsVisitor.
*
*/
public final class GenPyExprsVisitorTest extends TestCase {

public void testRawText() {
assertThatSoyCode("I'm feeling lucky!").compilesTo(
ImmutableList.of(new PyExpr("'I\\'m feeling lucky!'", Integer.MAX_VALUE)));
}

public void testIf() {
String soyNodeCode =
"{if $boo}\n" +
" Blah\n" +
"{elseif not $goo}\n" +
" Bleh\n" +
"{else}\n" +
" Bluh\n" +
"{/if}\n";
String expectedPyExprText =
"'Blah' if opt_data.get('boo') else 'Bleh' if not opt_data.get('goo') else 'Bluh'";

assertThatSoyCode(soyNodeCode).compilesTo(
ImmutableList.of(new PyExpr(expectedPyExprText,
PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL))));
}

public void testIf_nested() {
String soyNodeCode =
"{if $boo}\n" +
" {if $goo}\n" +
" Blah\n" +
" {/if}\n" +
"{else}\n" +
" Bleh\n" +
"{/if}\n";
String expectedPyExprText =
"('Blah' if opt_data.get('goo') else '') if opt_data.get('boo') else 'Bleh'";

assertThatSoyCode(soyNodeCode).compilesTo(
ImmutableList.of(new PyExpr(expectedPyExprText,
PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL))));
}
}
Loading

0 comments on commit 52647e7

Please sign in to comment.