为代码库做出贡献#

规范标准#

写出好的代码不只是你写了什么。它也是关于 how 你来写吧。在.期间 Continuous Integration 在测试过程中,将运行几个工具来检查代码的样式错误。生成任何警告都会导致测试失败。因此,良好的风格是向Pandas提交代码的要求。

Pandas中有一个工具可以帮助贡献者在将更改贡献给项目之前验证他们的更改::

./ci/code_checks.sh

该脚本验证文档测试、文档字符串中的格式、静态输入和导入的模块。可以使用参数独立运行检查 docstringcodetyping ,以及 doctests (例如 ./ci/code_checks.sh doctests )。

此外,因为有很多人使用我们的库,所以重要的是我们不要突然更改代码,这可能会导致大量用户代码崩溃,也就是说,我们需要它作为 向后兼容 尽可能地避免大规模破碎。

除了……之外 ./ci/code_checks.sh ,一些额外的检查是由 pre-commit -请参见 here 关于如何运营它们。

预提交#

另外, Continuous Integration 将运行代码格式检查,如 blackflake8 (包括 pandas-dev-flaker 插件)、 isort ,以及 cpplint 和更多的使用 pre-commit hooks 来自这些检查的任何警告都将导致 Continuous Integration 失败;因此,在提交代码之前自己运行检查是有帮助的。这可以通过安装 pre-commit ::

pip install pre-commit

然后运行::

pre-commit install

来自Pandas储存库的根。现在,每次提交更改时都会运行所有样式检查,而无需手动运行每个检查。此外,使用 pre-commit 还可以让您在代码检查更改时更轻松地保持最新状态。

请注意,如果需要,您可以使用跳过这些检查 git commit --no-verify

如果您不想使用 pre-commit 作为您的工作流的一部分,您仍然可以使用它来运行其检查:

pre-commit run --files <files you have modified>

而不需要做 pre-commit install 在此之前。

如果要对upstream/main上最近提交的所有文件运行检查,可以使用::

pre-commit run --from-ref=upstream/main --to-ref=HEAD --all-files

而不需要做 pre-commit install 在此之前。

备注

如果您有冲突的安装 virtualenv ,则可能会出现错误-请参阅 here

此外,由于 bug in virtualenv ,如果您使用的是Conda,则可能会遇到问题。要解决此问题,您可以降级 virtualenv 至版本 20.0.33

可选依赖项#

