IRCログを貼付け

nishio: @admin_requiredなビューにアクセスしたときに

google.appengine.ext.db.KindError
KindError: No implementation for kind 'main_user'

が出ることがあるのですが再現する方法がまだ見つかってません。特に何も修正していなくても久しぶりにアクセスすると出るように見えて、「いまどういうユーザとしてログインしているのか」を表示するためのビューが叩かれた後で同じURLにアクセスするとエラーにならずに見えるので、セッションのexpirationに関係する挙動かなと疑っています。問題の行は

File "…/kay/auth/backends/datastore.py", line 42, in get_user
return db.get(request.session['_user'])

です。本番サーバでも同じ現象がおきます。

セッションのexpirationにしては早い。なにかをしてしまったときにセッションがおかしくなるのだろうか。

_tmatsuo: nishio: ありがとうございます。ちょっと心当たりがあるので調べてみます。ローカルで runserver 後にまず /_ah/admin にアクセス -> アプリにアクセスとかやると起こったりするような気がしている。Model の衝突を避けるために appname_modelname という風に kind を上書きしてるんですが、それが諸悪の元ですww もうデフォルトでは Off にしてしまおうかなと思っていますが。ちなみに settings.ADD_APP_PREFIX_TO_KIND を False にするとこの挙動をしなくなるのでそのエラーは出なくなると思います。ただし今までのデータにはアクセスできなくなるので注意してください


nishio: うーむ。main_userとuserの両方にアクセスできるならBulk Updateとかで簡単に移行できるのでしょうけど、たぶん同時にはアクセス出来ないんですよね、settingsで決まっているし。実は手元の挙動では@admin_requiredな/adminにアクセスして、成功すると、次の/adminへのアクセスで確実に発生して@no_sessionではない/login_infoにアクセスして、それから/adminにアクセスするとたぶん100%成功するんですよ。なので/adminの中からAjaxで/login_infoにGETを投げればいいんじゃないかなーという全く本質的じゃないワークアラウンドを考えています。


