ラベル Python の投稿を表示しています。 すべての投稿を表示
ラベル Python の投稿を表示しています。 すべての投稿を表示

2020-09-18

Spyder on WSL1 + Ubuntu-18.04

Spyder を使ってみようと、WSL1 + Ubuntu-18.04 + Anaconda3 環境で Spyder を起動するも、エラーで起動せず。前提として、X11 周りは問題なく動いている。

user@MYPC:~$ echo $WSL_DISTRO_NAME
Ubuntu-18.04

user@MYPC:~$ conda activate

(base) user@MYPC:~$ conda list --full-name anaconda
# packages in environment at /home/user/anaconda3:
#
# Name                    Version                   Build  Channel
anaconda                  2020.07                  py38_0

(base) user@MYPC:~$ spyder
Traceback (most recent call last):
  File "/home/user/anaconda3/bin/spyder", line 11, in <module>
    sys.exit(main())
  File "/home/user/anaconda3/lib/python3.8/site-packages/spyder/app/start.py", line 201, in main
    from spyder.app import mainwindow
  File "/home/user/anaconda3/lib/python3.8/site-packages/spyder/app/mainwindow.py", line 71, in <module>
    from qtpy.compat import from_qvariant
  File "/home/user/anaconda3/lib/python3.8/site-packages/qtpy/compat.py", line 15, in <module>
    from .QtWidgets import QFileDialog
  File "/home/user/anaconda3/lib/python3.8/site-packages/qtpy/QtWidgets.py", line 22, in <module>
    from PyQt5.QtWidgets import *
ImportError: libGL.so.1: cannot open shared object file: No such file or directory

「libGL.so.1」が無いと言っているので、apt-file でパッケージを探す。

(base) user@MYPC:~$ apt-file search libGL.so.1
libgl1: /usr/lib/x86_64-linux-gnu/libGL.so.1
libgl1: /usr/lib/x86_64-linux-gnu/libGL.so.1.0.0
nvidia-340: /usr/lib/i386-linux-gnu/libGL.so.1
nvidia-340: /usr/lib/x86_64-linux-gnu/libGL.so.1
primus-libs: /usr/lib/x86_64-linux-gnu/primus/libGL.so.1
virtualbox-guest-x11: /usr/lib/virtualbox/additions/libGL.so.1
virtualbox-guest-x11-hwe: /usr/lib/virtualbox/additions/libGL.so.1

「libgl1」を入れて、リトライ。

(base) user@MYPC:~$ sudo apt install --no-install-recommends libgl1
<...snip...>

(base) user@MYPC:~$ spyder
Traceback (most recent call last):
  File "/home/user/anaconda3/lib/python3.8/site-packages/qtpy/QtWebEngineWidgets.py", line 22, in <module>
    from PyQt5.QtWebEngineWidgets import QWebEnginePage
ImportError: libXtst.so.6: cannot open shared object file: No such file or directory

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/user/anaconda3/bin/spyder", line 11, in <module>
    sys.exit(main())
  File "/home/user/anaconda3/lib/python3.8/site-packages/spyder/app/start.py", line 201, in main
    from spyder.app import mainwindow
  File "/home/user/anaconda3/lib/python3.8/site-packages/spyder/app/mainwindow.py", line 84, in <module>
    from qtpy import QtWebEngineWidgets  # analysis:ignore
  File "/home/user/anaconda3/lib/python3.8/site-packages/qtpy/QtWebEngineWidgets.py", line 26, in <module>
    from PyQt5.QtWebKitWidgets import QWebPage as QWebEnginePage
ModuleNotFoundError: No module named 'PyQt5.QtWebKitWidgets'

別のエラーが出た。ここでは最終行ではなく、4 行目のエラーが重要。「libXtst.so.6」が無いと言っているので、同様にして「libxtst6」を入れる。

(base) user@MYPC:~$ apt-file search libXtst.so.6
libxtst6: /usr/lib/x86_64-linux-gnu/libXtst.so.6
libxtst6: /usr/lib/x86_64-linux-gnu/libXtst.so.6.1.0

(base) user@MYPC:~$ sudo apt install --no-install-recommends libxtst6
<...snip...>