可选依赖项(例如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


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

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


def new_func():
    pass

您还需要

  1. 编写一个新测试,该测试断言在使用不推荐使用的参数调用时发出警告

  2. 更新所有PANDA现有测试和代码以使用新参数

看见 测试警告 想要更多。

类型提示#

Pandas强烈鼓励使用 PEP 484 样式类型提示。新的开发应该包含类型提示,注释现有代码的Pull请求也是可以接受的!

风格指南#

类型导入应遵循 from typing import ... 这是惯例。某些类型不需要导入,因为 PEP 585 一些内置构造,例如 listtuple ,可直接用于类型批注。所以,与其如此

import typing

primes: typing.List[int] = []

你应该写信给我

primes: list[int] = []

Optional 应避免使用较短的 | None ,所以不是

from typing import Union

maybe_primes: list[Union[int, None]] = []

from typing import Optional

maybe_primes: list[Optional[int]] = []

你应该写信给我

from __future__ import annotations  # noqa: F404

maybe_primes: list[int | None] = []

在某些情况下,在代码基类中,类可能定义隐藏在内置中的类变量。这会导致问题,如中所述 Mypy 1775 。这里的防御性解决方案是创建一个明确的内置别名,并在没有注释的情况下使用该别名。例如,如果您遇到这样的定义

class SomeClass1:
    str = None

对此进行注释的适当方式如下

str_type = str

class SomeClass2:
    str: str_type = None

在某些情况下,您可能会尝试使用 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_number 会赶上 intfloat 类型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" ,a numpy.dtype 喜欢 np.int64 甚至是一只Pandas ExtensionDtype 喜欢 pd.CategoricalDtype 。用户不必不断地注释所有这些选项,只需从Pandas._TYPING模块导入并重复使用就可以了

from pandas._typing import Dtype

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

此模块最终将包含重复使用的概念的类型,如“类路径”、“类数组”、“数值”等。还可以保存常见出现的参数的别名,如 axis 。此模块的开发正在进行中,因此请务必参考参考资料,以获取最新的可用类型列表。

正在验证类型提示#

Pandas使用 mypypyright 静态分析代码库和类型提示。进行任何更改后,可以通过运行以下命令来确保您的类型提示正确

./ci/code_checks.sh typing

最新版本的 numpy (>=1.21.0)是类型验证所必需的。

使用PANAS测试代码中的类型提示#

警告

  • Pandas还不是一个py.type类型库 (PEP 561 )!在本地将Pandas声明为一个py.type库的主要目的是测试和改进Pandas内置的类型批注。

在Pandas成为一个py.type类型库之前,可以通过在Pandas安装文件夹中创建一个名为“py.type”的空文件来轻松地试验Pandas附带的类型批注:

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

类型文件的存在向类型检查器发出信号,即Pandas已经是一个py.type库。这使类型检查器知道Pandas附带的类型批注。

使用持续集成进行测试#

PANAS测试套件将在 GitHub ActionsAzure Pipelines 持续集成服务,在您的拉式请求提交后。但是,如果您希望在提交Pull请求之前在分支上运行测试套件,那么需要将持续集成服务挂接到您的GitHub存储库。此处提供了以下说明 GitHub ActionsAzure Pipelines

当你有一个完全‘绿色’的构建时,拉式请求将被考虑用于合并。如果有任何测试失败,您将看到一个红色的‘X’,在这里您可以点击查看各个失败的测试。这是绿色建筑的一个例子。

../_images/ci.png

测试驱动的开发/代码编写#

Pandas认真对待测试,并强烈鼓励贡献者接受 test-driven development (TDD) 。这个开发过程“依赖于非常短的开发周期的重复:首先,开发人员编写(最初失败的)自动化测试用例,定义所需的改进或新功能,然后生成通过该测试所需的最少代码。”因此,在实际编写任何代码之前,您应该编写测试。通常,测试可以从最初的GitHub问题开始。然而,考虑其他用例并编写相应的测试总是值得的。

在向Pandas推送代码后,添加测试是最常见的请求之一。因此,养成提前编写测试的习惯是值得的,这样就永远不会有问题。

写试卷#

所有的测试都应该进入 tests 特定程序包的子目录。该文件夹包含许多当前的测试示例,我们建议您从这些示例中寻找灵感。请参考我们的 testing location guide 如果您不确定在哪里放置新的单元测试。

使用 pytest#

测试结构#

Pandas现有的测试结构是 大部分 基于类的,这意味着您通常会找到包装在类中的测试。

class TestReallyCoolFeature:
    pass

展望未来,我们正在向更多的 功能 样式使用 pytest 框架,它提供了一个更丰富的测试框架,将有助于测试和开发。因此,我们将不编写测试类,而是编写如下测试函数:

def test_really_cool_feature():
    pass

偏爱的成语#

  • 功能测试命名 def test_*only 参数可以是装备,也可以是参数。

  • 使用裸露的 assert 用于测试标量和真实性测试

  • 使用 tm.assert_series_equal(result, expected)tm.assert_frame_equal(result, expected) 用于比较 SeriesDataFrame 结果分别进行了两次比较。

  • 使用 @pytest.mark.parameterize 当测试多个案例时。

  • 使用 pytest.mark.xfail 当测试用例预计会失败时。

  • 使用 pytest.mark.skip 当测试用例永远不会通过时。

  • 使用 pytest.param 当测试用例需要特定标记时。

  • 使用 @pytest.fixture 如果多个测试可以共享一个设置对象。

警告

不要使用 pytest.xfail (它不同于 pytest.mark.xfail ),因为它立即停止测试并且不检查测试是否会失败。如果这是您想要的行为,请使用 pytest.skip 取而代之的是。

如果已知测试失败,但失败的方式不应被捕获,请使用 pytest.mark.xfail 对于表现出错误行为或未实现功能的测试,通常使用此方法。如果失败的测试行为不稳定,请使用以下参数 strict=False 。这将使它在测试碰巧通过时不会失败。

我更喜欢室内装饰师 @pytest.mark.xfail 而这一论点 pytest.param 在测试中过度使用,以便在pytest的收集阶段适当地标记该测试。对于涉及多个参数、一个夹具或它们的组合的测试不合格,只有在测试阶段才有可能不合格。要执行此操作,请使用 request 固定装置:

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

XFAIL不适用于因用户参数无效而导致失败的测试。对于这些测试,我们需要验证是否引发了正确的异常类型和错误消息 pytest.raises 取而代之的是。

如果您的测试需要使用文件或网络连接,请参阅 wiki Testing 维基百科的。

示例#

以下是文件中自包含的一组测试的示例 pandas/tests/test_cool_feature.py 这说明了我们喜欢使用的多个功能。请记住将Github问题编号作为注释添加到新测试中。

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 若要子选择 only 那些匹配的测试 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#

假设是基于属性的测试的库。您可以描述测试,而不是显式地将测试参数化 all 有效的输入,并让假设尝试找到失败的输入。更好的是,无论它尝试多少随机示例,假设总是报告对您的断言的单个最小反例-通常是一个您从未想过要测试的示例。

看见 Getting Started with Hypothesis 关于更多的介绍,那么 refer to the Hypothesis documentation for details

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

这个测试展示了假设的几个有用的特性,并演示了一个很好的用例:检查应该在大的或复杂的输入域上保持的属性。

为了保持PANAS测试套件快速运行,如果输入或逻辑简单,则首选参数化测试,假设测试保留用于具有复杂逻辑的情况,或者有太多选项组合或微妙的交互需要测试(或考虑!)他们所有人。

测试警告#

默认情况下, Continuous Integration 如果发出任何未经处理的警告,则将失败。

如果更改涉及检查是否实际发出了警告,请使用 tm.assert_produces_warning(ExpectedWarning)

import pandas._testing as tm


df = pd.DataFrame()
with tm.assert_produces_warning(FutureWarning):
    df.some_operation()

我们更喜欢这个而不是 pytest.warns 上下文管理器,因为我们的管理器检查警告的堆栈级别是否设置正确。堆栈级别是确保 用户的 警告中打印的是文件名和行号,而不是Pandas内部的内容。它表示来自用户代码的函数调用的数量(例如 df.some_operation() )添加到实际发出警告的函数。我们的林特将无法通过构建,如果您使用 pytest.warns 在一次测试中。

如果您有一个测试会发出警告,但您实际上并没有测试警告本身(比如说因为它将在将来被删除,或者因为我们正在匹配第三方库的行为),那么使用 pytest.mark.filterwarnings 以忽略该错误。

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

如果测试生成类的警告 category 谁的消息以谁开头 msg ,则该警告将被忽略,测试将通过。

如果您需要更细粒度的控制,您可以使用Python通常的 warnings module 控制是否在一次测试中的不同位置忽略/引发警告。

with warnings.catch_warnings():
    warnings.simplefilter("ignore", FutureWarning)
    # Or use warnings.filterwarnings(...)

或者,考虑拆分单元测试。

运行测试套件#

然后,可以通过键入以下命令在Git克隆中直接运行测试(无需安装Pandas):

pytest pandas

通常,在运行整个套件之前,首先围绕您的更改运行测试子集是值得的。

执行此操作的最简单方法是::

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]

