Skip to content

Commit 61b6cc8

Browse files
authored
Merge pull request #1352 from pypa/improve-preamble
Improve the formatting of the preamble
2 parents ecce3d9 + 9850864 commit 61b6cc8

11 files changed

+293
-99
lines changed

cibuildwheel/__main__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
from cibuildwheel.util import (
2323
CIBW_CACHE_PATH,
2424
BuildSelector,
25+
CIProvider,
2526
Unbuffered,
2627
chdir,
2728
detect_ci_provider,
29+
fix_ansi_codes_for_github_actions,
2830
)
2931

3032

@@ -229,7 +231,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
229231
)
230232
sys.exit(2)
231233

232-
options = compute_options(platform=platform, command_line_arguments=args)
234+
options = compute_options(platform=platform, command_line_arguments=args, env=os.environ)
233235

234236
package_dir = options.globals.package_dir
235237
package_files = {"setup.py", "setup.cfg", "pyproject.toml"}
@@ -318,9 +320,13 @@ def print_preamble(platform: str, options: Options, identifiers: list[str]) -> N
318320
print(f"cibuildwheel version {cibuildwheel.__version__}\n")
319321

320322
print("Build options:")
321-
print(f" platform: {platform!r}")
322-
print(textwrap.indent(options.summary(identifiers), " "))
323+
print(f" platform: {platform}")
324+
options_summary = textwrap.indent(options.summary(identifiers), " ")
325+
if detect_ci_provider() == CIProvider.github_actions:
326+
options_summary = fix_ansi_codes_for_github_actions(options_summary)
327+
print(options_summary)
323328

329+
print()
324330
print(f"Cache folder: {CIBW_CACHE_PATH}")
325331

326332
warnings = detect_warnings(options=options, identifiers=identifiers)

cibuildwheel/architecture.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ class Architecture(Enum):
4343
def __lt__(self, other: Architecture) -> bool:
4444
return self.value < other.value
4545

46+
def __str__(self) -> str:
47+
return self.name
48+
4649
@staticmethod
4750
def parse_config(config: str, platform: PlatformName) -> set[Architecture]:
4851
result = set()

cibuildwheel/environment.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(self, name: str, value: str):
7070
self.value = value
7171

7272
def __repr__(self) -> str:
73-
return f"{self.name}: {self.value}"
73+
return f"{self.name}={self.value}"
7474

7575
def evaluated_value(self, **_: Any) -> str:
7676
return self.value
@@ -131,6 +131,9 @@ def add(self, name: str, value: str) -> None:
131131
def __repr__(self) -> str:
132132
return f"{self.__class__.__name__}({[repr(a) for a in self.assignments]!r})"
133133

134+
def options_summary(self) -> Any:
135+
return self.assignments
136+
134137

135138
def parse_environment(env_string: str) -> ParsedEnvironment:
136139
env_items = split_env_items(env_string)

cibuildwheel/logger.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ def __init__(self, *, enabled: bool) -> None:
228228
self.bright_red = "\033[91m" if enabled else ""
229229
self.bright_green = "\033[92m" if enabled else ""
230230
self.white = "\033[37m\033[97m" if enabled else ""
231+
self.gray = "\033[38;5;244m" if enabled else ""
231232

232233
self.bg_grey = "\033[48;5;235m" if enabled else ""
233234

cibuildwheel/options.py

Lines changed: 129 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from __future__ import annotations
22

3+
import collections
34
import configparser
45
import contextlib
56
import dataclasses
67
import difflib
78
import functools
8-
import os
99
import shlex
1010
import sys
11+
import textwrap
1112
import traceback
1213
from pathlib import Path
1314
from typing import Any, Callable, Dict, Generator, Iterator, List, Mapping, Union, cast
@@ -21,6 +22,7 @@
2122

2223
from .architecture import Architecture
2324
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
25+
from .logger import log
2426
from .oci_container import ContainerEngine
2527
from .projectfiles import get_requires_python_str
2628
from .typing import PLATFORMS, Literal, NotRequired, PlatformName, TypedDict
@@ -52,6 +54,20 @@ class CommandLineArguments:
5254
allow_empty: bool
5355
prerelease_pythons: bool
5456

57+
@staticmethod
58+
def defaults() -> CommandLineArguments:
59+
return CommandLineArguments(
60+
platform="auto",
61+
allow_empty=False,
62+
archs=None,
63+
only=None,
64+
config_file="",
65+
output_dir=Path("wheelhouse"),
66+
package_dir=Path("."),
67+
prerelease_pythons=False,
68+
print_build_identifiers=False,
69+
)
70+
5571

5672
@dataclasses.dataclass(frozen=True)
5773
class GlobalOptions:
@@ -176,9 +192,11 @@ def __init__(
176192
config_file_path: Path | None = None,
177193
*,
178194
platform: PlatformName,
195+
env: Mapping[str, str],
179196
disallow: dict[str, set[str]] | None = None,
180197
) -> None:
181198
self.platform = platform
199+
self.env = env
182200
self.disallow = disallow or {}
183201

