読者です 読者をやめる 読者になる 読者になる

sftpserverモジュールをforkしていじった件

諸事情によりテスト用のsftpサーバーが必要になったのでsftpserverなるモジュールを見つけてきた。しかしこいつシングルスレッドなので、やりたいことのためにはmultiprocessingと併用する必要があったわけなんだけど、そうするとなんかエラーが出る。

raise AssertionError("PID check failed. RNG must be re-initialized after fork().
Hint: Try Random.atfork()")

で調べてみたところそのものズバリのエラーで困ってる人がいた。

python - multiprocess module with paramiko - Stack Overflow

どうやらparamikoのバグらしい。paramikoのforkで、pure python実装なsshモジュールだとこの問題は解決済みらしい。

ちなみにpyftpdlibはFTPSには対応しているけれど、SFTPには対応してないとのことだったので諦めた。issueに「SFTPにも対応しないの?」みたいなのもあったけど、「pyftpdlibがやるようなことじゃない」って蹴られてたので多分今後もそういう方向には行かないと思う。
あと、pyFileSystemというものがSFTPに対応していたので使ってみたけど、こいつもparamiko依存なので同じエラーが出る。

で、仕方がないのでforkしてちょこっと改良した。

kk6/sftpserver at spike · GitHub

変更点

  • 内部でのparamikoの使用をやめてsshに移行した
  • optparseをやめてargparseに移行した
  • なんとなくデーモン化したかったのでpython-daemonでデーモン起動できるようにした

今にして思えばデーモン化はちょっと余計だった気がしてpull-requestは飛ばしてない

テストで使う

unittestのsetupで、こんな感じでmultiprocessingと併用して立ち上げてやる

def setUp(self):
    from sftpserver import start_server
    from multiprocessing import Process

    args = (HOST, PORT, KEYFILE, 'DEBUG')
    self._process = Process(target=start_server, args=args)
    self._process.start()


def tearDown(self):
    self._process.terminate()

Djangoの発行する生SQLが見たい

ForeignKeyとfilterのメモ - AtAsAtAmAtArA
DjangoのORマッパーでSQL文を簡単に出力するサンプル - 十番目のムーサ

というやりとりを見て。シェルで確かめるのが楽だし便利。
django.db.connection.queriesで見られます。

おもむろにdjangoのシェルを起動

$ python manage.py shell

>>> from django.db import connection
>>> connection.queries
[]  # 最初は当然からっぽ
>>> from entry.models import Entry  # 適当にモデルをインポート
>>> Entry.objects.filter(is_active=True).order_by('-modified')
[<Entry: ぴよ>, <Entry: aa>, <Entry: ふが>, <Entry: ほげ>]
>>> connection.queries[-1]['sql']
u'SELECT "entry"."id", "entry"."del_flg", "entry"."author_id", "entry"."title", "entry"."body", "entry"."slug", "entry"."created", "entry"."modified", "entry"."publish", "entry"."is_active" FROM "entry" WHERE "entry"."is_active" = True  ORDER BY "entry"."modified" DESC LIMIT 21'
>>> connection.queries[0].keys()
['time', 'sql']  # 実行時間と生SQLが入ってる

詳しくは公式ドキュメントのFAQを参照してください。

シェル操作課題をPythonでやってみた

はてブのマイホットエントリーを眺めてたらこんなものを見つけた。
シェル操作課題 (cut, sort, uniq などで集計を行う) 設問編 - Yamashiro0217の日記
awkとか全然知らないから勉強になるかなと思って覗いてみたんだけど…

いっそpythonワンライナーで実行したらカッコイイと思うw

やりましょう

Python3.2でやってみたよ

問1 このファイルを表示しろ
$ python3 -c "import functools,operator;print(functools.reduce(operator.__add__, (open('hoge.csv','r').readlines())))"

server1,1343363124,30,/video.php
server2,1343363110,20,/profile.php
server3,1343363115,7,/login.php
server1,1343363105,8,/profile.php
server2,1343363205,35,/profile.php
server2,1343363110,20,/profile.php
server3,1343363205,30,/login.php
server4,1343363225,12,/video.php
server1,1343363265,7,/video.php

問2 このファイルからサーバー名とアクセス先だけ表示しろ
$ python3 -c "import csv,operator,functools;print(functools.reduce(operator.__add__, [','.join((r[0], r[3]+'\n')) for r in csv.reader(open('hoge.csv','r'))]))"

server1,/video.php
server2,/profile.php
server3,/login.php
server1,/profile.php
server2,/profile.php
server2,/profile.php
server3,/login.php
server4,/video.php
server1,/video.php

問3 このファイルからserver4の行だけ表示しろ
$ python3 -c "print([line for line in open('hoge.csv','r').readlines() if line[6]=='4'][0])"

server4,1343363225,12,/video.php

問4 このファイルの行数を表示しろ
$ python3 -c "print(len(open('hoge.csv','r').readlines()))"

9

問5 このファイルをサーバー名、ユーザーIDの昇順で5行だけ表示しろ
$ python3 -c "import operator,csv,functools;print(functools.reduce(operator.__add__, [','.join((r[0],str(r[1]),str(r[2]),r[3])) for r in sorted([(r[0], int(r[1]), int(r[2]), r[3]+'\n') for r in csv.reader(open('hoge.csv','r'))], key=operator.itemgetter(0,2))][:5]))"

server1,1343363265,7,/video.php
server1,1343363105,8,/profile.php
server1,1343363124,30,/video.php
server2,1343363110,20,/profile.php
server2,1343363110,20,/profile.php

これが一番ひどい。数値としてソートするためにintに変換して、その後文字列連結するためにintにしたものをstrに直してる。

問6 このファイルには重複行がある。重複行はまとめて数え行数を表示しろ
$ python3 -c "print(len(set(open('hoge.csv','r'))))"

8

問7 このログのUU(ユニークユーザー)数を表示しろ
$ python3 -c "import csv;print(len(set([r[2] for r in csv.reader(open('hoge.csv','r'))])))"

6

問8 このログのアクセス先ごとにアクセス数を数え上位1つを表示しろ
$ python3 -c "import csv,collections;print(collections.Counter([r[1] for r in set([(r[2],r[3]) for r in csv.reader(open('hoge.csv','r'))])]).most_common(1))"

[('/video.php', 3)]

表示がちょっとアレだけど、知りたい情報が取り出せてるしこれでいいよねもう。

問9 このログのserverという文字列をxxxという文字列に変え、サーバー毎のアクセス数を表示しろ
$ python3 -c "import csv,collections;print(collections.Counter(map(lambda x: x.replace('server', 'xxx'), [r[0] for r in csv.reader(open('hoge.csv','r'))])))"

Counter({'xxx1': 3, 'xxx2': 3, 'xxx3': 2, 'xxx4': 1})

これまた表示が形式が違うけどこれでいいよね。ていうか出題者の想定結果が間違ってる気がするんですが。

問10 このログのユーザーIDが10以上の人のユニークなユーザーIDをユーザーIDでソートして表示しろ
$ python3 -c "import csv;print(set([r[2] for r in csv.reader(open('hoge.csv','r')) if int(r[2]) >= 10]))"

{'12', '30', '20', '35'}

これまた表示が(ry

感想

Pythonワンライナーなんてやるもんじゃない。

Python2.7のunittestのassertRaisesはコンテキストマネージャーとして使用出来る

テスト対象

def add(x, y):
    return x + y

適当すぎだけどご勘弁。異なった型同士を足そうとすれば当然TypeError吐きますよね。

本題

Python2.7以前のunittestのassertRaisesは assertRaises(exception, callback, *args, **kwargs) というように呼び出す必要があった。

class SomeTest(unittest.TestCase):
    def test_add(self):
        # 正しい書き方
        self.assertRaises(TypeError, add, (2, [1, 2, 3]))
        # 間違った書き方
        self.assertRaises(TypeError, add(2, [1, 2, 3]))

このテストを実行すると間違った書き方のところでassertRaisesがTypeErrorを捕らえきれずに普通にテストが落ちます。でも、見た目的に間違った書き方のほうが感覚的にはしっくりするよなーっていうのが本音。

Python2.7以降のassertRaisesはコンテキストマネージャーとして使える

で、python2.7からはassertRaisesがコンテキストマネージャーとして使えるよう変更が加えられた。これにより、より直感的なこのような書き方が可能になった。

class SomeTest(unittest.TestCase):
    def test_add(self):
        with self.assertRaises(TypeError):
            add(2, [1, 2, 3])

実にスッキリしていてわかりやすい。まさにPythonicってやつですね。

Grubのデフォルト起動OSを変更する

あまり使わなくなったWin/Ubuntuデュアルブートマシンを同居人が使いたいということで、デフォルト起動OSをWindowsになるようにしなくてはいけなくなった。で、いちおうぐぐってみたらすぐ見つかったんだけど、同じくらいのタイミングでこんなアドバイスをいただいた。

ぐぐって見つけた記事はmenu.lstを編集すると書いてたけど、どうもそういうファイルはなくて、knzm2011さんの言うとおりに

$ sudo grub-update

して /boot/grub/grub.cfg を編集した。このファイルのはじめの方に

set default="0"

と書かれている行があるのだけど、この数値を変更すればいいみたい。OS選択画面にて Windows 7 は上から5番目だったので

set default="4"

として再起動したら、OS選択画面にて無事 windows 7 の行がデフォルトで選択されている状態になった。めでたしめでたし。

Django 1.4 で新しくプロジェクトを作成した際にしておくsettings.pyの設定

django 1.4 からはプロジェクトを作成した際のディレクトリ構成が変わったのでどのように設定するのがいいかを、まだ数の少ないネット上のサンプルを参考にしつつまとめてみた。

ちなみに以下の様な構成を想定してます。

└── project_root
    ├── app1
    │   ├── __init__.py
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    ├── app2
    │   ├── __init__.py
    │   ├── models.py
    │   ├── tests.py
    │   └── views.py
    ├── manage.py
    └── package_root
        ├── __init__.py
        ├── settings.py
        ├── site_media
        │   ├── media
        │   └── static
        ├── static
        ├── templates
        ├── urls.py
        └── wsgi.py

settings.pyを編集

序盤は公式ドキュメントのチュートリアルにも出てくるような内容だけど一応。path周りが今回のメインだけど、正直この構成がベストプラクティスなのかはわからない。

データベース設定
# とりあえず冒頭で os モジュールをインポートしておく
import os

# 中略

# データベースの設定。とりあえずsqlite3。
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'example.sqlite',
        'USER': '',
        'PASSWORD': '',
        'HOST': '',
        'PORT': '',
    }
}
タイムゾーン言語コードの設定を変更
TIME_ZONE = 'Asia/Tokyo'
LANGUAGE_CODE = 'ja'
各種Pathの指定
# プロジェクトのルート。
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
# パッケージのルート(settings.pyの入っているディレクトリ)
PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__))

