Skip to content

Commit 5086d1b

Browse files
committed
Rename formatters submodule and classes to converters
resolves #100
1 parent 759ec58 commit 5086d1b

File tree

18 files changed

+166
-165
lines changed

18 files changed

+166
-165
lines changed

src/undate/converters/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from undate.converters.base import BaseDateConverter as BaseDateConverter
Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
"""
2-
Base class for date format parsing and serializing
2+
Base class for converting date between different formats and calendars.
33
4-
To add support for a new date format:
4+
To add support for a new date format or conversion:
55
6-
- create a new file under undate/dateformat
7-
- extend BaseDateFormat and implement parse and to_string methods
6+
- create a new file or module under undate/converters
7+
- extend BaseDateConverter and implement parse and to_string methods
88
as desired/appropriate
99
10-
It should be loaded automatically and included in the formatters
11-
returned by :meth:`BaseDateFormat.available_formatters`
10+
The new subclass should be loaded automatically and included in the converters
11+
returned by :meth:`BaseDateConverter.available_converters`
1212
1313
"""
1414

@@ -21,11 +21,12 @@
2121
logger = logging.getLogger(__name__)
2222

2323

24-
class BaseDateFormat:
25-
"""Base class for parsing and formatting dates for specific formats."""
24+
class BaseDateConverter:
25+
"""Base class for parsing, formatting, and converting dates to handle
26+
specific formats and different calendars."""
2627

2728
# Subclasses should define a unique name.
28-
name: str = "Base Formatter"
29+
name: str = "Base Converter"
2930

