やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

Pythonのインポートでハマった(token.py 名前重複)

 毎回ハマる。マジで改修してほしいクソ機能。

成果物

背景

 自作スクリプトtoken.pyを書いた。アクセストークンをファイルから取得するやつだ。しかし名前重複がおきて自作スクリプトの要素がインポートできなかった。標準ライブラリ(/usr/lib/python3.7/token.py)と重複したのである。

  • dir/
    • src/
      • token.py
    • test/
      • test_token.py

token.py

class Token: pass

test_token.py

# --------------------------------------------------
# ここでテスト対象の自作token.pyをインポートしたい!
# --------------------------------------------------
import unittest
if __name__ == "__main__":
    unittest.main()

実行

 絶対パス相対パスにかかわらずどこからでも実行できるようでありたい。テストからも呼び出したい。

/.../token.py
./token.py
/.../test_token.py
./test_token.py

 どうやってインポートすればよいか?

正解

test_token.py

import os, sys, pathlib
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
print(sys.path)
import src # OK
from src import token # OK
from src.token import Token #OK

 ちなみにファイルパスはpathlibを使って表現することもできる。だが、字数が大幅に短くなるわけでもない。むしろstrなんて余計なメソッドを噛ませないとダメ。Python3.4以降でないと使えない。相対パスの指定方法を覚えていないとダメ。結局__file__を渡す。など色々とクソ。

sys.path.append(str(pathlib.Path(__file__ + '/../..').resolve()))
sys.path.append(str(pathlib.Path(__file__).parent.parent.resolve()))
sys.path.append(os.path.dirname(os.path.dirname(__file__)))

 どちらがスマートか? 答え:どちらもクソ

 私としては以下のように書きたかった。

sys.path.append(Here.Parent.Parent)
sys.path.append(Here.parent(2))

 言うまでもなく、Pythonではこんなふうに書けない。

 というか、そもそもこんなコード書きたくない。ファイル名で察してほしい。test_token.pyと書いたらtoken.pyを勝手にインポートしてほしい。むしろtest/ディレクトリ配下はどんな名前であっても全部テストとして認識してほしい。

 pytestとか使ったほうがいいのだろう。でも標準じゃないライブラリはできるだけ減らしたい。

失敗

test_token.py

import token # コレジャナイ! 標準ライブラリ(/usr/lib/python3.7/token.py)がインポートされてしまう!

 自作のtoken.pyで定義したTokenクラスをインポートしたかったが失敗。

from token import Token # ImportError: cannot import name 'Token' from 'token' (/usr/lib/python3.7/token.py)

 パスの位置が間違っていたので修正したつもり。

import ..src.token # SyntaxError: invalid syntax

 from相対パスを書いてimportでクラスなど要素を書いたつもり。

from .. import src.token # SyntaxError: invalid syntax

 from相対パスを書いてimportでクラスなど要素を書いたつもり2。

from ..src import token # ValueError: attempted relative import beyond top-level package

 ..は使えない。test_token.pyを実行したらその親ディレクトリがルートになる。そしてルートより上は辿れないようだ。よって、実行ファイルでは..が使えない。クソ仕様乙。

 なので発想の転換。パスに親ディレクトリを追加してみる。

# 親ディレクトリをパスに追加する(相対パス)
import os, sys, pathlib
sys.path.append('../')
print(sys.path)

 以降、事前に上記のパス設定があるものとする。

import src # ModuleNotFoundError: No module named 'src'
import src.token # ModuleNotFoundError: No module named 'src'
from src import token # ModuleNotFoundError: No module named 'src'
from . import src.token # SyntaxError: invalid syntax
from .src import token # ModuleNotFoundError: No module named '__main__.src'; '__main__' is not a package



test.py

#!/usr/bin/env python3
# coding: utf8
import os, sys
from unittest import TestLoader
from unittest import TextTestRunner
if __name__ == '__main__':
    TextTestRunner().run(TestLoader().discover(os.path.abspath(os.path.dirname(__file__))))

 このファイルと同じ階層にあるすべてのtest*.pyファイルを実行する。

 ようするに全テストを実行する。

 今回は1ファイルだけなのでべつにいらない。

所感

 

対象環境

$ uname -a
Linux raspberrypi 5.10.52-v7l+ #1441 SMP Tue Aug 3 18:11:56 BST 2021 armv7l GNU/Linux