conandeps: add progress output, silence Conan's SyntaxWarnings, allow Python 3.12+

- Progress lines on stderr for each of the three graph resolutions (with
  package/missing counts and timing) so a slow remote no longer looks like
  a hang; -q suppresses them. stdout remains the clean report.
- --verbose now streams Conan's output live via a tee instead of dumping it
  at the end.
- Filter SyntaxWarning/DeprecationWarning before importing conans: Conan 1.x
  modules trip the stricter escape-sequence checks of recent Pythons and
  printed a screenful of noise on 3.14.
- Drop the <3.12 requires-python ceiling. Conan 1.66 falls back to importlib
  where imp is missing; verified working on 3.14. Fix the Containerfile and
  docs that claimed otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYNdaGDrpaqDyT8QtrTQbM
This commit is contained in:
Ole-Morten Duesund 2026-08-25 15:07:07 +02:00
commit ac5a49ef62
6 changed files with 330 additions and 17 deletions

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.8,<3.12"
# requires-python = ">=3.8"
# dependencies = ["conan>=1.66,<2"]
# ///
"""conandeps - dependency, binary-availability and override report for Conan 1.x.
@ -39,10 +39,20 @@ import html
import io
import re
import sys
import time
import warnings
from collections import OrderedDict
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
# Conan 1.x predates the stricter escape-sequence checks of recent Pythons and
# emits a screenful of SyntaxWarning/DeprecationWarning from its own modules
# (patch_ng.py, model/ref.py, ...) when they are byte-compiled. They are not
# actionable for the user of this tool, so silence them before Conan is
# imported (imports of ``conans`` are deliberately deferred to _make_api()).
warnings.filterwarnings("ignore", category=SyntaxWarning)
warnings.filterwarnings("ignore", category=DeprecationWarning)
DEFAULT_BUILD_TYPES = ("Release", "Debug", "RelWithDebInfo")
# Conan 1.x: "<pkg>: requirement <old> overridden by <who> to <new> " where
@ -107,7 +117,37 @@ class Report:
# --------------------------------------------------------------------------- #
# Graph collection
# --------------------------------------------------------------------------- #
def _make_api(log: io.StringIO):
class _Tee(io.TextIOBase):
"""Capture Conan's output in memory and optionally echo it live to stderr.
The in-memory copy is parsed for override warnings afterwards; the live
echo (``--verbose``) lets the user watch recipe downloads and remote
queries while a slow graph resolution is running.
"""
def __init__(self, echo: bool):
self.buffer_ = io.StringIO()
self.echo = echo
def write(self, data: str) -> int:
self.buffer_.write(data)
if self.echo:
sys.stderr.write(data)
sys.stderr.flush()
return len(data)
def getvalue(self) -> str:
return self.buffer_.getvalue()
def _progress(quiet: bool, msg: str) -> None:
"""Status line on stderr so stdout stays a clean, parseable report."""
if not quiet:
sys.stderr.write("conandeps: %s\n" % msg)
sys.stderr.flush()
def _make_api(log: _Tee):
"""Create a Conan API that writes everything to ``log`` instead of stdout.
Colour is disabled so the override warnings can be parsed reliably.
@ -206,8 +246,10 @@ def build_report(
build_types: List[str],
update: bool,
verbose: bool = False,
quiet: bool = False,
) -> Report:
log = io.StringIO()
log = _Tee(echo=verbose)
_progress(quiet, "loading Conan API")
api = _make_api(log)
deps: "OrderedDict[str, Dep]" = OrderedDict()
ranges: Dict[str, str] = {}
@ -216,7 +258,14 @@ def build_report(
profile_settings: Dict[str, str] = {}
explicit: set = set()
for bt in build_types:
total = len(build_types)
for i, bt in enumerate(build_types, 1):
_progress(
quiet,
"[%d/%d] resolving graph for build_type=%s on %s ..."
% (i, total, bt, remote or "all remotes"),
)
t0 = time.monotonic()
graph, root_conanfile = api.info(
conanfile,
remote_name=remote,
@ -227,6 +276,12 @@ def build_report(
)
explicit |= _explicit_overrides(graph)
_collect(graph, bt, deps, ranges)
missing = sum(1 for d in deps.values() if d.binaries.get(bt, ("",))[0] == "Missing")
_progress(
quiet,
"[%d/%d] %s: %d packages, %d missing binaries (%.1fs)"
% (i, total, bt, len(graph.nodes) - 1, missing, time.monotonic() - t0),
)
if not root_requires:
root_requires = [str(r.ref) for r in root_conanfile.requires.values()]
profile_settings = {
@ -235,8 +290,7 @@ def build_report(
# The same override is warned once per graph build; parse once, de-duplicated.
overrides = _parse_overrides(log.getvalue(), explicit)
if verbose:
sys.stderr.write(log.getvalue())
_progress(quiet, "%d overrides detected" % len(overrides))
for dep in deps.values(): # keep column order stable
dep.binaries = OrderedDict(
@ -502,7 +556,10 @@ def main(argv: Optional[List[str]] = None) -> int:
help="check the remote for newer recipes/binaries (conan -u)",
)
p.add_argument("--html", metavar="FILE", help="also write an HTML report to FILE")
p.add_argument("-v", "--verbose", action="store_true", help="echo Conan's own output to stderr")
p.add_argument(
"-v", "--verbose", action="store_true", help="stream Conan's own output to stderr live"
)
p.add_argument("-q", "--quiet", action="store_true", help="no progress lines on stderr")
args = p.parse_args(argv)
build_types = [b.strip() for b in args.build_types.split(",") if b.strip()]
@ -516,6 +573,7 @@ def main(argv: Optional[List[str]] = None) -> int:
build_types,
args.update,
args.verbose,
args.quiet,
)
except Exception as exc: # ConanException and friends
sys.stderr.write("conandeps: error: %s\n" % exc)