Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,33 @@ schema = {
- strip-whitespaces
- left-strip

##### map

Maps a raw value to another one. By default, a value which is not one of the
`values` keys raises an error. Set the optional `default` parameter to map those
values instead of failing:

```python
{
"key": "unit-of-measurement",
"column-number": 12,
"type": "int",
"pre-processors": [
{
"name": "map",
"parameters": {"values": {"K": 0, "A": 1, "L": 2}, "default": 0},
}
],
}
```

An empty string can be used as a key to map blank values:
`{"values": {"": 0, "K": 1}}`. Note that a field whose raw value is empty is
never given to its pre-processors: it fails as a required field, or is parsed as
`None` when the field is declared `"optional": True`. Mapping an empty value
therefore only applies when a previous pre-processor produces an empty string,
for instance `strip-whitespaces` on a blank columnar field.

#### Validators

- regex-matches
Expand Down
13 changes: 12 additions & 1 deletion magicparse/pre_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,27 @@ def key() -> str:
return "left-pad-zeroes"


class _NoDefault:
"Sentinel allowing 'None' to be used as a mapping default value"


NO_DEFAULT = _NoDefault()


class Map(PreProcessor):
def __init__(self, on_error: OnError, values: dict[str, Any]) -> None:
def __init__(self, on_error: OnError, values: dict[str, Any], default: Any = NO_DEFAULT) -> None:
super().__init__(on_error)
self.values = values
self.default = default
self._keys = ", ".join(f"'{key}'" for key in self.values.keys())

def apply(self, value: str) -> str:
try:
return self.values[value]
except:
if not isinstance(self.default, _NoDefault):
return self.default

raise ValueError(f"value '{value}' does not map to any values in [{self._keys}]")

@staticmethod
Expand Down
30 changes: 30 additions & 0 deletions tests/test_pre_processors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
from typing import Any
from magicparse.pre_processors import (
NO_DEFAULT,
LeftPadZeroes,
Map,
PreProcessor,
Expand All @@ -23,6 +24,15 @@ def test_map(self):
pre_processor = PreProcessor.build({"name": "map", "parameters": {"values": {"input": "output"}}})
assert isinstance(pre_processor, Map)
assert pre_processor.values == {"input": "output"}
assert pre_processor.default is NO_DEFAULT

def test_map_with_default(self):
pre_processor = PreProcessor.build(
{"name": "map", "parameters": {"values": {"input": "output"}, "default": "fallback"}}
)
assert isinstance(pre_processor, Map)
assert pre_processor.values == {"input": "output"}
assert pre_processor.default == "fallback"

def test_replace(self):
pre_processor = PreProcessor.build({"name": "replace", "parameters": {"pattern": "aa", "replacement": "bb"}})
Expand Down Expand Up @@ -81,6 +91,26 @@ def test_known_input(self):
pre_processor = PreProcessor.build({"name": "map", "parameters": {"values": {"A": "1", "B": "2"}}})
assert pre_processor.apply("A") == "1"

def test_unknown_input_with_default(self):
pre_processor = PreProcessor.build(
{"name": "map", "parameters": {"values": {"A": "1", "B": "2"}, "default": "0"}}
)
assert pre_processor.apply("an input") == "0"

def test_known_input_with_default(self):
pre_processor = PreProcessor.build(
{"name": "map", "parameters": {"values": {"A": "1", "B": "2"}, "default": "0"}}
)
assert pre_processor.apply("A") == "1"

def test_none_default(self):
pre_processor = PreProcessor.build({"name": "map", "parameters": {"values": {"A": "1"}, "default": None}})
assert pre_processor.apply("an input") is None

def test_empty_value_can_be_mapped(self):
pre_processor = PreProcessor.build({"name": "map", "parameters": {"values": {"": "1", "A": "2"}}})
assert pre_processor.apply("") == "1"


class TestReplace(TestCase):
def test_pattern_not_found(self):
Expand Down
Loading