Repository: scrapy/scrapy

Issue:
S3 CsvItemExporter read of closed file error
### Description

Unable to use `batch_item_count` in s3 to export feeds using the `CsvItemExporter`. Could this be related to #4830? I've tried and succeeded in exporting the same feed to my local system, and also changing the format from `csv` to `json` and exporting to s3.

### Steps to Reproduce

1. Using the feed config of 
```
"FEEDS": {
            "s3://bucket/%(name)s/%(batch_time)s.tsv": {
                "format": "csv",
                "batch_item_count": 10,
                "item_export_kwargs": {"delimiter": "\t"},
            }
        }
```

2. Scrapy successfully parses data but has trouble exporting it to s3. However I'm not sure if this is an issue with `botocore` or `scrapy`.
`botocore==1.20.29` however I downgraded a few releases with no change in the issue.

**Expected behavior:** Produces the error below.

**Actual behavior:** Only successfully exports the last chunk that was processed.

**Reproduces how often:** 100%

### Versions

```
Scrapy       : 2.4.1
lxml         : 4.6.2.0
libxml2      : 2.9.10
cssselect    : 1.1.0
parsel       : 1.6.0
w3lib        : 1.22.0
Twisted      : 21.2.0
Python       : 3.8.5 (default, Aug 11 2020, 11:08:40) - [Clang 11.0.3 (clang-1103.0.32.62)]
pyOpenSSL    : 20.0.1 (OpenSSL 1.1.1i  8 Dec 2020)
cryptography : 3.3.1
Platform     : macOS-10.15.7-x86_64-i386-64bit
```

### Additional context

