Repository navigation
TST: Add direct C-extension tests for iterparse.c - #19922
Conversation
|
Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.
|
just curious what you mean by "strict" here. |
I just meant a fully isolated virtual environment... it wasn't meant to signify anything special |
96571f1 to
e378035
Compare
| for event in events: | ||
| assert type(event) is test_data.expected_event_type |
There was a problem hiding this comment.
I think we can do better here. Instead of just checking the type, please also check the contents (or absence thereof) match expectations.
|
|
||
| def test_c_iterparser_malformed_raises_error() -> None: | ||
| """Check that libexpat actively catches the error when we feed it broken XML.""" | ||
| # intentionally cutting off the XML stream mid-tag here to make sure Expat panics. |
There was a problem hiding this comment.
I'd avoid the term "panic" as it doesn't hold any precise meaning in Python or C, but it does in Rust (panics are uncatchable runtime exceptions)
| # intentionally cutting off the XML stream mid-tag here to make sure Expat panics. | |
| # intentionally cutting off the XML stream mid-tag here to make sure Expat raises. |
e378035 to
660afbb
Compare
| is_start = actual_event[0] | ||
| tag_name = actual_event[1] | ||
| node_data = actual_event[2] | ||
|
|
||
| assert is_start is expected_event[0] | ||
| assert tag_name == expected_event[1] | ||
| assert node_data == expected_event[2] |
There was a problem hiding this comment.
That's the same and more (it'll also fail if one of them grows larger than the other)
| is_start = actual_event[0] | |
| tag_name = actual_event[1] | |
| node_data = actual_event[2] | |
| assert is_start is expected_event[0] | |
| assert tag_name == expected_event[1] | |
| assert node_data == expected_event[2] | |
| assert actual_event == excpected_event |
There was a problem hiding this comment.
Okay, so during the previous iteration i figured that the raw C-engine actually yields a 4-tuple (it tacks a (line, column) position tuple onto the very end), so a direct actual_event == expected_event will fail against the 3-item expected tuples. But to add what you've suggested, I've updated it to slice the runtime tuple: assert actual_event[:3] == expected_event
There was a problem hiding this comment.
Thanks for the explanation, but then it's not clear to me why we wouldn't also match the fourth argument
There was a problem hiding this comment.
I initially sliced it out just to keep the test matrix simple and avoid hardcoding line numbers, but I get why we should strictly validate every single piece of data it yields..... fixing it rn
|
|
||
| assert len(events) == len(test_data.expected_events) | ||
|
|
||
| for actual_event, expected_event in zip(events, test_data.expected_events): |
There was a problem hiding this comment.
I suggest to always start with strict=True, and switch to an explicit strict=False only in the rarest cases where it's desirable.
For reference, the strict argument was only added in Python 3.10 and strict=False matches the 3.9 behavior, which is why it's still the default today, but I'd argue it's a really bad choice of a default, in retrospect.
| for actual_event, expected_event in zip(events, test_data.expected_events): | |
| for actual_event, expected_event in zip(events, test_data.expected_events, strict=True): |
| pytest.param( | ||
| XMLTestData( | ||
| xml_bytes=b'<?xml version="1.0"?><root><child>text</child></root>', | ||
| expected_event_type=tuple, |
There was a problem hiding this comment.
Now that you're doing a much deeper equality check, I don't think we need to check the output type separately
cfbb23f to
3fcf716
Compare
There was a problem hiding this comment.
LGTM now
@pllim should probably give her blessing too before we merge
|
Doesn't hurt. Merging. Thanks, all. |
- In get_xml_iterator, the end event of an element that contains other
elements now has empty text. Before, it repeated the text of the last
element inside it. This applies to both the C and pure-Python parsers.
This dramatically reduces memory use for reading large XML files.
- For a VOTable BINARY or BINARY2 table, the end events of BINARY2, DATA
and TABLE elements no longer each hold a full copy of the base64
data stream.
- The C parser frees a text buffer that has grown past 1 MB once its
text is handed over, instead of keeping it until parsing ends.
- VOTable reading decodes the stream with binascii.a2b_base64 directly,
which skips a full copy of the stream text. Base64 text is always
ASCII, so valid files decode exactly as before. Only the error for a
malformed stream with a non-ASCII character changes: it is now
ValueError ("string argument should contain only ASCII characters")
instead of UnicodeEncodeError, which is a ValueError subclass.
- Reading the 2.5 GB file from issue astropy#8946 now needs about 6.8 GB of
memory at its peak, down from about 17 GB.
- Update the one test that expected the repeated text, which was just
a coverage test pinning current behavior from PR astropy#19922.
- Add tests for both parsers and for elements holding more than 1 MB
of text.
- In get_xml_iterator, the end event of an element that contains other
elements now has empty text. Before, it repeated the text of the last
element inside it. This applies to both the C and pure-Python parsers.
This dramatically reduces memory use for reading large XML files.
- For a VOTable BINARY or BINARY2 table, the end events of BINARY2, DATA
and TABLE elements no longer each hold a full copy of the base64
data stream.
- The C parser frees a text buffer that has grown past 1 MB once its
text is handed over, instead of keeping it until parsing ends.
- VOTable reading decodes the stream with binascii.a2b_base64 directly,
which skips a full copy of the stream text. Base64 text is always
ASCII, so valid files decode exactly as before. Only the error for a
malformed stream with a non-ASCII character changes: it is now
ValueError ("string argument should contain only ASCII characters")
instead of UnicodeEncodeError, which is a ValueError subclass.
- Reading the 2.5 GB file from issue astropy#8946 now needs about 6.8 GB of
memory at its peak, down from about 17 GB.
- Update the one test that expected the repeated text, which was just
a coverage test pinning current behavior from PR astropy#19922.
- Add tests for both parsers and for elements holding more than 1 MB
of text.
- In get_xml_iterator, the end event of an element that contains other
elements now has empty text. Before, it repeated the text of the last
element inside it. This applies to both the C and pure-Python parsers.
This dramatically reduces memory use for reading large XML files.
- For a VOTable BINARY or BINARY2 table, the end events of BINARY2, DATA
and TABLE elements no longer each hold a full copy of the base64
data stream.
- The C parser frees a text buffer that has grown past 1 MB once its
text is handed over, instead of keeping it until parsing ends.
- VOTable reading decodes the stream with binascii.a2b_base64 directly,
which skips a full copy of the stream text. Base64 text is always
ASCII, so valid files decode exactly as before. Only the error for a
malformed stream with a non-ASCII character changes: it is now
ValueError ("string argument should contain only ASCII characters")
instead of UnicodeEncodeError, which is a ValueError subclass.
- Reading the 2.5 GB file from issue astropy#8946 now needs about 6.8 GB of
memory at its peak, down from about 17 GB.
- Update the one test that expected the repeated text, which was just
a coverage test pinning current behavior from PR astropy#19922.
- Add tests for both parsers and for elements holding more than 1 MB
of text.
- In get_xml_iterator, the end event of an element that contains other
elements now has empty text. Before, it repeated the text of the last
element inside it. This applies to both the C and pure-Python parsers.
This dramatically reduces memory use for reading large XML files.
- For a VOTable BINARY or BINARY2 table, the end events of BINARY2, DATA
and TABLE elements no longer each hold a full copy of the base64
data stream.
- The C parser frees a text buffer that has grown past 1 MB once its
text is handed over, instead of keeping it until parsing ends.
- VOTable reading decodes the stream with binascii.a2b_base64 directly,
which skips a full copy of the stream text. Base64 text is always
ASCII, so valid files decode exactly as before. Only the error for a
malformed stream with a non-ASCII character changes: it is now
ValueError ("string argument should contain only ASCII characters")
instead of UnicodeEncodeError, which is a ValueError subclass.
- Reading the 2.5 GB file from issue astropy#8946 now needs about 6.8 GB of
memory at its peak, down from about 17 GB.
- Update the one test that expected the repeated text, which was just
a coverage test pinning current behavior from PR astropy#19922.
- Add tests for both parsers and for elements holding more than 1 MB
of text.
* Stop XML end events from repeating the text of nested elements
- In get_xml_iterator, the end event of an element that contains other
elements now has empty text. Before, it repeated the text of the last
element inside it. This applies to both the C and pure-Python parsers.
This dramatically reduces memory use for reading large XML files.
- For a VOTable BINARY or BINARY2 table, the end events of BINARY2, DATA
and TABLE elements no longer each hold a full copy of the base64
data stream.
- The C parser frees a text buffer that has grown past 1 MB once its
text is handed over, instead of keeping it until parsing ends.
- VOTable reading decodes the stream with binascii.a2b_base64 directly,
which skips a full copy of the stream text. Base64 text is always
ASCII, so valid files decode exactly as before. Only the error for a
malformed stream with a non-ASCII character changes: it is now
ValueError ("string argument should contain only ASCII characters")
instead of UnicodeEncodeError, which is a ValueError subclass.
- Reading the 2.5 GB file from issue #8946 now needs about 6.8 GB of
memory at its peak, down from about 17 GB.
- Update the one test that expected the repeated text, which was just
a coverage test pinning current behavior from PR #19922.
- Add tests for both parsers and for elements holding more than 1 MB
of text.
* Update changelog numbers and mention more accurate warning positions
- Give the memory figures in decimal GB, matching the PR description
- Note that end events now report a position near their own closing tag,
so VOTable warnings and validation output point at the right line.
* Update performance changelog to be more general
* Add an API changelog entry for the get_xml_iterator change
Description:
Continuing with my GSoC roadmap to harden Astropy's compiled extensions, this PR focuses on
astropy/utils/xml/src/iterparse.c.Previously, this module was only tested indirectly through the high-level
_fast_iterparsePython wrapper. To safely prepare this low-level layer for the upcoming APE split, we need to prove that the compiled C-engine can handle memory sizing, encoding, and string parsing completely on its own.What This PR Does
This introduces
test_c_iterparse.py, a dedicated test suite that strictly targets the C-boundary in pure isolation:Bypassed the public Python API entirely by directly importing and testing the
_iterparser.sobinary.Replaced unreadable parameter tuples with a frozen, kw-only
XMLTestDatadataclass. This tightly links the input byte streams to the expected outputs.Included tests for standard parsing, ISO-8859-1 decoding, and deliberately malformed byte streams to ensure
libexpatcorrectly throws itsno element found/unclosed tokenerrors back over the C-boundary.Testing
The tests pass cleanly locally within a strict
uvvirtual environment.Looking forward to your feedback! Happy to adjust anything if needed.