From 62a324e740cf0f19a219e358304bce24bd0212de Mon Sep 17 00:00:00 2001 From: youknowdot Date: Fri, 9 Oct 2026 21:21:36 +0900 Subject: [PATCH] Preserve CSV dialect conversion errors and ordering Collect raw dialect fields and keyword overrides before converting them in CPython 3.14 order. Preserve truth-conversion exceptions, read classes without instantiating them, and retain defaulting for failed attribute lookup. Use one shared parser for reader, writer, registry, and native dialect construction. Extend the existing CSV snippet with class, instance, direct-construction, lookup, override, conversion-order, and argument-binding regressions. Assisted-by: OpenAI Codex:model identifier unavailable --- crates/stdlib/src/csv.rs | 525 ++++++++--------------------- extra_tests/snippets/stdlib_csv.py | 243 +++++++++++++ 2 files changed, 390 insertions(+), 378 deletions(-) diff --git a/crates/stdlib/src/csv.rs b/crates/stdlib/src/csv.rs index b07f9c644ad..51bba58c91e 100644 --- a/crates/stdlib/src/csv.rs +++ b/crates/stdlib/src/csv.rs @@ -6,7 +6,7 @@ mod _csv { use crate::vm::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, - builtins::{PyBaseExceptionRef, PyInt, PyNone, PyStr, PyType, PyTypeRef, PyUtf8StrRef}, + builtins::{PyBaseExceptionRef, PyInt, PyStr, PyType, PyTypeRef, PyUtf8StrRef}, function::{ArgIterable, ArgumentError, FromArgs, FuncArgs, OptionalArg, Param}, protocol::{PyIter, PyIterReturn, PyNumber}, types::{Callable, Constructor, IterNext, Iterable, SelfIter}, @@ -88,12 +88,10 @@ mod _csv { } impl Constructor for PyDialect { - type Args = PyObjectRef; + type Args = FormatOptions; - fn py_new(_cls: &Py, obj: Self::Args, vm: &VirtualMachine) -> PyResult { - let dialect = Self::try_from_object(vm, obj)?; - validate_dialect(vm, &dialect)?; - Ok(dialect) + fn py_new(_cls: &Py, opts: Self::Args, vm: &VirtualMachine) -> PyResult { + opts.result(vm) } } @@ -140,96 +138,32 @@ mod _csv { } } - /// Parses the delimiter from a Python object and returns its ASCII value. - /// - /// This function attempts to extract the 'delimiter' attribute from the given Python object and ensures that the attribute is a single-character string. If successful, it returns the ASCII value of the character. If the attribute is not a single-character string, an error is returned. - /// - /// # Arguments - /// - /// * `vm` - A reference to the VirtualMachine, used for executing Python code and manipulating Python objects. - /// * `obj` - A reference to the PyObjectRef from which the 'delimiter' attribute is to be parsed. - /// - /// # Returns - /// - /// If successful, returns a `PyResult` representing the ASCII value of the 'delimiter' attribute. If unsuccessful, returns a `PyResult` containing an error message. - /// - /// # Errors - /// - /// This function can return the following errors: - /// - /// * If the 'delimiter' attribute is not a single-character string, a type error is returned. - /// * If the 'obj' is not of string type and does not have a 'delimiter' attribute, a type error is returned. - fn parse_delimiter_from_obj(vm: &VirtualMachine, obj: &PyObject) -> PyResult { - if let Ok(attr) = obj.get_attr("delimiter", vm) { - parse_delimiter_from_obj(vm, &attr) - } else { - match_class!(match obj.to_owned() { - s @ PyStr => { - parse_single_char(&s, |len| { - vm.new_type_error(format!( - r#""delimiter" must be a unicode character, not a string of length {len}"# - )) - }) - } - attr => { - Err(vm.new_type_error(format!( - r#""delimiter" must be a unicode character, not {}"#, - attr.class().name() - ))) - } - }) + fn parse_char( + vm: &VirtualMachine, + obj: &PyObject, + name: &str, + allow_none: bool, + ) -> PyResult> { + if allow_none && vm.is_none(obj) { + return Ok(None); } - } - - fn parse_quotechar_from_obj(vm: &VirtualMachine, obj: &PyObject) -> PyResult> { - match_class!(match obj.get_attr("quotechar", vm)? { - s @ PyStr => { - Ok(Some(parse_single_char(&s, |len| { - new_csv_error( - vm, - format!( - r#""quotechar" must be a unicode character or None, not a string of length {len}"# - ), - ) - })?)) - } - _n @ PyNone => { - Ok(None) - } - attr => { - Err(new_csv_error( - vm, - format!( - r#""quotechar" must be a unicode character or None, not {}"#, - attr.class().name() - ), - )) - } - }) - } - - fn parse_escapechar_from_obj(vm: &VirtualMachine, obj: &PyObject) -> PyResult> { - match_class!(match obj.get_attr("escapechar", vm)? { - s @ PyStr => { - Ok(Some(parse_single_char(&s, |len| { - new_csv_error( - vm, - format!( - r#""escapechar" must be a unicode character or None, not a string of length {len}"# - ), - ) - })?)) - } - _n @ PyNone => { - Ok(None) - } - attr => { - Err(vm.new_type_error(format!( - r#""escapechar" must be a unicode character or None, not {}"#, - attr.class().name() - ))) - } + let expected = if allow_none { + "a unicode character or None" + } else { + "a unicode character" + }; + let value = obj.downcast_ref::().ok_or_else(|| { + vm.new_type_error(format!( + r#""{name}" must be {expected}, not {}"#, + obj.class().name() + )) + })?; + parse_single_char(value, |len| { + vm.new_type_error(format!( + r#""{name}" must be {expected}, not a string of length {len}"# + )) }) + .map(Some) } fn parse_lineterminator<'a>(vm: &VirtualMachine, s: &'a Py) -> PyResult<&'a str> { @@ -237,25 +171,6 @@ mod _csv { .ok_or_else(|| new_csv_error(vm, r#""lineterminator" must be a string"#)) } - fn prase_lineterminator_from_obj(vm: &VirtualMachine, obj: &PyObject) -> PyResult { - match_class!(match obj.get_attr("lineterminator", vm)? { - s @ PyStr => { - // Store the full line terminator string. CPython accepts an - // arbitrary-length terminator; the manual writer paths emit it - // verbatim and the csv-core writer path appends it after a - // sentinel terminator (see `writerow`). - let value = parse_lineterminator(vm, &s)?; - Ok(value.to_owned()) - } - attr => { - Err(vm.new_type_error(format!( - r#""lineterminator" must be a string, not {}"#, - attr.class().name() - ))) - } - }) - } - fn parse_single_char( s: &Py, error: impl Fn(usize) -> PyBaseExceptionRef, @@ -268,51 +183,6 @@ mod _csv { u8::try_from(ch.to_u32()).map_err(|_| error(s.char_len())) } - fn prase_quoting_from_obj(vm: &VirtualMachine, obj: &PyObject) -> PyResult { - match_class!(match obj.get_attr("quoting", vm)? { - i @ PyInt => { - Ok(i.try_to_primitive::(vm)? - .try_into() - .map_err(|_| vm.new_type_error(r#"bad "quoting" value"#))?) - } - attr => { - Err(vm.new_type_error(format!( - r#""quoting" must be string or None, not {}"#, - attr.class().name() - ))) - } - }) - } - - impl TryFromObject for PyDialect { - fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult { - let delimiter = parse_delimiter_from_obj(vm, &obj)?; - let quotechar = parse_quotechar_from_obj(vm, &obj)?; - let escapechar = parse_escapechar_from_obj(vm, &obj)?; - let doublequote = obj.get_attr("doublequote", vm)?.try_to_bool(vm)?; - let skipinitialspace = obj.get_attr("skipinitialspace", vm)?.try_to_bool(vm)?; - let lineterminator = prase_lineterminator_from_obj(vm, &obj)?; - let quoting = prase_quoting_from_obj(vm, &obj)?; - - let strict = if let Ok(t) = obj.get_attr("strict", vm) { - t.try_to_bool(vm).unwrap_or(false) - } else { - false - }; - - Ok(Self { - delimiter, - quotechar, - escapechar, - doublequote, - skipinitialspace, - lineterminator, - quoting, - strict, - }) - } - } - #[derive(FromArgs)] struct DialectName { #[pyarg(any)] @@ -492,79 +362,17 @@ mod _csv { } } - #[derive(Default)] - enum DialectItem { - Str(String), - Obj(PyDialect), - #[default] - None, - } - #[derive(Default)] struct FormatOptions { - dialect: DialectItem, - delimiter: Option, - quotechar: Option>, - escapechar: Option>, - doublequote: Option, - skipinitialspace: Option, - lineterminator: Option, - quoting: Option, - strict: Option, - } - - /// prase a dialect item from a Python argument and returns a `DialectItem` or an `ArgumentError`. - /// - /// This function takes a reference to the VirtualMachine and a PyObjectRef as input and attempts to parse a dialect item from the provided Python argument. It returns a `DialectItem` if successful, or an `ArgumentError` if unsuccessful. - /// - /// # Arguments - /// - /// * `vm` - A reference to the VirtualMachine, used for executing Python code and manipulating Python objects. - /// * `obj` - The PyObjectRef from which the dialect item is to be parsed. - /// - /// # Returns - /// - /// If successful, returns a `Result` representing the parsed dialect item. If unsuccessful, returns an `ArgumentError`. - /// - /// # Errors - /// - /// This function can return the following errors: - /// - /// * If the provided object is a PyStr, it returns a `DialectItem::Str` containing the string value. - /// * If the provided object is PyNone, it returns an `ArgumentError` with the message "InvalidKeywordArgument('dialect')". - /// * If the provided object is a PyType, it attempts to create a PyDialect from the object and returns a `DialectItem::Obj` containing the PyDialect if successful. If unsuccessful, it returns an `ArgumentError` with the message "InvalidKeywordArgument('dialect')". - /// * If the provided object is none of the above types, it attempts to create a PyDialect from the object and returns a `DialectItem::Obj` containing the PyDialect if successful. If unsuccessful, it returns an `ArgumentError` with the message "InvalidKeywordArgument('dialect')". - fn prase_dialect_item_from_arg( - vm: &VirtualMachine, - obj: PyObjectRef, - ) -> Result { - match_class!(match obj { - s @ PyStr => { - let s = s.try_into_utf8(vm).map_err(ArgumentError::Exception)?; - Ok(DialectItem::Str(s.as_str().to_owned())) - } - PyNone => { - Err(ArgumentError::InvalidKeywordArgument("dialect".to_string())) - } - t @ PyType => { - let temp = t - .as_object() - .call(vec![], vm) - .map_err(|_e| ArgumentError::InvalidKeywordArgument("dialect".to_string()))?; - Ok(DialectItem::Obj( - PyDialect::try_from_object(vm, temp).map_err(|_| { - ArgumentError::InvalidKeywordArgument("dialect".to_string()) - })?, - )) - } - obj => { - if let Ok(cur_dialect_item) = PyDialect::try_from_object(vm, obj) { - Ok(DialectItem::Obj(cur_dialect_item)) - } else { - Err(ArgumentError::InvalidKeywordArgument("dialect".to_string())) - } - } - }) + dialect: Option, + delimiter: Option, + doublequote: Option, + escapechar: Option, + lineterminator: Option, + quotechar: Option, + quoting: Option, + skipinitialspace: Option, + strict: Option, } impl FromArgs for FormatOptions { @@ -578,107 +386,16 @@ mod _csv { ]); fn from_args(vm: &VirtualMachine, args: &mut FuncArgs) -> Result { - let dialect = if let Some(dialect) = args.take_positional_keyword("dialect") { - prase_dialect_item_from_arg(vm, dialect)? - } else { - DialectItem::None - }; - - let mut res = Self { - dialect, - ..Default::default() - }; - - if let Some(delimiter) = args.kwargs.swap_remove("delimiter") { - res.delimiter = Some(parse_delimiter_from_obj(vm, &delimiter)?); - } - - if let Some(escapechar) = args.kwargs.swap_remove("escapechar") { - res.escapechar = match_class!(match escapechar { - s @ PyStr => Some(Some(parse_single_char(&s, |_| { - vm.new_type_error(r#""escapechar" must be a 1-character string"#) - })?)), - PyNone => Some(None), - _ => { - return Err(ArgumentError::Exception( - vm.new_type_error(r#""escapechar" must be a 1-character string"#), - )); - } - }) - }; - - if let Some(lineterminator) = args.kwargs.swap_remove("lineterminator") { - let s = lineterminator.downcast_ref::().ok_or_else(|| { - vm.new_type_error(format!( - r#""lineterminator" must be a string, not {}"#, - lineterminator.class().name() - )) - })?; - let value = parse_lineterminator(vm, s)?; - res.lineterminator = Some(value.to_owned()); - }; - - if let Some(doublequote) = args.kwargs.swap_remove("doublequote") { - res.doublequote = Some( - doublequote - .try_to_bool(vm) - .map_err(|_| vm.new_type_error(r#""doublequote" must be a bool"#))?, - ) - }; - - if let Some(skipinitialspace) = args.kwargs.swap_remove("skipinitialspace") { - res.skipinitialspace = Some( - skipinitialspace - .try_to_bool(vm) - .map_err(|_| vm.new_type_error(r#""skipinitialspace" must be a bool"#))?, - ) - }; - - if let Some(quoting) = args.kwargs.swap_remove("quoting") { - res.quoting = match_class!(match quoting { - i @ PyInt => - Some(i.try_to_primitive::(vm)?.try_into().map_err(|_e| { - ArgumentError::InvalidKeywordArgument("quoting".to_string()) - })?), - _ => { - // let msg = r#""quoting" must be a int enum"#; - return Err(ArgumentError::InvalidKeywordArgument("quoting".to_string())); - } - }); - }; - - if let Some(quotechar) = args.kwargs.swap_remove("quotechar") { - res.quotechar = match_class!(match quotechar { - s @ PyStr => Some(Some(parse_single_char(&s, |_| { - vm.new_type_error(r#""quotechar" must be a 1-character string"#) - })?)), - PyNone => { - if res - .quoting - .is_some_and(|quoting| quoting != QuoteStyle::None) - { - return Err(ArgumentError::Exception( - vm.new_type_error("quotechar must be set if quoting enabled"), - )); - } - Some(None) - } - _o => { - return Err( - rustpython_vm::function::ArgumentError::InvalidKeywordArgument( - "quotechar".to_string(), - ), - ); - } - }) - }; - - if let Some(strict) = args.kwargs.swap_remove("strict") { - res.strict = Some( - strict - .try_to_bool(vm) - .map_err(|_| vm.new_type_error(r#""strict" must be a int enum"#))?, - ) + let res = Self { + dialect: args.take_positional_keyword("dialect"), + delimiter: args.kwargs.swap_remove("delimiter"), + doublequote: args.kwargs.swap_remove("doublequote"), + escapechar: args.kwargs.swap_remove("escapechar"), + lineterminator: args.kwargs.swap_remove("lineterminator"), + quotechar: args.kwargs.swap_remove("quotechar"), + quoting: args.kwargs.swap_remove("quoting"), + skipinitialspace: args.kwargs.swap_remove("skipinitialspace"), + strict: args.kwargs.swap_remove("strict"), }; if let Some(last_arg) = args.kwargs.pop() { @@ -688,7 +405,6 @@ mod _csv { vm.new_unexpected_keyword_type_error(None, &last_arg.0.to_string()), )); } - Ok(res) } } @@ -748,60 +464,113 @@ mod _csv { } impl FormatOptions { - fn update_py_dialect(&self, mut res: PyDialect) -> PyDialect { - macro_rules! check_and_fill { - ($res:ident, $e:ident) => {{ - if let Some(t) = self.$e { - $res.$e = t; - } - }}; + fn result(mut self, vm: &VirtualMachine) -> PyResult { + if let Some(obj) = self.dialect.take() { + let obj = if obj.downcast_ref::().is_some() { + get_dialect(DialectName { name: obj }, vm)? + .into_ref(&vm.ctx) + .into() + } else { + obj + }; + // CPython ignores attribute lookup errors, but not errors converting + // the retrieved values. Read all non-overridden attributes first. + macro_rules! fill_from_dialect { + ($($name:ident),* $(,)?) => { + $(if self.$name.is_none() { + self.$name = obj.get_attr(stringify!($name), vm).ok(); + })* + }; + } + fill_from_dialect!( + delimiter, + doublequote, + escapechar, + lineterminator, + quotechar, + quoting, + skipinitialspace, + strict, + ); + self.dialect = Some(obj); } - check_and_fill!(res, delimiter); - // check_and_fill!(res, quotechar); - check_and_fill!(res, delimiter); - check_and_fill!(res, doublequote); - check_and_fill!(res, skipinitialspace); - - if let Some(t) = self.escapechar { - res.escapechar = t; + let delimiter = match &self.delimiter { + Some(obj) => parse_char(vm, obj, "delimiter", false)?.unwrap(), + None => b',', }; - - if let Some(t) = self.quotechar { - res.quotechar = t; + let doublequote = match &self.doublequote { + Some(obj) => obj.try_to_bool(vm)?, + None => true, }; - - check_and_fill!(res, quoting); - if let Some(t) = &self.lineterminator { - res.lineterminator.clone_from(t); + let escapechar = match &self.escapechar { + Some(obj) => parse_char(vm, obj, "escapechar", true)?, + None => None, }; - check_and_fill!(res, strict); - res - } - - fn result(&self, vm: &VirtualMachine) -> PyResult { - let dialect = match &self.dialect { - DialectItem::Str(name) => { - let g = GLOBAL_HASHMAP.lock(); - if let Some(dialect) = g.get(name) { - Ok(self.update_py_dialect(dialect.clone())) - } else { - Err(new_csv_error(vm, format!("{name} is not registered."))) + let lineterminator = match &self.lineterminator { + Some(obj) => { + let value = obj.downcast_ref::().ok_or_else(|| { + vm.new_type_error(format!( + r#""lineterminator" must be a string, not {}"#, + obj.class().name() + )) + })?; + // Store the full line terminator string. CPython accepts an + // arbitrary-length terminator; the manual writer paths emit it + // verbatim and the csv-core writer path appends it after a + // sentinel terminator (see `writerow`). + parse_lineterminator(vm, value)?.to_owned() + } + None => "\r\n".to_owned(), + }; + let quotechar = match &self.quotechar { + Some(obj) => parse_char(vm, obj, "quotechar", true)?, + None => Some(b'"'), + }; + let quoting = match &self.quoting { + Some(obj) => { + if !obj.class().is(vm.ctx.types.int_type) { + return Err(vm.new_type_error(format!( + r#""quoting" must be an integer, not {}"#, + obj.class().name() + ))); } - // TODO: Maybe need to update the obj from HashMap + obj.downcast_ref::() + .unwrap() + .try_to_primitive::(vm)? } - DialectItem::Obj(o) => Ok(self.update_py_dialect(o.clone())), - DialectItem::None => Ok(self.update_py_dialect(PyDialect { - delimiter: b',', - quotechar: Some(b'"'), - escapechar: None, - doublequote: true, - skipinitialspace: false, - lineterminator: "\r\n".to_owned(), - quoting: QuoteStyle::Minimal, - strict: false, - })), - }?; + None => QuoteStyle::Minimal as i32, + }; + let skipinitialspace = match &self.skipinitialspace { + Some(obj) => obj.try_to_bool(vm)?, + None => false, + }; + let strict = match &self.strict { + Some(obj) => obj.try_to_bool(vm)?, + None => false, + }; + + // Validate the quoting value only after all field conversions. + let quoting = QuoteStyle::try_from(quoting as isize) + .map_err(|_| vm.new_type_error(r#"bad "quoting" value"#))?; + let quoting = if quotechar.is_none() && self.quoting.is_none() { + QuoteStyle::None + } else { + quoting + }; + if quotechar.is_none() && quoting != QuoteStyle::None { + return Err(vm.new_type_error("quotechar must be set if quoting enabled")); + } + let dialect = PyDialect { + delimiter, + quotechar, + escapechar, + doublequote, + skipinitialspace, + lineterminator, + quoting, + strict, + }; validate_dialect(vm, &dialect)?; Ok(dialect) } diff --git a/extra_tests/snippets/stdlib_csv.py b/extra_tests/snippets/stdlib_csv.py index c80847bc741..08adeaf5294 100644 --- a/extra_tests/snippets/stdlib_csv.py +++ b/extra_tests/snippets/stdlib_csv.py @@ -361,3 +361,246 @@ def __str__(self): test_quote_nonnumeric_writer() + + +def test_dialect_truth_conversion(): + def registered(dialect, **kwargs): + name = "truth_conversion" + try: + csv.register_dialect(name, dialect, **kwargs) + return csv.get_dialect(name) + finally: + if name in csv.list_dialects(): + csv.unregister_dialect(name) + + constructors = ( + lambda dialect, **kwargs: csv.reader([], dialect, **kwargs).dialect, + lambda dialect, **kwargs: csv.writer(io.StringIO(), dialect, **kwargs).dialect, + _csv.Dialect, + registered, + ) + + class TruthError: + def __init__(self, error): + self.error = error + self.calls = 0 + + def __bool__(self): + self.calls += 1 + raise self.error + + class InvalidTruth: + def __bool__(self): + return 1 + + for construct in constructors: + for field in ("strict", "doublequote", "skipinitialspace"): + for error_type in (RuntimeError, AttributeError, TypeError): + for use_class in (False, True): + error = error_type("dialect truth conversion") + value = TruthError(error) + + class Dialect(csv.excel): + def __new__(cls): + raise AssertionError( + "dialect classes must not be instantiated" + ) + + dialect = Dialect if use_class else csv.excel() + setattr(dialect, field, value) + try: + construct(dialect) + except error_type as caught: + assert caught is error + else: + raise AssertionError("dialect truth error was suppressed") + assert value.calls == 1 + + # An explicit option replaces the attribute before conversion. + result = construct(dialect, **{field: True}) + assert getattr(result, field) is True + assert value.calls == 1 + + value = TruthError(error) + try: + construct(None, **{field: value}) + except error_type as caught: + assert caught is error + else: + raise AssertionError("keyword truth error was replaced") + assert value.calls == 1 + + for value in (None, False, True, 0, 1, [], [1]): + dialect = csv.excel() + setattr(dialect, field, value) + assert getattr(construct(dialect), field) is bool(value) + assert getattr(construct(None, **{field: value}), field) is bool(value) + + dialect = csv.excel() + setattr(dialect, field, InvalidTruth()) + with assert_raises(TypeError): + construct(dialect) + with assert_raises(TypeError): + construct(None, **{field: InvalidTruth()}) + + # csv.Dialect's Python validation wrapper translates TypeError to csv.Error. + for error_type in (RuntimeError, AttributeError, TypeError): + error = error_type("direct Python dialect validation") + value = TruthError(error) + + class Dialect(csv.excel): + strict = value + + expected = csv.Error if error_type is TypeError else error_type + try: + Dialect() + except expected as caught: + assert str(caught) == str(error) + if error_type is not TypeError: + assert caught is error + else: + raise AssertionError("Python dialect validation swallowed the error") + assert value.calls == 1 + + +test_dialect_truth_conversion() + + +def test_dialect_lookup_and_conversion_order(): + fields = ( + "delimiter", + "doublequote", + "escapechar", + "lineterminator", + "quotechar", + "quoting", + "skipinitialspace", + "strict", + ) + defaults = (",", True, None, "\r\n", '"', csv.QUOTE_MINIMAL, False, False) + constructors = ( + lambda dialect, **kwargs: csv.reader([], dialect, **kwargs).dialect, + lambda dialect, **kwargs: csv.writer(io.StringIO(), dialect, **kwargs).dialect, + _csv.Dialect, + ) + + for construct in constructors: + for error_type in (AttributeError, RuntimeError, KeyboardInterrupt): + events = [] + + class Missing: + def __getattribute__(self, name): + events.append(name) + raise error_type(name) + + result = construct(Missing()) + assert tuple(getattr(result, field) for field in fields) == defaults + assert events == list(fields) + for dialect in (None, object(), _csv.Dialect(csv.excel)): + result = construct(dialect) + assert tuple(getattr(result, field) for field in fields) == defaults + + events = [] + + class Truth: + def __init__(self, name): + self.name = name + + def __bool__(self): + events.append("bool:" + self.name) + return False + + values = dict(zip(fields, defaults)) + for field in ("doublequote", "skipinitialspace", "strict"): + values[field] = Truth(field) + + class Dialect: + def __getattribute__(self, name): + events.append(name) + return values[name] + + construct(Dialect()) + assert events == list(fields) + [ + "bool:doublequote", + "bool:skipinitialspace", + "bool:strict", + ] + + events.clear() + construct(Dialect(), strict=True, doublequote=True) + assert events == [ + field for field in fields if field not in ("strict", "doublequote") + ] + ["bool:skipinitialspace"] + + events.clear() + construct( + None, + strict=Truth("strict"), + skipinitialspace=Truth("skipinitialspace"), + doublequote=Truth("doublequote"), + ) + assert events == ["bool:doublequote", "bool:skipinitialspace", "bool:strict"] + + # Collect every attribute before starting conversions, even on failure. + events.clear() + values["delimiter"] = "too long" + with assert_raises(TypeError): + construct(Dialect()) + assert events == list(fields) + + error = RuntimeError("truth conversion wins") + + class BadTruth: + def __bool__(self): + raise error + + # Conversion order differs from option-validation order. + for options in ( + {"doublequote": BadTruth(), "escapechar": "too long"}, + {"strict": BadTruth(), "quoting": 42}, + {"strict": BadTruth(), "quotechar": None, "quoting": csv.QUOTE_ALL}, + {"strict": BadTruth(), "delimiter": "\n"}, + ): + try: + construct(None, **options) + except RuntimeError as caught: + assert caught is error + else: + raise AssertionError("conversion order did not preserve truth error") + for options in ( + {"delimiter": "too long", "doublequote": BadTruth()}, + {"lineterminator": None, "skipinitialspace": BadTruth()}, + {"quoting": None, "strict": BadTruth()}, + {"unknown_option": None, "strict": BadTruth()}, + ): + with assert_raises(TypeError): + construct(None, **options) + + class IntegerSubclass(int): + pass + + class Index: + def __index__(self): + raise AssertionError("quoting must not call __index__") + + for value in (True, IntegerSubclass(0), Index()): + with assert_raises(TypeError): + construct(None, quoting=value) + with assert_raises(OverflowError): + construct(None, quoting=2**40, strict=BadTruth()) + + # None quotechar disables quoting only when quoting was not supplied. + assert construct(None, quotechar=None).quoting == csv.QUOTE_NONE + with assert_raises(TypeError): + construct(csv.excel, quotechar=None) + with assert_raises(TypeError): + construct(None, quotechar=None, quoting=csv.QUOTE_MINIMAL) + + assert _csv.Dialect().strict is False + assert _csv.Dialect(strict=True).strict is True + for constructor, arg in ((csv.reader, []), (csv.writer, io.StringIO())): + with assert_raises(TypeError): + constructor(arg, None, None, strict=BadTruth()) + + +test_dialect_lookup_and_conversion_order()