detox が依存性の解決に失敗してたまにコケる話

書こう、書こうとおもってさぼってた話を書きます。

複数Python バージョンをサポートするライブラリを作っていると、tox があるとテストがとても楽になります。
ただし、Python のバージョンによって依存するライブラリが変化する場合は、
setup.py と tox.ini の両方にその記述を書く必要があります。

例えば、python2.6 では unittest2 を使い、python2.7 以降では unittest2 を使わない場合は次のようになります。

# setup.py

# -*- coding: utf-8 -*-
test_requires = ['mock']
if sys.version_info < (2, 7):
    test_requires.append('unittest2')

setup(
    name='example',
    extras_require=dict(
        testing=test_requires,
    ),
)
; tox.ini

[tox]
envlist=py26,py27,py32,py33

[testenv]
deps=
    mock

[testenv:py26]
deps=
    mock
    unittest2

python2.6 用の定義を setup.py と tox.ini の両方に書かなくてはならないのでちょっと厄介ですね。

作っているものによっては、この依存パッケージの定義が複雑怪奇になってしまうことがあり、
書き換えるのがめんどうになります。
たとえば、拙作の diff-highlight はとても読みづらいことになっています。

tox で extra_requires を参照してみた

両方に定義を書くのがイヤだったので、tox で extra_require の定義を参照するようにしてみました。

[tox]
envlist=py26,py27,py32,py33

[testenv]
deps=
    mock
commands=
    pip install -e .[testing]

pip の -e オプションで extra_require を指定することができるので、
このやり方であれば tox.ini に同じことを書かずに済むようになります。

実際に tox を実行して試してみると、正しく動きます。

detox に罠があった

うまくいきました、めでたしめでたし、と終わりにしたかったのですがそうも行きませんでした。

detox コマンドで tox を並列実行するようにすると、このやり方は簡単に崩壊します。
なんどか detox を呼ぶと、数回に一回、実行が失敗します。

$ detox
GLOB sdist-make: /Users/tkomiya/work/experimental/tox-test/setup.py
py33 inst-nodeps: /Users/tkomiya/work/experimental/tox-test/.tox/dist/example-0.0.0.zip
py27 inst-nodeps: /Users/tkomiya/work/experimental/tox-test/.tox/dist/example-0.0.0.zip
py32 inst-nodeps: /Users/tkomiya/work/experimental/tox-test/.tox/dist/example-0.0.0.zip
py26 inst-nodeps: /Users/tkomiya/work/experimental/tox-test/.tox/dist/example-0.0.0.zip
py27 runtests: PYTHONHASHSEED='248044164'
py27 runtests: commands[0] | pip install -e .[testing]
py26 runtests: PYTHONHASHSEED='248044164'
py26 runtests: commands[0] | pip install -e .[testing]
py32 runtests: PYTHONHASHSEED='248044164'
py32 runtests: commands[0] | pip install -e .[testing]
py33 runtests: PYTHONHASHSEED='248044164'
py33 runtests: commands[0] | pip install -e .[testing]
ERROR: invocation failed, logfile: /Users/tkomiya/work/experimental/tox-test/.tox/py32/log/py32-20.log
ERROR: actionid=py32
msg=runtests
cmdargs=[local('/Users/tkomiya/work/experimental/tox-test/.tox/py32/bin/pip'), 'install', '-e', '.[testing]']
env={'DESTINATION': '/var/folders/2s/79pf79d94r9gxhzl4kpztrr00000gp/T/iTerm 1.0.0.20140112 Update', 'PYENV_DIR': '/Users/tkomiya/work/experimental/tox-test', 'LOGNAME': 'tkomiya', 'USER': 'tkomiya', 'PATH': '/Users/tkomiya/work/experimental/tox-test/.tox/py32/bin:/Users/tkomiya/work/experimental/tox-test/.tox/py27/bin:/Users/tkomiya/work/experimental/tox-test/.tox/py33/bin:/Users/tkomiya/.pyenv/versions/2.7.6/bin:/Users/tkomiya/.pyenv/libexec:/Users/tkomiya/.pyenv/plugins/python-build/bin:/Users/tkomiya/.pyenv/plugins/python-virtualenv/bin:/Users/tkomiya/.plenv/shims:/Users/tkomiya/.plenv/bin:/Users/tkomiya/.pyenv/shims:/Users/tkomiya/.pyenv/bin:/Users/tkomiya/.rbenv/shims:/Users/tkomiya/.rbenv/bin:/Users/tkomiya/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin/X11:/usr/games', 'HOME': '/Users/tkomiya', 'TERM_PROGRAM': 'iTerm.app', 'LANG': 'ja_JP.UTF-8', 'TERM': 'xterm-256color', 'SHELL': '/bin/zsh', 'COLORFGBG': '7;0', 'PYENV_SHELL': 'zsh', 'SHLVL': '1', 'SECURITYSESSIONID': '186a4', 'ITERM_SESSION_ID': 'w0t3p0', 'RBENV_SHELL': 'zsh', 'PYENV_VERSION': '2.7.6:2.5.6:2.6.9:3.2.5:3.3.5:3.4.0', 'PYTHONHASHSEED': '248044164', 'EDITOR': 'vi', 'GPG_AGENT_INFO': '/Users/tkomiya/.gnupg/S.gpg-agent:278:1', 'RPROMPT': '%F{green}%~%f', 'PROMPT': '%B%(?..[%?] )%b%n@%U%m%u> ', 'SSH_AUTH_SOCK': '/tmp/launch-EfapyJ/Listeners', 'Apple_PubSub_Socket_Render': '/tmp/launch-viag1O/Render', 'ITERM_PROFILE': 'Default', 'LC_ALL': 'ja_JP.UTF-8', 'TMPDIR': '/var/folders/2s/79pf79d94r9gxhzl4kpztrr00000gp/T/', 'PYENV_HOOK_PATH': ':/Users/tkomiya/.pyenv/pyenv.d:/usr/local/etc/pyenv.d:/etc/pyenv.d:/usr/lib/pyenv/hooks', 'PYENV_ROOT': '/Users/tkomiya/.pyenv', '__CF_USER_TEXT_ENCODING': '0x1F6:1:14', 'PWD': '/Users/tkomiya/work/experimental/tox-test', '__CHECKFIX1436934': '1', 'COMMAND_MODE': 'unix2003'}
Obtaining file:///Users/tkomiya/work/experimental/tox-test
  Running setup.py (path:/Users/tkomiya/work/experimental/tox-test/setup.py) egg_info for package from file:///Users/tkomiya/work/experimental/tox-test

  Installing extra requirements: 'testing'