```
2021-03-16 15:19:17 [scrapy.extensions.feedexport] ERROR: Error storing csv feed (10 items) in: s3://bucket/bucket-file.tsv
Traceback (most recent call last):
  File ".venv/lib/python3.8/site-packages/botocore/httpsession.py", line 314, in send
    urllib_response = conn.urlopen(
  File ".venv/lib/python3.8/site-packages/urllib3/connectionpool.py", line 699, in urlopen
    httplib_response = self._make_request(
  File ".venv/lib/python3.8/site-packages/urllib3/connectionpool.py", line 394, in _make_request
    conn.request(method, url, **httplib_request_kw)
  File ".venv/lib/python3.8/site-packages/urllib3/connection.py", line 234, in request
    super(HTTPConnection, self).request(method, url, body=body, headers=headers)
  File ".pyenv/versions/3.8.5/lib/python3.8/http/client.py", line 1255, in request
    self._send_request(method, url, body, headers, encode_chunked)
  File ".venv/lib/python3.8/site-packages/botocore/awsrequest.py", line 92, in _send_request
    rval = super(AWSConnection, self)._send_request(
  File ".pyenv/versions/3.8.5/lib/python3.8/http/client.py", line 1301, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
  File ".pyenv/versions/3.8.5/lib/python3.8/http/client.py", line 1250, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
  File ".venv/lib/python3.8/site-packages/botocore/awsrequest.py", line 127, in _send_output
    self._handle_expect_response(message_body)
  File ".venv/lib/python3.8/site-packages/botocore/awsrequest.py", line 170, in _handle_expect_response
    self._send_message_body(message_body)
  File ".venv/lib/python3.8/site-packages/botocore/awsrequest.py", line 197, in _send_message_body
    self.send(message_body)
  File ".venv/lib/python3.8/site-packages/botocore/awsrequest.py", line 204, in send
    return super(AWSConnection, self).send(str)
  File ".pyenv/versions/3.8.5/lib/python3.8/http/client.py", line 963, in send
    datablock = data.read(self.blocksize)
  File ".pyenv/versions/3.8.5/lib/python3.8/tempfile.py", line 474, in func_wrapper
    return func(*args, **kwargs)
ValueError: read of closed file

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".venv/lib/python3.8/site-packages/twisted/python/threadpool.py", line 238, in inContext
    result = inContext.theWork()  # type: ignore[attr-defined]
  File ".venv/lib/python3.8/site-packages/twisted/python/threadpool.py", line 254, in <lambda>
    inContext.theWork = lambda: context.call(  # type: ignore[attr-defined]
  File ".venv/lib/python3.8/site-packages/twisted/python/context.py", line 118, in callWithContext
    return self.currentContext().callWithContext(ctx, func, *args, **kw)
  File ".venv/lib/python3.8/site-packages/twisted/python/context.py", line 83, in callWithContext
    return func(*args, **kw)
  File ".venv/lib/python3.8/site-packages/scrapy/extensions/feedexport.py", line 155, in _store_in_thread
    self.s3_client.put_object(
  File ".venv/lib/python3.8/site-packages/botocore/client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File ".venv/lib/python3.8/site-packages/botocore/client.py", line 662, in _make_api_call
    http, parsed_response = self._make_request(
  File ".venv/lib/python3.8/site-packages/botocore/client.py", line 682, in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
  File ".venv/lib/python3.8/site-packages/botocore/endpoint.py", line 102, in make_request
    return self._send_request(request_dict, operation_model)
  File ".venv/lib/python3.8/site-packages/botocore/endpoint.py", line 136, in _send_request
    while self._needs_retry(attempts, operation_model, request_dict,
  File ".venv/lib/python3.8/site-packages/botocore/endpoint.py", line 253, in _needs_retry
    responses = self._event_emitter.emit(
  File ".venv/lib/python3.8/site-packages/botocore/hooks.py", line 356, in emit
    return self._emitter.emit(aliased_event_name, **kwargs)
  File ".venv/lib/python3.8/site-packages/botocore/hooks.py", line 228, in emit
    return self._emit(event_name, kwargs)
  File ".venv/lib/python3.8/site-packages/botocore/hooks.py", line 211, in _emit
    response = handler(**kwargs)
  File ".venv/lib/python3.8/site-packages/botocore/retryhandler.py", line 183, in __call__
    if self._checker(attempts, response, caught_exception):
  File ".venv/lib/python3.8/site-packages/botocore/retryhandler.py", line 250, in __call__
    should_retry = self._should_retry(attempt_number, response,
  File ".venv/lib/python3.8/site-packages/botocore/retryhandler.py", line 269, in _should_retry
    return self._checker(attempt_number, response, caught_exception)
  File ".venv/lib/python3.8/site-packages/botocore/retryhandler.py", line 316, in __call__
    checker_response = checker(attempt_number, response,
  File ".venv/lib/python3.8/site-packages/botocore/retryhandler.py", line 222, in __call__
    return self._check_caught_exception(
  File ".venv/lib/python3.8/site-packages/botocore/retryhandler.py", line 359, in _check_caught_exception
    raise caught_exception
  File ".venv/lib/python3.8/site-packages/botocore/endpoint.py", line 200, in _do_get_response
    http_response = self._send(request)
  File ".venv/lib/python3.8/site-packages/botocore/endpoint.py", line 269, in _send
    return self.http_session.send(request)
  File ".venv/lib/python3.8/site-packages/botocore/httpsession.py", line 359, in send
    raise HTTPClientError(error=e)
botocore.exceptions.HTTPClientError: An HTTP Client raised an unhandled exception: read of closed file

```



Discussion:
https://github.com/scrapy/scrapy/pull/4830 was really a documentation issue, so providing you are not using a custom exporter or something in the lines, this looks more like a bug in either the CSV exporter or the handling of batch exports, maybe specific to storages like S3, which first write to a temporary file and then upload that file.
Hello! @mmitropoulou and I would like to contribute to this issue.
@Gallaecio @mmaybeno Hello!
I have reproduced the issue and what Scrapy exports, is not the last chunk, but all the elements, that should be exported (in multiple files), in one .csv file. And no exceptions at all.

Versions
```
Package                       Version
----------------------------- --------------------
botocore                      1.20.62
cryptography                  3.3.2
cssselect                     1.1.0
lxml                          4.6.3
parsel                        1.6.0
pyOpenSSL                     20.0.1
Python 			      3.9.1
Scrapy                        2.5.0
Twisted                       21.2.0
w3lib                         1.22.0
Platform 		      Windows 10 Home - 64 bit
```
Is it a problem that I am working on Windows?
Interesting. I wonder if something in the csv reader/writer made changes in python 3.9 which fixed the issue.
Well, the export is still implemented wrong. Should I continue looking for the reason only one file is exported, instead of multiple? @Gallaecio 
Hello again @mmaybeno!
I think I just reproduced the issue in the correct way.

