python @gwt

借助Google的Web工具包(GWT),您可以完全使用Java™代码使用Ajax开发Rich Internet Application(RIA)。 您可以使用丰富的Java工具集(IDE,重构,代码完成,调试器等)来开发可以部署在所有主要Web浏览器上的应用程序。 使用GWT,您可以编写行为类似于桌面应用程序但在浏览器中运行的应用程序。 Pyjamas是GWT的端口,是用于在Python中开发Ajax应用程序的工具和框架。

睡衣包含一个独立的Python到JavaScript编译器,以及一个Ajax框架和小部件集。 使用这些组件,您可以编写全面的应用程序,而无需编写任何JavaScript。

本文介绍了睡衣的背景,原理,相关工具以及优点,同时向您展示了如何创建一个示例应用程序,该应用程序将存储基本联系信息(姓名,电子邮件地址,电话号码)。 您也可以下载示例应用程序的代码。

本系列的第二部分将说明如何构建自定义的睡衣组件。

背景

Python是最早移植到JVM(Jython),后来又移植到.Net(IronPython)的流行语言之一。 类似于Python的语法已被移植以产生与使用C(Cython)编写程序可比的机器代码。 因此,毫不奇怪,Python是最早的一种语言(在Google用Java语言开创先河之后)被翻译成JavaScript并以跨浏览器的方式运行。

强大的XUL

在2009年,Pyjamas-Desktop(现在是Pajamas的一部分)也被移植到XUL上。 XUL对Firefox就像WebKit对Safari一样,或多或少。 您可以在XUL上运行睡衣。 据报道,由于项目Hulahop(来自OLPC Sugar团队)和python-xpcom的开发人员,将睡衣移植到XUL仅用了两天时间。

在最近的过去,在Ajax中完成整个应用程序的机会似乎很少。 但是,使用GWT,您可以完全使用Java代码使用Ajax开发RIA。 GWT使您可以编写行为类似于桌面应用程序但在浏览器中运行的应用程序。

相反,Adobe AIR和Silverlight允许Web样式的应用程序在桌面上运行。 Android,Adobe AIR,Google Chrome,Safari和iPhone均使用WebKit进行渲染。 GWT的一个问题是,它不允许您编写作为桌面应用程序运行的应用程序(即使GWT的呈现开发工具集基于WebKit)。

Pajamas具有类似于GWT的Python到JavaScript编译器,以及一组Ajax小部件,这些小部件具有与GWT对应的API相同的API。 (您实际上可以使用GWT文档来开发Pajamas应用程序。)Python具有简洁而强大的语法。 例如,GWT 1.2花费了80,000行代码来编写,而睡衣只花了8,000行来完成相同的任务。

睡衣概述

XUL和WebKit Python绑定问题

MSHTML端口似乎是最好的端口,与WebKit和XUL的底层Python绑定也在不断变化。 当WebKit团队不将Python绑定移植到WebKit GTK时,它将引起难以言喻的痛苦。

有时,WebKit和xulrunner Python绑定似乎被破坏或至少被忽略了。

请记住,Pyjamas-Desktop并非仅与WebKit绑定。 睡衣将WebKit,XUL和MSHTML带给Python开发人员。 因此,Pyjamas-Desktop可以使用三个浏览器引擎中的任何一个。 通过它们,Pajamas既成为跨浏览器又是跨平台的GUI小部件集。

WebKit,XUL及其类似物为桌面应用程序带来了现代气息。 睡衣将WebKit带给Python开发人员。 借助Webkit,Pajamas成为跨浏览器和跨平台的GUI小部件集。 您可以开发可在任何运行WebKit和XUL的地方运行的小部件。 基于Pajamas API的应用程序可以存在于GWT应用程序所在的任何位置。 另外,Pajamas可让您编写基于WebKit和XUL构建的桌面应用程序。 这比在Qt或GTK上构建应用程序更可取,因为WebKit支持CSS,并且它在许多其他地方用于可靠的呈现(iPhone,Safari,Android等)。 但是,关于Python,XUL和WebKit有点麻烦(请参见侧栏)。

像GWT一样,Pyjamas是一个GUI组件框架。 如果您使用过Swing或GWT,那么睡衣的开发应该会很熟悉。 像大多数GUI框架一样,睡衣也是事件驱动的。

使用睡衣,您可以创建容器,然后将小部件添加到容器中。 这些小部件可以是标签,文本字段,按钮等。 小部件(如按钮)具有事件处理程序,因此您可以侦听按钮中的单击事件。

