Make組ブログ

Python、Webアプリや製品・サービス開発についてhirokikyが書きます。

PasteScript空騒ぎ

PasteScript空騒ぎ

PasteScript の話。

PasteScriptって?

アプリケーションに必要なコマンドを作る地盤を提供してくれるものです。 PasteScript 自身が提供するものでは、テンプレートからアプリケーションを作成する コマンドがあります:

paster create

テンプレートを作っておけばアプリケーションがさくっとできます。

この説明だけだとなんじゃそりゃという感じですが、ここで PasteDeploy も紹介します。

PasteDeployWSGIアプリケーションの構成を設定ファイルから指定できるものです。 例えばDBの設定やLoggingの設定をファイルに記述して、その設定を元に アプリケーションを構築、起動できます。

この PasteDeployPasteScript を併用することで以下のようなコマンドで 簡単にWSGIアプリケーションが起動できたりします:

paster serve

この serve コマンドも PasteScript の提供するもので、 PasteDeploy の 機能を使って簡単にWSGIアプリケーションを構築、起動できます。 (必要な設定はdevelopment.iniのようなiniファイルに書いておきます)

WSGIアプリケーションで開発するにも Django でいう management command とか startapp が欲しくなりますが、まぁ PasteScript はそんなとこです。

PasteScript空騒ぎ

その PasteScriptWSGIアプリケーションやWebフレームワークで広く使われており デファクトスタンダードといっても良いくらい有用なものですが、一つ大きな問題がありました。

Python3 に対応していないということです。(あぁレガシー)。

で一番困るのは PasteScript を使っていた各Webフレームワークたちです。 Webフレームワークも「よしPython3に対応するか」と思うわけですが、 依存するパッケージがPython3に対応していないと話になりません。

ここで各フレームワークが頑張って脱 PasteScript を模索します。

Pyramid はその内部に PasteScript を取り込み/修正し、 pcreate などの コマンドを提供しました(Python3対応をしている PasteDeploy はPyramid1.4の現在でも使われています)

Turbogearsgearbox を用意しました。これは PasteScript の代替になる パッケージで、 Python3 でも動作します。

いっぽうDjangoPasteScript に依存せず独自のmanagement commandという地盤と project/application templateのしくみを提供していたので、全く関係ありませんでした。

gearboxおすすめ

さてさて PasteScript が使えない今、WSGIアプリケーションを作る際には どうしたものでしょうか。

gearbox を使いましょう。

これはTurboGears2がPython3対応する際にできたパッケージですが、TurboGearsに無関係な WSGIアプリケーションでも使えます。ついでに PasteDeploy に依存して serve 用の コマンドも直接提供してくれます:

gearbox serve

gearbox はどのWSGIアプリケーションでも使えて素晴らしいです。 WeiWei というWebフレームワークレス なWSGIアプリケーションを最近書いたのですが、これでも使わせてもらってます。

ついでにScirptとかDeployとかややこしいのをマルっと一つにしてるあたりも分かりやすいです。

gearbox 素晴らしい。WSGIアプリケーション+Python3を書くならこれを使うのを薦めます。

PasteScriptのPython3対応

さてPython3非対応でお騒がせした PasteScript ですが、 つい最近今年の 9 月にようやく Python3 に対応したようです!!

すごい!!やった!!!!

と思いきや、残念なことに 2to3 を前提としています。

まぁこれは使いたく無いですね。 classifierも書き換えられてないしインストール時に2to3がかかるようにもしてないようです。 (gearbox は現時点でまだ0.0.2ですが Python3 でも使えるいいこですよ٩(๑❛ᴗ❛๑)۶)

このマージを見つけたときは興奮しましたがちょっと違う。 PasteScript をPython3対応をちゃんとさせたいんだという人は aodagさんのプルリクエストを修正/レビュー/応援すると良いと思います。

私は gearbox の恩恵に甘えます。

まとめ

gearbox 使いましょう。

余談

世の中に入門記事ばっかり溢れてもつまらないので、ちょっとした小ネタを書きました。 パッケージの歴史の話 とかも結構面白い ので、こういうどうでもよくないどうでもいい記事が増えればいいと思います。

(PasteScript や Pyramid のPy3対応については人から聞いた話も多いので 間違ってたら教えてください。たまに PasteDeploy と逆に言っちゃうし )

