Skip to content

Commit b118cab

Browse files
author
Rodrigo Roldán
committed
perf: Document new error classes
1 parent 789856b commit b118cab

8 files changed

Lines changed: 73 additions & 19 deletions

File tree

README.es.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ print(z.diff_for_humans("2025-06-20", locale="es")) # → hace 5 días
6666
Podés agregar más idiomas creando un archivo en `eones/locales/` con las
6767
traducciones para tu idioma. Por ejemplo, `fr.py` para francés.
6868

69+
Manejo de errores
70+
71+
Eones muestra excepciones claras derivadas de `EonesError`. Las zonas horarias no válidas generan `InvalidTimezoneError`, mientras que las cadenas no analizables generan `InvalidFormatError`.
72+
6973
---
7074

7175
## 🧾 Comparación con otras librerías

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ print(z.diff_for_humans("2025-06-20", locale="es")) # → hace 5 días
6565
You can add more languages by creating a new file in `eones/locales/` with the
6666
translations for your locale. For example, `fr.py` for French.
6767

68+
Error Handling
69+
70+
Eones surfaces clear exceptions derived from `EonesError`. Invalid timezones
71+
raise `InvalidTimezoneError`, while unparsable strings raise
72+
`InvalidFormatError`.
73+
6874
---
6975

7076
## 🧾 Comparison with other libraries

src/eones/core/date.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
from calendar import monthrange
66
from datetime import datetime, timedelta, timezone
77
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
8-
from zoneinfo import ZoneInfo
8+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
99

1010
from eones.constants import VALID_KEYS
11+
from eones.errors import InvalidTimezoneError
1112
from eones.humanize import diff_for_humans as _diff_for_humans
1213

1314
if TYPE_CHECKING: # pragma: no cover - import for type checking only
@@ -31,7 +32,11 @@ def __init__(
3132
tz: Optional[str] = "UTC",
3233
naive: Literal["utc", "local", "raise"] = "raise",
3334
):
34-
self._zone = ZoneInfo(tz)
35+
try:
36+
self._zone = ZoneInfo(tz)
37+
38+
except ZoneInfoNotFoundError as exc:
39+
raise InvalidTimezoneError(tz) from exc
3540