3031
def parse(self, value: str):
3132
# can't add type hint here because of circular import
@@ -40,22 +41,22 @@ def to_string(self, undate) -> str:
4041
# cache import class method to ensure we only import once
4142
@classmethod
4243
@cache
43-
def import_formatters(cls) -> int:
44-
"""Import all undate.dateformat formatters
45-
so that they will be included in available formatters
44+
def import_converters(cls) -> int:
45+
"""Import all undate converters
46+
so that they will be included in available converters
4647
even if not explicitly imported. Only import once.
4748
returns the count of modules imported."""
4849

49-
logger.debug("Loading formatters under undate.dateformat")
50-
import undate.dateformat
50+
logger.debug("Loading converters under undate.converters")
51+
import undate.converters
5152

5253
# load packages under this path with curent package prefix
53-
formatter_path = undate.dateformat.__path__
54-
formatter_prefix = f"{undate.dateformat.__name__}."
54+
converter_path = undate.converters.__path__
55+
converter_prefix = f"{undate.converters.__name__}."
5556

5657
import_count = 0
5758
for importer, modname, ispkg in pkgutil.iter_modules(
58-
formatter_path, formatter_prefix
59+
converter_path, converter_prefix
5960
):
6061
# import everything except the current file
6162
if not modname.endswith(".base"):
@@ -65,7 +66,7 @@ def import_formatters(cls) -> int:
6566
return import_count
6667

6768
@classmethod
68-
def available_formatters(cls) -> Dict[str, Type["BaseDateFormat"]]:
69-
# ensure undate formatters are imported
70-
cls.import_formatters()
69+
def available_converters(cls) -> Dict[str, Type["BaseDateConverter"]]:
70+
# ensure undate converters are imported
71+
cls.import_converters()
7172
return {c.name: c for c in cls.__subclasses__()} # type: ignore
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from undate.converters.edtf.converter import EDTFDateConverter as EDTFDateConverter

src/undate/dateformat/edtf/formatter.py renamed to src/undate/converters/edtf/converter.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
from lark.exceptions import UnexpectedCharacters
44

5+
from undate.converters.base import BaseDateConverter
6+
from undate.converters.edtf.parser import edtf_parser
7+
from undate.converters.edtf.transformer import EDTFTransformer
58
from undate.date import DatePrecision
6-
from undate.dateformat.base import BaseDateFormat
7-
from undate.dateformat.edtf.parser import edtf_parser
8-
from undate.dateformat.edtf.transformer import EDTFTransformer
99
from undate.undate import Undate, UndateInterval
1010

1111
EDTF_UNSPECIFIED_DIGIT: str = "X"
1212

1313

14-
class EDTFDateFormat(BaseDateFormat):
14+
class EDTFDateConverter(BaseDateConverter):
1515
name: str = "EDTF"
1616

1717
def __init__(self):

src/undate/dateformat/iso8601.py renamed to src/undate/converters/iso8601.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from typing import Dict, List, Union
22

3-
from undate.dateformat.base import BaseDateFormat
3+
from undate.converters.base import BaseDateConverter
44
from undate.undate import Undate, UndateInterval
55

66

7-
class ISO8601DateFormat(BaseDateFormat):
7+
class ISO8601DateFormat(BaseDateConverter):
88
# NOTE: do we care about validation? could use regex
99
# but maybe be permissive, warn if invalid but we can parse
1010

src/undate/dateformat/__init__.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/undate/dateformat/edtf/__init__.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/undate/undate.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
# Pre 3.10 requires Union for multiple types, e.g. Union[int, None] instead of int | None
66
from typing import Dict, Optional, Union
77

8+
from undate.converters.base import BaseDateConverter
89
from undate.date import ONE_DAY, ONE_MONTH_MAX, ONE_YEAR, Date, DatePrecision, Timedelta
9-
from undate.dateformat.base import BaseDateFormat
1010

1111

1212
class Undate:
@@ -22,7 +22,7 @@ class Undate:
2222
#: A string to label a specific undate, e.g. "German Unity Date 2022" for Oct. 3, 2022.
2323
#: Labels are not taken into account when comparing undate objects.
2424
label: Union[str, None] = None
25-
formatter: BaseDateFormat
25+
converter: BaseDateConverter
2626
#: precision of the date (day, month, year, etc.)
2727
precision: DatePrecision
2828

@@ -41,7 +41,7 @@ def __init__(
4141
year: Optional[Union[int, str]] = None,
4242
month: Optional[Union[int, str]] = None,
4343
day: Optional[Union[int, str]] = None,
44-
formatter: Optional[BaseDateFormat] = None,
44+
converter: Optional[BaseDateConverter] = None,
4545
label: Optional[str] = None,
4646
):
4747
# keep track of initial values and which values are known
@@ -135,11 +135,13 @@ def __init__(
135135
self.earliest = Date(min_year, min_month, min_day)
136136
self.latest = Date(max_year, max_month, max_day)
137137

138-
if formatter is None:
138+
if converter is None:
139139
# import all subclass definitions; initialize the default
140-
formatter_cls = BaseDateFormat.available_formatters()[self.DEFAULT_FORMAT]
141-
formatter = formatter_cls()
142-
self.formatter = formatter
140+
converter_cls = BaseDateConverter.available_converters()[
141+
self.DEFAULT_FORMAT
142+
]
143+
converter = converter_cls()
144+
self.converter = converter
143145

144146
self.label = label
145147

@@ -162,7 +164,7 @@ def __str__(self) -> str:
162164
# combine, skipping any values that are None
163165
return "-".join([str(p) for p in parts if p is not None])
164166

165-
return self.formatter.to_string(self)
167+
return self.converter.to_string(self)
166168

167169
def __repr__(self) -> str:
168170
if self.label:
@@ -172,21 +174,21 @@ def __repr__(self) -> str:
172174
@classmethod
173175
def parse(cls, date_string, format) -> Union["Undate", "UndateInterval"]:
174176
"""parse a string to an undate or undate interval using the specified format;
175-
for now, only supports named formatters"""
176-
formatter_cls = BaseDateFormat.available_formatters().get(format, None)
177-
if formatter_cls:
177+
for now, only supports named converters"""
178+
converter_cls = BaseDateConverter.available_converters().get(format, None)
179+
if converter_cls:
178180
# NOTE: some parsers may return intervals; is that ok here?
179-
return formatter_cls().parse(date_string)
181+
return converter_cls().parse(date_string)
180182