(base) user@MYPC:~$ spyder
Traceback (most recent call last):
  File "/home/user/anaconda3/lib/python3.8/site-packages/qtpy/QtWebEngineWidgets.py", line 22, in <module>
    from PyQt5.QtWebEngineWidgets import QWebEnginePage
ImportError: libXss.so.1: cannot open shared object file: No such file or directory

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/user/anaconda3/bin/spyder", line 11, in <module>
    sys.exit(main())
  File "/home/user/anaconda3/lib/python3.8/site-packages/spyder/app/start.py", line 201, in main
    from spyder.app import mainwindow
  File "/home/user/anaconda3/lib/python3.8/site-packages/spyder/app/mainwindow.py", line 84, in <module>
    from qtpy import QtWebEngineWidgets  # analysis:ignore
  File "/home/user/anaconda3/lib/python3.8/site-packages/qtpy/QtWebEngineWidgets.py", line 26, in <module>
    from PyQt5.QtWebKitWidgets import QWebPage as QWebEnginePage
ModuleNotFoundError: No module named 'PyQt5.QtWebKitWidgets'

しつこい。同様に「libxss1」を入れる。

(base) user@MYPC:~$ apt-file search libXss.so.1
libxss1: /usr/lib/x86_64-linux-gnu/libXss.so.1
libxss1: /usr/lib/x86_64-linux-gnu/libXss.so.1.0.0
libxss1-dbg: /usr/lib/debug/usr/lib/x86_64-linux-gnu/libXss.so.1.0.0

(base) user@MYPC:~$ sudo apt install --no-install-recommends libxss1
<...snip...>

(base) user@MYPC:~$ spyder
Update LANGUAGE_CODES (inside config/base.py) if a new translation has been added to Spyder
QStandardPaths: XDG_RUNTIME_DIR not set, defaulting to '/tmp/runtime-user'
libGL error: No matching fbConfigs or visuals found
libGL error: failed to load driver: swrast
libpng warning: bKGD: invalid
libpng warning: bKGD: invalid
libpng warning: bKGD: invalid
libpng warning: bKGD: invalid

ようやく起動した。「libGL error:」は、下記の環境変数を export すれば消すことができる。

export LIBGL_ALWAYS_INDIRECT=1

「libpng warning:」の方は、たぶん Spyder が使っている PNG 画像の方の問題で、無視しても問題ないはず。どうしても気になるなら、下記を参考にすれば消せるかもしれない。

2015-02-18

Python: Unbuffered read and write

今回の環境。少し古いのは、そんな環境でも動かす必要があるから。

# cat /etc/redhat-release
CentOS release 5.5 (Final)

# rpm -q python
python-2.4.3-27.el5

長年システムの仕事をしていると、例えば「出力にタイムスタンプを付けたい」とかいうことが一度くらいはあるはず。普段そんな時は Perl でサクッと作るのだが、今回は訳あって Python を使う必要があった。

この手のスクリプトで考えるべきは、バッファリング制御。Python にもこの手のパターンというのがあって、少しググれば stackoverflow とかに山ほどスレッドが見つかる。

timestamp.py:

#!/usr/bin/python

import fileinput
import os
import signal
import sys
import time

sys.stdin = os.fdopen(sys.stdin.fileno(), 'rb', 0)
sys.stdout = os.fdopen(sys.stdout.fileno(), 'wb', 0)
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

def main():
    for line in fileinput.input(sys.argv[1:]):
        sys.stdout.write(time.strftime('%F %T ') + line)
    return 0

if __name__ == '__main__':
    sys.exit(main())

上記で、fdopen の「おまじない」部分がそれに当たる。これで標準入出力のバッファリングを無効にしている。その下の signal 云々は、忌まわしき broken pipe を封じる呪文。これも Python 暗黒面の 1 つ。

と作ったスクリプトだが、

# ping -c 5 127.0.0.1 | python timestamp.py -
2015-02-18 18:41:42 PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
2015-02-18 18:41:42 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.016 ms
2015-02-18 18:41:42 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.014 ms
2015-02-18 18:41:42 64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.009 ms
2015-02-18 18:41:42 64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.009 ms
2015-02-18 18:41:42 64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.016 ms
2015-02-18 18:41:42 
2015-02-18 18:41:42 --- 127.0.0.1 ping statistics ---
2015-02-18 18:41:42 5 packets transmitted, 5 received, 0% packet loss, time 3999ms
2015-02-03 18:41:42 rtt min/avg/max/mdev = 0.009/0.012/0.016/0.005 ms

