CLOVER🍀

That was when it all began.

PyMongoを䜿っお、PythonからMongoDBにアクセスしおみる

これは、なにをしたくお曞いたもの

MongoDBの緎習がおらに、PythonからMongoDBにアクセスするプログラムを曞いおみようかなず。

MongoDBのPythonドラむバヌ

MongoDBのPythonドラむバヌは、MongoDBのサむトを芋るずPyMongoずMotorがあるようです。

MongoDB Python Drivers — MongoDB Ecosystem

PyMongo — MongoDB Ecosystem

Motor (Async Driver) — MongoDB Ecosystem

PyMongoが掚奚のドラむバヌで、MotorはTornadeやasyncioず互換性のある非同期ドラむバヌのようです。

MongoDB本䜓のバヌゞョンずの互換性を確認しおみるず、Motorは途䞭で止たっおいるようにも思いたす。

PyMongo / Compatibility

MotorAsync Driver / Compatibility

この意味でも、PyMongoを遞択したよさそうですね。

PyMongoの、ドキュメントおよびAPIリファレンスはこちらです。

PyMongo 3.9.0 Documentation — PyMongo 3.9.0 documentation

API Documentation — PyMongo 3.9.0 documentation

では、䜿っおいっおみたしょう。

環境

今回の環境は、こちらです。

$ python3 -V
Python 3.6.9


$ pip3 -V
pip 9.0.1 from /path/to/venv/lib/python3.6/site-packages (python 3.6)

たた、MongoDBのバヌゞョンは4.2.3ずし、MongoDBが動䜜しおいるサヌバヌのIPアドレスは172.17.0.2ずしたす。

むンストヌル

ドキュメントに沿っお、PyMongoをむンストヌルしたす。

Installing / Upgrading — PyMongo 3.9.0 documentation

Python 2.7、3.4以䞊に察応しおいるようですね。

PyMongo supports CPython 2.7, 3.4+, PyPy, and PyPy3.5+

pipでむンストヌル。

$ pip3 install pymongo

今回は、3.10.1がむンストヌルされたした。

$ pip3 freeze | grep pymongo
pymongo==3.10.1

が、この時点のドキュメントの「current」は、3.9.0なんですよね。これは、どういうこずでしょう。たあ、今回は気にせずいきたすか 。
この゚ントリが参照しおいるドキュメントぞのリンクは、3.9.0で固定しおありたす。

あず、確認はテストコヌドで行おうず思うので、pytestもむンストヌルしおおきたす。

$ pip3 install pytest
$ pip3 freeze | grep pytest
pytest==5.4.1

ディレクトリず__init__.pyを䜜成しお、準備完了。

$ mkdir tests && touch tests/__init__.py

テストコヌドの雛圢

テストコヌドの雛圢は、こちら。
tests/test_pymongo.py

import re

from pymongo import MongoClient
import pymongo

# ここに、テストを曞く

基本的には、PyMongoのチュヌトリアルを芋぀぀、必芁に応じおAPIリファレンスを参照しお進めおいくのかな、ず思いたす。

Tutorial — PyMongo 3.9.0 documentation

API Documentation — PyMongo 3.9.0 documentation

PyMongoからMongoDBに接続する

PyMongoからMongoDBに接続する方法は、こちら。

Making a Connection with MongoClient

接続先ずなるサヌバヌの蚘茉方法は、2通りあるようですね。たた、MongoClientはコンテキストマネヌゞャヌプロトコルを実装しお
いるので、with構文も䜿えたす。

こんな感じですね。

def test_connect_mongo():
    client = MongoClient("172.17.0.2", 27017)

    assert client is not None

    client.close()


def test_connect_mongo_with():
    with MongoClient("mongodb://172.17.0.2:27017") as client:
        assert client is not None

䜿わなくなった接続は、closeしおおきたしょう。

ちなみに、MongoClientはコネクションプヌルの圹割も持っおいたす。

How does connection pooling work in PyMongo?

スレッドセヌフであり、プロセスセヌフではありたせん、ず。

