null

Ищем потерявшееся содержимое одного из CSV файлов в tar архиве, сгенерированным с помощью Python

В моей предыдущей статье про генерацию tar архива с двумя CSV я сразу показал правильный код для решения задачи. Здесь же я хочу рассказать про то, как я наткнулся на одну замечательную прикрытую листвой граблю. Повторю свою задачу: нужно сгенерировать архив с двумя CSV файлами. CSV заполнялись данными из БД. Попросили, сделали. Я быстренько нашёл на stackoverflow пример того как создать tar.gz архив и добавить туда файл. Сделал временный именованный файл и заполнил его с помощью csv writer'а. На выходе получился примерно такой код:

import csv
import tarfile
from tempfile import TemporaryFile, NamedTemporaryFile
from io import TextIOWrapper

with tarfile.open('/tmp/test.tar', mode='w:gz') as tar:
    with NamedTemporaryFile() as csv1:
        with TextIOWrapper(csv1, encoding='utf-8', newline='') as t1:
            out2 = csv.writer(t1, quoting=csv.QUOTE_ALL)
            out2.writerow(['Column1', 'Column2'])
            //Заполняем CSV данными из БД
            csv1.flush()
            tar.add(csv1.name, "csv1.csv")
    with NamedTemporaryFile() as csv1:
        with TextIOWrapper(csv2, encoding='utf-8', newline='') as t2:
            out2 = csv.writer(t2, quoting=csv.QUOTE_ALL)
            out2.writerow(['Column1', 'Column2'])
            //Заполняем CSV данными из БД
            csv2.flush()
            tar.add(csv2.name, "csv2.csv")


Всё было хорошо. Код выполнялся, архив создавался. Но, как оказалось, после извлечения содержимого из архива, один из двух csv файлов был пустым. Я сначало начал грешить на свои запросы к БД. Подумал, что написал неправильное обращение за данными и мне просто возвращается пустой список объектов. Но, во-первых, запрос оказался верным, во-вторых, у меня же пишется константный заголовок таблицы в коде. Значит содержимое файло пропадает где-то по пути при сохранении. Не долго думая, я решил, что у меня не хватает где-то закрытия файла, или же вызова flush. Но код для двух CSV был идентичен. И я просто не мог понять. Как же так. Почему один файл в архив попадает, а другой пустой?

Я решил, что там какая-то интересная магия и именно первый файл, который мы будем писать в архив окажется пустым. Поменял CSV местами. Оказалось, что порядок не важен. Конкретный файл не хочет писаться в архив. Но ведь вызовы writerow срабатывают. И в цикл он заходит. Вера в магию приходила и не отпускала. Я решил, что нужно проверить создание архива из двух уже существующих на диске CSV. Написал пример и всё работает. Окей, что тогда не так у меня? Что с генерацией CSV из временных файлов? Убрал совсем заполнение данными из БД. Оставил только по одному вызову writerow с захардкоженным списком.

То, что я увидел при запуске этого кода меня осенило. Оба файла в архиве были пусты. Я сразу понял, что проблема во flush. Один файл в моем исходном примере был непустым, т.к. он был очень большим. Данные переполняли буфер и всё сбрасывалось в файл. Теперь я уже четко понимал, что нет магие в одном участке кода. В обоих не работает вызов строки csv2.flush(). Но просто для большого файла flush выполнялся принудительно. Далее мне понадобилось всего несколько минут, чтобы узнать, что TextIOWrapper, который я использовал для того чтобы писать тестовые данные в NamedTemproraryFule, является буфферизированным потоком. Соотсветственно, когда я делал flush csv файла, то данные лежали в буффере внутри переменных t1 и t2. И flush нужно было вызывать именно у них. Полученно знание позволило исправить код. Он выглядел следующим образом:

import csv
import tarfile
from tempfile import TemporaryFile, NamedTemporaryFile
from io import TextIOWrapper

with tarfile.open('/tmp/test.tar', mode='w:gz') as tar:
    with NamedTemporaryFile() as csv1:
        with TextIOWrapper(csv1, encoding='utf-8', newline='') as t1:
            out2 = csv.writer(t1, quoting=csv.QUOTE_ALL)
            out2.writerow(['Column1', 'Column2'])
            //Заполняем CSV данными из БД
            t1.flush()
            tar.add(csv1.name, "csv1.csv")
    with NamedTemporaryFile() as csv1:
        with TextIOWrapper(csv2, encoding='utf-8', newline='') as t2:
            out2 = csv.writer(t2, quoting=csv.QUOTE_ALL)
            out2.writerow(['Column1', 'Column2'])
            //Заполняем CSV данными из БД
            t2.flush()
            tar.add(csv2.name, "csv2.csv")

Код выше работает без ошибок. Но при этом его можно сократить, отказавшись от использования TextIOWrapper. Для этого необходимо при открытии временного файла указать mode = 'w'.  Итоговую версию кода можно поглядеть в этой заметке. На этом, пожалуй, откланяюсь. Не допускайте глупых ошибок и не верьте в магию!