264 lines
9.3 KiB
Python
264 lines
9.3 KiB
Python
# SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
# Copyright (c) 2024 Phil Thompson <phil@riverbankcomputing.com>
|
|
|
|
|
|
from packaging.markers import Marker
|
|
|
|
from .exceptions import UserException
|
|
from .pyproject import PyProjectOptionException
|
|
|
|
|
|
class Configurable:
|
|
""" A base class for an object that can be configured by a pyproject.toml,
|
|
a build script or (possibly) the user via command line options.
|
|
"""
|
|
|
|
def add_command_line_options(self, parser, tool, all_options, options=None):
|
|
""" Add the object's command line options to an argument parser and
|
|
update a map of Object instances and the configurables that they should
|
|
be applied to.
|
|
"""
|
|
|
|
if options is None:
|
|
options = self.get_options()
|
|
|
|
for option in options:
|
|
# If it has already been set explicitly then the user cannot change
|
|
# it.
|
|
if getattr(self, option.name) is not None:
|
|
continue
|
|
|
|
# If there is no help then the user can never specify it.
|
|
if option.help is None:
|
|
continue
|
|
|
|
# See if the option is applicable to this tool.
|
|
if option.tools and tool not in option.tools:
|
|
continue
|
|
|
|
# See if the option has already been added.
|
|
configurables = all_options.setdefault(option, list())
|
|
configurables.append(self)
|
|
|
|
if len(configurables) != 1:
|
|
continue
|
|
|
|
# Add the option according to its type.
|
|
argument_name = option.user_name
|
|
|
|
if option.inverted:
|
|
argument_name = 'no-' + argument_name
|
|
|
|
argument_name = '--' + argument_name
|
|
|
|
if option.option_type is bool:
|
|
parser.add_argument(argument_name, dest=option.dest,
|
|
action=('store_false' if option.inverted
|
|
else 'store_true'),
|
|
help=option.help)
|
|
elif option.option_type is list:
|
|
# Remove any plural.
|
|
if argument_name.endswith('s'):
|
|
argument_name = argument_name[:-1]
|
|
|
|
parser.add_argument(argument_name, dest=option.dest,
|
|
choices=option.choices, action='append',
|
|
help=option.help, metavar=option.metavar)
|
|
else:
|
|
parser.add_argument(argument_name, dest=option.dest,
|
|
type=option.option_type, choices=option.choices,
|
|
help=option.help, metavar=option.metavar)
|
|
|
|
def apply_nonuser_defaults(self, tool):
|
|
""" Set default values for each non-user configurable option that
|
|
hasn't been set yet.
|
|
"""
|
|
|
|
self._apply_defaults(tool, user=False)
|
|
|
|
def apply_user_defaults(self, tool):
|
|
""" Set default values for each user configurable option that hasn't
|
|
been set yet.
|
|
"""
|
|
|
|
self._apply_defaults(tool, user=True)
|
|
|
|
def configure(self, pyproject, section_name, tool):
|
|
""" Perform the initial configuration of an object. """
|
|
|
|
section = pyproject.get_section(section_name)
|
|
|
|
if section is not None:
|
|
for name, value in section.items():
|
|
# Find the corresponding option.
|
|
for option in self.get_options():
|
|
if option.user_name == name:
|
|
break
|
|
else:
|
|
raise PyProjectOptionException(name,
|
|
"is not a supported option",
|
|
section_name=section_name)
|
|
|
|
# Check the type of the option.
|
|
if not isinstance(value, option.option_type):
|
|
raise PyProjectOptionException(name,
|
|
"should be of type '{0}' and not '{1}'".format(
|
|
option.option_type.__name__,
|
|
type(value).__name__),
|
|
section_name=section_name)
|
|
|
|
# Check the option hasn't already been initialised.
|
|
if getattr(self, option.name) is not None:
|
|
raise PyProjectOptionException(name,
|
|
"has already been set in code and cannot be "
|
|
"changed",
|
|
section_name=section_name)
|
|
|
|
# Evaluate any environment markers if the option supports them.
|
|
if isinstance(value, list):
|
|
new_value = []
|
|
|
|
for v in value:
|
|
v = self._handle_marker(v, name, section_name)
|
|
if v is not None:
|
|
new_value.append(v)
|
|
|
|
value = new_value
|
|
|
|
setattr(self, option.name, value)
|
|
|
|
self.apply_nonuser_defaults(tool)
|
|
|
|
def get_options(self):
|
|
""" Return a list of configurable options. """
|
|
|
|
return list()
|
|
|
|
def initialise_options(self, kwargs):
|
|
""" Initialise the options. """
|
|
|
|
# Set the value for each option from the keyword arguments or undefined
|
|
# if not specified.
|
|
names = []
|
|
|
|
for option in self.get_options():
|
|
name = option.name
|
|
names.append(name)
|
|
|
|
setattr(self, name, kwargs.get(name))
|
|
|
|
# Check that all keyword arguments are valid options.
|
|
for kw in kwargs.keys():
|
|
if kw not in names:
|
|
raise UserException("'{0}' is not a valid option".format(kw))
|
|
|
|
def verify_configuration(self, tool):
|
|
""" Verify that the configuration is complete and consistent. """
|
|
|
|
# This default implementation does nothing.
|
|
|
|
def _apply_defaults(self, tool, user):
|
|
""" Set default values for each user/non-user option that hasn't been
|
|
set yet.
|
|
"""
|
|
|
|
for option in self.get_options():
|
|
if user and option.help is None:
|
|
continue
|
|
|
|
if not user and option.help is not None:
|
|
continue
|
|
|
|
value = getattr(self, option.name)
|
|
if value is None:
|
|
value = option.default
|
|
if value is None:
|
|
# Provide a default default based on the type.
|
|
if option.option_type is list:
|
|
value = []
|
|
elif option.option_type is float:
|
|
value = 0.0
|
|
elif option.option_type is int:
|
|
value = 0
|
|
elif option.option_type is bool:
|
|
value = True if option.inverted else False
|
|
elif option.option_type is str:
|
|
value = ''
|
|
else:
|
|
# Make a copy of the default in case it is mutable.
|
|
value = option.option_type(value)
|
|
|
|
setattr(self, option.name, value)
|
|
|
|
@staticmethod
|
|
def _handle_marker(value, name, section_name):
|
|
""" Handle any environment marker in a value. The value is returned if
|
|
a marker evaluates to True. None is returned if a marker evaluates to
|
|
False.
|
|
"""
|
|
|
|
# Handle the trivial case of there being no marker.
|
|
if ';' not in value:
|
|
return value
|
|
|
|
value, marker = value.split(';', maxsplit=1)
|
|
|
|
try:
|
|
satisfied = Marker(marker).evaluate()
|
|
except:
|
|
raise PyProjectOptionException(name,
|
|
"has an invalid marker '{0}'".format(marker),
|
|
section_name=section_name)
|
|
|
|
return value if satisfied else None
|
|
|
|
|
|
class Option:
|
|
""" Encapsulate a configuration option. This defines and implements an
|
|
attribute of a Configurable object. The value of the attribute can be set
|
|
either by __init__(), the pyproject.toml file and by the user using a
|
|
command line argument (in that order). Once the value is set it cannot be
|
|
changed subsequently. For example, if an attribute is set
|
|
in pyproject.toml then the user will not then be able to modify it from the
|
|
command line. The value can only be changed from the command line if the
|
|
Option object has help text specified.
|
|
"""
|
|
|
|
# The tools that will build a set of bindings.
|
|
BUILD_TOOLS = ('build', 'install', 'wheel')
|
|
|
|
# All the valid tools.
|
|
_ALL_TOOLS = BUILD_TOOLS + ('sdist', )
|
|
|
|
# This is used to make sure each option (even if they are handling the same
|
|
# attribute) has a unique 'dest'.
|
|
option_nr = 0
|
|
|
|
def __init__(self, name, *, option_type=str, choices=None, default=None,
|
|
help=None, metavar=None, inverted=False, tools=None):
|
|
""" Initialise the option. """
|
|
|
|
self.name = name
|
|
self.user_name = name.replace('_', '-')
|
|
self.option_type = option_type
|
|
self.default = default
|
|
self.choices = choices
|
|
self.help = help
|
|
self.metavar = metavar
|
|
self.inverted = inverted
|
|
|
|
if tools is None:
|
|
self.tools = self.BUILD_TOOLS
|
|
else:
|
|
for tool in tools:
|
|
if tool not in self._ALL_TOOLS:
|
|
raise UserException(
|
|
"'{0}' option has an invalid tools '{1}'".format(
|
|
name, tool))
|
|
|
|
self.tools = tools
|
|
|
|
self.dest = 'd' + str(type(self).option_nr)
|
|
type(self).option_nr += 1
|