lexeme-type 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: lexeme-type
3
+ Version: 1.0.0
4
+ Summary: TODO
5
+ Author: Gwyn Uttmark
6
+ Author-email: biosafetylevel5@gmail.com
7
+ Requires-Python: >=3.11,<3.14
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Typing :: Typed
17
+ Provides-Extra: doc
18
+ Provides-Extra: lint
19
+ Provides-Extra: test
20
+ Requires-Dist: commitizen ; extra == "doc"
21
+ Requires-Dist: doc8 ; extra == "doc" or extra == "lint"
22
+ Requires-Dist: mypy ; extra == "lint"
23
+ Requires-Dist: myst-parser ; extra == "doc"
24
+ Requires-Dist: pinkrst ; extra == "doc" or extra == "lint"
25
+ Requires-Dist: prettier ; extra == "lint"
26
+ Requires-Dist: pydantic ; extra == "test"
27
+ Requires-Dist: pytest ; extra == "test"
28
+ Requires-Dist: pytest-cov ; extra == "test"
29
+ Requires-Dist: pytest-xdist ; extra == "test"
30
+ Requires-Dist: ruff ; extra == "lint"
31
+ Requires-Dist: sphinx ; extra == "doc"
32
+ Requires-Dist: sphinx-argparse ; extra == "doc"
33
+ Requires-Dist: sphinx-autodoc-typehints ; extra == "doc"
34
+ Requires-Dist: sphinx-copybutton ; extra == "doc"
35
+ Requires-Dist: sphinx-rtd-theme ; extra == "doc"
36
+ Requires-Dist: sphinxcontrib-mermaid ; extra == "doc"
37
+ Project-URL: Documentation, https://biosafetylvl5.github.io/lexeme-type/
38
+ Project-URL: Homepage, https://github.com/biosafetylvl5/lexeme-type
39
+ Project-URL: Issues, https://github.com/biosafetylvl5/lexeme-type/issues
40
+ Project-URL: Repository, https://github.com/biosafetylvl5/lexeme-type
41
+ Description-Content-Type: text/markdown
42
+
43
+ # lexeme-type
44
+
45
+ ![Tests](.github/badges/tests-badge.svg)
46
+ ![Coverage](.github/badges/coverage-badge.svg)
47
+ ![Mypy](.github/badges/mypy-badge.svg)
48
+ ![Ruff](.github/badges/ruff-badge.svg)
49
+ ![Install](.github/badges/install-badge.svg)
50
+ ![CSpell](.github/badges/cspell-badge.svg)
51
+ ![Commitizen](.github/badges/commitizen-badge.svg)
52
+
53
+ [![GitHub stars](https://img.shields.io/github/stars/biosafetylvl5/lexeme-type.svg)](https://github.com/biosafetylvl5/lexeme-type/stargazers)
54
+
55
+ # Lexeme type
56
+
57
+ Lightweight no-dependency helper for treating singular and plural spellings as equivalent.
58
+
59
+ A drop‑in str subclass that normalizes English nouns so that their
60
+ singular and plural forms compare as equal, hash to the same key, and work
61
+ inside Pydantic v2 models. Ignores capitalization.
62
+
63
+ Examples
64
+
65
+ ```python
66
+ >>> from partial_lexeme.lexeme import Lexeme
67
+ >>> Lexeme("reader") == "readers" == Lexeme("readers")
68
+ True
69
+ >>> {Lexeme("analyses"): 1} == {"analysis": 1}
70
+ True
71
+
72
+ >>> from pydantic import BaseModel
73
+ >>> class _Plugin(BaseModel):
74
+ >>> kind: Lexeme
75
+ >>> interface: Lexeme
76
+ >>> model = _Plugin(kind="reader", interface="readers")
77
+ >>> assert model.kind == model.interface == "reader"
78
+ ```
79
+
@@ -0,0 +1,36 @@
1
+ # lexeme-type
2
+
3
+ ![Tests](.github/badges/tests-badge.svg)
4
+ ![Coverage](.github/badges/coverage-badge.svg)
5
+ ![Mypy](.github/badges/mypy-badge.svg)
6
+ ![Ruff](.github/badges/ruff-badge.svg)
7
+ ![Install](.github/badges/install-badge.svg)
8
+ ![CSpell](.github/badges/cspell-badge.svg)
9
+ ![Commitizen](.github/badges/commitizen-badge.svg)
10
+
11
+ [![GitHub stars](https://img.shields.io/github/stars/biosafetylvl5/lexeme-type.svg)](https://github.com/biosafetylvl5/lexeme-type/stargazers)
12
+
13
+ # Lexeme type
14
+
15
+ Lightweight no-dependency helper for treating singular and plural spellings as equivalent.
16
+
17
+ A drop‑in str subclass that normalizes English nouns so that their
18
+ singular and plural forms compare as equal, hash to the same key, and work
19
+ inside Pydantic v2 models. Ignores capitalization.
20
+
21
+ Examples
22
+
23
+ ```python
24
+ >>> from partial_lexeme.lexeme import Lexeme
25
+ >>> Lexeme("reader") == "readers" == Lexeme("readers")
26
+ True
27
+ >>> {Lexeme("analyses"): 1} == {"analysis": 1}
28
+ True
29
+
30
+ >>> from pydantic import BaseModel
31
+ >>> class _Plugin(BaseModel):
32
+ >>> kind: Lexeme
33
+ >>> interface: Lexeme
34
+ >>> model = _Plugin(kind="reader", interface="readers")
35
+ >>> assert model.kind == model.interface == "reader"
36
+ ```
@@ -0,0 +1,221 @@
1
+ [tool.poetry]
2
+ name = "lexeme-type"
3
+ version = "1.0.0"
4
+ description = "TODO"
5
+ authors = ["Gwyn Uttmark <biosafetylevel5@gmail.com>"]
6
+ license = ""
7
+ repository = "https://github.com/biosafetylvl5/lexeme-type"
8
+
9
+ packages = [{include="lexeme_type", from="src"}]
10
+
11
+ readme = "README.md"
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.11",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Programming Language :: Python :: 3.13",
17
+ "Programming Language :: Python :: 3.14",
18
+ "Operating System :: OS Independent",
19
+ "Development Status :: 3 - Alpha",
20
+ "Intended Audience :: Developers",
21
+ "Typing :: Typed",
22
+ ]
23
+
24
+ [tool.poetry.dependencies]
25
+ python = ">=3.11,<3.14"
26
+
27
+ # Optional dependencies for extras
28
+ sphinx = { version = "*", optional = true }
29
+ sphinx-rtd-theme = { version = "*", optional = true }
30
+ sphinx-argparse = { version = "*", optional = true }
31
+ sphinx-copybutton = { version = "*", optional = true }
32
+ sphinx-autodoc-typehints = { version = "*", optional = true }
33
+ sphinxcontrib-mermaid = { version = "*", optional = true }
34
+ myst-parser = { version = "*", optional = true }
35
+ pinkrst = { version = "*", optional = true }
36
+ doc8 = { version = "*", optional = true }
37
+ prettier = { version = "*", optional = true }
38
+ ruff = { version = "*", optional = true }
39
+ mypy = { version = "*", optional = true }
40
+ pytest = { version = "*", optional = true }
41
+ pytest-cov = { version = "*", optional = true }
42
+ pytest-xdist = { version = "*", optional = true }
43
+ commitizen = { version = "*", optional = true }
44
+ pydantic = { version = "*", optional = true }
45
+
46
+ [tool.poetry.group.dev.dependencies]
47
+ # Development dependencies that are always installed for development
48
+ black = "*"
49
+ pre-commit = "*"
50
+ brassy = "*"
51
+ commitizen = "*"
52
+
53
+ [tool.poetry.extras]
54
+ doc = [
55
+ "sphinx", # for docs building
56
+ "myst-parser",
57
+ "sphinx-rtd-theme", # for docs building
58
+ "sphinx-copybutton", # for docs building
59
+ "sphinx-autodoc-typehints", # for docs building
60
+ "sphinxcontrib-mermaid", # for docs building
61
+ "sphinx-argparse", # for docs building
62
+ "pinkrst", # for rst formatting
63
+ "doc8", # for rst linting
64
+ "commitizen", # for better commits
65
+ ]
66
+
67
+ lint = [
68
+ "pinkrst", # for rst auto-formatting
69
+ "doc8", # for rst linting
70
+ "prettier", # YAML, JSON linting
71
+ "ruff", # Python linting
72
+ "mypy", # Type checking
73
+ ]
74
+
75
+ test = [
76
+ "pytest", # for running tests
77
+ "pytest-cov", # for test coverage
78
+ "pytest-xdist", # for parallel testing
79
+ "pydantic", # for pydantic compat. testing
80
+ ]
81
+
82
+ [tool.poetry.scripts]
83
+ # project_cli = "project.cli:main"
84
+
85
+ [tool.poetry.urls]
86
+ Homepage = "https://github.com/biosafetylvl5/lexeme-type"
87
+ Issues = "https://github.com/biosafetylvl5/lexeme-type/issues"
88
+ Documentation = "https://biosafetylvl5.github.io/lexeme-type/"
89
+
90
+ [tool.mypy]
91
+ python_version = "3.12"
92
+ warn_return_any = true
93
+ warn_unused_configs = true
94
+ disallow_untyped_defs = true
95
+ disallow_incomplete_defs = true
96
+ check_untyped_defs = true
97
+ disallow_untyped_decorators = true
98
+ no_implicit_optional = true
99
+ strict_optional = true
100
+ warn_redundant_casts = true
101
+ warn_unused_ignores = true
102
+ warn_no_return = true
103
+ warn_unreachable = true
104
+
105
+ [[tool.mypy.overrides]]
106
+ module = "tests.*"
107
+ disallow_untyped_defs = false
108
+ follow_untyped_imports = true
109
+
110
+ [tool.pytest.ini_options]
111
+ pythonpath = ["."]
112
+ testpaths = ["tests"]
113
+ python_files = "test_*.py"
114
+ norecursedirs = ["xarray_utils"]
115
+ addopts = "-v -rf --ff --cov=src --cov-report=term-missing --cov-report=xml -m \"not limited_test_dataset_availability\""
116
+ markers = [
117
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
118
+ "integration: CONCURRENT - marks tests as integration (select with '-m \"integration\"')",
119
+ "spans_multiple_packages: CONCURRENT - marks integration tests that test functionality that could fail due to errors in multiple repositories. e.g. listing or validating all plugins, etc. Allows calling tests to ensure the same error will not cause tests in multiple repos to fail.",
120
+ "limited_test_dataset_availability: CONCURRENT - marks integration tests that may have limited test dataset availability so may not be available to all users.",
121
+ "base: OPTIONAL INDEPENDENT - integration tests that complete very quickly and require only a minimal installation for testing basic functionality within the current repository",
122
+ "full: REQUIRED INDEPENDENT - integration tests that require a 'full' installation for testing all functionality within the current repository. 'full' marker includes all tests for the current repo except for tests included in the optional 'base' marker.",
123
+ ]
124
+
125
+ [tool.coverage.run]
126
+ source = ["src"]
127
+ omit = ["tests/*"]
128
+
129
+ [tool.coverage.report]
130
+ exclude_lines = [
131
+ "pragma: no cover",
132
+ "def __repr__",
133
+ "raise NotImplementedError",
134
+ "if __name__ == .__main__.:",
135
+ "pass",
136
+ "raise ImportError",
137
+ ]
138
+
139
+ [tool.bandit]
140
+ exclude_dirs = ["tests", "docs"]
141
+ skips = ["B101"] # Skip assert warnings
142
+
143
+ [tool.ruff]
144
+ # Target Python version
145
+ target-version = "py312"
146
+ line-length = 88
147
+ exclude = [
148
+ "tests",
149
+ "src/legacy",
150
+ ]
151
+
152
+ [tool.ruff.lint]
153
+ # Enable rules
154
+ select = [
155
+ "E", "F", "W", # pycodestyle and Pyflakes
156
+ "I", # isort
157
+ "N", # PEP8 naming
158
+ "D", # pydocstyle
159
+ "UP", # pyupgrade
160
+ "B", # flake8-bugbear
161
+ "C4", # flake8-comprehensions
162
+ "SIM", # flake8-simplify
163
+ "ARG", # flake8-unused-arguments
164
+ "ERA", # eradicate
165
+ "PL", # Pylint
166
+ "RUF", # Ruff-specific rules
167
+ "TCH", # flake8-type-checking
168
+ "S", # flake8-bandit (security)
169
+ "A", # flake8-builtins
170
+ "COM", # flake8-commas
171
+ "PT", # flake8-pytest-style
172
+ "PTH", # flake8-use-pathlib
173
+ "TRY", # tryceratops (exception handling)
174
+ ]
175
+
176
+ ignore = [
177
+ "D203", # one-blank-line-before-class (conflicts with D211)
178
+ "D212", # multi-line-summary-first-line (conflicts with D213)
179
+ "E731", # allow variable assigned lambdas (PLEASE!! I need my lambdas!)
180
+ ]
181
+
182
+ # Allow fix for all enabled rules (when `--fix`) is provided.
183
+ fixable = ["ALL"]
184
+ unfixable = []
185
+
186
+ # Allow unused variables when underscore-prefixed.
187
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
188
+
189
+ [tool.ruff.lint.pydocstyle]
190
+ convention = "numpy"
191
+
192
+ [tool.ruff.lint.mccabe]
193
+ max-complexity = 5
194
+
195
+ [tool.ruff.lint.flake8-tidy-imports]
196
+ ban-relative-imports = "all"
197
+
198
+ [tool.ruff.lint.per-file-ignores]
199
+ # Allow assert statements in tests
200
+ "tests/**/*" = ["S101"]
201
+ # Ignore import violations in __init__.py files
202
+ "__init__.py" = ["F401"]
203
+
204
+ [tool.ruff.format]
205
+ # Like Black, use double quotes for strings.
206
+ quote-style = "double"
207
+ # Like Black, indent with spaces, rather than tabs.
208
+ indent-style = "space"
209
+ # Like Black, respect magic trailing commas.
210
+ skip-magic-trailing-comma = false
211
+ # Unlike Black, force unix style line endings ("\n")
212
+ line-ending = "lf"
213
+
214
+ [build-system]
215
+ requires = ["poetry-core>=1.0.0"]
216
+ build-backend = "poetry.core.masonry.api"
217
+
218
+ [tool.commitizen]
219
+ name = "cz_conventional_commits"
220
+ tag_format = "v$version"
221
+ version_provider = "poetry"
@@ -0,0 +1,16 @@
1
+ import logging
2
+
3
+ try:
4
+ import importlib.metadata
5
+
6
+ __version__ = importlib.metadata.version(__package__ or __name__)
7
+ except ModuleNotFoundError:
8
+ try:
9
+ import importlib_metadata
10
+
11
+ __version__ = importlib_metadata.version(__package__ or __name__)
12
+ except ModuleNotFoundError:
13
+ logging.debug(
14
+ "Could not set __version__ because importlib.metadata is not available."
15
+ + "If running python 3.7, installing importlib-metadata will fix this issue"
16
+ )
@@ -0,0 +1,261 @@
1
+ # # # This source code is subject to the license referenced at
2
+ # # # https://github.com/NRLMMD-GEOIPS.
3
+
4
+ """Lightweight helper for treating singular and plural spellings as equivalent.
5
+
6
+ A drop‑in str subclass that normalizes English nouns so that their
7
+ singular and plural forms compare as equal, hash to the same key, and work
8
+ inside Pydantic v2 models. Ignores capitalization.
9
+
10
+ Examples
11
+ --------
12
+ >>> from partial_lexeme import Lexeme
13
+ >>> Lexeme("reader") == "readers" == Lexeme("readers")
14
+ True
15
+ >>> {Lexeme("analyses"): 1} == {"analysis": 1}
16
+ True
17
+
18
+ >>> from pydantic import BaseModel
19
+ >>> class _Plugin(BaseModel):
20
+ >>> kind: Lexeme
21
+ >>> interface: Lexeme
22
+ >>> model = _Plugin(kind="reader", interface="readers")
23
+ >>> assert model.kind == model.interface == "reader"
24
+ """
25
+
26
+ from __future__ import annotations
27
+ from typing import Any, Dict
28
+
29
+ __all__ = ["Lexeme"]
30
+
31
+ # Irregular nouns – plural ↔ singular
32
+ # - Each plural maps to its singular
33
+ # - Singulars map to themselves so that a single lookup works
34
+ _IRREGULAR: Dict[str, str] = {
35
+ "analyses": "analysis",
36
+ "analysis": "analysis",
37
+ "indices": "index",
38
+ "index": "index",
39
+ "children": "child",
40
+ "data": "datum",
41
+ "criteria": "criterion",
42
+ }
43
+
44
+ # Reverse map – singular → plural
45
+ _S_TO_P: Dict[str, str] = {
46
+ singular: plural for plural, singular in _IRREGULAR.items() if plural != singular
47
+ }
48
+
49
+
50
+ def _normalize(word: str) -> str:
51
+ """Convert nouns to their singular form.
52
+
53
+ When provided with a noun, whether singular or plural, returns its singular form.
54
+ This is done using:
55
+
56
+ 1. An irregular lookup table, `_IRREGULAR` for known exceptions.
57
+ 2. Heuristic suffix rules for regular pluralization:
58
+
59
+ - Words ending in "ies" - replace "ies" with "y" if preceded by a consonant
60
+ - Words in double "zzes" - removes "zes"
61
+ - Words ending in "sses", "xes", or "zes" - remove "es"
62
+ - Words ending in "ses" - remove "s"
63
+ - Words ending in "es" and containing "sh" or "ch" in their singular form -
64
+ remove "es"
65
+ - Words ending in singular "s" and more than three characters long - remove "s"
66
+
67
+ Parameters
68
+ ----------
69
+ word : str
70
+ The noun in its singular or plural form.
71
+
72
+ Returns
73
+ -------
74
+ str
75
+ A plausible singular form of the given word.
76
+
77
+ Notes
78
+ -----
79
+ - It does not handle all English irregulars such as mice -> mouse unless present
80
+ in `_IRREGULAR`.
81
+ - Words that do not match any rule are returned unchanged in lowercase.
82
+ """
83
+ w = word.strip().lower()
84
+ if not w:
85
+ return ""
86
+
87
+ if w in _IRREGULAR:
88
+ return _IRREGULAR[w]
89
+
90
+ # heuristics
91
+
92
+ # parties -> party (replace "ies" with "y" if the preceding letter is a consonant)
93
+ if w.endswith("ies") and len(w) > 3 and w[-4] not in "aeiou":
94
+ return w[:-3] + "y"
95
+
96
+ # quizzes -> quiz (remove "zes")
97
+ if w.endswith("zzes"):
98
+ return w[:-3]
99
+
100
+ # processes -> process (remove "es")
101
+ # boxes -> box, buzzes -> buzz (remove "es")
102
+ if w.endswith(("sses", "xes", "zes")):
103
+ return w[:-2]
104
+
105
+ # processes -> process (remove "s")
106
+ if w.endswith("ses"):
107
+ return w[:-1]
108
+
109
+ # dishes -> dish, churches -> church (remove "es")
110
+ if w.endswith("es") and w[-4:-2] in {"sh", "ch"}:
111
+ return w[:-2]
112
+
113
+ # readers -> reader (remove "s")
114
+ if w.endswith("s") and not w.endswith("ss") and len(w) > 3:
115
+ return w[:-1]
116
+
117
+ return w
118
+
119
+
120
+ def _to_plural(word: str) -> str:
121
+ r"""Return a plausible plural spelling for *word*.
122
+
123
+ In this function’s documentation, consider * a regular Linux wildcard character.
124
+ This function attempts tp pluralize a singular English noun using:
125
+
126
+ 1. An irregular lookup table, `_S_TO_P` for known exceptions.
127
+ 2. Heuristic spelling rules for regular pluralization:
128
+
129
+ - \\*consonant + y --> \\*+ies such as party -> parties
130
+ - \\*(x, ch, sh, ss, zz) --> \\*+es such as box -> boxes, buzz -> buzzes
131
+ - \\*z -> \\*+zes --> such as quiz -> quizzes
132
+ - near-default with min length of two characters -> default+s
133
+
134
+ Parameters
135
+ ----------
136
+ word : str
137
+ The singular form of noun.
138
+
139
+ Returns
140
+ -------
141
+ str
142
+ A plausible plural form of the given word.
143
+
144
+ Raises
145
+ ------
146
+ ValueError
147
+ If the `word` is empty or no pluralization rule can be applied.
148
+
149
+ Notes
150
+ -----
151
+ - This is best‑effort but the lexeme type does not depend on it because equality
152
+ and hashing always normalizes back to singular.
153
+ - It does not handle all English irregulars such as mouse -> mice unless present
154
+ in `_S_TO_P`.
155
+ - Consider ``*`` a regular Linux wildcard character for reference.
156
+
157
+ """
158
+ w = word.strip().lower()
159
+
160
+ # check for irregular words first
161
+ if w in _S_TO_P:
162
+ return _S_TO_P[w]
163
+
164
+ # heuristics
165
+
166
+ # party -> parties (replace "y" with "ies" if preceding by a consonant)
167
+ if w.endswith("y") and w[-2] not in "aeiou":
168
+ return w[:-1] + "ies"
169
+
170
+ # box -> boxes (ends with "x" -> append "es")
171
+ # church -> churches (ends with "ch" -> append "es")
172
+ # dish -> dishes (ends with "sh" -> append "es")
173
+ # process -> processes (ends with "ss" -> append "es")
174
+ # buzz -> buzzes (ends with double "z" -> append "es")
175
+ if w.endswith(("x", "ch", "sh", "ss", "zz")):
176
+ return w + "es"
177
+
178
+ # quiz -> quizzes (ends with single "z" -> append zes)
179
+ if w.endswith("z"):
180
+ return w + "zes"
181
+
182
+ # reader -> readers (near default scenario, append "s")
183
+ if len(w) >= 2:
184
+ return w + "s"
185
+
186
+ raise ValueError(
187
+ f"No pluralization rule for word: {w}. Check if it's a valid noun!!"
188
+ )
189
+
190
+
191
+ class Lexeme(str):
192
+ """A string that treats singular and plural spellings as equal.
193
+
194
+ It behaves exactly like a built‑in ``str`` but overrides equality and
195
+ hashing to use the canonical singular form. This makes singular and
196
+ plural spellings interchangeable as dict keys, set members, CLI options,
197
+ etc.
198
+ """
199
+
200
+ def __new__(cls, value: Any) -> "Lexeme":
201
+ """Create new Lexeme."""
202
+ if isinstance(value, str):
203
+ return super().__new__(cls, value)
204
+ raise TypeError(
205
+ f"{cls.__name__} must be built from str, got {type(value).__name__}"
206
+ )
207
+
208
+ @property
209
+ def singular(self) -> "Lexeme":
210
+ """Return the singular spelling."""
211
+ return _normalize(self)
212
+
213
+ @property
214
+ def plural(self) -> "Lexeme":
215
+ """Return a plural spelling."""
216
+ return _to_plural(self.singular)
217
+
218
+ def _key(self) -> str:
219
+ """Normalize key for comparisons and hashing."""
220
+ return _normalize(self)
221
+
222
+ def __eq__(self, other: object) -> bool:
223
+ """Plural or singular equality check."""
224
+ if isinstance(other, str):
225
+ return self._key() == _normalize(other)
226
+ if isinstance(other, Lexeme):
227
+ return self._key() == other._key()
228
+ return False
229
+
230
+ def __hash__(self) -> int:
231
+ """Hash lexeme."""
232
+ return hash(self._key())
233
+
234
+ def __repr__(self) -> str:
235
+ """Print out un-serialized self."""
236
+ return f"{self.__class__.__name__}({super().__str__()!r})"
237
+
238
+ @classmethod
239
+ def __get_pydantic_core_schema__(cls, _source_type, _handler):
240
+ """Return a core_schema that allows ``Lexeme`` for use as a field in pydantic.
241
+
242
+ Accepts ``str`` or ``Lexeme``; coerces to ``Lexeme``.
243
+ Always serializes output as plain string.
244
+ """
245
+ # lazy import so only imported if using pydantic
246
+ from pydantic_core import core_schema
247
+
248
+ def _to_lexeme(value: object, _info=None):
249
+ if isinstance(value, cls):
250
+ return value
251
+ if isinstance(value, str):
252
+ return cls(value)
253
+ raise TypeError("String or Lexeme required")
254
+
255
+ return core_schema.no_info_after_validator_function(
256
+ _to_lexeme,
257
+ core_schema.str_schema(),
258
+ serialization=core_schema.plain_serializer_function_ser_schema(
259
+ lambda v: str(v)
260
+ ),
261
+ )