使用 pytest-xdist ,您可以加快在多核计算机上进行本地测试。要使用此功能,您需要安装 pytest-xdist 途经::

pip install pytest-xdist

提供了两个脚本来帮助实现这一点。这些脚本将测试分布在4个线程上。

在Unix变体上,用户可以键入::

test_fast.sh

在Windows上,用户可以键入::

test_fast.bat

这可以显著减少在提交Pull请求之前本地运行测试所需的时间。

有关更多信息,请参阅 pytest 文档。

此外,人们还可以跑步

pd.test()

与一只进口大Pandas进行类似的测试。

运行性能测试套件#

性能很重要,值得考虑您的代码是否引入了性能回归。大Pandas正在迁徙到 asv benchmarks 以便能够轻松监测关键大Pandas行动的表现。这些基准都可以在 pandas/asv_bench directory, and the test results can be found here

要使用ASV的所有功能,您需要 condavirtualenv 。有关更多详细信息,请查看 asv installation webpage

要安装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

运行完整的基准测试套件可能需要一整天的时间,具体取决于您的硬件及其资源利用率。然而,通常只将结果的一个子集粘贴到Pull请求中就足够了,以表明提交的更改不会导致意外的性能衰退。方法运行特定的基准测试。 -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

将仅运行 GroupByMethods 中定义的基准 groupby.py

您还可以使用以下版本运行基准测试套件 pandas 已安装在您当前的Python环境中。如果您没有Viralenv或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,并使用您的本地 python 那是来自你的 $PATH

有关如何编写基准测试以及如何使用ASV的信息,请参阅 asv documentation

记录您的代码#

更改应反映在以下位置的发行说明中 doc/source/whatsnew/vx.y.z.rst 。该文件包含每个版本的持续更改日志。在此文件中添加一个条目,以记录您的修复、增强或(不可避免的)破坏性更改。添加条目时,请确保包含GitHub问题编号(使用 :issue:`1234 在哪里 ``1234` 是发出/拉出请求编号)。你的参赛作品应该使用完整的句子和适当的语法。

当提到API的某些部分时,请使用Sphinx :func::meth: ,或 :class: 适当的指令。并不是所有的公共API函数和方法都有文档页;理想情况下,只有在解析后才会添加链接。您通常可以通过查看以前版本的发行说明来找到类似的示例。

如果您的代码是错误修复的,请将您的条目添加到相关的错误修复部分。避免添加到 Other 部分;只有在极少数情况下,条目才应该放在那里。Bug的描述应该尽可能简洁,包括用户可能遇到它的方式和Bug本身的指示,例如“产生错误的结果”或“错误的引发”。可能还有必要指出新的行为。

如果您的代码是一种增强,则很可能需要在现有文档中添加用法示例。这可以按照有关以下内容的部分完成 documentation 。此外,为了让用户知道何时添加了此功能, versionadded 指令被使用。它的狮身人面像语法是:

.. versionadded:: 1.1.0

This will put the text New in version 1.1.0 wherever you put the sphinx directive. This should also be put in the docstring when adding a new function or method (example) or a new keyword argument (example).