全然効いてなーい。めっちゃバッファリングしてる。

前述のおまじない以外にも、Python の起動オプション -u でバッファリングを無効にできるが、それでやっても結果は変わらず。訳が分からないので help & man を良く読んでみる。

# python -h

<...snip...>

-u     : unbuffered binary stdout and stderr (also PYTHONUNBUFFERED=x)
         see man page for details on internal buffering relating to '-u'

# man python

<...snip...>

       -u     Force stdin, stdout and stderr to  be  totally  unbuffered.   On
              systems  where  it matters, also put stdin, stdout and stderr in
              binary mode.  Note that there is internal  buffering  in  xread-
              lines(),  readlines()  and  file-object  iterators ("for line in
              sys.stdin") which is not influenced by  this  option.   To  work
              around  this, you will want to use "sys.stdin.readline()" inside
              a "while 1:" loop.

man の方に衝撃的なことが書いてある。「xreadlines, readlines, ファイルオブジェクトイテレーターは内部バッファリングするから、while + readline を使えよ」って、それってあんまりじゃね? readlines の方はともかく、イテレーターの方は裏切られた気持ちで一杯だ。

と文句を言っても仕方がないので、while + readline で書き直してみる。

def main():
    readline = fileinput.input(sys.argv[1:]).readline
    while True:
        line = readline()
        if not line:
            break
        sys.stdout.write(time.strftime('%F %T ') + line)
    return 0

しかし結果は全く変わらず。もしかして、と思い fileinput の実装を見てみると、

案の定、FileInput.readline で readlines を呼んでおり、ここでバッファリングされるらしい。Python3 でも改善は見られず。なんだよ、fileinput 使えねー。今までファイル入力にはずっと fileinput を使ってきたのに、今後は考え直した方が良さそうだ。

そんなこんなで、最終的には下記のようになった。iter(fh.readline, '') の意味は各自で調べて欲しい。私も今回初めてこの使い方を知った。

timestamp.py:

#!/usr/bin/python

import signal
import sys
import time

signal.signal(signal.SIGPIPE, signal.SIG_DFL)

def main():
    strftime = time.strftime
    write = sys.stdout.write
    for arg in sys.argv[1:]:
        fh = arg == '-' and sys.stdin or open(arg)
        try:
            for line in iter(fh.readline, ''):
                write(strftime('%F %T ') + line)
        finally:
            if fh is not sys.stdin:
                fh.close()
    return 0

if __name__ == '__main__':
    sys.exit(main())
# ping -c 5 127.0.0.1 | python timestamp.py -
2015-02-18 18:46:36 PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
2015-02-18 18:46:36 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.019 ms
2015-02-18 18:46:37 64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.012 ms
2015-02-18 18:46:38 64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.011 ms
2015-02-18 18:46:39 64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.009 ms
2015-02-18 18:46:40 64 bytes from 127.0.0.1: icmp_seq=5 ttl=64 time=0.009 ms
2015-02-18 18:46:40 
2015-02-18 18:46:40 --- 127.0.0.1 ping statistics ---
2015-02-18 18:46:40 5 packets transmitted, 5 received, 0% packet loss, time 3999ms
2015-02-03 18:46:40 rtt min/avg/max/mdev = 0.009/0.012/0.019/0.003 ms

fdopen のおまじない部分は、なくても動いたので削除した。公式ドキュメントによれば、標準入出力は「行バッファリング」が規定値らしいので、今回の用途ではこれで良さそうだ。

A negative bufsize means to use the system default, which is usually line buffered for tty devices and fully buffered for other files. If omitted, the system default is used.

結局のところ、

  • ファイルオブジェクトイテレーターが入力をバッファリングする
  • fileinput に至っては、Fileinput.readline でさえも入力をバッファリングする

というのが事の元凶だったらしい。特に fileinput、お前はこの手のリアルタイム入力には二度と使わねえ。

まさかこんな単純なスクリプトでここまでハマるとは思わなかった。これでまた 1 つ Python が嫌いになった。使えば使うほど嫌いになっていくのが Python、使うほど手に馴染むのが Perl。:-)


