Skip to content

Commit f27444d

Browse files
gh-160106: Set slot-backed dialect attributes via setattr and add tests
1 parent 376074f commit f27444d

2 files changed

Lines changed: 91 additions & 7 deletions

File tree

‎Lib/csv.py‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,21 @@ def __replace__(self, /, **changes):
124124
raise TypeError(f'__replace__() got an unexpected keyword '
125125
f'argument {min(unexpected)!r}')
126126
new = object.__new__(self.__class__)
127-
new.__dict__.update(self.__dict__)
128-
# Copy slot-backed dialect attributes from the instance's MRO,
129-
# since __dict__ only captures attributes stored in __dict__.
127+
if hasattr(self, '__dict__'):
128+
new.__dict__.update(self.__dict__)
129+
# Copy slot-backed dialect attributes from the instance's MRO.
130130
for cls in type(self).__mro__:
131-
for attr in getattr(cls, '__slots__', ()):
132-
if attr in _dialect_attributes and attr not in new.__dict__:
131+
slots = getattr(cls, '__slots__', ())
132+
if isinstance(slots, str):
133+
slots = (slots,)
134+
for attr in slots:
135+
if attr in _dialect_attributes and attr not in changes:
133136
try:
134-
new.__dict__[attr] = getattr(self, attr)
137+
setattr(new, attr, getattr(self, attr))
135138
except AttributeError:
136139
pass
137-
new.__dict__.update(changes)
140+
for attr, value in changes.items():
141+
setattr(new, attr, value)
138142
new._validate()
139143
return new
140144

‎Lib/test/test_csv.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -781,6 +781,86 @@ class mydialect(csv.Dialect):
781781
with self.assertRaisesRegex(TypeError, "'delimeter'"):
782782
copy.replace(dialect, delimeter=':')
783783

784+
def test_replace_dialect_slots(self):
785+
# Dialect with single string __slots__
786+
class SingleSlotDialect(csv.excel):
787+
__slots__ = 'delimiter'
788+
def __init__(self):
789+
self.delimiter = ';'
790+
super().__init__()
791+
792+
d1 = SingleSlotDialect()
793+
r1 = copy.replace(d1, quoting=csv.QUOTE_ALL)
794+
self.assertIsInstance(r1, SingleSlotDialect)
795+
self.assertEqual(r1.delimiter, ';')
796+
self.assertEqual(r1.quoting, csv.QUOTE_ALL)
797+
self.assertEqual(list(csv.reader(['a;b'], r1)), [['a', 'b']])
798+
799+
# Replacing a slot-backed attribute
800+
r1_mod = copy.replace(d1, delimiter=':')
801+
self.assertEqual(r1_mod.delimiter, ':')
802+
self.assertEqual(list(csv.reader(['a:b'], r1_mod)), [['a', 'b']])
803+
804+
# Dialect with tuple __slots__
805+
class MultiSlotDialect(csv.excel):
806+
__slots__ = ('delimiter', 'quotechar')
807+
def __init__(self):
808+
self.delimiter = '|'
809+
self.quotechar = '#'
810+
super().__init__()
811+
812+
d2 = MultiSlotDialect()
813+
r2 = copy.replace(d2, quoting=csv.QUOTE_MINIMAL)
814+
self.assertIsInstance(r2, MultiSlotDialect)
815+
self.assertEqual(r2.delimiter, '|')
816+
self.assertEqual(r2.quotechar, '#')
817+
self.assertEqual(list(csv.reader(['#a|b#|c'], r2)), [['a|b', 'c']])
818+
819+
# Subclassing with __slots__ across multiple inheritance levels
820+
class BaseSlotDialect(csv.excel):
821+
__slots__ = ('delimiter',)
822+
def __init__(self):
823+
self.delimiter = ';'
824+
super().__init__()
825+
826+
class SubSlotDialect(BaseSlotDialect):
827+
__slots__ = ('quotechar', 'escapechar')
828+
def __init__(self):
829+
super().__init__()
830+
self.quotechar = '~'
831+
self.escapechar = '\\'
832+
833+
d3 = SubSlotDialect()
834+
r3 = copy.replace(d3, doublequote=False)
835+
self.assertIsInstance(r3, SubSlotDialect)
836+
self.assertEqual(r3.delimiter, ';')
837+
self.assertEqual(r3.quotechar, '~')
838+
self.assertEqual(r3.escapechar, '\\')
839+
self.assertFalse(r3.doublequote)
840+
self.assertEqual(list(csv.reader(['~a;b~;c'], r3)), [['a;b', 'c']])
841+
842+
# Unset slot attribute on instance
843+
class UnsetSlotDialect(csv.excel):
844+
__slots__ = ('delimiter',)
845+
846+
d4 = UnsetSlotDialect()
847+
r4 = copy.replace(d4, quoting=csv.QUOTE_ALL)
848+
self.assertIsInstance(r4, UnsetSlotDialect)
849+
self.assertFalse(hasattr(r4, 'delimiter'))
850+
851+
# Mapping __slots__
852+
class DictSlotDialect(csv.excel):
853+
__slots__ = {'delimiter': 'doc for delimiter'}
854+
def __init__(self):
855+
self.delimiter = '\t'
856+
super().__init__()
857+
858+
d5 = DictSlotDialect()
859+
r5 = copy.replace(d5, quoting=csv.QUOTE_ALL)
860+
self.assertIsInstance(r5, DictSlotDialect)
861+
self.assertEqual(r5.delimiter, '\t')
862+
self.assertEqual(list(csv.reader(['a\tb'], r5)), [['a', 'b']])
863+
784864
def test_pickle(self):
785865
for name in csv.list_dialects():
786866
dialect = csv.get_dialect(name)

0 commit comments

Comments
 (0)