コードベースへの貢献#

コード標準#

良いコードを書くことは、単に何を書くかだけではありません。それは、どのように書くかでもあります。継続的インテグレーションテスト中、いくつかのツールがコードのスタイルエラーをチェックするために実行されます。警告が発生するとテストは失敗します。したがって、適切なスタイルはpandasにコードを提出するための要件です。

pandasには、コントリビューターがプロジェクトに貢献する前に変更を検証するのに役立ついくつかのツールがあります。

  • ./ci/code_checks.sh: doctest、docstringの書式設定、およびインポートされたモジュールを検証するスクリプトです。 docstringscode、およびdoctestsパラメータを使用して(例: ./ci/code_checks.sh doctests)、チェックを個別に実行できます。

  • pre-commitについては、次のセクションで詳しく説明します。

さらに、多くの人が私たちのライブラリを使用しているため、ユーザーのコードを大量に破壊する可能性のある突然の変更をコードに加えないことが重要です。つまり、大規模な破損を避けるために、可能な限り後方互換性を保つ必要があります。

Pre-commit#

さらに、継続的インテグレーションは、pre-commit hooksを使用してblackruffisortclang-formatなどのコード書式チェックを実行します。これらのチェックからの警告は、継続的インテグレーションを失敗させます。したがって、コードを提出する前に自分でチェックを実行すると便利です。これは、pre-commitをインストールし(開発環境のセットアップの手順に従っていれば、すでにインストールされているはずです)、次に実行することで行えます。

pre-commit install

pandasリポジトリのルートから。これにより、すべてのスタイルチェックが、各コミット時に手動で実行する必要なく実行されます。さらに、pre-commitを使用することで、コードチェックが変更された場合でも、より簡単に最新の状態を維持できます。

必要であれば、git commit --no-verify でこれらのチェックをスキップできることに注意してください。

ワークフローの一部として pre-commit を使用したくない場合でも、以下のいずれかを使用してチェックを実行できます。

pre-commit run --files <files you have modified>
pre-commit run --from-ref=upstream/main --to-ref=HEAD --all-files

pre-commit install を事前に実行する必要はありません。

最後に、コミットごとに実行されるわけではないが、継続的インテグレーション中に実行される、遅いpre-commitチェックもあります。これらは手動でトリガーできます。

pre-commit run --hook-stage manual --all-files

もはや使用されていないリポジトリをクリーンアップするために、定期的に pre-commit gc を実行したい場合があります。

virtualenv のインストールが競合している場合、エラーが発生する可能性があります - こちらを参照してください。

また、virtualenvのバグにより、condaを使用していると問題が発生する可能性があります。これを解決するには、virtualenv をバージョン 20.0.33 にダウングレードできます。

最近、上流ブランチからメインにマージした場合、pre-commit で使用される依存関係の一部が変更されている可能性があります。開発環境を更新していることを確認してください。

オプションの依存関係#

オプションの依存関係 (例: matplotlib) は、プライベートヘルパー pandas.compat._optional.import_optional_dependency を使ってインポートする必要があります。これにより、依存関係が満たされていない場合に一貫したエラーメッセージが表示されます。

オプションの依存関係を使用するすべてのメソッドには、オプションの依存関係が見つからない場合に ImportError が発生することを確認するテストを含める必要があります。このテストは、ライブラリが存在する場合はスキップする必要があります。

すべてのオプションの依存関係は、オプションの依存関係 に文書化され、必要な最小バージョンは pandas.compat._optional.VERSIONS 辞書に設定されるべきです。

下位互換性#

下位互換性を維持するよう努めてください。pandasには既存のコードを使用している多くのユーザーがいるため、可能な限りそれを壊さないでください。もし破壊が必要だと考える場合は、プルリクエストの一部としてその理由を明確に述べてください。また、メソッドのシグネチャを変更する際には注意し、必要に応じて非推奨の警告を追加してください。さらに、非推奨のsphinxディレクティブを非推奨の関数やメソッドに追加してください。

非推奨にする関数と同じ引数を持つ関数が存在する場合は、pandas.util._decorators.deprecate を使用できます。

from pandas.util._decorators import deprecate

deprecate('old_func', 'new_func', '1.1.0')

そうでない場合は、手動で行う必要があります。

import warnings
from pandas.util._exceptions import find_stack_level