使用睡衣进行开发很容易,因为您可以使用通常用于Python的调试工具。 示例包括单元测试,打印语句和Python调试器(pdb,命令行调试器)。 您甚至可以使用Eclipse的Python支持进行调试。 请记住,您可以编写作为本机Python应用程序运行的Pajamas应用程序。 您无需将睡衣应用程序转换为JavaScript。 您可以像使用其他任何Python GUI工具包一样使用睡衣。

本文中的示例应用程序的GUI的第一个版本是仅使用从命令行运行的Python开发的。 它甚至没有最初部署到Web上,而是作为桌面应用程序运行。 这对于开发RIA应用程序是一个很大的优势,因为能够轻松调试程序是一个巨大的好处。

当您准备将应用程序部署到Web上时,需要更加小心所包含的库。 通常在浏览器中运行的Pajamas应用程序中使用JavaScript Object Notation(JSON)-RPC服务。

先决条件

要构建本文中的示例应用程序,您需要下载并安装Pyjamas。 这不是一个小任务。 在尝试让Pajamas在Ubuntu上运行并失败后,我放弃了并将其安装在Debian上。 (有传言说睡衣也可以在Windows®上很好地运行。)已安装的版本在Debian上运行良好。 安装过程可能会持续一段时间,因此您应该遵循Pajamas站点上针对您的环境的最新说明(请参阅参考资料 )。

为了构建服务层,使用了MySQL,Apache,mod_python和Python JSON-RPC。

构建示例应用程序

样本联系人管理应用程序存储基本的联系人信息,例如姓名,电子邮件地址和电话号码。 您将从一个简单的创建,读取,更新和删除(CRUD)应用程序开始,然后再添加实际存储。 您可以使用一个带有内存“数据库”的简单Python脚本来完成全部操作。 该示例使用一个服务层,然后用JSON支持的服务层版本替换此内存中服务层版本,该版本使用MySQL将联系信息存储在关系数据库中。

分而治之

我更喜欢开发与模拟层对话的完整GUI,以将GUI开发与持久性以及业务逻辑层分开。 这样,我可以专注于GUI逻辑,而不必担心调试远程RPC等问题。

要了解如何编写模拟服务,您必须了解运行时应用程序将如何运行。 JSON服务将被异步调用。 当您将Pajamas应用程序编译为RIA应用程序(HTML和JavaScript代码)时,在进行调用时,Ajax调用将异步返回结果。 因此,在构建模拟服务时,将模拟Ajax库以异步方式回调GUI。 清单1展示了ContactService调用GUI的callback方法,这将在后面显示。 这是为了模拟JSON异步行为,将在以后添加。

清单1.联系服务
class Contact:
    def __init__(self, name="", email="", phone=""):
        self.name = name
        self.email = email
        self.phone = phone

class ContactService:
    def __init__(self, callback):
        self.callback = callback
        self.contacts = []

    def addContact(self, contact):
        self.contacts.append(contact)
        self.callback.service_eventAddContactSuccessful()
    
    def updateContact(self, contact):
        self.callback.service_eventUpdateContactSuccessful()

    def removeContact(self, contact):
        self.contacts.remove(contact)
        self.callback.service_eventRemoveContactSuccessful()
        
    def listContacts(self):
        self.callback.service_eventListRetrievedFromService(self.contacts)

Contact类仅表示联系人(姓名,电子邮件,电话号码)。 ContactService仅具有一个内存中列表(不持久存储到磁盘)。 这个简单的类可让您开发GUI,然后稍加修改即可在开发显示逻辑后使用真正的JSON服务测试GUI。

ContactService使用以service_eventXXX开头的方法将服务事件通知给ContactListGUI (在清单2中定义)。

ContactListGUI仅有125行,相当简单,可管理9个GUI小部件。 它还与ContactService合作管理CRUD清单,如清单2所示。

清单2. ContactListGUI
import pyjd # this get stripped out for JavaScript translation
from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.Button import Button
from pyjamas.ui.Label import Label
from pyjamas import Window

from  pyjamas.ui.Grid import Grid
from  pyjamas.ui.Hyperlink import Hyperlink
from  pyjamas.ui.TextBox import TextBox

# Constants
CONTACT_LISTING_ROOT_PANEL = "contactListing"
CONTACT_FORM_ROOT_PANEL = "contactForm"
CONTACT_STATUS_ROOT_PANEL = "contactStatus"
CONTACT_TOOL_BAR_ROOT_PANEL = "contactToolBar"
EDIT_LINK = 3
REMOVE_LINK = 4