**settings.py**
```
FEEDS = {
        's3://mybucket/folder1/%(batch_id)d-filename.csv': {
            'format': 'csv',
            'batch_item_count': 50,
            'item_export_kwargs': {"delimiter": "\t"},
        },
        '%(name)s/%(batch_time)s.csv': {
            'format': 'csv',
            'batch_item_count': 50,
            'item_export_kwargs': {"delimiter": "\t"},
        },
}
```
And the **output** is:
```
 'feedexport/failed_count/S3FeedStorage': 4,
 'feedexport/success_count/FileFeedStorage': 5,
 'feedexport/success_count/S3FeedStorage': 1,
```
while it works fine locally, in s3 only the last chunk is exported.

**exceptions:**
```
Traceback (most recent call last):
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\awsrequest.py", line 525, in reset_stream
    self.body.seek(0)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\tempfile.py", line 474, in func_wrapper
    return func(*args, **kwargs)
ValueError: seek of closed file

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\threadpool.py", line 238, in inContext
    result = inContext.theWork()  # type: ignore[attr-defined]
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\threadpool.py", line 254, in <lambda>
    inContext.theWork = lambda: context.call(  # type: ignore[attr-defined]
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\context.py", line 118, in callWithContext
    return self.currentContext().callWithContext(ctx, func, *args, **kw)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\context.py", line 83, in callWithContext
    return func(*args, **kw)
  File "c:\users\hp\scrapy\scrapy\extensions\feedexport.py", line 155, in _store_in_thread
    self.s3_client.put_object(
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\client.py", line 357, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\client.py", line 662, in _make_api_call
    http, parsed_response = self._make_request(
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\client.py", line 682, in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\endpoint.py", line 102, in make_request
    return self._send_request(request_dict, operation_model)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\endpoint.py", line 143, in _send_request
    request.reset_stream()
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\awsrequest.py", line 528, in reset_stream
    raise UnseekableStreamError(stream_object=self.body)
botocore.exceptions.UnseekableStreamError: Need to rewind the stream <tempfile._TemporaryFileWrapper object at 0x00000157CE47BE80>, but stream is not seekable.
```

When exporting in 'json' format in s3, it works the way it should:  `'feedexport/success_count/S3FeedStorage': 5,`, no exceptions.

So, I think it's not a fault neither of [S3FeedStorage](https://github.com/scrapy/scrapy/blob/ee682af3b06d48815dbdaa27c1177b94aaf679e1/scrapy/extensions/feedexport.py#L119), nor  [FeedExporter](https://github.com/scrapy/scrapy/blob/ee682af3b06d48815dbdaa27c1177b94aaf679e1/scrapy/extensions/feedexport.py#L243). I suppose it is either of [CsvItemExporter](https://github.com/scrapy/scrapy/blob/ee682af3b06d48815dbdaa27c1177b94aaf679e1/scrapy/exporters.py#L196), or `botocore`.
Nice work @marlenachatzigrigoriou. Looks like you ruled out some of the possible options and confirmed it's probably either the `CsvItemExporter` or `botocore`. Hopefully it's not `botocore` since that codebase maybe be harder to fix.

For context, I ended up using the json exporter since I was able to use that anyway. But I am still interested in having this resolved for csv.
Thank you @mmaybeno. 
Unfortunately, I think it is neither a `CsvItemExporter` problem, as it exports correctly locally. 
I posted a question in botocore's Gitter asking if this issue could be a botocore one, but I got no reply. In addition, I thought about making an issue in botocore's repository to ask about the issue.

Considering I am new at Scrapy, these are only my assumptions. I am interested in going deeper into this issue, too, but I need some guidance.
Hello again!
Taking into consideration botocore's team answer, I focused my effort on lines 74, 75 of [feedexport.py](https://github.com/scrapy/scrapy/blob/master/scrapy/extensions/feedexport.py). 
Looking at the different implementations of the `store(self, file)` method in this file, studying the threads in Twisted, and trying many differentials, 