2016-07-18 追記

fileinput の方は、最近になって修正されたっぽい。たぶん 2.7.12 と 3.5.2 から。動作は未確認。

2014-03-31

Python: Portablize Sphinx

そろそろ、以前から気になっていた Sphinx を触ってみようかと。スタンドアロン版ソフトウェアをこよなく愛す私は、当然のようにスタンドアロン版を選択。(以降、スタンドアロン版インストーラー SphinxInstaller-1.2.20131210-py2.7-win32.zip を使用)

しかし使ってみて、コレジャナイ・・・orz。うん、私が悪かった。「スタンドアロン」ではなくて、「ポータブル」と言うべきだった。上記はスタンドアロンではあるが、ポータブルではない。具体的には、インストールしたディレクトリを別の場所に移動すると動かなくなる。これでは zip で固めてメンバーへ配布、という訳にはいかない。

調べると、bin/sphinx-*-script.py 中に絶対パスが記述されているのが原因のようだ。特に shebang 行がまずい。shebang は bin/sphinx-*.exe が bin/sphinx-*-script.py を実行するために使用するのだと思うが、shebang は絶対パスでないと動かない。

ここで諦めようとも思ったが、何か悔しいので「起動時に毎回 sphinx-*-script.py を書き換える」という力技に出てみた。

$SPHINX_HOME/sphinx.bat:

@echo off
setlocal

set _dp0=%~dp0
set SPHINX_HOME=%_dp0:~0,-1%
set _dp0=
set PYTHON_HOME=%SPHINX_HOME%\python
set PATH=%PATH%;%SPHINX_HOME%\bin;%PYTHON_HOME%;%PYTHON_HOME%\Scripts

python.exe "%SPHINX_HOME%\bin\setup-portable.py" -x >NUL
if errorlevel 1 (
  echo ERROR: failed to setup
  pause
  exit 1
)
title PortableSphinx
echo *** Welcome to PortableSphinx! ***
echo.
cmd.exe /k

$SPHINX_HOME/bin/setup-portable.py:

#!python.exe

__doc__ = """\
Usage:
  python %s -x
Description:
  Setup PortableSphinx.  This should be executed first whenever the
  Sphinx directory is moved on your PC.
""" % __file__

import filecmp
import glob
import os
import re
import shutil
import sys
import tempfile

_sphinx_home = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def _create_tempfile(lines=None):
    path = ''
    try:
        with tempfile.NamedTemporaryFile(delete=False) as fh:
            path = fh.name
            if lines:
                fh.writelines(lines)
        return path
    except StandardError:
        _remove_file(path)
        raise

def _remove_file(path):
    try:
        os.remove(path)
    except StandardError:
        pass

def _read_file(path):
    with open(path) as fh:
        return fh.readlines()

def _write_file(path, lines):
    tmp_path = _create_tempfile(lines)
    try:
        if not filecmp.cmp(tmp_path, path):
            shutil.copyfile(tmp_path, path)
    finally:
        _remove_file(tmp_path)

def _convert_script(lines):
    lines = iter(lines)
    for line in lines:
        yield '#!"%s"\n' % os.path.join(_sphinx_home, 'python', 'python.exe')
        break
    sphinx_home = _sphinx_home.replace('\\', '\\\\\\\\')
    re_library = re.compile(r"^(\s*').+?(?=\\\\(eggs|python)\\\\)", re.I)
    for line in lines:
        yield re_library.sub(r'\1' + sphinx_home, line)

def _setup_bin():
    script_glob = os.path.join(_sphinx_home, 'bin', 'sphinx-*-script.py')
    script_paths = glob.glob(script_glob)
    if not script_paths:
        raise RuntimeError('not found: %s' % script_glob)
    for script_path in script_paths:
        lines = _read_file(script_path)
        _write_file(script_path, _convert_script(lines))

def main():
    status = 1
    args = sys.argv[1:]
    if args and args[0] == '-x':
        _setup_bin()
        print 'OK'
        status = 0
    else:
        print __doc__,
    return status