Requirement already satisfied (use --upgrade to upgrade): mock in ./.tox/py32/lib/python3.2/site-packages (from example==0.0.0)
Downloading/unpacking unittest2 (from example==0.0.0)
  Running setup.py (path:/Users/tkomiya/work/experimental/tox-test/.tox/py32/build/unittest2/setup.py) egg_info for package unittest2
    Traceback (most recent call last):
      File "<string>", line 17, in <module>
      File "/Users/tkomiya/work/experimental/tox-test/.tox/py32/build/unittest2/setup.py", line 12, in <module>
        from unittest2 import __version__ as VERSION
      File "unittest2/__init__.py", line 40, in <module>
        from unittest2.collector import collector
      File "unittest2/collector.py", line 3, in <module>
        from unittest2.loader import defaultTestLoader
      File "unittest2/loader.py", line 92
        except Exception, e:
                        ^
    SyntaxError: invalid syntax
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):

  File "<string>", line 17, in <module>

  File "/Users/tkomiya/work/experimental/tox-test/.tox/py32/build/unittest2/setup.py", line 12, in <module>

    from unittest2 import __version__ as VERSION

  File "unittest2/__init__.py", line 40, in <module>

    from unittest2.collector import collector

  File "unittest2/collector.py", line 3, in <module>

    from unittest2.loader import defaultTestLoader

  File "unittest2/loader.py", line 92

    except Exception, e:

                    ^

SyntaxError: invalid syntax

----------------------------------------
Cleaning up...
Command python setup.py egg_info failed with error code 1 in /Users/tkomiya/work/experimental/tox-test/.tox/py32/build/unittest2
Storing debug log for failure in /Users/tkomiya/.pip/pip.log

ERROR: InvocationError: /Users/tkomiya/work/experimental/tox-test/.tox/py32/bin/pip install -e .[testing] (see /Users/tkomiya/work/experimental/tox-test/.tox/py32/log/py32-20.log)
ERROR: invocation failed, logfile: /Users/tkomiya/work/experimental/tox-test/.tox/py33/log/py33-20.log
ERROR: actionid=py33
msg=runtests
cmdargs=[local('/Users/tkomiya/work/experimental/tox-test/.tox/py33/bin/pip'), 'install', '-e', '.[testing]']
env={'DESTINATION': '/var/folders/2s/79pf79d94r9gxhzl4kpztrr00000gp/T/iTerm 1.0.0.20140112 Update', 'PYENV_DIR': '/Users/tkomiya/work/experimental/tox-test', 'LOGNAME': 'tkomiya', 'USER': 'tkomiya', 'PATH': '/Users/tkomiya/work/experimental/tox-test/.tox/py33/bin:/Users/tkomiya/.pyenv/versions/2.7.6/bin:/Users/tkomiya/.pyenv/libexec:/Users/tkomiya/.pyenv/plugins/python-build/bin:/Users/tkomiya/.pyenv/plugins/python-virtualenv/bin:/Users/tkomiya/.plenv/shims:/Users/tkomiya/.plenv/bin:/Users/tkomiya/.pyenv/shims:/Users/tkomiya/.pyenv/bin:/Users/tkomiya/.rbenv/shims:/Users/tkomiya/.rbenv/bin:/Users/tkomiya/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin/X11:/usr/games', 'HOME': '/Users/tkomiya', 'TERM_PROGRAM': 'iTerm.app', 'LANG': 'ja_JP.UTF-8', 'TERM': 'xterm-256color', 'SHELL': '/bin/zsh', 'COLORFGBG': '7;0', 'PYENV_SHELL': 'zsh', 'SHLVL': '1', 'SECURITYSESSIONID': '186a4', 'ITERM_SESSION_ID': 'w0t3p0', 'RBENV_SHELL': 'zsh', 'PYENV_VERSION': '2.7.6:2.5.6:2.6.9:3.2.5:3.3.5:3.4.0', 'PYTHONHASHSEED': '248044164', 'EDITOR': 'vi', 'GPG_AGENT_INFO': '/Users/tkomiya/.gnupg/S.gpg-agent:278:1', 'RPROMPT': '%F{green}%~%f', 'PROMPT': '%B%(?..[%?] )%b%n@%U%m%u> ', 'SSH_AUTH_SOCK': '/tmp/launch-EfapyJ/Listeners', 'Apple_PubSub_Socket_Render': '/tmp/launch-viag1O/Render', 'ITERM_PROFILE': 'Default', 'LC_ALL': 'ja_JP.UTF-8', 'TMPDIR': '/var/folders/2s/79pf79d94r9gxhzl4kpztrr00000gp/T/', 'PYENV_HOOK_PATH': ':/Users/tkomiya/.pyenv/pyenv.d:/usr/local/etc/pyenv.d:/etc/pyenv.d:/usr/lib/pyenv/hooks', 'PYENV_ROOT': '/Users/tkomiya/.pyenv', '__CF_USER_TEXT_ENCODING': '0x1F6:1:14', 'PWD': '/Users/tkomiya/work/experimental/tox-test', '__CHECKFIX1436934': '1', 'COMMAND_MODE': 'unix2003'}
Obtaining file:///Users/tkomiya/work/experimental/tox-test
  Running setup.py (path:/Users/tkomiya/work/experimental/tox-test/setup.py) egg_info for package from file:///Users/tkomiya/work/experimental/tox-test

  Installing extra requirements: 'testing'
Requirement already satisfied (use --upgrade to upgrade): mock in ./.tox/py33/lib/python3.3/site-packages (from example==0.0.0)
Downloading/unpacking unittest2 (from example==0.0.0)
  Running setup.py (path:/Users/tkomiya/work/experimental/tox-test/.tox/py33/build/unittest2/setup.py) egg_info for package unittest2
    Traceback (most recent call last):
      File "<string>", line 17, in <module>
      File "/Users/tkomiya/work/experimental/tox-test/.tox/py33/build/unittest2/setup.py", line 12, in <module>
        from unittest2 import __version__ as VERSION
      File "./unittest2/__init__.py", line 40, in <module>
        from unittest2.collector import collector
      File "./unittest2/collector.py", line 3, in <module>
        from unittest2.loader import defaultTestLoader
      File "./unittest2/loader.py", line 92
        except Exception, e:
                        ^
    SyntaxError: invalid syntax
    Complete output from command python setup.py egg_info:
    Traceback (most recent call last):

  File "<string>", line 17, in <module>

  File "/Users/tkomiya/work/experimental/tox-test/.tox/py33/build/unittest2/setup.py", line 12, in <module>

    from unittest2 import __version__ as VERSION

  File "./unittest2/__init__.py", line 40, in <module>

    from unittest2.collector import collector

  File "./unittest2/collector.py", line 3, in <module>

    from unittest2.loader import defaultTestLoader

  File "./unittest2/loader.py", line 92

    except Exception, e:

                    ^

SyntaxError: invalid syntax

----------------------------------------
Cleaning up...
Command python setup.py egg_info failed with error code 1 in /Users/tkomiya/work/experimental/tox-test/.tox/py33/build/unittest2
Storing debug log for failure in /Users/tkomiya/.pip/pip.log

ERROR: InvocationError: /Users/tkomiya/work/experimental/tox-test/.tox/py33/bin/pip install -e .[testing] (see /Users/tkomiya/work/experimental/tox-test/.tox/py33/log/py33-20.log)
______________________________________________________________ summary _______________________________________________________________
  py26: commands succeeded
  py27: commands succeeded
ERROR:   py32: commands failed
ERROR:   py33: commands failed

ログを読むと、本来 python3.x では依存していないはずのパッケージ(unittest2)を python3.x にインストールしようとしているために失敗しています。


調べていくと、detox によって "pip install -e .[testing]" が並列実行される際に、
*.egg-info/requires.txt が上書きしあって、実行するタイミングや順序によって、
本来「python2.6 で適用されるべき依存関係」が「python3.x に適用されてしまう」という現象が起きていることが分かりました。

まとめ

ダブっててつらいけど setup.cfg と tox.ini に依存関係を書くことにしました。