def old_func():
    """Summary of the function.

    .. deprecated:: 1.1.0
       Use new_func instead.
    """
    warnings.warn(
        'Use new_func instead.',
        FutureWarning,
        stacklevel=find_stack_level(),
    )
    new_func()


def new_func():
    pass

また、次の作業も必要です。

  1. 非推奨の引数で呼び出したときに警告が発行されることを確認する新しいテストを作成する

  2. pandasの既存のすべてのテストとコードを新しい引数を使用するように更新する

詳細については、警告のテスト を参照してください。

型ヒント#

pandasはPEP 484スタイルの型ヒントの使用を強く推奨しています。新しい開発には型ヒントを含めるべきであり、既存のコードにアノテーションを追加するプルリクエストも受け付けます!

スタイルガイドライン#

型のインポートは from typing import ... 規則に従うべきです。あなたのコードは、pre-commit checks によって、いくつかのモダンな構文(例えば、typing.List の代わりに組み込みの list を使用するなど)を使用するように自動的に書き換えられることがあります。

コードベースのいくつかのケースでは、クラスが組み込み関数を隠すクラス変数を定義していることがあります。これは、Mypy 1775で説明されているように問題を引き起こします。ここでの防御的な解決策は、組み込み関数の曖昧でないエイリアスを作成し、それをアノテーションなしで使用することです。例えば、以下のような定義に出くわした場合

class SomeClass1:
    str = None

これを注釈付けする適切な方法は次のとおりです。

str_type = str

class SomeClass2:
    str: str_type = None

アナライザよりも理解している場合、typingモジュールから cast を使用したくなることがあります。これは特にカスタム推論関数を使用する場合に発生します。例えば

from typing import cast

from pandas.core.dtypes.common import is_number

def cannot_infer_bad(obj: Union[str, int, float]):

    if is_number(obj):
        ...
    else:  # Reasonably only str objects would reach this but...
        obj = cast(str, obj)  # Mypy complains without this!
        return obj.upper()

