web-dev-qa-db-fra.com

ResourceWarning unclosed socket dans le test unitaire Python 3

Je modifie un code pour qu'il soit compatible entre Python 2 et Python 3, mais j'ai observé un avertissement dans la sortie du test unitaire.

/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)

Un peu de recherche a déterminé que cela se produisait également à partir de bibliothèques populaires telles que request et boto3 .

Je pourrais ignorer l'avertissement ou le filtrer complètement. Si c'était mon service, je pourrais définir l'en-tête connection: close dans ma réponse ( link ).

Voici un exemple présentant l'avertissement dans 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 __== '__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 __== '__main__':
    unittest.main()

Existe-t-il un moyen meilleur/correct de gérer la session de sorte qu'elle soit explicitement fermée et de ne pas compter sur __del__() pour générer ce type d'avertissement.

Merci pour toute aide.

10
j12y

Avoir la logique de démontage dans __del__ peut rendre votre programme incorrect ou plus difficile à raisonner, car il n'y a aucune garantie quant au moment où cette méthode sera appelée, ce qui peut potentiellement conduire à l'avertissement que vous avez reçu. Il y a deux façons de résoudre ce problème:

1) Exposer une méthode pour fermer la session et l'appeler dans le test tearDown

unittest 's tearDown method vous permet de définir du code qui sera exécuté après chaque test. L'utilisation de ce hook pour fermer la session fonctionnera même si le test échoue ou comporte une exception, ce qui est Nice.

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 __== '__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 __== '__main__':
    unittest.main()

2) Utiliser un gestionnaire de contexte

Un gestionnaire de contexte est également un moyen très utile de définir explicitement la portée de quelque chose. Dans l'exemple précédent, vous devez vous assurer que .close() est appelé correctement sur chaque site d'appel, sinon vos ressources risquent de fuir. Avec un gestionnaire de contexte, cela est géré automatiquement même s'il existe une exception dans l'étendue du gestionnaire de contexte.

En vous basant sur la solution 1), vous pouvez définir des méthodes magiques supplémentaires (__enter__ et __exit__) afin que votre classe fonctionne avec l'instruction with.

Remarque: ce qui est bien ici, c'est que ce code prend également en charge l'utilisation de la solution 1), avec la fonction explicite .close(), ce qui peut être utile si un gestionnaire de contexte était gênant pour une raison quelconque.

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 __== '__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 __== '__main__':
    unittest.main()

En fonction de vos besoins, vous pouvez utiliser l'un ou l'autre ou une combinaison de setUp/tearDown et du gestionnaire de contexte, et vous débarrasser de cet avertissement, sans oublier une gestion plus explicite des ressources dans votre code!

1
Samuel Dion-Girardeau