Hatena::ブログ(Diary)

Memo

2016-05-29

[] Flask 0.11 の後方互換がなくなる機能

Flask 0.11 がリリースされた。

Flask 0.11 Released | The Pallets Projects

# ちなみにどうでもいいけど、Flask のコードネームは酒の名前


後方互換がいくつかなくなってるので、まとめてみる。

# 漏れがあったら追記する予定


とりあえず自分のプロジェクト生成ツールで作成した Flask project を起動して試してみただけ。

flask.ext.* が非推奨に

/tmp/virtualenvs/flask_011/lib/python3.5/site-packages/flask/exthook.py:71: ExtDeprecationWarning: Importing flask.ext.cache is deprecated, use flask_cache instead.
  .format(x=modname), ExtDeprecationWarning

こんな感じで、Flask の拡張ライブラリ系で flask.ext.* を使っていると、警告が表示される。

自分のプロジェクトは flask.ext.* を flask_* に変更すればいい。

GitHub - pallets/flask-ext-migrate: A script for migrating extension import syntax.

あるいはマイグレーションツールがあるので、それを利用すると解決する。


ただし、ライブラリ内で使われている(例えば、Flask-Cache)場合は、ライブラリの対応を待つ必要がある。

Flask-Cache は Pull Request が出てたので、取り込まれるのを待つ。

# 自分のプロジェクトの場合、未だに flask.ext.* を使ってたのが、Flask-Cache だけだった


個人的には名前空間的に ext というのに属しているのはわかりやすくて良いし、flask_ とプレフィックスがつくのはイマイチ好きじゃない…。

# ext hook を呼んでいてるのでそのオーバヘッドはもちろんあるだろうけど…。

request.json が非推奨に

API サーバーとして Flask を利用していて、クライアントから JSONパラメーターとして要求している場合に、よく使われると思うけど、request.json じゃなくて request.get_json() にする必要がある。


メソッドを呼ぶ方がわかりやすいので、良いと思う。

Jinja2 のテンプレートのオートリロード

今まで、アプリケーションを起動したまま、Jinja2 のテンプレートを変更したら、次のリクエストからは、変更されたテンプレートが反映されたが、今回からは TEMPLATES_AUTO_RELOAD を Flask の設定に含めないといけない。

TEMPLATES_AUTO_RELOAD の設定がない場合は、デバッグモードの値を Jinja2 に auto_reload に突っ込んでいる。


本番環境では debug=False にしているので、ちょっと HTMLテンプレートを起動したまま変更…というのをやりたい場合は、TEMPLATES_AUTO_RELOAD にする必要がある。


大きい変更

flask コマンドが追加された。

Command Line Interface — Flask Documentation (0.11)

今まで、Flask-Script を使って、Flask にコマンドを追加していた。

それを使わなくても Flask だけでできるようになる(つまり Flask-Script を差し替える必要がある)。

時期を見て置き換えよう…。


他諸々追加になったりしてるものがあるので、何かあったら追記する。

# きになるのが、SESSION_REFRESH_EACH_REQUEST くらいか。

2016-05-14

[] Electron1.0 の起動時のエラー

遊びで作ってる Electron なアプリケーションの electron-prebuilt を 1.0.2 にあげて起動したら、エラーになった。

Cannot find module ‘app’

Cannot find module 'app' - electron - Atom Discussion


ということで、

import electron from 'electron';
import path from 'path';

const app = electron.app;
const BrowserWindow = electron.BrowserWindow;
const crashReporter = electron.crashReporter;

というふうに変えたら、いけた。

あと、crashReporter で submitURL を設定してないとエラーになって起動しないので、適当な URL を設定した。

2016-04-30

[] Vim の起動速度2

前回 Vim の packadd を使って、起動速度改善をした。

その後 MacVim Kaoriya に Timer 機能が追加されたバージョンが出たので、Timer に置き換えた。

.vimrc は以下みたいな感じで定義。

let s:plugins = [
  \ 'vim-l9',
  \ 'vim-fuzzyfinder',
  \ 'vim-fuzzyfinder-plugins',
  \ 'yankround.vim',
  \ ]
let s:idx = 0

function! PackAddHandler(timer)
  execute 'packadd ' . s:plugins[s:idx]
  let s:idx += 1
  "doautocmd BufReadPost
  autocmd! lazy_load_bundle
endfunction

augroup lazy_load_python
  autocmd! VimEnter *.py call timer_start(1, 'LazyLoadPython', {'repeat': 0})
augroup END

augroup lazy_load_filetype
  autocmd! VimEnter *.go,*.js,*.php call timer_start(1, 'LazyLoadFileType', {'repeat': 0})
augroup END