ここでの制限は、人間は is_numberint および float 型を捕捉することを合理的に理解できる一方で、mypy はまだ同じ推論ができないということです(mypy #5206を参照)。上記のコードは機能しますが、cast の使用は強く推奨されません。可能な場合は、静的解析を満足させるためにコードのリファクタリングが推奨されます。

def cannot_infer_good(obj: Union[str, int, float]):

    if isinstance(obj, str):
        return obj.upper()
    else:
        ...

カスタム型と推論の場合、これは常に可能ではないため例外が設けられていますが、そのような道に進む前に cast を避けるためにあらゆる努力を尽くすべきです。

pandas固有の型#

pandasに固有の一般的に使用される型は、pandas._typingに表示され、該当する場合はこれらを使用する必要があります。このモジュールは現在プライベートですが、最終的にはpandasに対する型チェックを実装したいサードパーティライブラリに公開されるべきです。

例えば、pandasの多くの関数は dtype 引数を受け入れます。これは "object" のような文字列、np.int64 のような numpy.dtype、または pd.CategoricalDtype のような pandas の ExtensionDtype として表現できます。ユーザーに常にこれらのオプションすべてを注釈付けさせる手間をかける代わりに、これを単に pandas._typing モジュールからインポートして再利用できます。

from pandas._typing import Dtype

def as_type(dtype: Dtype) -> ...:
    ...

このモジュールは最終的に、「パスライク」、「配列ライク」、「数値」などの繰り返し使用される概念の型を格納し、axisのような頻繁に現れるパラメータのエイリアスも保持できます。このモジュールの開発は活発であるため、最新の利用可能な型リストについてはソースを参照してください。

型ヒントの検証#

pandasはmypypyrightを使用して、コードベースと型ヒントを静的に解析します。変更を加えた後、以下のコマンドを実行することで型ヒントの一貫性を確保できます。

pre-commit run --hook-stage manual --all-files mypy
pre-commit run --hook-stage manual --all-files pyright
pre-commit run --hook-stage manual --all-files pyright_reportGeneralTypeIssues
# the following might fail if the installed pandas version does not correspond to your local git version
pre-commit run --hook-stage manual --all-files stubtest

Python 環境で。

警告

  • 上記のコマンドは現在のPython環境を使用することに注意してください。Pythonパッケージがpandas CIによってインストールされたものより古い/新しい場合、上記のコマンドは失敗する可能性があります。これは、mypy または numpy のバージョンが一致しない場合によく発生します。Python環境のセットアップ方法 を参照するか、最近成功したワークフロー を選択し、「Docstring validation, typing, and other manual pre-commit hooks」ジョブをクリックしてから、「Set up Conda」と「Environment info」をクリックして、pandas CIがインストールするバージョンを確認してください。

pandasを使用するコードでの型ヒントのテスト#

警告

  • Pandasはまだpy.typedライブラリではありません (PEP 561)! pandasをローカルでpy.typedライブラリとして宣言する主な目的は、pandas組み込みの型アノテーションをテストし、改善することです。

pandas が py.typed ライブラリになるまで、pandas のインストールフォルダに「py.typed」という名前の空のファイルを作成することで、pandas に同梱されている型アノテーションを簡単に試すことができます。

python -c "import pandas; import pathlib; (pathlib.Path(pandas.__path__[0]) / 'py.typed').touch()"

py.typedファイルの存在は、型チェッカーにpandasがすでにpy.typedライブラリであることを示します。これにより、型チェッカーはpandasに付属の型アノテーションを認識します。

継続的インテグレーションでのテスト#

プルリクエストが提出されると、pandasのテストスイートはGitHub Actionsの継続的インテグレーションサービスで自動的に実行されます。ただし、プルリクエストを提出する前にブランチでテストスイートを実行したい場合は、継続的インテグレーションサービスをGitHubリポジトリにフックする必要があります。GitHub Actionsの指示はここにあります。

すべてのビルドが「グリーン」であれば、プルリクエストはマージが検討されます。テストが失敗している場合は、赤い「X」が表示され、クリックすると個々の失敗したテストを確認できます。これはグリーンビルドの例です。

../_images/ci.png

テスト駆動開発#

pandasはテストに真剣に取り組んでおり、コントリビューターがテスト駆動開発(TDD)を採用することを強く推奨しています。この開発プロセスは、「非常に短い開発サイクルの繰り返しに依存します。まず開発者は、望ましい改善点や新機能を定義する(最初は失敗する)自動テストケースを記述し、次にそのテストに合格するための最小限のコードを生成します。」したがって、実際にコードを書く前に、テストを記述する必要があります。多くの場合、テストは元のGitHubイシューから取得できます。しかし、追加のユースケースを検討し、それに対応するテストを記述する価値は常にあります。

テストの追加は、コードがpandasにプッシュされた後に最も一般的な要求の一つです。したがって、これが問題にならないように、事前にテストを記述する習慣を身につける価値があります。

テストの作成#

すべてのテストは、特定のパッケージの tests サブディレクトリに配置する必要があります。このフォルダには、現在のテストの多くの例が含まれており、これらを参考にすることをお勧めします。

一般的なヒントとして、統合開発環境 (IDE) の検索機能やターミナルでの `git grep` コマンドを使用して、メソッドが呼び出されているテストファイルを見つけることができます。テストをどこに配置するのが最適かわからない場合は、最善の推測をしてください。ただし、レビュー担当者がテストを別の場所に移動するよう要求する可能性があることに注意してください。

git grep を使用するには、ターミナルで次のコマンドを実行します。

git grep "function_name("

これは、リポジトリ内のすべてのファイルから function_name( というテキストを検索します。これは、コードベース内の関数をすばやく見つけ、それに対するテストを追加するのに最適な場所を特定するのに役立つ方法です。

理想的には、テストが存在するべき明白な場所は一つだけであるべきです。その理想に達するまで、テストをどこに配置すべきかについていくつかの経験則があります。

  1. あなたのテストは pd._libs.tslibs 内のコードのみに依存していますか?このテストは次のいずれかに属する可能性が高いです。

    • tests.tslibs

      tests.tslibs 内のどのファイルも、pd._libs.tslibs 以外のpandasモジュールからインポートしてはなりません。

    • tests.scalar

    • tests.tseries.offsets

  2. あなたのテストは pd._libs 内のコードのみに依存していますか?このテストは次のいずれかに属する可能性が高いです。

    • tests.libs

    • tests.groupby.test_libgroupby

  3. あなたのテストは算術または比較メソッド用ですか?このテストは次のいずれかに属する可能性が高いです。

    • tests.arithmetic

      これらは、box_with_array フィクスチャを使用して DataFrame/Series/Index/ExtensionArray の動作をテストするために共有できるテストを対象としています。

    • tests.frame.test_arithmetic

    • tests.series.test_arithmetic

  4. あなたのテストは削減メソッド(min、max、sum、prod、…)用ですか?このテストは次のいずれかに属する可能性が高いです。

    • tests.reductions

      これらは、DataFrame/Series/Index/ExtensionArray の動作をテストするために共有できるテストを意図しています。

    • tests.frame.test_reductions

    • tests.series.test_reductions

    • tests.test_nanops

  5. あなたのテストはインデックス作成メソッド用ですか?これはテストの場所を決定するのが最も難しいケースです。なぜなら、これらのテストはたくさんあり、その多くが複数のメソッドをテストするからです(例: Series.__getitem__Series.loc.__getitem__ の両方)。

    1. テストは特にインデックスメソッドをテストしていますか(例:Index.get_locIndex.get_indexer)?このテストは次のいずれかに属する可能性が高いです。

      • tests.indexes.test_indexing

      • tests.indexes.fooindex.test_indexing

      そのファイル内には、メソッド固有のテストクラス(例:TestGetLoc)があるべきです。

      ほとんどの場合、これらのテストでは Series オブジェクトも DataFrame オブジェクトも必要ありません。

    2. テストは __getitem__ または __setitem__ 以外の Series または DataFrame のインデックス作成メソッド用ですか?例えば、xswheretakemasklookup、または insert などです。このテストは次のいずれかに属する可能性が高いです。

      • tests.frame.indexing.test_methodname

      • tests.series.indexing.test_methodname

    3. テストは locilocat、または iat のいずれか用ですか?このテストは次のいずれかに属する可能性が高いです。

      • tests.indexing.test_loc

      • tests.indexing.test_iloc

      • tests.indexing.test_at

      • tests.indexing.test_iat

      適切なファイル内で、テストクラスはインデクサの種類(例:TestLocBooleanMask)または主要なユースケース(例:TestLocSetitemWithExpansion)に対応します。

      複数のインデックス作成メソッドをテストするテストについては、D) セクションの注記を参照してください。

    4. あなたのテストは Series.__getitem__, Series.__setitem__, DataFrame.__getitem__, または DataFrame.__setitem__ 用ですか?このテストは次のいずれかに属する可能性が高いです。

      • tests.series.test_getitem

      • tests.series.test_setitem

      • tests.frame.test_getitem

      • tests.frame.test_setitem

      このようなテストでは、多くの場合、複数の類似するメソッドをテストすることがあります。例えば、

      import pandas as pd
      import pandas._testing as tm
      
      def test_getitem_listlike_of_ints():
          ser = pd.Series(range(5))
      
          result = ser[[3, 4]]
          expected = pd.Series([2, 3])
          tm.assert_series_equal(result, expected)
      
          result = ser.loc[[3, 4]]
          tm.assert_series_equal(result, expected)
      

    このような場合、テストの場所は、テスト対象の基となるメソッドに基づいているべきです。または、バグ修正のテストの場合は、実際のバグの場所に基づいているべきです。したがって、この例では、Series.__getitem__Series.loc.__getitem__ を呼び出すことがわかっているので、これは実際には loc.__getitem__ のテストです。したがって、このテストは tests.indexing.test_loc に属します。

  6. あなたのテストはDataFrameまたはSeriesメソッド用ですか?

    1. そのメソッドはプロットメソッドですか?このテストは次のいずれかに属する可能性が高いです。

      • tests.plotting

    2. このメソッドはI/Oメソッドですか?このテストは次のいずれかに属する可能性が高いです。

      • tests.io

        これには to_string が含まれますが、__repr__ は含まれません。__repr__tests.frame.test_reprtests.series.test_repr でテストされます。他のクラスにも test_formats ファイルがあることがよくあります。

    3. それ以外の場合、このテストは次のいずれかに属する可能性が高いです。

      • tests.series.methods.test_mymethod

      • tests.frame.methods.test_mymethod

        frame_or_series フィクスチャを使用してDataFrame/Series間でテストを共有できる場合、慣例として tests.frame ファイルに格納されます。

  7. あなたのテストは、Series/DataFrameに依存しないIndexメソッド用ですか?このテストは次のいずれかに属する可能性が高いです。

    • tests.indexes

  1. あなたのテストは、pandas提供のExtensionArrays(CategoricalDatetimeArrayTimedeltaArrayPeriodArrayIntervalArrayNumpyExtensionArrayFloatArrayBoolArrayStringArray)のいずれか用ですか?このテストは次のいずれかに属する可能性が高いです。

    • tests.arrays

  2. あなたのテストは、すべての ExtensionArray サブクラス(「EA インターフェース」)用ですか?このテストは次のいずれかに属する可能性が高いです。

    • tests.extension

pytest の使用#

テスト構造#

pandas の既存のテスト構造は、ほとんどクラスベースであり、通常、テストがクラスにラップされていることがわかります。

class TestReallyCoolFeature:
    def test_cool_feature_aspect(self):
        pass

私たちは、テストと開発を促進するより豊富なテストフレームワークを提供するpytestフレームワークを使用した、より機能的なスタイルを好みます。したがって、テストクラスを記述する代わりに、次のようなテスト関数を記述します。

def test_really_cool_feature():
    pass

推奨される pytest イディオム#

  • def test_* という名前の関数テストで、フィクスチャまたはパラメータのいずれかである引数のみを受け取る。

  • スカラーと真偽値のテストには、裸の assert を使用する。

  • SeriesDataFrame の結果を比較するには、それぞれ tm.assert_series_equal(result, expected)tm.assert_frame_equal(result, expected) を使用します。

  • 複数のケースをテストする場合は、@pytest.mark.parameterize を使用します。

  • テストケースが失敗すると予想される場合は、pytest.mark.xfail を使用します。

  • テストケースがパスしないと予想される場合は、pytest.mark.skip を使用します。

  • テストケースに特定のマークが必要な場合は、pytest.param を使用します。

  • 複数のテストでセットアップオブジェクトを共有できる場合は、@pytest.fixture を使用します。

警告

pytest.xfailpytest.mark.xfail とは異なります)は、テストを即座に停止し、テストが失敗するかどうかを確認しないため、使用しないでください。この動作が必要な場合は、代わりに pytest.skip を使用してください。

