Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/check-test-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
with:
fetch-depth: 0

Expand All @@ -44,7 +44,7 @@ jobs:
- run: pytest

- name: "Upload coverage to Codecov"
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v7
with:
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
Expand All @@ -59,12 +59,12 @@ jobs:
id-token: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-python@v6
with:
python-version: '3.13'
python-version: '3.14'

- run: pip install build twine
- run: python -m build
Expand Down
1 change: 1 addition & 0 deletions gto/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ def format_hexsha(hexsha):
d["version"],
]
+ [d["stage"][name] for name in stages],
strict=False,
Comment thread
shcheklein marked this conversation as resolved.
Outdated
Comment thread
Copilot marked this conversation as resolved.
Outdated
),
)
for name, d in sorted(models_state.items())
Expand Down
18 changes: 10 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ keywords = ["git", "repo", "repository", "artifact", "registry", "developer-tool
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skshetry are we good to drop Python 3.9 now? should not be impacting DVC I think, right?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against current DVC main — no impact:

  • DVC only uses gto's pure library surface: dvc/repo/artifacts.py imports gto.constants (assert_name_is_valid, fullname_re), gto.exceptions, gto.tag (find, parse_tag), gto.base (sort_versions). None of those modules import click/typer (that's confined to gto/cli.py/gto/utils.py), and none changed in this PR.
  • DVC still supports and tests py3.9 (requires-python = ">=3.9", 3.9 in its matrix) with gto>=1.6.0,<2. Once gto releases with >=3.10, pip on py3.9 simply resolves the last 3.9-compatible gto (1.9.x) — installs keep working; 3.9 users just stop receiving gto updates (3.9 has been EOL since Oct 2025).
  • No pin conflicts: DVC envs already carry click via celery (which requires click>=8.1,<9 — same range as the new pin), and nothing in DVC's tree uses typer.
  • ⚠️ One requirement: the next gto release must be 1.10.0, not 2.0.0 — DVC caps at gto<2, so a major bump would be invisible to DVC until they raise the cap.

Side benefit for DVC docs/users: this PR fixes the gto CLI being broken on fresh 3.10+ installs (unpinned typer resolves to 0.26.x → gto --help crashes).

@skshetry skshetry Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, 3.9 is already EOL'd. I don't plan to drop support in dvc though. But 3.9 users should be able to install older version of GTO.

