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) 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 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", 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 = (