テストが失敗することが分かっているが、その失敗の方法をキャプチャする必要がない場合は、pytest.mark.xfail を使用します。これは、バグのある動作や未実装の機能を示すテストで一般的に使用されます。失敗するテストが不安定な動作を示す場合は、引数 strict=False を使用します。これにより、テストがたまたまパスした場合でもpytestが失敗しなくなります。strict=False の使用は非常に推奨されません。最終手段としてのみ使用してください。

テストの収集フェーズ中にテストが適切にマークされるように、テスト内での使用よりもデコレータ @pytest.mark.xfail と引数 pytest.param を優先します。複数のパラメーター、フィクスチャ、またはそれらの組み合わせを含むテストをxfailにするには、テストフェーズ中にのみxfailすることができます。これを行うには、request フィクスチャを使用します。

def test_xfail(request):
    mark = pytest.mark.xfail(raises=TypeError, reason="Indicate why here")
    request.applymarker(mark)

xfail は、無効なユーザー引数による失敗を伴うテストには使用しないでください。これらのテストでは、pytest.raises を使用して、正しい例外タイプとエラーメッセージが発行されていることを確認する必要があります。

警告のテスト#

コードブロックが警告を発生させることを確認するには、コンテキストマネージャとして tm.assert_produces_warning を使用します。

