From e63f4297292252d9d6034fde2caafb875ddfc4cd Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 29 Aug 2025 13:46:21 -0400 Subject: [PATCH 1/7] Update and test undate repr method resolves #142 --- src/undate/undate.py | 12 ++++++++++-- tests/test_undate.py | 32 ++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/undate/undate.py b/src/undate/undate.py index e6561bf..91d51f7 100644 --- a/src/undate/undate.py +++ b/src/undate/undate.py @@ -250,8 +250,16 @@ def __str__(self) -> str: return self.converter.to_string(self) def __repr__(self) -> str: - label_str = f" '{self.label}'" if self.label else "" - return f"" + init_opts = {k: v for k, v in self.initial_values.items() if v is not None} + if self.label: + init_opts["label"] = self.label + init_opts["calendar"] = str(self.calendar) + init_params = [] + for key, val in init_opts.items(): + if isinstance(val, str): + val = f'"{val}"' + init_params.append(f"{key}={val}") + return f"undate.Undate({', '.join(init_params)})" @classmethod def parse(cls, date_string, format) -> Union["Undate", UndateInterval]: diff --git a/tests/test_undate.py b/tests/test_undate.py index 2cbaf7d..7de41a6 100644 --- a/tests/test_undate.py +++ b/tests/test_undate.py @@ -29,12 +29,36 @@ def test_partially_known_str(self): # assert str(Undate(2022, day=7)) == "2022-XX-07" @ currently returns 2022-07 def test_repr(self): - assert repr(Undate(2022, 11, 7)) == "" + # import undate to test eval of fully-qualified undate repr string + import undate # noqa: F401 + + nov2022 = Undate(2022, 11, 7) + # repr string should provide sufficient details to initialize + assert ( + repr(nov2022) + == 'undate.Undate(year=2022, month=11, day=7, calendar="gregorian")' + ) + # eval on repr string should be equivalent to the object + assert eval(repr(nov2022)) == nov2022 + nov2022_labeled = Undate(2022, 11, 7, label="A Special Day") + assert ( + repr(nov2022_labeled) + == 'undate.Undate(year=2022, month=11, day=7, label="A Special Day", calendar="gregorian")' + ) + assert eval(repr(nov2022_labeled)) == nov2022_labeled + # different calendar, missing fields + islamic_date = Undate(484, calendar=Calendar.ISLAMIC) + assert repr(islamic_date) == 'undate.Undate(year=484, calendar="islamic")' + assert eval(repr(islamic_date)) == islamic_date + + # test string values for month/day + unknown_year = Undate(month="1X", day="3X") assert ( - repr(Undate(2022, 11, 7, label="A Special Day")) - == "" + repr(unknown_year) + == 'undate.Undate(month="1X", day="3X", calendar="gregorian")' ) - assert repr(Undate(484, calendar=Calendar.ISLAMIC)) == "" + # unknown dates aren't equal, but string representation should match + assert str(eval(repr(unknown_year))) == str(unknown_year) def test_init_str(self): assert Undate("2000").earliest.year == 2000 From 51061c4dd20d469a1bc8cbd24fa2ebf40a24c507 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 29 Aug 2025 13:57:00 -0400 Subject: [PATCH 2/7] Update and test repr string for UndateInterval --- src/undate/interval.py | 12 +++++++++--- tests/test_interval.py | 27 +++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/undate/interval.py b/src/undate/interval.py index ddfacdb..583eaa0 100644 --- a/src/undate/interval.py +++ b/src/undate/interval.py @@ -70,9 +70,15 @@ def format(self, format) -> str: raise ValueError(f"Unsupported format '{format}'") def __repr__(self) -> str: - if self.label: - return "" % (self.label, self) - return "" % self + init_opts = { + "earliest": repr(self.earliest) if self.earliest else None, + "latest": repr(self.latest) if self.latest else None, + "label": f'"{self.label}"' if self.label else None, + } + init_str = ", ".join( + [f"{key}={val}" for key, val in init_opts.items() if val is not None] + ) + return f"undate.UndateInterval({init_str})" def __eq__(self, other) -> bool: # currently doesn't support comparison with any other types diff --git a/tests/test_interval.py b/tests/test_interval.py index cf1a716..301e61f 100644 --- a/tests/test_interval.py +++ b/tests/test_interval.py @@ -56,14 +56,33 @@ def test_format(self): assert open_end.format("ISO8601") == "2000/" def test_repr(self): + # import undate to test eval of fully-qualified repr string + import undate # noqa: F401 + + # interval with start and end + closed_interval = UndateInterval(Undate(2022), Undate(2023)) + assert ( + repr(closed_interval) + == f"undate.UndateInterval(earliest={repr(closed_interval.earliest)}, latest={repr(closed_interval.latest)})" + ) + # should be able to evaluate repr string to get an equivalent object + assert eval(repr(closed_interval)) == closed_interval + # interval with a label + fancy_epoch = UndateInterval(Undate(2022), Undate(2023), label="Fancy Epoch") assert ( - repr(UndateInterval(Undate(2022), Undate(2023))) - == "" + repr(fancy_epoch) + == f'undate.UndateInterval(earliest={repr(fancy_epoch.earliest)}, latest={repr(fancy_epoch.latest)}, label="Fancy Epoch")' + ) + assert eval(repr(fancy_epoch)) == fancy_epoch + + open_interval = UndateInterval( + Undate(33), ) assert ( - repr(UndateInterval(Undate(2022), Undate(2023), label="Fancy Epoch")) - == "" + repr(open_interval) + == f"undate.UndateInterval(earliest={repr(open_interval.earliest)})" ) + assert eval(repr(open_interval)) == open_interval def test_str_open_range(self): # 900 - From 58e2448333ea7386ed67ad3ad505438a03ee041b Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 29 Aug 2025 14:02:09 -0400 Subject: [PATCH 3/7] Fix undelta repr so it works with eval() --- src/undate/__init__.py | 11 +++++++++-- src/undate/date.py | 2 +- tests/test_date.py | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/undate/__init__.py b/src/undate/__init__.py index 71e09ae..44e9b04 100644 --- a/src/undate/__init__.py +++ b/src/undate/__init__.py @@ -1,7 +1,14 @@ __version__ = "0.6.0.dev0" -from undate.date import DatePrecision +from undate.date import DatePrecision, UnDelta from undate.undate import Undate, Calendar from undate.interval import UndateInterval -__all__ = ["Undate", "UndateInterval", "Calendar", "DatePrecision", "__version__"] +__all__ = [ + "Undate", + "UndateInterval", + "Calendar", + "DatePrecision", + "UnDelta", + "__version__", +] diff --git a/src/undate/date.py b/src/undate/date.py index 4e9eddc..44f79fa 100644 --- a/src/undate/date.py +++ b/src/undate/date.py @@ -145,7 +145,7 @@ def __init__(self, *days: int): def __repr__(self): # customize string representation for simpler notation; default # specifies full UnInt initialization with upper and lower keywords - return f"{self.__class__.__name__}(days=[{self.days.lower},{self.days.upper}])" + return f"undate.{self.__class__.__name__}({self.days.lower},{self.days.upper})" def __eq__(self, other: object) -> bool: # is an uncertain duration ever *equal* another, even if the values are the same? diff --git a/tests/test_date.py b/tests/test_date.py index 24703cb..94b5168 100644 --- a/tests/test_date.py +++ b/tests/test_date.py @@ -230,8 +230,20 @@ def test_init_validation(self): UnDelta(10) def test_repr(self): - # customized string representation - assert repr(UnDelta(28, 29)) == "UnDelta(days=[28,29])" + # test customized string representation + + # import undate to test eval of fully-qualified repr string + import undate # noqa: F401 + + feb_undelt = UnDelta(28, 29) + assert repr(feb_undelt) == "undate.UnDelta(28,29)" + # can't compare directly because uncertain deltas aren't equal, + # but compare vlaues + assert eval(repr(feb_undelt.days.lower)) == feb_undelt.days.lower + assert eval(repr(feb_undelt.days.upper)) == feb_undelt.days.upper + + larger_undelt = UnDelta(10, 12, 14, 16) + assert repr(larger_undelt) == "undate.UnDelta(10,16)" def test_eq(self): # uncertain deltas are not equivalent From 23620c773ef7868095676d260da793368d35cf55 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 29 Aug 2025 14:06:02 -0400 Subject: [PATCH 4/7] Use title case for calendar in undate repr method --- src/undate/undate.py | 2 +- tests/test_undate.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/undate/undate.py b/src/undate/undate.py index 91d51f7..8544074 100644 --- a/src/undate/undate.py +++ b/src/undate/undate.py @@ -253,7 +253,7 @@ def __repr__(self) -> str: init_opts = {k: v for k, v in self.initial_values.items() if v is not None} if self.label: init_opts["label"] = self.label - init_opts["calendar"] = str(self.calendar) + init_opts["calendar"] = self.calendar.value.title() init_params = [] for key, val in init_opts.items(): if isinstance(val, str): diff --git a/tests/test_undate.py b/tests/test_undate.py index 7de41a6..b3bf6b5 100644 --- a/tests/test_undate.py +++ b/tests/test_undate.py @@ -36,26 +36,26 @@ def test_repr(self): # repr string should provide sufficient details to initialize assert ( repr(nov2022) - == 'undate.Undate(year=2022, month=11, day=7, calendar="gregorian")' + == 'undate.Undate(year=2022, month=11, day=7, calendar="Gregorian")' ) # eval on repr string should be equivalent to the object assert eval(repr(nov2022)) == nov2022 nov2022_labeled = Undate(2022, 11, 7, label="A Special Day") assert ( repr(nov2022_labeled) - == 'undate.Undate(year=2022, month=11, day=7, label="A Special Day", calendar="gregorian")' + == 'undate.Undate(year=2022, month=11, day=7, label="A Special Day", calendar="Gregorian")' ) assert eval(repr(nov2022_labeled)) == nov2022_labeled # different calendar, missing fields islamic_date = Undate(484, calendar=Calendar.ISLAMIC) - assert repr(islamic_date) == 'undate.Undate(year=484, calendar="islamic")' + assert repr(islamic_date) == 'undate.Undate(year=484, calendar="Islamic")' assert eval(repr(islamic_date)) == islamic_date # test string values for month/day unknown_year = Undate(month="1X", day="3X") assert ( repr(unknown_year) - == 'undate.Undate(month="1X", day="3X", calendar="gregorian")' + == 'undate.Undate(month="1X", day="3X", calendar="Gregorian")' ) # unknown dates aren't equal, but string representation should match assert str(eval(repr(unknown_year))) == str(unknown_year) From 8dfe28bc44735f03808a330c5e159402c027f621 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 29 Aug 2025 14:25:10 -0400 Subject: [PATCH 5/7] Update repr output in readme examples to match revised implementation --- README.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index cd73453..9e535e9 100644 --- a/README.md +++ b/README.md @@ -121,13 +121,13 @@ earliest and latest possible dates for comparison purposes so you can sort dates and compare with equals, greater than, and less than. You can also compare with python `datetime.date` objects. -```python +```Python console >>> november7_2020 = Undate(2020, 11, 7) >>> november_2001 = Undate(2001, 11) >>> year2k = Undate(2000) >>> ad100 = Undate(100) >>> sorted([november7_2020, november_2001, year2k, ad100]) -[, , , ] +[undate.Undate(year=100, calendar="Gregorian"), undate.Undate(year=2000, calendar="Gregorian"), undate.Undate(year=2001, month=11, calendar="Gregorian"), undate.Undate(year=2020, month=11, day=7, calendar="Gregorian")] >>> november7_2020 > november_2001 True >>> year2k < ad100 @@ -161,17 +161,17 @@ and latest date as part of the range. ```python >>> from undate import UndateInterval >>> UndateInterval(Undate(1900), Undate(2000)) - +undate.UndateInterval(earliest=undate.Undate(year=1900, calendar="Gregorian"), latest=undate.Undate(year=2000, calendar="Gregorian")) >>> UndateInterval(Undate(1801), Undate(1900), label="19th century") +undate.UndateInterval(earliest=undate.Undate(year=1801, calendar="Gregorian"), latest=undate.Undate(year=1900, calendar="Gregorian"), label="19th century") >>> UndateInterval(Undate(1801), Undate(1900), label="19th century").duration().days 36524 - >>> UndateInterval(Undate(1901), Undate(2000), label="20th century") - +undate.UndateInterval(earliest=undate.Undate(year=1901, calendar="Gregorian"), latest=undate.Undate(year=2000, calendar="Gregorian"), label="20th century") >>> UndateInterval(latest=Undate(2000)) # before 2000 - +undate.UndateInterval(latest=undate.Undate(year=2000, calendar="Gregorian")) >>> UndateInterval(Undate(1900)) # after 1900 - +undate.UndateInterval(earliest=undate.Undate(year=1900, calendar="Gregorian")) >>> UndateInterval(Undate(1900), Undate(2000), label="19th century").duration().days 36890 >>> UndateInterval(Undate(2000, 1, 1), Undate(2000, 1,31)).duration().days @@ -186,15 +186,15 @@ are "ISO8601" and "EDTF" and supported calendars. ```python >>> from undate import Undate >>> Undate.parse("2002", "ISO8601") - +undate.Undate(year=2002, calendar="Gregorian") >>> Undate.parse("2002-05", "EDTF") - +undate.Undate(year=2002, month=5, calendar="Gregorian") >>> Undate.parse("--05-03", "ISO8601") - +undate.Undate(month=5, day=3, calendar="Gregorian") >>> Undate.parse("--05-03", "ISO8601").format("EDTF") 'XXXX-05-03' ->>> Undate.parse("1800/1900") - +>>> Undate.parse("1800/1900", format="EDTF") +undate.UndateInterval(earliest=undate.Undate(year=1800, calendar="Gregorian"), latest=undate.Undate(year=1900, calendar="Gregorian")) ``` ### Calendars @@ -215,25 +215,25 @@ comparison across dates from different calendars. >>> from undate import Undate >>> tammuz4816 = Undate.parse("26 Tammuz 4816", "Hebrew") >>> tammuz4816 - +undate.Undate(year=4816, month=4, day=26, label="26 Tammuz 4816 Anno Mundi", calendar="Hebrew") >>> rajab495 = Undate.parse("Rajab 495", "Islamic") >>> rajab495 - +undate.Undate(year=495, month=7, label="Rajab 495 Islamic", calendar="Islamic") >>> y2k = Undate.parse("2001", "EDTF") >>> y2k - +undate.Undate(year=2001, calendar="Gregorian") >>> [str(d.earliest) for d in [rajab495, tammuz4816, y2k]] ['1102-04-28', '1056-07-17', '2001-01-01'] >>> [str(d.precision) for d in [rajab495, tammuz4816, y2k]] ['MONTH', 'DAY', 'YEAR'] >>> sorted([rajab495, tammuz4816, y2k]) -[, , ] +[undate.Undate(year=4816, month=4, day=26, label="26 Tammuz 4816 Anno Mundi", calendar="Hebrew"), undate.Undate(year=495, month=7, label="Rajab 495 Islamic", calendar="Islamic"), undate.Undate(year=2001, calendar="Gregorian")] ``` --- -For more examples, refer to the code notebooks included in the[examples] -(https://github.com/dh-tech/undate-python/tree/main/examples/) in this +For more examples, refer to the code notebooks included in the +[examples](https://github.com/dh-tech/undate-python/tree/main/examples/) in this repository. ## Documentation From beb9d9cfd8516322a5147a0ece6156e54c1ec928 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 29 Aug 2025 15:09:45 -0400 Subject: [PATCH 6/7] Simplify formatting for string parameters in repr methods --- src/undate/interval.py | 2 +- src/undate/undate.py | 11 +++++------ tests/test_interval.py | 2 +- tests/test_undate.py | 8 ++++---- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/undate/interval.py b/src/undate/interval.py index 583eaa0..a7fbe55 100644 --- a/src/undate/interval.py +++ b/src/undate/interval.py @@ -73,7 +73,7 @@ def __repr__(self) -> str: init_opts = { "earliest": repr(self.earliest) if self.earliest else None, "latest": repr(self.latest) if self.latest else None, - "label": f'"{self.label}"' if self.label else None, + "label": f"{self.label!r}" if self.label else None, } init_str = ", ".join( [f"{key}={val}" for key, val in init_opts.items() if val is not None] diff --git a/src/undate/undate.py b/src/undate/undate.py index 8544074..5eaf6b9 100644 --- a/src/undate/undate.py +++ b/src/undate/undate.py @@ -251,15 +251,14 @@ def __str__(self) -> str: def __repr__(self) -> str: init_opts = {k: v for k, v in self.initial_values.items() if v is not None} + # include label if set if self.label: init_opts["label"] = self.label + # always include calendar init_opts["calendar"] = self.calendar.value.title() - init_params = [] - for key, val in init_opts.items(): - if isinstance(val, str): - val = f'"{val}"' - init_params.append(f"{key}={val}") - return f"undate.Undate({', '.join(init_params)})" + # combine parameters; use !r to quote strings + init_str = ", ".join([f"{key}={val!r}" for key, val in init_opts.items()]) + return f"undate.Undate({init_str})" @classmethod def parse(cls, date_string, format) -> Union["Undate", UndateInterval]: diff --git a/tests/test_interval.py b/tests/test_interval.py index 301e61f..dbf28b3 100644 --- a/tests/test_interval.py +++ b/tests/test_interval.py @@ -71,7 +71,7 @@ def test_repr(self): fancy_epoch = UndateInterval(Undate(2022), Undate(2023), label="Fancy Epoch") assert ( repr(fancy_epoch) - == f'undate.UndateInterval(earliest={repr(fancy_epoch.earliest)}, latest={repr(fancy_epoch.latest)}, label="Fancy Epoch")' + == f"undate.UndateInterval(earliest={repr(fancy_epoch.earliest)}, latest={repr(fancy_epoch.latest)}, label='Fancy Epoch')" ) assert eval(repr(fancy_epoch)) == fancy_epoch diff --git a/tests/test_undate.py b/tests/test_undate.py index b3bf6b5..9c0e862 100644 --- a/tests/test_undate.py +++ b/tests/test_undate.py @@ -36,26 +36,26 @@ def test_repr(self): # repr string should provide sufficient details to initialize assert ( repr(nov2022) - == 'undate.Undate(year=2022, month=11, day=7, calendar="Gregorian")' + == "undate.Undate(year=2022, month=11, day=7, calendar='Gregorian')" ) # eval on repr string should be equivalent to the object assert eval(repr(nov2022)) == nov2022 nov2022_labeled = Undate(2022, 11, 7, label="A Special Day") assert ( repr(nov2022_labeled) - == 'undate.Undate(year=2022, month=11, day=7, label="A Special Day", calendar="Gregorian")' + == "undate.Undate(year=2022, month=11, day=7, label='A Special Day', calendar='Gregorian')" ) assert eval(repr(nov2022_labeled)) == nov2022_labeled # different calendar, missing fields islamic_date = Undate(484, calendar=Calendar.ISLAMIC) - assert repr(islamic_date) == 'undate.Undate(year=484, calendar="Islamic")' + assert repr(islamic_date) == "undate.Undate(year=484, calendar='Islamic')" assert eval(repr(islamic_date)) == islamic_date # test string values for month/day unknown_year = Undate(month="1X", day="3X") assert ( repr(unknown_year) - == 'undate.Undate(month="1X", day="3X", calendar="Gregorian")' + == "undate.Undate(month='1X', day='3X', calendar='Gregorian')" ) # unknown dates aren't equal, but string representation should match assert str(eval(repr(unknown_year))) == str(unknown_year) From 8b30880e93743fa29f477de283aff92cb1fb2808 Mon Sep 17 00:00:00 2001 From: rlskoeser Date: Fri, 29 Aug 2025 15:12:20 -0400 Subject: [PATCH 7/7] Cleanup and fix typos --- README.md | 8 ++++---- tests/test_date.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9e535e9..c1b662d 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ earliest and latest possible dates for comparison purposes so you can sort dates and compare with equals, greater than, and less than. You can also compare with python `datetime.date` objects. -```Python console +```python >>> november7_2020 = Undate(2020, 11, 7) >>> november_2001 = Undate(2001, 11) >>> year2k = Undate(2000) @@ -193,7 +193,7 @@ undate.Undate(year=2002, month=5, calendar="Gregorian") undate.Undate(month=5, day=3, calendar="Gregorian") >>> Undate.parse("--05-03", "ISO8601").format("EDTF") 'XXXX-05-03' ->>> Undate.parse("1800/1900", format="EDTF") +>>> Undate.parse("1800/1900", format="EDTF") undate.UndateInterval(earliest=undate.Undate(year=1800, calendar="Gregorian"), latest=undate.Undate(year=1900, calendar="Gregorian")) ``` @@ -233,8 +233,8 @@ undate.Undate(year=2001, calendar="Gregorian") --- For more examples, refer to the code notebooks included in the -[examples](https://github.com/dh-tech/undate-python/tree/main/examples/) in this -repository. +[examples](https://github.com/dh-tech/undate-python/tree/main/examples/) +directory in this repository. ## Documentation diff --git a/tests/test_date.py b/tests/test_date.py index 94b5168..fc6cc72 100644 --- a/tests/test_date.py +++ b/tests/test_date.py @@ -238,7 +238,7 @@ def test_repr(self): feb_undelt = UnDelta(28, 29) assert repr(feb_undelt) == "undate.UnDelta(28,29)" # can't compare directly because uncertain deltas aren't equal, - # but compare vlaues + # but compare values assert eval(repr(feb_undelt.days.lower)) == feb_undelt.days.lower assert eval(repr(feb_undelt.days.upper)) == feb_undelt.days.upper