Skip to content

Commit

Permalink
plugins can override existing formats
Browse files Browse the repository at this point in the history
  • Loading branch information
drkane committed Dec 19, 2023
1 parent ec02eeb commit 23d5d19
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 15 deletions.
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

**New in version 0.9.0**: Change plugin loading to allow for overriding existing format specifiers.

**New in version 0.8.2**: More permissive date format acceptance

**New in version 0.8.1**: Add full support for python 3.12

**New in version 0.8.0**: Add `raise_on_error` support if context parsing fails. This release also introduces the `ixbrlError` class if an error is found, instead of a dict - this is a minor breaking change compared to how errors were stored before. Add provisional support for python 3.12.

**New in version 0.7.1**: Allow for case-insensitive schema tags
Expand Down
32 changes: 30 additions & 2 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The formats used within iXBRL™ files can vary between schemas and countries. R

To create a plugin, you first need to create a new format class that subclasses `ixbrlparse.ixbrlFormat`. This has two key components:

- a `format_names` attribute which consists of a tuple of possible names for the format. These are the values that will be checked against the iXBRL™ items. These names must not clash with other formats that have already been defined.
- a `format_names` attribute which consists of a tuple of possible names for the format. These are the values that will be checked against the iXBRL™ items. These names will override any already defined by the ixbrlparse, so it is possible to override the default implementation.
- a `parse_value` function which takes the original text value and returns the processed value.

An example class might look like (in the file `ixbrlparse-dateplugin/ixbrlparse_dateplugin.py`):
Expand All @@ -37,7 +37,7 @@ def ixbrl_add_formats():
return [ixtParseIsoDate]
```

or
or you can specify the specname if you don't want to call the function `ixbrl_add_formats`

```python
@ixbrlparse.hookimpl(specname="ixbrl_add_formats")
Expand All @@ -58,6 +58,34 @@ setup(
)
```

Or to use `pyproject.toml` you would add

```toml
[project.entry-points.ixbrlparse]
dateplugin = "ixbrlparse_dateplugin"
```

### Override an existing format

By default, formats from plugins are loaded after the default formats included with the module. This means it is possible to override
them by subclassing the format class.

For example, if you wanted to add additional date formats to a format class:

```python
from ixbrlparse.components.formats import ixtDateDayMonthYear

class ixtDateDayMonthYearExtended(ixtDateDayMonthYear):
date_format = (*ixtDateDayMonthYear.date_format, "%d-%b-%Y", "%d-%b-%y")