181183
raise ValueError(f"Unsupported format '{format}'")
182184

183185
def format(self, format) -> str:
184186
"""format this undate as a string using the specified format;
185-
for now, only supports named formatters"""
186-
formatter_cls = BaseDateFormat.available_formatters().get(format, None)
187-
if formatter_cls:
187+
for now, only supports named converters"""
188+
converter_cls = BaseDateConverter.available_converters().get(format, None)
189+
if converter_cls:
188190
# NOTE: some parsers may return intervals; is that ok here?
189-
return formatter_cls().to_string(self)
191+
return converter_cls().to_string(self)
190192

191193
raise ValueError(f"Unsupported format '{format}'")
192194

@@ -459,10 +461,10 @@ def __str__(self) -> str:
459461

460462
def format(self, format) -> str:
461463
"""format this undate interval as a string using the specified format;
462-
for now, only supports named formatters"""
463-
formatter_cls = BaseDateFormat.available_formatters().get(format, None)
464-
if formatter_cls:
465-
return formatter_cls().to_string(self)
464+
for now, only supports named converters"""
465+
converter_cls = BaseDateConverter.available_converters().get(format, None)
466+
if converter_cls:
467+
return converter_cls().to_string(self)
466468

467469
raise ValueError(f"Unsupported format '{format}'")
468470

tests/test_dateformat/edtf/test_edtf_parser.py renamed to tests/test_converters/edtf/test_edtf_parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pytest
2-
from undate.dateformat.edtf.parser import edtf_parser
2+
from undate.converters.edtf.parser import edtf_parser
33

44
# for now, just test that valid dates can be parsed
55

tests/test_dateformat/edtf/test_edtf_transformer.py renamed to tests/test_converters/edtf/test_edtf_transformer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
2-
from undate.dateformat.edtf.parser import edtf_parser
3-
from undate.dateformat.edtf.transformer import EDTFTransformer
2+
from undate.converters.edtf.parser import edtf_parser
3+
from undate.converters.edtf.transformer import EDTFTransformer
44
from undate.undate import Undate, UndateInterval
55

66
# for now, just test that valid dates can be parsed

tests/test_converters/test_base.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import logging
2+
3+
import pytest
4+
from undate.converters.base import BaseDateConverter
5+
6+
7+
class TestBaseDateConverter:
8+
def test_available_converters(self):
9+
available_converters = BaseDateConverter.available_converters()
10+
assert isinstance(available_converters, dict)
11+
12+
# NOTE: import _after_ generating available formatters
13+
# so we can confirm it gets loaded
14+
from undate.converters.iso8601 import ISO8601DateFormat
15+
16+
assert ISO8601DateFormat.name in available_converters
17+
assert available_converters[ISO8601DateFormat.name] == ISO8601DateFormat
18+
19+
def test_converters_are_unique(self):
20+
assert len(BaseDateConverter.available_converters()) == len(
21+
BaseDateConverter.__subclasses__()
22+
), "Formatter names have to be unique."
23+
24+
def test_parse_not_implemented(self):
25+
with pytest.raises(NotImplementedError):
26+
BaseDateConverter().parse("foo bar baz")
27+
28+
def test_parse_to_string(self):
29+
with pytest.raises(NotImplementedError):
30+
BaseDateConverter().to_string(1991)
31+
32+
33+
def test_import_converters_import_only_once(caplog):
34+
# clear the cache, since any instantiation of an Undate
35+
# object anywhere in the test suite will populate it
36+
BaseDateConverter.import_converters.cache_clear()
37+
38+
# run first, and confirm it runs and loads formatters
39+
with caplog.at_level(logging.DEBUG):
40+
import_count = BaseDateConverter.import_converters()
41+
# should import at least one thing (iso8601)
42+
assert import_count >= 1
43+
# should have log entry
44+
assert "Loading converters" in caplog.text
45+
46+
# if we clear the log and run again, should not do anything
47+
caplog.clear()
48+
with caplog.at_level(logging.DEBUG):
49+
BaseDateConverter.import_converters()
50+
assert "Loading converters" not in caplog.text
51+
52+
53+
@pytest.mark.last
54+
def test_converters_unique_error():
55+
# confirm that unique converter check fails when it should
56+
57+
# run this test last because we can't undefine the subclass
58+
# once it exists...
59+
class ISO8601DateFormat2(BaseDateConverter):
60+
name = "ISO8601" # duplicates existing formatter
61+
62+
assert len(BaseDateConverter.available_converters()) != len(
63+
BaseDateConverter.__subclasses__()
64+
)

