はじめに
ごみけついきたい、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の方がよいと思います↩