Studio3104::BLOG.new

uninitialized constant Studio3104 (NameError)

突然 ERD を要求されたときに便利な eralchemy

開発してるプロダクトで突然 ERD 出せと言われることがあります。
そんなときには eralchemy を使うと便利です。
実際の Schema から ERD を出力してくれます。

使いからは上記のリンク先の README を見るだけでイナフですがこんな感じ。

brew install eralchemy
eralchemy -i sqlite:///db.sqlite -o erd_from_sqlite.pdf

スーパーイージーですね。pip からもインストール出来ます。

Django の Abstract Model を py.test でテストする

※これは Python Advent Calendar 2015 2日目のエントリです。


各モデルが共通のカラムや振る舞いを持つようになってきたら

Django でアプリケーションを開発していると、各モデルに共通のカラムを持たせたり、共通の振る舞いをさせたりしたいということが起こると思います。
そのような場合は、各モデルに対して同じような実装を施すのではなく、Abstract Model を定義して各モデルがそれを継承するようにするのが自然でしょう。

例えば以下の様な感じです。

from django.db import models


class CommonAbstractModel(models.Model):
    created_datetime = models.DateTimeField(auto_now_add=True)
    updated_datetime = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True
from common.models import CommonAbstractModel


class User(CommonAbstractModel):
    name = models.models.CharField(max_length=100)

このように実装することによって、User モデルに直接 created_datetime, updated_datetime を定義する必要がなくなるので便利です。

ではテストをどうするか

CommonAbstractModel を継承したモデルが確実に created_datetime, updated_datetime を持っているかということをテストした気持ちになるのが自然ですよね。
ただ abstract = True なモデルは manage.py makemigrationsマイグレーションが作成されないため、通常のモデルのように普通にテストを書くことがかないません。
とはいえ、子クラスである User に対してそれらテストを行うのは、あくまで User のテストであって CommonAbstractModel をテストするということにはならないです(なるかも知れないけど、例えば CommonAbstractModel に論理削除の実装が入ったりしたらそれも子クラスでテストするんですか?という話になりますね) 。

py.test を使った例

実はこの例(継承したモデルが確実に created_datetime, updated_datetime を持っているかというテスト)だと、擬似的なマイグレーションを行う必要はまったくなく、擬似的な子クラスだけがあれば良かったりします。。。
Abstract Model に論理削除とかを実装したくなったときには必要ですが。

import pytest
import datetime

from common.models import CommonAbstractModel
from django.db import connection
from django.core.management.color import no_style


@pytest.fixture(scope='module')
def fx_TestInheritedModel():
    class TestInheritedModel(CommonAbstractModel):
        class Meta:
            app_label = 'common'

    return TestInheritedModel


@pytest.yield_fixture(scope='function')
@pytest.mark.django_db
def fx_test_model_cls(request, fx_TestInheritedModel):
    cursor = connection.cursor()
    statements, pending = connection.creation.sql_create_model(fx_TestInheritedModel, no_style())
    for sql in statements:
        cursor.execute(sql)

    yield fx_TestInheritedModel

    statements = connection.creation.sql_destroy_model(fx_TestInheritedModel, (), no_style())
    for sql in statements:
        cursor.execute(sql)


@pytest.mark.django_db
class TestCommonAbstractModel:
    @pytest.mark.parametrize(('attr_name'), [
        'created_datetime',
        'updated_datetime',
    ])
    def test_inherited_model_has_proper_fields(self, fx_TestInheritedModel, attr_name):
        inherited_model = fx_TestInheritedModel()
        assert hasattr(inherited_model, attr_name), 'the inherited model must have %s field' % (attr_name, )

ちょっとだけ解説

テスト内部で擬似クラスを作成してそれに CommonAbstractModel を継承させて、それを擬似マイグレーションをさせてテストしてしまおうという戦略です。

擬似クラス

フィクスチャをこのように定義します。
app_label = 'common' がめっちゃ重要で、これを定義しておかないと擬似マイグレーションさせることが出来ません。

@pytest.fixture(scope='module')
def fx_TestInheritedModel():
    class TestInheritedModel(CommonAbstractModel):
        class Meta:
            app_label = 'common'

    return TestInheritedModel
擬似マイグレーション

各テストメソッドが実行される前にテーブルが作成され、終わったら消されます。
yield_fixture を使うのがポイントです(scope='function' はデフォルトなのであえて指定する必要はないかもしれませんが、Explicit is better than implicit. です。)

@pytest.yield_fixture(scope='function')
@pytest.mark.django_db
def fx_test_model_cls(request, fx_TestInheritedModel):
    cursor = connection.cursor()
    statements, pending = connection.creation.sql_create_model(fx_TestInheritedModel, no_style())
    for sql in statements:
        cursor.execute(sql)

    yield fx_TestInheritedModel

    statements = connection.creation.sql_destroy_model(fx_TestInheritedModel, (), no_style())
    for sql in statements:
        cursor.execute(sql)
