diff --git a/src/docx/oxml/simpletypes.py b/src/docx/oxml/simpletypes.py index a0fc87d3f..a061bedfe 100644 --- a/src/docx/oxml/simpletypes.py +++ b/src/docx/oxml/simpletypes.py @@ -399,7 +399,10 @@ class ST_TwipsMeasure(XsdUnsignedLong): def convert_from_xml(cls, str_value: str) -> Length: if "i" in str_value or "m" in str_value or "p" in str_value: return ST_UniversalMeasure.convert_from_xml(str_value) - return Twips(int(str_value)) + try: + return Twips(int(str_value)) + except ValueError: + return Twips(int(round(float(str_value)))) @classmethod def convert_to_xml(cls, value: int | Length) -> str: diff --git a/tests/oxml/test_simpletypes.py b/tests/oxml/test_simpletypes.py new file mode 100644 index 000000000..5d430086f --- /dev/null +++ b/tests/oxml/test_simpletypes.py @@ -0,0 +1,38 @@ +"""Unit test suite for the docx.oxml.simpletypes module.""" + +import pytest + +from docx.oxml.simpletypes import ST_TwipsMeasure +from docx.shared import Inches, Twips + + +class DescribeST_TwipsMeasure: + @pytest.mark.parametrize( + ("str_value", "expected_twips"), + [("0.49", 0), ("0.51", 1), ("1.49", 1), ("1.51", 2)], + ) + def it_accepts_fractional_twips(self, str_value: str, expected_twips: int): + value = ST_TwipsMeasure.convert_from_xml(str_value) + + assert value == Twips(expected_twips) + + @pytest.mark.parametrize("str_value", ["120", "9007199254740993"]) + def it_preserves_exact_integer_twips(self, str_value: str): + value = ST_TwipsMeasure.convert_from_xml(str_value) + + assert value == Twips(int(str_value)) + + def it_preserves_unit_suffixed_measurements(self): + value = ST_TwipsMeasure.convert_from_xml("1in") + + assert value == Inches(1) + + @pytest.mark.parametrize("str_value", ["not-a-number", "NaN", "Infinity", "-Infinity"]) + def it_rejects_invalid_or_non_finite_measurements(self, str_value: str): + with pytest.raises((ValueError, OverflowError)): + ST_TwipsMeasure.convert_from_xml(str_value) + + def it_serializes_measurements_as_integer_twips(self): + str_value = ST_TwipsMeasure.convert_to_xml(Twips(42)) + + assert str_value == "42" diff --git a/tests/test_section.py b/tests/test_section.py index 54d665768..df55bf5a0 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -470,6 +470,14 @@ def it_knows_its_page_margins( assert value == expected_value + def it_accepts_a_fractional_right_margin(self, document_part_: Mock): + sectPr = cast(CT_SectPr, element("w:sectPr/w:pgMar{w:right=0.218505859375}")) + section = Section(sectPr, document_part_) + + right_margin = section.right_margin + + assert right_margin == 0 + @pytest.mark.parametrize( ("sectPr_cxml", "margin_prop_name", "value", "expected_cxml"), [