Pythonにおける「許されざる悪事」を避けるために

Pythonにおける「許されざる悪事」を避けるために

許されざる悪事というものが存在する

モジュールを import するだけでグローバルな値が設定されるというもの。

「import の順番に依存した処理ほど不愉快なものはない。こういった依存性を持つ処理は非常に脆弱で、ちょっとしたことですぐエラーとなってしまい、メンテナンスしにくいコードになってしまうものなのである。」

ぁっぉ

やってみよう

importに依存しない処理を書いてみましょう:

# In mymodule.py

hoge = None

def setup_hoge():
    global hoge
    hoge = 'hoge'

としてアプリケーションの設定をする処理のうちにsetup_hogeを呼び出してやります。 (まぁ paste.app_factory に指定する main 関数とかそんなとこで呼ぶ)

だめだった

でもここで、別のモジュール内でhogeが直接読まれてるとちょっと問題がありました。 直接っていうのはこういうこと:

from mymodule import hoge  # Forever None

このモジュールがひょんなことで読み込まれていると、このモジュール内でのhogeは setup_hogeが呼ばれてもNoneのままです。

setup_hogeが書き換えるのはあくまでmymodule内のhogeなので、別モジュールに読んでいると そっちまでは反映されないんですね。当たり前っちゃそうか。

解決

というわけで間接的に使います:

import module

module.hoge

もしくは取得用の関数を用意する:

# In mymodule.py

hoge = None

def get_hoge():
    return hoge

def setup_hoge():
    ...

これでうまくいきそうです

すぺしゃるさんくす

nakanurayさんとpodhmoさんが教えてくれました

podhmoさんが書いてくれたgistがわかりやすい

ありがとうございます

追記

zope.proxy というのを使うと もっと綺麗/安全に書けるようです

すごい。これならhogeをうっかり直接使っても問題ないですね。 aodagさんが教えてくれました。あzさzs

WSGI向け認証フレームワーク、repoze.whoを使ってみた

WSGI向け認証フレームワーク、repoze.whoを使ってみた

repozewwho というWSGI用の認証フレームワークがあります。

Webアプリケーションを書くうえでユーザー認証はほぼほぼ必須となりますが、 それを提供してくれる素晴らしいものです。

なかなかよくできたやつなのですが、分かるまでが分かりにくいです。 repozewwho がどんなものか、どう使うのかを中心に、 理解への助けになるものをまとめておこうと思います。

pyhack 36 でやってたことです。

repoze.whoで何ができたか

WSGIでWebアプリケーションを作る際になどに、認証の枠組みを提供できます。

実際に今作っている WeiWei という(WSGIベース、つまりWebフレームワークなしで作っている) Wikiエンジンで導入してみました。 これがその変更点なのでこれをパクれば導入できます:

導入できたものは「WSGIアプリケーションが401を返したときにBasic認証を促し、 入力された場合は入力が何であれ認証を許可する」もの。 (WSGIミドルウェアとして repozewwho を設定しています)

ここで注意すべきなのは、この変更点では「利用者の入力が正当なものか」の検証はしていないということ。 実際の認証処理は SillyPlugin が担当しているけど、ここではユーザーの入力があればとりあえず「認証OK!!!」としています。 (Sillyなんです)。 この変更点はあくまで repozewwho の導入であり「ユーザーの入力が正しいかどうか」を判定する処理は 書いていない。

言ってしまうと repozewwho はそんなものを提供するものじゃないです。

repoze.whoって何よ

repozewwhodjango.contrib.auth のような「なんかユーザーモデルとかあるやつ」じゃないと分かったところで、 repozewwho とは何なのよ。

簡単に言うと「単なる枠組み」。 そこに付随する処理(例えば上記した、実際の認証をする'Authenticator')を自分たちで プラグインとして提供できます。 repozewwho 自体が提供するプラグインもあるけど、根幹としてある repozewwho は枠組み/ 流れのみを提供してくれるものです(美しい)。

repozewwhoプラグインを差し込む場所として提供しているのは以下4点:

  • Identifier: 認証情報を取り出す部分
  • Authenticator: 認証を実際にする部分
  • MetadataProvidor: 認証時に取れた付加情報を追加する部分 (今回は使ってない)
  • Challenger: 認証を促す部分

