From 4f007613e569ab244c119b7b5a6bb6f867668fc6 Mon Sep 17 00:00:00 2001 From: Vilhjalmur Thorsteinsson Date: Fri, 31 Jul 2026 17:07:27 +0000 Subject: [PATCH 1/4] Pass only recognized options to the Greynir constructor Greynir (reynir >= 3.7) raises TypeError on unknown keyword arguments, so GreynirCorrect's own options (one_sent, all_errors, ...) can no longer be forwarded to it verbatim. Filter the options against Greynir._KNOWN_OPTIONS when available. Co-Authored-By: Claude Fable 5 --- src/reynir_correct/checker.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/reynir_correct/checker.py b/src/reynir_correct/checker.py index ad463c9..9891f2f 100644 --- a/src/reynir_correct/checker.py +++ b/src/reynir_correct/checker.py @@ -176,6 +176,11 @@ def __init__( pipeline: CorrectionPipeline, **options: Any, ) -> None: + # Greynir (reynir >= 3.7) rejects unknown keyword arguments; + # only pass through the options that it recognizes + known = getattr(self, "_KNOWN_OPTIONS", None) + if known is not None: + options = {k: v for k, v in options.items() if k in known} super().__init__(**options) self.settings = settings self.pipeline = pipeline From 61a8891f3958a4984d5b29fd27047f939f84ddd8 Mon Sep 17 00:00:00 2001 From: Vilhjalmur Thorsteinsson Date: Fri, 31 Jul 2026 17:07:27 +0000 Subject: [PATCH 2/4] Annotate postposed adjectives that do not agree in case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New VillaLoEftirNlMeðAndlagi annotation handler for the corresponding error grammar rule (upcoming in GreynirEngine): a postposed adjective with a case-governed complement that does not agree with its head noun phrase in case, e.g. 'móðurfélag fleiri félaga tengdum rekstri Morgunblaðsins' where 'tengdum' should be 'tengdra'. The suggestion is generated with BinPackage lookup_variants(), keeping number, gender and degree while replacing the case. The new test skips when the installed reynir version lacks the grammar rule. Co-Authored-By: Claude Fable 5 --- src/reynir_correct/errfinder.py | 40 +++++++++++++++++++++++++++++++++ test/test_annotator.py | 24 ++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/reynir_correct/errfinder.py b/src/reynir_correct/errfinder.py index 30a001e..5a730ab 100644 --- a/src/reynir_correct/errfinder.py +++ b/src/reynir_correct/errfinder.py @@ -84,6 +84,14 @@ def fget(self, tree: SimpleTree) -> str: # Case name prefixes CASE_NAMES = {"nf": "nefni", "þf": "þol", "þgf": "þágu", "ef": "eignar"} +# Adjective inflection variants other than case, kept unchanged when +# suggesting a corrected case form. Note that strong/weak declension +# variants ('sb'/'vb'/'esb'/'evb') are deliberately not included, as +# BinPackage's lookup_variants() does not map them onto the FSB/FVB/ +# ESB/EVB inflection marks; BÍN returns strong (FSB) forms first, +# which is the standard declension in this construction anyway. +ADJECTIVE_NON_CASE_VARIANTS = frozenset(("et", "ft", "kk", "kvk", "hk", "mst")) + # Replacements for numeric ordinals, in the various genders and cases ORDINALS = { 1: { @@ -552,6 +560,38 @@ def VillaEinkunn(self, txt: str, variants: str, node: Node) -> AnnotationDict: suggest=correct_pronoun, ) + def VillaLoEftirNlMeðAndlagi(self, txt: str, variants: str, node: Node) -> Optional[AnnotationDict]: + # Lýsingarorð með fallstýrðu andlagi, á eftir nafnlið sem það + # sambeygist ekki í falli, t.d. + # 'móðurfélag fleiri félaga tengdum rekstri' -> 'tengdra' + correct_case = variants.split("_")[0] + # The offending adjective is the first terminal within the error phrase + tnode = self._terminal_nodes[node.start] + wrong_adj = tnode.text + # Preserve the adjective's other inflection features, + # replacing only the case + keep = [v for v in tnode.all_variants if v in ADJECTIVE_NON_CASE_VARIANTS] + suggestion = PatternMatcher.get_wordform( + wrong_adj.lower(), tnode.lemma, tnode.cat, keep + [correct_case] + ) + if not suggestion or suggestion == wrong_adj.lower(): + # Can't find a correct form, or it's identical to the original: + # don't annotate + return None + suggestion = emulate_case(suggestion, template=wrong_adj) + start, end = tnode.span + return AnnotationDict( + text="'{0}' á sennilega að vera '{1}'".format(wrong_adj, suggestion), + detail=( + "Lýsingarorðið '{0}' á að vera í {1}falli, í samræmi við " + "nafnliðinn á undan".format(wrong_adj, CASE_NAMES[correct_case]) + ), + start=start, + end=end, + original=wrong_adj, + suggest=suggestion, + ) + def AðvörunSemOg(self, txt: str, variants: str, node: Node) -> AnnotationDict: # 'sem' er sennilega ofaukið ch1, ch2 = self._terminal_nodes[node.start : node.end] diff --git a/test/test_annotator.py b/test/test_annotator.py index dd0dff8..bd873df 100644 --- a/test/test_annotator.py +++ b/test/test_annotator.py @@ -252,6 +252,30 @@ def test_corrected_meanings(api) -> None: ) +def test_adjective_agreement(api) -> None: + """Check annotation of a postposed adjective with a case-governed + complement that does not agree with its head noun phrase in case: + 'forstöðumenn fyrirtækja tengdum sjávarútvegi' -> 'tengdra'""" + nts = api.gc.parser.grammar.nonterminals + if not any(name.startswith("VillaLoEftirNlMeðAndlagi") for name in nts): + # The installed reynir version does not include the + # VillaLoEftirNlMeðAndlagi error grammar rule + pytest.skip("reynir version lacks the VillaLoEftirNlMeðAndlagi grammar rule") + s = "Ég ræddi við forstöðumenn fyrirtækja tengdum sjávarútvegi." + check_sentence(api, s, [(5, 5, "P_NT_LoEftirNlMeðAndlagi")]) + # The corresponding correct sentence should not be annotated + s = "Ég ræddi við forstöðumenn fyrirtækja tengdra sjávarútvegi." + check_sentence(api, s, []) + # Check the suggested correction + sent = reynir_correct.check_single( + "Ég ræddi við forstöðumenn fyrirtækja tengdum sjávarútvegi." + ) + anns = [a for a in sent.annotations if a.code == "P_NT_LoEftirNlMeðAndlagi"] + assert len(anns) == 1 + assert anns[0].suggest == "tengdra" + assert anns[0].original == "tengdum" + + def test_lhþt_variant(api) -> None: """Check for a regression in the handling of LHÞT variants in BinPackage""" s = ( From 04132765c375511712f0f036a1f4fc62cc185eb9 Mon Sep 17 00:00:00 2001 From: Vilhjalmur Thorsteinsson Date: Fri, 31 Jul 2026 17:37:22 +0000 Subject: [PATCH 3/4] Require Python >= 3.11; pin ruff rule selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop Python 3.9 and 3.10 from the CI matrix and from the project metadata; move the ruff/mypy CI gates from 3.9 to 3.11. Also pin the ruff lint rule selection to the E4/E7/E9/F set, which was ruff's default selection before version 0.15 — CI installs ruff unpinned, and the broader default selection of newer ruff versions broke the lint step with hundreds of stylistic complaints. Co-Authored-By: Claude Fable 5 --- .github/workflows/python-package.yml | 12 ++++++------ pyproject.toml | 9 +++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f9fede4..c7e3ee2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11'] + python-version: ['3.11', '3.12', '3.13', '3.14', 'pypy-3.11'] steps: - uses: actions/checkout@v4 @@ -28,19 +28,19 @@ jobs: python -m pip install uv uv pip install --system --upgrade wheel setuptools pytest tokenizer reynir # No need to test the sentence classifier in every build (also doesn't work with PyPy) - if [ "${{ matrix.python-version }}" == "3.9" ]; then + if [ "${{ matrix.python-version }}" == "3.11" ]; then uv pip install --system -e ".[sentence_classifier]" else uv pip install --system -e ".[dev]" fi - name: Lint with ruff run: | - if [ "${{ matrix.python-version }}" == "3.9" ]; then uv pip install --system ruff; fi - if [ "${{ matrix.python-version }}" == "3.9" ]; then ruff check src/reynir_correct; fi + if [ "${{ matrix.python-version }}" == "3.11" ]; then uv pip install --system ruff; fi + if [ "${{ matrix.python-version }}" == "3.11" ]; then ruff check src/reynir_correct; fi - name: Typecheck with mypy run: | - if [ "${{ matrix.python-version }}" == "3.9" ]; then uv pip install --system mypy; fi - if [ "${{ matrix.python-version }}" == "3.9" ]; then mypy --ignore-missing-imports --python-version=3.9 src/reynir_correct; fi + if [ "${{ matrix.python-version }}" == "3.11" ]; then uv pip install --system mypy; fi + if [ "${{ matrix.python-version }}" == "3.11" ]; then mypy --ignore-missing-imports --python-version=3.11 src/reynir_correct; fi - name: Test with pytest run: | python -m pytest diff --git a/pyproject.toml b/pyproject.toml index 4126968..130adc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,8 +17,6 @@ classifiers = [ "Natural Language :: Icelandic", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -28,7 +26,7 @@ classifiers = [ "Topic :: Utilities", "Topic :: Text Processing :: Linguistic", ] -requires-python = ">=3.9" +requires-python = ">=3.11" dependencies = ["reynir>=3.5.7", "icegrams>=1.1.2", "typing_extensions"] [project.urls] @@ -62,7 +60,10 @@ filterwarnings = [ line-length = 120 [tool.ruff.lint] -#select = ["ALL"] # We use default rules for now +# Pin the rule selection (this was ruff's default selection before +# version 0.15; later versions enable many more rules by default) +select = ["E4", "E7", "E9", "F"] +#select = ["ALL"] # extend-select = ["E501"] # Complain about line length # Ignore specific rules # (we should aim to have these as few as possible) From d5e7db28e988bd50a298f091d8bbfab7af421e21 Mon Sep 17 00:00:00 2001 From: Vilhjalmur Thorsteinsson Date: Fri, 31 Jul 2026 17:40:45 +0000 Subject: [PATCH 4/4] Silence mypy call-overload complaint on transformers pipeline() Newer transformers versions have pipeline() overloads that mypy cannot match for this call; the result is assigned to Any anyway. Co-Authored-By: Claude Fable 5 --- src/reynir_correct/classifier.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reynir_correct/classifier.py b/src/reynir_correct/classifier.py index 7c9de8d..ef69d59 100644 --- a/src/reynir_correct/classifier.py +++ b/src/reynir_correct/classifier.py @@ -66,7 +66,7 @@ def __init__(self) -> None: ) warnings.warn(warningtext) raise ImportError(warningtext) - self.pipe: Any = pipeline( + self.pipe: Any = pipeline( # type: ignore[call-overload] "text2text-generation", model=self._model_name, tokenizer="google/byt5-base",