From 9f7b21f2d53d351dab148ea011b0a3e3713e4b7c Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Tue, 17 Feb 2026 20:57:46 +0100
Subject: [PATCH 1/6] Use explicit command names for click 8.2 compatibility

Click 8.2 auto-strips _cmd/_command/_group/_grp suffixes from
function-derived command names. Pin names with explicit name= parameters
so tests work across click versions.
---
 tests/test_basic.py                                       | 4 ++--
 tests/test_command_collection.py                          | 4 ++--
 .../test_click_version_ge_8/test_arg_completion_v8.py     | 6 +++---
 .../test_click_version_ge_8/test_option_completion_v8.py  | 2 +-
 .../test_click_version_le_7/test_arg_completion_v7.py     | 2 +-
 .../test_common_tests/test_hidden_cmd_and_args.py         | 8 ++++----
 6 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/tests/test_basic.py b/tests/test_basic.py
index 3f5ec33..9389593 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -13,7 +13,7 @@ c = ClickCompleter(root_command, click.Context(root_command))
 
 
 def test_arg_completion():
-    @root_command.command()
+    @root_command.command(name="arg-cmd")
     @click.argument("handler", type=click.Choice(("foo", "bar")))
     def arg_cmd(handler):
         pass
@@ -22,7 +22,7 @@ def test_arg_completion():
     assert {x.text for x in completions} == {"foo", "bar"}
 
 
-@root_command.command()
+@root_command.command(name="option-cmd")
 @click.option("--handler", "-h", type=click.Choice(("foo", "bar")), help="Demo option")
 def option_cmd(handler):
     pass
diff --git a/tests/test_command_collection.py b/tests/test_command_collection.py
index 48bcb8c..6eafbdd 100644
--- a/tests/test_command_collection.py
+++ b/tests/test_command_collection.py
@@ -9,7 +9,7 @@ def test_command_collection():
     def foo_group():
         pass
 
-    @foo_group.command()
+    @foo_group.command(name="foo-cmd")
     def foo_cmd():
         pass
 
@@ -17,7 +17,7 @@ def test_command_collection():
     def foobar_group():
         pass
 
-    @foobar_group.command()
+    @foobar_group.command(name="foobar-cmd")
     def foobar_cmd():
         pass
 
diff --git a/tests/test_completion/test_click_version_ge_8/test_arg_completion_v8.py b/tests/test_completion/test_click_version_ge_8/test_arg_completion_v8.py
index 27bebe6..e15d106 100644
--- a/tests/test_completion/test_click_version_ge_8/test_arg_completion_v8.py
+++ b/tests/test_completion/test_click_version_ge_8/test_arg_completion_v8.py
@@ -28,12 +28,12 @@ with pytest.importorskip(
                     if name.startswith(incomplete)
                 ]
 
-        @root_command.command()
+        @root_command.command(name="autocompletion-arg-cmd")
         @click.argument("handler", type=MyVar())
         def autocompletion_arg_cmd(handler):
             pass
 
-        completions = list(c.get_completions(Document("autocompletion-cmd ")))
+        completions = list(c.get_completions(Document("autocompletion-arg-cmd ")))
         assert {x.text for x in completions} == {"foo", "bar"}
 
 
@@ -77,7 +77,7 @@ def test_tuple_return_type_shell_complete_func():
             if i[1].startswith(incomplete)
         ]
 
-    @root_command.command()
+    @root_command.command(name="tuple-type-autocompletion-cmd")
     @click.argument("foo", shell_complete=return_type_tuple_shell_complete)
     def tuple_type_autocompletion_cmd(foo):
         pass
diff --git a/tests/test_completion/test_click_version_ge_8/test_option_completion_v8.py b/tests/test_completion/test_click_version_ge_8/test_option_completion_v8.py
index 41169b8..75d12ac 100644
--- a/tests/test_completion/test_click_version_ge_8/test_option_completion_v8.py
+++ b/tests/test_completion/test_click_version_ge_8/test_option_completion_v8.py
@@ -28,7 +28,7 @@ with pytest.importorskip(
                     if name.startswith(incomplete)
                 ]
 
-        @root_command.command()
+        @root_command.command(name="autocompletion-opt-cmd")
         @click.option("--handler", "-h", type=MyVar())
         def autocompletion_opt_cmd(handler):
             pass
diff --git a/tests/test_completion/test_click_version_le_7/test_arg_completion_v7.py b/tests/test_completion/test_click_version_le_7/test_arg_completion_v7.py
index b8c3667..107f934 100644
--- a/tests/test_completion/test_click_version_le_7/test_arg_completion_v7.py
+++ b/tests/test_completion/test_click_version_le_7/test_arg_completion_v7.py
@@ -46,7 +46,7 @@ def test_tuple_return_type_shell_complete_func_click7():
             if i[1].startswith(incomplete)
         ]
 
