From 415f56289674297e5aec406960ec5c4b02adce89 Mon Sep 17 00:00:00 2001
From: cclauss <cclauss@bluewin.ch>
Date: Fri, 24 Feb 2017 10:45:12 +0100
Subject: [PATCH] Add python:3 action support.

Update tests to work in both Python 2 and Python 3.
Rename pythonaction to python3action for container image name for clarity.
Add tests for python:2 and python:3.
Add image names for all the actions. Rename javaaction to java8action for consistency.
---
 core/actionProxy/actionproxy.py               |  5 +--
 core/python2Action/Dockerfile                 | 38 +++++++++++++++++++
 core/python2Action/build.gradle               | 19 ++++++++++
 core/pythonAction/Dockerfile                  | 22 +++++------
 core/pythonAction/build.gradle                |  2 +-
 settings.gradle                               |  1 +
 tests/build.gradle                            |  1 +
 .../ActionProxyContainerTests.scala           |  9 +++--
 .../Python2ActionContainerTests.scala         | 30 +++++++++++++++
 .../PythonActionContainerTests.scala          | 33 +++++++++++-----
 10 files changed, 132 insertions(+), 28 deletions(-)
 create mode 100644 core/python2Action/Dockerfile
 create mode 100644 core/python2Action/build.gradle
 create mode 100644 tests/src/test/scala/actionContainers/Python2ActionContainerTests.scala

diff --git a/core/actionProxy/actionproxy.py b/core/actionProxy/actionproxy.py
index 5975e35c..1c162798 100644
--- a/core/actionProxy/actionproxy.py
+++ b/core/actionProxy/actionproxy.py
@@ -235,12 +235,11 @@ def run():
 
     if runner.verify():
         try:
-            (code, result) = runner.run(args,
-                                        runner.env(message if message else {}))
+            code, result = runner.run(args, runner.env(message or {}))
             response = flask.jsonify(result)
             response.status_code = code
         except Exception as e:
