Skip to content

Commit

Permalink
Merge pull request #1352 from pypa/improve-preamble
Browse files Browse the repository at this point in the history
Improve the formatting of the preamble
  • Loading branch information
joerick authored Dec 5, 2022
2 parents ecce3d9 + 9850864 commit 61b6cc8
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 99 deletions.
12 changes: 9 additions & 3 deletions cibuildwheel/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
from cibuildwheel.util import (
CIBW_CACHE_PATH,
BuildSelector,
CIProvider,
Unbuffered,
chdir,
detect_ci_provider,
fix_ansi_codes_for_github_actions,
)


Expand Down Expand Up @@ -229,7 +231,7 @@ def build_in_directory(args: CommandLineArguments) -> None:
)
sys.exit(2)

options = compute_options(platform=platform, command_line_arguments=args)
options = compute_options(platform=platform, command_line_arguments=args, env=os.environ)

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

print("Build options:")
print(f" platform: {platform!r}")
print(textwrap.indent(options.summary(identifiers), " "))
print(f" platform: {platform}")
options_summary = textwrap.indent(options.summary(identifiers), " ")
if detect_ci_provider() == CIProvider.github_actions:
options_summary = fix_ansi_codes_for_github_actions(options_summary)
print(options_summary)

print()
print(f"Cache folder: {CIBW_CACHE_PATH}")

warnings = detect_warnings(options=options, identifiers=identifiers)
Expand Down
3 changes: 3 additions & 0 deletions cibuildwheel/architecture.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ class Architecture(Enum):
def __lt__(self, other: Architecture) -> bool:
return self.value < other.value

def __str__(self) -> str:
return self.name

@staticmethod
def parse_config(config: str, platform: PlatformName) -> set[Architecture]:
result = set()
Expand Down
5 changes: 4 additions & 1 deletion cibuildwheel/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def __init__(self, name: str, value: str):
self.value = value

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

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

def options_summary(self) -> Any:
return self.assignments


def parse_environment(env_string: str) -> ParsedEnvironment:
env_items = split_env_items(env_string)
Expand Down
1 change: 1 addition & 0 deletions cibuildwheel/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ def __init__(self, *, enabled: bool) -> None:
self.bright_red = "\033[91m" if enabled else ""
self.bright_green = "\033[92m" if enabled else ""
self.white = "\033[37m\033[97m" if enabled else ""
self.gray = "\033[38;5;244m" if enabled else ""

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

Expand Down
150 changes: 129 additions & 21 deletions cibuildwheel/options.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from __future__ import annotations

import collections
import configparser
import contextlib
import dataclasses
import difflib
import functools
import os
import shlex
import sys
import textwrap
import traceback
from pathlib import Path
from typing import Any, Callable, Dict, Generator, Iterator, List, Mapping, Union, cast
Expand All @@ -21,6 +22,7 @@

from .architecture import Architecture
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
from .logger import log
from .oci_container import ContainerEngine
from .projectfiles import get_requires_python_str
from .typing import PLATFORMS, Literal, NotRequired, PlatformName, TypedDict
Expand Down Expand Up @@ -52,6 +54,20 @@ class CommandLineArguments:
allow_empty: bool
prerelease_pythons: bool

@staticmethod
def defaults() -> CommandLineArguments:
return CommandLineArguments(
platform="auto",
allow_empty=False,
archs=None,
only=None,
config_file="",
output_dir=Path("wheelhouse"),
package_dir=Path("."),
prerelease_pythons=False,
print_build_identifiers=False,
)


@dataclasses.dataclass(frozen=True)
class GlobalOptions:
Expand Down Expand Up @@ -176,9 +192,11 @@ def __init__(
config_file_path: Path | None = None,
*,
platform: PlatformName,
env: Mapping[str, str],
disallow: dict[str, set[str]] | None = None,
) -> None:
self.platform = platform
self.env = env
self.disallow = disallow or {}

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


class Options:
def __init__(self, platform: PlatformName, command_line_arguments: CommandLineArguments):
def __init__(
self,
platform: PlatformName,
command_line_arguments: CommandLineArguments,
env: Mapping[str, str],
read_config_file: bool = True,
):
self.platform = platform
self.command_line_arguments = command_line_arguments
self.env = env

self.reader = OptionsReader(
self.config_file_path,
self.config_file_path if read_config_file else None,
platform=platform,
env=env,
disallow=DISALLOWED_OPTIONS,
)

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

prerelease_pythons = args.prerelease_pythons or strtobool(
os.environ.get("CIBW_PRERELEASE_PYTHONS", "0")
self.env.get("CIBW_PRERELEASE_PYTHONS", "0")
)

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

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

