はじめに
ごみけついきたい、nikkieです。
Ryeを使った開発をきっかけに、mypyのドキュメントにあたりました。
mypyにディレクトリのパスを渡した時にどう動いているか、少し理解が深まった感覚です1。
目次
- はじめに
- 目次
- Ryeのworkspace
- リポジトリルートからmypyを流したい
- 結論:Duplicate module namedを解決するには
- 「Mapping file paths to modules」(mypyがファイルとモジュールの対応を付けるやり方)
- ケース3の--explicit-package-basesがRyeのworkspaceに効く理由
- P.S. 名前空間パッケージ
- 変更履歴
Ryeのworkspace
https://rye-up.com/guide/workspaces/
Ryeにはworkspaceという概念があります。
複数のパッケージを開発しているときに選択肢に入ると認識しています。
Afterwards all projects within that workspace share a singular virtualenv.
workspaceとして設定すると、Ryeは1つの仮想環境で開発環境を管理してくれます2。
workspaceの例
百聞は一見に如かず!
. # rye-workspace-example ディレクトリ
├── .venv
├── pyproject.toml
├── src/
│ └── rye_workspace_example/
│ └── __init__.py
├── awesome/
│ ├── pyproject.toml
│ ├── src/
│ │ └── awesome/
│ │ └── __init__.py
│ └── tests/
│ ├── __init__.py
│ └── test_awesome.py
└── fabulous/
├── pyproject.toml
├── src/
│ └── fabulous/
│ └── __init__.py
└── tests/
├── __init__.py
└── test_fabulous.py
モノレポでawesomeパッケージとfabulousパッケージを同時に開発しています。
それぞれディレクトリを作ってrye initしました。
リポジトリルートのpyproject.tomlの設定
[tool.rye.workspace] members = ["awesome", "fabulous"]
rye syncを叩くと、リポジトリルートの.venvにawesomeもfabulousも全部インストールされます(editable installです)
requirements.lock
-e file:awesome -e file:fabulous -e file:.
インストールされたことの確認
(rye initでできたpyproject.tomlのscriptのところを少しいじっています)
% rye run hello Hello from rye-workspace-example! % rye run ahello Hello from awesome! % rye run fhello Hello from fabulous!
リポジトリルートからmypyを流したい
このworkspaceで、リポジトリルートからmypy .と実行したいです3。
単に実行するとエラーが送出されます。
% rye run mypy . fabulous/tests/__init__.py: error: Duplicate module named "tests" (also at "./awesome/tests/__init__.py") fabulous/tests/__init__.py: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules for more info fabulous/tests/__init__.py: note: Common resolutions include: a) using `--exclude` to avoid checking one of them, b) adding `__init__.py` somewhere, c) using `--explicit-package-bases` or adjusting MYPYPATH Found 1 error in 1 file (errors prevented further checking)
それぞれ個別に指定すると何も問題なく流れるんですよ!
% rye run mypy awesome Success: no issues found in 3 source files % rye run mypy fabulous Success: no issues found in 3 source files
mypy .と実行したときの「Duplicate module named "tests"」の解決法を調べました。
そもそもなんでうまくいかないのかがワカンナイヨー
結論:Duplicate module namedを解決するには
mypy --explicit-package-bases .とフラグを指定します。
ちょうどnoteとして案内されていますね(c)
% rye run mypy --explicit-package-bases . Success: no issues found in 7 source files
エラーメッセージ中のリンク https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules を見た末に「--explicit-package-bases」を指定する必要があると理解しました。
「Mapping file paths to modules」(mypyがファイルとモジュールの対応を付けるやり方)
mypy [files ...]とファイルパスを渡した時に、どのようにファイルをモジュールに対応付けるのかが述べられている箇所です。
mypyに渡されたファイルの扱い
- コマンドラインで与えられた、ファイルに対応するパスをmypyはすべてチェックする4
--exclude引数を考慮した後で、与えられたディレクトリパスの中の.pyまたは.pyiで終わる全てのファイルを再帰的に見つけ、チェックする5- チェックする各ファイルについて、mypyはファイルとモジュールの完全修飾名を関連させようとする6
3点目について、完全修飾名とは、用語集より
https://docs.python.org/ja/3/glossary.html#term-qualified-name
モジュールへの参照で使われると、完全修飾名 (fully qualified name) はすべての親パッケージを含む全体のドット名表記
ここでの例は、ファイルがproject/foo/bar/baz.pyのとき
- 完全修飾名は
foo.bar.baz - ディレクトリprojectがmypyのモジュール検索パスに追加されます
モジュールの完全修飾名の決め方
mypyのオプション--no-namespace-packagesと--explicit-package-basesによって決まるとあります。
ケース1:--no-namespace-packagesが指定された場合
__init__.pyまたは__init__.pyiの有無によります。
__init__.pyまたは__init__.pyiを見つける限り、mypyはディレクトリツリーを上にたどり続けます7。
ここで挙げられている例は以下です。
.
└── pkg/
├── __init__.py
└── subpkg/
├── __init__.py
└── mod.py
mod.pyに対応する完全修飾名ですが、__init__.pyがあるので subpkg -> pkg とたどっていきます。
結果、完全修飾名はpkg.subpkg.modとなります。
ケース2:デフォルトの場合(--namespace-packagesが有効で、--explicit-package-basesは無効)
デフォルトでは、__init__.pyも__init__.pyiも持たないディレクトリもパッケージと見なします。
上位(原語はtop-level)のパッケージを決めるために、mypyはファイルの親のディレクトリをすべて見て、__init__.pyまたは__init__.pyiを持つ最上位のディレクトリを用います8。
例を見てみましょう。
pkg/
__init__.py
a/
b/
c/
d/
mod.py
mod.pyに対応する完全修飾名ですが、上位のパッケージは__init__.pyを持つpkgと決まります。
それにより、pkg.a.b.c.d.modとなります。
ケース3:--explicit-package-bases(__init__.pyを配置できない場合)
--explicit-package-basesが指定されると、mypyは以下3つのうちから、最も近い親ディレクトリを見つけ出します9。
例はこちら(__init__.pyがありませんね)
.
└── src/
└── namespace_pkg/
└── mod.py
MYPYPATHで指定したsrcが使われます(カレントディレクトリよりもモジュールに近いと理解しました)。
srcからの相対パスで完全修飾名になるので、mod.pyに対応する完全修飾名はnamespace_pkg.modとなります。
ケース3の--explicit-package-basesがRyeのworkspaceに効く理由
そもそもなぜ「Duplicate module named "tests"」というエラーになっているかですが、デフォルトのケース2で動いたことで完全修飾名の重複が発生しています。
これは-vを指定することで確認できます
LOG: Found source: BuildSource(path='./awesome/tests/__init__.py', module='tests', has_text=False, base_dir='/.../rye-workspace-example/awesome', followed=False) LOG: Found source: BuildSource(path='./fabulous/tests/__init__.py', module='tests', has_text=False, base_dir='/.../rye-workspace-example/fabulous', followed=False)
testsというモジュール名が重複していますね(testsが__init__.pyを持つ最上位ディレクトリに当たります)
ケース2の説明からリポジトリルートに__init__.pyを配置すれば、rye-workspace-example.awesome.tests と rye-workspace-example.fabulous.tests となって区別できそうですよね。
ところがこのアイデアは、rye-workspace-exampleがハイフンを含んでいるためにモジュール名として使えず、実現できません10
ケース3をそのまま(MYPYPATHやmypy_pathを設定せずに)実行すると、カレントディレクトリ(=リポジトリのルート)からの相対パスになります。
これは__init__.py配置案と結果的には同様に、awesome.tests と fabulous.tests とを区別できます!
LOG: Found source: BuildSource(path='./awesome/tests/__init__.py', module='awesome.tests', has_text=False, base_dir='/.../rye-workspace-example', followed=False) LOG: Found source: BuildSource(path='./fabulous/tests/__init__.py', module='fabulous.tests', has_text=False, base_dir='/.../rye-workspace-example', followed=False)
こうしてエラーを解決できました🙌
なお、完全修飾名の基点が変わるので、srcの方も完全修飾名は変わります
-LOG: Found source: BuildSource(path='./awesome/src/awesome/__init__.py', module='awesome', has_text=False, base_dir='/.../rye-workspace-example/awesome/src', followed=False) +LOG: Found source: BuildSource(path='./awesome/src/awesome/__init__.py', module='awesome.src.awesome', has_text=False, base_dir='/.../rye-workspace-example', followed=False)
P.S. 名前空間パッケージ
ケース1・2のフラグについて少しだけ
Pythonのパッケージには2種類あります
https://docs.python.org/ja/3/glossary.html#term-package
- regular package
- https://docs.python.org/ja/3/glossary.html#term-regular-package
伝統的な、
__init__.pyファイルを含むディレクトリとしての package。
- namespace package(名前空間パッケージ)
- https://docs.python.org/ja/3/glossary.html#term-namespace-package
- PEP 420により、
__init__.pyを持たないディレクトリも(名前空間)パッケージとして扱えるようになりました
ケース1で指定した--no-namespace-packagesは、名前空間パッケージなし(=regular packageオンリー)と言っているわけですね。
なので、regular packageが持つ__init__.pyが関心事です。
ケース2は--namespace-packagesが有効ですから、regular packageもnamespace packageも両方ありな状況です。
regular packageを見つけ、その中はnamespace packageもありとして扱っているように思われます
変更履歴
- 2024/02/20 ディレクトリツリーのsrcの下にawesomeとfabulousを追加(リポジトリルートからは
awesome/src/awesome/__init__.py) - 2024/02/21 https://github.com/ftnext/rye-workspace-example へのリンクをコミットハッシュを指定したものに差し替え
-
このエントリの元ツイートです
↩1リポジトリで複数のパッケージを開発するシーンでリポジトリルートでmypy .すると、libA/testsとlibB/testsがtestsというモジュール名でダブっちゃって「error: Duplicate module named "tests"」。
— nikkie / にっきー (@ftnext) 2024年2月18日
mypy --explicit-package-bases .でカレントディレクトリをbaseとして解決https://t.co/OhjqG31LXZ - 以前書いたモノレポでのpipの悩みは解消していますね ↩
-
リポジトリルートのpyproject.tomlに
[tool.rye.scripts]でショートカットコマンドを用意したいという動機もあります ref: https://rye-up.com/guide/pyproject/#toolryescripts↩ - 原文 Mypy will check all paths provided that correspond to files.↩
- 原文 Mypy will recursively discover and check all files ending in .py or .pyi in directory paths provided, after accounting for --exclude.↩
- 原文 For each file to be checked, mypy will attempt to associate the file (e.g. project/foo/bar/baz.py) with a fully qualified module name (e.g. foo.bar.baz). The directory the package is in (project) is then added to mypy’s module search paths.↩
-
原文 That is, mypy will crawl up the directory tree for as long as it continues to find
__init__.py(or__init__.pyi) files.↩ -
原文 Specifically, mypy will look at all parent directories of the file and use the location of the highest
__init__.py[i]in the directory tree to determine the top-level package.↩ - 原文 With --explicit-package-bases, mypy will locate the nearest parent directory that is a member of the MYPYPATH environment variable, the mypy_path config or is the current working directory.↩
- renameするという道もありますが、そこまでするならケース3の方がよいと思います↩