augroup lazy_load_bundle
  autocmd VimEnter * call timer_start(1, 'PackAddHandler', {'repeat': len(s:plugins)})
augroup END

.vim/config/plugins.vim

function! LazyLoadPython(timer) abort
  let _ft = &filetype
  packadd jedi-vim
  packadd pyflakes-vim
  packadd vim-autopep8
  packadd vim-virtualenv

  " Reload autocmd
  exec 'doautoall BufRead'

  " For Quickrun.vim
  exec 'set ft=' . _ft

  autocmd! lazy_load_python
endfunction 

function! LazyLoadFileType(timer) abort
  let _ft = &filetype
  echomsg string(_ft)
  if _ft =~ 'go'
    packadd vim-ft-go
    packadd vim-go-extra
    packadd vim-godef
  elseif _ft =~ 'javascript'
    packadd! vim-javascript
    packadd! vim-jsx
    packadd vim-jsdoc
  elseif _ft =~ 'php'
    packadd vmustache
    packadd pdv
  endif
  exec 'doautoall BufRead'
  " For Quickrun.vim
  exec 'set ft=' . _ft
  autocmd! lazy_load_filetype
endfunction

~/.vim/pack/ftbundle にファイルタイプごとのプラグインを入れている。

ftbundle は Timer を使って、Vim 起動後にバックグラウンドプラグインを読み込む。


他のプラグインは ~/.vim/pack/bundle/start というところに入れて、起動時に読み込むようにしている。


これで起動速度がどうなったかというと、vim-plug と比べてみた。

vim-plug の速度。

357.614  030.946: opening buffers
359.083  001.469: BufEnter autocommands
359.089  000.006: editing files in windows
359.430  000.341: VimEnter autocommands
359.436  000.006: before starting main loop
360.991  001.555: first screen update
360.993  000.002: --- VIM STARTED ---

packadd の速度。

132.434  030.058: opening buffers
132.907  000.473: BufEnter autocommands
132.913  000.006: editing files in windows
134.438  000.653  000.653: sourcing /Users/s_ohyanagi/.vim/pack/bundle/start/unite.vim/autoload/unite/custom.vim
139.671  004.501  004.501: sourcing /Users/heavenshell/.vim/pack/bundle/start/vim-watchdogs/autoload/watchdogs.vim
143.496  010.225  005.071: sourcing /Users/heavenshell/.vim/config/plugins.vim
143.862  000.724: VimEnter autocommands
143.868  000.006: before starting main loop
145.161  001.293: first screen update

起動速度が大体倍ほど速くなった。

2016-03-21

[] Jedi.vimVim の起動速度

あるいは、いかにして Vim の起動速度を取り戻したか。


いつのころか Vim の起動速度が遅くなった。

正確に言うと、Python のファイルを起動する際に遅い。


とりあえず起動時に何が遅いかプロファイルを取ってみた。

954.157  614.589  614.589: sourcing /Users/me/.vim/ftbundle/python/jedi-vim/./autoload/jedi.vim
954.426  617.175  002.586: sourcing /Users/me/.vim/ftbundle/python/jedi-vim/./ftplugin/python/jedi.vim
2947.941  1926.078  1926.078: sourcing /Users/me/.vim/ftbundle/python/jedi-vim/./autoload/jedi.vim
2948.210  1946.837  020.759: sourcing /Users/me/.vim/ftbundle/python/jedi-vim/./ftplugin/python/jedi.vim
757.152  499.467  499.467: sourcing /Users/me/.vim/ftbundle/python/jedi-vim/./autoload/jedi.vim
757.358  500.011  000.544: sourcing /Users/me/.vim/ftbundle/python/jedi-vim/./ftplugin/python/jedi.vim

速度が安定しないけど、酷い時で約 1.95sec 速い時で 500msec かかってる。

一度 Vim を起動して、すぐに再起動すると、もっと速い。

# OSキャッシュにのせないため、マシンを再起動しながらやったから時間かかった…


いつからこんな遅くなったのかというのを調べてみた。

jedi-vim 0.7 jedi 0.7

108.692  000.672  000.672: sourcing /Users/me/.vim/ftbundle/python/jedi-vim/./ftplugin/python/jedi.vim
172.864  000.078  000.078: sourcing /Users/s_ohyanagi/.vim/ftbundle/python/jedi-vim/./after/./ftplugin/python/jedi.vim

速い。ただし補完の候補が出てくるのが遅い(最新は0.7 に比べると速い)。


jedi-vim 0.8 jedi 0.8

1255.951  984.472  984.472: sourcing /Users/me/.vim/ftbundle/python/jedi-vim/./autoload/jedi.vim
1256.185  1009.923  026.451: sourcing /Users/me/.vim/ftbundle/python/jedi-vim/./ftplugin/python/jedi.vim