その差し込んだプラグインが、 repozewwhoWSGIミドルウェアとして使うことで適宜呼び出されて いきます(repozewwho が提供するAPIで明示的に呼ぶこともできますヾ(´∀`)ノキャッキャ)。

それぞれの説明は、 repozewwho コントリビューターでもあるaodagさんのブログ記事も参考に なります。

使ったプラグインとその説明

今回使ったプラグインと、 repozewwho の実際の動きについて少々。

今回の場合は以下のプラグインを使いました:

  • Identifier: basicauth (repoze.who提供)
  • Authenticator: silly (自前の仮置き)
  • Challenger: basicauth (repoze.who提供)

Identifierとしてのbasicauth

Basic認証から認証情報(identity)をとりだします。 identity[‘login’]とidentity[‘password’]からクライアントの入力をとれます (このidentityは例えばAuthentiatorなどで使われる)。

Challengerとしてのbasicauth

クライアントに対してBasic認証をさせるヘッダを送信する。

他のChallengerとしてはredirectorなどがあり、ログイン画面にリダイレクトさせたりできます。

Challengerの発火

ちらっと説明したように、Challengerが発火するタイミングは 「 repozewwho 配下のWSGIアプリケーションが401を返したとき」としています。

この挙動はwho.iniに記述したgeneralセクションの challenge_decider による挙動で、そこは適宜変更できます。

今回の場合はloginを担当するWSGIアプリケーションから「ユーザーがログインしていなければ 401を返す」ようにしてChallengerを呼び出させています。 が、やりかたとしてはあんまり賢くないですね。今後改良することになりそうです。

(余談: login_viewはDjango/Pyramidのviewと同じ立ち位置のもので、WSGIアプリケーションとしてはweiwei.web.login_dispatchというものが担当しています。これは WeiWei の実装の話です)

サードパーティプラグイン

プラグインとしては優秀そうなものがいくつかありますが、あまりメンテされてない印象。 たいていのものは repoze.who 1.0 系にのみ対応したものになるならしい。 以下の3つはなかなか優秀そうであるけど、repoze.who 2系に対応してないくさいとか Python 3系に対応してないとかで使用しなかったものです:

  • repoze.wo_sqlalchemy :バックエンドをSQLAlchemyとしてUserModelとかをうまく扱ってくれるっぽい(UserModel!!)
  • repoze.what: 認証したうえでの権限の扱いなどをしてくれるものっぽい
  • repoze.who-use_beaker : repozeが提供する認証を保持するauth_tktというプラグインCookieベースでいけてないので、それをbeaker (セッション)でやろうというもの。

まぁ使いたければメンテしてやるか参考にして自分で作ればいいです。

repozewwho は美しい(プラグインを基本としてその枠組みのみ提供するとこが良い)ですが、 どうにもプラグインとして外部に提供させると使う側としては選別が面倒になりますね。

さっき書いたSillyPluginではあまにり滑稽なので、その後にちゃんとUserモデルを使った 認証を WeiWei では書いています

まぁ自分でプラグインを書いていきましょう。

まとめ

結局 repozewwho とは、認証の流れを提供するだけのものでした。 プラグインを開発者が提供して初めて期待する「認証」ができます。 repozewwho は .ini. の記述によるプラグイン設定、zope.interfaceによるプラグインの実装 など非常に美しいものでした。

「ユーザーモデルとかあってDBに保存するもの欲しい」という人はプラグインを探すか 自分で書くか、WSGIだけというのは諦めて優秀なWebフレームワークのDjangoとかを使いましょう。

matcha0.1リリースのお知らせとその背景

matcha0.1リリースのお知らせとその背景

matcha という WSGI dispatcher をリリースしました。 WSGIのライブラリ/ミドルウェアで、PATH_INFOを考慮した WSGIアプリケーションの呼び出しを主目的にしています。

この記事では matcha の紹介を軽くしつつ、 後半では本音として書きたかった dispatcher を実装して思ったことを書きます。

matchaの利点

matcha が売りにできるのは Djangoのurls.pyっぽく書ける の、1点ですね。 基本的にはこんなかんじで記述します:

>>> from matcha import Matching as m, bundle
>>> 
>>> from yourproject import home_app
>>> from yourproject.blog import post_list_app, post_detail_app
>>> 
>>> matching = bundle(
...     m('/', home_app, 'home'),
...     m('/post/', post_list_app, 'post_list'),
...     m('/post/{post_slug}/', post_detail_app, 'post_detail'),
... )

bundleとかMatchingとかちょっと名前が違うくらいですね。

他にも matcha は以下の点において便利です:

  • 上記のように宣言的に書けるし、手続き的にも書ける
  • 名前(上記の例だと'post_listなど')からURLの逆引きができる
  • WSGIアプリケーションの呼び出し意外にも使える

機能的に matcha は他のdispatcherと大して変わらないです。 他にもいくつか dispatcher ありますが、 matcha を除いた中では WebDispatchが良さげです:

dispatcherを4つくらい作って思った

公開したもの2つ、公開してないもので2、3 dispatcher を書いてみた印象としては URL での dispatch は分割して用意したほうが良い ということです。

URL での dispatch にはある程度持っておきたい機能があります:

  • environのPATH_INFO, SCRIPT_NAMEに副作用を起こす
  • URLからの引数取得
  • 名前/対象アプリケーションから URL の逆引き

これらの機能を実現しつつ、「URL にとらわれない柔軟な dispatcher」を作るのは 難しかったです(詳細は後述)。

matcha では(少なくとも 0.1 リリースでは) URL からのマッチングのみ 提供し、それ意外の条件での dispatch は利用者や matcha を使ったWebフレームワーク の開発者に提供してもらうことにします。

route_nameを噛ませればPyramidのように使えますし、URLでのdispatchしか提供しないので あればDjangoのようになります。

matchaと私とときどきgargant.dispatch

つい1ヶ月ほど前に gargant.dispach を リリースし、 PyCon APAC 2013 のLTでも紹介したばかり ですが、 matcha を書き始めました。その経緯など。

gargant.dispatch はかなり優秀で実装も面白いのですが、柔軟にしすぎようとしたせいもあり 以下の点で URL dispatcher として劣っていました。:

  • PATH_INFO, SCRIPT_NAMEに副作用を起こさない
  • 名前/対象アプリケーションから URL の逆引きができない
  • URLから引数を取るのが面倒

先ほども少し触れたところです。

これは gargant.dispatch が「matchingというものを PATH_INFO や REQUEST_METHODのみを 対象にしない」という考えのもとに作られているので「URLの逆引き」のような PATH_INFO に必ず 依存する実装が持たせにくいというものでした。

もちろんやりようによってはできると思いますが、柔軟性を維持しつつ そういった制約(WSGIの仕様やWebアプリケーションでよく使われる機能)への対応 を入れるのは意外と難しかった(だるかった)です。

まぁ gargant.dispatch もかなり良い勉強になったのでこのまま廃れてもまぁいいかなという思いです。 パッケージにおける gargant 以下は実験的なWebフレームワークを作るための場所としていますし。 ただ実験的なものだけじゃなくて、実用的なものもちゃんと作っておきたいところなので、 matcha は集大成でもありますね。

テンプレートエンジンでも作るか

と思っています。 そもそも私は別に「dispatcherを書きたいオジサン」というわけでなく、 Webアプリケーションにおいてサーバーサイドで必要なものを下から見た場合に dispatcherがあったということです。すでにWebサーバーは書き散らして飽きて、 Request/Responseオブジェクトはだるかったので飽きてます。

なので次はテンプレートエンジンでも作ってみようかなと思っています。

Introducing django-websettings

Introducing django-websettings

I released new package named django-websettings. This is django’s third party application to provide a web interface to set a yet another django’s settings.

If you are similar to read English, please read the README of django-websettings.

I write in Japanene from here.

django-websettingsの紹介

私のpoorなGithub Englishが好きな人は今すぐREADMEから読んでください。

django-websettingsはDjangoサードパーティアプリケーションで、 Webインタフェースから設定可能なsettings.pyのようなものを提供するものです。

ユーザーさんにちゃっちゃと手直しして欲しいような値を、Webインタフェースから 入力して貰えたらなと思って作った。インストール方法などもREADMEを見てほしい。

基本的には予め指定したwebsettings.pyというファイルに以下のように書く:

# In websettings.py
DRUM = 'Ritsu Tainaka'
BASS = 'Mio Akiyama'

値は websettings から取れる。

>>> from websettings import websettings
>>> websettings.BASS
'Mio Akiyama'

websettings.pyを書くと自動でWebインタフェースが作られて、これらの設定値を入力できる。

image

こんなかんじで書き換える(ベースを純ちゃんにした)

image

これでsubmitしてからもう一度シェルを叩くと、値が変わっている:

>>> websettings.BASS
'Jun Suzuki'

というもの。 詳しくは README みてください

Tips: for Initial arg of Django's Form fields

Tips: for Initial arg of Django’s Form fields

Django’s Form fields take a initial argument to specify the initial value for that Field.

For more detail about initial, check out the doc Form fields #initial

Basically…

As written there, if you want to set a initial value as a result of a callable, you should pass the callable directly to the argument. like this:

class DateTimeForm(forms.Form):
     now = forms.DateTimeField(initial=datetime.datetime.now)

With arguments

Mistake

Let’s consider the case passing some argument to callable to pass to initial. A common mistake is like this:

# Don't do this
class DateTimeForm(forms.Form):
     now = forms.DateTimeField(initial=datetime.datetime.now(tz))

In this case, the now value would have been fixed at the time you start the server. You should pass a callabse directary, how do you pass arguments to the callable?

Elegant solution

Let’s put together the calling with partial function application. The following is a simple example using functools.partial.

>>> from functools import partial
>>>
>>> zero_until_ten = partial(range, 10)
>>> zero_until_ten()
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Yes, you made a callable without arguments. This technique can be used in the previous example, like this:

class DateTimeForm(forms.Form):
     now = forms.DateTimeField(initial=partial(datetime.datetime.now, tz))

The initial will be passed a callable, so the resulting value will not be fixed.

It’s beautiful.

Inelegant solution

Alternatively, you can write like this:

class DateTimeForm(forms.Form):
     now = forms.DateTimeField()

    def __init__(self, *args, **kwargs):
        super(DateTimeForm, self).__init__(*args, **kwargs)
        self.fields.get('now')._set_choices(datetime.datetime.now(tz))

The beauty declarative was lost.

Another develoaper must read __init__, and you should be careful not to forget the calling super.

Not recommended. Use the functools.partial function.

Search view with long long parameters

Search view with long long parameters

At most of web application have the so-called search view. The view get some request parameters and display some search results by using these parameters.

A search view does not cause any side effects, so it should expect a GET request. The URL will be like this:

/search?code=R100100&code=R1001001

and the view will be like this:

def search_studends_view(request):
    f = SearchForm(data=request.GET)

    students = None
    if f.is_valid():
        codes = f.cleaned_data['code']
        students = get_students(codes=codes)

    return HttpResponse(",".join(students))

(This is a sample code, so I have not verified the correctness of it)

In most cases, the GET search view seems good and enough. But unfortunately, it have some limitations.

With long long parameter

Let’s consider a searching by using very long parameters. If you should create a view that can be OR search by 1,000 birthdays…

The URL will be like this:

/search?code=R100100&code=R100101&code=R100102&code=R102000&code=...

Then, what would happen? (You may have a doubt the requirement itself)

The header of this request will be very long too. so, It will couse “Request URI too large (414)”.

Gunicorn’s maxcimum size of a request line is 4094 bytes by default.

At Nginx, it is 8k bytes.

If you use reverse proxying, you also should consider settings proxy_buffers, proxy_buffer_size.

だるい。

With POST

Another solution, you can use POST request with search view.

But, if you make the honest, you will run into trouble caused browser history backing. If you back to POST result, most browser will raise warning (like ‘Web page has expired’).

A re-POSTing is handled as bad process to protect such as multiple registration. Also, the POST results will not be cached, because a POST request cause side effect in general. (Some browsers like Chrome, Firefox will cache it. but IE will not so even if you provide Cache-Controll header)

To avoid this, you should redirect to a GET result page after POSTed. POSTed parameter will store to session, and GET page display by using it.

like this:

def search_studends_view(request):
    if request.method == 'POST':
        request.session['search_params'] = request.POST
        return HttpResponseRedirect('/search')
    else:
        try:
            params = request.session['search_params']
        except KeyError:
            params = {}
        f = SearchForm(data=params)

        students = None
        if f.is_valid():
            codes = f.cleaned_data['code']
            students = get_students(codes=codes)

        return HttpResponse(",".join(students))

(Yes, this is a just sample code)

It is not beautiful.

If you want to paginate these results you shuould get a page number from GET parameter (puke).

And more

Serializing and compressing GET parameters on client side may also be good solution.