Skip to content

Commit

Permalink
dynamically import dateformatter subclasses (#50)
Browse files Browse the repository at this point in the history
* Use pkgutils to dynamically import dateformat formatter subclasses

* Adjust formatter import and test to confirm we only import once

* Add pytest-ordering dependency to tox

* Use python3.8 compatible caching

* Add pytest-ordering to tox coverage deps
  • Loading branch information
rlskoeser authored Nov 22, 2022
1 parent f686225 commit 1d7a041
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 10 deletions.
3 changes: 2 additions & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[pytest]
markers =
last: run marked tests after all others
last: run marked tests after all others
first: run marked tests before all others
50 changes: 41 additions & 9 deletions src/undate/dateformat/base.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
# base class for date format parsers
from typing import Dict

"""Base class for date format parsing and serializing
"""
Base class for date format parsing and serializing
To add support for a new date format:
- create a new file under undate/dateformat
- extend BaseDateFormat and implement parse and to_string methods
as desired/appropriate
- Add your new formatter to [... details TBD ...]
so that it will be included in the available formatters
It should be loaded automatically and included in the formatters
returned by :meth:`BaseDateFormat.available_formatters`
"""

import importlib
import logging
import pkgutil
from typing import Dict
from functools import lru_cache # functools.cache not available until 3.9


logger = logging.getLogger(__name__)


class BaseDateFormat:
"""Base class for parsing and formatting dates for specific formats."""
Expand All @@ -30,10 +38,34 @@ def to_string(self, undate) -> str:
# convert an undate or interval to string representation for this format
raise NotImplementedError

# cache import class method to ensure we only import once
@classmethod
def available_formatters(cls) -> Dict[str, "BaseDateFormat"]:
# FIXME: workaround for circular import problem"
@lru_cache
def import_formatters(cls):
"""Import all undate.dateformat formatters
so that they will be included in available formatters
even if not explicitly imported. Only import once.
returns the count of modules imported."""
logger.debug("Loading formatters under undate.dateformat")
import undate.dateformat

# load packages under this path with curent package prefix
formatter_path = undate.dateformat.__path__
formatter_prefix = f"{undate.dateformat.__name__}."

from undate.dateformat.iso8601 import ISO8601DateFormat
import_count = 0
for importer, modname, ispkg in pkgutil.iter_modules(
formatter_path, formatter_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_formatters(cls) -> Dict[str, "BaseDateFormat"]:
# ensure undate formatters are imported
cls.import_formatters()
return {c.name: c for c in cls.__subclasses__()} # type: ignore
19 changes: 19 additions & 0 deletions tests/test_dateformat/test_base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging

import pytest

from undate.dateformat.base import BaseDateFormat
Expand Down Expand Up @@ -29,6 +31,23 @@ def test_parse_to_string(self):
BaseDateFormat().to_string(1991)


@pytest.mark.first
def test_import_formatters_import_only_once(caplog):
# run first so we can confirm it runs once
with caplog.at_level(logging.DEBUG):
import_count = BaseDateFormat.import_formatters()
# should import at least one thing (iso8601)
assert import_count >= 1
# should have log entry
assert "Loading formatters" in caplog.text

# if we clear the log and run again, should not do anything
caplog.clear()
with caplog.at_level(logging.DEBUG):
BaseDateFormat.import_formatters()
assert "Loading formatters" not in caplog.text


@pytest.mark.last
def test_formatters_unique_error():
# confirm that our uniqe formatters check fails when it should
Expand Down

0 comments on commit 1d7a041

Please sign in to comment.