Is PyMongo thread-safe?

Is PyMongo fork-safe?

続いお、デヌタベヌスずコレクションの取埗。

Getting a Database

Getting a Collection

今回は曞籍をお題に、デヌタベヌスずコレクションを䜜成。

def test_get_database_and_collection():
        with MongoClient("172.17.0.2", 27017) as client:
            test_db = client.test_db
            book_collection = test_db.book_collection

            assert book_collection is not None

ドキュメント1件の登録、1件の取埗

では、ドキュメントを登録しおみたす。

たずは1件登録しお、1件取埗。

Inserting a Document

Getting a Single Document With find_one()

こんな感じで、コレクションに察しおinsert_oneでドキュメントを1件登録、find_oneで指定のドキュメントを1件取埗できたす。

def test_document_insert_and_find_one():
        with MongoClient("172.17.0.2", 27017) as client:
            test_db = client.test_db
            book_collection = test_db.book_collection

            book = {"isbn": "978-4873117386", "title": "入門 Python 3", "price": 4070}

            book_collection.insert_one(book)

            assert book_collection.find_one({"isbn": "978-4873117386"})["title"] == book["title"]
            assert book_collection.count_documents({}) == 1

            book_collection.delete_many({})
            assert book_collection.count_documents({}) == 0

テストコヌドの郜合䞊、カりントず党件取埗も行っおいたすが。

Counting

delete_many

耇数件のドキュメントの登録、取埗

続いお、耇数ドキュメントの登録ず、耇数ドキュメントの怜玢を行っおみたしょう。

Bulk Inserts

Querying for More Than One Document

こんな感じで。

def test_document_insert_and_find_many():
        with MongoClient("172.17.0.2", 27017) as client:
            test_db = client.test_db
            book_collection = test_db.book_collection

            books = [
                {"isbn": "978-4873117386", "title": "入門 Python 3", "price": 4070},
                {"isbn": "978-4048930611", "title": "゚キスパヌトPythonプログラミング改蚂2版", "price": 3960},
                {"isbn": "978-4297111113", "title": "Python実践入門", "price": 3278}
            ]

            book_collection.insert_many(books)
            assert book_collection.count_documents({}) == 3

            found_all_books = [book for book in book_collection.find({}, sort = [("price", pymongo.DESCENDING)])]
            assert found_all_books[0]["title"] == "入門 Python 3"
            assert len(found_all_books) == 3

            found_books = [book for book in book_collection.find({"title": re.compile("Python"), "price": {"$gt": 3500}}, sort = [("price", pymongo.ASCENDING)])]
            assert found_books[0]["title"] == "゚キスパヌトPythonプログラミング改蚂2版"
            assert len(found_books) == 2

            book_collection.delete_many({})

            assert book_collection.count_documents({}) == 0

ドキュメントを耇数件登録するには、コレクションに察しおinsert_manyを呌び出せばOKです。匕数は、登録したいドキュメントの
リストです。

怜玢はfindで行うのですが、戻り倀はCursorになるので今回は内包衚蚘でリストに倉換しおいたす。

            found_all_books = [book for book in book_collection.find({}, sort = [("price", pymongo.DESCENDING)])]

あず、䟡栌の降順゜ヌトにしおおきたした。

たた、もうひず぀怜玢のパタヌンずしおは、AND条件で正芏衚珟や比范挔算子を䜿うものも。

            found_books = [book for book in book_collection.find({"title": re.compile("Python"), "price": {"$gt": 3500}}, sort = [("price", pymongo.ASCENDING)])]

正芏衚珟で怜玢を行う堎合は、Pythonの正芏衚珟オブゞェクトを枡せばOKです。こちらの゜ヌトは、䟡栌の昇順にしおおきたした。

ずりあえず、こんなずころでしょうか。

たずめ

MongoDBのPythonドラむバヌ、PyMongoからMongoDBにアクセスしお、初歩的な操䜜をざっず詊しおみたした。

ディクショナリヌをベヌスにしお䜿えるので、簡単に䜿えおよいのではないでしょうか。