with tm.assert_produces_warning(DeprecationWarning):
    pd.deprecated_function()

コードブロック内で特定の警告が発生しないようにしたい場合は、コンテキストマネージャーに False を渡します。

with tm.assert_produces_warning(False):
    pd.no_warning_function()

警告を発するテストがあるが、実際に警告自体をテストしているわけではない場合(例えば、将来削除される予定であるとか、サードパーティライブラリの動作に合わせているためなど)、pytest.mark.filterwarnings を使用してエラーを無視します。

@pytest.mark.filterwarnings("ignore:msg:category")
def test_thing(self):
    pass

例外のテスト#

pytest.raises を、特定の例外サブクラス(つまり、Exception は絶対に使用しない)と、match に例外メッセージを指定したコンテキストマネージャとして使用します。

with pytest.raises(ValueError, match="an error"):
    raise ValueError("an error")

ファイルを含むテスト#

tm.ensure_clean コンテキストマネージャは、テスト用の仮ファイルを作成し、自動的に生成されたファイル名(または指定されたファイル名)を持ち、コンテキストブロックが終了すると自動的に削除されます。

with tm.ensure_clean('my_file_path') as path:
    # do something with the path

ネットワーク接続を伴うテスト#

単体テストは、ネットワーク接続の不安定性や接続先のサーバーの所有権がないため、インターネット経由で公開データセットにアクセスすべきではありません。この相互作用をモックするには、pytest-localserver プラグインhttpserver フィクスチャと合成データを使用します。