#Service code removed

class ContactListGUI:

    def __init__(self):
        self.contactService = ContactService(self)
        self.currentContact = Contact("Rick", "rhightower@gmail.com", "555-555-5555")
        self.addButton = Button("Add contact", self.gui_eventAddButtonClicked)
        self.addNewButton = Button("Add new contact", self.gui_eventAddNewButtonClicked)
        self.updateButton = Button("Update contact", self.gui_eventUpdateButtonClicked)

        self.nameField = TextBox()
        self.emailField = TextBox()
        self.phoneField = TextBox()
        self.status = Label()
        self.contactGrid = Grid(2,5)
        self.contactGrid.addTableListener(self)

        self.buildForm()
        self.placeWidgets()
        self.contactService.listContacts()	

    
    def onCellClicked(self, sender, row, cell):
        print "sender=%s row=%s cell=%s" % (sender, row, cell)
        self.gui_eventContactGridClicked(row, cell)

    def onClick(self, sender):
        if sender == self.addButton:
            self.gui_eventAddButtonClicked()
        elif sender == self.addNewButton:
            self.gui_eventAddNewButtonClicked()
        elif sender == self.updateButton:
            self.gui_eventUpdateButtonClicked()
                
    def buildForm(self):
        formGrid = Grid(4,3)
        formGrid.setVisible(False)
        
        formGrid.setWidget(0, 0, Label("Name"))
        formGrid.setWidget(0, 1, self.nameField);

        formGrid.setWidget(1, 0, Label("email"))
        formGrid.setWidget(1, 1, self.emailField)
        
        formGrid.setWidget(2, 0, Label("phone"))
        formGrid.setWidget(2, 1, self.phoneField)
        
        formGrid.setWidget(3, 0, self.updateButton)
        formGrid.setWidget(3, 1, self.addButton)

        self.formGrid = formGrid
        
    def placeWidgets(self):
        RootPanel(CONTACT_LISTING_ROOT_PANEL).add(self.contactGrid)
        RootPanel(CONTACT_FORM_ROOT_PANEL).add(self.formGrid)
        RootPanel(CONTACT_STATUS_ROOT_PANEL).add(self.status)
        RootPanel(CONTACT_TOOL_BAR_ROOT_PANEL).add(self.addNewButton)

    def loadForm(self, contact):
        self.formGrid.setVisible(True)
        self.currentContact = contact
        self.emailField.setText(contact.email)
        self.phoneField.setText(contact.phone)
        self.nameField.setText(contact.name)
    
    def copyFieldDateToContact(self):
        self.currentContact.email = self.emailField.getText()
        self.currentContact.name = self.nameField.getText()
        self.currentContact.phone = self.phoneField.getText()

ContactListGUI init方法调用buildForm方法来创建一个新的表单网格,并在其中填充字段以编辑联系人数据。 然后, init方法调用placeWidgets方法,该方法将contactGrid , formGrid , status和addNewButton小部件放置在承载此GUI应用程序HTML页面中定义的插槽中。 清单3中对此进行了定义。

图1显示了联系人管理应用程序中使用的小部件的概述。

图1.联系人管理GUI中的小部件

清单3. ContactListGUI GUI事件处理程序
<html>
    <head>
      <meta name="pygwt:module" content="Contacts">
      <link rel='stylesheet' href='Contacts.css'>
      <title>Contacts</title>
    </head>
    <body bgcolor="white">

      <script language="javascript" src="bootstrap.js"></script>

      <h1>Contact List Example</h1>

      <table align="center">
      <tr>
        <td id="contactStatus"></td> 
      </tr>
      <tr>
        <td id="contactToolBar"></td>
      </tr>
      <tr>
        <td id="contactForm"></td>
      </tr>
      <tr>
        <td id="contactListing"></td>
      </tr>
      </table>
    </body>
</html>

常量(例如CONTACT_LISTING_ROOT_PANEL="contactListing" )对应于HTML页面中定义的元素的ID(例如id="contactListing" )。 这使页面设计者可以更好地控制应用程序小部件的布局。

现在已构建了基本应用程序。 下一节将介绍几个常见的使用场景。

在页面加载中显示列表

首次加载示例应用程序的页面时,它将调用ContactListEntryPoint的__init__方法。 __init__方法调用ContactServiceDelegate的listContacts方法,该方法异步地调用服务的listContact方法。 模拟的ContactService的listContact方法调用称为service_eventListRetrievedFromService的服务事件处理程序方法,如清单4所示。