if dependency_versions == "pinned":
dependency_constraints: None | (
Expand Down Expand Up @@ -594,37 +620,119 @@ def check_for_deprecated_options(self) -> None:
deprecated_selectors("CIBW_SKIP", build_selector.skip_config)
deprecated_selectors("CIBW_TEST_SKIP", test_selector.skip_config)

@cached_property
def defaults(self) -> Options:
return Options(
platform=self.platform,
command_line_arguments=CommandLineArguments.defaults(),
env={},
read_config_file=False,
)

def summary(self, identifiers: list[str]) -> str:
lines = [
f"{option_name}: {option_value!r}"
for option_name, option_value in sorted(dataclasses.asdict(self.globals).items())
]
lines = []
global_option_names = sorted(f.name for f in dataclasses.fields(self.globals))

build_option_defaults = self.build_options(identifier=None)
for option_name in global_option_names:
option_value = getattr(self.globals, option_name)
default_value = getattr(self.defaults.globals, option_name)
lines.append(self.option_summary(option_name, option_value, default_value))

build_options = self.build_options(identifier=None)
build_options_defaults = self.defaults.build_options(identifier=None)
build_options_for_identifier = {
identifier: self.build_options(identifier) for identifier in identifiers
}

for option_name, default_value in sorted(dataclasses.asdict(build_option_defaults).items()):
build_option_names = sorted(f.name for f in dataclasses.fields(build_options))

for option_name in build_option_names:
if option_name == "globals":
continue

lines.append(f"{option_name}: {default_value!r}")
option_value = getattr(build_options, option_name)
default_value = getattr(build_options_defaults, option_name)
overrides = {
i: getattr(build_options_for_identifier[i], option_name) for i in identifiers
}

# if any identifiers have an overridden value, print that too
for identifier in identifiers:
option_value = getattr(build_options_for_identifier[identifier], option_name)
if option_value != default_value:
lines.append(f" {identifier}: {option_value!r}")
lines.append(
self.option_summary(option_name, option_value, default_value, overrides=overrides)
)

return "\n".join(lines)

def option_summary(
self,
option_name: str,
option_value: Any,
default_value: Any,
overrides: dict[str, Any] | None = None,
) -> str:
"""
Return a summary of the option value, including any overrides, with
ANSI 'dim' color if it's the default.
"""
value_str = self.option_summary_value(option_value)
default_value_str = self.option_summary_value(default_value)
overrides_value_strs = {
k: self.option_summary_value(v) for k, v in (overrides or {}).items()
}
# if the override value is the same as the non-overridden value, don't print it
overrides_value_strs = {k: v for k, v in overrides_value_strs.items() if v != value_str}

has_been_set = (value_str != default_value_str) or overrides_value_strs
c = log.colors

result = c.gray if not has_been_set else ""
result += f"{option_name}: "

if overrides_value_strs:
overrides_groups = collections.defaultdict(list)
for k, v in overrides_value_strs.items():
overrides_groups[v].append(k)

result += "\n *: "
result += self.indent_if_multiline(value_str, " ")

for override_value_str, identifiers in overrides_groups.items():
result += f"\n {', '.join(identifiers)}: "
result += self.indent_if_multiline(override_value_str, " ")
else:
result += self.indent_if_multiline(value_str, " ")

result += c.end

return result

def indent_if_multiline(self, value: str, indent: str) -> str:
if "\n" in value:
return "\n" + textwrap.indent(value.strip(), indent)
else:
return value

def option_summary_value(self, option_value: Any) -> str:
if hasattr(option_value, "options_summary"):
option_value = option_value.options_summary()

if isinstance(option_value, list):
return "".join(f"{el}\n" for el in option_value)

if isinstance(option_value, set):
return ", ".join(str(el) for el in sorted(option_value))

if isinstance(option_value, dict):
return "".join(f"{k}: {v}\n" for k, v in option_value.items())

return str(option_value)


def compute_options(
platform: PlatformName,
command_line_arguments: CommandLineArguments,
env: Mapping[str, str],
) -> Options:
options = Options(platform=platform, command_line_arguments=command_line_arguments)
options = Options(platform=platform, command_line_arguments=command_line_arguments, env=env)
options.check_for_deprecated_options()
return options

Expand Down
45 changes: 45 additions & 0 deletions cibuildwheel/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,14 @@ def __call__(self, build_id: str) -> bool:

return should_build and not should_skip

def options_summary(self) -> Any:
return {
"build_config": self.build_config,
"skip_config": self.skip_config,
"requires_python": str(self.requires_python),
"prerelease_pythons": self.prerelease_pythons,
}


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

def options_summary(self) -> Any:
return {"skip_config": self.skip_config}


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

return self.base_file_path == o.base_file_path

def options_summary(self) -> Any:
if self == DependencyConstraints.with_defaults():
return "pinned"
else:
return self.base_file_path.name


class NonPlatformWheelError(Exception):
def __init__(self) -> None:
Expand Down Expand Up @@ -657,3 +674,31 @@ def chdir(new_path: Path | str) -> Generator[None, None, None]:
yield
finally:
os.chdir(cwd)


def fix_ansi_codes_for_github_actions(text: str) -> str:
"""
Github Actions forgets the current ANSI style on every new line. This
function repeats the current ANSI style on every new line.
"""
ansi_code_regex = re.compile(r"(\033\[[0-9;]*m)")
ansi_codes: list[str] = []
output = ""

for line in text.splitlines(keepends=True):
# add the current ANSI codes to the beginning of the line
output += "".join(ansi_codes) + line

# split the line at each ANSI code
parts = ansi_code_regex.split(line)
# if there are any ANSI codes, save them
if len(parts) > 1:
# iterate over the ANSI codes in this line
for code in parts[1::2]:
if code == "\033[0m":
# reset the list of ANSI codes when the clear code is found
ansi_codes = []
else:
ansi_codes.append(code)

return output
Loading

0 comments on commit 61b6cc8

Please sign in to comment.