@pytest.mark.network
@pytest.mark.single_cpu
def test_network(httpserver):
    httpserver.serve_content(content="content")
    result = pd.read_html(httpserver.url)

#

ここに、pandas/tests/test_cool_feature.py ファイル内にある、私たちが使用したい複数の機能を例示する自己完結型のテストセットの例を示します。新しいテストには、GitHub Issue Number をコメントとして追加することを忘れないでください。

import pytest
import numpy as np
import pandas as pd


@pytest.mark.parametrize('dtype', ['int8', 'int16', 'int32', 'int64'])
def test_dtypes(dtype):
    assert str(np.dtype(dtype)) == dtype


@pytest.mark.parametrize(
    'dtype', ['float32', pytest.param('int16', marks=pytest.mark.skip),
              pytest.param('int32', marks=pytest.mark.xfail(
                  reason='to show how it works'))])
def test_mark(dtype):
    assert str(np.dtype(dtype)) == 'float32'


@pytest.fixture
def series():
    return pd.Series([1, 2, 3])


@pytest.fixture(params=['int8', 'int16', 'int32', 'int64'])
def dtype(request):
    return request.param


def test_series(series, dtype):
    # GH <issue_number>
    result = series.astype(dtype)
    assert result.dtype == dtype

    expected = pd.Series([1, 2, 3], dtype=dtype)
    tm.assert_series_equal(result, expected)

これをテスト実行すると、以下のようになります。