if __name__ == '__main__':
    sys.exit(main())

スタンドアロン版 Sphinx のインストール先に上記を突っ込んで、毎回 sphinx.bat から起動するようにすれば、擬似 PortableSphinx の出来上がり。後はディレクトリを zip で固めればそのままプロジェクトメンバーに配布できる。今のところ、私が使う範囲で不具合は出ていない。(不具合がないとは言っていない)

さて、実際に Sphinx を使ってみると、確かにこれはなかなか良い。懸念していた日本語ファイル名(CP932)も問題ないし、印刷時の改頁も(組み込み機能にはないものの)制御可能。簡単なブロック図であれば Sphinx 上で生成できる。正に、至れり尽くせり。うん、きっとこれはハヤル。(実際にはずっと前から流行っている)

Sphinx の「ソースを編集 → make → 動作確認」という流れは、昨今のプログラマーには受け入れ難いだろうが、古き良き時代のプログラム開発みたいで老年プログラマーには逆に心地良い。私も最初はウキウキしながら make していたが、デザイン調整などで CSS を書き換える度に make するのは流石に面倒になってきた。人間って本当に我がままだと思う。

ということで、(誰もが考え付くであろう)変更を自動で検知して make してみる。

$SPHINX_HOME/bin/makebot.bat:

@echo off
setlocal

:loop
echo [%~n0] watching... press Ctrl-C to abort
python.exe "%~dp0notify-changed.py" .
if errorlevel 1 exit /b 1
echo [%~n0] detected
call make clean && call make html
echo.
goto loop

$SPHINX_HOME/bin/notify-changed.py:

#!python.exe

__doc__ = """\
Usage:
  python %s DIRECTORY
Description:
  Watch and wait for changes in a DIRECTORY and ends with status 0.
""" % __file__

import fnmatch
import os
import sys
import time

_include_patterns = ('*.conf', '*.css', '*.css_t', '*.html', '*.py', '*.rst')
_exclude_patterns = ('_*',)

def _fnmatch_any(name, patterns):
    for pattern in patterns:
        if fnmatch.fnmatch(name, pattern):
            return True
    return False

def _filter_name(names, includes=None, excludes=None):
    if includes:
        names = [x for x in names if _fnmatch_any(x, includes)]
    if excludes:
        names = [x for x in names if not _fnmatch_any(x, excludes)]
    return names

def _filter_dir(names):
    return _filter_name(names, excludes=_exclude_patterns)

def _filter_file(names):
    return _filter_name(names,
                        includes=_include_patterns,
                        excludes=_exclude_patterns)

def _find_files(path):
    for dirpath, dirnames, filenames in os.walk(path):
        dirnames[:] = _filter_dir(dirnames)
        for filename in _filter_file(filenames):
            yield os.path.join(dirpath, filename)

def _iter_filestats(path):
    for filepath in _find_files(path):
        yield filepath, int(os.stat(filepath).st_mtime)

def _diff_filestats(before, after):
    for filepath, mtime in after.iteritems():
        if mtime != before.get(filepath, 0):
            return True
    return False

def _watch_directory(path, interval=1):
    if not os.path.isdir(path):
        return "ERROR: directory not found: `%s'" % path
    before = dict(_iter_filestats(path))
    while True:
        time.sleep(interval)
        after = dict(_iter_filestats(path))
        if _diff_filestats(before, after):
            break
        before = after
    return 0

def main():
    status = 1
    args = sys.argv[1:]
    if args:
        try:
            status = _watch_directory(args[0])
        except KeyboardInterrupt:
            pass
    else:
        print __doc__,
    return status

if __name__ == '__main__':
    sys.exit(main())

pywin32 辺りを使えばファイル変更を検知する API が使えたりするのかも知れないが、生憎 Sphinx には pywin32 が添付されていないので、力技で実装。数千くらいのファイルなら問題ないはず(数万とかは知らない)。後は Ctrl-C で終了すると「バッチ ジョブを終了しますか (Y/N)?」とか聞かれるのがウザいが、これは Windows BAT のクソ仕様の問題だし、基本 makebot しっ放しなので我慢する。

取りあえず、これで私の Sphinx on Windows 環境は幸せになった。

2013-10-31