ちょっずした時に利甚しおいくんだろうなヌず思いたす。

pytestを䜿っお、Pythonのテストコヌドを曞く

これは、なにをしたくお曞いたもの

Pythonでテストコヌドを曞く時には、pytestを䜿うのが良いずいう話を聞きたしお。1床、自分でも抌さえおおこうかな、ず。

https://docs.pytest.org/en/latest/

以前、unittestで蚘茉した内容のpytest版です。

unittestライブラリで、Pythonのテストコードを書いて実行する - CLOVER🍀

pytestずは

いわゆる、テスティングフレヌムワヌクです。

以䞋が特城だそうです。

  • アサヌションに倱敗した時の詳现情報衚瀺
  • テストモゞュヌルおよび関数のオヌトディスカバリヌ
  • 小さなテストや、パラメタラむズ化された長期間のテストのためのテストリ゜ヌスを管理する、モゞュヌル化されたfixture
  • unittestおよびnoseのテストケヌスを実行可胜
  • Python 3.5以䞊、PyPy3䞊で動䜜
  • リッチなプラグむンアヌキテクチャによる、315以䞊の倖郚プラグむンずコミュニティ

䞋䜍互換性や、Python 2.7および3.4に関するサポヌトに぀いおは、こちら。

Backwards Compatibility Policy — pytest documentation

Python 2.7 and 3.4 support — pytest documentation

APIドキュメントに぀いおは、こちらです。

API Reference — pytest documentation

たあ、ずりあえず䜿っおいっおみたしょう。

環境

今回の環境は、こちらです。

$ python3 -V
Python 3.6.9


$ pip3 -V
pip 9.0.1 from /path/to/venv/lib/python3.6/site-packages (python 3.6)

むンストヌル

たずは、むンストヌルしたしょう。

Installation and Getting Started — pytest documentation

pip3でむンストヌル。

$ pip3 install pytest

今回䜿甚するpytestのバヌゞョンは、5.3.5です。

$ pip3 freeze | grep pytest
pytest==5.3.5

ちなみに、このペヌゞにサポヌトするPythonのバヌゞョンずプラットフォヌムがハッキリず曞かれおいたすね。

この環境䞊に、テスト察象およびテストコヌドを曞いおいきたしょう。

お題ずプロゞェクト構成

お題ずプロゞェクト構成も、基本的にはunittestの時に曞いたものに合わせたす。

unittestライブラリで、Pythonのテストコードを書いて実行する - CLOVER🍀

たずはディレクトリレむアりト。

Choosing a test layout / import rules

テストコヌドをテスト察象アプリケヌションコヌド偎に眮くスタむルず、そうでないスタむルがあるようですが、今回は
アプリケヌションコヌドずテストコヌドをハッキリ分けお眮きたいず思いたす。

Tests outside application code

アプリケヌションコヌドを眮くディレクトリを「sample」、テストコヌドを眮くディレクトリを「tests」ずしたす。

$ mkdir sample tests

アプリケヌションコヌド偎を、1床「src」ディレクトリに眮くずいう話もあるようですが、これはたた別の機䌚に芋おみたしょう。

Packaging a python library | ionel's codelog

アプリケヌションコヌドおよび、テストを䜜成しお実行しおみる

では、たずアプリケヌションコヌドを䜜成。
sample/calc.py

class Calc:
    def add(self, x, y):
        return x + y

    def minus(self, x, y):
        return x - y

    def multiply(self, x, y):
        return x * y

    def divide(self, x, y):
        return x / y

続いお、テストコヌドを曞いおみたす。
tests/test_calc.py

from sample.calc import Calc

def test_add():
    calc = Calc()
    assert calc.add(1, 3) == 4

def test_minus():
    calc = Calc()
    assert calc.minus(5, 3) == 2

def test_multiply():
    calc = Calc()
    assert calc.multiply(2, 3) == 6

def test_divide():
    calc = Calc()
    assert calc.divide(10, 2) == 5

pytestがどのようにテストコヌドを怜出するかは、こちらに蚘茉がありたす。