((pandas) bash-3.2$ pytest  test_cool_feature.py  -v
=========================== test session starts ===========================
platform darwin -- Python 3.6.2, pytest-3.6.0, py-1.4.31, pluggy-0.4.0
collected 11 items

tester.py::test_dtypes[int8] PASSED
tester.py::test_dtypes[int16] PASSED
tester.py::test_dtypes[int32] PASSED
tester.py::test_dtypes[int64] PASSED
tester.py::test_mark[float32] PASSED
tester.py::test_mark[int16] SKIPPED
tester.py::test_mark[int32] xfail
tester.py::test_series[int8] PASSED
tester.py::test_series[int16] PASSED
tester.py::test_series[int32] PASSED
tester.py::test_series[int64] PASSED

parametrized 化されたテストは、テスト名でアクセスできるようになりました。例えば、-k int8 を使用して、int8 に一致するテストのみを選択して実行することができます。

((pandas) bash-3.2$ pytest  test_cool_feature.py  -v -k int8
=========================== test session starts ===========================
platform darwin -- Python 3.6.2, pytest-3.6.0, py-1.4.31, pluggy-0.4.0
collected 11 items

test_cool_feature.py::test_dtypes[int8] PASSED
test_cool_feature.py::test_series[int8] PASSED

hypothesis の使用#

Hypothesisは、プロパティベースのテストのためのライブラリです。明示的にテストをパラメータ化する代わりに、すべての有効な入力を記述し、Hypothesisに失敗する入力を見つけさせることができます。さらに良いことに、Hypothesisは、どれだけ多くのランダムな例を試しても、常にアサーションに対する単一の最小限の反例を報告します。これは、あなたが決してテストしようとは思わなかったであろう例であることもよくあります。

より詳しい紹介については、Hypothesis の入門 を参照し、詳細については Hypothesis のドキュメント を参照してください。

import json
from hypothesis import given, strategies as st

any_json_value = st.deferred(lambda: st.one_of(
    st.none(), st.booleans(), st.floats(allow_nan=False), st.text(),
    st.lists(any_json_value), st.dictionaries(st.text(), any_json_value)
))


@given(value=any_json_value)
def test_json_roundtrip(value):
    result = json.loads(json.dumps(value))
    assert value == result

このテストは、Hypothesisのいくつかの便利な機能を示すだけでなく、適切なユースケース、つまり、大量または複雑な入力ドメインにわたって保持されるべきプロパティをチェックする方法を示しています。

pandas のテストスイートを迅速に実行するために、入力またはロジックが単純な場合はパラメータ化されたテストが推奨され、複雑なロジックがある場合や、オプションの組み合わせが多すぎる場合、またはテストする(あるいは考える!)のが微妙な相互作用がある場合に Hypothesis テストが予約されています。

テストスイートの実行#

テストは、Git クローン内で直接(pandas をインストールする必要なく)次のコマンドを入力して実行できます。

pytest pandas

一部のテストがパスしなくても、pandas のインストールに問題があるわけではないかもしれません。一部のテスト(SQLAlchemy の一部など)には追加の設定が必要であり、非固定のライブラリが新しいバージョンをリリースしたために一部のテストが失敗し始める可能性があり、並行して実行すると不安定になるテストもあります。ローカルでビルドしたバージョンから pandas をインポートできる限り、インストールは問題なく、貢献を開始できます!

多くの場合、スイート全体を実行する前に、変更に関連するテストのサブセットのみを実行する価値があります(ヒント:pandas-coverage アプリを使用して、変更したコード行にヒットするテストを見つけ、それらのみを実行できます)。

これを実行する最も簡単な方法は、

pytest pandas/path/to/test.py -k regex_matching_test_name

または、次のいずれかの構成を使用します。

pytest pandas/tests/[test-module].py
pytest pandas/tests/[test-module].py::[TestClass]
pytest pandas/tests/[test-module].py::[TestClass]::[test_method]

私たちの「pandas-dev」環境に含まれているpytest-xdistを使用すると、マルチコアマシンでのローカルテストを高速化できます。-n 数値フラグをpytestの実行時に指定することで、指定されたコア数にわたってテスト実行を並列化したり、すべての利用可能なコアを利用するために「auto」を指定したりできます。

# Utilize 4 cores
pytest -n 4 pandas

# Utilizes all available cores
pytest -n auto pandas

さらに高速化したい場合は、このコマンドのより高度な使用法は次のようになります。

pytest pandas -n 4 -m "not slow and not network and not db and not single_cpu" -r sxX

マルチスレッド性能向上に加えて、-m マークフラグを使用して一部のテストをスキップすることでテスト速度が向上します。

  • slow: 長時間かかるテスト(ミリ秒ではなく秒単位で考える)

  • network: ネットワーク接続を必要とするテスト

  • db: データベース (mysql または postgres) を必要とするテスト

  • single_cpu: 単一のCPUのみで実行されるべきテスト

関連する場合は、以下のオプションを有効にしたい場合があります。

  • arm_slow: arm64 アーキテクチャで時間がかかるテスト

これらのマーカーは、この toml ファイル[tool.pytest.ini_options]markers というリストの下に定義されており、興味のある新しいマーカーが作成されているかどうかを確認したい場合に参照できます。

-r レポートフラグは、簡単な要約情報(pytest ドキュメントを参照)を表示します。ここでは、次の数を表示しています。

  • s: スキップされたテスト

  • x: xfailed テスト

  • X: xpassed テスト

この要約は任意であり、追加情報が不要な場合は削除できます。並列化オプションを使用すると、プルリクエストを送信する前にローカルでテストを実行するのにかかる時間を大幅に短縮できます。

過去にもあったように、結果についてサポートが必要な場合は、コマンドを実行してバグレポートを開く前にシードを設定してください。そうすれば、再現できます。Windowsでシードを設定する例は次のとおりです。

set PYTHONHASHSEED=314159265
pytest pandas -n 4 -m "not slow and not network and not db and not single_cpu" -r sxX

Unixでは、

export PYTHONHASHSEED=314159265
pytest pandas -n 4 -m "not slow and not network and not db and not single_cpu" -r sxX

詳細については、pytest のドキュメントを参照してください。

さらに、

pd.test()

インポートされた pandas を使用して同様にテストを実行できます。

性能テストスイートの実行#

パフォーマンスは重要であり、コードがパフォーマンスの低下を引き起こしていないか検討する価値があります。pandasは、重要なpandas操作のパフォーマンスを簡単に監視できるように、asv benchmarksへの移行を進めています。これらのベンチマークはすべてpandas/asv_benchディレクトリにあり、テスト結果はこちらで確認できます。

asv のすべての機能を使用するには、conda または virtualenv のいずれかが必要です。詳細については、asv のインストール Web ページを確認してください。

asv をインストールするには、

pip install git+https://github.com/airspeed-velocity/asv

ベンチマークを実行する必要がある場合は、ディレクトリを asv_bench/ に変更して実行します。

asv continuous -f 1.1 upstream/main HEAD

HEAD を作業中のブランチ名に置き換え、10%以上変更されたベンチマークを報告できます。このコマンドは、デフォルトで conda を使用してベンチマーク環境を作成します。代わりに virtualenv を使用したい場合は、次のように記述します。

asv continuous -f 1.1 -E virtualenv upstream/main HEAD

-E virtualenv オプションは、ベンチマークを実行するすべての asv コマンドに追加する必要があります。デフォルト値は asv.conf.json で定義されています。

フルベンチマークスイートの実行は、ハードウェアとそのリソース使用状況によっては一日中かかる場合があります。しかし、通常は、コミットされた変更が予期せぬパフォーマンス低下を引き起こさないことを示すために、結果の一部のみをプルリクエストに貼り付けるだけで十分です。-b フラグを使用して特定のベンチマークを実行できます。このフラグは正規表現を受け取ります。例えば、これは pandas/asv_bench/benchmarks/groupby.py ファイルからのベンチマークのみを実行します。

asv continuous -f 1.1 upstream/main HEAD -b ^groupby

ファイルから特定のベンチマークグループのみを実行したい場合は、. を区切り文字として使用して実行できます。例えば、

asv continuous -f 1.1 upstream/main HEAD -b groupby.GroupByMethods

groupby.py で定義されている GroupByMethods ベンチマークのみを実行します。

現在のPython環境にすでにインストールされている pandas のバージョンを使用してベンチマークスイートを実行することもできます。これは、virtualenvやcondaがない場合、または上述の setup.py develop アプローチを使用している場合に役立ちます。インプレースビルドの場合、PYTHONPATH を設定する必要があります。例えば、PYTHONPATH="$PWD/.." asv [remaining arguments] です。既存のPython環境を使用してベンチマークを実行するには、次のようにします。

asv run -e -E existing

または、特定の Python インタープリタを使用するには、

asv run -e -E existing:python3.6

これにより、ベンチマークからの stderr が表示され、$PATH に含まれるローカルの python が使用されます。

ベンチマークの書き方とasvの使用方法に関する情報は、asvドキュメントに記載されています。

コードのドキュメント化#

変更は doc/source/whatsnew/vx.y.z.rst にあるリリースノートに反映されるべきです。このファイルには、各リリースの変更履歴が継続的に記録されています。修正、改善、または(避けられない)破壊的変更を文書化するために、このファイルにエントリを追加してください。エントリを追加する際には、GitHubのイシュー番号(1234 がイシュー/プルリクエスト番号である場合、:issue:`1234` を使用)を含めるようにしてください。エントリは完全な文章と適切な文法で書かれるべきです。

API の一部を言及する場合、適切な Sphinx の :func::meth:、または :class: ディレクティブを使用してください。すべての公開 API 関数やメソッドにドキュメントページがあるわけではありません。理想的には、リンクは解決できる場合にのみ追加されるべきです。通常、以前のバージョンのリリースノートを確認することで、類似の例を見つけることができます。

コードがバグ修正の場合、関連するバグ修正セクションにエントリを追加してください。Other セクションへの追加は避けてください。ごくまれなケースでのみ、エントリがそこに含まれるべきです。可能な限り簡潔に、バグの説明には、ユーザーがどのようにバグに遭遇する可能性があるか、およびバグ自体(例: 「誤った結果を生成する」または「誤って例外を発生させる」)を示すべきです。新しい動作を示す必要もあるかもしれません。

コードが改善の場合、既存のドキュメントに使用例を追加する必要がある可能性が高いです。これは、ドキュメントに関するセクションに従って行うことができます。さらに、この機能がいつ追加されたかをユーザーに知らせるために、versionadded ディレクティブが使用されます。そのためのSphinx構文は次のとおりです。

.. versionadded:: 2.1.0

これにより、Sphinxディレクティブを置いた場所に「New in version 2.1.0」というテキストが表示されます。これは、新しい関数やメソッド()または新しいキーワード引数()を追加する際にも、docstringに含めるべきです。