🐈

sphinx doctest poetry pytestを使用したプログラム開発フローまとめ(個人用)

2023/04/20に公開

こういう系はいろんな人がまとめているので再度まとめる必要性はないのですが、個人的な備忘録としてまとめていきます。詳しい説明は省略してます。
個人で簡単なパッケージを作成することを考えています。

また、今回の内容ではないですが、Pythonの実装のポイントはこちら(https://qiita.com/sugulu_Ogawa_ISID/items/c0e8a5e6b177bfe05e99)が非常に参考になりました。

準備

pip install sphinx
pip install sphinx_rtd_theme
pip install poetry

0. 開発ディレクトリ作成

mkdir dirname_package

1. document、test、src用のディレクトリを作成

docsにSphinxドキュメント、testsにテストコード、packagenameにコードを置きます。

cd dirname_package
mkdir packagename tests docs

2. sphinxのプロジェクト準備

対話形式で入力必要ですが、こだわりがなければ基本的にEnter key押す感じでOKです。
私は最初の ソースディレクトリとビルドディレクトリを分ける(y / n) [n]: yesにしてます。

sphinx-quickstart docs

3. conf.pyの編集

docs/source/conf.pyを以下のように編集します。

import os
import sys
sys.path.insert(0, os.path.abspath('../../packagename/')
extensions = [
  'sphinx.ext.autodoc',
  'sphinx.ext.todo',
  'sphinx.ext.viewcode',
  'sphinx.ext.napoleon',
]
html_theme = 'sphinx_rtd_theme'

4. poetry準備

pyproject.tomlがない場合に以下のコマンドをdirname_package直下で実行します。
後でpoetry addでパッケージを追加してもいいですが、下の画像のようにこの段階でも追加できます。

poetry init

[f:id:yamayou_1:20210301235917p:plain]

5. このパッケージの専用のpoetry仮想環境を作成

poetry install

このpoetry仮想環境内で実行する場合は以下のようにします。

poetry run pytest ./tests/test.py
poetry run python ./packagename/script.py

6. 適当な関数を作成とdoctest

最初に、__init__.pyを作成した後に、例として簡単な関数をpackagename内に作成します。
docstringの形式は以下のURLを参考にしてください。

[https://qiita.com/yokoc1322/items/ebf25c9cb779ff5ebc9c:title]

cd packagename
touch __init__.py
def add_integer_value(a: int, b: int) -> int:
  """add number

  This function sums up two arguments.

  Args:
      a (int): a value to be added
      b (int): b value to be added

  Returns:
      int: summation of a and b

  Example:
      >>> add_integer_value(1, 2)
      3
      >>> add_integer_value(3, -4)
      -1
  """
  return a + b

この時、以下のようにしてdoctestを行います。

poetry run python -m doctest main.py 

#7. docstringから自動でドキュメントを作成

画像のように綺麗になります。

cd ../
sphinx-apidoc -f -o ./docs/source ./src
cd docs
make html

open build/html/index.html

[f:id:yamayou_1:20210302003259p:plain]

pytest

多人数で作成または相手がいるような場合はpytestでユニットテストなどを行った方が良いですが、個人で簡単なものを作る場合は書いてないかもしれません。。。
書く場合は以下のようにtests下にテストコードを書いて実行しましょう。

cd tests
touch __init__.py
import pytest

from packagename.main import add_integer_value

@pytest.fixture
def sample_var():
  a, b = 1, 2
  return a, b

def test_add_integer_value(sample_var):
  """ 整数値の足し算
  """
  sum_value = add_integer_value(*sample_var)

  assert sum_value == 3
cd ../
poetry run pytest tests/test_add_integer_value.py

補足

以上が基本的な構成・流れですが、補足としてloggerやconstantを扱う例を載せておきます。

cd packagename
mkdir config

configフォルダ内に以下のlogging.jsonを保存します。

{
  "version": 1,
  "disable_existing_loggers": "False",
  "root": {
    "level": "DEBUG",
    "handlers": [
      "consoleHandler",
      "logFileHandler"
    ]
  },
  "handlers": {
    "consoleHandler": {
      "class": "logging.StreamHandler",
      "level": "DEBUG",
      "formatter": "consoleFormatter",
      "stream": "ext://sys.stdout"
    },
    "logFileHandler": {
      "class": "logging.FileHandler",
      "level": "DEBUG",
      "formatter": "logFileFormatter",
      "filename": "./log_file.log",
      "mode": "w",
      "encoding": "utf-8"
    }
  },
  "formatters": {
    "consoleFormatter": {
      "format": "[%(asctime)s] %(levelname)s: file=%(name)s func_name=%(funcName)s message=%(message)s"
    },
    "logFileFormatter": {
      "format": "[%(asctime)s] %(levelname)s: file=%(name)s func_name=%(funcName)s message=%(message)s"
    }
  }
}

作成したファイルのパスを定数としてアクセスできるようにしておきます。その場合、以下のようなconstants.pyを作成します。

from pathlib import Path

HOME = Path(__file__).resolve().parent

# logging config file
LOGGING_CONFIG_FILEPATH = HOME / "config" / "logging.json"

あとは、以下の関数内でconfigファイルを読み込めばOKです。

import logging
from logging import Formatter, StreamHandler, FileHandler, config, getLogger
from json import load

from packagename.constants import LOGGING_CONFIG_FILEPATH

def get_logger(name: str):
  with open(LOGGING_CONFIG_FILEPATH, "r", encoding="utf-8") as f:
    config.dictConfig(load(f))
  logger = logging.getLogger(name)
  return logger

使うときは以下のようにpackagename.logger.get_loggerをインポートします。

from packagename.logger import get_logger

logger = get_logger("main")

logger.info("test")

Discussion