Criei um módulo "persist" contendo uma função obj_pickle que estou usando para conservar vários objetos no meu projeto. Funciona bem, mas está falhando no pytest, retornando:
> pickle.dump(obj, file_handle, protocol=protocol)
E AttributeError: Can't pickle local object 'test_object.<locals>.TestObject'
Correndo:
- python 3.12
- Versão do pytest: 8.3.5
Vejo que há problemas semelhantes relacionados ao multiprocessamento, mas não acho que o PyTest esteja expondo esse problema. Código abaixo, e muito obrigado.
# lib.persist.py
from pathlib import Path
import pickle
def obj_pickle(obj: object, dir:Path, protocol: int = pickle.HIGHEST_PROTOCOL) -> None:
"""
Pickle an object to a byte file.
"""
if not dir.exists():
dir.mkdir(parents=True, exist_ok=True)
path = Path(dir, obj.instance_name + '.pkl')
with open(path, "wb") as file_handle:
pickle.dump(obj, file_handle, protocol=protocol)
print(f"{obj.__class__.__name__} object {obj.instance_name} saved to {path}")
# tests.test_persist.py
from pathlib import Path
import pytest
from lib.persist import obj_pickle
TEST_DIR = Path("test_dir")
@pytest.fixture
def test_object():
class TestObject():
def __init__(self, instance_file_name):
self.instance_file_name = instance_file_name
self.data = "This is a test object."
test_object = TestObject("test_object_file")
return test_object
def test_obj_pickle(test_object):
obj_pickle(test_object, Path(TEST_DIR))
path = Path(TEST_DIR, "test_object_file" + ".pkl")
assert path.exists()
Isso não tem nada a ver com o pytest.
Quando você carrega um objeto de um pickle, o
pickle
mecanismo precisa saber qual deve ser a classe desse objeto. Mas se você definir uma classe dentro de uma função, cada execução dessa função gera uma classe totalmente nova.pickle
não tem como dizer qual versão da classe usar, se é que existe alguma versão na sessão Python em que você está carregando o pickle.Em vez de tentar fazê-lo funcionar e criar uma pilha de inconsistências estranhas e casos extremos,
pickle
ele simplesmente não oferece suporte à decapagem de instâncias de classes como essa.Você precisa mover sua definição de classe para fora da função em que ela está:
O pacote pickle essencialmente faz a pickle de um objeto, salvando-o como ele é em relação ao módulo atual. Se essa relação relativa não for definida, a pickle falhará. A maneira como o pytest é executado remove essa relação relativa, pois o pickle espera recebê-la. Aqui estão 4 soluções que você pode tentar para que os testes funcionem:
1: Basta omitir a linha que está causando o problema ao executar o pytest. Você pode fazer isso com uma instrução if do tipo:
2: Você pode codificar o caminho relativo do seu objeto para a existência no nível do módulo. Isso é um pouco ruim e pode ser bastante instável, mas funciona se o seu objetivo for apenas executar o pytest durante o pickle. Isso provavelmente fará com que o pickle seja descompactado diretamente no módulo que o carrega (dependendo das versões do pacote e do interpretador do pickle) em vez de onde você esperaria, o que também pode criar colisões de namespace (especialmente porque o nome do seu objeto é "obj"). Modifiquei sua função para fazer isso:
3: Você pode combinar as duas sugestões anteriores para que a codificação do objeto no módulo SOMENTE aconteça durante o uso do pytest e os casos de uso normais de produção operem normalmente (não sofra com a instabilidade de forçar o objeto na função principal). Em seguida, você precisará recarregar o que estava na variável obj antes. Modifiquei sua função para fazer isso:
4: Considere os pacotes
marshal
oudill
, eles também serializam objetos, mas têm dependências muito diferentes do pickle e podem oferecer melhor compatibilidade com o pytest.Espero que tudo isso ajude!