# ユーザーからアップロードされた画像などを保存する場所
MEDIA_ROOT = os.path.join(PACKAGE_ROOT, 'site_media', 'media')
MEDIA_URL = '/site_media/media/'
# js/css/imgなどの静的ファイルの保存場所
STATIC_ROOT = os.path.join(PACKAGE_ROOT, 'site_media', 'static')
STATIC_URL = '/site_media/static/'
# 特定のアプリと関係のないstaticファイルの格納場所
STATICFILES_DIRS = (
    os.path.join(PACKAGE_ROOT, 'static'),
)
# テンプレートファイルの格納場所
TEMPLATE_DIRS = (
    os.path.join(PACKAGE_ROOT, 'templates'),
)

os.path関連の解説

os.path周りのちょっとした解説をしておきます。ほんとちょっとだけ。__file__ == /home/kk6/hoge.py だとします。

絶対パスの表示
os.path.abspath(__file__)  # /home/kk6/hoge.py
ファイルのディレクトリの表示
os.path.dirname(__file__)  # /home/kk6
1階層上のディレクトリを表す
os.pardir  # '..'
カレントディレクトリ
os.curdir  # '.'
ディレクトリ間のセパレータ
os.sep  # '/'
ファイル名と拡張子のセパレータ
os.extsep  # '.'

