Add tests

This commit is contained in:
Patrick Elmer 2023-02-21 15:25:53 +09:00
parent 3018d16cfe
commit 2515f8b343
6 changed files with 183 additions and 20 deletions

View File

@ -1,13 +1,60 @@
import re
import argparse
__version__ = '0.1.0'
def main():
"""
CLI entry point of the program.
"""
parser = argparse.ArgumentParser(prog="soundchanger")
parser.add_argument(
"changes",
help="Sound change to be applied. Multiple sound changes should be separated by a space."
)
parser.add_argument(
"strings",
help="Word that the sound change should be applied to. Multiple words should be separated by a space."
)
parser.add_argument(
"-C", "--categories",
default={},
help="Categories to be used in the sound change."
)
parser.add_argument(
"-i", "--ignore-errors",
action="store_false",
help="Categories to be used in the sound change."
)
parser.add_argument(
"-z", "--zero-characters",
default='∅-',
help="Characters that should be empty strings in the changed words."
)
parser.add_argument(
"-v", "--version",
action="version",
version=f"%(prog)s {__version__}"
)
args = parser.parse_args()
print(apply(
args.changes.split(),
args.strings.split(),
args.categories,
args.ignore_errors,
args.zero_characters
))
def apply(
changes,
strings,
apply=True,
categories={},
ignore_errors=True,
zero_characters=['']):
zero_characters='-'):
"""
Applies a sound change or a list of sound changes to a string or
a list of given strings.
@ -16,22 +63,19 @@ def apply(
If the input value is of type str, the output will also be of type str.
Options:
- apply (default: True)
Whether or not the changes should be applied.
- categories (default: {})
Which categories will be detected.
For vowels it would be {'V'='aeiou'} or {'V'=['a', 'e', 'i', 'o', 'u']})
- ignore_errors (default: True)
If this option is set to `True`, any erroneous sound change will be skipped.
If set to `False`, a ValueError will be raised.
- zero_characters (default: [''])
If set to `False`, a ValueError will be raised instead.
- zero_characters (default: '∅-')
These characters will be removed in the changed words.
For example, `apply('h>∅', 'aha')` will return 'aa', not 'a∅a'.
"""
if not apply:
return strings
if isinstance(changes, str):
changes = [changes]
@ -42,10 +86,10 @@ def apply(
strings = strings.copy()
for change in changes:
if validate_change(change, ignore_errors=ignore_errors) == False:
if is_valid_change(change, ignore_errors=ignore_errors) == False:
continue
change = convert_change_to_regex(change, categories=categories, zero_characters=zero_characters)
change = reformat_change_to_regex(change, categories=categories, zero_characters=zero_characters)
original, change_to, before, after = split_change(change)
pattern = f"({before})({original})({after})"
@ -60,8 +104,15 @@ def apply(
return strings
def validate_change(change, ignore_errors):
valid = re.search(r'^[^>_/]+?>[^>_/]*?(:?/[^>_/]*?_+[^>_/]*)?$', change)
def is_valid_change(change, ignore_errors=True):
"""
Returns `True` if the change is valid.
If the change is not valid, it returns `False`
unless `ignore_errors` is set to `False`,
in which case it raises a ValueError.
"""
valid = re.search(r'^[^>_/]+?>[^>_/]*?/?(:?[^>_/]*?_+[^>_/]*)?$', change)
if ignore_errors:
return valid != None
if not valid:
@ -69,7 +120,14 @@ def validate_change(change, ignore_errors):
return True
def convert_change_to_regex(change, categories, zero_characters):
def reformat_change_to_regex(change, categories={}, zero_characters='∅-'):
"""
Reformats a sound change for use in regular expressions.
Replaces mentions of categories with their actual values
and removes all zero characters.
Returns the reformatted change.
"""
# Prepare change for regex
for k, v in {' ': '', '{': '(', '}': ')', ',': '|'}.items():
change = change.replace(k, v)
@ -89,15 +147,33 @@ def convert_change_to_regex(change, categories, zero_characters):
def split_change(change):
"""
Splits a valid change into four components.
Syntax:
original > change_to / before __ after
Change Environment
Returns a tuple of the four components:
(original, change_to, before, after)
"""
if '/' in change:
change, environment = change.split('/')
else:
environment = '_'
# Collaplse multiple underscores
# Collapse multiple underscores
environment = re.sub('_+', '_', environment)
original, change_to = change.split('>')
if '_' in environment:
before, after = environment.split('_')
else:
before = after = ''
return (original, change_to, before, after)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,32 @@
from soundchanger.change import is_valid_change
import pytest
def test_valid_change():
assert is_valid_change('a>b/c_d') == True
assert is_valid_change('a>b/c__d') == True
assert is_valid_change('a>b/c_') == True
assert is_valid_change('a>b/_d') == True
assert is_valid_change('a>b') == True
assert is_valid_change('a>b/_') == True
assert is_valid_change('a>b/') == True
def test_invalid_change():
assert is_valid_change('>') == False
assert is_valid_change('>b') == False
assert is_valid_change('a>b/a') == False
assert is_valid_change('>/_') == False
assert is_valid_change('a>b//c_d') == False
def test_raises_value_error():
with pytest.raises(ValueError):
is_valid_change('>', ignore_errors=False)
with pytest.raises(ValueError):
is_valid_change('>', ignore_errors=False)
with pytest.raises(ValueError):
is_valid_change('>b', ignore_errors=False)
with pytest.raises(ValueError):
is_valid_change('a>b/a', ignore_errors=False)
with pytest.raises(ValueError):
is_valid_change('>/_', ignore_errors=False)
with pytest.raises(ValueError):
is_valid_change('a>b//c_d', ignore_errors=False)

View File

@ -0,0 +1,21 @@
from soundchanger.change import reformat_change_to_regex
def test_replace_spaces():
assert reformat_change_to_regex(' a > b / c _ d ') == 'a>b/c_d'
def test_replace_brackets_and_commas():
assert reformat_change_to_regex('a>b/{#,a}_') == 'a>b/(#|a)_'
def test_replace_categories():
assert reformat_change_to_regex('a>b/V_',
categories={'V': 'aiu'}
) == 'a>b/(a|i|u)_'
def test_replace_categories_in_brackets():
assert reformat_change_to_regex('a>b/{#,V}_',
categories={'V': 'aiu'}
) == 'a>b/(#|(a|i|u))_'
def test_replace_zero_characters():
assert reformat_change_to_regex('a>∅') == 'a>'

View File

@ -0,0 +1,34 @@
from soundchanger.change import split_change
import pytest
def test_simple_change():
assert split_change('a>b') == ('a', 'b', '', '')
def test_change_with_slash():
assert split_change('a>b/') == ('a', 'b', '', '')
def test_change_with_environment():
assert split_change('a>b/c_d') == ('a', 'b', 'c', 'd')
def test_change_with_environment_double_underscore():
assert split_change('a>b/c__d') == ('a', 'b', 'c', 'd')
def test_change_with_environment_before():
assert split_change('a>b/#_') == ('a', 'b', '#', '')
def test_change_with_environment_after():
assert split_change('a>b/_#') == ('a', 'b', '', '#')
def test_change_with_empty_environment():
assert split_change('a>b/_') == ('a', 'b', '', '')
def test_change_to_nothing():
assert split_change('a>') == ('a', '', '', '')
def test_change_to_zero():
assert split_change('a>-') == ('a', '-', '', '')
def test_invalid_input():
with pytest.raises(ValueError):
assert split_change('')