1. I tried the following:
```
def store(self, file):
        reactor.callInThread(self._store_in_thread, file)
        file.close()
```
which had as an **output**:
```
.
.
2021-06-23 18:16:46 [scrapy.extensions.feedexport] INFO: Stored csv feed (50 items) in: s3://mybucket/folder1/4-filename.csv
.
.
.
2021-06-23 18:16:47 [scrapy.extensions.feedexport] INFO: Stored csv feed (29 items) in: s3://mybucket/folder1/5-filename.csv
.
.
.
Unhandled Error
Traceback (most recent call last):
  File "c:\users\hp\appdata\local\programs\python\python39\lib\threading.py", line 954, in _bootstrap_inner
    self.run()
  File "c:\users\hp\appdata\local\programs\python\python39\lib\threading.py", line 892, in run
    self._target(*self._args, **self._kwargs)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\_threads\_threadworker.py", line 47, in work
    task()
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\_threads\_team.py", line 181, in doWork
    task()
--- <exception caught here> ---
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\threadpool.py", line 238, in inContext
    result = inContext.theWork()  # type: ignore[attr-defined]
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\threadpool.py", line 254, in <lambda>
    inContext.theWork = lambda: context.call(  # type: ignore[attr-defined]
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\context.py", line 118, in callWithContext
    return self.currentContext().callWithContext(ctx, func, *args, **kw)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\context.py", line 83, in callWithContext
    return func(*args, **kw)
  File "c:\users\hp\scrapy\scrapy\extensions\feedexport.py", line 167, in _store_in_thread
    file.seek(0)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\tempfile.py", line 474, in func_wrapper
    return func(*args, **kwargs)
builtins.ValueError: I/O operation on closed file
.
.
. 
'feedexport/success_count/S3FeedStorage': 5,
.
.
```
As you can see, the files seem to be created and saved, but when I check my bucket in the s3, again, only the last chunk is exported.
    
2. When not closing the file:
```
def store(self, file):
        reactor.callInThread(self._store_in_thread, file)
```
the difference in the **output** is:
```
2021-06-23 19:18:08 [twisted] CRITICAL: Unhandled Error
Traceback (most recent call last):
  File "c:\users\hp\appdata\local\programs\python\python39\lib\threading.py", line 954, in _bootstrap_inner
    self.run()
  File "c:\users\hp\appdata\local\programs\python\python39\lib\threading.py", line 892, in run
    self._target(*self._args, **self._kwargs)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\_threads\_threadworker.py", line 47, in work
    task()
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\_threads\_team.py", line 181, in doWork
    task()
--- <exception caught here> ---
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\threadpool.py", line 238, in inContext
    result = inContext.theWork()  # type: ignore[attr-defined]
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\threadpool.py", line 254, in <lambda>
    inContext.theWork = lambda: context.call(  # type: ignore[attr-defined]
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\context.py", line 118, in callWithContext
    return self.currentContext().callWithContext(ctx, func, *args, **kw)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\twisted\python\context.py", line 83, in callWithContext
    return func(*args, **kw)
  File "c:\users\hp\scrapy\scrapy\extensions\feedexport.py", line 157, in _store_in_thread
    self.s3_client.put_object(
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\client.py", line 386, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\client.py", line 691, in _make_api_call
    http, parsed_response = self._make_request(
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\client.py", line 711, in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\endpoint.py", line 102, in make_request
    return self._send_request(request_dict, operation_model)
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\endpoint.py", line 143, in _send_request
    request.reset_stream()
  File "c:\users\hp\appdata\local\programs\python\python39\lib\site-packages\botocore\awsrequest.py", line 528, in reset_stream
    raise UnseekableStreamError(stream_object=self.body)
botocore.exceptions.UnseekableStreamError: Need to rewind the stream <tempfile._TemporaryFileWrapper object at 0x00000107660A54F0>, but stream is not seekable.
```
and again, only the last chunk is exported.

3. Also, when changing the `return `to `yield`:
 ```
  def store(self, file):
        yield threads.deferToThread(self._store_in_thread, file)
```
there is no exception and `'feedexport/success_count/S3FeedStorage': 5`, but again, only the last chunk is exported.
Looks like great exploration into the internals. The comments from the boto team are helpful. Seeing that the `S3FeedStorage` method inherits from `BlockingFeedStorage`, you can see the method that does the writing: https://github.com/scrapy/scrapy/blob/016c7e92d1d2893e7a8ce61c7f2e76818e71d019/scrapy/extensions/feedexport.py#L152-L158. I wonder if scrapy is creating a temp file that is too generic, then each batch has the same name, effectively overwriting each other and then only the last batch is uploaded. This shouldn't happen since you are using `batch_id`, but worth a check. 