-    @root_command.command()
+    @root_command.command(name="tuple-type-autocompletion-cmd")
     @click.argument("foo", autocompletion=return_type_tuple_shell_complete)
     def tuple_type_autocompletion_cmd(foo):
         pass
diff --git a/tests/test_completion/test_common_tests/test_hidden_cmd_and_args.py b/tests/test_completion/test_common_tests/test_hidden_cmd_and_args.py
index b0ed79c..7e41090 100644
--- a/tests/test_completion/test_common_tests/test_hidden_cmd_and_args.py
+++ b/tests/test_completion/test_common_tests/test_hidden_cmd_and_args.py
@@ -12,7 +12,7 @@ c = ClickCompleter(root_command, click.Context(root_command))
 
 
 def test_hidden_cmd():
-    @root_command.command(hidden=True)
+    @root_command.command(name="hidden-cmd", hidden=True)
     @click.option("--handler", "-h")
     def hidden_cmd(handler):
         pass
@@ -22,7 +22,7 @@ def test_hidden_cmd():
 
 
 def test_hidden_option():
-    @root_command.command()
+    @root_command.command(name="hidden-option-cmd")
     @click.option("--handler", "-h", hidden=True)
     def hidden_option_cmd(handler):
         pass
@@ -32,7 +32,7 @@ def test_hidden_option():
 
 
 def test_args_of_hidden_command():
-    @root_command.command(hidden=True)
+    @root_command.command(name="args-choices-hidden-cmd", hidden=True)
     @click.argument("handler1", type=click.Choice(("foo", "bar")))
     @click.option("--handler2", type=click.Choice(("foo", "bar")))
     def args_choices_hidden_cmd(handler):
@@ -55,7 +55,7 @@ def test_completion_multilevel_command():
     def root_group():
         pass
 
-    @root_group.group()
+    @root_group.group(name="first-level-command")
     def first_level_command():
         pass
 
-- 
2.53.0


From 3b40adefba19da294bddb535e133a3963123e46f Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Tue, 17 Feb 2026 20:59:04 +0100
Subject: [PATCH 2/6] Fix protected_args access for click 8.2+

Click 8.2 made Context.protected_args a read-only property, renaming
the underlying storage to _protected_args. Add compat helpers that
use the private attribute when available, falling back to the public
one for older click versions.
---
 click_repl/_repl.py | 12 ++++++++----
 click_repl/utils.py | 18 ++++++++++++++++--
 2 files changed, 24 insertions(+), 6 deletions(-)

diff --git a/click_repl/_repl.py b/click_repl/_repl.py
index 5693f52..a915a83 100644
--- a/click_repl/_repl.py
+++ b/click_repl/_repl.py
@@ -8,7 +8,11 @@ from prompt_toolkit.history import InMemoryHistory
 from ._completer import ClickCompleter
 from .exceptions import ClickExit  # type: ignore[attr-defined]
 from .exceptions import CommandLineParserError, ExitReplException, InvalidGroupFormat
-from .utils import _execute_internal_and_sys_cmds
+from .utils import (
+    _execute_internal_and_sys_cmds,
+    _get_protected_args,
+    _set_protected_args,
+)
 
 
 __all__ = ["bootstrap_prompt", "register_repl", "repl"]
