Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-aiocsv for openSUSE:Factory 
checked in at 2025-09-29 16:37:21
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-aiocsv (Old)
 and      /work/SRC/openSUSE:Factory/.python-aiocsv.new.11973 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-aiocsv"

Mon Sep 29 16:37:21 2025 rev:4 rq:1307719 version:1.4.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-aiocsv/python-aiocsv.changes      
2025-08-11 13:54:04.277014622 +0200
+++ /work/SRC/openSUSE:Factory/.python-aiocsv.new.11973/python-aiocsv.changes   
2025-09-29 16:37:36.923985882 +0200
@@ -1,0 +2,12 @@
+Mon Sep 29 10:22:39 UTC 2025 - Dirk Müller <[email protected]>
+
+- update to 1.4.0:
+  * Allow numeric fields to start with escapechar
+  * Fix Dialect typing (wtf cpython)
+  * drop 3.8 support
+  * backport PyModule_Add
+  * Fix seg faults when Parser_new fails partway-through
+  * Consider CPython bug #113732 when running on 3.12
+  * fix type checking issues with new quote settings
+
+-------------------------------------------------------------------

Old:
----
  aiocsv-1.3.2.tar.gz

New:
----
  aiocsv-1.4.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-aiocsv.spec ++++++
--- /var/tmp/diff_new_pack.K1fL08/_old  2025-09-29 16:37:37.584013535 +0200
+++ /var/tmp/diff_new_pack.K1fL08/_new  2025-09-29 16:37:37.584013535 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-aiocsv
-Version:        1.3.2
+Version:        1.4.0
 Release:        0
 Summary:        Asynchronous CSV reading/writing in Python
 License:        MIT

++++++ aiocsv-1.3.2.tar.gz -> aiocsv-1.4.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/.github/workflows/deploy.yml 
new/aiocsv-1.4.0/.github/workflows/deploy.yml
--- old/aiocsv-1.3.2/.github/workflows/deploy.yml       2024-04-28 
12:29:50.000000000 +0200
+++ new/aiocsv-1.4.0/.github/workflows/deploy.yml       2025-09-22 
14:04:03.000000000 +0200
@@ -12,15 +12,15 @@
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        os: [ubuntu-latest, windows-latest, macos-13]
+        os: [ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, 
macos-latest]
 
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
 
       - name: Build wheels
-        uses: pypa/[email protected]
+        uses: pypa/[email protected]
         env:
-          CIBW_ARCHS: auto64
+          CIBW_ARCHS: auto
           CIBW_BUILD: cp3*-*
 
       - uses: actions/upload-artifact@v4
@@ -32,7 +32,7 @@
     needs: [build_wheels]
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/download-artifact@v4
+      - uses: actions/download-artifact@v5
         with:
           pattern: cibw-wheels-*
           merge-multiple: true
@@ -42,3 +42,4 @@
         with:
           user: __token__
           password: ${{ secrets.pypi_password }}
+          skip-existing: true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/.github/workflows/test.yml 
new/aiocsv-1.4.0/.github/workflows/test.yml
--- old/aiocsv-1.3.2/.github/workflows/test.yml 2024-04-28 12:29:50.000000000 
+0200
+++ new/aiocsv-1.4.0/.github/workflows/test.yml 2025-09-22 14:04:03.000000000 
+0200
@@ -6,11 +6,11 @@
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
+        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
     steps:
-    - uses: actions/checkout@v4
+    - uses: actions/checkout@v5
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v5
+      uses: actions/setup-python@v6
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
@@ -23,11 +23,11 @@
     name: Lint code
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v4
-    - name: Set up Python 3.8
-      uses: actions/setup-python@v5
+    - uses: actions/checkout@v5
+    - name: Set up Python 3.9
+      uses: actions/setup-python@v6
       with:
-        python-version: "3.8"
+        python-version: "3.9"
     - name: Install dependencies
       run: pip install -Ur requirements.dev.txt
     - name: Check code formatting
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/aiocsv/__init__.py 
new/aiocsv-1.4.0/aiocsv/__init__.py
--- old/aiocsv-1.3.2/aiocsv/__init__.py 2024-04-28 12:29:50.000000000 +0200
+++ new/aiocsv-1.4.0/aiocsv/__init__.py 2025-09-22 14:04:03.000000000 +0200
@@ -3,7 +3,7 @@
 
 __title__ = "aiocsv"
 __description__ = "Asynchronous CSV reading/writing"
-__version__ = "1.3.2"
+__version__ = "1.4.0"
 
 __url__ = "https://github.com/MKuranowski/aiocsv";
 __author__ = "Mikołaj Kuranowski"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/aiocsv/_parser.c 
new/aiocsv-1.4.0/aiocsv/_parser.c
--- old/aiocsv-1.3.2/aiocsv/_parser.c   2024-04-28 12:29:50.000000000 +0200
+++ new/aiocsv-1.4.0/aiocsv/_parser.c   2025-09-22 14:04:03.000000000 +0200
@@ -1,4 +1,4 @@
-// © Copyright 2020-2024 Mikołaj Kuranowski
+// © Copyright 2020-2025 Mikołaj Kuranowski
 // SPDX-License-Identifier: MIT
 
 #include <assert.h>