どうやら 0.8 から遅くなった。


ソースコードを見ていると、0.7 に比べて jedi 自体がファイル数が増えている。

最新の jedi 0.9 になるともっと増えている。

プロファイル取るために全てのコードにデバッグコードを仕込んだわけではないが、おそらく Python のファイルの import に時間が掛かっている模様。


インポートが遅いのは Python 自体を速くする、マシンの性能を速くする、ファイル数を減らすなどをしないと無理なので、ちょっと簡単に手を出せない。


次に試したのが、Vim-Plug による lazy load。

Vim-Plug とかそこら辺のプラグインマネージャ系は lazy load があるそうなので試してみた。

結果は対して変らない。

Lazy load ってのが filetype が python の時に読み込むというのが lazy load やら on demand load という事ならそらそうだ。

もともと unbundle.vim を使って filetype ごとのロードしかしていない。

# 余談だけど、vimrc とかに読み込むプラグイン候補をいちいち書くの、めんどくさかった


どうしたもんかと思っていたら、Vim 自体のパッケージ機能は使えないかと思ったら、タイムリーに

vim pluginのbackground loading - daisuzu's notes を読んだ。


MacVim Kaoriya(7.4 1-1537) にはまだタイマー機能は入っていなかったが、 Vimのパッケージ機能を試してみました - Blank File にある遅延読み込みをすれば出来るんじゃないかと思い、試してみた。

" ~/.vim/config/plugins.vim に定義
function! LazyLoadPython() abort
  packadd jedi-vim
  packadd pyflakes-vim
  packadd vim-autopep8
  packadd vim-virtualenv
  set filetype=python
  autocmd! lazy_load_python
endfunction

" .vimrc に定義
autocmd VimEnter * source ~/.vim/config/plugins.vim
augroup lazy_load_python
  autocmd! InsertEnter *.py :call LazyLoadPython()
  autocmd Filetype python    setlocal omnifunc=jedi#completions
augroup END

タイマー機能でバックグラウンドで読み込みたいが、まだできないので、とりあえず初回のインサートモードに入った際に実行するようにした。

これで Vim の起動速度が以下の感じになった。

jedi-vim 0.7 191.565
jedi-vim 0.8 482.318
jedi-vim 0.91015.580
pack178.358

ただこれでもインサートモードの最初は遅いので、早くタイマー機能使ってバックグラウンドで読み込みさせたい。

Pack について

Pack については、Vimのパッケージ機能を試してみました - Blank File にあるとおり、ハマりどころがある。

完全に stable になっているわけではないし、変更が色々入る可能性が高い。

ただ packadd したらプラグインが有効になるという設計は素晴らしいと思った。

タイマー機能が安定して使えるようになれば、完全にバックグラウンドで遅延読み込みができる。


vimrc などに読み込むパッケージを定義しなくて良いのも素晴らしい。

自分は気になるプラグインgit clone して Vim を起動するだけにしたいので、自分の使い方にもあっている。

2016-03-18

[] Browserify と fs モジュール

Electron で Watchify(Browserify)使ってると、fs モジュールが使えないという現象にあたった。


今遊んでいる環境が、Gulp で babel と wathify で Electron なアプリケーションを差分コンパイルしながらのため、brfs を使えばいけるとか、ググったら色々でてくるが、gulp で brfs を transform に入れてもだめだった。

const fs = window.require('fs');

とかだと行けた。


が、window とかつけなくても Electron で Babel を使った Markdownアプリケーションとかあるので、絶対いけるはずと、ググったら、node-webkitでもbrowserify使いたいしnodeのrequireも使いたい - Qiita がヒットした。

gulp.task('compile:js', $.watchify((watchify) => {
  var b = watchify({
    watch: watching,
    verbose: true,
    debug: true,
    ignoreMissing: true,     // これ
    detectGlobals: false,     // これ
    bare: true,
    transform: ['babelify', 'envify'],
    builtins: [],                   // これ
  });

  ['boot.js'].forEach((f) => {
    gulp.src(SRC_DIR + f)
      .pipe($.plumber())
      .pipe(b)
      .pipe(buffer())
      .pipe($.sourcemaps.init({ loadMaps: true }))
      .pipe($.if(DEBUG, $.uglify({ compress: true })))
      .pipe($.if(DEBUG, $.sourcemaps.write()))
      .pipe(gulp.dest(DIST_DIR));
  });
}));

上記のように watchify の定義の順番を変えてやればいけた。