@@ -129,12 +133,12 @@ def repl(
 
         try:
             # The group command will dispatch based on args.
-            old_protected_args = group_ctx.protected_args
+            old_protected_args = _get_protected_args(group_ctx)
             try:
-                group_ctx.protected_args = args
+                _set_protected_args(group_ctx, args)
                 group.invoke(group_ctx)
             finally:
-                group_ctx.protected_args = old_protected_args
+                _set_protected_args(group_ctx, old_protected_args)
         except click.ClickException as e:
             e.show()
         except (ClickExit, SystemExit):
diff --git a/click_repl/utils.py b/click_repl/utils.py
index 9aa9800..ca717fb 100644
--- a/click_repl/utils.py
+++ b/click_repl/utils.py
@@ -29,6 +29,20 @@ else:
     from collections import Iterable, Mapping
 
 
+def _get_protected_args(ctx: click.Context) -> list[str]:
+    # In click >= 8.2, protected_args is a read-only property over _protected_args.
+    if hasattr(ctx, "_protected_args"):
+        return ctx._protected_args
+    return ctx.protected_args
+
+
+def _set_protected_args(ctx: click.Context, args: list[str]) -> None:
+    if hasattr(ctx, "_protected_args"):
+        ctx._protected_args = args
+    else:
+        ctx.protected_args = args
+
+
 def _resolve_context(args, ctx=None):
     """Produce the context hierarchy starting with the command and
     traversing the complete arguments. This only follows the commands,
@@ -49,7 +63,7 @@ def _resolve_context(args, ctx=None):
                     return ctx
 
                 ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True)
-                args = ctx.protected_args + ctx.args
+                args = _get_protected_args(ctx) + ctx.args
             else:
                 while args:
                     name, cmd, args = command.resolve_command(ctx, args)
@@ -68,7 +82,7 @@ def _resolve_context(args, ctx=None):
                     args = sub_ctx.args
 
                 ctx = sub_ctx
-                args = [*sub_ctx.protected_args, *sub_ctx.args]
+                args = [*_get_protected_args(sub_ctx), *sub_ctx.args]
         else:
             break
 
-- 
2.53.0


From 294e2b02e3428e5b6d30aa02e51e15c7d16935d3 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Tue, 17 Feb 2026 21:00:49 +0100
Subject: [PATCH 3/6] Update click's version pin

---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index dcc7986..3c00ce5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -25,7 +25,7 @@ packages=
   click_repl
 
 install_requires =
-  click>=7.0
+  click>=7.0,<9.0
   prompt_toolkit>=3.0.36
 
 python_requires = >=3.6
-- 
2.53.0


From fa47398213b674dcd731b9c3333141407567021d Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Wed, 18 Feb 2026 19:21:12 +0100
Subject: [PATCH 4/6] Update python_requires and classifiers

Drop Python 3.6/3.7/3.8, add 3.12/3.13/3.14 to match the
test matrix.
---
 setup.cfg | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/setup.cfg b/setup.cfg
index 3c00ce5..7788047 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -13,12 +13,12 @@ license = MIT
 classifiers =
     Programming Language :: Python :: 3
     Programming Language :: Python :: 3 :: Only
-    Programming Language :: Python :: 3.6
-    Programming Language :: Python :: 3.7
-    Programming Language :: Python :: 3.8
     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
+    Programming Language :: Python :: 3.14
 
 [options]
 packages=
@@ -28,7 +28,7 @@ install_requires =
   click>=7.0,<9.0
   prompt_toolkit>=3.0.36
 
-python_requires = >=3.6
+python_requires = >=3.9
 zip_safe = no
 
 [options.extras_require]
-- 
2.53.0


From 5054a8770f703b56c60a18d0b08ecb1532139b5d Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Wed, 18 Feb 2026 19:16:19 +0100
Subject: [PATCH 5/6] ci: Add click 8.2/8.3 test envs, Python 3.13/3.14

Removed the click-version matrix dimension in GH Actions as it had
no effect on tox's isolated virtualenvs.

Add dedicated tox envs that pin click 8.2.1 and 8.3.1, run on all
compatible Python versions (3.10+).

Move mypy and flake8 to Python 3.14.
---
 .github/workflows/tests.yml |  5 ++---
 tox.ini                     | 32 ++++++++++++++++++++++++--------
 2 files changed, 26 insertions(+), 11 deletions(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 3b08cfe..42ecb61 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -10,8 +10,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-latest, windows-latest]
-        python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12-dev', 'pypy-3.8', 'pypy-3.9']
-        click-version: ['7.1.2', '8.1.2']
+        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy-3.11']
 
     steps:
     - uses: actions/checkout@v3
@@ -22,6 +21,6 @@ jobs:
     - name: Install dependencies
       run: |
         python -m pip install --upgrade pip
-        pip install tox tox-gh-actions click==${{ matrix.click-version }}
+        pip install tox tox-gh-actions
     - name: Test with tox
       run: tox
diff --git a/tox.ini b/tox.ini
index 92b95cc..c5aa6de 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,20 +1,22 @@
 [tox]
 envlist =
-    py{py,37,38,39,310,311,312}
+    py{py,39,310,311,312,313,314}
     flake8
     click7
+    click82
+    click83
 
 minversion = 3.7.14
 isolated_build = true
 
 [gh-actions]
 python =
-    3.7: py37, click7, flake8
-    3.8: py38
-    3.9: py39
-    3.10: py310
-    3.11: py311
-    3.12: py312
+    3.9: py39, click7
+    3.10: py310, click82, click83
+    3.11: py311, click82, click83
+    3.12: py312, click82, click83
+    3.13: py313, click82, click83
+    3.14: py314, mypy, flake8, click82, click83
 
 [testenv]
 setenv =
@@ -29,7 +31,7 @@ commands =
     pytest --basetemp={envtmpdir}
 
 [testenv:flake8]
-basepython = python3.7
+basepython = python3.14
 deps = flake8
 commands = flake8 click_repl tests
 
@@ -40,3 +42,17 @@ deps =
     pytest
     pytest-cov
 commands = pytest --basetemp={envtmpdir}
+
+[testenv:click82]
+deps =
+    click==8.2.1
+    pytest
+    pytest-cov
+commands = pytest --basetemp={envtmpdir}
+
+[testenv:click83]
+deps =
+    click==8.3.1
+    pytest
+    pytest-cov
+commands = pytest --basetemp={envtmpdir}
-- 
2.53.0


From e2073bc5efda3fce380d3bf95ab468aa9680a653 Mon Sep 17 00:00:00 2001
From: Charalampos Stratakis <cstratak@redhat.com>
Date: Wed, 18 Feb 2026 19:23:54 +0100
Subject: [PATCH 6/6] Use importlib.metadata for click version check

click.__version__ is deprecated and will be removed in click 9.1.
Use importlib.metadata.version() to determine the major version for
the skippable tests.
---
 .../test_click_version_ge_8/test_arg_completion_v8.py     | 6 +++++-
 .../test_click_version_le_7/test_arg_completion_v7.py     | 8 ++++++--
 .../test_click_version_le_7/test_option_completion_v7.py  | 6 +++++-
 3 files changed, 16 insertions(+), 4 deletions(-)

diff --git a/tests/test_completion/test_click_version_ge_8/test_arg_completion_v8.py b/tests/test_completion/test_click_version_ge_8/test_arg_completion_v8.py
index e15d106..3c1e875 100644
--- a/tests/test_completion/test_click_version_ge_8/test_arg_completion_v8.py
+++ b/tests/test_completion/test_click_version_ge_8/test_arg_completion_v8.py
@@ -1,8 +1,12 @@
+from importlib.metadata import version as _get_version
+
 import click
 from click_repl import ClickCompleter
 from prompt_toolkit.document import Document
 import pytest
 
+_click_major = int(_get_version("click").split(".")[0])
+
 
 @click.group()
 def root_command():
@@ -61,7 +65,7 @@ with pytest.importorskip(
 
 
 @pytest.mark.skipif(
-    click.__version__[0] < "8",
+    _click_major < 8,
     reason="click-v8 built-in shell complete is not available, so skipped",
 )
 def test_tuple_return_type_shell_complete_func():
diff --git a/tests/test_completion/test_click_version_le_7/test_arg_completion_v7.py b/tests/test_completion/test_click_version_le_7/test_arg_completion_v7.py
index 107f934..00f6ec6 100644
--- a/tests/test_completion/test_click_version_le_7/test_arg_completion_v7.py
+++ b/tests/test_completion/test_click_version_le_7/test_arg_completion_v7.py
@@ -1,8 +1,12 @@
+from importlib.metadata import version as _get_version
+
 import click
 from click_repl import ClickCompleter
 from prompt_toolkit.document import Document
 import pytest
 
+_click_major = int(_get_version("click").split(".")[0])
+
 
 @click.group()
 def root_command():
@@ -13,7 +17,7 @@ c = ClickCompleter(root_command, click.Context(root_command))
 
 
 @pytest.mark.skipif(
-    click.__version__[0] > "7",
+    _click_major >= 8,
     reason="click-v7 old autocomplete function is not available, so skipped",
 )
 def test_click7_autocomplete_arg():
@@ -30,7 +34,7 @@ def test_click7_autocomplete_arg():
 
 
 @pytest.mark.skipif(
-    click.__version__[0] > "7",
+    _click_major >= 8,
     reason="click-v7 old autocomplete function is not available, so skipped",
 )
 def test_tuple_return_type_shell_complete_func_click7():
diff --git a/tests/test_completion/test_click_version_le_7/test_option_completion_v7.py b/tests/test_completion/test_click_version_le_7/test_option_completion_v7.py
index 13ff670..04e89ce 100644
--- a/tests/test_completion/test_click_version_le_7/test_option_completion_v7.py
+++ b/tests/test_completion/test_click_version_le_7/test_option_completion_v7.py
@@ -1,8 +1,12 @@
+from importlib.metadata import version as _get_version
+
 import click
 from click_repl import ClickCompleter
 from prompt_toolkit.document import Document
 import pytest
 
+_click_major = int(_get_version("click").split(".")[0])
+
 
 @click.group()
 def root_command():
@@ -13,7 +17,7 @@ c = ClickCompleter(root_command, click.Context(root_command))
 
 
 @pytest.mark.skipif(
-    click.__version__[0] > "7",
+    _click_major >= 8,
     reason="click-v7 old autocomplete function is not available, so skipped",
 )
 def test_click7_autocomplete_option():
-- 
2.53.0