清单4. ContactListGUI:调用listContact事件处理程序
class ContactListGUI:
    …
    def service_eventListRetrievedFromService(self, results):
        self.status.setText("Retrieved contact list")
        self.contacts = results;
        self.contactGrid.clear();
        self.contactGrid.resizeRows(len(self.contacts))
        row = 0
        
        for contact in results:
            self.contactGrid.setWidget(row, 0, Label(contact.name))
            self.contactGrid.setWidget(row, 1, Label (contact.phone))
            self.contactGrid.setWidget(row, 2, Label (contact.email))
            self.contactGrid.setWidget(row, EDIT_LINK, Hyperlink("Edit", None))
            self.contactGrid.setWidget(row, REMOVE_LINK, Hyperlink("Remove", None))
            row += 1

service_eventListRetrievedFromService事件处理程序方法存储服务器发送的联系人列表。 然后:

清除显示联系人列表的contactGrid 。

调整行数以匹配从服务器返回的联系人列表的大小。

遍历联系人列表,将每个联系人的姓名,电话和电子邮件数据放入每行的前三列。

为每个联系人提供“编辑”链接和“删除”链接,使用户可以轻松地删除和编辑联系人。

编辑现有联系人

当用户单击联系人列表中的Edit链接时,将gui_eventContactGridClicked ,如清单5所示。

清单5. ContactListGUI的gui_eventContactGridClicked事件处理程序方法
class ContactListGUI:

    …
    def gui_eventContactGridClicked(self, row, col):
         contact = self.contacts[row]
         self.status.setText("Name was " + contact.name + " clicked ")
         if col==EDIT_LINK:
             self.addNewButton.setVisible(False)
             self.updateButton.setVisible(True)
             self.addButton.setVisible(False)
             self.loadForm(contact)
         elif (col==REMOVE_LINK):
             self.contactService.removeContact(contact)

    …
    def loadForm(self, contact):
        self.formGrid.setVisible(True)
        self.currentContact = contact
        self.emailField.setText(contact.email)
        self.phoneField.setText(contact.phone)
        self.nameField.setText(contact.name)

gui_eventContactGridClicked方法通过找出被单击的列来确定是否已单击“编辑”链接或“删除”链接。 然后,它隐藏addNewButton和addButton ,并使updateButton可见。 updateButton显示在formGrid并允许用户将更新信息发送回ContactService 。 gui_eventContactGridClicked然后调用loadForm (如清单5所示),它:

  • 将formGrid设置为可见。
  • 设置正在编辑的联系人。
  • 将联系人属性复制到emailField , phoneField和nameField小部件中。

当用户单击Update按钮时,将gui_eventUpdateButtonClicked事件处理程序方法,如清单6所示。该方法:

  • 使addNewButton可见,以便用户可以添加新联系人。
  • 隐藏formGrid 。
  • 调用copyFieldDateToContact ,后者将emailField , phoneField和nameField小部件中的文本复制回currentContact的属性中。
  • 调用ContactServiceDelegate updateContact方法将新更新的联系人传递回服务。
清单6. ContactListGUI的gui_eventUpdateButtonClicked事件处理程序方法
class ContactListGUI:

    …

    def gui_eventUpdateButtonClicked(self, sender):
        self.addNewButton.setVisible(True)
        self.formGrid.setVisible(False)
        self.copyFieldDateToContact()
        self.contactService.updateContact(self.currentContact)

    def copyFieldDateToContact(self):
        self.currentContact.email = self.emailField.getText()
        self.currentContact.name = self.nameField.getText()
        self.currentContact.phone = self.phoneField.getText()

上面的两种情况说明了应用程序的工作方式,以及如何利用App Engine for Java提供的基础结构。 清单7显示了ContactListGUI的其余GUI事件处理程序, 清单8显示了其余的服务回调处理程序。