Maybe change `return NamedTemporaryFile(prefix='feed-', dir=path)` to `return NamedTemporaryFile(prefix='feed-', dir=path, delete=False)` so you can inspect the temp files.

https://github.com/scrapy/scrapy/blob/016c7e92d1d2893e7a8ce61c7f2e76818e71d019/scrapy/extensions/feedexport.py#L67-L72
Adding `return NamedTemporaryFile(prefix='feed-', dir=path, delete=False)` does not change the export, and as I use the FEEDS dir format (for the needs of this issue) in settings.py -instead of the old one- the 'FEED_TEMPDIR' is not working (or I don't know how to make it work), so I cannot inspect the temp files.
But, influenced by your comments, I run the spider and "cut" the running after some seconds, without allowing the spider to end the whole execution, and the second chunk was exported in my s3. So, I guess that somewhere overwriting does happen.
Maybe add a random string that you add to the `prefix`, basically something that will ensure the temporary file is unique.

`NamedTemporaryFile(prefix=f'feed-{random_bits_here}-', dir=path, delete=False)`
Yes, that works.
I changed the line into: `return NamedTemporaryFile(prefix=f'feed-{random.getrandbits(10)}-', dir=path, delete=False)`. It had as a result 5 different files with names similar to: "feed-124-x63cvlhk". 
But, what I introduced with the random bits (e.g. 124) does not change the export, as there were already unique names due to the sequence of "x63cvlhk".
So, I guess that is not a problem of overwriting.


Well I guess that is ruled out then :). Are you familiar with the debugger at all? Curious if you can drop in some breakpoints around those code blocks and step through the execution.
Hi @mmaybeno. I am trying to debug the related lines, but I think that already the changes of the majority of variables appear in the crawl output.
This is the spider I run: 
```
class BlogSpider(scrapy.Spider):
    name = 'blogspider'
    start_urls = ['https://www.zyte.com/blog/']

    def parse(self, response):
        for title in response.css('.oxy-post-title'):
            yield {'title': title.css('::text').get()}
        for next_page in response.css('a.next'):
            yield response.follow(next_page, self.parse)
```
Dropping breakpoints in different lines each time, the `feedexport.py` is never reached from the debugger. It's probably my fault, but I can't understand why this file does not participate in the debugging (as it does in the running).

Let me see if I can get it to work on my end. I'll take a look tomorrow or the next.
Sorry for bumping but any possible workaround?

Currently I just have to use JSON instead of CSV if I want to use `batch_item_count` as a "workaround"
@jackblk I more or less moved away from the issue as priorities changed for me. @marlenachatzigrigoriou did a great job and debugging most of it but we never found the underlying issue. Not sure if she wants to pick it up again? 
Hello again @mmaybeno! Honestly, I'm not intending to pick it up again.  
👋 @marlenachatzigrigoriou. Honestly don't blame you. This bug seems to be troublesome.

Resolving patch:
diff --git a/scrapy/exporters.py b/scrapy/exporters.py
--- a/scrapy/exporters.py
+++ b/scrapy/exporters.py
@@ -255,6 +255,9 @@ def export_item(self, item):
         values = list(self._build_row(x for _, x in fields))
         self.csv_writer.writerow(values)
 
+    def finish_exporting(self):
+        self.stream.detach()  # Avoid closing the wrapped file.
+
     def _build_row(self, values):
         for s in values:
             try:


Test patch:
diff --git a/tests/test_exporters.py b/tests/test_exporters.py
--- a/tests/test_exporters.py
+++ b/tests/test_exporters.py
@@ -91,6 +91,10 @@ def assertItemExportWorks(self, item):
             if self.ie.__class__ is not BaseItemExporter:
                 raise
         self.ie.finish_exporting()
+        # Delete the item exporter object, so that if it causes the output
+        # file handle to be closed, which should not be the case, follow-up
+        # interactions with the output file handle will surface the issue.
+        del self.ie
         self._check_output()
 
     def test_export_item(self):
@@ -244,6 +248,7 @@ def test_export_multiple_items(self):
         ie.export_item(i1)
         ie.export_item(i2)
         ie.finish_exporting()