3641
if dt is None:
3742
self._dt = datetime.now(self._zone)
@@ -103,7 +108,7 @@ def __add__(self, delta: Union[timedelta, "Delta"]) -> Date:
103108
Returns:
104109
Date: The resulting shifted date.
105110
"""
106-
from eones.core.delta import Delta
111+
from eones.core.delta import Delta # pylint: disable=import-outside-toplevel
107112

108113
if isinstance(delta, Delta):
109114
return delta.apply(self)
@@ -113,7 +118,10 @@ def __add__(self, delta: Union[timedelta, "Delta"]) -> Date:
113118

114119
return NotImplemented
115120

116-
def __sub__(self, other: Union[Date, timedelta, "Delta"]) -> Union[Date, timedelta]:
121+
def __sub__(
122+
self,
123+
other: Union[Date, timedelta, "Delta"],
124+
) -> Union[Date, timedelta]:
117125
"""Subtract ``other`` from this date.
118126
119127
``other`` may be a :class:`datetime.timedelta` or a
@@ -125,7 +133,7 @@ def __sub__(self, other: Union[Date, timedelta, "Delta"]) -> Union[Date, timedel
125133
Union[Date, timedelta]: ``Date`` or time difference depending on the
126134
operand type.
127135
"""
128-
from eones.core.delta import Delta
136+
from eones.core.delta import Delta # pylint: disable=import-outside-toplevel
129137

130138
if isinstance(other, Delta):
131139
return other.invert().apply(self)
@@ -297,7 +305,12 @@ def from_iso(cls, iso_str: str, tz: Optional[str] = "UTC") -> Date:
297305
Returns:
298306
Date: Parsed Date.
299307
"""
300-
dt = datetime.fromisoformat(iso_str)
308+
try:
309+
dt = datetime.fromisoformat(iso_str)
310+
311+
except ZoneInfoNotFoundError as exc:
312+
raise InvalidTimezoneError(tz) from exc
313+
301314
if dt.tzinfo is None:
302315
dt = dt.replace(tzinfo=ZoneInfo(tz))
303316
return cls(dt, tz)
@@ -313,7 +326,12 @@ def from_unix(cls, timestamp: float, tz: Optional[str] = "UTC") -> Date:
313326
Returns:
314327
Date: Parsed Date.
315328
"""
316-
dt = datetime.fromtimestamp(timestamp, tz=ZoneInfo(tz))
329+
try:
330+
dt = datetime.fromtimestamp(timestamp, tz=ZoneInfo(tz))
331+
332+
except ZoneInfoNotFoundError as exc:
333+
raise InvalidTimezoneError(tz) from exc
334+
317335
return cls(dt, tz)
318336

319337
def is_within(self, other: Date, check_month: bool = True) -> bool:
@@ -403,7 +421,11 @@ def as_zone(self, zone: str) -> datetime:
403421
Returns:
404422
datetime: Datetime in the new timezone.
405423
"""
406-
return self._dt.astimezone(ZoneInfo(zone))
424+
try:
425+
return self._dt.astimezone(ZoneInfo(zone))
426+
427+
except ZoneInfoNotFoundError as exc:
428+
raise InvalidTimezoneError(zone) from exc
407429

408430
def truncate(self, unit: str) -> Date:
409431
"""Truncate the Date to the specified unit (e.g., 'day', 'hour', etc.)."""

src/eones/core/delta.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""core.delta.py"""
22

33
import re
4+
from datetime import timedelta
45
from typing import Dict
56

67
from eones.constants import DELTA_KEYS
@@ -277,8 +278,6 @@ def from_iso(cls, iso: str) -> "Delta":
277278
@classmethod
278279
def from_timedelta(cls, td: "timedelta") -> "Delta":
279280
"""Create a Delta instance from :class:`datetime.timedelta`."""
280-
from datetime import timedelta
281-
282281
if not isinstance(td, timedelta):
283282
raise TypeError(f"'td' must be timedelta, got {type(td).__name__}")
284283

src/eones/core/parser.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
from datetime import datetime
66
from typing import Dict, List, Optional, Union
7-
from zoneinfo import ZoneInfo
7+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
88

99
from eones.constants import VALID_KEYS
1010
from eones.core.date import Date
11+
from eones.errors import InvalidFormatError, InvalidTimezoneError
1112

1213
EonesLike = Union[str, datetime, Dict[str, int], Date]
1314

@@ -29,7 +30,12 @@ def __init__(self, tz: str = "UTC", formats: Optional[List[str]] = None) -> None
2930
tz (str): Timezone string (e.g., 'UTC', 'America/New_York').
3031
formats (Optional[List[str]]): List of datetime string formats to try.
3132
"""
32-
self._zone = ZoneInfo(tz)
33+
try:
34+
self._zone = ZoneInfo(tz)
35+
36+
except ZoneInfoNotFoundError as exc:
37+
raise InvalidTimezoneError(tz) from exc
38+
3339
self._formats = formats if formats else ["%Y-%m-%d", "%d/%m/%Y"]
3440

3541
def parse(
@@ -114,7 +120,9 @@ def _from_str(self, date_str: str) -> "Date":
114120
except ValueError:
115121
continue
116122

117-
raise ValueError(f"Date string '{date_str}' does not match expected formats.")
123+
raise InvalidFormatError(
124+
f"Date string '{date_str}' does not match expected formats {self._formats}"
125+
)
118126

119127
def to_eones_date(self, value: EonesLike) -> Date:
120128
"""

src/eones/core/range.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Tuple
88

99
from eones.core.date import Date
10+
from eones.core.delta import Delta
1011

1112

1213
class Range:
@@ -112,8 +113,6 @@ def custom_range(
112113
Returns:
113114
Tuple[datetime, datetime]: Resulting start and end datetimes.
114115
"""
115-
from eones.core.delta import Delta # local import to avoid circular
116-
117116
if not isinstance(start_delta, Delta) or not isinstance(end_delta, Delta):
118117
raise TypeError("start_delta and end_delta must be Delta instances")
119118

src/eones/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ def __init__(self, message: str = "Invalid or unsupported date format") -> None:
2121
super().__init__(message)
2222

2323

24+
class InvalidTimezoneError(EonesError):
25+
"""Raised when an invalid timezone string is provided."""
26+
27+
def __init__(self, tz: str) -> None:
28+
"""Initialize InvalidTimezoneError with the problematic timezone."""
29+
super().__init__(f"Invalid timezone: {tz}")
30+
31+
2432
class UnsupportedInputError(EonesError):
2533
"""Raised when the input type is not supported by the parser."""
2634

tests/test_parser.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from eones.constants import DEFAULT_FORMATS
88
from eones.core.date import Date
99
from eones.core.parser import Parser
10+
from eones.errors import InvalidFormatError, InvalidTimezoneError
1011

1112
# ==== FIXTURE ====
1213

@@ -16,6 +17,11 @@ def parser():
1617
return Parser(tz="UTC", formats=["%Y-%m-%d"]) # solo año-mes-día
1718

1819

20+
def test_invalid_timezone_raises():
21+
with pytest.raises(InvalidTimezoneError):
22+
Parser(tz="Mars/Phobos")
23+
24+
1925
# ==== VALID INPUTS ====
2026

2127

@@ -41,9 +47,11 @@ def test_parse_existing_date_returns_same_instance(parser):
4147
# ==== INVALID INPUTS ====
4248

4349

44-
@pytest.mark.parametrize("invalid_input", [12345, "15/06/2025"])
45-
def test_parse_invalid_inputs_raise(parser, invalid_input):
46-
with pytest.raises(ValueError):
50+
@pytest.mark.parametrize(
51+
"invalid_input, exc", [(12345, ValueError), ("15/06/2025", InvalidFormatError)]
52+
)
53+
def test_parse_invalid_inputs_raise(parser, invalid_input, exc):
54+
with pytest.raises(exc):
4755
parser.parse(invalid_input)
4856

4957

@@ -107,7 +115,7 @@ def test_multiple_formats_fallback_success():
107115
def test_multiple_formats_fallback_failure():
108116
formats = ["%d/%m/%Y", "%Y-%m-%d"]
109117
p = Parser(tz="UTC", formats=formats)
110-
with pytest.raises(ValueError):
118+
with pytest.raises(InvalidFormatError):
111119
p.parse("15.06.2025") # no matching format
112120

113121

0 commit comments

Comments
 (0)