清单7. ContactListGUI的gui_eventUpdateButtonClicked事件处理程序方法
class ContactListGUI:

    …
    def gui_eventContactGridClicked(self, row, col):
         contact = self.contacts[row]
         self.status.setText("Name was " + contact.name + " clicked ")
         if col==EDIT_LINK:
             self.addNewButton.setVisible(False)
             self.updateButton.setVisible(True)
             self.addButton.setVisible(False)
             self.loadForm(contact)
         elif (col==REMOVE_LINK):
             self.contactService.removeContact(contact)


    def gui_eventAddButtonClicked(self, sender):
        self.addNewButton.setVisible(True)
        self.formGrid.setVisible(False)
        self.copyFieldDateToContact()
        self.contactService.addContact(self.currentContact)

    def gui_eventUpdateButtonClicked(self, sender):
        self.addNewButton.setVisible(True)
        self.formGrid.setVisible(False)
        self.copyFieldDateToContact()
        self.contactService.updateContact(self.currentContact)


    def gui_eventAddNewButtonClicked(self, sender):
        self.addNewButton.setVisible(False)
        self.updateButton.setVisible(False)
        self.addButton.setVisible(True)
        self.loadForm(Contact())
清单8. ContactListGUI服务回调方法
class ContactListGUI:
    …
    def service_eventListRetrievedFromService(self, results):
        self.status.setText("Retrieved contact list")
        self.contacts = results;
        self.contactGrid.clear();
        self.contactGrid.resizeRows(len(self.contacts))
        row = 0
        
        for contact in results:
            self.contactGrid.setWidget(row, 0, Label(contact.name))
            self.contactGrid.setWidget(row, 1, Label (contact.phone))
            self.contactGrid.setWidget(row, 2, Label (contact.email))
            self.contactGrid.setWidget(row, EDIT_LINK, Hyperlink("Edit", None))
            self.contactGrid.setWidget(row, REMOVE_LINK, Hyperlink("Remove", None))
            row += 1

    def service_eventAddContactSuccessful(self):
        self.status.setText("Contact was successfully added")
        self.contactService.listContacts()

    def service_eventUpdateContactSuccessful(self):
        self.status.setText("Contact was successfully updated")
        self.contactService.listContacts()

    def service_eventRemoveContactSuccessful(self):
        self.status.setText("Contact was removed")
        self.contactService.listContacts()

编译示例

您可以编译此示例应用程序,然后在任何现代浏览器中本机运行。 但是,尝试调试在浏览器中运行的RIA应用程序并不有趣。 幸运的是,您可以使用Pyjamas-Desktop将整个应用程序作为本地Python应用程序运行,如清单9所示。

清单9.运行Pyjamas-Desktop
import pyjd # this get stripped out for JavaScript translation
...
if __name__ == '__main__':
    pyjd.setup("public/Contacts.html")
    contacts = ContactListGUI()
pyjd.run()

清单9中的代码实例化了Python桌面应用程序,然后通过调用run方法启动桌面。 当您将此应用程序作为桌面应用程序运行时,可以使用支持可视调试的pdb或Python IDE对其进行调试。

我在主目录下的工具目录中安装了睡衣。 使用Python调试器时,请确保将Pajamas和Pyjamas-Desktop库添加到路径中,如清单10所示。

清单10.将睡衣添加到PYTHONPATH
export PYTHONPATH=/home/rick/tools/pyjamas:/home/rick/tools/pyjamas/library

完成编写应用程序后,您可以运行pyjsbuild将应用程序编译为HTML,JavaScript和JSON-RPC。 清单11显示了运行pyjsbuild的示例脚本。

清单11. build.sh
#!/bin/sh

options="$*"
#if [ -z $options ] ; then options="-O";fi
~/tools/pyjamas/bin/pyjsbuild --print-statements $options Contacts.py

编译应用程序时,您要做的就是由Web服务器托管/ output文件夹。 该示例使用了全新的Debian安装,因此apache2和mod_python是使用apt-get安装的,如清单12所示。

清单12.安装apache2和mod_python
$sudo apt-get install apache2 libapache2-mod-python

在下一个版本的联系人列表中将使用mod_python。 该示例应用程序是在/ home / rick / tools / pyjamas / examples / contact1下创建的。 要将其托管在Apache上,请将以下代码添加到Apache httpd.conf文件中(在Debian上,此文件安装在/ etc / apache2下)。

清单13. /etc/apache2/httpd.conf
Alias /pj "/home/rick/tools/pyjamas" 
<Directory "/home/rick/tools/pyjamas">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride None
    Order deny,allow
    allow from all
</Directory>

添加JSON-RPC支持

关于mod_python和sqllite3的提示

最初,本文中的小示例似乎只有一个小服务,并且不需要单元测试或日志记录。 大错! 我首先尝试使用sqllite3(因为它是Python附带的),并且存在一些锁定问题,这促使人们切换到MySQL。 有关mod_python,JSON-RPC和sqllite3的一些课程:

  • 当使用单元测试在本地运行,然后在apache下作为su运行时,sqlite3会以奇怪的方式锁定文件。
  • 在mod_python中很难调试,因为您不会收到错误消息。 日志记录和单元测试至关重要。