以上を踏まえた上で

# これだとなんかモヤモヤするので
os.path.join(os.path.dirname(__file__), os.pardir)  # /home/kk6/..

# 絶対パスにする
os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)  # /home

結局...

os モジュールをしっかり理解すればそれで済むことに気づいた。

デコレータを作ってみる

前回の記事(Pythonのデコレータを理解するときに残したメモ - kk6のメモ帳*)でデコレータについて理解したので、じゃあ今度はデコレータを作ってみようと思う。あんまり詳しくないけどflaskを題材としてみる。

# hello.py
from flask import Flask
from flask import render_template

app = Flask(__name__)

@app.route('/<name>')
def hello(name):
    return render_template('hello.html', name=name)

if __name__ == '__main__':
    app.run(debug=True)

htmlはなんか適当に下みたいな感じに用意してください。

<!DOCTYPE HTML>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Hello Flask</title>
</head>
<body>
  {% if name %}
    <h1>Hello {{ name }}!</h1>
  {% else %}
    <h1>Hello World!</h1>
  {% endif %}
</body>
</html>

構成はこんな感じ

├── hello.py
└── templates
     └── hello.html

flaskのview関数って基本的に return render_template('hoge.html', hoge=hoge)とかすると思うんだけど、それは面倒なのでview関数はシンプルにテンプレートに渡すコンテキストの辞書を返すようにして、その辞書とテンプレートを受け取ってrender_templateを実行するデコレータを作ってみる。

