From 99c8c065371fde3b62cd4d9fe1b6c6fdd28b95f0 Mon Sep 17 00:00:00 2001 From: Maxim Kolyubyakin Date: Sun, 1 Nov 2026 11:35:40 +0100 Subject: [PATCH 1/4] fix(core): load plugins on Python 3.12 and newer Since Python 3.12, importlib.metadata.entry_points() no longer returns a dict, so entry_points().get(group) raised AttributeError and every ccli command failed, --help included. Select the group instead, which works on Python 3.10 and newer. Also drop the unused distutils import: distutils is gone in Python 3.12 and only resolved through setuptools' shim. A new test reads the plugins that are actually installed, so a change like this one can't go unnoticed again. --- connect/cli/core/utils.py | 11 ++--------- tests/core/test_utils.py | 8 +++++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/connect/cli/core/utils.py b/connect/cli/core/utils.py index 1221a56..7595ab4 100644 --- a/connect/cli/core/utils.py +++ b/connect/cli/core/utils.py @@ -5,7 +5,6 @@ import os from collections import OrderedDict from importlib.metadata import entry_points -from distutils.version import StrictVersion import click import requests @@ -141,12 +140,6 @@ def field_to_check_mark(predicate, false_value=''): def iter_entry_points(group, name=None): - group_entrypoints = entry_points().get(group) - if not group_entrypoints: - return - for ep in group_entrypoints: - if name: - if ep.name == name: - yield ep - else: + for ep in entry_points(group=group): + if not name or ep.name == name: yield ep diff --git a/tests/core/test_utils.py b/tests/core/test_utils.py index 3f33bc7..99c9dbf 100644 --- a/tests/core/test_utils.py +++ b/tests/core/test_utils.py @@ -385,9 +385,15 @@ def test_iter_entry_points(mocker): ep2.name = 'ep2' mocker.patch( 'connect.cli.core.utils.entry_points', - return_value={'ep.group': [ep1, ep2]}, + side_effect=lambda group: {'ep.group': (ep1, ep2)}.get(group, ()), ) assert list(iter_entry_points('ep.group', name='ep1')) == [ep1] assert list(iter_entry_points('ep.group')) == [ep1, ep2] assert list(iter_entry_points('ep.other_group')) == [] + + +def test_iter_entry_points_finds_the_installed_plugins(): + names = {ep.name for ep in iter_entry_points('connect.cli.plugins')} + + assert {'customer', 'product', 'project', 'report'} <= names From 6f5c5c440e2c6336adfdb728145c55e510649fca Mon Sep 17 00:00:00 2001 From: Maxim Kolyubyakin Date: Sun, 1 Nov 2026 11:35:40 +0100 Subject: [PATCH 2/4] fix(core): stop waiting for the version check after 5 seconds ccli --version asks Connect and PyPI for the latest versions without a timeout, so a slow network made it hang. The check is best effort and already ignores request errors, so a timeout now ends it quietly. --- connect/cli/core/constants.py | 2 ++ connect/cli/core/utils.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/connect/cli/core/constants.py b/connect/cli/core/constants.py index cb671e9..b478605 100644 --- a/connect/cli/core/constants.py +++ b/connect/cli/core/constants.py @@ -11,3 +11,5 @@ """ PYPI_JSON_API_URL = 'https://pypi.org/pypi/connect-cli/json' + +VERSION_CHECK_TIMEOUT = 5 # seconds diff --git a/connect/cli/core/utils.py b/connect/cli/core/utils.py index 7595ab4..35b3d16 100644 --- a/connect/cli/core/utils.py +++ b/connect/cli/core/utils.py @@ -11,7 +11,7 @@ from packaging.version import InvalidVersion, Version from connect.cli import get_version -from connect.cli.core.constants import DEFAULT_ENDPOINT, PYPI_JSON_API_URL +from connect.cli.core.constants import DEFAULT_ENDPOINT, PYPI_JSON_API_URL, VERSION_CHECK_TIMEOUT from connect.cli.core.terminal import console @@ -51,7 +51,7 @@ def get_last_version_by_major(tags, major): def get_last_cli_version(): try: - res = requests.get(PYPI_JSON_API_URL) + res = requests.get(PYPI_JSON_API_URL, timeout=VERSION_CHECK_TIMEOUT) if res.status_code == 200: data = res.json() return data['info']['version'] @@ -61,7 +61,7 @@ def get_last_cli_version(): def get_connect_version(): try: - response = requests.get(DEFAULT_ENDPOINT) + response = requests.get(DEFAULT_ENDPOINT, timeout=VERSION_CHECK_TIMEOUT) return response.headers.get('Connect-Version') except requests.RequestException: return @@ -75,7 +75,7 @@ def check_for_updates(*args): current = get_version() last_version = None try: - res = requests.get(PYPI_JSON_API_URL) + res = requests.get(PYPI_JSON_API_URL, timeout=VERSION_CHECK_TIMEOUT) if res.status_code == 200: data = res.json() last_version = get_last_version_by_major( From 5cb5c8a761f711ce20d7824b94811efd9b01413a Mon Sep 17 00:00:00 2001 From: Maxim Kolyubyakin Date: Sun, 1 Nov 2026 11:36:03 +0100 Subject: [PATCH 3/4] build(deps): fix the flake8 plugins that break on Python 3.14 Python 3.14 removed ast.Str, so flake8 could not load two plugins and the extension project tests failed: - flake8-string-format moves to 0.4, which supports Python 3.14. - flake8-future-import goes. Its last release is from 2022, and its checks did nothing here: they are on the ignore list, and code that requires Python 3.10 needs no __future__ imports. No other locked version changes. --- poetry.lock | 36 ++++++++++++------------------------ pyproject.toml | 3 +-- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/poetry.lock b/poetry.lock index 21f6bb8..7757942 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -914,7 +914,7 @@ files = [ ] [package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +pydantic = ">=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0" starlette = ">=0.40.0,<0.47.0" typing-extensions = ">=4.8.0" @@ -1057,7 +1057,7 @@ files = [ ] [package.dependencies] -flake8 = ">=3,<3.2 || >3.2" +flake8 = ">=3,!=3.2" [[package]] name = "flake8-debugger" @@ -1092,21 +1092,6 @@ attrs = "*" eradicate = ">=2.0,<3.0" flake8 = ">5" -[[package]] -name = "flake8-future-import" -version = "0.4.7" -description = "__future__ import checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["test"] -files = [ - {file = "flake8-future-import-0.4.7.tar.gz", hash = "sha256:ed826cca28fd4b55c486d69c4bfb32e9a052efc66714bf22bf471f50ecf8e0de"}, - {file = "flake8_future_import-0.4.7-py2.py3-none-any.whl", hash = "sha256:7ecb1b89f5e026afb0cfdb8642d150bf56efbb6fb86a713ea4568398142f47ae"}, -] - -[package.dependencies] -flake8 = "*" - [[package]] name = "flake8-isort" version = "6.1.2" @@ -1146,19 +1131,22 @@ dev = ["Flit (>=3.4)", "pyTest (>=7)", "pyTest-cov (>=7) ; python_version >= \"3 [[package]] name = "flake8-string-format" -version = "0.3.0" +version = "0.4.0" description = "string format checker, plugin for flake8" optional = false -python-versions = "*" +python-versions = ">=3.8" groups = ["test"] files = [ - {file = "flake8-string-format-0.3.0.tar.gz", hash = "sha256:65f3da786a1461ef77fca3780b314edb2853c377f2e35069723348c8917deaa2"}, - {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, + {file = "flake8_string_format-0.4.0-py3-none-any.whl", hash = "sha256:650f4d1167a393179fbc62074c523111136b3797281a44f5715b69a560ed82b6"}, + {file = "flake8_string_format-0.4.0.tar.gz", hash = "sha256:817431aeed09a9cad28099425b5a2a03d58c10e9460dcf8475d7f923ac10f26a"}, ] [package.dependencies] flake8 = "*" +[package.extras] +test = ["six"] + [[package]] name = "fonttools" version = "4.61.0" @@ -3056,6 +3044,7 @@ description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main", "test"] +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, @@ -3100,7 +3089,6 @@ files = [ {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, ] -markers = {main = "python_version == \"3.10\"", test = "python_full_version <= \"3.11.0a6\""} [[package]] name = "traitlets" @@ -3438,4 +3426,4 @@ test = ["pytest"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.15" -content-hash = "50af1627962db62fece3913ba896c57aeaefe0eef9bd276edfdee407bd419ebc" +content-hash = "dccea6a03c86bd4e5c99d4e010f8818699f691b024f3b3c34557cf2eeee27122" diff --git a/pyproject.toml b/pyproject.toml index bd0760f..1ca4216 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,13 +89,12 @@ flake8-black = "0.*" flake8-bugbear = ">=24" flake8-cognitive-complexity = "^0.1" flake8-commas = "~4" -flake8-future-import = "~0.4" flake8-isort = "^6.0" flake8-broken-line = ">=1.0" flake8-comprehensions = "^3.10" flake8-debugger = "^4.0.0" flake8-eradicate = "^1.3.0" -flake8-string-format = "^0.3.0" +flake8-string-format = "^0.4.0" flake8-pyproject = "^1.2.2" ipython = "^8.10" isort = "5.*" From 13d8431d32ff43cac872ec3589862a151bf505c1 Mon Sep 17 00:00:00 2001 From: Maxim Kolyubyakin Date: Sun, 1 Nov 2026 11:36:03 +0100 Subject: [PATCH 4/4] ci: test on Python 3.10 to 3.14 and on macOS CI tested Python 3.10 on Linux only, so the Python 3.12 failure above went unnoticed while the package said it supports 3.10 to 3.14. Test every supported version on Linux, and the oldest and newest on macOS, where the PDF reports need Pango from Homebrew. The classifiers listed Python 3.8 and 3.9, which the package does not support. Poetry now derives them from the python range. --- .github/workflows/build.yml | 21 +++++++++++++++++---- pyproject.toml | 3 --- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c786b53..dbe168d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,19 +12,29 @@ on: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - python-version: ["3.10"] + os: [ubuntu-latest] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + include: + - os: macos-latest + python-version: "3.10" + - os: macos-latest + python-version: "3.14" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 with: fetch-depth: 0 submodules: true - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} + - name: Install Pango for the PDF reports (macOS) + if: runner.os == 'macOS' + run: brew install pango - name: Install dependencies run: | python -m pip install --upgrade pip @@ -35,6 +45,9 @@ jobs: poetry run flake8 - name: Testing run: | + # WeasyPrint finds Homebrew's libraries only through this path on Apple Silicon. + # It is exported here because macOS drops DYLD_* variables passed to /bin/bash. + if [ "$RUNNER_OS" = "macOS" ]; then export DYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix)/lib"; fi poetry run pytest -v sonar: needs: [test] diff --git a/pyproject.toml b/pyproject.toml index 1ca4216..28ef52e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,9 +23,6 @@ classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Topic :: Utilities" ] keywords = [