我切换到MySQL,使用具有强大异常处理功能的日志记录,并编写了单元测试。 没有单元测试和记录,该示例可能无法完成。 如果始终使用sudo运行单元测试,则可以使用sqlite3。 或者,您可以设计其他方法来避免锁定问题。

在使GUI逻辑正常工作之后,该开始对用Python实现的JSON-RPC服务进行编程了。 JSON-RPC是标准; 您可以使用任何编程语言来实现服务器端。 这样,可以将Pajamas前端应用程序安装到具有JSON-RPC后端Web服务的现有项目中。 JSON是一种数据交换格式。 它使用两种结构:

  • 名称/值对的集合(Python中的字典,Java代码中的哈希表或Perl的关联数组)
  • 数组

JSON-RPC是一种远程过程调用协议,该协议使用JSON编码和封送参数和返回类型。 JSON-RPC项目具有适用于Python的绑定。 Twisted,Django和许多其他Python框架也支持JSON-RPC。 清单14显示了一种获取JSON-RPC的简单方法。

清单14.安装JSON-RPC
$ svn checkout 
   http://svn.json-rpc.org/trunk/python-jsonrpc

$ cd python-jsonrpc
$ python setup.py install

要编写JSON-RPC服务,您可以使用@ServiceMethod注释方法调用,然后公开一个名为service的模块变量,该变量指向要使用JSON-RPC公开的实例。 清单15显示了一个示例。

清单15. ContactService:联系人列表的JSON-RPC服务
import logging

logging.basicConfig(filename="/tmp/contactjson.log",
                    level=logging.DEBUG)


logging.debug("Loading contact service")

from jsonrpc import ServiceMethod

use_mysql=True

if use_mysql:
    import MySQLdb as db_api
    logging.debug("Using mysql")
else:
    import sqlite3 as db_api
    logging.debug("Using sqllite3")


db_url = "/tmp/contacts"


class ContactService:

    @ServiceMethod
    def test(self):
        logging.info("Test called")
        return "test"

    def connection(self):
        if use_mysql:
            connection =  db_api.connect(passwd="mypassword", db="contactdb", user="root")
        else:
            connection =  db_api.connect(db_url)
        return connection

    def run_update(self, func):
        
        connection = self.connection()
        cursor = connection.cursor()
        try:
            func(cursor)
            cursor.close()
            connection.commit()
        except Exception, e:
            connection.rollback()
            logging.exception("problem handling update")
            raise e
        finally:
            connection.close()

    def run_query(self, func):
        connection = self.connection()
        cursor = connection.cursor()
        lst = None
        try:
            func(cursor)
            lst = cursor.fetchall()
            cursor.close()
        except Exception, e:
            logging.exception("problem handling query")
            raise e
        finally:
            connection.close()
        return lst

    @ServiceMethod
    def addContact(self, contact):
        logging.debug("Add contact called %s", `contact`)
        def ac(cursor):
            if use_mysql:
                cursor.execute(""" 
                  insert into contact 
                          (name, phone, email) 
                  values (%(name)s, %(phone)s, %(email)s) 
                  """, contact)
            else:
                cursor.execute(""" 
                  insert into contact 
                          (id, name, phone, email) 
                  values (NULL, :name, :phone, :email) 
                  """, contact)
        self.run_update(ac)


    @ServiceMethod
    def updateContact(self, contact):
       logging.debug("Update contact called %s", `contact`)
       def uc(cursor):
           if use_mysql:
               cursor.execute(""" 
                  update contact 
                          set name = %(name)s, email = %(email)s, phone = %(phone)s
                  where id=%(id)s;
                  """, contact)
           else:
               cursor.execute(""" 
                  update contact 
                          set name = :name, email = :email, phone = :phone
                  where id=:id;
                  """, contact)

       self.run_update(uc)


    @ServiceMethod
    def removeContact(self, contact):
       logging.debug("Remove contact called %s", `contact`)
       def uc(cursor):
           if use_mysql:
               cursor.execute("delete from contact where id=%(id)s;", contact)
           else:
               cursor.execute("delete from contact where id=:id;", contact)
       self.run_update(uc)

        
    @ServiceMethod
    def listContacts(self):
        logging.debug("list contact called")
        def lc(cursor):
            cursor.execute("select name, phone, email, id from contact")
        lst = self.run_query(lc)
        def toMap(x):
            return {"name":x[0],"phone": x[1], "email":x[2], "id":x[3]}
        return map(toMap, lst)


