gh-143715: Deprecate incomplete initialization of struct.Struct()#145580
gh-143715: Deprecate incomplete initialization of struct.Struct()#145580serhiy-storchaka wants to merge 35 commits intopython:mainfrom
Conversation
* ``Struct.__new__()`` will require a mandatory argument (format) * Calls of ``__init__()`` method on initialized Struct are deprecated
This make format argument in the __init__() - optional. If it's missing, the object must be already initialized in __new__().
Co-authored-by: Victor Stinner <vstinner@python.org>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This catch current pattern for Struct's subclassing like
class MyStruct(Struct):
def __init__(self):
super().__init__('>h')
| :meth:`~object.__init__` method on initialized :class:`~struct.Struct` | ||
| objects is deprecated and will be removed in Python 3.20. | ||
|
|
||
| (Contributed by Sergey B Kirpichev in :gh:`143715`.) |
There was a problem hiding this comment.
| (Contributed by Sergey B Kirpichev in :gh:`143715`.) | |
| (Contributed by Serhiy Storchaka in :gh:`143715`.) |
Doc/whatsnew/3.15.rst
Outdated
| :meth:`~object.__init__` method on initialized :class:`~struct.Struct` | ||
| objects is deprecated and will be removed in Python 3.20. | ||
|
|
||
| (Contributed by Sergey B Kirpichev in :gh:`143715`.) |
There was a problem hiding this comment.
| (Contributed by Sergey B Kirpichev in :gh:`143715`.) | |
| (Contributed by Serhiy Storchaka in :gh:`143715`.) |
Lib/test/test_struct.py
Outdated
| def check_sizeof(self, format_str, number_of_codes): | ||
| # The size of 'PyStructObject' | ||
| totalsize = support.calcobjsize('2n3P') | ||
| totalsize = support.calcobjsize('2n3P1?') |
There was a problem hiding this comment.
| totalsize = support.calcobjsize('2n3P1?') | |
| totalsize = support.calcobjsize('2n3P?') |
There was a problem hiding this comment.
It should be '2n3P?0P', with padding.
| super().__init__('>h') | ||
|
|
||
| my_struct = MyStruct('>h') | ||
| self.assertEqual(my_struct.pack(12345), b'\x30\x39') |
There was a problem hiding this comment.
I think this will be more clear, per Victor's suggestion:
| self.assertEqual(my_struct.pack(12345), b'\x30\x39') | |
| self.assertEqual(my_struct.format, '>h') |
(and in all cases below too)
There was a problem hiding this comment.
What if self->s_format and and self->s_codes are de-synchronized? The original test used Struct.pack().
There was a problem hiding this comment.
The original test used Struct.pack().
That's true. But I think that with format field tests will be more clear.
What if self->s_format and and self->s_codes are de-synchronized?
I see, you test some such cases with bad characters.
There was a problem hiding this comment.
I added assertions for format, but we need also functional tests, because internals can be initialized twice, and if something goes wrong, we will and with inconsistent format and pack(). See new cases in test_Struct_reinitialization. We should fix this in other issue.
| def __init__(self, *args, **kwargs): | ||
| super().__init__('>h') | ||
|
|
||
| my_struct = MyStruct('>h') |
There was a problem hiding this comment.
Why this not raises a warning? I think we should warn in all cases, where Struct.__init__() was explicitly called. // #143659 (comment)
There was a problem hiding this comment.
Because this works with old and with new code. Both __new__ and __init__ take the same argument.
The code
class MyStruct(struct.Struct):
def __init__(self, format):
super().__init__(format)
# some other initializationworks now and will work in future.
|
|
||
| my_struct = MyStruct('>h') | ||
| self.assertEqual(my_struct.pack(12345), b'\x30\x39') | ||
| my_struct = MyStruct(format='>h') |
There was a problem hiding this comment.
Ah, I entirely forgot that format is a positional or keyword argument in the master. I'll fix my patch.
But again, this should emit a warning.
| # New way, no warnings: | ||
| class MyStruct(struct.Struct): | ||
| def __new__(cls, newargs, initargs): | ||
| return super().__new__(cls, *newargs) | ||
| def __init__(self, newargs, initargs): | ||
| if initargs is not None: | ||
| super().__init__(*initargs) | ||
|
|
||
| my_struct = MyStruct(('>h',), ('>h',)) |
There was a problem hiding this comment.
Again, why no warnings here?
BTW, I doubt this usage pattern come from reality ;-)
There was a problem hiding this comment.
Because __new__() and __init__() are called with the same argument, as expected. You are supposed to do this if you write a code that works in all versions.
As for other cases, having both custom __new__() and __init__() which call corresponding parent's methods with different arguments is unusual, but this need to be tested. I found bugs in my code when added these tests.
skirpichev
left a comment
There was a problem hiding this comment.
This looks ok for me and have much better test coverage than my pr #143659.
Though, I worry that it's too complex: #143659 (comment)
| super().__init__('>h') | ||
|
|
||
| my_struct = MyStruct('>h') | ||
| self.assertEqual(my_struct.pack(12345), b'\x30\x39') |
There was a problem hiding this comment.
The original test used Struct.pack().
That's true. But I think that with format field tests will be more clear.
What if self->s_format and and self->s_codes are de-synchronized?
I see, you test some such cases with bad characters.
serhiy-storchaka
left a comment
There was a problem hiding this comment.
The code is complex because the problem is complex.
We should minimize user inconvenience. If the code works in past and future versions, it should not emit warnings. There should be a way to write a warning-free code compatible with old versions (because you usually support more than one Python versions), and it should be valid in future versions.
If ignore this, we could just make a breaking change without deprecation period. And we do this if there is no other way (for example, the format attribute is now a string, not a bytes object like in older versions -- it was impossible to make this change not abrupt).
| super().__init__('>h') | ||
|
|
||
| my_struct = MyStruct('>h') | ||
| self.assertEqual(my_struct.pack(12345), b'\x30\x39') |
There was a problem hiding this comment.
I added assertions for format, but we need also functional tests, because internals can be initialized twice, and if something goes wrong, we will and with inconsistent format and pack(). See new cases in test_Struct_reinitialization. We should fix this in other issue.
As I said in #143659 (comment), this requirement looks too strong for me. To support several versions, people could also use CC @encukou, would you mind to review this pr? |
Modules/_struct.c
Outdated
| return -1; | ||
| } | ||
| Py_SETREF(self->s_format, format); | ||
| if (prepare_s(self)) { |
There was a problem hiding this comment.
What do you think of restoring s_format to its previous value if prepare_s() fails?
Lib/test/test_struct.py
Outdated
| def __new__(cls, *args, **kwargs): | ||
| return super().__new__(cls, '>h') | ||
|
|
||
| my_struct = MyStruct('>h') |
There was a problem hiding this comment.
You can factorize some tests using ony 1 positional argument:
for format in ('>h', '<h', 42, '$', '\u20ac'):
with self.subTest(format=format):
my_struct = MyStruct(format)
self.assertEqual(my_struct.format, '>h')
self.assertEqual(my_struct.pack(12345), b'\x30\x39')
Lib/test/test_struct.py
Outdated
| with self.assertWarns(DeprecationWarning): | ||
| with self.assertRaises(struct.error): | ||
| s.__init__('$') | ||
| self.assertEqual(s.format, '$') |
There was a problem hiding this comment.
I would prefer to restore the old format if __init__() tries to set an invalid format string.
Struct.__new__()will require a mandatory argument (format)__init__()method on initialized Struct are deprecatedThis is an evolution of #143659, but since virtually all code and test were rewritten I created a new PR.
📚 Documentation preview 📚: https://cpython-previews--145580.org.readthedocs.build/