Repository: raphaelm/defusedcsv

Issue:
writer.writerow() does not return anything
The stdlib python csv writer will return the row (as string) when calling `writer.writerow(row)`. The `defusedcsv` proxy is not handing the value along though.

```
from defusedcsv import csv

class EchoWriter:
    def write(self, value):
        return value

pseudo_buffer = Echo()

writer = csv.writer(pseudo_buffer)

value = writer.writerow(["foo", "bar""])
```

That returns `None`. When using stock `csv`, it'll return `"foo,bar"`.

I admit, the above example may be a bit convoluted and there may be better ways to do this. However for maximum compatibility with the stdlib csv library it might make sense to mimic this behaviour.



Comments:

Patch:
diff --git a/defusedcsv/csv.py b/defusedcsv/csv.py
index 2656c96..a027b85 100644
--- a/defusedcsv/csv.py
+++ b/defusedcsv/csv.py
@@ -1,20 +1,28 @@
 import re
 from csv import (
     QUOTE_ALL, QUOTE_MINIMAL, QUOTE_NONE, QUOTE_NONNUMERIC, Dialect,
-    DictReader, DictWriter as BaseDictWriter, Error, Sniffer, excel, excel_tab,
+    DictReader, DictWriter as _BaseDictWriter, Error, Sniffer, excel, excel_tab,
     field_size_limit, get_dialect, list_dialects, reader, register_dialect,
-    unix_dialect, unregister_dialect, writer as basewriter,
+    unix_dialect, unregister_dialect, writer as _basewriter,
+    __doc__,
 )
+from numbers import Number
+
+from . import version as __version__
 
 __all__ = ["QUOTE_MINIMAL", "QUOTE_ALL", "QUOTE_NONNUMERIC", "QUOTE_NONE",
-           "Error", "Dialect", "excel", "excel_tab", "field_size_limit", "reader", "writer",
+           "Error", "Dialect", "__doc__", "excel", "excel_tab",
+           "field_size_limit", "reader", "writer",
            "register_dialect", "get_dialect", "list_dialects", "Sniffer",
-           "unregister_dialect", "DictReader", "DictWriter", "unix_dialect"]
+           "unregister_dialect", "__version__", "DictReader", "DictWriter",
+           "unix_dialect"]
 
 
-def escape(payload):
+def _escape(payload):
     if payload is None:
         return ''
+    if isinstance(payload, Number):
+        return payload
 
     payload = str(payload)
     if payload and payload[0] in ('@', '+', '-', '=', '|', '%') and not re.match("^-?[0-9,\\.]+$", payload):
@@ -23,25 +31,30 @@ def escape(payload):
     return payload
 
 
-class ProxyWriter:
+class _ProxyWriter:
     def __init__(self, writer):
         self.writer = writer
 
     def writerow(self, row):
-        self.writer.writerow([escape(field) for field in row])
+        try:
+            iter(row)
+        except TypeError as err:
+            msg = "iterable expected, not %s" % type(row).__name__
+            raise Error(msg) from err
+        return self.writer.writerow([_escape(field) for field in row])
 
     def writerows(self, rows):
-        self.writer.writerows([[escape(field) for field in row] for row in rows])
+        return self.writer.writerows([[_escape(field) for field in row] for row in rows])
 
     def __getattr__(self, item):
         return getattr(self.writer, item)
 
 
 def writer(csvfile, dialect='excel', **fmtparams):
-    return ProxyWriter(basewriter(csvfile, dialect, **fmtparams))
+    return _ProxyWriter(_basewriter(csvfile, dialect, **fmtparams))
 
 
-class DictWriter(BaseDictWriter):
+class DictWriter(_BaseDictWriter):
     def __init__(self, f, fieldnames, restval="", extrasaction="raise",
                  dialect="excel", *args, **kwds):
         super().__init__(f, fieldnames, restval, extrasaction, dialect, *args, **kwds)


Test patch:
diff --git a/tests/test_escape.py b/tests/test_escape.py
index fa14347..93e2a50 100644
--- a/tests/test_escape.py
+++ b/tests/test_escape.py
@@ -1,5 +1,5 @@
 import pytest
-from defusedcsv.csv import escape
+from defusedcsv.csv import _escape as escape
 
 
 @pytest.mark.parametrize("input,expected", [
@@ -44,9 +44,14 @@ def test_dangerous_sample_payloads(input, expected):
     "Test | Foo",
     "",
     None,
+])
+def test_safe_sample_payloads(input):
+    assert escape(input) == (str(input) if input is not None else '')
+
+@pytest.mark.parametrize("input", [
     1,
     2,
     True
 ])
-def test_safe_sample_payloads(input):
-    assert escape(input) == (str(input) if input is not None else '')
+def test_safe_nonstr_sample_payloads(input):
+    assert escape(input) == input
diff --git a/tests/test_unmodified.py b/tests/test_unmodified.py
index e1d1326..e2548c3 100644
--- a/tests/test_unmodified.py
+++ b/tests/test_unmodified.py
@@ -32,6 +32,9 @@ def test_has_attributes():
     assert hasattr(csv, 'QUOTE_NONNUMERIC')
     assert hasattr(csv, 'QUOTE_NONE')
     assert hasattr(csv, 'Error')
+    assert hasattr(csv, 'writer')
+    assert hasattr(csv, '__doc__')
+    assert hasattr(csv, '__version__')
 
 
 def test_dialect_registry():