Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rename formatters submodule and classes to converters #101

Merged
merged 5 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,4 @@
}

# turn on relative links; make sure both github and sphinx links work
myst_enable_extensions = ["linkify"]
# myst_enable_extensions = ["linkify"] # disabling because not found
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ undate documentation
:caption: Contents:

readme
undate
undate/index
CONTRIBUTING
DEVELOPER_NOTES
CONTRIBUTORS
Expand Down
11 changes: 0 additions & 11 deletions docs/undate.rst

This file was deleted.

30 changes: 30 additions & 0 deletions docs/undate/converters.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Converters
==========

.. automodule:: undate.converters.base
:members:
:undoc-members:

ISO8601
-------

.. automodule:: undate.converters.iso8601
:members:
:undoc-members:

Extended Date-Time Format (EDTF)
--------------------------------

.. automodule:: undate.converters.edtf.converter
:members:
:undoc-members:

.. automodule:: undate.converters.edtf.parser
:members:
:undoc-members:

.. transformer is more of an internal, probably doesn't make sense to include
.. .. automodule:: undate.converters.edtf.transformer
.. :members:
.. :undoc-members:

22 changes: 22 additions & 0 deletions docs/undate/core.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Undate objects
==============

undates and undate intervals
------------------------------

.. autoclass:: undate.undate.Undate
:members:

.. autoclass:: undate.undate.UndateInterval
:members:

date, timedelta, and date precision
-----------------------------------

.. autoclass:: undate.date.Date
:members:

.. autoclass:: undate.date.Timedelta
:members:

.. autoclass:: undate.date.DatePrecision
9 changes: 9 additions & 0 deletions docs/undate/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
API documentation
=================

.. toctree::
:maxdepth: 2
:caption: Contents:

core
converters
1 change: 1 addition & 0 deletions src/undate/converters/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from undate.converters.base import BaseDateConverter as BaseDateConverter
95 changes: 95 additions & 0 deletions src/undate/converters/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""
:class:`undate.converters.BaseDateConverter` provides a base class for
implementing date converters, which can provide support for
parsing and generating dates in different formats and also converting
dates between different calendars.

To add support for a new date format or calendar conversion:

- Create a new file under ``undate/converters/``
- For converters with sufficient complexity, you may want to create a submodule;
see ``undate.converters.edtf`` for an example.
- Extend ``BaseDateConverter`` and implement ``parse`` and ``to_string`` methods
as desired/appropriate for your converter
- Add unit tests for the new converter in ``tests/test_converters/``
- Optionally, you may want to create a notebook to demonstrate the use and value
of the new converter.

The new subclass should be loaded automatically and included in the converters
returned by :meth:`BaseDateConverter.available_converters`