@@ -23,14 +23,6 @@
 // * PYTHON API BACKPORTS *
 // ************************
 
-#if PY_VERSION_HEX < 0x03090000
-
-static inline PyObject* PyObject_CallMethodOneArg(PyObject* self, PyObject* 
name, PyObject* arg) {
-    return PyObject_CallMethodObjArgs(self, name, arg, NULL);
-}
-
-#endif
-
 #if PY_VERSION_HEX < 0x030A0000
 
 #define Py_TPFLAGS_IMMUTABLETYPE 0
@@ -96,6 +88,18 @@
 
 #endif
 
+#if PY_VERSION_HEX < 0x030D0000
+
+static int PyModule_Add(PyObject* module, const char* name, PyObject* value) {
+    if (PyModule_AddObject(module, name, value) < 0) {
+        Py_XDECREF(value);
+        return -1;
+    }
+    return 0;
+}
+
+#endif
+
 // ***************
 // * DEFINITIONS *
 // ***************
@@ -114,19 +118,18 @@
 
     /// The string "read"
     PyObject* str_read;
-
-    /// Parser class exposed by this module
-    PyTypeObject* parser_type;
 } ModuleState;
 
-/// Returns a ModuleState* corresponding to a provided PyObject* representing 
a module
-#define module_get_state(m) ((ModuleState*)PyModule_GetState(m))
+/// Returns a ModuleState* from a Parser instance (Parser*)
+#define parser_get_module_state(p) 
((ModuleState*)PyType_GetModuleState(Py_TYPE(p)))
 
 typedef enum {
     QUOTE_MINIMAL = 0,
     QUOTE_ALL = 1,
     QUOTE_NON_NUMERIC = 2,
     QUOTE_NONE = 3,
+    QUOTE_STRINGS = 4,
+    QUOTE_NOT_NULL = 5,
 } Quoting;
 
 typedef enum {
@@ -197,12 +200,6 @@
     // clang-format off
     PyObject_HEAD
 
-    /// Pointer to the _parser module. Required for 3.8 compatibility.
-    ///
-    /// TODO: Drop field once support for 3.8 is dropped.
-    ///       PyType_GetModuleState(Py_TYPE(self)) should be used instead.
-    PyObject* module;
-
     /// Anything with a `async def read(self, n: int) -> str` method.
     PyObject* reader;
 
@@ -240,8 +237,8 @@
     /// ParserState for the parser state machine.
     unsigned char state;
 
-    /// True if current field should be interpreted as a float.
-    bool field_was_numeric;
+    /// True if current field was quoted.
+    bool field_was_quoted;
 
     /// True if last returned character was a CR, used to avoid counting CR-LF 
as two separate lines.
     bool last_char_was_cr;
@@ -306,7 +303,8 @@
     if (PyErr_Occurred()) {                                                    
                 \
         return 0;                                                              
                 \
     }                                                                          
                 \
-    if (quoting_value < (Py_ssize_t)QUOTE_MINIMAL || quoting_value > 
(Py_ssize_t)QUOTE_NONE) {  \
+    if (quoting_value < (Py_ssize_t)QUOTE_MINIMAL ||                           
                 \
+        quoting_value > (Py_ssize_t)QUOTE_NOT_NULL) {                          
                 \
         PyErr_Format(PyExc_ValueError, "dialect.quoting: unexpected value 
%zd", quoting_value); \
         return 0;                                                              
                 \
     }                                                                          
                 \
@@ -342,31 +340,44 @@
 }
 
 static int Parser_traverse(Parser* self, visitproc visit, void* arg) {
-    Py_VISIT(self->module);
     Py_VISIT(self->reader);
     Py_VISIT(self->current_read);
     Py_VISIT(self->buffer_str);
     Py_VISIT(self->record_so_far);
-#if PY_VERSION_HEX >= 0x03090000
     Py_VISIT(Py_TYPE(self));
-#endif
     return 0;
 }
 
 static int Parser_clear(Parser* self) {
-    Py_CLEAR(self->module);
     Py_CLEAR(self->reader);
     Py_CLEAR(self->current_read);
     Py_CLEAR(self->record_so_far);
     return 0;
 }
 
-static PyObject* Parser_new(PyObject* module, PyObject* args, PyObject* 
kwargs) {
-    ModuleState* state = module_get_state(module);
-
-    Parser* self = PyObject_GC_New(Parser, state->parser_type);
+static PyObject* Parser_new(PyTypeObject* subtype, PyObject* args, PyObject* 
kwargs) {
+    ModuleState* state = PyType_GetModuleState(subtype);
+    Parser* self = PyObject_GC_New(Parser, subtype);
     if (!self) return NULL;
 
+    // Zero-initialize all custom Parser fields. In case the initialization 
fails,
+    // we need to ensure the deallocator doesn't stumble on garbage values.
+    self->reader = NULL;
+    self->current_read = NULL;
+    self->buffer_str = NULL;
+    self->buffer_idx = 0;
+    self->record_so_far = NULL;
+    self->field_so_far = NULL;
+    self->field_so_far_capacity = 0;
+    self->field_so_far_len = 0;
+    self->dialect = (Dialect){0};
+    self->field_size_limit = 0;
+    self->line_num = 0;
+    self->state = STATE_START_RECORD;
+    self->field_was_quoted = false;
+    self->last_char_was_cr = false;
+    self->eof = false;
+
     PyObject* reader;
     PyObject* dialect;
     static char* kw_list[] = {"reader", "dialect", NULL};
@@ -380,11 +391,9 @@
         return NULL;
     }
 
-    self->module = Py_NewRef(module);
     self->reader = Py_NewRef(reader);
 
-    PyObject* field_size_limit_obj =
-        PyObject_CallObject(module_get_state(module)->csv_field_size_limit, 
NULL);
+    PyObject* field_size_limit_obj = 
PyObject_CallObject(state->csv_field_size_limit, NULL);
     if (!field_size_limit_obj) {
         Py_DECREF(self);
         return NULL;
@@ -397,19 +406,6 @@
         return NULL;
     }
 
-    self->current_read = NULL;
-    self->record_so_far = NULL;
-    self->buffer_str = NULL;
-    self->buffer_idx = 0;
-    self->field_so_far = NULL;
-    self->field_so_far_capacity = 0;
-    self->field_so_far_len = 0;
-    self->line_num = 0;
-    self->state = STATE_START_RECORD;
-    self->field_was_numeric = false;
-    self->last_char_was_cr = false;
-    self->eof = false;
-
     PyObject_GC_Track(self);
     return (PyObject*)self;
 }
@@ -422,7 +418,7 @@
 
 static int Parser_add_char(Parser* self, Py_UCS4 c) {
     if (self->field_so_far_len == self->field_size_limit) {
-        PyObject* err = module_get_state(self->module)->csv_error;
+        PyObject* err = parser_get_module_state(self)->csv_error;
         PyErr_Format(err, "field larger than field limit (%ld)", 
self->field_size_limit);
         return 0;
     } else if (self->field_so_far_len >= self->field_so_far_capacity) {
@@ -471,14 +467,32 @@
 
     self->field_so_far_len = 0;
 
-    // Cast the field to a float, if applicable
-    if (self->field_was_numeric) {
-        self->field_was_numeric = false;
-
-        PyObject* field_as_float = PyFloat_FromString(field);
-        Py_DECREF(field);
-        if (!field_as_float) return 0;
-        field = field_as_float;
+    // Cast the field to a float or None, if applicable
+    if (!self->field_was_quoted) {
+        // Check if this field should be converted to float or None
+        bool is_none = false;
+        bool is_float = false;
+        if (self->dialect.quoting == QUOTE_NON_NUMERIC) {
+            is_float = PyObject_IsTrue(field);
+        } else if (self->dialect.quoting == QUOTE_STRINGS) {
+            is_none = PyObject_Not(field);
+            is_float = !is_none;
+        } else if (self->dialect.quoting == QUOTE_NOT_NULL) {
+            is_none = PyObject_Not(field);
+        }
+
+        // Convert to None or float
+        if (is_none) {
+            Py_DECREF(field);
+            field = Py_NewRef(Py_None);
+        } else if (is_float) {
+            PyObject* field_as_float = PyFloat_FromString(field);
+            Py_DECREF(field);
+            if (!field_as_float) return 0;
+            field = field_as_float;
+        }
+    } else {
+        self->field_was_quoted = false;
     }
 
     // Append the field to the record
@@ -551,7 +565,7 @@
         self->state = STATE_IN_FIELD;
         return DECISION_CONTINUE;
     } else {
-        PyObject* csv_error = module_get_state(self->module)->csv_error;
+        PyObject* csv_error = parser_get_module_state(self)->csv_error;
         PyErr_Format(csv_error, "'%c' expected after '%c'", 
self->dialect.delimiter,
                      self->dialect.quotechar);
         return DECISION_ERROR;
@@ -616,6 +630,7 @@
         self->state = STATE_START_RECORD;
         return DECISION_DONE;
     } else if (c == self->dialect.quotechar && self->dialect.quoting != 
QUOTE_NONE) {
+        self->field_was_quoted = true;
         self->state = STATE_IN_QUOTED_FIELD;
         return DECISION_CONTINUE;
     } else if (c == self->dialect.escapechar) {
@@ -626,7 +641,6 @@
         self->state = STATE_START_FIELD;
         return DECISION_CONTINUE;
     } else {
-        self->field_was_numeric = self->dialect.quoting == QUOTE_NON_NUMERIC;
         if (!Parser_add_char(self, c)) return DECISION_ERROR;
         self->state = STATE_IN_FIELD;
         return DECISION_CONTINUE;
@@ -688,7 +702,7 @@
 
     if (decision != DECISION_CONTINUE || (self->eof && 
!state_is_end_of_record(self->state))) {
         if (self->dialect.strict && state_is_unexpected_at_eof(self->state)) {
-            PyErr_SetString(module_get_state(self->module)->csv_error, 
"unexpected end of data");
+            PyErr_SetString(parser_get_module_state(self)->csv_error, 
"unexpected end of data");
             return NULL;
         }
 
@@ -706,7 +720,7 @@
     PyObject* read_coro = NULL;
     int result = 1;
 
-    ModuleState* module_state = module_get_state(self->module);
+    ModuleState* module_state = parser_get_module_state(self);
     read_coro = PyObject_CallMethodOneArg(self->reader, module_state->str_read,
                                           
module_state->io_default_buffer_size);
     if (!read_coro) FINISH_WITH(0);
@@ -823,6 +837,7 @@
     {Py_tp_clear, Parser_clear},
     {Py_tp_dealloc, Parser_dealloc},
     {Py_tp_members, ParserMembers},
+    {Py_tp_new, Parser_new},
     {Py_am_await, Py_NewRef},  // Return "self" unchanged
     {Py_am_aiter, Py_NewRef},  // Return "self" unchanged
     {Py_am_anext, Py_NewRef},  // Return "self" unchanged
@@ -835,8 +850,7 @@
     .name = "_parser._Parser",
     .basicsize = sizeof(Parser),
     .itemsize = 0,
-    .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | 
Py_TPFLAGS_IMMUTABLETYPE |
-              Py_TPFLAGS_DISALLOW_INSTANTIATION),
+    .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | 
Py_TPFLAGS_IMMUTABLETYPE),
     .slots = ParserSlots,
 };
 
@@ -845,7 +859,7 @@
 // *************************
 
 static int module_clear(PyObject* module) {
-    ModuleState* state = module_get_state(module);
+    ModuleState* state = PyModule_GetState(module);
     if (state) {
         Py_CLEAR(state->csv_error);
         Py_CLEAR(state->csv_field_size_limit);
@@ -856,7 +870,7 @@
 }
 
 static int module_traverse(PyObject* module, visitproc visit, void* arg) {
-    ModuleState* state = module_get_state(module);
+    ModuleState* state = PyModule_GetState(module);
     if (state) {
         Py_VISIT(state->csv_error);
         Py_VISIT(state->csv_field_size_limit);
@@ -873,7 +887,7 @@
     PyObject* csv_module = NULL;
     PyObject* io_module = NULL;
 
-    ModuleState* state = module_get_state(module);
+    ModuleState* state = PyModule_GetState(module);
 
     state->str_read = PyUnicode_InternFromString("read");
     if (!state->str_read) FINISH_WITH(-1);
@@ -902,8 +916,8 @@
         FINISH_WITH(-1);
     }
 
-    state->parser_type = (PyTypeObject*)PyType_FromSpec(&ParserSpec);
-    if (!state->parser_type) FINISH_WITH(-1);
+    if (PyModule_Add(module, "Parser", PyType_FromModuleAndSpec(module, 
&ParserSpec, NULL)))
+        FINISH_WITH(-1);
 
 ret:
     Py_XDECREF(csv_module);
@@ -911,12 +925,6 @@
     return result;
 }
 
-static PyMethodDef ModuleMethods[] = {
-    {"Parser", (PyCFunction)Parser_new, METH_VARARGS | METH_KEYWORDS,
-     "Creates a new Parser instance"},
-    {NULL, NULL},
-};
-
 static PyModuleDef_Slot ModuleSlots[] = {
     {Py_mod_exec, module_exec},
 #if PY_VERSION_HEX >= 0x030C0000
@@ -931,7 +939,6 @@
     .m_doc = "_parser implements asynchronous CSV record parsing",
     .m_size = sizeof(ModuleState),
     .m_slots = ModuleSlots,
-    .m_methods = ModuleMethods,
     .m_traverse = module_traverse,
     .m_clear = module_clear,
     .m_free = module_free,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/aiocsv/_parser.pyi 
new/aiocsv-1.4.0/aiocsv/_parser.pyi
--- old/aiocsv-1.3.2/aiocsv/_parser.pyi 2024-04-28 12:29:50.000000000 +0200
+++ new/aiocsv-1.4.0/aiocsv/_parser.pyi 2025-09-22 14:04:03.000000000 +0200
@@ -1,16 +1,17 @@
-# © Copyright 2020-2024 Mikołaj Kuranowski
+# © Copyright 2020-2025 Mikołaj Kuranowski
 # SPDX-License-Identifier: MIT
 
 from typing import AsyncIterator, Awaitable, List
 
+from typing_extensions import Self
+
 from .protocols import DialectLike, WithAsyncRead
 
-class _Parser:
+class Parser:
     """Return type of the "Parser" function, not accessible from Python."""
 
+    def __new__(cls, reader: WithAsyncRead, dialect: DialectLike) -> Self: ...
     def __aiter__(self) -> AsyncIterator[List[str]]: ...
     def __anext__(self) -> Awaitable[List[str]]: ...
     @property
     def line_num(self) -> int: ...
-
-def Parser(reader: WithAsyncRead, dialect: DialectLike) -> _Parser: ...
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/aiocsv/parser.py 
new/aiocsv-1.4.0/aiocsv/parser.py
--- old/aiocsv-1.3.2/aiocsv/parser.py   2024-04-28 12:29:50.000000000 +0200
+++ new/aiocsv-1.4.0/aiocsv/parser.py   2025-09-22 14:04:03.000000000 +0200
@@ -1,9 +1,11 @@
-# © Copyright 2020-2024 Mikołaj Kuranowski
+# © Copyright 2020-2025 Mikołaj Kuranowski
 # SPDX-License-Identifier: MIT
+# pyright: reportUnnecessaryComparison=false
 
 import csv
+import sys
 from enum import IntEnum, auto
-from typing import Any, AsyncIterator, Awaitable, Generator, List, Optional, 
Sequence, Union
+from typing import Any, AsyncIterator, Awaitable, Final, Generator, List, 
Optional, Sequence, Union
 
 from .protocols import DialectLike, WithAsyncRead
 
@@ -39,6 +41,12 @@
 QUOTE_ALL = csv.QUOTE_ALL
 QUOTE_NONNUMERIC = csv.QUOTE_NONNUMERIC
 QUOTE_NONE = csv.QUOTE_NONE
+if sys.version_info >= (3, 12):
+    QUOTE_STRINGS = csv.QUOTE_STRINGS
+    QUOTE_NOTNULL = csv.QUOTE_NOTNULL
+else:
+    QUOTE_STRINGS: Final = 4
+    QUOTE_NOTNULL: Final = 5
 
 
 class Parser:
@@ -55,7 +63,7 @@
         self.record_so_far: List[str] = []
         self.field_so_far: List[str] = []
         self.field_limit: int = csv.field_size_limit()
-        self.field_was_numeric: bool = False
+        self.field_was_quoted: bool = False
         self.last_char_was_cr: bool = False
 
     # AsyncIterator[List[str]] interface
@@ -161,6 +169,7 @@
             self.state = ParserState.START_RECORD
             return Decision.DONE
         elif c == self.dialect.quotechar and self.dialect.quoting != 
QUOTE_NONE:
+            self.field_was_quoted = True
             self.state = ParserState.IN_QUOTED_FIELD
         elif c == self.dialect.escapechar:
             self.state = ParserState.ESCAPE
@@ -169,7 +178,6 @@
             self.save_field()
             self.state = ParserState.START_FIELD
         else:
-            self.field_was_numeric = self.dialect.quoting == QUOTE_NONNUMERIC
             self.add_char(c)
             self.state = ParserState.IN_FIELD
         return Decision.CONTINUE
@@ -234,7 +242,7 @@
             self.state = ParserState.IN_FIELD
         else:
             raise csv.Error(
-                f"{self.dialect.delimiter!r} expected after 
{self.dialect.quotechar!r}"
+                f"{self.dialect.delimiter!r} expected after 
{self.dialect.quotechar!r}",
             )
         return Decision.CONTINUE
 
@@ -254,11 +262,15 @@
         else:
             field = "".join(self.field_so_far)
 
-        # Convert to float if QUOTE_NONNUMERIC
-        if self.dialect.quoting == QUOTE_NONNUMERIC and field and 
self.field_was_numeric:
-            self.field_was_numeric = False
-            field = float(field)
+        # Handle unquoted fields for special quote modes
+        if self.dialect.quoting == QUOTE_NONNUMERIC and not 
self.field_was_quoted:
+            field = float(field) if field else ""
+        elif self.dialect.quoting == QUOTE_STRINGS and not 
self.field_was_quoted:
+            field = float(field) if field else None
+        elif self.dialect.quoting == QUOTE_NOTNULL and not 
self.field_was_quoted:
+            field = field if field else None
 
+        self.field_was_quoted = False
         self.record_so_far.append(field)  # type: ignore
         self.field_so_far.clear()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/aiocsv/protocols.py 
new/aiocsv-1.4.0/aiocsv/protocols.py
--- old/aiocsv-1.3.2/aiocsv/protocols.py        2024-04-28 12:29:50.000000000 
+0200
+++ new/aiocsv-1.4.0/aiocsv/protocols.py        2025-09-22 14:04:03.000000000 
+0200
@@ -1,13 +1,19 @@
-# © Copyright 2020-2024 Mikołaj Kuranowski
+# © Copyright 2020-2025 Mikołaj Kuranowski
 # SPDX-License-Identifier: MIT
 
-from typing import TYPE_CHECKING, Any, Optional, Protocol, Type, TypedDict, 
Union
+import sys
+from typing import TYPE_CHECKING, Any, Literal, Optional, Protocol, Type, 
TypedDict, Union
 
 from typing_extensions import NotRequired
 
 if TYPE_CHECKING:
     import csv
 
+if sys.version_info < (3, 12):
+    _QuotingType = Literal[0, 1, 2, 3]
+else:
+    _QuotingType = Literal[0, 1, 2, 3, 4, 5]
+
 
 class WithAsyncWrite(Protocol):
     async def write(self, __b: str) -> Any: ...
@@ -23,7 +29,7 @@
     escapechar: Optional[str]
     doublequote: bool
     skipinitialspace: bool
-    quoting: int
+    quoting: _QuotingType
     strict: bool
 
 
@@ -37,5 +43,5 @@
     doublequote: NotRequired[bool]
     skipinitialspace: NotRequired[bool]
     lineterminator: NotRequired[str]
-    quoting: NotRequired[int]
+    quoting: NotRequired[_QuotingType]
     strict: NotRequired[bool]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/aiocsv/readers.py 
new/aiocsv-1.4.0/aiocsv/readers.py
--- old/aiocsv-1.3.2/aiocsv/readers.py  2024-04-28 12:29:50.000000000 +0200
+++ new/aiocsv-1.4.0/aiocsv/readers.py  2025-09-22 14:04:03.000000000 +0200
@@ -1,4 +1,4 @@
-# © Copyright 2020-2024 Mikołaj Kuranowski
+# © Copyright 2020-2025 Mikołaj Kuranowski
 # SPDX-License-Identifier: MIT
 
 # cSpell: words asyncfile restkey restval
@@ -9,7 +9,7 @@
 
 from typing_extensions import Unpack
 
-from .protocols import CsvDialectArg, CsvDialectKwargs, WithAsyncRead
+from .protocols import CsvDialectArg, CsvDialectKwargs, DialectLike, 
WithAsyncRead
 
 try:
     from ._parser import Parser
@@ -40,7 +40,7 @@
         self._parser = Parser(self._file, self._dialect)
 
     @property
-    def dialect(self) -> csv.Dialect:
+    def dialect(self) -> DialectLike:
         return self._dialect
 
     @property
@@ -78,7 +78,7 @@
         self.reader = AsyncReader(asyncfile, dialect=dialect, 
**csv_dialect_kwargs)
 
     @property
-    def dialect(self) -> csv.Dialect:
+    def dialect(self) -> DialectLike:
         return self.reader.dialect
 
     @property
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/aiocsv/writers.py 
new/aiocsv-1.4.0/aiocsv/writers.py
--- old/aiocsv-1.3.2/aiocsv/writers.py  2024-04-28 12:29:50.000000000 +0200
+++ new/aiocsv-1.4.0/aiocsv/writers.py  2025-09-22 14:04:03.000000000 +0200
@@ -1,4 +1,4 @@
-# © Copyright 2020-2024 Mikołaj Kuranowski
+# © Copyright 2020-2025 Mikołaj Kuranowski
 # SPDX-License-Identifier: MIT
 
 # cSpell: words asyncfile extrasaction fieldnames restval
@@ -9,7 +9,7 @@
 
 from typing_extensions import Unpack
 
-from .protocols import CsvDialectArg, CsvDialectKwargs, WithAsyncWrite
+from .protocols import CsvDialectArg, CsvDialectKwargs, DialectLike, 
WithAsyncWrite
 
 
 class AsyncWriter:
@@ -30,7 +30,7 @@
         self._csv_writer = csv.writer(self._buffer, dialect=dialect, 
**csv_dialect_kwargs)
 
     @property
-    def dialect(self) -> csv.Dialect:
+    def dialect(self) -> DialectLike:
         return self._csv_writer.dialect
 
     async def _rewrite_buffer(self) -> None:
@@ -86,7 +86,7 @@
         self.writer = AsyncWriter(asyncfile, dialect, **csv_dialect_kwargs)
 
     @property
-    def dialect(self) -> csv.Dialect:
+    def dialect(self) -> DialectLike:
         return self.writer.dialect
 
     def _dict_to_iterable(self, row_dict: Mapping[str, Any]) -> Iterable[Any]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/pyproject.toml 
new/aiocsv-1.4.0/pyproject.toml
--- old/aiocsv-1.3.2/pyproject.toml     2024-04-28 12:29:50.000000000 +0200
+++ new/aiocsv-1.4.0/pyproject.toml     2025-09-22 14:04:03.000000000 +0200
@@ -5,7 +5,7 @@
 [project]
 name = "aiocsv"
 readme = "readme.md"
-requires-python = ">=3.8"
+requires-python = ">=3.9"
 dynamic = ["version"]
 authors = [
     {name = "Mikołaj Kuranowski", email = "[email protected]"},
@@ -36,10 +36,12 @@
 
 [tool.black]
 line-length = 99
+target-version = ['py39']
 
 [tool.isort]
 profile = "black"
 line_length = 99
+py_version = 39
 
 [tool.pyright]
 typeCheckingMode = "strict"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/readme.md new/aiocsv-1.4.0/readme.md
--- old/aiocsv-1.3.2/readme.md  2024-04-28 12:29:50.000000000 +0200
+++ new/aiocsv-1.4.0/readme.md  2025-09-22 14:04:03.000000000 +0200
@@ -71,8 +71,8 @@
 
 ## Differences with `csv`
 
-`aiocsv` strives to be a drop-in replacement for Python's builtin
-[csv module](https://docs.python.org/3/library/csv.html). However, there are 3 
notable differences:
+`aiocsv` strives to be a drop-in replacement for Python's builtin [csv 
module](https://docs.python.org/3/library/csv.html).
+However, there are a few notable differences, due to technical limitations or 
CPython bugs:
 
 - Readers accept objects with async `read` methods, instead of an 
AsyncIterable over lines
     from a file.
@@ -80,6 +80,8 @@
 - Changes to `csv.field_size_limit` are not picked up by existing Reader 
instances.
     The field size limit is cached on Reader instantiation to avoid expensive 
function calls
     on each character of the input.
+- `QUOTE_NOTNULL` and `QUOTE_STRINGS` work on readers even in 3.12. aiocsv 
does not replicate
+    [CPython bug #113732](https://github.com/python/cpython/issues/113732).
 
 Other, minor, differences include:
 - `AsyncReader.line_num`, `AsyncDictReader.line_num` and 
`AsyncDictReader.dialect` are not settable,
@@ -111,8 +113,8 @@
 - `async __anext__(self) -> List[str]`
 
 *Read-only properties*:
-- `dialect`: The csv.Dialect used when parsing
-- `line_num`: The number of lines read from the source file. This coincides 
with a 1-based index
+- `dialect` (`aiocsv.protocols.DialectLike`): The dialect used when parsing
+- `line_num` (`int`): The number of lines read from the source file. This 
coincides with a 1-based index
     of the line number of the last line of the recently parsed record.
 
 
@@ -141,7 +143,7 @@
 
 
 *Properties*:
-- `fieldnames`: field names used when converting rows to dictionaries  
+- `fieldnames` (`List[str] | None`): field names used when converting rows to 
dictionaries  
     **⚠️** Unlike csv.DictReader, this property can't read the fieldnames if 
they are missing -
     it's not possible to `await` on the header row in a property getter.
     **Use `await reader.get_fieldnames()`**.
@@ -153,15 +155,15 @@
     areader.fieldnames   # ⚠️ None
     await areader.get_fieldnames()  # ["cells", "from", "the", "header"]
     ```
-- `restkey`: If a row has more cells then the header, all remaining cells are 
stored under
+- `restkey` (`str | None`): If a row has more cells then the header, all 
remaining cells are stored under
     this key in the returned dictionary. Defaults to `None`.
-- `restval`: If a row has less cells then the header, then missing keys will 
use this
+- `restval` (`str | None`): If a row has less cells then the header, then 
missing keys will use this
     value. Defaults to `None`.
 - `reader`: Underlying `aiofiles.AsyncReader` instance
 
 *Read-only properties*:
-- `dialect`: Link to `self.reader.dialect` - the current csv.Dialect
-- `line_num`: The number of lines read from the source file. This coincides 
with a 1-based index
+- `dialect` (`aiocsv.protocols.DialectLike`): Link to `self.reader.dialect` - 
the current csv.Dialect
+- `line_num` (`int`): The number of lines read from the source file. This 
coincides with a 1-based index
     of the line number of the last line of the recently parsed record.
 
 
@@ -187,7 +189,7 @@
     Writes multiple rows to the specified file.
 
 *Readonly properties*:
-- `dialect`: Link to underlying's csv.writer's `dialect` attribute
+- `dialect` (`aiocsv.protocols.DialectLike`): Link to underlying's 
csv.writer's `dialect` attribute
 
 
 ### aiocsv.AsyncDictWriter
@@ -216,17 +218,17 @@
     Writes multiple rows to the specified file.
 
 *Properties*:
-- `fieldnames`: Sequence of keys to identify the order of values when writing 
rows
+- `fieldnames` (`Sequence[str]`): Sequence of keys to identify the order of 
values when writing rows
     to the underlying file
-- `restval`: Placeholder value used when a key from fieldnames is missing in a 
row,
+- `restval` (`Any`): Placeholder value used when a key from fieldnames is 
missing in a row,
     defaults to `""`
-- `extrasaction`: Action to take when there are keys in a row, which are not 
present in
+- `extrasaction` (`Literal["raise", "ignore"]`): Action to take when there are 
keys in a row, which are not present in
     fieldnames, defaults to `"raise"` which causes ValueError to be raised on 
extra keys,
     may be also set to `"ignore"` to ignore any extra keys
 - `writer`: Link to the underlying `AsyncWriter`
 
 *Readonly properties*:
-- `dialect`: Link to underlying's csv.reader's `dialect` attribute
+- `dialect` (`aiocsv.protocols.DialectLike`): Link to underlying's 
csv.reader's `dialect` attribute
 
 
 ### aiocsv.protocols.WithAsyncRead
@@ -236,6 +238,9 @@
 ### aiocsv.protocols.WithAsyncWrite
 A `typing.Protocol` describing an asynchronous file, which can be written to.
 
+### aiocsv.protocols.DialectLike
+Type of an instantiated `dialect` property. Thank CPython for an incredible 
mess of
+having unrelated and disjoint `csv.Dialect` and `_csv.Dialect` classes.
 
 ### aiocsv.protocols.CsvDialectArg
 Type of the `dialect` argument, as used in the `csv` module.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/aiocsv-1.3.2/tests/test_parser.py 
new/aiocsv-1.4.0/tests/test_parser.py
--- old/aiocsv-1.3.2/tests/test_parser.py       2024-04-28 12:29:50.000000000 
+0200
+++ new/aiocsv-1.4.0/tests/test_parser.py       2025-09-22 14:04:03.000000000 
+0200
@@ -1,5 +1,6 @@
 import csv
 import io
+import sys
 from typing import AsyncIterator, Callable, List, Protocol, Type
 
 import pytest
@@ -157,6 +158,10 @@
 
 
 @pytest.mark.asyncio
[email protected](
+    sys.version_info < (3, 12, 9),
+    reason="CPython bug gh-113785 was fixed in 3.12.9",
+)
 @pytest.mark.parametrize("parser", PARSERS, ids=PARSER_NAMES)
 async def test_parsing_weird_quotes_nonnumeric(parser: Type[Parser]):
     data = '3.0,\r\n"1."5,"15"\r\n$2,"-4".5\r\n-5$.2,-11'
@@ -173,7 +178,7 @@
     ]
 
     assert csv_result == custom_result
-    assert custom_result == [[3.0, ""], ["1.5", "15"], ["2", "-4.5"], [-5.2, 
-11.0]]
+    assert custom_result == [[3.0, ""], ["1.5", "15"], [2.0, "-4.5"], [-5.2, 
-11.0]]
 
 
 @pytest.mark.asyncio
@@ -367,3 +372,80 @@
 
     assert csv_result == expected_result
     assert custom_result == expected_result
+
+
[email protected]
[email protected]("parser", PARSERS, ids=PARSER_NAMES)
+async def test_parsing_no_newline_at_the_end(parser: Type[Parser]):
+    data = "pi,3.1416\r\nsqrt2,1.4142\r\nphi,1.618\r\ne,2.7183"
+
+    csv_result = list(csv.reader(io.StringIO(data, newline="")))
+    custom_result = [
+        r async for r in parser(AsyncStringIO(data), csv.get_dialect("excel")) 
 # type: ignore
+    ]
+
+    assert csv_result == custom_result
+    assert custom_result == [
+        ["pi", "3.1416"],
+        ["sqrt2", "1.4142"],
+        ["phi", "1.618"],
+        ["e", "2.7183"],
+    ]
+
+
[email protected]
[email protected](sys.version_info < (3, 12), reason="csv.QUOTE_STRINGS was 
added in 3.12")
[email protected]("parser", PARSERS, ids=PARSER_NAMES)
+async def test_parsing_quote_strings(parser: Type[Parser]):
+    data = '3.14,,"abc",""\r\n'
+
+    csv_parser = csv.reader(io.StringIO(data, newline=""), 
quoting=csv.QUOTE_STRINGS, strict=True)  # type: ignore
+    csv_result = list(csv_parser)
+    custom_result = [
+        r async for r in parser(AsyncStringIO(data), csv_parser.dialect)  # 
type: ignore
+    ]
+
+    if sys.version_info < (3, 13):
+        # https://github.com/python/cpython/issues/113732
+        assert csv_result == [["3.14", "", "abc", ""]]
+    else:
+        assert csv_result == [[3.14, None, "abc", ""]]
+    assert custom_result == [[3.14, None, "abc", ""]]
+
+
[email protected]
[email protected](sys.version_info < (3, 12), reason="csv.QUOTE_STRINGS was 
added in 3.12")
[email protected]("parser", PARSERS, ids=PARSER_NAMES)
+async def test_parsing_quote_strings_non_float(parser: Type[Parser]):
+    data = "abc"
+
+    csv_parser = csv.reader(io.StringIO(data, newline=""), 
quoting=csv.QUOTE_STRINGS, strict=True)  # type: ignore
+    if sys.version_info < (3, 13):
+        # https://github.com/python/cpython/issues/113732
+        assert list(csv_parser) == [["abc"]]
+    else:
+        with pytest.raises(ValueError):
+            list(csv_parser)
+
+    with pytest.raises(ValueError):
+        [r async for r in parser(AsyncStringIO(data), csv_parser.dialect)]  # 
type: ignore
+
+
[email protected]
[email protected](sys.version_info < (3, 12), reason="csv.QUOTE_NOTNULL was 
added in 3.12")
[email protected]("parser", PARSERS, ids=PARSER_NAMES)
+async def test_parsing_quote_not_null(parser: Type[Parser]):
+    data = '3.14,,abc,""\r\n'
+
+    csv_parser = csv.reader(io.StringIO(data, newline=""), 
quoting=csv.QUOTE_NOTNULL, strict=True)  # type: ignore
+    csv_result = list(csv_parser)
+    custom_result = [
+        r async for r in parser(AsyncStringIO(data), csv_parser.dialect)  # 
type: ignore
+    ]
+
+    if sys.version_info < (3, 13):
+        # https://github.com/python/cpython/issues/113732
+        assert csv_result == [["3.14", "", "abc", ""]]
+    else:
+        assert csv_result == [["3.14", None, "abc", ""]]
+    assert custom_result == [["3.14", None, "abc", ""]]

Reply via email to