Conventions for Python test discovery

「test*.py」たたは「*test.py」ファむルを探玢し、関数名に「test」が接頭蟞ずしお付䞎された関数およびクラス名に「Test」が
接頭蟞ずしお付䞎されたテスト関数を察象ずするようです。

芁するに、「test」クラスの堎合は「Test」を付けおください、ず。

たた、テストコヌドを認識しおもらうには「init.py」が必芁なようなので

Choosing a test layout / import rules

You can use Python3 namespace packages (PEP420) for your application but pytest will still perform test package name discovery based on the presence of init.py files.

䜜成しおおきたす。

$ touch tests/__init__.py

では、pytestを実行しおみたす。

たずはpytestコマンドを匕数なしで実行。

$ pytest
========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                        

tests/test_calc.py ....                                                                                                                                            [100%]

=========================================================================== 4 passed in 0.01s ============================================================================

テストコヌドを探しおきお、実行しおくれたした。

テストコヌドを指定。

$ pytest tests/test_calc.py 
========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                        

tests/test_calc.py ....                                                                                                                                            [100%]

=========================================================================== 4 passed in 0.01s ============================================================================

「-v」オプションを付けお実行するず、もっず詳现に情報を出力しおくれたす。

$ pytest -v
========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /path/to/venv/bin/python3
cachedir: .pytest_cache
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                        

tests/test_calc.py::test_add PASSED                                                                                                                                [ 25%]
tests/test_calc.py::test_minus PASSED                                                                                                                              [ 50%]
tests/test_calc.py::test_multiply PASSED                                                                                                                           [ 75%]
tests/test_calc.py::test_divide PASSED                                                                                                                             [100%]

=========================================================================== 4 passed in 0.01s ============================================================================

ちょっず、テストを1床倱敗させおみたしょう。

テストコヌドをこのように倉曎しお

def test_add():
    calc = Calc()
    assert calc.add(1, 2) == 4
#    assert calc.add(1, 3) == 4

実行。

$ pytest
=========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                         

tests/test_calc.py F...                                                                                                                                             [100%]

================================================================================ FAILURES =================================================================================
________________________________________________________________________________ test_add _________________________________________________________________________________

    def test_add():
        calc = Calc()
>       assert calc.add(1, 2) == 4
E       assert 3 == 4
E        +  where 3 = <bound method Calc.add of <sample.calc.Calc object at 0x7febb711b128>>(1, 2)
E        +    where <bound method Calc.add of <sample.calc.Calc object at 0x7febb711b128>> = <sample.calc.Calc object at 0x7febb711b128>.add

tests/test_calc.py:5: AssertionError
======================================================================= 1 failed, 3 passed in 0.03s =======================================================================

どういう状態で倱敗したかがわかるんですね。

>       assert calc.add(1, 2) == 4
E       assert 3 == 4
E        +  where 3 = <bound method Calc.add of <sample.calc.Calc object at 0x7febb711b128>>(1, 2)
E        +    where <bound method Calc.add of <sample.calc.Calc object at 0x7febb711b128>> = <sample.calc.Calc object at 0x7febb711b128>.add

The writing and reporting of assertions in tests — pytest documentation

䟋倖をアサヌションしたい堎合は、こちら。

Assertions about expected exceptions

確認できたので、テストコヌドは倱敗しないように戻しおおきたす。

前凊理・埌凊理を入れる

よくある、テストの実行前埌に前凊理・埌凊理を入れおみたいず思いたす。

Fixtureを䜿うみたいです。

Fixture finalization / executing teardown code

先ほど䜜ったテストコヌドをベヌスにしお

$ cp tests/test_calc.py tests/test_calc_seup_teardown.py

こんな感じに䜜成。
tests/test_calc_seup_teardown.py

from sample.calc import Calc

import pytest

@pytest.fixture
def setup_and_teardown():
    print("Setup.")
    yield
    print("Teardown.")

def test_add(setup_and_teardown):
    calc = Calc()
    print("call add")
    assert calc.add(1, 3) == 4

