Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Changelog


## Unreleased

**Fixed**

- Rendering a trimmed-fraction (``F``) field in a format-pattern error message
(e.g. a duplicate or unsupported ``F`` field) raised ``AttributeError``
instead of the intended ``ValueError`` in the pure-Python implementation.


## 0.10.1 (2026-07-03)

**Improved**
Expand Down
3 changes: 3 additions & 0 deletions pysrc/whenever/_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,9 @@ def apply_pending(self, ch: str) -> list[_Element]:
return [_DotFrac(self.width)]
return [_Literal(ch), self]

def __repr__(self) -> str:
return "F" * self.width


class _DotFrac(_Field):
"""Decimal point + trimmed fractional seconds (``.FFF``).
Expand Down
46 changes: 46 additions & 0 deletions tests/test_format_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,52 @@ def test_control_char_rejected(self):
Date(2024, 1, 1).format("YYYY\x00MM")


class TestFracTrimErrorRendering:
"""Rendering a trimmed-fraction (``F``) field inside an error message
must raise the intended ``ValueError``, not crash.

``_FracTrim`` lacked a ``__repr__``, so any error that referenced such a
field (duplicate-field detection, unsupported-field rejection) fell back
to ``_Field.__repr__``, which reads the class-only ``pattern`` annotation
and raised ``AttributeError: '_FracTrim' object has no attribute
'pattern'`` instead. The Rust extension already renders these correctly.
"""

def test_repr_matches_letter_times_count(self):
from whenever._format import _FracExact, _FracTrim

assert repr(_FracTrim(1)) == "F"
assert repr(_FracTrim(3)) == "FFF"
assert repr(_FracTrim(9)) == "FFFFFFFFF"
# mirrors the sibling fixed-width fractional field
assert repr(_FracExact(3)) == "fff"

@pytest.mark.parametrize("pattern", ["fF", "Ff", "ffF", "Fff", "FFFff"])
def test_compile_duplicate_nanos_with_frac_trim(self, pattern):
with pytest.raises(ValueError, match="Duplicate.*nanos"):
compile_pattern(pattern)

@pytest.mark.parametrize("pattern", ["fF", "Ff", "ffF", "Fff"])
def test_format_duplicate_nanos_with_frac_trim(self, pattern):
with pytest.raises(ValueError, match="Duplicate.*nanos"):
Time(1, 2, 3, nanosecond=4).format(pattern)

@pytest.mark.parametrize("pattern", ["fF", "ffF", "Fff"])
def test_parse_duplicate_nanos_with_frac_trim(self, pattern):
with pytest.raises(ValueError, match="Duplicate.*nanos"):
Time.parse("01:02:03", format=pattern)

def test_frac_trim_unsupported_for_date_format(self):
with pytest.raises(ValueError, match="does not support.*F"):
Date(2024, 3, 15).format("F")
with pytest.raises(ValueError, match="does not support.*FFF"):
Date(2024, 3, 15).format("YYYY-MM-DD FFF")

def test_frac_trim_unsupported_for_date_parse(self):
with pytest.raises(ValueError, match="does not support.*F"):
Date.parse("2024-03-15", format="F")


class TestDateFormat:
def test_basic(self):
d = Date(2024, 3, 15)
Expand Down
Loading