service = ContactService()

#If you can't get mod_python working 
# you can use CGI with the following line.
#handleCGI(service)
# You have to import handleCGI from jsonrpc

清单15可以使用易于安装的MySQL或Python附带的sqlite3。 要使用sqlite3,请将use_mysql设置为False。

清单16显示了测试该服务的单元测试,这对于开发示例应用程序是必不可少的。 清单显示了单元测试使用的实用程序类。

清单16. TestContactService
import unittest
from contacts import ContactService
from dbscript import *

class TestContactService(unittest.TestCase):

    def setUp(self):
        self.cs = ContactService()
        try:
            drop_table()
        except:
            print "unable to drop contact table"
        try:
            create_table()        
        except:
            print "unable to create contact table"

    def testAdd(self):
        clear_table()
        cs = self.cs
        cs.addContact({"name":"Richard",
                       "phone":"5205551212",
                       "email":"rick@rick.com"
                      })
        list = cs.listContacts()
        print list
        found = False
        for cdict in list:
            if cdict["name"]=="Richard": found = True
        self.assertTrue(found)

    def testUpdate(self):
        cs = self.cs
        insert_test_data()
        cs.updateContact(
            {"name":"Richard",
                       "phone":"5205551212",
                       "email":"rick@rick.com",
                       "id":1})

        list = cs.listContacts()
        print list
        found = 0
        for cdict in list:
            if cdict["name"]=="Richard": found +=1
        self.assertTrue(found==1)


    def testRemove(self):
        cs = self.cs
        insert_test_data()
        cs.removeContact(
            {"name":"Richard",
                       "phone":"5205551212",
                       "email":"rick@rick.com",
                       "id":1})

        list = cs.listContacts()
        print list
        found = 0
        for cdict in list:
            if cdict["name"]=="Richard": found +=1
        self.assertTrue(found==0)


        
        
if __name__ == '__main__':
    unittest.main()

该dbscript.py清单17中可以建立无论是MySQLdb的联系人表或sqlite3的联系人表。

清单17. Dbscript,创建,删除,填充联系人
use_mysql = True

if use_mysql:
    import MySQLdb as db_api
else:
    import sqlite3 as db_api

db_url = "/tmp/contacts"


create_table_sql = """ 
create table contact (
     id INTEGER %s PRIMARY KEY, 
     name VARCHAR(50), 
     phone VARCHAR(50), 
     email VARCHAR(50));
"""

if use_mysql:
    create_table_sql = create_table_sql % ("AUTO_INCREMENT",)
else:
    create_table_sql = create_table_sql % ("",)


def run_script(func):
    if use_mysql:
        connection =  db_api.connect(passwd="mypassword", db="contactdb", user="root")
    else:
        connection =  db_api.connect(db_url)

    cursor = connection.cursor()
    try:
        func(cursor)
        connection.commit()
        cursor.close()
    finally:
        connection.close()
    
def create_table():
    def ct(cursor):
        cursor.execute(create_table_sql)

    run_script(ct)

def drop_table():
    def dt(cursor):
        cursor.execute("drop table contact;")
    run_script(dt)

def clear_table():
    def dt(cursor):
        cursor.execute("delete from contact;")
    run_script(dt)

def insert_test_data():
    def itd(cursor):
        if use_mysql:
            cursor.execute("insert into contact (id, name, phone, email) values (NULL, 
               'Bob', '5', 'b@b.com');") 
            cursor.execute("insert into contact (id, name, phone, email) values (NULL, 
                'Rick', '5', 'b@b.com');")
            cursor.execute("insert into contact (id, name, phone, email) values (NULL, 
                'Sam', '5', 'b@b.com');")
        else:
            cursor.executescript(""" 
    insert into contact (id, name, phone, email) values (NULL, "Bob", "5", "b@b.com"); 
    insert into contact (id, name, phone, email) values (NULL, "Rick", "5", "b@b.com"); 
    insert into contact (id, name, phone, email) values (NULL, "Sam", "5", "b@b.com"); 
           """)

    run_script(itd)

本质上, dbscript创建并删除联系人表,并使用单元测试使用的测试数据填充该表。 完成JSON-RPC服务之后,您可以通过将清单18中所示的代码添加到httpd.conf文件中来安装要由Apache HTTPD提供的服务。

