Make組ブログ

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

Don't format strings before logging in python

Don’t format strings before logging in python

You should not provide formatted string to loggers in Python, like this:

logger.info('Logged in: %s' % username)

You should write like this:

logger.info('Logged in: %s', username)

Why?

In mont cases, it is the same as a result. But, internally, the later one contains more information which part of this string represents a username.

You shoud realize the first argument (message) can also be used as a signature.

If you want to aggregate logs, you will group logs by messages, like this:

  • message: ‘Logged in: %s’, args: (‘Ritsu Tainaka’,)
  • message: ‘Logged in: %s’, args: (‘Mio Akiyama’,)

Yes, you will be able to group these logs. They are same log, just username is different.

OK, now consider this grouping with formatted strings, it will be not work:

  • message: ‘Logged in: Ritsu Tainaka’, args: ()
  • message: ‘Logged in: Mio Akiyama’, args: ()

They will be handled as different. Of cause, these messages are totally different.

Practical

Sentry, error logging and aggregation platform, it displays logs grouping by these messages.

So, If you use Sentry, you should provide not formatted message to any loggers. Without this, all logs containing some variables will be handled as different. Yes, as thousands of different logs.

Django's TestCase.multi\_db attribute is mistake

Django’s TestCase.multi_db attribute is mistake

Django’s test framework (django.test.TestCase) has a atribute multi_db . It should be set True when testing on multiple databases (False, by default).

If you forget this setting, and your test uses multiple databases, the test suite only flushes the ‘default’ database (more exact django.db.utils.DEFAULT_DB_ALIAS) without flushing another databases.

And then, some trash datas will left on these databases (ignore the ‘default’ database). After tests will have risk of failing in absurd reason. I’m handling multiple databases on my work, and every time I forget setting this. And the mistake is difficlut to notice. Of cause, there are no errors. This is expected behavior.

This behavior (only flushing ‘default’) is feature for speeding up. Because, flushing a database is slow, and it will be run for each tests.

But, I think, it should be flush all databases by default.

If your application using multiple databases, but a TestCase use only ‘default’, then you can set ‘flush_only_default = True’ (for example) to force a test suite flushing only ‘default’ database.

Of cause, your application uses only ‘default’ database, only ‘default’ will be flushed even if without setting flush_only_default = True.

I think the behabior for speeding up is optional. By default, it should be performed as indubitable even if it will be slow.

The change for this will be like this:

diff --git a/django/test/testcases.py b/django/test/testcases.py
index a9fcc2b..0558476 100644
--- a/django/test/testcases.py
+++ b/django/test/testcases.py
@@ -466,11 +466,11 @@ class TransactionTestCase(SimpleTestCase):
     def _databases_names(self, include_mirrors=True):
         # If the test case has a multi_db=True flag, act on all databases,
         # including mirrors or not. Otherwise, just on the default DB.
