Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the formatting of the preamble #1352

Merged
merged 7 commits into from Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 9 additions & 3 deletions cibuildwheel/__main__.py
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
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
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
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
@@ -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
Expand Up @@ -270,6 +270,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 @@ -283,6 +291,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 @@ -356,6 +367,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 @@ -655,3 +672,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.
Comment on lines +679 to +680
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is weird - though I see how it could happen, GH does a lot of optimization for logs. Is there a GH ticket or commend anywhere? I've not generally noticed GH breaking elsewhere with color, but maybe most things like compilers and pytest do per-line coloring too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is not. I should raise an issue somewhere. Not sure where though. Maybe I'll just raise it at actions/runner and see what they say.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""
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