Python自作パッケージのunittestをそれっぽいディレクトリ構成で実施する際にModuleNotFoundErrorを回避する


はじめに

  • Pythonパッケージ(Pypiで公開)を作成しようとしていて
  • かつそれっぽいPythonディレクトリ構成でリリースを検討した際にハマった点を備忘として記録
  • あとはIDE(Pycharm)様々という感謝の気持ち

こんなエラー・事象にハマった

パッケージ構成

  • 再現可能なテストを実行するために、かつ公開パッケージとして適切なディレクトリ構造にするために作成するパッケージのディレクトリとテストディレクトリは分類し、以下のようなパッケージ構成にしています。

    • 偉い人がそう言っている
    • 世の素晴らしい公開パッケージの構成もそうなっているため
  • パッケージ構成

Macintosh:$ tree
Project
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.rst
├── docs
│   ├── Makefile
│   ├── conf.py
│   ├── index.rst
│   └── make.bat
├── setup.py
├── my_package_name
│   ├── __init__.py
│   ├── my_package_core.py
│   └── my_package_helper.py
└── tests
    ├── __init__.py
    ├── test_my_package_helper.py
    └── test_my_package_core.py

いざテスト

  • testsディレクトリ配下のtest_my_package_core.pyのテストを実装(my_package_nameのmy_package_core.pyのあるクラスのみをimport)し、ターミナルからテスト実行しようとしたところ、エラーが発生した。
test_my_package_core.py
import os
import traceback
import unittest

from my_package_name.my_package_core import PackageClass

class TestPage(unittest.TestCase):

()

if __name__ == '__main__':
    unittest.main()
  • テスト実行すると ModuleNotFoundError
$ python tests/test_my_package_core.py
Traceback (most recent call last):
  File "my_package_core.py", line 5, in <module>
    from my_package_name.my_package_core import PackageClass
ModuleNotFoundError: No module named 'my_package_name'
  • IDEで作成したProjectのパスはsys.pathに登録されるのでは?と確認してみる
(project) Macintosh:$ python
Python 3.5.2 (default, Dec 30 2016, 02:25:25) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> print(sys.path)
['', '/Users/name/.pyenv/versions/3.5.2/lib/python35.zip', '/Users/name/.pyenv/versions/3.5.2/lib/python3.5', '/Users/name/.pyenv/versions/3.5.2/lib/python3.5/plat-darwin', '/Users/name/.pyenv/versions/3.5.2/lib/python3.5/lib-dynload', '/Users/name/project/lib/python3.5/site-packages', '/Users/name/project/lib/python3.5/site-packages/setuptools-39.1.0-py3.5.egg', '/Users/name/project/lib/python3.5/site-packages/pip-10.0.1-py3.5.egg']
  • 登録されていないことが分かった。
  • それは失敗するよね、と思いつつあれ確かPycharmのテスト実行機能では問題なかった気が・・・と試してみると
    • 成功してしまった

Why?

Pycharmコンソールで実行する場合

  • Pycharm下部のこのボタン
/Users/name/project/bin/python "/Applications/PyCharm CE.app/Contents/helpers/pydev/pydevconsole.py" 51470 51471

import sys; print('Python %s on %s' % (sys.version, sys.platform))
sys.path.extend(['/Users/name/PycharmProjects/project'])
PyDev console: starting.
Python 3.5.2 (default, Dec 30 2016, 02:25:25) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
  • Pythonコンソールの初期化処理でsys.path.extend(['/Users/name/PycharmProjects/project'])を実行してくれていることが分かった。

Pycharmのテスト機能で実行する場合

$ Launching unittests with arguments python -m unittest test_package_core.TestPage in /Users/name/PycharmProjects/project/tests
  • なるほど -m など様々な点で配慮していただいているようで。

IDEサマサマ

  • Pycharmが意識せず吸収してくれている点が多々ある
  • Pythonコンソール起動時の作成したプロジェクトフォルダをsys.pathに追加する処理や
  • Pycharmのユニットテスト実行機能が python -m を付与し、ModuleNotFoundを回避してくれている

とはいえ、分かったからにはterminalからだろうが同じ状態に出来ることを目指したい

testcase側でProjectパスを追加することで対応

test_my_package_core.py
import os
import sys
import traceback
import unittest

# 以下1lineを追加
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))

from my_package_name.my_package_core import PackageClass

class TestPage(unittest.TestCase):

()

if __name__ == '__main__':
    unittest.main()
$ python tests/test_my_package_core.py

Success

以上