-        if getattr(self, 'multi_db', False):
+        if getattr(self, 'flush_only_default', False):
+            return [DEFAULT_DB_ALIAS]
+        else:
             return [alias for alias in connections
                     if include_mirrors or not connections[alias].settings_dict[
-        else:
-            return [DEFAULT_DB_ALIAS]

     def _reset_sequences(self, db_name):
         conn = connections[db_name]

I asked about this proposal on django developers IRC channel (#django-dev on freenode). Some people answered me (Thanks a lot!), in side disagree. Certainly, this proposal is not have great gain. and having a big risk breaking compabitity. I noticed this is not good proposal, but I still claim the multi_db behavior is mistake.

Yes, I will set multi_db = True on a base class, and subclassing it on own tests.

Sentry with django-newauth

Sentry with django-newauth

djangonnewauth is a library to add customizable user model (It developed before Django 1.5). And sentry is a platform to collect logs and aggregate these.

Sentry can collect errors raised by some django applications, and displays informations of these errors.

Problem

Sentry can tell us a user encountered some errors. But, if the application uses djangonnewauth, the user provided by newauth will not be displayed.

I tried to write a SentryPlugin to display the newauth’s user information. As it turns out, I could not do it. It was a difficult than I thought.

Solution

I wrote a package to solve this raven_django_newauth .

You can set SENTRY_CLIENT = ‘raven_django_newauth.client.DjangoNewauthClient’, and then, User interface of Sentry provides a information of newauth’s user.

Sentry and Raven

Sentry collects informations sended by raven. Sentry defines general interfaces (for example a User), and raven traslates collected data to thing in consideration of these interfaces. A client send dictionary thats key is path to each interfaces (like ‘setry.interfaces.User).

The User interfaces in sentry.interfaces.User. And on django application, the raven client is raven.contrib.django.client.DjnagoClient. The client gets user information from get_user_info method and stores to the dictionary as a key ‘sentry.interfaces.User’

Djangoでアップロードしたファイルを非同期に削除する

Djangoでアップロードしたファイルを非同期に削除する

Django の FileField を通してアップロードしたファイルを非同期に削除したい。

本質的にはモデルが削除されると同時にファイルも削除したいというものなんだけど、 そこを同期処理しているとやってられないので、非同期で削除しようという話。

案1

  • Django のシグナルを使って、モデルが削除されたときに非同期でファイルを削除する タスクを呼び出す。

私はシグナルにいい思い出がなかったので、この方法はちょっと疎遠したかった。

案2

  • カスタムのストレージを書いて、ファイルの削除を非同期に行わせる。

これを採用。

残念な思い込み

FileField を持つモデルのインスタンスが delete されるときに、ファイルの delete も走ると思っていた。 でも実際はそうじゃなくてモデルが delete されても、ファイルの delete (ストレージの delete) は走らなかった(まぁ勝手に削除されるのはこわいか)。

明示的に FieldFile インスタンスの delete を呼ぶ必要があった:

class Test(models.Model):
    file = models.FileField(....)

test.file.delete()  # まぁこんなかんじ。

(ここでモデルのインスタンスにある file 属性は django.db.models.fields.FieldFile のインスタンス で、こいつの delete がストレージの delete を呼び出す)

書いたもの

非同期にファイルを削除するストレージを書いている。

でもまぁ結局 UploadedFile モデルの delete をオーバーライドして、ファイルも削除するようにしている。 これなら案1のように、 delete のシグナルでファイル削除用の非同期タスクを走らせても大差なかった。

非同期処理の書き方については自分のブログ記事が役に立った:

まとめ

残念

Djangoのチケット18481についての雑記

Djangoのチケット18481についての雑記

Django に投げていた パッチが昨夜取り込まれた のでその話。

前に取り込まれた チケット 18558 以来。

チケット 18481 について

このチケットは、別のチケット 17277 に由来してる。 17277 は POST の body 読み込み時にエラーがあれば、それ専用のエラーをあげるよう修正するというもの。

17277 以前は例外処理が入っていないので、例外があれば IOError が投げられていた。 でもこれってわかりにくいから、 IOError を継承した UnreadablePostError を投げるよう修正された。

でもこのチケット 17277 には漏れがあって、 FILES の場合はそのままだった。 なので今回取り込まれたチケット 18481 では、FILES で例外が発生するときも UnreadablePostError を 投げるよう修正しましょうというもの。

やったこと

私がこのチケットをみたときには、すでにバグ修正のチケットがあったのでテストのパッチを書いた (KyleMac に報告されて、 edevil が実装の修正を追加していた)。

owner もついていなかったしチケットも 2 ヶ月ほど放置されていたので、 ここはテストのパッチを書いてやるかと思った次第。

チケット 17277 で追加されたチケットを参考に、 FILES で例外が発生するようにしたテストを書いた。

取り込んでくれたのは claudep 、また彼にお世話になった。

PythonでWebサーバー書きましょう

PythonでWebサーバー書きましょう

「Webサーバー書いたことある?」

と聞かれ、何じゃらホイと尋ねると

「書いたことあれば分かりそうな問題にハマってる事例見てね。みんな書いたことあるわけじゃないのね、と思って」

とのこと。

そういえば私も書いたこと無かったのでWebの悟りを開くために書いた。

書いた

リクエストからURIとって、静的ファイルを読み込んでそのまま返す荒々しいヤツ

  • 404は返せません。
  • 500も返せません。
  • /etc/passwdは返せます。

socket-低レベルネットワークインターフェース 読んだだけ。

周りの人の話

  • とりあえずsocketで通信多重度1のstatic file返すだけのサーバー書くだけでもかなり勉強になると思うよ
  • waitress 読みやすくていいよ
  • GETだけなら簡単だよ
  • python以外で勉強として作りたいなら C でソケット開いて、まじめにパースする

との教えが。

まとめ

自己同一性の達成過程においてWebサーバー書くことは重要らしい。

ふとWSGIサーバーとか書きたくなる。

Pyramidでzope.interfaceを使う

Pyramidでzope.interfaceを使う

Pyramidzope.interface を使うときの話

zope.intreface

zope.intreface_ が面白いと最近知った。

実装を持たない「インターフェース」だけ定義して、 そこに後で実装をもたせる、というかんじ(超ざっくり)

折れ線グラフとしてのインターフェースをILinechart、 なにやら描画してくれるインターフェースをIRendererとして考えてみる。:

import zope.interface


class IRenderer(zope.interface.Interface):
    def render():
        """なんか描画する的"""


class ILinechart(zope.interface.Interface):
    series = zope.interface.Attribute("""Y軸の値みたいな""")
    category = zope.interface.Attribute("""X軸の値みたいな""")


@zope.interface.implementer(IRenderer)
class LinechartRenderer(object):

    __used_for__ = ILinechart

    def __init__(self, context):
        self.context = context

    def render(self):
        return str(zip(self.context.series, self.context.category))

こんなかんじか。 嬉しいのは:

  • インターフェースだけ先に書いといて実装はあとから持たせられる。
  • 継承みたいに書いた時点で関係が固定されるわけじゃない。

とかかな。正直面白半分で使い始めてる節あるので何とも。

zope.interface のドキュメント読んでこのへん読んどけば捗りそう:

Pyramidにおけるzope.interfaceの使い方

zope.interface とか zope.component でもアダプターの登録ができますが、 それは Pyramid でやりましょう。

Registry というものに登録する。 まぁconfigとかrequestにひっついてるから:

# config 経由で登録して
config.registry.registerAdapter(LinechartRenderer,
                                (ILinechart,), IRenderer, '')

# request 経由で取ると
adapted = request.registry.getAdapter(linechart, IRenderer, '')

# 良い感じ
adapted.render() # '[(0, 0), (1, 1), (2, 4)]' とか

でまぁ:

config.registry.registerAdapter(LinechartRendererJSON,
                                (ILinechart,), IRenderer, 'json')
config.registry.registerAdapter(LinechartRendererHTML,
                                (ILinechart,), IRenderer, 'html')

とかname変えて登録していくとさらに良い (この場合rendererのメソッドにもたせたほうがいいかもだけど)。

お作法

そんな感じで、アプリケーション書くときに

  • __init__.pyでconfig.registry.registerAdapter
  • view_callable内でrequest.registry.getAdapter

とかやっちゃうわけだけど、これはまぁ行儀良くないらしい。 zope.interface を意識しないといけないのはライブラリやフレームワークの開発者であって ユーザー(ライブラリ等を使って開発する人)には見せたくないとのこと[要出典]。

まあたしかに、もっとわかりやすい書き方してよって気になるしね。

そのお行儀の良い書き方というのは簡単で:

  • config.add_directiveでdirectiveを追加
  • そのdirective経由でアダプター登録
  • requestを受け取るAPIとしての関数から、アダプトされたオブジェクトを返す

というもの。 さらに venusian を使って、directive経由でアダプター登録してたのを デコレーターで書いてやることができる。

まぁこんなかんじになるのか(renderer_configとto_rendererは自分で書くのよ):

@renderer_config('',
                 chart_type='linechart')
def str_linechart(linechart):
    return str(zip(linechart.series, linechart.category))

renderer = to_renderer(linechart, '')
renderer.render()

このrenderer_configのなかではconfig.set_rendererとか呼び出して登録してやるといい。 さながらPyramidのview_configとconfig.add_viewみたいなもんである。

まあ一見に如かずなのでrebecca.todictを読めばいいと思う:

これを参考に私もpyramid_tochartというのを書いてるので、こっちも参考になるかも:

ただまあ良い書き方を模索してるところ