From f0d08d8b8a382d0316cdcac812240b9b113e85ec Mon Sep 17 00:00:00 2001 From: James Estevez Date: Thu, 14 May 2026 10:57:38 -0700 Subject: [PATCH 1/2] test(utils): regression test for #3852: sarge Capture has flush() sarge==0.1.7.post1 ships without Capture.flush, and Python 3.13+ logging calls flush() during interpreter shutdown after refresh_oauth_token, which surfaces a cosmetic AttributeError. Add a regression test that asserts sarge.Capture exposes a callable, no-op flush() after importing cumulusci.utils. Currently failing: the fix lands in the next commit. Refs SFDO-Tooling/CumulusCI#3852 --- cumulusci/utils/tests/test_sarge_patch.py | 34 +++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 cumulusci/utils/tests/test_sarge_patch.py diff --git a/cumulusci/utils/tests/test_sarge_patch.py b/cumulusci/utils/tests/test_sarge_patch.py new file mode 100644 index 0000000000..5e83e2dc06 --- /dev/null +++ b/cumulusci/utils/tests/test_sarge_patch.py @@ -0,0 +1,34 @@ +"""Regression test for SFDO-Tooling/CumulusCI#3852. + +``sarge==0.1.7.post1`` (the version pinned via ``pyproject.toml``) ships a +``sarge.Capture`` class with no ``flush()`` method. On Python 3.13+ the +interpreter-shutdown logging path calls ``.flush()`` on the captured stream +objects CumulusCI hands to logging handlers, which surfaces a cosmetic +``AttributeError: 'Capture' object has no attribute 'flush'`` after +``refresh_oauth_token`` runs. + +The upstream sarge fix (``def flush(self): pass``) is unreleased, so +``cumulusci.utils`` installs a defensive shim at import time. This test +guards that shim. +""" + +import sarge + +import cumulusci.utils # noqa: F401 -- importing applies the Capture.flush shim + + +def test_sarge_capture_has_flush_after_importing_cumulusci_utils(): + assert hasattr(sarge.Capture, "flush"), ( + "sarge.Capture is missing flush(); CumulusCI must patch it to avoid " + "AttributeError during interpreter-shutdown logging on Python 3.13+ " + "(see SFDO-Tooling/CumulusCI#3852)." + ) + assert callable(sarge.Capture.flush) + + +def test_sarge_capture_instance_flush_is_a_no_op(): + capture = sarge.Capture() + try: + assert capture.flush() is None + finally: + capture.close() From 4cd731783a38f777a07d516f01ccfafb957e2fa8 Mon Sep 17 00:00:00 2001 From: James Estevez Date: Thu, 14 May 2026 10:57:38 -0700 Subject: [PATCH 2/2] fix(utils): #3852: ensure sarge Capture.flush exists on Py3.13+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pyproject.toml pins ``sarge`` with no version constraint, so the installed version is 0.1.7.post1, whose Capture class lacks a flush() method. The upstream fix is unreleased on sarge's master branch. Python 3.13+ interpreter-shutdown logging path calls flush() on captured streams that CumulusCI hands to logging handlers via sarge.Command(stdout=Capture(...)). After refresh_oauth_token (which calls sfdx_info → sfdx() → sarge.Command with Capture) the shutdown logger therefore emits a cosmetic ``AttributeError: 'Capture' object has no attribute 'flush'``. Functionally a no-op, but noisy and confusing. Install an idempotent shim at the cumulusci.utils import site that adds Capture.flush = lambda self: None only if sarge does not already provide flush(). When sarge eventually ships its own flush(), the guard skips the patch automatically. Refs SFDO-Tooling/CumulusCI#3852 --- cumulusci/utils/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cumulusci/utils/__init__.py b/cumulusci/utils/__init__.py index 7d68f2cd35..ae09c7766a 100644 --- a/cumulusci/utils/__init__.py +++ b/cumulusci/utils/__init__.py @@ -16,6 +16,14 @@ import requests import sarge +if not hasattr(sarge.Capture, "flush"): + # sarge==0.1.7.post1 ships Capture without flush(); on Python 3.13+ the + # interpreter-shutdown logging path calls flush() on captured streams and + # surfaces a cosmetic AttributeError after refresh_oauth_token. Upstream + # has the fix on master but no release. Defensive, idempotent shim. + # See SFDO-Tooling/CumulusCI#3852. + sarge.Capture.flush = lambda self: None + from cumulusci.core.exceptions import CumulusCIException from .xml import ( # noqa elementtree_parse_file,