From bc8143e376063f8fd481b70f70930d432b346a74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jul 2022 14:44:49 -0400 Subject: [PATCH] Additionally validate words in other public methods. Doing so required expanding some of the interfaces to pass validation. Blank words are no longer allowed. --- CHANGES.rst | 12 ++++++++- inflect/__init__.py | 60 +++++++++++++++++++++++++++++++-------------- tests/test_pwd.py | 19 ++++++++------ 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9290a28..3488335 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -v5.7.0 +v6.0.0 ====== * #157: ``compare`` methods now validate their inputs @@ -6,6 +6,16 @@ v5.7.0 empty string or None is passed. This expectation is now documented. +* Many public methods now perform validation on arguments. + An empty string is no longer allowed for words or text. + Callers are expected to pass non-empty text or trap + the validation errors that are raised. The exceptions + raised are ``pydantic.error_wrappers.ValidationError``, + which are currently a subclass of ``ValueError``, but since + that + `may change `_, + tests check for a generic ``Exception``. + v5.6.2 ====== diff --git a/inflect/__init__.py b/inflect/__init__.py index 824fe2d..e357f99 100644 --- a/inflect/__init__.py +++ b/inflect/__init__.py @@ -64,7 +64,9 @@ Callable, Sequence, cast, + Any, ) +from numbers import Number from pydantic import Field, validate_arguments @@ -2038,6 +2040,7 @@ def __init__(self, orig) -> None: Word = Annotated[str, Field(min_length=1)] +Falsish = Any # ideally, falsish would only validate on bool(value) is False class engine: @@ -2156,14 +2159,15 @@ def checkpatplural(self, pattern: str) -> None: """ return - def ud_match(self, word: str, wordlist: List[str]) -> Optional[str]: + @validate_arguments + def ud_match(self, word: Word, wordlist: Sequence[Optional[Word]]) -> Optional[str]: for i in range(len(wordlist) - 2, -2, -2): # backwards through even elements mo = re.search(fr"^{wordlist[i]}$", word, re.IGNORECASE) if mo: if wordlist[i + 1] is None: return None pl = DOLLAR_DIGITS.sub( - r"\\1", wordlist[i + 1] + r"\\1", cast(Word, wordlist[i + 1]) ) # change $n to \n for expand return mo.expand(pl) return None @@ -2295,7 +2299,8 @@ def _string_to_substitute( # 0. PERFORM GENERAL INFLECTIONS IN A STRING - def inflect(self, text: str) -> str: + @validate_arguments + def inflect(self, text: Word) -> str: """ Perform inflections in a string. @@ -2371,7 +2376,8 @@ def partition_word(self, text: str) -> Tuple[str, str, str]: else: return "", "", "" - def plural(self, text: str, count: Optional[Union[str, int]] = None) -> str: + @validate_arguments + def plural(self, text: Word, count: Optional[Union[str, int, Any]] = None) -> str: """ Return the plural of text. @@ -2394,7 +2400,10 @@ def plural(self, text: str, count: Optional[Union[str, int]] = None) -> str: ) return f"{pre}{plural}{post}" - def plural_noun(self, text: str, count: Optional[Union[str, int]] = None) -> str: + @validate_arguments + def plural_noun( + self, text: Word, count: Optional[Union[str, int, Any]] = None + ) -> str: """ Return the plural of text, where text is a noun. @@ -2412,7 +2421,10 @@ def plural_noun(self, text: str, count: Optional[Union[str, int]] = None) -> str plural = self.postprocess(word, self._plnoun(word, count)) return f"{pre}{plural}{post}" - def plural_verb(self, text: str, count: Optional[Union[str, int]] = None) -> str: + @validate_arguments + def plural_verb( + self, text: Word, count: Optional[Union[str, int, Any]] = None + ) -> str: """ Return the plural of text, where text is a verb. @@ -2433,7 +2445,10 @@ def plural_verb(self, text: str, count: Optional[Union[str, int]] = None) -> str ) return f"{pre}{plural}{post}" - def plural_adj(self, text: str, count: str = None) -> str: + @validate_arguments + def plural_adj( + self, text: Word, count: Optional[Union[str, int, Any]] = None + ) -> str: """ Return the plural of text, where text is an adjective. @@ -2530,10 +2545,11 @@ def compare_adjs(self, word1: Word, word2: Word) -> Union[str, bool]: """ return self._plequal(word1, word2, self.plural_adj) + @validate_arguments def singular_noun( self, - text: str, - count: Optional[Union[int, str]] = None, + text: Word, + count: Optional[Union[int, str, Any]] = None, gender: Optional[str] = None, ) -> Union[str, bool]: """ @@ -3465,7 +3481,8 @@ def _sinoun( # noqa: C901 # ADJECTIVES - def a(self, text: str, count: int = 1) -> str: + @validate_arguments + def a(self, text: Word, count: Optional[Union[int, str, Any]] = 1) -> str: """ Return the appropriate indefinite article followed by text. @@ -3490,7 +3507,9 @@ def a(self, text: str, count: int = 1) -> str: an = a - def _indef_article(self, word: str, count: int) -> str: # noqa: C901 + def _indef_article( + self, word: str, count: Union[int, str, Any] + ) -> str: # noqa: C901 mycount = self.get_count(count) if mycount != 1: @@ -3541,7 +3560,8 @@ def _indef_article(self, word: str, count: int) -> str: # noqa: C901 # 2. TRANSLATE ZERO-QUANTIFIED $word TO "no plural($word)" - def no(self, text: str, count: Optional[Union[int, str]] = None) -> str: + @validate_arguments + def no(self, text: Word, count: Optional[Union[int, str]] = None) -> str: """ If count is 0, no, zero or nil, return 'no' followed by the plural of text. @@ -3578,7 +3598,8 @@ def no(self, text: str, count: Optional[Union[int, str]] = None) -> str: # PARTICIPLES - def present_participle(self, word: str) -> str: + @validate_arguments + def present_participle(self, word: Word) -> str: """ Return the present participle for word. @@ -3596,7 +3617,8 @@ def present_participle(self, word: str) -> str: # NUMERICAL INFLECTIONS - def ordinal(self, num: Union[int, str]) -> str: # noqa: C901 + @validate_arguments + def ordinal(self, num: Union[int, Word]) -> str: # noqa: C901 """ Return the ordinal of num. @@ -3755,16 +3777,17 @@ def enword(self, num: str, group: int) -> str: num = ONE_DIGIT_WORD.sub(self.unitsub, num, 1) return num + @validate_arguments(config=dict(arbitrary_types_allowed=True)) def number_to_words( # noqa: C901 self, - num: Union[int, str], + num: Union[Number, Word], wantlist: bool = False, group: int = 0, - comma: str = ",", + comma: Union[Falsish, str] = ",", andword: str = "and", zero: str = "zero", one: str = "one", - decimal: str = "point", + decimal: Union[Falsish, str] = "point", threshold: Optional[int] = None, ) -> Union[str, List[str]]: """ @@ -3906,9 +3929,10 @@ def number_to_words( # noqa: C901 # Join words with commas and a trailing 'and' (when appropriate)... + @validate_arguments def join( self, - words: Optional[Sequence[str]], + words: Optional[Sequence[Word]], sep: Optional[str] = None, sep_spaced: bool = True, final_sep: Optional[str] = None, diff --git a/tests/test_pwd.py b/tests/test_pwd.py index 21a20f6..a8b841e 100644 --- a/tests/test_pwd.py +++ b/tests/test_pwd.py @@ -2,6 +2,8 @@ import unittest +import pytest + from inflect import ( BadChunkingOptionError, NumOutOfRangeError, @@ -253,7 +255,6 @@ def test_partition_word(self): def test_pl(self): p = inflect.engine() for fn, sing, plur in ( - (p.plural, "", ""), (p.plural, "cow", "cows"), (p.plural, "thought", "thoughts"), (p.plural, "mouse", "mice"), @@ -265,15 +266,12 @@ def test_pl(self): (p.plural, "carmen", "carmina"), (p.plural, "quartz", "quartzes"), (p.plural, "care", "cares"), - (p.plural_noun, "", ""), (p.plural_noun, "cow", "cows"), (p.plural_noun, "thought", "thoughts"), (p.plural_noun, "snooze", "snoozes"), - (p.plural_verb, "", ""), (p.plural_verb, "runs", "run"), (p.plural_verb, "thought", "thought"), (p.plural_verb, "eyes", "eye"), - (p.plural_adj, "", ""), (p.plural_adj, "a", "some"), (p.plural_adj, "this", "these"), (p.plural_adj, "that", "those"), @@ -313,6 +311,12 @@ def test_pl(self): self.assertEqual(p.plural("die"), "dice") self.assertEqual(p.plural_noun("die"), "dice") + with pytest.raises(Exception): + p.plural("") + p.plural_noun("") + p.plural_verb("") + p.plural_adj("") + def test_sinoun(self): p = inflect.engine() for sing, plur in ( @@ -567,7 +571,6 @@ def test_count(self): def test__plnoun(self): p = inflect.engine() for sing, plur in ( - ("", ""), ("tuna", "tuna"), ("TUNA", "TUNA"), ("swordfish", "swordfish"), @@ -698,7 +701,8 @@ def test_classical_pl(self): def test__pl_special_verb(self): p = inflect.engine() - self.assertEqual(p._pl_special_verb(""), False) + with pytest.raises(Exception): + self.assertEqual(p._pl_special_verb(""), False) self.assertEqual(p._pl_special_verb("am"), "are") self.assertEqual(p._pl_special_verb("am", 0), "are") self.assertEqual(p._pl_special_verb("runs", 0), "run") @@ -800,7 +804,8 @@ def test_a(self): self.assertEqual(p.a("cat", 2), "2 cat") self.assertEqual(p.a, p.an) - self.assertEqual(p.a(""), "") + with pytest.raises(Exception): + p.a("") def test_no(self): p = inflect.engine()