なお環境は

    "babel-core": "^6.5.2",
    "babel-plugin-react-transform": "^2.0.0",
    "babel-preset-es2015": "^6.5.0",
    "babel-preset-react": "^6.5.0",
    "babelify": "^7.2.0",
    "electron-prebuilt": "^0.36.9",
    "watchify": "^3.7.0"

な環境。

2016-02-20

[] SQLAlchemy との戦い

ここ数ヶ月 SQLAlchemy を使って開発をしている。


開発当初は特に問題もなく調子よく行っていたが、自分のローカル環境の MySQL の設定を本番に近づけたため、 SQLAlchemy がエラーをはいた - Memo が出た。

pool_recycle の値を短くすれば解決と思ったが、解決しなかった。

前提として FW は Flask を利用。SQLAlchemy は素で scoped_session を使っている。

Flask-SQLAlchey や Flask-Alchemy-Session は使ってない。

autocommit, autoflush は False の設定。


でたエラーが

sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (2013, 'Lost connection to MySQL server during query') 

だったり

sqlalchemy.exc.StatementError: (sqlalchemy.exc.InvalidRequestError) Can't reconnect until invalid transaction is rolled back 

だったり。


現象としては、一つのページにとどまっていて、wait_timeout で設定した 60 秒を超えて、ページをリロード(MySQL にアクセス)した際にでる。

# 更新系は出ない


とりあえずぐぐって、出てきたページを片っ端から試してみた。

SQLAlchemyの OperationalError: MySQL Connection not available エラー - Life is Really Short, Have Your Life!!

コメント欄にあるように、session.remove() をリクエストの最後に呼んでやる。

これは元々 Flask の teardown_request で呼んでいる。

効果無し。


FlaskとSQLAlchemyを使っててMySQL server has gone awayってなる - petitviolet_blog

SQLAlchemy+MySQLで「Lost connection to MySQL server during query」|python|サムライファクトリー開発ブログ

にあるように、Pool event を使って、MySQL リクエストごとに ping を飛ばす。

Connection Pooling — SQLAlchemy 1.1 Documentation

効果無し。


Session.removeを呼ぶタイミング - スコトプリゴニエフスク通信

@soundkitchen さんに教えて貰った。

リクエストの最後は漏れる可能性があるから、リクエストの最初に remove() してセッションを作り直してみたら? というもの。

効果なし。


更新系ではでなくて、参照系で出るのはなにか違いがあるのかと思って、コードを見直したら、session.commit() しているかしていないかの違いがあった。

これかと思って、試してみたら、正解だった。


これで万事解決したかと思ったが、依然でる所があった。

因みにでたのが、WTForms で SQLAlchemy からセレクトボックスに DB の項目を出している所だった。


こんな感じで、セッションのクローズ漏れが出てくるのが怖い。

SQLAlchemy — The Pyramid Community Cookbook v0.2

Pyramid のドキュメントではリクエストコンテキストの最後に、必ず commit, close をしていた。

これにならって、以下のようにした。

@app.teardown_appcontext
def session_clear(exception):
    if exception and Session.is_active:
        session.rollback()
    else:
        #session.commit() 最初こうしてたけど、意図して Rollback した際に不具合おきるからなにもしない
        pass
    Session.close()

あと、session 生成時に、session.expire_on_commit = False しておかないと、例外が起きた。

DetachedInstanceError: Instance <MyModel at 0x36bb190> is not bound to a Session;
attribute refresh operation cannot proceed

なので、sessionmaker の際に expire_on_commit を付けるようにした。


とりあえずこれで安定して動くようになった。

2016-01-11

[] WTForms で QuerySelectMultipleField に default を設定する

WTFormsのSelectFieldで選択値(selected)を保持する方法 - Life is Really Short, Have Your Life!!

WTForms で Select 関連は default を外部から設定できない。

SelectField や QuerySelectField は上記にある。

QuerySelectMultipleField で DB から読み込んだ値をセレクトボックスで複数をデフォルトで設定したい場合は以下のようにすれば行けた。

# -*- coding: utf-8 -*-
from wtforms.ext.sqlalchemy.fields import QuerySelectMultipleField as Base

    """
    QuerySelectMultipleField can only add `default` value at constructor.

    If you want to do like following, you should use this class.

    .. code-block:: python
        form = MyForm(**values)
        #: `users` is `QuerySelectMultipleField` value.
        #: Rows is SQLAlchemy's row list.
        form.users.default = ([u.users.id for u in rows])
    """
    def iter_choices(self):
        if self.allow_blank:
            yield ('', self.blank_text, self.data is None)

        for pk, obj in self._get_object_list():
            if obj.id in self.default:
                yield (pk, self.get_label(obj), True)
            else:
                yield (pk, self.get_label(obj), False)