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 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( def apply(
changes, changes,
strings, strings,
apply=True, categories={},
categories={}, ignore_errors=True,
ignore_errors=True, zero_characters='∅-'):
zero_characters=['']):
""" """
Applies a sound change or a list of sound changes to a string or Applies a sound change or a list of sound changes to a string or
a list of given strings. 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. If the input value is of type str, the output will also be of type str.
Options: Options:
- apply (default: True)
Whether or not the changes should be applied.
- categories (default: {}) - categories (default: {})
Which categories will be detected. Which categories will be detected.
For vowels it would be {'V'='aeiou'} or {'V'=['a', 'e', 'i', 'o', 'u']}) For vowels it would be {'V'='aeiou'} or {'V'=['a', 'e', 'i', 'o', 'u']})
- ignore_errors (default: True) - ignore_errors (default: True)
If this option is set to `True`, any erroneous sound change will be skipped. If this option is set to `True`, any erroneous sound change will be skipped.
If set to `False`, a ValueError will be raised. If set to `False`, a ValueError will be raised instead.
- zero_characters (default: [''])
- zero_characters (default: '∅-')
These characters will be removed in the changed words. These characters will be removed in the changed words.
For example, `apply('h>∅', 'aha')` will return 'aa', not 'a∅a'. For example, `apply('h>∅', 'aha')` will return 'aa', not 'a∅a'.
""" """
if not apply:
return strings
if isinstance(changes, str): if isinstance(changes, str):
changes = [changes] changes = [changes]
@ -42,10 +86,10 @@ def apply(
strings = strings.copy() strings = strings.copy()
for change in changes: for change in changes:
if validate_change(change, ignore_errors=ignore_errors) == False: if is_valid_change(change, ignore_errors=ignore_errors) == False:
continue 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) original, change_to, before, after = split_change(change)
pattern = f"({before})({original})({after})" pattern = f"({before})({original})({after})"
@ -60,8 +104,15 @@ def apply(
return strings return strings
def validate_change(change, ignore_errors): def is_valid_change(change, ignore_errors=True):
valid = re.search(r'^[^>_/]+?>[^>_/]*?(:?/[^>_/]*?_+[^>_/]*)?$', change) """
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: if ignore_errors:
return valid != None return valid != None
if not valid: if not valid:
@ -69,7 +120,14 @@ def validate_change(change, ignore_errors):
return True 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 # Prepare change for regex
for k, v in {' ': '', '{': '(', '}': ')', ',': '|'}.items(): for k, v in {' ': '', '{': '(', '}': ')', ',': '|'}.items():
change = change.replace(k, v) change = change.replace(k, v)
@ -89,15 +147,33 @@ def convert_change_to_regex(change, categories, zero_characters):
def split_change(change): 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: if '/' in change:
change, environment = change.split('/') change, environment = change.split('/')
else: else:
environment = '_' environment = '_'
# Collaplse multiple underscores # Collapse multiple underscores
environment = re.sub('_+', '_', environment) environment = re.sub('_+', '_', environment)
original, change_to = change.split('>') original, change_to = change.split('>')
before, after = environment.split('_') if '_' in environment:
before, after = environment.split('_')
else:
before = after = ''
return (original, change_to, 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('')