tests/test_converters/test_edtf.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import pytest
2+
from undate.converters.edtf import EDTFDateConverter
3+
from undate.undate import Undate, UndateInterval
4+
5+
6+
class TestEDTFDateConverter:
7+
def test_parse_singledate(self):
8+
assert EDTFDateConverter().parse("2002") == Undate(2002)
9+
assert EDTFDateConverter().parse("1991-05") == Undate(1991, 5)
10+
assert EDTFDateConverter().parse("1991-05-03") == Undate(1991, 5, 3)
11+
# unknown dates are not strictly equal, but string comparison should match
12+
assert str(EDTFDateConverter().parse("201X")) == str(Undate("201X"))
13+
assert str(EDTFDateConverter().parse("2004-XX")) == str(Undate(2004, "XX"))
14+
# missing year but month/day known
15+
# assert EDTFDateConverter().parse("--05-03") == Undate(month=5, day=3)
16+
17+
def test_parse_singledate_unequal(self):
18+
assert EDTFDateConverter().parse("2002") != Undate(2003)
19+
assert EDTFDateConverter().parse("1991-05") != Undate(1991, 6)
20+
assert EDTFDateConverter().parse("1991-05-03") != Undate(1991, 5, 4)
21+
# missing year but month/day known
22+
# - does EDTF not support this or is parsing logic incorrect?
23+
# assert EDTFDateConverter().parse("XXXX-05-03") != Undate(month=5, day=4)
24+
25+
def test_parse_invalid(self):
26+
with pytest.raises(ValueError):
27+
EDTFDateConverter().parse("1991-5")
28+
29+
def test_parse_range(self):
30+
assert EDTFDateConverter().parse("1800/1900") == UndateInterval(
31+
Undate(1800), Undate(1900)
32+
)
33+
34+
def test_to_string(self):
35+
assert EDTFDateConverter().to_string(Undate(900)) == "0900"
36+
assert EDTFDateConverter().to_string(Undate("80")) == "0080"
37+
assert EDTFDateConverter().to_string(Undate(33)) == "0033"
38+
assert EDTFDateConverter().to_string(Undate("20XX")) == "20XX"
39+
assert EDTFDateConverter().to_string(Undate(17000002)) == "Y17000002"
40+
41+
assert EDTFDateConverter().to_string(Undate(1991, 6)) == "1991-06"
42+
assert EDTFDateConverter().to_string(Undate(1991, 5, 3)) == "1991-05-03"
43+
44+
assert EDTFDateConverter().to_string(Undate(1991, "0X")) == "1991-0X"
45+
assert EDTFDateConverter().to_string(Undate(1991, None, 3)) == "1991-XX-03"
46+
47+
# TODO: override missing digit and confirm replacement

tests/test_dateformat/test_iso8601.py renamed to tests/test_converters/test_iso8601.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from undate.dateformat.iso8601 import ISO8601DateFormat
1+
from undate.converters.iso8601 import ISO8601DateFormat
22
from undate.undate import Undate, UndateInterval
33

44

0 commit comments

Comments
 (0)