-            response = flask.jsonify({'error': 'Internal error.'})
+            response = flask.jsonify({'error': 'Internal error. {}'.format(e)})
             response.status_code = 500
     else:
         response = flask.jsonify({'error': 'The action failed to locate '
diff --git a/core/python2Action/Dockerfile b/core/python2Action/Dockerfile
new file mode 100644
index 00000000..0bb0d204
--- /dev/null
+++ b/core/python2Action/Dockerfile
@@ -0,0 +1,38 @@
+# Dockerfile for Python 2 actions, similar to the Python 3-based core/pythonAction
+FROM python:2.7.12-alpine
+
+# Upgrade and install basic Python dependencies
+RUN apk add --no-cache \
+        bash \
+        bzip2-dev \
+        gcc \
+        libc-dev \
+        libxslt-dev \
+        libxml2-dev \
+        libffi-dev \
+        openssl-dev \
+        python-dev
+
+# Install common modules for python
+RUN pip install --no-cache-dir --upgrade pip setuptools \
+ && pip install --no-cache-dir \
+        gevent==1.1.2 \
+        flask==0.11.1 \
+        beautifulsoup4==4.5.1 \
+        httplib2==0.9.2 \
+        kafka_python==1.3.1 \
+        lxml==3.6.4 \
+        python-dateutil==2.5.3 \
+        requests==2.11.1 \
+        scrapy==1.1.2 \
+        simplejson==3.8.2 \
+        virtualenv==15.1.0 \
+        twisted==16.4.0
+
+ENV FLASK_PROXY_PORT 8080
+
+RUN mkdir -p /pythonAction
+ADD actionproxy.py /pythonAction/
+ADD pythonrunner.py /pythonAction/
+
+CMD ["/bin/bash", "-c", "cd pythonAction && python -u pythonrunner.py"]
diff --git a/core/python2Action/build.gradle b/core/python2Action/build.gradle
new file mode 100644
index 00000000..a7a3f61e
--- /dev/null
+++ b/core/python2Action/build.gradle
@@ -0,0 +1,19 @@
+ext.dockerImageName = 'python2action'
+apply from: '../../gradle/docker.gradle'
+distDocker.dependsOn 'copyFiles'
+distDocker.finalizedBy 'rmFiles'
+
+def runners = files(
+    new File(project(':core:actionProxy').projectDir, 'actionproxy.py'),
+    new File(project(':core:pythonAction').projectDir, 'pythonrunner.py')
+)
+
+task copyFiles(type: Copy) {
+    from runners
+    into '.'
+}
+
+task rmFiles(type: Delete) {
+    delete runners.collect { it.getName() }
+}
+
diff --git a/core/pythonAction/Dockerfile b/core/pythonAction/Dockerfile
index e096e8e9..e2a1cf5b 100644
--- a/core/pythonAction/Dockerfile
+++ b/core/pythonAction/Dockerfile
@@ -8,20 +8,20 @@ RUN apk add --no-cache \
         libxslt-dev \
         libxml2-dev \
         libffi-dev \
-        openssl-dev \
-        python-dev
+        openssl-dev
 
 # Install common modules for python
 RUN pip install \
-    beautifulsoup4==4.5.1 \
-    httplib2==0.9.2 \
-    kafka_python==1.3.1 \
-    lxml==3.6.4 \
-    python-dateutil==2.5.3 \
-    requests==2.11.1 \
-    scrapy==1.1.2 \
-    simplejson==3.8.2 \
-    twisted==16.4.0
+    beautifulsoup4==4.5.3 \
+    httplib2==0.10.3 \
+    kafka_python==1.3.2 \
+    lxml==3.7.3 \
+    python-dateutil==2.6.0 \
+    requests==2.13.0 \
+    scrapy==1.3.3 \
+    simplejson==3.10.0 \
+    virtualenv==15.1.0 \
+    twisted==17.1.0
 
 ENV FLASK_PROXY_PORT 8080
 
diff --git a/core/pythonAction/build.gradle b/core/pythonAction/build.gradle
index 5d192552..0149b709 100644
--- a/core/pythonAction/build.gradle
+++ b/core/pythonAction/build.gradle
@@ -1,3 +1,3 @@
-ext.dockerImageName = 'pythonaction'
+ext.dockerImageName = 'python3action'
 apply from: '../../gradle/docker.gradle'
 distDocker.dependsOn ':core:actionProxy:distDocker'
diff --git a/settings.gradle b/settings.gradle
index ed84a466..1955febb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,6 +6,7 @@ include 'core:nodejsActionBase'
 include 'core:nodejs6Action'
 include 'core:actionProxy'
 include 'core:pythonAction'
+include 'core:python2Action'
 include 'core:swift3Action'
 include 'core:javaAction'
 
diff --git a/tests/build.gradle b/tests/build.gradle
index f9e95d98..32275c2e 100644
--- a/tests/build.gradle
+++ b/tests/build.gradle
@@ -27,6 +27,7 @@ test.dependsOn([
     ':core:nodejs6Action:distDocker',
     ':core:actionProxy:distDocker',
     ':core:pythonAction:distDocker',
+    ':core:python2Action:distDocker',
     ':core:javaAction:distDocker',
     ':core:swift3Action:distDocker',
     ':sdk:docker:distDocker',
diff --git a/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala b/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala
index 7c1e5a2a..cee440ba 100644
--- a/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala
+++ b/tests/src/test/scala/actionContainers/ActionProxyContainerTests.scala
@@ -56,9 +56,10 @@ class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSyst
 
         val python = """
                 |#!/usr/bin/env python
+                |from __future__ import print_function
                 |import sys
-                |print 'hello stdout'
-                |print >> sys.stderr, 'hello stderr'
+                |print('hello stdout')
+                |print('hello stderr', file=sys.stderr)
                 |print(sys.argv[1])
             """.stripMargin.trim
 
@@ -87,10 +88,10 @@ class ActionProxyContainerTests extends BasicActionRunnerTests with WskActorSyst
                 |#!/usr/bin/env python
                 |import os
                 |
-                |print '{ "api_host": "%s", "api_key": "%s", "namespace": "%s", "action_name" : "%s", "activation_id": "%s", "deadline": "%s" }' % (
+                |print('{ "api_host": "%s", "api_key": "%s", "namespace": "%s", "action_name" : "%s", "activation_id": "%s", "deadline": "%s" }' % (
                 |  os.environ['__OW_API_HOST'], os.environ['__OW_API_KEY'],
                 |  os.environ['__OW_NAMESPACE'], os.environ['__OW_ACTION_NAME'],
-                |  os.environ['__OW_ACTIVATION_ID'], os.environ['__OW_DEADLINE'])
+                |  os.environ['__OW_ACTIVATION_ID'], os.environ['__OW_DEADLINE']))
             """.stripMargin.trim
 
         val perl = """
diff --git a/tests/src/test/scala/actionContainers/Python2ActionContainerTests.scala b/tests/src/test/scala/actionContainers/Python2ActionContainerTests.scala
new file mode 100644
index 00000000..74f1b3c1
--- /dev/null
+++ b/tests/src/test/scala/actionContainers/Python2ActionContainerTests.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * 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 actionContainers
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import common.WskActorSystem
+
+@RunWith(classOf[JUnitRunner])
+class Python2ActionContainerTests extends PythonActionContainerTests with WskActorSystem {
+
+    override lazy val imageName = "python2action"
+
+    /** indicates if strings in python are unicode by default (i.e., python3 -> true, python2.7 -> false) */
+    override lazy val pythonStringAsUnicode = false
+}
diff --git a/tests/src/test/scala/actionContainers/PythonActionContainerTests.scala b/tests/src/test/scala/actionContainers/PythonActionContainerTests.scala
index 06d9e1ef..f25ec9bb 100644
--- a/tests/src/test/scala/actionContainers/PythonActionContainerTests.scala
+++ b/tests/src/test/scala/actionContainers/PythonActionContainerTests.scala
@@ -29,11 +29,16 @@ import common.WskActorSystem
 @RunWith(classOf[JUnitRunner])
 class PythonActionContainerTests extends BasicActionRunnerTests with WskActorSystem {
 
+    lazy val imageName = "python3action"
+
+    /** indicates if strings in python are unicode by default (i.e., python3 -> true, python2.7 -> false) */
+    lazy val pythonStringAsUnicode = true
+
     override def withActionContainer(env: Map[String, String] = Map.empty)(code: ActionContainer => Unit) = {
-        withContainer("pythonaction", env)(code)
+        withContainer(imageName, env)(code)
     }
 
-    behavior of "pythonaction"
+    behavior of imageName
 
     testNotReturningJson(
         """
@@ -137,13 +142,23 @@ class PythonActionContainerTests extends BasicActionRunnerTests with WskActorSys
 
     it should "handle unicode in source, input params, logs, and result" in {
         val (out, err) = withActionContainer() { c =>
-            val code = """
-                |def main(dict):
-                |    sep = dict['delimiter']
-                |    str = sep + " ☃ ".decode('utf-8') + sep
-                |    print(str.encode('utf-8'))
-                |    return {"winter" : str }
-            """.stripMargin
+            val code = if (pythonStringAsUnicode) {
+                """
+                |def main(args):
+                |    sep = args['delimiter']
+                |    s = sep + " ☃ " + sep
+                |    print(s)
+                |    return {"winter" : s }
+                """.stripMargin
+            } else {
+                """
+                |def main(args):
+                |    sep = args['delimiter']
+                |    s = sep + " ☃ ".decode('utf-8') + sep
+                |    print(s.encode('utf-8'))
+                |    return {"winter" : s }
+                """.stripMargin
+            }
 
             val (initCode, _) = c.init(initPayload(code))
             initCode should be(200)
-- 
GitLab