具体的にはこんな感じになるように実装する。

@app.route('/<name>')
@render('hello.html')
def hello(name):
    return {
        'name': name,
    }

考え方というか説明の仕方として二通り思いついたんだけど、どっちがわかりやすいか判断つかなかったので一応両方掲載。個人的には後者のほうがわかりやすいような気もする。

考え方その1:内側から組み立てる

まず、最終的に実行したいものを明確にする。view関数が返す辞書をrender_templateに渡して実行したい。

ctx = view_func(*args, **kw)
render_template(template, **ctx)

まずはこれを関数にする

def wrapped(*args, **kw)
    ctx = view_func(*args, **kw)
    return render_template(template, **ctx)

この時点で足りないのはview_funcとtemplate。view_funcはデコレート対象の関数。templateはデコレータ自身に渡す引数。なのでまずはview関数を引数に受取る関数で包む。(@render('hello.html')みたいな使い方のデコレータを作りたいわけなので、当然最も外側の関数名はrenderになるし、その引数はテンプレートのパスになる。必然的にview関数を受け取る関数はその内側にネストしてなくてはならなくなる)

def wrapper(view_func):
    @wraps(view_func)
    def wrapped(*args, **kw)
        ctx = view_func(*args, **kw)
        return render_template(template, **ctx)
    return wrapped

さらにデコレータ自身に渡す引数を受け取る関数で包む

def render(template)
    def wrapper(view_func):
        @wraps(view_func)
        def wrapped(*args, **kw)
            ctx = view_func(*args, **kw)
            return render_template(template, **ctx)
        return wrapped
    return wrapper

考え方その2:外側から組み立てる

まず、@render('hello.html')という使い方をしたいので、最も外側の関数は

def render(template):
    ....
    return ...

のようになる。続いて、その内部にview関数を受け取る関数wrapperを用意し、renderがwrapperを返すようにする。

def render(template):
    def wrapper(view_func):
        ...
        return ...
    return wrapper

さらに、元の関数の引数を受け取る関数wrappedを用意し、wrapperがwrappedを返すようにする。

def render(template):
    def wrapper(view_func):
        @wraps(view_func)
        def wrapped(*args, **kw):
            ...
            return ...
        return wrapped
    return wrapper

これでテンプレート(template)、view関数(view_func)、view関数の引数(*args, **kw)が揃ったので、あとはやりたい処理(render_templateの実行)を記述するだけ。

def render(template)
    def wrapper(view_func):
        @wraps(view_func)
        def wrapped(*args, **kw)
            ctx = view_func(*args, **kw)
            return render_template(template, **ctx)
        return wrapped
    return wrapper

ここからは余談

デコレータの作り方としては以上なんだけど、これだとview関数の戻り値が辞書じゃない場合(return redirect(url_for('hogehoge'))とかしてBase Responseとかが返ってくる場合)に、それをrende_templateに渡そうとしてしまってエラーになったりするので、辞書以外が来た場合はそのまま返すように手を加える。

def render(template)
    def wrapper(view_func):
        @wraps(view_func)
        def wrapped(*args, **kw)
            ctx = view_func(*args, **kw)
            if isinstance(ctx, dict):
                return render_template(template, **ctx)
            else:
                return ctx
        return wrapped
    return wrapper

これでひとまず使えるものができたと思います。flask詳しくないので何か足りないかもしれないけど、それはその都度拡張・修正ということで。