ResourceWarning закрытого сокета в Python 3 Unit Test

Я изменяю некоторый код, чтобы он был совместим между Python 2 и Python 3, но в выводе модульного теста появилось предупреждение.

/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/unittest/case.py:601:
    ResourceWarning: unclosed socket.socket fd=4,
    family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=6,
    laddr=('1.1.2.3', 65087), raddr=('5.8.13.21', 8080)

Небольшое исследование показало, что это также происходит из популярных библиотек, таких как запросы и boto3.

Я мог проигнорировать предупреждение или отфильтровать его полностью. Если бы был мой сервис, я мог бы установить connection: close заголовок в моем ответе (ссылка).

Вот пример, который демонстрирует предупреждение в Python 3.6.1:

app.py

import requests

class Service(object):
    def __init__(self):
        self.session = requests.Session()

    def get_info(self):
        uri = 'http://api.stackexchange.com/2.2/info?site=stackoverflow'
        response = self.session.get(uri)
        if response.status_code == 200:
            return response.json()
        else:
            response.raise_for_status()

    def __del__(self):
        self.session.close()

if __name__ == '__main__':
    service = Service()
    print(service.get_info())

test.py

import unittest

class TestService(unittest.TestCase):
    def test_growing(self):
        import app
        service = app.Service()
        res = service.get_info()
        self.assertTrue(res['items'][0]['new_active_users'] > 1)


if __name__ == '__main__':
    unittest.main()

Существует ли лучший/правильный способ управления сеансом, чтобы он был закрыт в явном виде и не полагался на __del__() для получения такого рода предупреждений.

Спасибо за любую помощь.

Ответы

Ответ 1

Наличие логики разрыва в __del__ может сделать вашу программу неправильной или трудной для рассуждения, потому что нет гарантии того, когда этот метод будет вызван, что может привести к полученному вами предупреждению. Есть несколько способов решить эту проблему:

1) Выставьте метод, чтобы закрыть сеанс, и вызовите его в тесте tearDown

unittest tearDown позволяет вам определить код, который будет запускаться после каждого теста. Использование этого хука для закрытия сеанса будет работать, даже если тест не пройден или есть исключение, что хорошо.

app.py

import requests

class Service(object):

    def __init__(self):
        self.session = requests.Session()

    def get_info(self):
        uri = 'http://api.stackexchange.com/2.2/info?site=stackoverflow'
        response = self.session.get(uri)
        if response.status_code == 200:
            return response.json()
        else:
            response.raise_for_status()

    def close(self):
        self.session.close()

if __name__ == '__main__':
    service = Service()
    print(service.get_info())
    service.close()

test.py

import unittest
import app

class TestService(unittest.TestCase):

    def setUp(self):
        self.service = app.Service()
        super().setUp()

    def tearDown(self):
        self.service.close()

    def test_growing(self):
        res = self.service.get_info()
        self.assertTrue(res['items'][0]['new_active_users'] > 1)

if __name__ == '__main__':
    unittest.main()

2) Используйте контекстный менеджер

Диспетчер контекста также является очень полезным способом явно определить область действия чего-либо. В предыдущем примере вы должны убедиться, что .close() вызывается правильно на каждом сайте вызовов, иначе ваши ресурсы будут просачиваться. С помощью диспетчера контекста это обрабатывается автоматически, даже если есть исключение в рамках диспетчера контекста.

Основываясь на решении 1), вы можете определить дополнительные магические методы (__enter__ и __exit__), чтобы ваш класс работал with оператором with.

Примечание: здесь хорошо то, что этот код также поддерживает использование в решении 1) с явным .close(), что может быть полезно, если менеджер контекста был неудобен по какой-то причине.

app.py

import requests

class Service(object):

    def __init__(self):
        self.session = requests.Session()

    def __enter__(self):
        return self

    def get_info(self):
        uri = 'http://api.stackexchange.com/2.2/info?site=stackoverflow'
        response = self.session.get(uri)
        if response.status_code == 200:
            return response.json()
        else:
            response.raise_for_status()

    def close(self):
        self.session.close()

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()

if __name__ == '__main__':
    with Service() as service:
        print(service.get_info())

test.py

import unittest

import app

class TestService(unittest.TestCase):

    def test_growing(self):
        with app.Service() as service:
            res = service.get_info()
        self.assertTrue(res['items'][0]['new_active_users'] > 1)

if __name__ == '__main__':
    unittest.main()

В зависимости от того, что вам нужно, вы можете использовать либо комбинацию setUp/tearDown и менеджера контекста, либо избавиться от этого предупреждения, а также иметь более четкое управление ресурсами в своем коде!

Ответ 2

Это лучшее решение, если вас не очень волнуют предупреждения

Просто импортируйте предупреждения и добавьте эту строку, где ваш драйвер инициирует -

warnings.filterwarnings(action="ignore", message="unclosed", category=ResourceWarning)