Skip to content

Commit

Permalink
Refactor to make code a bit more linear and immutable
Browse files Browse the repository at this point in the history
  • Loading branch information
joerick authored and henryiii committed Jun 21, 2021
1 parent fe2c3d7 commit 21e19b6
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 93 deletions.
7 changes: 3 additions & 4 deletions cibuildwheel/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,9 @@ def main() -> None:
parser.add_argument(
"--config-file",
help="""
TOML config file for cibuildwheel; usually pyproject.toml, but
can be overridden with this option. Use {package} for the package
directory.""",
default="{package}/pyproject.toml",
TOML config file for cibuildwheel. Defaults to pyproject.toml, but
can be overridden with this option.
""",
)

parser.add_argument(
Expand Down
171 changes: 82 additions & 89 deletions cibuildwheel/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
import toml

from .typing import PLATFORMS, TypedDict

DIR = Path(__file__).parent.resolve()

from .util import resources_dir

Setting = Union[Dict[str, str], List[str], str]

Expand All @@ -34,99 +32,95 @@ def _dig_first(*pairs: Tuple[Mapping[str, Any], str]) -> Setting:

class ConfigOptions:
"""
Gets options from the environment, optionally scoped by the platform.
Gets options from the environment, config or defaults, optionally scoped
by the platform.
Example:
ConfigOptions(package_dir, 'platform='macos')
options('cool-color')
This will return the value of CIBW_COOL_COLOR_MACOS if it exists, otherwise the value of
CIBW_COOL_COLOR, otherwise 'tool.cibuildwheel.cool-color' from pyproject.toml or from
cibuildwheel/resources/defaults.toml. An error is thrown if there are any unexpected
keys or sections in tool.cibuildwheel.
>>> options = ConfigOptions(package_dir, platform='macos')
>>> options('cool-color')
This will return the value of CIBW_COOL_COLOR_MACOS if it exists,
otherwise the value of CIBW_COOL_COLOR, otherwise
'tool.cibuildwheel.macos.cool-color' or 'tool.cibuildwheel.cool-color'
from pyproject.toml, or from cibuildwheel/resources/defaults.toml. An
error is thrown if there are any unexpected keys or sections in
tool.cibuildwheel.
"""

def __init__(
self,
project_path: Path,
config_file: str = "{package}/pyproject.toml",
package_path: Path,
config_file: Optional[str] = None,
*,
platform: str,
disallow: Optional[Dict[str, Set[str]]] = None,
) -> None:
self.platform = platform
self.config: Dict[str, Any] = {}
self.disallow = disallow or {}

# Open defaults.toml and load tool.cibuildwheel.global, then update with tool.cibuildwheel.<platform>
self._load_file(DIR.joinpath("resources", "defaults.toml"), update=False)
# Open defaults.toml, loading both global and platform sections
defaults_path = resources_dir / "defaults.toml"
self.default_options, self.default_platform_options = self._load_file(defaults_path)

# Open pyproject.toml or user specified file
config_toml = Path(config_file.format(package=project_path))
if config_toml != project_path / "pyproject.toml" and not config_toml.exists():
raise FileNotFoundError(f"{config_toml} required.")
self._load_file(config_toml, update=True)
# load the project config file
config_options: Dict[str, Any] = {}
config_platform_options: Dict[str, Any] = {}

def _update(
self,
old_dict: Dict[str, Any],
new_dict: Dict[str, Any],
*,
update: bool,
_platform: bool = False,
) -> None:
if config_file is not None:
config_path = Path(config_file.format(package=package_path))
config_options, config_platform_options = self._load_file(config_path)
else:
# load pyproject.toml, if it's available
pyproject_toml_path = package_path / "pyproject.toml"
if pyproject_toml_path.exists():
config_options, config_platform_options = self._load_file(pyproject_toml_path)

# validate project config
for option_name in config_options:
if not self._is_valid_global_option(option_name):
raise ConfigOptionError(f'Option "{option_name}" not supported in a config file')

for option_name in config_platform_options:
if not self._is_valid_platform_option(option_name):
raise ConfigOptionError(
f'Option "{option_name}" not supported in the "{self.platform}" section'
)

self.config_options = config_options
self.config_platform_options = config_platform_options

def _is_valid_global_option(self, name: str) -> bool:
"""
Updates a dict with a new dict - optionally checking to see if the key
is unexpected based on the current global options - call this with
check=False when loading the defaults.toml file, and all future files
will be forced to have no new keys.
Returns True if an option with this name is allowed in the
[tool.cibuildwheel] section of a config file.
"""
allowed_option_names = self.default_options.keys() | PLATFORMS

# _platform will be True if this is being called on tool.cibuildwheel.<platform>
# for the new_dict (old_dict does not have platforms in it)
if _platform:
normal_keys = set(new_dict)
bad_keys = self.disallow.get(self.platform, set()) & normal_keys
if bad_keys:
msg = f"Options {bad_keys} not allowed in {self.platform}"
raise ConfigOptionError(msg)
else:
normal_keys = set(new_dict) - PLATFORMS
return name in allowed_option_names

for key in normal_keys:
# Check to see if key is already present
if update:
# TODO: Also check nested items
if key not in self.config:
msg = f"Key not supported, problem in config file: {key}"
raise ConfigOptionError(msg)
def _is_valid_platform_option(self, name: str) -> bool:
"""
Returns True if an option with this name is allowed in the
[tool.cibuildwheel.<current-platform>] section of a config file.
"""
disallowed_platform_options = self.disallow.get(self.platform, set())
if name in disallowed_platform_options:
return False

old_dict[key] = new_dict[key]
allowed_option_names = self.default_options.keys() | self.default_platform_options.keys()

# Allow new_dict[<platform>][key] to override old_dict[key]
if not _platform and self.platform in new_dict:
self._update(old_dict, new_dict[self.platform], update=update, _platform=True)
return name in allowed_option_names

def _load_file(self, filename: Path, *, update: bool) -> None:
def _load_file(self, filename: Path) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""
Load a toml file, global and current platform. Raise an error if any
unexpected sections are present in tool.cibuildwheel if updating, and
raise if any are missing if not.
Load a toml file, returns global and platform as separate dicts.
"""
# Only load if present.
try:
config = toml.load(filename)
except FileNotFoundError:
assert update, "Missing default.toml, this should not happen!"
return
config = toml.load(filename)

# If these sections are not present, go on.
tool_cibuildwheel = config.get("tool", {}).get("cibuildwheel")
if not tool_cibuildwheel:
assert update, "Malformed internal default.toml, this should not happen!"
return
global_options = config.get("tool", {}).get("cibuildwheel", {})
platform_options = global_options.get(self.platform, {})

self._update(self.config, tool_cibuildwheel, update=update)
return global_options, platform_options

def __call__(
self,
Expand All @@ -137,32 +131,31 @@ def __call__(
table: Optional[TableFmt] = None,
) -> str:
"""
Get and return envvar for name or the override or the default. If
env_plat is False, then don't accept platform versions of the
environment variable. If this is an array it will be merged with "sep"
before returning. If it is a table, it will be formatted with
"table['item']" using {k} and {v} and merged with "table['sep']".
Get and return the value for the named option from environment,
configuration file, or the default. If env_plat is False, then don't
accept platform versions of the environment variable. If this is an
array it will be merged with "sep" before returning. If it is a table,
it will be formatted with "table['item']" using {k} and {v} and merged
with "table['sep']".
"""

if name not in self.config:
if name not in self.default_options and name not in self.default_platform_options:
raise ConfigOptionError(f"{name} must be in cibuildwheel/resources/defaults.toml file")

# Environment variable form
envvar = f"CIBW_{name.upper().replace('-', '_').replace('.', '_')}"
envvar = f"CIBW_{name.upper().replace('-', '_')}"
plat_envvar = f"{envvar}_{self.platform.upper()}"

# Let environment variable override setting in config
if env_plat:
result = _dig_first(
(os.environ, plat_envvar),
(os.environ, envvar),
(self.config, name),
)
else:
result = _dig_first(
(os.environ, envvar),
(self.config, name),
)
# 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
(os.environ, envvar),
(self.config_platform_options, name),
(self.config_options, name),
(self.default_platform_options, name),
(self.default_options, name),
)

if isinstance(result, dict):
if table is None:
Expand Down

0 comments on commit 21e19b6

Please sign in to comment.