StandardError hides KeyboardInterrupt in Python 2.4

私は、Python で次のようなコードを書くことがある。

import datetime

def validate_date_string(s):
    try:
        assert len(s) == 8
        datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))
        return True
    except StandardError:
        return False

def test():
    assert validate_date_string('20120229') # leap year
    assert not validate_date_string('20130229') # not leap year

StandardError を捕捉するのは、Google 先生の教えによる。

  • Never use catch-all except: statements, or catch Exception or StandardError, unless you are re-raising the exception or in the outermost block in your thread (and printing an error message). Python is very tolerant in this regard and except: will really catch everything including misspelled names, sys.exit() calls, Ctrl+C interrupts, unittest failures and all kinds of other exceptions that you simply don't want to catch.

しかしここには、Python のバージョンによって例外のクラス階層が異なる、という重要な情報が抜けている。

実際、Python 2.4 以前は KeyboardInterruput が StarndardError のサブクラスになっているため、上記のコードは try ブロック中で Ctrl-C を受けると意図通りに動かない。具体的には、Ctrl-C で終了できず、無条件に False が返ってしまう。

Exception
 +-- SystemExit
 +-- StopIteration
 +-- StandardError
 |    +-- KeyboardInterrupt
<...snip...>
BaseException
 +-- SystemExit
 +-- KeyboardInterrupt
 +-- Exception
      +-- GeneratorExit
      +-- StopIteration
      +-- StandardError
<...snip...>

Python 2.4 以前でも動くようにするには、StandardError を処理する に、KeyboardInterruput を明示的に処理する必要がある。

def validate_date_string(s):
    try:
        assert len(s) == 8
        datetime.date(int(s[:4]), int(s[4:6]), int(s[6:]))
        return True
    except KeyboardInterrupt:
        raise
    except StandardError:
        return False

Python は保守的な言語だと思っていたので、まさかこんな所に罠があるとは思わなかった。これでまた 1 つ、Python が嫌いになった。:-p

2012-12-16

Python: cElementTree returns non open object

xml.etree.cElementTree は、xml.etree.ElementTree の C 実装だ。

ElementTree クラスはエレメントの構造を包み込み、それと XML を行き来するのに使えます。

この API の C 実装である xml.etree.cElementTree も使用可能です。

C 実装であること以外、ElementTree と cElementTree の違いについては述べられていない。違いがないなら、C 実装の方を使わない手はない。しかし、