nishio: あれー、一晩寝たら昨日と挙動が違う…(ぇ
nishio: エラーが起きなくなってしまった…
mopemope: お、session?
nishio: @admin_requiredなビューにアクセスしたときに

google.appengine.ext.db.KindError
KindError: No implementation for kind 'main_user'

/kay/auth/backends/datastore.py", line 42, in get_user
return db.get(request.session['_user'])

ででるという問題です。ログを何処かに貼りつけようかな。


mopemope: ごった煮なってるのかな。これに似た話?? http://code.google.com/p/googleappengine/issues/detail?id=1133
nishio: リンク先を読んでいるナウ。Kindを検索するときのキーがバイト列であるべきなのにユニコードだったせいで見つからなかったということですね、なるほど。エラーメッセージが%sで表示されているからunicodeになってても分からない。とりあえずlogging.debug(type(kind))とか挟んで再現するのを待つかな…
mopemope: ソース全く見てないけど、たぶんその辺が怪しいのかなと。VPNつなぐのでオチ


nishio: エラーになる時にはtypeがunicode、エラーにならないときにはclass_for_kindがそもそも呼ばれていない。普段の操作をしているとclass_for_kindがそもそも呼ばれないのでunicodeなのが正しいのかおかしいのか確認できない。

nishio: うーん、正しい時もエラーの時もrequest.session['_user']はdatastore_types.Key.from_path(u'main_user', 144L, _app=u'foobar')。unicodeなのは同じ。_kind_mapのキーがstrなのでclass_for_kindにunicodeを渡してはいけない。
mopemope: 動いたり動かなかったりってのがよくわからんな
nishio: そう。なんかキャッシュされてるのだろうか。動いたり動かなかったりする要素が見つからない。とりあえずclass_for_kindの冒頭で「kindがunicodeだったらstrに変換」ってするだけで問題は解決するけど、別の大きな問題を悪質化させてしまうだろうなぁ



_tmatsuo: kay.auth.backends.datastore.DatastoreBackend の __init__ 一行目にimport_string(settings.AUTH_USER_MODEL)を入れれば大丈夫な気がする。Python の dict の key は unicode/str どちらでもアクセス可能みたいなので、問題は別のところにあるぽいです。key により db.get しているので、それ以前に Model 定義を import しておく必要があっただけの話だと思われます。サーバー側で現象が起きるケースと起きないケースとある件ですが、ログイン画面を通って、ログイン処理をした server instance 上ではそのプロセス内で main.models.User が import されています。このインスタンスが生きている間は大丈夫(エラーは起きない)で、このインスタンスがメモリから追い出されると、問題になっているエラーが出るというわけです。

Werkzeugすごいなー

ここしばらくJinja2+WerkzeugなKay-frameworkを使っていて「Jinja2のパワフルさは痛感したけど、Werkzeugの良さがいまいちわからないなぁ」と思っていたのだが前言撤回。

エラー画面から該当部分のソースを読んだり、各関数のフレームの中で対話的に実行したりできるんだって!

今までlogging.debugなどを突っ込んでprintデバッグしていたけど、そんなことしなくてもエラーメッセージが出たらすぐにその場でコードや各変数の値、メソッドを読んだ返り値、などなどを調べて回ることができる!

例えていうなら殺人事件で、断末魔の悲鳴だけから犯人を探すのと、現場や死体を鑑識が色々調べてくれるのとぐらい違う!今朝までは次のアプリをDjango+appenginepatchで作るか、Kayで作るか考えていたけど、もう僕はKay一択でいいや!

Pythonの辞書にunicodeとstrで同一の文字列を入れたらどうなるか

In [1]: {u"a": 1, "a": 2}
Out[1]: {u'a': 2}

ほう、なるほど。これは

In [2]: u"a" == "a"
Out[2]: True

なのでu"a"と"a"が同一視されているんだな。もちろん

In [3]: u"a" is "a"
Out[3]: False

なのだけど、isで「同じキーか」のチェックをしたのでは

In [4]: (1, 2) is (1, 2)
Out[4]: False

とタプルが区別されてしまって嬉しくない。言語を設計するうえで割と悩ましいところだよね。このバイト列とユニコード文字列の自動変換は色々なわかりにくいバグの原因になるのでPython3.0からは取り除かれた。

>>> b"a" == "a"
False
>>> {b"a": 1, "a": 2}
{b'a': 1, 'a': 2}

おかたづけ

kayをつかって2つ目のアプリを作ろうと思ったらkayをどこに置いたかわからなくなった。~/tools/にない。~/cur/にあった。動かすとリンクが切れて困るらしいのでmvしてからln -sしておいた。置き場所とか片付け方とか、いい方法ないんだろうか。自分の机が片付けられないのだから無理か。


コンピュータのいいところは、散らかっていても検索出来るところだ!

DjangoやKayを使って新しいアプリを作る方法の自分用メモ

何事も始める時が一番腰が重いものであり、腰を軽くするためには考えずに作業出来る必要があり、その為には手順を記録してなんども修正して洗練させる必要がある、だからとりあえず記録。DjangoやKayを使ってウェブアプリを作る方法

サービス名称を決める
  • 他人に話してみる。「ダサッ」とか「え、何々って意味?(誤解)」とか言われたら考え直す。
  • ドメインが取れるかどうかを調べる。Google App Engineならその名前のアプリを作れるか調べる。取れないなら考えなおす。
  • ドメインを取る。アプリの名前をとる。作ってから取れなくなってたらガッカリするから。
プロジェクトフォルダを作る

僕はBitbucketでホスティングされているプライベートのMercurialリポジトリの中に作る。

フォルダの中でstartprojectする

RATIONALE: ここで作られるフォルダのルートにはapp.yaml, settings.pyなどのファイルが複数おかれて散らかるし、それらのファイルとそのプロジェクトの「ウェブアプリではない何か」とが混在しておかれるのはややこしいから。いずれGAEにアップロードする必要のない資料とか画像とかスクリプトとかができてくるから。

startapp coreする

RATIONALE: appが単一である時の「適当な名前」は以前までmainにしてたが、これはmanage.pyと2文字目まで同一なのでcdなどで移動するときにmai[TAB]の4文字打たなければ行けない。coreならc[TAB]でいい。

hg addとhg commitをする

RATIONALE: この後の操作でファイルのコピー位置を間違えることがある

app.yamlを書き換え

applicationとversionあたりを。後でstaticを使うならついでに:

- url: /static
  static_dir: static
  expiration: 3000d
settings.pyを書き換え

特にINSTALLED_APPSとAPP_MOUNT_POINTSとSECRET_KEYを。APP_MOUNT_POINTSを指定し忘れて404とかなったりする。authやsessionを使う予定があるなら今のうちにやっとく。

INSTALLED_APPS = (
  'kay.sessions',
  'kay.auth',
  "core",
)

APP_MOUNT_POINTS = {
    "core": "/"
}

MIDDLEWARE_CLASSES = (
  'kay.sessions.middleware.SessionMiddleware',
  'kay.auth.middleware.AuthenticationMiddleware',
)

AUTH_USER_BACKEND = 'kay.auth.backends.datastore.DatastoreBackend'
AUTH_USER_MODEL = 'core.models.User'

AUTHにこの設定を入れたならmodels.pyにこれが必要

from kay.auth.models import DatastoreUser

class User(DatastoreUser):
    pass
GoogleAppEngineLauncherにドラッグドロップし、サーバを起動してブラウザで見えることを確認
staticフォルダを作ってYUI reset-font-gridとJQueryUIとを入れる

YUI LibraryjQuery: The Write Less, Do More, JavaScript LibraryDeveloper's Guide - Google AJAX Libraries API - Google Codeを使うという選択肢もあるみたい。

.hgignoreの無視リストにYUIJQueryを入れる
Google Analyticsで新しいプロフィールを作る
template/_base.htmlを作る
  • 既存のアプリからコピーしてきてtemplate/_base.htmlを作る。
  • Google Analiticsのトラックコードを書き換える。
  • タイトルを書き換える
  • template/index.htmlを{% extends "core/_base.html" %}にする
  • R: テンプレートの継承ツリーは(何も継承しないテンプレート)→(_base: タイトル、Analitics、ヘッダなどの「見せるための準備」はあるが中身のないテンプレート)→(index: トップバナー、カラム、末尾の連絡先などの一揃いついたテンプレート)としてある。これはログイン情報の表示にiframeを使っていたときにCSSとかは同じようについているけどタイトルがないページが欲しくて作ったものなので、全部のページに同じようにヘッダとフッタがついてよければこれをする必要はないかも。
README.txtを作る

プロジェクトフォルダのルートにREADME.txtを置く。何を作ろうとしたのか、何をする必要があるか、などを書き連ねる。

  • R: とりあえずこれを書き連ねてない状態で中断して暫く間が開くと「何するつもりだったんだっけ…」となるので。
  • 実行可能なことは□(TODO)を前置する。終わったら■に書き換える。うっかり□を書いたものでも、実行不可能だと思ったら-に書き換える。「なになにはどう実装する?」はTODOじゃない。
データベース保存場所の変更

Macの場合、開発サーバのデータベース保存場所をプロジェクトフォルダの中に指定しておく。ランチャーのEdit-Application Settingsで--datastore_path=/...projdir.../database_projnameとか。

  • デフォルトだと/varの中に作られて、マシン再起動の時に消されるようなので。
作りたいところから作る。

まあ大体はmodel.pyを書かないとなにもできないわけだけど、今回は一番重要なブックマークレットから作り始めた。前回も冷静に考えるとコンテンツを作り出すスクリプトから作ったなぁ。

  • 未実装の部分には必ず raise NotImplementedErrorを書く。書かないで困るのは自分。
  • Emacsの話。modelsやviewsを書く際には、新しく作る方のmodels.pyを開いてから、別のアプリで作ったmodels.pyも開く。動的略語補完でdb.Iとかからdb.IntegerPropertyと補完されるので「db.IntegetPropertyと書いてしまってエラーになる」なんて時間のロスを防げる
CSS
def css(request):
  r = render_to_response(
    'core/style.css', dict())
  r.content_type = "text/css"
  return r
認証

認証は多少普通と違うからといって1から自分で作ろうとしたこともなんどもあったけど結局は既存の物に吸収されたので、今後はなるべく自分で作らない。

/admin

/adminをつくる。公開する前にちゃんと@admin_requiedすること。ad hocなコード(付け替えやなんかのキックなど)はここへ。そのうち肥大化してくるので別ファイルに切り出す。

utils.pyなど

欲しいページ、欲しい機能を実装していくとおそらくview.pyが真っ先に肥大化する。ある程度以上のサイズになると脳内でどこに何があったか思い出せなくなってくるのでかぶっているコードをどんどんくくりだして短くする。

tests.py

ページの種類が3枚以上になったあたりで「何かを修正した後に全部の機能をテストしてない」という状態になるからテストケースを書き始める。さもないと「新しい機能をつけてupdateしたんだが、ふと気がついたらわずか1行の修正が足りないせいでこっちの機能が壊れてるじゃん!」という自体が起きて心臓に悪い。

  • ローカルの開発環境で自分が操作して挙動を眺めているのは「手作業によるテスト」だと自覚すること。