あとは...

実際に fx_test_model_cls を利用するテストをガシガシ書いていくだけです!

django.test.TestCase を使いたい場合

普通に setUp, tearDown を使って同じようなことすれば良さそう。


EOF

Python でオブジェクト内部の dict に直接アクセスする

  • Ruby でいうとこのこういうやつ。
class Hage
  def initialize()
    @hage = { bozu: 1, hage: 2 }
  end

  def [](key)
    @hage[key]
  end

  def []=(key, value)
    @hage[key] = value
  end
end

h = Hage.new()
p h                    #=> #<Hage:0x007f94428a2c20 @hage={:bozu=>1, :hage=>2}>
p h[:bozu]         #=> 1
h[:fusa] = 3
p h[:fusa]          #=> 3
  • Python でやるとこう。
class Hage:
    def __init__(self):
        self.hage = { 'bozu': 1, 'hage': 2 }

    def __getitem__(self, key):
        return self.hage[key]

    def __setitem__(self, key, value):
        self.hage[key] = value

h = Hage()
print h            #=> <__main__.Hage instance at 0x109f6aef0>
print h['hage'] #=> 2
h['fusa'] = 3
print h['fusa']  #=> 3

thank you for letting me, @!

n 種類のカラーコードのちょうど平均のカラーコードが知りたいとき

R,G,B でそれぞれの平均値で再構成したらちょうど真ん中になるのでは?

という感じで冗長だけど書いた

自作ライブラリを GitHub に置いて、そこから pip インストール出来るようにするまで

Rubyist でしたが Python もはじめました。
パーフェクト PythonKindle で購入しましたが、検索出来ないし文字を選択できないのでコピペ出来なくて大変不便なので、物理版を購入すれば良かったと後悔しております。

話が逸れました。本題に。
これを書いている現在、当方 Python 歴数日 なので温かいアドバイスなどあれば是非ともよろしくお願いいたしますm(__)m

雛形を作る

paster create を実行し、プロジェクトの設定を対話的に入力していきます。

$ pip install python_boilerplate_template
$ paster create

こんな感じになる。

$ tree -C hoge
hoge
├── hoge
│   └── __init__.py
├── hoge.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── entry_points.txt
│   ├── not-zip-safe
│   └── top_level.txt
├── setup.cfg
└── setup.py

2 directories, 9 files

実装

コードを書きます。

依存ライブラリを書いておく

requirements.txt に依存ライブラリを書いていくのが普通っぽい。
んだけど、上述の雛形の作成方法だと requirements.txt は作られないし、作成された setup.py からは当然参照されなくて不便。
なので、setup.py から requirements.txt を参照させるようにして、作成したライブラリがインストールされるときにちゃんと依存ライブラリも一緒にインストールされるようにする。

--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,14 @@
 from setuptools import setup, find_packages
-import sys, os
+from pip.req import parse_requirements
+import sys, os, pip

 version = '0.1'