$ python
Python 2.6.6 (r266:84292, Sep 12 2011, 14:03:14)
[GCC 4.4.5 20110214 (Red Hat 4.4.5-6)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import xml.etree.ElementTree
>>> html = '<html></html>'
>>> tree = xml.etree.ElementTree.fromstring(html)
>>> tree.html = html
>>> tree.html
'<html></html>'
$ python
Python 2.6.6 (r266:84292, Sep 12 2011, 14:03:14)
[GCC 4.4.5 20110214 (Red Hat 4.4.5-6)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import xml.etree.cElementTree
>>> html = '<html></html>'
>>> tree = xml.etree.cElementTree.fromstring(html)
>>> tree.html = html
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: html

cElementTree が返すオブジェクトはオープン・オブジェクトではないらしく、属性を追加することができない。C 実装されたモジュールとはそういうもんか、とも思ったが、

$ python
Python 2.6.6 (r266:84292, Sep 12 2011, 14:03:14)
[GCC 4.4.5 20110214 (Red Hat 4.4.5-6)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import cProfile
>>> p = cProfile.Profile()
>>> p.foo = 'foo'
>>> p.foo
'foo'

同じ C 実装モジュールの cProfile では問題ない。想像するに、ElementTree の場合、木構造で大量のオブジェクトを使用するためメモリをケチってるのかも知れない。

ともかく、RHEL5 + elementtree で作ったスクリプトを RHEL6 で動そうと喜んで cElementTree を使った私にとっては、この上ないぬか喜びだった。

2012-11-21

psycopg2: Can't mogrify `None' in a tuple to `NULL'

RHEL6 付属の psycopg2 で、

# rpm -qa | grep psycopg
python-psycopg2-2.0.13-2.el6_1.1.x86_64

tuple 中にある None を mogrify するとエラーになる。

test1.py:

#!/usr/bin/python

import psycopg2.extensions

def main():
    conn = psycopg2.connect('dbname=testdb user=testuser')
    cur = conn.cursor()
    for param in [0, [0], (0,), '', [''], ('',), None, [None], (None,)]:
        print '%s\t->' % repr(param), cur.mogrify('%s', [param])

if __name__ == '__main__':
    main()
$ python test1.py
0       -> 0
[0]     -> ARRAY[0]
(0,)    -> (0)
''      -> E''
['']    -> ARRAY[E'']
('',)   -> (E'')
None    -> NULL
[None]  -> ARRAY[NULL]
(None,) ->
Traceback (most recent call last):
  File "/root/tmp/test1.py", line 12, in <module>
    main()
  File "/root/tmp/test1.py", line 9, in main
    print '%s\t->' % repr(param), cur.mogrify('%s', [param])
  File "/usr/lib64/python2.6/site-packages/psycopg2/extensions.py", line 100, in getquoted
    qobjs = [str(o.getquoted()) for o in pobjs]
AttributeError: 'str' object has no attribute 'getquoted'

落ちている場所は、

/usr/lib64/python2.6/site-packages/psycopg2/extensions.py:

# The SQL_IN class is the official adapter for tuples starting from 2.0.6.
class SQL_IN(object):
    """Adapt any iterable to an SQL quotable object."""

    def __init__(self, seq):
        self._seq = seq

    def prepare(self, conn):
        self._conn = conn

    def getquoted(self):
        # this is the important line: note how every object in the
        # list is adapted and then how getquoted() is called on it
        pobjs = [adapt(o) for o in self._seq]
        for obj in pobjs:
            if hasattr(obj, 'prepare'):
                obj.prepare(self._conn)
        qobjs = [str(o.getquoted()) for o in pobjs]
        return '(' + ', '.join(qobjs) + ')'

    __str__ = getquoted

register_adapter(tuple, SQL_IN)

print デバッグすると直ぐに分かるが、このとき o には 'NULL' という文字列(str 型)が入っている。当然、単なる文字列に getquoted なんてメソッドがあるはずがない。そりゃ落ちるわ。

何がどうあるべきなのかは分からないが、「動かないシステムに価値はない」の持論に従い、ともかく動くようにする。

test2.py:

#!/usr/bin/python

import psycopg2.extensions

# Fix a bug that can't mogrify `None' in a tuple to `NULL'
class FIXED_SQL_IN(object):
    def __init__(self, seq):
        self._seq = seq

    def prepare(self, conn):
        self._conn = conn

    def getquoted(self):
        adapt = psycopg2.extensions.adapt
        pobjs = [adapt(o) for o in self._seq]
        for obj in pobjs:
            if hasattr(obj, 'prepare'):
                obj.prepare(self._conn)
        qobjs = [getattr(o, 'getquoted', o.__str__)() for o in pobjs]
        return '(' + ', '.join(qobjs) + ')'

    __str__ = getquoted

psycopg2.extensions.register_adapter(tuple, FIXED_SQL_IN)

def main():
    conn = psycopg2.connect('dbname=testdb user=testuser')
    cur = conn.cursor()
    for param in [0, [0], (0,), '', [''], ('',), None, [None], (None,)]:
        print '%s\t->' % repr(param), cur.mogrify('%s', [param])

if __name__ == '__main__':
    main()
$ python test2.py
0       -> 0
[0]     -> ARRAY[0]
(0,)    -> (0)
''      -> E''
['']    -> ARRAY[E'']
('',)   -> (E'')
None    -> NULL
[None]  -> ARRAY[NULL]
(None,) -> (NULL)

ということを、実は 2 年くらい前に RHEL5 + psycopg2-2.0.12 (from rpmforge)という組み合わせでやっていたのだが、最近になって検索すると次が出てきた。

本件は psycopg2-2.4 で直っている模様。しかし私が試した限り、この変更部分を 2.0 系に持ってきても動かない。None に対してアダプターを登録しても、全く動く気配がない。None に対するアダプターが動くようになる変更が 2.4 までの何処かに入ったのかも知れないが、私的にはもう上述の方法で解決しているのでこれ以上は調べる気なし。