From c0857c5a7771ad0b079ad9ebfa044a99d56311eb Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 29 Jul 2023 14:20:03 +0100 Subject: [PATCH 1/9] Replace pydantic...validate_call with typeguard.typecheck --- inflect/__init__.py | 45 +++++++++++++++++++++++---------------------- setup.cfg | 1 + 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/inflect/__init__.py b/inflect/__init__.py index ccef776..d15551c 100644 --- a/inflect/__init__.py +++ b/inflect/__init__.py @@ -76,6 +76,7 @@ from pydantic import Field +from typeguard import typechecked from typing_extensions import Annotated from more_itertools import windowed_complete @@ -2049,7 +2050,7 @@ def _number_args(self): def _number_args(self, val): self.__number_args = val - @validate_call + @typechecked def defnoun(self, singular: Optional[Word], plural: Optional[Word]) -> int: """ Set the noun plural of singular to plural. @@ -2061,7 +2062,7 @@ def defnoun(self, singular: Optional[Word], plural: Optional[Word]) -> int: self.si_sb_user_defined.extend((plural, singular)) return 1 - @validate_call + @typechecked def defverb( self, s1: Optional[Word], @@ -2086,7 +2087,7 @@ def defverb( self.pl_v_user_defined.extend((s1, p1, s2, p2, s3, p3)) return 1 - @validate_call + @typechecked def defadj(self, singular: Optional[Word], plural: Optional[Word]) -> int: """ Set the adjective plural of singular to plural. @@ -2097,7 +2098,7 @@ def defadj(self, singular: Optional[Word], plural: Optional[Word]) -> int: self.pl_adj_user_defined.extend((singular, plural)) return 1 - @validate_call + @typechecked def defa(self, pattern: Optional[Word]) -> int: """ Define the indefinite article as 'a' for words matching pattern. @@ -2107,7 +2108,7 @@ def defa(self, pattern: Optional[Word]) -> int: self.A_a_user_defined.extend((pattern, "a")) return 1 - @validate_call + @typechecked def defan(self, pattern: Optional[Word]) -> int: """ Define the indefinite article as 'an' for words matching pattern. @@ -2134,7 +2135,7 @@ def checkpatplural(self, pattern: Optional[Word]) -> None: """ return - @validate_call + @typechecked 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) @@ -2274,7 +2275,7 @@ def _string_to_substitute( # 0. PERFORM GENERAL INFLECTIONS IN A STRING - @validate_call + @typechecked def inflect(self, text: Word) -> str: """ Perform inflections in a string. @@ -2351,7 +2352,7 @@ def partition_word(self, text: str) -> Tuple[str, str, str]: else: return "", "", "" - @validate_call + @typechecked def plural(self, text: Word, count: Optional[Union[str, int, Any]] = None) -> str: """ Return the plural of text. @@ -2375,7 +2376,7 @@ def plural(self, text: Word, count: Optional[Union[str, int, Any]] = None) -> st ) return f"{pre}{plural}{post}" - @validate_call + @typechecked def plural_noun( self, text: Word, count: Optional[Union[str, int, Any]] = None ) -> str: @@ -2396,7 +2397,7 @@ def plural_noun( plural = self.postprocess(word, self._plnoun(word, count)) return f"{pre}{plural}{post}" - @validate_call + @typechecked def plural_verb( self, text: Word, count: Optional[Union[str, int, Any]] = None ) -> str: @@ -2420,7 +2421,7 @@ def plural_verb( ) return f"{pre}{plural}{post}" - @validate_call + @typechecked def plural_adj( self, text: Word, count: Optional[Union[str, int, Any]] = None ) -> str: @@ -2441,7 +2442,7 @@ def plural_adj( plural = self.postprocess(word, self._pl_special_adjective(word, count) or word) return f"{pre}{plural}{post}" - @validate_call + @typechecked def compare(self, word1: Word, word2: Word) -> Union[str, bool]: """ compare word1 and word2 for equality regardless of plurality @@ -2472,7 +2473,7 @@ def compare(self, word1: Word, word2: Word) -> Union[str, bool]: results = (self._plequal(word1, word2, norm) for norm in norms) return next(filter(None, results), False) - @validate_call + @typechecked def compare_nouns(self, word1: Word, word2: Word) -> Union[str, bool]: """ compare word1 and word2 for equality regardless of plurality @@ -2488,7 +2489,7 @@ def compare_nouns(self, word1: Word, word2: Word) -> Union[str, bool]: """ return self._plequal(word1, word2, self.plural_noun) - @validate_call + @typechecked def compare_verbs(self, word1: Word, word2: Word) -> Union[str, bool]: """ compare word1 and word2 for equality regardless of plurality @@ -2504,7 +2505,7 @@ def compare_verbs(self, word1: Word, word2: Word) -> Union[str, bool]: """ return self._plequal(word1, word2, self.plural_verb) - @validate_call + @typechecked def compare_adjs(self, word1: Word, word2: Word) -> Union[str, bool]: """ compare word1 and word2 for equality regardless of plurality @@ -2520,7 +2521,7 @@ def compare_adjs(self, word1: Word, word2: Word) -> Union[str, bool]: """ return self._plequal(word1, word2, self.plural_adj) - @validate_call + @typechecked def singular_noun( self, text: Word, @@ -3478,7 +3479,7 @@ def _sinoun( # noqa: C901 # ADJECTIVES - @validate_call + @typechecked def a(self, text: Word, count: Optional[Union[int, str, Any]] = 1) -> str: """ Return the appropriate indefinite article followed by text. @@ -3559,7 +3560,7 @@ def _indef_article(self, word: str, count: Union[int, str, Any]) -> str: # 2. TRANSLATE ZERO-QUANTIFIED $word TO "no plural($word)" - @validate_call + @typechecked 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 @@ -3597,7 +3598,7 @@ def no(self, text: Word, count: Optional[Union[int, str]] = None) -> str: # PARTICIPLES - @validate_call + @typechecked def present_participle(self, word: Word) -> str: """ Return the present participle for word. @@ -3616,7 +3617,7 @@ def present_participle(self, word: Word) -> str: # NUMERICAL INFLECTIONS - @validate_call(config=dict(arbitrary_types_allowed=True)) + @typechecked def ordinal(self, num: Union[Number, Word]) -> str: """ Return the ordinal of num. @@ -3775,7 +3776,7 @@ def enword(self, num: str, group: int) -> str: num = ONE_DIGIT_WORD.sub(self.unitsub, num, 1) return num - @validate_call(config=dict(arbitrary_types_allowed=True)) # noqa: C901 + @typechecked def number_to_words( # noqa: C901 self, num: Union[Number, Word], @@ -3927,7 +3928,7 @@ def number_to_words( # noqa: C901 # Join words with commas and a trailing 'and' (when appropriate)... - @validate_call + @typechecked def join( self, words: Optional[Sequence[Word]], diff --git a/setup.cfg b/setup.cfg index d1c8544..c890e18 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ python_requires = >=3.8 install_requires = more_itertools pydantic >= 1.9.1 + typeguard >= 4.0.1 typing_extensions keywords = plural inflect participle From b164fae8853e6ddb211a897374034adafb46fbff Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 29 Jul 2023 14:59:11 +0100 Subject: [PATCH 2/9] Use a metaclass for Word --- inflect/__init__.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/inflect/__init__.py b/inflect/__init__.py index d15551c..9a70b9b 100644 --- a/inflect/__init__.py +++ b/inflect/__init__.py @@ -59,6 +59,7 @@ import contextlib import itertools from typing import ( + TYPE_CHECKING, Dict, Union, Optional, @@ -2025,10 +2026,24 @@ def __init__(self, orig) -> None: self.last = self.split_[-1] -Word = Annotated[str, Field(min_length=1)] Falsish = Any # ideally, falsish would only validate on bool(value) is False +_STATIC_TYPE_CHECKING = TYPE_CHECKING +# ^-- Workaround for typeguard AST manipulation: +# https://github.com/agronholm/typeguard/issues/353#issuecomment-1556306554 + +if _STATIC_TYPE_CHECKING: + Word = Annotated[str, "String with at least 1 character"] +else: + class _WordMeta(type): # Too dynamic to be supported by mypy... + def __instancecheck__(self, instance: Any) -> bool: + return isinstance(instance, str) and len(instance) >= 1 + + class Word(metaclass=_WordMeta): # type: ignore[no-redef] + """String with at least 1 character""" + + class engine: def __init__(self) -> None: self.classical_dict = def_classical.copy() @@ -2465,9 +2480,7 @@ def compare(self, word1: Word, word2: Word) -> Union[str, bool]: >>> compare('egg', '') Traceback (most recent call last): ... - pydantic...ValidationError: ... - ... - ...at least 1 characters... + typeguard.TypeCheckError:...is not an instance of inflect.Word """ norms = self.plural_noun, self.plural_verb, self.plural_adj results = (self._plequal(word1, word2, norm) for norm in norms) From 4f98d5db84788ff89cc7ec8c405c90f84b0a91c9 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 29 Jul 2023 15:03:26 +0100 Subject: [PATCH 3/9] Replace same_method with regular comparisson --- inflect/__init__.py | 4 ++-- tests/test_pwd.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/inflect/__init__.py b/inflect/__init__.py index 9a70b9b..41a2599 100644 --- a/inflect/__init__.py +++ b/inflect/__init__.py @@ -2592,12 +2592,12 @@ def _plequal(self, word1: str, word2: str, pl) -> Union[str, bool]: # noqa: C90 return "s:p" self.classical_dict = classval.copy() - if same_method(pl, self.plural) or same_method(pl, self.plural_noun): + if pl == self.plural or pl == self.plural_noun: if self._pl_check_plurals_N(word1, word2): return "p:p" if self._pl_check_plurals_N(word2, word1): return "p:p" - if same_method(pl, self.plural) or same_method(pl, self.plural_adj): + if pl == self.plural or pl == self.plural_adj: if self._pl_check_plurals_adj(word1, word2): return "p:p" return False diff --git a/tests/test_pwd.py b/tests/test_pwd.py index acacf00..a37b4cd 100644 --- a/tests/test_pwd.py +++ b/tests/test_pwd.py @@ -8,7 +8,6 @@ UnknownClassicalModeError, ) import inflect -from inflect.compat.pydantic import same_method missing = object() @@ -821,9 +820,9 @@ def test_a_alt(self): p.a("") def test_a_and_an_same_method(self): - assert same_method(inflect.engine.a, inflect.engine.an) + assert inflect.engine.a == inflect.engine.an p = inflect.engine() - assert same_method(p.a, p.an) + assert p.a == p.an def test_no(self): p = inflect.engine() From 979f8fa40e083c38caef7b3fbbce87a3d7c61eb4 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 29 Jul 2023 15:05:57 +0100 Subject: [PATCH 4/9] Fix black error --- inflect/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/inflect/__init__.py b/inflect/__init__.py index 41a2599..83d99c1 100644 --- a/inflect/__init__.py +++ b/inflect/__init__.py @@ -2036,6 +2036,7 @@ def __init__(self, orig) -> None: if _STATIC_TYPE_CHECKING: Word = Annotated[str, "String with at least 1 character"] else: + class _WordMeta(type): # Too dynamic to be supported by mypy... def __instancecheck__(self, instance: Any) -> bool: return isinstance(instance, str) and len(instance) >= 1 From 376c97746be541f89255745cb7ccf34d419f015a Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 29 Jul 2023 15:06:52 +0100 Subject: [PATCH 5/9] Remove pydantic imports --- inflect/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/inflect/__init__.py b/inflect/__init__.py index 83d99c1..ebb4662 100644 --- a/inflect/__init__.py +++ b/inflect/__init__.py @@ -76,16 +76,11 @@ from numbers import Number -from pydantic import Field from typeguard import typechecked from typing_extensions import Annotated from more_itertools import windowed_complete -from .compat.pydantic1 import validate_call -from .compat.pydantic import same_method - - class UnknownClassicalModeError(Exception): pass From 46c641c22ae5292a23d63a9b970ae92352987671 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 29 Jul 2023 15:09:48 +0100 Subject: [PATCH 6/9] Remove pydantic compatibility modules --- inflect/compat/__init__.py | 0 inflect/compat/pydantic.py | 27 --------------------------- inflect/compat/pydantic1.py | 8 -------- 3 files changed, 35 deletions(-) delete mode 100644 inflect/compat/__init__.py delete mode 100644 inflect/compat/pydantic.py delete mode 100644 inflect/compat/pydantic1.py diff --git a/inflect/compat/__init__.py b/inflect/compat/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/inflect/compat/pydantic.py b/inflect/compat/pydantic.py deleted file mode 100644 index 1e3fc1b..0000000 --- a/inflect/compat/pydantic.py +++ /dev/null @@ -1,27 +0,0 @@ -class ValidateCallWrapperWrapper: - def __new__(cls, wrapped): - # bypass wrapper if wrapped method has the fix - if wrapped.__class__.__name__ != 'ValidateCallWrapper': - return wrapped - if hasattr(wrapped, '_name'): - return wrapped - return super().__new__(cls) - - def __init__(self, wrapped): - self.orig = wrapped - - def __eq__(self, other): - return self.raw_function == other.raw_function - - @property - def raw_function(self): - return getattr(self.orig, 'raw_function') - - -def same_method(m1, m2) -> bool: - """ - Return whether m1 and m2 are the same method. - - Workaround for pydantic/pydantic#6390. - """ - return ValidateCallWrapperWrapper(m1) == ValidateCallWrapperWrapper(m2) diff --git a/inflect/compat/pydantic1.py b/inflect/compat/pydantic1.py deleted file mode 100644 index 8262fdc..0000000 --- a/inflect/compat/pydantic1.py +++ /dev/null @@ -1,8 +0,0 @@ -try: - from pydantic import validate_call # type: ignore -except ImportError: - # Pydantic 1 - from pydantic import validate_arguments as validate_call # type: ignore - - -__all__ = ['validate_call'] From dd61dd457f506df722e0acc5c89524091ec8aaab Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 29 Jul 2023 15:11:47 +0100 Subject: [PATCH 7/9] Remove dependency on pydantic --- .github/workflows/main.yml | 2 -- setup.cfg | 1 - tox.ini | 4 ---- 3 files changed, 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 27bfda5..82ac1bb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,8 +58,6 @@ jobs: platform: ubuntu-latest - python: "3.x" platform: ubuntu-latest - env: - TOX_ENV: pydantic1 runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.python == '3.12' }} steps: diff --git a/setup.cfg b/setup.cfg index c890e18..8ede649 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,6 @@ include_package_data = true python_requires = >=3.8 install_requires = more_itertools - pydantic >= 1.9.1 typeguard >= 4.0.1 typing_extensions keywords = plural inflect participle diff --git a/tox.ini b/tox.ini index e72c064..e51d652 100644 --- a/tox.ini +++ b/tox.ini @@ -8,10 +8,6 @@ usedevelop = True extras = testing -[testenv:pydantic1] -deps = - pydantic < 2 - [testenv:docs] extras = docs From fea125b7e7e85bc6a41f6a4f8bb380bbd691dcc4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 31 Mar 2024 03:44:14 -0400 Subject: [PATCH 8/9] Add news fragment. --- newsfragments/195.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/195.feature.rst diff --git a/newsfragments/195.feature.rst b/newsfragments/195.feature.rst new file mode 100644 index 0000000..f61e285 --- /dev/null +++ b/newsfragments/195.feature.rst @@ -0,0 +1 @@ +Replace pydantic with typeguard \ No newline at end of file From c8c27d950ca19c1fe0209c71b8f3f80012874d92 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 31 Mar 2024 03:57:09 -0400 Subject: [PATCH 9/9] Mark type checking block as uncovered. --- inflect/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inflect/__init__.py b/inflect/__init__.py index 22d7669..fe9ce3a 100644 --- a/inflect/__init__.py +++ b/inflect/__init__.py @@ -2026,7 +2026,7 @@ def __init__(self, orig) -> None: # ^-- Workaround for typeguard AST manipulation: # https://github.com/agronholm/typeguard/issues/353#issuecomment-1556306554 -if _STATIC_TYPE_CHECKING: +if _STATIC_TYPE_CHECKING: # pragma: no cover Word = Annotated[str, "String with at least 1 character"] else: