[python]importについて理解したい件―pytest, aws lambdaで使う

# TL;DR

`tests/conftest.py` でデプロイ用のlambdaディレクトリをpathに追加する必要がある。 

```graphql
sys.path.append(os.path.join(os.dirname(__file__), '../{lambdaコードのディレクトリ}')
```

# はじめに

pytestでテストを実行した際に、インポートエラーに遭遇した。
importについての理解が浅いせいで、アホみたいな勘違いをして時間を浪費したのでメモる。
`sys.path.append` は定義していたのだが、通すpathが間違っていたので苦労した。

# ディレクトリ構成

`test_app.ppy` で `[app.py](http://app.py)` をテストするときに `mypkg` をimportしたいケースを考えます。
パッケージ（ディレクトリ？）にはアンダースコア（_）は非推奨らしいので、 `lambadasrc` とした。また、ハイフン（-）を利用する場合、パッケージ化したときにimport文で読み込めなくなるので、命名について理解する必要がある。。。

```
.
├── lambadasrc
│   ├── app.py
│   └── mypkg
│       └── module1.py
└── tests
    ├── __init__.py
    ├── conftest.py
    └── test_app.py
```

# エラー内容

pytestを実行するとエラー出力される。

```bash
.venv ❯ pytest tests -s

~
lambadasrc/app.py:8: in import_mypkg
    from mypkg.module1 import func
E   ModuleNotFoundError: No module named 'mypkg'
```

app.pyは下記。
`lambadasrc` ディレクトリは、lambdaのデプロイ単位のディレクトリと考えて、その直下からimportする。

```python
from pprint import pprint
import sys
pprint(sys.path)

def import_mypkg():
    from mypkg.module1 import func
    return func

func = import_mypkg()

def lambada_handler(event, context):
    func()
```

なぜエラーになるかというと、 `sys.path` に `lambdasrc` へのPATHが通っていないため。（おそらく。。）

```python
['/Users/hoge/import_pytest_202203',
 '/Users/hoge/import_pytest_202203/.venv/bin',
 '/Users/hoge/.pyenv/versions/3.9.7/lib/python39.zip',
 '/Users/hoge/.pyenv/versions/3.9.7/lib/python3.9',
 '/Users/hoge/.pyenv/versions/3.9.7/lib/python3.9/lib-dynload',
 '/Users/hoge/import_pytest_202203/.venv/lib/python3.9/site-packages']
```

pytestはtestのソースコードから上の階層にむかって `__init__.py` の有無でベースディレクトリを判断する仕様になっているので、 `/Users/hoge/import_pytest_202203` が定義されていると理解。
しかし、 `lambdasrc` まではpathが通っていないため、import エラーが発生する。
なので、conftest.pyに `sys.path.append` をかく。

```python
import sys
import os

sys.path.append(os.path.join(os.path.dirname(__file__), '../lambadasrc'))
```

sys.pathに下記が追加される。testを実行してもエラーは発生しなくなる。

```python
'/Users/hoge/import_pytest_202203/tests/../lambadasrc',
```

# 勘違いしてたこと、色々調べてさらに沼ったこと

- `sys.path.append` で、 `lambadasrc/mypkg` にpathを通していた
    - 何故かimportするパッケージまでpathが通っている必要があると思いこんでた。。。
- `__init__.py` の有無
    - pytestでは、ベースディレクトリの判断のために必要
    - srcでは、レギュラーパッケージとして認識させる場合は__init__.pyが必要
    - 暗黙的な名前空間パッケージ（****[Implicit Namespace Packages](https://peps.python.org/pep-0420/)****）として利用するなら不要
        - まぁ、パフォーマンス的に定義してほうがよいという記事もあるが、、
    - `__init__.py` で `__all__` 定義している情報も見かけ、更に混乱した。。。
        - __all__の用途は、from~importのワイルドカードを制御するために使用。また、途中のmodule名を省略するために利用しているケースも観測
        - from mypkg.hoge from Hoge → from mypkg from Hoge(hogeを省略）
    - setup.py（公式では、pyproject.toml,setup.cfg）を使用して、ローカルモジュールとしてpathを通す解決策もあり？
        - `pip install -e .` で編集モードでパッケージ作成
        - 上記試したが、解決できなかった
        - おそらく、本来はパッケージ開発にpytestをインテグレーションするトピックだと推測。(src/とtest/のディレクトリ構成もパッケージ開発のためのトピックか？）
        - [https://docs.pytest.org/en/latest/explanation/goodpractices.html#src-layout](https://docs.pytest.org/en/latest/explanation/goodpractices.html#src-layout)
- namespaceもよくわかってない
- デバッグと単純なRunだと、挙動が違いみたい？

# 他の対応案？

- `pytest` コマンド実行でなく、 `python -m pytest` 実行だとカレントディレクトリをsys.pathに追加するっぽい
- `importlib` がちょっと気になる

> `importlib`: new in pytest-6.0, this mode uses **`[importlib](https://docs.python.org/3/library/importlib.html#module-importlib)`** to import test modules. This gives full control over the import process, and doesn’t require changing **`[sys.path](https://docs.python.org/3/library/sys.html#sys.path)`**.
> 

# 終わりに

今回のエラーについてググると、ヒットする記事の内容に微妙に差異があるので自分のエラー原因を特定するのに苦労した。ググり力を高めていきたい。

# 参考

[pytest import mechanisms and sys.path/PYTHONPATH - pytest documentation](https://docs.pytest.org/en/latest/explanation/pythonpath.html)

[Good Integration Practices - pytest documentation](https://docs.pytest.org/en/latest/explanation/goodpractices.html#src-layout)