23

単体テストを作成するとき、テストを切り取って貼り付けることがあり、メソッド名を変更することを忘れます。これにより、前のテストが上書きされ、効果的に非表示になり、実行できなくなります。例えば;

class WidgetTestCase(unittest.TestCase):

  def test_foo_should_do_some_behavior(self):
    self.assertEquals(42, self.widget.foo())

  def test_foo_should_do_some_behavior(self):
    self.widget.bar()
    self.assertEquals(314, self.widget.foo())

この場合、後者のテストのみが呼び出されます。生のソースコードを直接解析する以外に、この種のエラーをプログラムでキャッチする方法はありますか?

4

5 に答える 5

27

コードに対してpylintを実行すると、別のメソッドを上書きしたときに通知されます。

たとえば、私はこれを実行しました:

class A(object):
    def blah(self):
        print("Hello, World!")

    def blah(self):
        print("I give up!")

このオンラインピリントチェッカーでは。不足しているすべてのdocstringなどに加えて、私はこれを取得します:

E: 5:A.blah: method already defined line 2

または、コマンドラインから:

$ python -m pyflakes .
.\blah.py:5:5 redefinition of unused 'blah' from line 2
于 2012-05-25T22:25:11.213 に答える
15

以下は、文書化されていない実装固有のPython機能を使用する恐ろしいハックです。このようなことは絶対にしないください。

Python2.6.1および2.7.2でテストされています。書かれているようにPython3.2で動作するようには見えませんが、それなら、とにかくPython3.xでこれを正しく行うことができます。

import sys

class NoDupNames(object):

    def __init__(self):
        self.namespaces = []

    def __call__(self, frame, event, arg):
        if event == "call":
            if frame.f_code.co_flags == 66:
                self.namespaces.append({})
        elif event in ("line", "return") and self.namespaces:
            for key in frame.f_locals.iterkeys():
                if key in self.namespaces[-1]:
                    raise NameError("attribute '%s' already declared" % key) 
            self.namespaces[-1].update(frame.f_locals)
            frame.f_locals.clear()
            if event == "return":
                frame.f_locals.update(self.namespaces.pop())
        return self

    def __enter__(self):
        self.oldtrace = sys.gettrace()
        sys.settrace(self)

    def __exit__(self, type, value, traceback):
        sys.settrace(self.oldtrace)

使用法:

with NoDupNames():
    class Foo(object):
        num = None
        num = 42

結果:

NameError: attribute 'num' already declared

仕組み:システムトレースフックに接続します。Pythonが行を実行しようとするたびに、呼び出されます。これにより、最後に実行されたステートメントによって定義された名前を確認できます。重複を確実にキャッチできるようにするために、実際には独自のローカル変数ディクショナリを維持し、各行の後にPythonをクリアします。クラス定義の最後に、ローカルをPythonにコピーして戻します。ネストされたクラス定義を処理し、単一のステートメントで複数の割り当てを処理するために、他のいくつかの不正行為があります。

欠点として、私たちの「すべての地元の人々をクリアしてください!」アプローチはあなたがこれを行うことができないことを意味します:

with NoDupNames():
    class Foo(object):
        a = 6
        b = 7
        c = a * b

Pythonが知る限り、名前はなくabいつc = a * b実行されるかはわかりません。私たちはそれらを見るとすぐにそれらをクリアしました。また、同じ変数を1行に2回割り当てると(たとえばa = 0; a = 1)、それをキャッチできません。ただし、より一般的なクラス定義では機能します。

NoDupNamesまた、コンテキスト内にクラス定義以外のものを入れないでください。何が起こるかわかりません。多分悪いことは何もない。しかし、私はそれを試したことがないので、理論的には宇宙はそれ自身のプラグホールに吸い込まれる可能性があります。

これはおそらく私が今まで書いた中で最も邪悪なコードですが、それは確かに楽しかったです!

于 2012-05-26T01:23:53.343 に答える
6

分析ツールを必要とせずにデコレータを使用して実行時にこれを検出する方法の1つのオプションを次に示します。

def one_def_only():
  names = set()
  def assert_first_def(func):
    assert func.__name__ not in names, func.__name__ + ' defined twice'
    names.add(func.__name__)
    return func
  return assert_first_def

class WidgetTestCase(unittest.TestCase):
  assert_first_def = one_def_only()

  @assert_first_def
  def test_foo_should_do_some_behavior(self):
    self.assertEquals(42, self.widget.foo())

  @assert_first_def
  def test_foo_should_do_some_behavior(self):
    self.widget.bar()
    self.assertEquals(314, self.widget.foo())

インポートまたは実行の試みの例:

>>> import testcases
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "testcases.py", line 13, in <module>
    class WidgetTestCase(unittest.TestCase):
  File "testcases.py", line 20, in WidgetTestCase
    @assert_first_def
  File "testcases.py", line 7, in assert_first_def
    assert func.__name__ not in names, func.__name__ + ' defined twice'
AssertionError: test_foo_should_do_some_behavior defined twice
于 2012-05-25T22:36:50.240 に答える
4

古いメソッドは単純に置き換えられ、すべての関数定義でデコレータを使用する必要があるため、実行時に簡単に/きれいに検出することはできません。静的解析(Pylintなど)がそれを行うための最良の方法です。

私はそれをテストしたばかり__setattr__で、メタクラスはクラスブロックで定義されたものに対して呼び出されません。

于 2012-05-25T22:24:57.753 に答える
1
CI/CDによるソリューション

プルリクエストが発生するとテストを実行するビルド(Jenkins CI / CDなど)がある場合は、次のようなものを追加できます pylint --fail-under=7 --fail-on=E0102 paths_of_files_changed

これは、Function defined alreadyE0102 ORの場合、コードの品質が7ゼロ以外の終了コードを返すよりも低いことを意味します。

その後、これを使用してビルドを失敗させることができます。

事前コミットによるクライアント側ソリューション

または、これをgit pre commitフックと統合することもできます。これにより、コミット時に特定のコマンドを実行でき、失敗した場合はコミットできなくなります。

チェックアウトのルートに.pre-commit-config.yaml 、次のファイルを作成します。

# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/PyCQA/pylint
    rev: v2.9.6
    hooks:
    -   id: pylint
        args: [--fail-under=7, --fail-on=E0102]
pre-commitをインストールします

(推奨される方法は)pipxを使用するか、CLIアプリケーションであるため仮想環境の外部でこれを行うことです

python3 -m pip install pre-commit 
フックを取り付けます
pre-commit install # you only do this once per "git clone"

すべての関数が一度だけ定義されるまで、コミットすることはできません。これらのソリューションはどちらも、複数回定義されているメソッドがコードベースにコミットされるのを防ぎます。

于 2021-08-03T18:47:01.880 に答える