+requirements = [
+    str(requirement.req)
+    for requirement in parse_requirements('requirements.txt', session = pip.download.PipSession())
+]
+
 setup(name='hoge',
       version=version,
       description="test project",
@@ -17,9 +23,7 @@ setup(name='hoge',
       packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
       include_package_data=True,
       zip_safe=False,
-      install_requires=[
-          # -*- Extra requirements: -*-
-      ],
+      install_requires=requirements,
       entry_points="""
       # -*- Entry points: -*-
       """,

あとは

普通に実装が終わったら普通に GitHub にコードを上げる。
で、使うときはこんな感じで。

  • 作成するアプリケーションの依存に突っ込みたい場合

    • requirements.txt に追記

      -e git://github.com/youraccount/youreggname#egg=eggname

    • インストール

      $ pip install -r requirements.txt

  • 普通にインストールする場合

    $ pip install -e git://github.com/youraccount/youreggname#egg=eggname

この手順で作ったやつ

github.com

大物 alter table の途中で Lost connection to MySQL server during query が発生したときに確認したいこと

とりあえず show processlist してまだ動いてるっぽかったら、datadir 配下で #sql-xxxx_xxxxx.ibd な名前のファイルが育っているかどうか確認する。
タイムスタンプが更新されていればテンポラリテーブルへのデータコピーが生きてるということになるので、あとは show processlist を監視して終わるのを待てば良い。
で、終わったっぽかったら show create table を確認して変更が反映されてれば完了。

って @ さんに教えてもらった。ありがとうございます!

ゆるく `my.cnf` の比較などが出来る gem を作った

複数my.cnf を比較して差異を知りたいが、通常のファイル同士の比較(diff) ではわかりにくいしそもそも知りたい情報を得るのはつらい。
ということがあり作成していた書き捨てのスクリプトがあったのだが、整理してテストまで書いて rubygems に放流した。

mycnf | RubyGems.org | your community gem host

studio3104/mycnf · GitHub

mycnf.gem

機能概要

parse

my.cnf のファイルパスを食わせると parse して hash を返す

generate

上記の parse のフォーマットの hash を食わせると my.cnf を文字列で返す

compare

上記の parse のフォーマットの hash を複数食わせると、比較しやすいフォーマットで hash を返す
(ファイルパスを複数食わせて同様の結果を返す compare_files もある)

diff

上記の compare の結果から、差異のあるパラメタのみを select して返す
(ファイルパスを複数食わせて同様の結果を返す diff_files もある)

使い方

こんな感じの my.cnf があったとする。

$ cat /etc/my.cnf.1
[client]
port            = 3306
socket          = /var/lib/mysql/mysql.sock

[mysql]
no_auto_rehash

[mysqld]
datadir         = /var/lib/mysql
port            = 3306
socket          = /var/lib/mysql/mysql.sock
$ cat /etc/my.cnf.2
[client]
port            = 3308
socket          = /var/lib/mysql/mysql.sock

[mysql]
no_auto_rehash
safe-updates

[mysqld]
datadir         = /var/lib/mysql
port            = 3308
socket          = /var/lib/mysql/mysql.sock
parse
MyCnf.parse('/etc/my.cnf.1')
{
   client: {
     port: 3306, socket: '/var/lib/mysql/mysql.sock'
   },
   mysql: {
     no_auto_rehash: ''
   },
   mysqld: {
     datadir: '/var/lib/mysql', port: 3306, socket: '/var/lib/mysql/mysql.sock'
   }
}
generate
MyCnf.generate({
  client: {
    port: 3306,
    socket: '/var/lib/mysql/mysql.sock'
  }
})
[client]
port = 3306
socket = /var/lib/mysql/mysql.sock
compare
MyCnf.compare(MyCnf.parse('/etc/my.cnf.1'), MyCnf.parse('/etc/my.cnf.2'))
MyCnf.compare_files('/etc/my.cnf.1', '/etc/my.cnf.2')
{
  client: {
    port: [ 3306, 3308 ],
    socket: [ '/var/lib/mysql/mysql.sock', '/var/lib/mysql/mysql.sock' ]
  },
  mysql: {
    no_auto_rehash: [ '', '' ],
    safe_updates: [ nil, '' ]
  },
  mysqld: {
    datadir: [ '/var/lib/mysql', '/var/lib/mysql' ],
    port: [ 3306, 3308 ],
    socket: [ '/var/lib/mysql/mysql.sock', '/var/lib/mysql/mysql.sock' ]
  }
}
diff
MyCnf.diff(MyCnf.parse('/etc/my.cnf.1'), MyCnf.parse('/etc/my.cnf.2'))
MyCnf.diff_files('/etc/my.cnf.1', '/etc/my.cnf.2')
{
  client: {
    port: [ 3306, 3308 ]
  },
  mysql: {
    safe_updates: [ nil, '' ]
  },
  mysqld: {
    port: [ 3306, 3308 ]
  }
}

出来ないこと(0.0.1 現在)

暗黙のデフォルト値との比較は出来ない

たとえば、innodb_buffer_pool_size の値が明示されいるものとそうでないものを比較した場合は以下のような結果が返る。

$ cat /tmp/my.a.cnf
[mysqld]
server_id = 1
innodb_buffer_pool_size = 128M
$ cat /tmp/my.b.cnf
[mysqld]
server_id = 2
$ pry
[1] pry(main)> require 'mycnf'
=> true
[2] pry(main)> MyCnf.diff_files('/tmp/my.a.cnf', '/tmp/my.b.cnf')
=> {:mysqld=>{:server_id=>[1, 2], :innodb_buffer_pool_size=>["128M", nil]}}

MySQL 5.6 系において、innodb_buffer_pool_size のデフォルト値は 128MB であるが、そういったデフォルト値を考慮した比較をする実装にはなっていない。
あくまで明示的に記述されているパラメタのみの比較を行う。

!include, !includedir の先に定義してある設定を追いかける

すべてのメソッドにおいて、!include, !includedir の先で定義してあるパラメタを追うことは出来ない。
あくまで引数に与えたファイル及び hash の中だけで完結する範囲で処理を行います。

さて

こういうのって書き捨てて終わっちゃうことが多くて、あとでまた使いたいときに見つからないとかあったりして困ることがある。
ので、まぁこの程度ならすぐまた書けるべって思わなくもないけど、整理して次の機会にもまたすぐに使えるようにしておいた。