-------------------
"""

import importlib
import logging
import pkgutil
from functools import cache
from typing import Dict, Type

logger = logging.getLogger(__name__)


class BaseDateConverter:
"""Base class for parsing, formatting, and converting dates to handle
specific formats and different calendars."""

#: Converter name. Subclasses must define a unique name.
name: str = "Base Converter"

def parse(self, value: str):
"""
Parse a string and return an :class:`~undate.undate.Undate` or
:class:`~undate.undate.UndateInterval`. Must be implemented by
subclasses.
"""
# can't add type hint here because of circular import
# should return an undate or undate interval
raise NotImplementedError

def to_string(self, undate) -> str:
"""
Convert an :class:`~undate.undate.Undate` or
:class:`~undate.undate.UndateInterval` to string.
Must be implemented by subclasses.
"""

# undate param should be of type Union[Undate, UndateInterval] but can't add type hint here because of circular import
# convert an undate or interval to string representation for this format
raise NotImplementedError

# cache import class method to ensure we only import once
@classmethod
@cache
def import_converters(cls) -> int:
"""Import all undate converters
so that they will be included in available converters
even if not explicitly imported. Only import once.
returns the count of modules imported."""

logger.debug("Loading converters under undate.converters")
import undate.converters

# load packages under this path with curent package prefix
converter_path = undate.converters.__path__
converter_prefix = f"{undate.converters.__name__}."

import_count = 0
for importer, modname, ispkg in pkgutil.iter_modules(
converter_path, converter_prefix
):
# import everything except the current file
if not modname.endswith(".base"):
importlib.import_module(modname)
import_count += 1

return import_count

@classmethod
def available_converters(cls) -> Dict[str, Type["BaseDateConverter"]]:
"""
Dictionary of available converters keyed on name.
"""
# ensure undate converters are imported
cls.import_converters()
return {c.name: c for c in cls.__subclasses__()} # type: ignore
1 change: 1 addition & 0 deletions src/undate/converters/edtf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from undate.converters.edtf.converter import EDTFDateConverter as EDTFDateConverter
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,29 @@

from lark.exceptions import UnexpectedCharacters

from undate.converters.base import BaseDateConverter
from undate.converters.edtf.parser import edtf_parser
from undate.converters.edtf.transformer import EDTFTransformer
from undate.date import DatePrecision
from undate.dateformat.base import BaseDateFormat
from undate.dateformat.edtf.parser import edtf_parser
from undate.dateformat.edtf.transformer import EDTFTransformer
from undate.undate import Undate, UndateInterval

#: character for unspecified digits
EDTF_UNSPECIFIED_DIGIT: str = "X"


class EDTFDateFormat(BaseDateFormat):
class EDTFDateConverter(BaseDateConverter):
#: converter name: EDTF
name: str = "EDTF"

def __init__(self):
self.transformer = EDTFTransformer()

def parse(self, value: str) -> Union[Undate, UndateInterval]:
"""
Parse a string in a supported EDTF date or date interval format and
return an :class:`~undate.undate.Undate` or
:class:`~undate.undate.UndateInterval`.
"""
# parse the input string, then transform to undate object
try:
parsetree = edtf_parser.parse(value)
Expand All @@ -33,6 +40,10 @@ def _convert_missing_digits(
return None

def to_string(self, undate: Union[Undate, UndateInterval]) -> str:
"""
Convert an :class:`~undate.undate.Undate` or
:class:`~undate.undate.UndateInterval` to EDTF format.
"""
if isinstance(undate, Undate):
return self._undate_to_string(undate)
elif isinstance(undate, UndateInterval):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from typing import Dict, List, Union

from undate.dateformat.base import BaseDateFormat
from undate.converters.base import BaseDateConverter
from undate.undate import Undate, UndateInterval


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

# do not change; Undate relies on this string
#: converter name: ISO8601
name: str = "ISO8601"
rlskoeser marked this conversation as resolved.
Show resolved Hide resolved
# do not change; Undate relies on this string

#: datetime strftime format for known part of date
iso_format: Dict[str, str] = {
Expand All @@ -19,12 +20,15 @@ class ISO8601DateFormat(BaseDateFormat):
}

def parse(self, value: str) -> Union[Undate, UndateInterval]:
# TODO: must return value of type "Union[Undate, UndateInterval]"
"""
Parse an ISO88601 string and return an :class:`~undate.undate.Undate` or
:class:`~undate.undate.UndateInterval`. Currently supports
YYYY, YYYY-MM, YYYY-MM-DD, --MM-DD for single date
and interval format (YYYY/YYYY in any supported single date format).
"""
# TODO: what happens if someone gives us a full isoformat date with time?
# (ignore, error?)
# TODO: what about invalid format?
# could be YYYY, YYYY-MM, YYYY-MM-DD, --MM-DD for single date
# or YYYY/YYYY (etc.) for an interval
parts: List[str] = value.split("/") # split in case we have a range
if len(parts) == 1:
return self._parse_single_date(parts[0])
Expand All @@ -50,6 +54,10 @@ def _parse_single_date(self, value: str) -> Undate:
return Undate(*date_parts) # type: ignore

def to_string(self, undate: Union[Undate, UndateInterval]) -> str:
"""
Convert an :class:`~undate.undate.Undate` or
:class:`~undate.undate.UndateInterval` to ISO8601 string format.
"""
if isinstance(undate, Undate):
return self._undate_to_string(undate)
elif isinstance(undate, UndateInterval):
Expand Down
3 changes: 0 additions & 3 deletions src/undate/dateformat/__init__.py

This file was deleted.

71 changes: 0 additions & 71 deletions src/undate/dateformat/base.py

This file was deleted.

1 change: 0 additions & 1 deletion src/undate/dateformat/edtf/__init__.py

This file was deleted.

Loading
Loading