清单18. /etc/apache2/httpd.conf
Alias /services "/home/rick/services" 

<Location /services/>
    AddHandler mod_python .py
    PythonHandler jsonrpc
</Location>

请记住,对服务进行更改后,需要重新启动它,如清单19所示。

清单19.重新启动Apache2以获取对mod_python的更改
$sudo /etc/init.d/apache2 restart

在Pyjamas中运行JSON-RPC代理时,您会得到讨厌的递归错误。 为了帮助调试错误,我使用了JSON-RPC独立客户端lib,如清单20所示。

清单20. Python JSON-RPC客户端
from jsonrpc import ServiceProxy, JSONRPCException
 
cs = ServiceProxy("http://localhost/services/contacts.py")
 
if cs.test()=="test":
    print "connected"
 
try:
    cs.addContact(
        {"name":"Larry Wall", 
         "phone":"5551212", 
         "email":"rick@rick.com"})
 
except Exception, e:
    print e.error
    print `e.error`

上一步是测试和调试中的重要一步。 睡衣的开发还有些新生,因此最好有另一种方法来从另一个来源测试JSON-RPC。

该示例仅将ContactService更改为使用JSONProxy。 JSONProxy是Pajamas客户端对JSON-RPC的支持。 您可以为刚刚编写的服务创建一个代理对象,如清单21中的ContactsJSONProxy所示。JSON服务的返回对象是异步返回的。 因此,当您在JSON代理上进行调用时,您将传递一个ContactService实例,该实例实现onRemoteResponse以异步地从该服务获取响应。

清单21. JSONRPC格式的联系人清单
from pyjamas.JSONService import JSONProxy
...
class Contact:
    def __init__(self, name="", email="", phone="", id=None):
        self.name = name
        self.email = email
        self.phone = phone
        self.id = id
    def to_dict(self):
        return {"name":self.name, "email":self.email, 
                "phone":self.phone, "id":self.id}


class ContactsJSONProxy(JSONProxy):
    def __init__(self):
        JSONProxy.__init__(self, "/services/contacts.py", 
                           ["addContact", "removeContact", 
                            "updateContact", "listContacts","test"])

    

class ContactService:
    def __init__(self, callback):
        self.callback = callback
        self.proxy = ContactsJSONProxy()

    def test(self):
        self.proxy.test(self)

    def addContact(self, contact):
        self.callback.showStatus("Add contact called")
        self.proxy.addContact(contact.to_dict(), self)

    def updateContact(self, contact):
        self.callback.showStatus("Update contact was called")
        self.proxy.updateContact(contact.to_dict(), self)


    def removeContact(self, contact):
        self.callback.showStatus("Remove contact was called")
        self.proxy.removeContact(contact.to_dict(), self)

        
    def listContacts(self):
        self.proxy.listContacts(self)


    def onRemoteResponse(self, response, request_info):        
        if request_info.method == "addContact":
            self.callback.service_eventAddContactSuccessful()
        elif request_info.method == "updateContact":
            self.callback.service_eventUpdateContactSuccessful()
        elif request_info.method == "listContacts":
            def toContact(x):
                return Contact(x["name"], x["email"], x["phone"], x["id"])  
            contacts = map(toContact, response)
            self.callback.service_eventListRetrievedFromService(contacts)
        elif request_info.method == "removeContact":
            self.callback.service_eventRemoveContactSuccessful()
        else:
            self.callback.showStatus(""" REQ METHOD = %s RESP %s """ %
                 (request_info.method,response)) 

    def onRemoteError(self, code, errobj, request_info):
        message = errobj['message']
        if code != 0:
            self.callback.showStatus("HTTP error %d: %s" % (code, message))
        else:
            json_code = errobj['code']
            self.callback.showStatus("JSONRPC Error %s: %s" % (json_code, message))

客户端代码的其余部分与以前的方式非常相似,只有一些外观上的更改。 令人惊奇的是,使用真正的远程RPC服务的客户端与使用独立服务的版本没有太大区别。 这使您可以快速开发GUI,然后仅插入JSON-RPC服务,该服务是单独开发和调试的。

摘要

在“睡衣简介”系列的第一部分中,您探索了睡衣背后的历史和愿景。 您还学习了如何使用Pyjamas,mod_python和Python JSON-RPC创建基于Pyjamas的应用程序。 请继续关注本系列的第2部分,它将介绍如何构建自定义的睡衣组件。

翻译自: https://www.ibm.com/developerworks/web/library/wa-aj-pyjamas/index.html

python @gwt