+        del ie  # See the first “del self.ie” in this file for context.
         f.seek(0)
         self.assertEqual(self.item_class(**pickle.load(f)), i1)
         self.assertEqual(self.item_class(**pickle.load(f)), i2)
@@ -255,6 +260,7 @@ def test_nonstring_types_item(self):
         ie.start_exporting()
         ie.export_item(item)
         ie.finish_exporting()
+        del ie  # See the first “del self.ie” in this file for context.
         self.assertEqual(pickle.loads(fp.getvalue()), item)
 
 
@@ -280,6 +286,7 @@ def test_nonstring_types_item(self):
         ie.start_exporting()
         ie.export_item(item)
         ie.finish_exporting()
+        del ie  # See the first “del self.ie” in this file for context.
         fp.seek(0)
         self.assertEqual(marshal.load(fp), item)
 
@@ -315,6 +322,7 @@ def assertExportResult(self, item, expected, **kwargs):
         ie.start_exporting()
         ie.export_item(item)
         ie.finish_exporting()
+        del ie  # See the first “del self.ie” in this file for context.
         self.assertCsvEqual(fp.getvalue(), expected)
 
     def test_header_export_all(self):
@@ -346,6 +354,7 @@ def test_header_export_two_items(self):
             ie.export_item(item)
             ie.export_item(item)
             ie.finish_exporting()
+            del ie  # See the first “del self.ie” in this file for context.
             self.assertCsvEqual(
                 output.getvalue(), b"age,name\r\n22,John\xc2\xa3\r\n22,John\xc2\xa3\r\n"
             )
@@ -430,6 +439,7 @@ def assertExportResult(self, item, expected_value):
         ie.start_exporting()
         ie.export_item(item)
         ie.finish_exporting()
+        del ie  # See the first “del self.ie” in this file for context.
         self.assertXmlEquivalent(fp.getvalue(), expected_value)
 
     def _check_output(self):
@@ -539,6 +549,7 @@ def test_nested_item(self):
         self.ie.start_exporting()
         self.ie.export_item(i3)
         self.ie.finish_exporting()
+        del self.ie  # See the first “del self.ie” in this file for context.
         exported = json.loads(to_unicode(self.output.getvalue()))
         self.assertEqual(exported, self._expected_nested)
 
@@ -553,6 +564,7 @@ def test_nonstring_types_item(self):
         self.ie.start_exporting()
         self.ie.export_item(item)
         self.ie.finish_exporting()
+        del self.ie  # See the first “del self.ie” in this file for context.
         exported = json.loads(to_unicode(self.output.getvalue()))
         item["time"] = str(item["time"])
         self.assertEqual(exported, item)
@@ -580,6 +592,7 @@ def assertTwoItemsExported(self, item):
         self.ie.export_item(item)
         self.ie.export_item(item)
         self.ie.finish_exporting()
+        del self.ie  # See the first “del self.ie” in this file for context.
         exported = json.loads(to_unicode(self.output.getvalue()))
         self.assertEqual(
             exported, [ItemAdapter(item).asdict(), ItemAdapter(item).asdict()]
@@ -598,6 +611,7 @@ def test_nested_item(self):
         self.ie.start_exporting()
         self.ie.export_item(i3)
         self.ie.finish_exporting()
+        del self.ie  # See the first “del self.ie” in this file for context.
         exported = json.loads(to_unicode(self.output.getvalue()))
         expected = {
             "name": "Jesus",
@@ -612,6 +626,7 @@ def test_nested_dict_item(self):
         self.ie.start_exporting()
         self.ie.export_item(i3)
         self.ie.finish_exporting()
+        del self.ie  # See the first “del self.ie” in this file for context.
         exported = json.loads(to_unicode(self.output.getvalue()))
         expected = {"name": "Jesus", "age": {"name": "Maria", "age": i1}}
         self.assertEqual(exported, [expected])
@@ -621,6 +636,7 @@ def test_nonstring_types_item(self):
         self.ie.start_exporting()
         self.ie.export_item(item)
         self.ie.finish_exporting()
+        del self.ie  # See the first “del self.ie” in this file for context.
         exported = json.loads(to_unicode(self.output.getvalue()))
         item["time"] = str(item["time"])
         self.assertEqual(exported, [item])