@hookimpl
def ixbrl_add_formats(self) -> List[Type[ixbrlFormat]]:
return [ixtDateDayMonthYearExtended]
```

After installation, this new format class would override the existing formats for `ixtDateDayMonthYear` and
allow for dates like "29 aug 2022" to be parsed as well as dates like "29/08/2022".

### Install the plugin

If you then install the plugin it should be picked up by ixbrlparse and will also include the additional formats when checking.
Expand Down
2 changes: 1 addition & 1 deletion src/ixbrlparse/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.8.2"
__version__ = "0.9.0"
3 changes: 0 additions & 3 deletions src/ixbrlparse/components/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ def get_format(format_: Optional[str]) -> Type[ixbrlFormat]:
for additional_formats in pm.hook.ixbrl_add_formats():
for format_class in additional_formats:
for format_str in format_class.format_names:
if format_str in formats:
msg = f'Format "{format_str}" already exists (namespace "{namespace}")'
raise ValueError(msg)
formats[format_str] = format_class

if format_ in formats:
Expand Down
5 changes: 3 additions & 2 deletions tests/test_formats.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
(ixtDateFormat, "2019-01-05", date(2019, 1, 5), "0400502019"),
(ixtDateLongUK, "05 January 2019", date(2019, 1, 5), "0400502019"),
(ixtDateLongUK, "05 January 19", date(2019, 1, 5), "0400502019"),
(ixtDateLongUK, "10 July 1023", date(1023, 7, 10), "2019005004"),
(ixtDateLongUS, "January 05, 2019", date(2019, 1, 5), "0400502019"),
(ixtDateLongUS, "January 05, 19", date(2019, 1, 5), "0400502019"),
(ixtDateShortUK, "05 Jan 2019", date(2019, 1, 5), "0400502019"),
Expand All @@ -38,10 +39,10 @@
(ixtDateDayMonthYear, "05.01.19", date(2019, 1, 5), "0400502019"),
(ixtDateDayMonthYear, "05/01/2019", date(2019, 1, 5), "2019005004"),
(ixtDateDayMonthYear, "05/01/19", date(2019, 1, 5), "2019005004"),
(ixtDateSlashUS, "01/05/2019", date(2019, 1, 5), "2019005004"),
(ixtDateSlashUS, "01/05/19", date(2019, 1, 5), "2019005004"),
(ixtDateDayMonthYear, "05.01.2019", date(2019, 1, 5), "0400502019"),
(ixtDateDayMonthYear, "05.01.19", date(2019, 1, 5), "0400502019"),
(ixtDateSlashUS, "01/05/2019", date(2019, 1, 5), "2019005004"),
(ixtDateSlashUS, "01/05/19", date(2019, 1, 5), "2019005004"),
(ixtDateSlashUS, "01.05.2019", date(2019, 1, 5), "0400502019"),
(ixtDateSlashUS, "01.05.19", date(2019, 1, 5), "0400502019"),
),
Expand Down
88 changes: 81 additions & 7 deletions tests/test_plugins.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from datetime import date
from typing import List, Type, Union

import pytest

from ixbrlparse import hookimpl
from ixbrlparse.components._base import ixbrlFormat
from ixbrlparse.components.formats import ixtZeroDash
from ixbrlparse.components.formats import ixtDateDayMonthYear, ixtZeroDash
from ixbrlparse.components.transform import get_format
from ixbrlparse.plugins import pm

Expand All @@ -27,7 +28,7 @@ def ixbrl_add_formats(self) -> List[Type[ixbrlFormat]]:
assert get_format("flurg") == FlurgFormat

# check existing formats are still available
assert get_format("ixt:zerodash") == ixtZeroDash
assert get_format("zerodash") == ixtZeroDash
finally:
pm.unregister(name="flurg")

Expand All @@ -50,27 +51,100 @@ def add_flurg_format(self) -> List[Type[ixbrlFormat]]:
assert get_format("flurg") == FlurgFormat

# check existing formats are still available
assert get_format("ixt:zerodash") == ixtZeroDash
assert get_format("zerodash") == ixtZeroDash
finally:
pm.unregister(name="flurg")


def test_registering_duplicate_plugin():
class FlurgFormat(ixbrlFormat):
format_names = ("ixt:zerodash",)
format_names = ("zerodash",)

def parse_value(self, value: Union[str, int, float]) -> str: # noqa: ARG002
return "flurg"

class TestPlugin:
@hookimpl()
def ixbrl_add_formats(self) -> List[Type[ixbrlFormat]]:
return [FlurgFormat]

pm.register(TestPlugin(), name="flurg")
try:
assert get_format("zerodash") == FlurgFormat
with pytest.raises(NotImplementedError):
get_format("flurg")
finally:
pm.unregister(name="flurg")


def test_registering_duplicate_plugin_last():
class FlurgFormat(ixbrlFormat):
format_names = ("zerodash",)

def parse_value(self, value: Union[str, int, float]) -> str: # noqa: ARG002
return "flurg"

class TestPlugin:
@hookimpl(trylast=True)
def ixbrl_add_formats(self) -> List[Type[ixbrlFormat]]:
return [FlurgFormat]

pm.register(TestPlugin(), name="flurg")
try:
assert get_format("zerodash") == FlurgFormat
with pytest.raises(NotImplementedError):
get_format("flurg")
finally:
pm.unregister(name="flurg")


def test_registering_duplicate_plugin_first():
class FlurgFormat(ixbrlFormat):
format_names = ("zerodash",)

def parse_value(self, value: Union[str, int, float]) -> str: # noqa: ARG002
return "flurg"

class TestPlugin:
@hookimpl(tryfirst=True)
def ixbrl_add_formats(self) -> List[Type[ixbrlFormat]]:
return [FlurgFormat]

pm.register(TestPlugin(), name="flurg")
try:
assert get_format("zerodash") == ixtZeroDash
with pytest.raises(NotImplementedError):
get_format("flurg")
finally:
pm.unregister(name="flurg")


@pytest.mark.parametrize(
"datestring, expecteddate",
(
("05/01/2019", date(2019, 1, 5)),
("05.01.2019", date(2019, 1, 5)),
("05.01.19", date(2019, 1, 5)),
("05/01/2019", date(2019, 1, 5)),
("05/01/19", date(2019, 1, 5)),
("05.01.2019", date(2019, 1, 5)),
("05.01.19", date(2019, 1, 5)),
("29 aug 2022", date(2022, 8, 29)),
),
)
def test_plugin_override_date(datestring, expecteddate):
class FlurgFormat(ixtDateDayMonthYear):
date_format = (*ixtDateDayMonthYear.date_format, "%d-%b-%Y", "%d-%b-%y")

class TestPlugin:
@hookimpl
def ixbrl_add_formats(self) -> List[Type[ixbrlFormat]]:
return [FlurgFormat]

pm.register(TestPlugin(), name="flurg")
format_class = get_format("datedaymonthyear")
try:
with pytest.raises(ValueError):
# check new format is available
assert get_format("flurg") == FlurgFormat
assert format_class == FlurgFormat
assert format_class("datedaymonthyear").parse_value(datestring) == expecteddate
finally:
pm.unregister(name="flurg")

0 comments on commit 23d5d19

Please sign in to comment.