"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",
]
requires-python = ">=3.9"
requires-python = ">=3.10"
dynamic = ["version"]
dependencies = [
# gto imports click directly, so declare it explicitly
"click>=8,<9",
"entrypoints",
"funcy",
"pydantic>=2",
Expand All @@ -30,7 +32,10 @@ dependencies = [
"scmrepo>=3,<4",
"semver>=2.13.0",
"tabulate>=0.8.10",
"typer>=0.4.1",
# typer>=0.24 vendors click and is incompatible with gto's click-based
# CLI machinery (subclassing click.Command, @click.pass_context, etc.):
# the CLI crashes on startup. Migration tracked separately.
"typer>=0.4.1,<0.24",

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do we use typer for? explain, plan the mitigation strategy

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What typer is used for: the entire CLI (gto/cli.py) is built on it — app = Typer(cls=GtoGroup), every command declared via typer Argument/Option, plus typer.Context/typer.Exit/typer.colors. On top of that, gto customizes help/UX by subclassing typer.core.TyperCommand/TyperGroup mixed with real-click classes: GtoCliMixin(click.Command) implements grouped help sections ("Read the registry" / "Modify artifacts"), command aliases, and "Examples:" epilogs; gto_command() wraps every command with @click.pass_context for unified error handling; tests use typer.testing.CliRunner.

Why the pin: typer 0.24+ replaced its click dependency with a vendored copy (typer._click). Two consequences: (1) fresh installs stopped getting click at all while gto imports it directly — the current red main; (2) real-click classes mixed into vendored-click TyperCommand/TyperGroup MROs are incompatible — with typer 0.26 even gto --help crashes (TypeError: Command.__init__() got an unexpected keyword argument 'rich_markup_mode'). Every fresh pip install gto on 3.10+ has shipped a broken CLI since typer 0.24 started resolving.

Mitigation plan (in preference order):

  1. Migrate to typer-native APIs (preferred): rich_help_panel covers the command grouping, epilog covers the Examples blocks, aliases via registering additional command names; replace the @click.pass_context error-handling wrapper with a ctx: typer.Context param and typer's re-exported exceptions (typer.Exit/Abort/BadParameter); tests drop click.testing.Result for typer.testing's. Then remove both the typer<0.24 and click pins. Scope: essentially one focused PR touching gto/cli.py + tests/conftest.py.
  2. Drop typer for plain click 8.x — the custom machinery is click-level already; typer mainly provides the decorator ergonomics.
  3. Rejected: importing from typer._click (private API, breaks on any typer minor).

Happy to open a tracking issue for option 1.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction after checking typer's release notes: the vendoring landed in typer 0.26.0 (2026-05-26), not 0.24 — 0.24.0 only dropped Python 3.9. Timeline matches our CI: the weekly run started failing June 8, right after 0.26.0's release.

Relaxed the pin accordingly to typer>=0.4.1,<0.26 in 70bf976 (verified typer 0.25.1 + click 8.4.2: CLI, lints, and the full suite pass).

Typer's stated reasons for vendoring, from the 0.26.0 release notes: simplifies work for both teams, lets typer evolve independently and enables planned features, and avoids dependency conflicts between packages pinning different click versions. The trade-off is exactly what bit us: click-specific integrations (subclassing click.Command, @click.pass_context, click.testing) are no longer supported against typer's runtime — hence the migration plan above.

]

[project.urls]
Expand All @@ -48,11 +53,8 @@ tests = [
]
dev = [
"gto[tests]",
"mypy==1.17.1; python_version > '3.9'",
# mypy>=1.11.0 crashes when used with pydantic-settings on Python 3.9,
# see: https://github.com/python/mypy/issues/17535
"mypy<1.11.0; python_version <= '3.9'",
"pylint==3.3.8",
"mypy==2.2.0",
"pylint==4.0.6",
"types-PyYAML",
"types-filelock",
"types-freezegun",
Expand Down
9 changes: 1 addition & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# pylint: disable=redefined-outer-name
import inspect
import os
import sys
from time import sleep
Expand All @@ -19,13 +18,7 @@

class Runner:
def __init__(self):
# Older versions of CliRunner need this
# Can we removed when we drop Py 3.9 support
init_sig = inspect.signature(CliRunner.__init__)
if "mix_stderr" in init_sig.parameters:
self._runner = CliRunner(mix_stderr=False) # pylint: disable=unexpected-keyword-arg
else:
self._runner = CliRunner()
self._runner = CliRunner()

def invoke(self, *args, **kwargs) -> Result:
return self._runner.invoke(app, *args, **kwargs)
Expand Down
1 change: 1 addition & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ def test_check_ref_catch_the_bug(scm: Git):
for assignment, tag in zip(
[assignment1, assignment2, assignment3],
[f"{NAME}#staging#1", f"{NAME}#prod#2", f"{NAME}#dev#3"],
strict=False,
Comment thread
shcheklein marked this conversation as resolved.
Outdated
Comment thread
Copilot marked this conversation as resolved.
Outdated
):
events = gto.api.check_ref(scm, tag)
assert len(events) == 1, events
Expand Down
2 changes: 2 additions & 0 deletions tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,13 +431,15 @@ def test_registry_state_tag_tag(tmp_dir: TmpDir):
for appeared, expected in zip(
iter_over(appeared_state["artifacts"][artifact]["versions"]),
iter_over(expected_state["artifacts"][artifact]["versions"]),
strict=False,
Comment thread
Copilot marked this conversation as resolved.
Outdated
):
check_obj(appeared, expected, exclude["versions"])

for key in ["enrichments", "registrations", "deregistrations"]:
for a, e in zip(
iter_over(appeared[key]),
iter_over(expected[key]),
strict=False,
Comment thread
Copilot marked this conversation as resolved.
Outdated
):
check_obj(a, e, exclude[key])

Expand Down
Loading