184202
# Open defaults.toml, loading both global and platform sections
@@ -319,8 +337,8 @@ def get(
319337
# get the option from the environment, then the config file, then finally the default.
320338
# platform-specific options are preferred, if they're allowed.
321339
result = _dig_first(
322-
(os.environ if env_plat else {}, plat_envvar), # type: ignore[arg-type]
323-
(os.environ, envvar),
340+
(self.env if env_plat else {}, plat_envvar),
341+
(self.env, envvar),
324342
*[(o.options, name) for o in active_config_overrides],
325343
(self.config_platform_options, name),
326344
(self.config_options, name),
@@ -362,13 +380,21 @@ def _inner_fmt(k: str, v: Any, table: TableFmt) -> Iterator[str]:
362380

363381

364382
class Options:
365-
def __init__(self, platform: PlatformName, command_line_arguments: CommandLineArguments):
383+
def __init__(
384+
self,
385+
platform: PlatformName,
386+
command_line_arguments: CommandLineArguments,
387+
env: Mapping[str, str],
388+
read_config_file: bool = True,
389+
):
366390
self.platform = platform
367391
self.command_line_arguments = command_line_arguments
392+
self.env = env
368393

369394
self.reader = OptionsReader(
370-
self.config_file_path,
395+
self.config_file_path if read_config_file else None,
371396
platform=platform,
397+
env=env,
372398
disallow=DISALLOWED_OPTIONS,
373399
)
374400

@@ -402,13 +428,13 @@ def globals(self) -> GlobalOptions:
402428
test_skip = self.reader.get("test-skip", env_plat=False, sep=" ")
403429

404430
prerelease_pythons = args.prerelease_pythons or strtobool(
405-
os.environ.get("CIBW_PRERELEASE_PYTHONS", "0")
431+
self.env.get("CIBW_PRERELEASE_PYTHONS", "0")
406432
)
407433

408434
# This is not supported in tool.cibuildwheel, as it comes from a standard location.
409435
# Passing this in as an environment variable will override pyproject.toml, setup.cfg, or setup.py
410436
requires_python_str: str | None = (
411-
os.environ.get("CIBW_PROJECT_REQUIRES_PYTHON") or self.package_requires_python_str
437+
self.env.get("CIBW_PROJECT_REQUIRES_PYTHON") or self.package_requires_python_str
412438
)
413439
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)
414440

@@ -497,7 +523,7 @@ def build_options(self, identifier: str | None) -> BuildOptions:
497523
if self.platform == "linux":
498524
for env_var_name in environment_pass:
499525
with contextlib.suppress(KeyError):
500-
environment.add(env_var_name, os.environ[env_var_name])
526+
environment.add(env_var_name, self.env[env_var_name])
501527

502528
if dependency_versions == "pinned":
503529
dependency_constraints: None | (
@@ -594,37 +620,119 @@ def check_for_deprecated_options(self) -> None:
594620
deprecated_selectors("CIBW_SKIP", build_selector.skip_config)
595621
deprecated_selectors("CIBW_TEST_SKIP", test_selector.skip_config)
596622

623+
@cached_property
624+
def defaults(self) -> Options:
625+
return Options(
626+
platform=self.platform,
627+
command_line_arguments=CommandLineArguments.defaults(),
628+
env={},
629+
read_config_file=False,
630+
)
631+
597632
def summary(self, identifiers: list[str]) -> str:
598-
lines = [
599-
f"{option_name}: {option_value!r}"
600-
for option_name, option_value in sorted(dataclasses.asdict(self.globals).items())
601-
]
633+
lines = []
634+
global_option_names = sorted(f.name for f in dataclasses.fields(self.globals))
602635

603-
build_option_defaults = self.build_options(identifier=None)
636+
for option_name in global_option_names:
637+
option_value = getattr(self.globals, option_name)
638+
default_value = getattr(self.defaults.globals, option_name)
639+
lines.append(self.option_summary(option_name, option_value, default_value))
640+
641+
build_options = self.build_options(identifier=None)
642+
build_options_defaults = self.defaults.build_options(identifier=None)
604643
build_options_for_identifier = {
605644
identifier: self.build_options(identifier) for identifier in identifiers
606645
}
607646

608-
for option_name, default_value in sorted(dataclasses.asdict(build_option_defaults).items()):
647+
build_option_names = sorted(f.name for f in dataclasses.fields(build_options))
648+
649+
for option_name in build_option_names:
609650
if option_name == "globals":
610651
continue
611652

612-
lines.append(f"{option_name}: {default_value!r}")
653+
option_value = getattr(build_options, option_name)
654+
default_value = getattr(build_options_defaults, option_name)
655+
overrides = {
656+
i: getattr(build_options_for_identifier[i], option_name) for i in identifiers
657+
}
613658

614-
# if any identifiers have an overridden value, print that too
615-
for identifier in identifiers:
616-
option_value = getattr(build_options_for_identifier[identifier], option_name)
617-
if option_value != default_value:
618-
lines.append(f" {identifier}: {option_value!r}")
659+
lines.append(
660+
self.option_summary(option_name, option_value, default_value, overrides=overrides)
661+
)
619662

620663
return "\n".join(lines)
621664

665+
def option_summary(
666+
self,
667+
option_name: str,
668+
option_value: Any,
669+
default_value: Any,
670+
overrides: dict[str, Any] | None = None,
671+
) -> str:
672+
"""
673+
Return a summary of the option value, including any overrides, with
674+
ANSI 'dim' color if it's the default.
675+
"""
676+
value_str = self.option_summary_value(option_value)
677+
default_value_str = self.option_summary_value(default_value)
678+
overrides_value_strs = {
679+
k: self.option_summary_value(v) for k, v in (overrides or {}).items()
680+
}
681+
# if the override value is the same as the non-overridden value, don't print it
682+
overrides_value_strs = {k: v for k, v in overrides_value_strs.items() if v != value_str}
683+
684+
has_been_set = (value_str != default_value_str) or overrides_value_strs
685+
c = log.colors
686+
687+
result = c.gray if not has_been_set else ""
688+
result += f"{option_name}: "
689+
690+
if overrides_value_strs:
691+
overrides_groups = collections.defaultdict(list)
692+
for k, v in overrides_value_strs.items():
693+
overrides_groups[v].append(k)
694+
695+
result += "\n *: "
696+
result += self.indent_if_multiline(value_str, " ")
697+
698+
for override_value_str, identifiers in overrides_groups.items():
699+
result += f"\n {', '.join(identifiers)}: "
700+
result += self.indent_if_multiline(override_value_str, " ")
701+
else:
702+
result += self.indent_if_multiline(value_str, " ")
703+
704+
result += c.end
705+
706+
return result
707+
708+
def indent_if_multiline(self, value: str, indent: str) -> str:
709+
if "\n" in value:
710+
return "\n" + textwrap.indent(value.strip(), indent)
711+
else:
712+
return value
713+
714+
def option_summary_value(self, option_value: Any) -> str:
715+
if hasattr(option_value, "options_summary"):
716+
option_value = option_value.options_summary()
717+
718+
if isinstance(option_value, list):
719+
return "".join(f"{el}\n" for el in option_value)
720+
721+
if isinstance(option_value, set):
722+
return ", ".join(str(el) for el in sorted(option_value))
723+
724+
if isinstance(option_value, dict):
725+
return "".join(f"{k}: {v}\n" for k, v in option_value.items())
726+
727+
return str(option_value)
728+
622729

623730
def compute_options(
624731
platform: PlatformName,
625732
command_line_arguments: CommandLineArguments,
733+
env: Mapping[str, str],
626734
) -> Options:
627-
options = Options(platform=platform, command_line_arguments=command_line_arguments)
735+
options = Options(platform=platform, command_line_arguments=command_line_arguments, env=env)
628736
options.check_for_deprecated_options()
629737
return options
630738

cibuildwheel/util.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,14 @@ def __call__(self, build_id: str) -> bool:
272272

273273
return should_build and not should_skip
274274

275+
def options_summary(self) -> Any:
276+
return {
277+
"build_config": self.build_config,
278+
"skip_config": self.skip_config,
279+
"requires_python": str(self.requires_python),
280+
"prerelease_pythons": self.prerelease_pythons,
281+
}
282+
275283

276284
@dataclass(frozen=True)
277285
class TestSelector:
@@ -285,6 +293,9 @@ def __call__(self, build_id: str) -> bool:
285293
should_skip = selector_matches(self.skip_config, build_id)
286294
return not should_skip
287295

296+
def options_summary(self) -> Any:
297+
return {"skip_config": self.skip_config}
298+
288299

289300
# Taken from https://stackoverflow.com/a/107717
290301
class Unbuffered:
@@ -358,6 +369,12 @@ def __eq__(self, o: object) -> bool:
358369

359370
return self.base_file_path == o.base_file_path
360371

372+
def options_summary(self) -> Any:
373+
if self == DependencyConstraints.with_defaults():
374+
return "pinned"
375+
else:
376+
return self.base_file_path.name
377+
361378

362379
class NonPlatformWheelError(Exception):
363380
def __init__(self) -> None:
@@ -657,3 +674,31 @@ def chdir(new_path: Path | str) -> Generator[None, None, None]:
657674
yield
658675
finally:
659676
os.chdir(cwd)
677+
678+
679+
def fix_ansi_codes_for_github_actions(text: str) -> str:
680+
"""
681+
Github Actions forgets the current ANSI style on every new line. This
682+
function repeats the current ANSI style on every new line.
683+
"""
684+
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
685+
ansi_codes: list[str] = []
686+
output = ""
687+
688+
for line in text.splitlines(keepends=True):
689+
# add the current ANSI codes to the beginning of the line
690+
output += "".join(ansi_codes) + line
691+
692+
# split the line at each ANSI code
693+
parts = ansi_code_regex.split(line)
694+
# if there are any ANSI codes, save them
695+
if len(parts) > 1:
696+
# iterate over the ANSI codes in this line
697+
for code in parts[1::2]:
698+
if code == "\033[0m":
699+
# reset the list of ANSI codes when the clear code is found
700+
ansi_codes = []
701+
else:
702+
ansi_codes.append(code)
703+
704+
return output

0 commit comments

Comments
 (0)