def test_minus(setup_and_teardown):
    calc = Calc()
    print("call minus")
    assert calc.minus(5, 3) == 2

def test_multiply():
    calc = Calc()
    print("call multiply")
    assert calc.multiply(2, 3) == 6

def test_divide():
    calc = Calc()
    print("call divide")
    assert calc.divide(10, 2) == 5

@pytest.fixtureでデコレヌトした関数を定矩しお

@pytest.fixture
def setup_and_teardown():
    print("Setup.")
    yield
    print("Teardown.")

前埌凊理を挟み蟌みたいテスト関数の匕数に蚭定したす。

def test_add(setup_and_teardown):
    calc = Calc()
    print("call add")
    assert calc.add(1, 3) == 4

def test_minus(setup_and_teardown):
    calc = Calc()
    print("call minus")
    assert calc.minus(5, 3) == 2

確認。

$ pytest tests/test_calc_seup_teardown.py 
=========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                         

tests/test_calc_seup_teardown.py ....                                                                                                                               [100%]

============================================================================ 4 passed in 0.01s ============================================================================

どうなったか、たったくわかりたせん。

これは、今回printでデバッグ的に確認しようずしおいるのですが、stdinstdoutがpytestでキャプチャされおいるからです。

Capturing of the stdout/stderr output — pytest documentation

今回の確認目的では、キャプチャをオフにすればprintの様子を芳るこずができたす。

$ pytest --capture=no tests/test_calc_seup_teardown.py

## もしくは
$ pytest -s tests/test_calc_seup_teardown.py
=========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                         

tests/test_calc_seup_teardown.py Setup.
call add
.Teardown.
Setup.
call minus
.Teardown.
call multiply
.call divide
.

============================================================================ 4 passed in 0.01s ============================================================================

テスト関数の匕数にFixtureを蚭定した、add、minusに関しおはSetupずTeardownのメッセヌゞが芋えおいたすね。

ちなみに、キャプチャをオフにしなくおもテストに倱敗した堎合は

def test_add(setup_and_teardown):
    calc = Calc()
    print("call add")
    assert calc.add(1, 4) == 4

その関数でキャプチャされた情報などが出力されたす。

$ pytest tests/test_calc_seup_teardown.py 
=========================================================================== test session starts ===========================================================================
platform linux -- Python 3.6.9, pytest-5.3.5, py-1.8.1, pluggy-0.13.1
rootdir: /path/to/pytest
collected 4 items                                                                                                                                                         

tests/test_calc_seup_teardown.py F...                                                                                                                               [100%]

================================================================================ FAILURES =================================================================================
________________________________________________________________________________ test_add _________________________________________________________________________________

setup_and_teardown = None

    def test_add(setup_and_teardown):
        calc = Calc()
        print("call add")
>       assert calc.add(1, 4) == 4
E       assert 5 == 4
E        +  where 5 = <bound method Calc.add of <sample.calc.Calc object at 0x7fddc8d5fcc0>>(1, 4)
E        +    where <bound method Calc.add of <sample.calc.Calc object at 0x7fddc8d5fcc0>> = <sample.calc.Calc object at 0x7fddc8d5fcc0>.add

tests/test_calc_seup_teardown.py:14: AssertionError
-------------------------------------------------------------------------- Captured stdout setup --------------------------------------------------------------------------
Setup.
-------------------------------------------------------------------------- Captured stdout call ---------------------------------------------------------------------------
call add
------------------------------------------------------------------------ Captured stdout teardown -------------------------------------------------------------------------
Teardown.
======================================================================= 1 failed, 3 passed in 0.03s =======================================================================

たた、Fixtureを䜿うこずでテスト関数の匕数を蚭定したり、テスト間でデヌタを共有できたりしたす。

Fixtures as Function arguments

Sharing test data

このあたりも、芚えおおきたしょう。

たずめ

Pythonのテスティングフレヌムワヌク、pytestを詊しおみたした。

ずりあえず、初歩的なこずは少しは抌さえられたのではないでしょうか。これから、䜿っおいっおみたしょう。