166 lines
5.5 KiB
Python
166 lines
5.5 KiB
Python
# SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
# Copyright (c) 2024 Phil Thompson <phil@riverbankcomputing.com>
|
|
|
|
|
|
from abc import ABC, abstractmethod
|
|
import importlib
|
|
import importlib.util
|
|
import os
|
|
import sys
|
|
|
|
from .exceptions import set_deprecations_are_errors, UserException
|
|
from .pyproject import PyProject, PyProjectOptionException
|
|
|
|
|
|
class AbstractProject(ABC):
|
|
""" This specifies the API of a project. """
|
|
|
|
@classmethod
|
|
def bootstrap(cls, tool, tool_description='', arguments=None):
|
|
""" Return an AbstractProject instance fully configured for a
|
|
particular command line tool.
|
|
"""
|
|
|
|
# We have to do this very early.
|
|
if '--deprecations-are-errors' in sys.argv:
|
|
set_deprecations_are_errors(True)
|
|
|
|
# Get the contents of the pyproject.toml file.
|
|
pyproject = PyProject()
|
|
|
|
# Get the name of the project factory.
|
|
project_factory_name = None
|
|
|
|
sip_section_name = 'tool.sip'
|
|
value_name = 'project-factory'
|
|
|
|
sip_section = pyproject.get_section(sip_section_name)
|
|
if sip_section is not None:
|
|
f_name = sip_section.get(value_name)
|
|
if f_name is not None:
|
|
if not isinstance(f_name, str):
|
|
raise PyProjectOptionException(value_name,
|
|
"should be a 'str' and not '{0}'".format(
|
|
type(f_name).__name__),
|
|
section_name=sip_section_name)
|
|
|
|
project_factory_name = f_name
|
|
|
|
# See if there is a corresponding callable.
|
|
if project_factory_name is None:
|
|
default_factory_py = 'project.py'
|
|
|
|
if os.path.isfile(default_factory_py):
|
|
project_factory = cls.import_callable(default_factory_py, cls)
|
|
else:
|
|
# The default project factory.
|
|
from .project import Project as project_factory
|
|
else:
|
|
project_factory = cls.import_callable(project_factory_name, cls)
|
|
|
|
project = project_factory()
|
|
|
|
if not isinstance(project, cls):
|
|
raise UserException(
|
|
"The project factory did not return an AbstractProject "
|
|
"object")
|
|
|
|
# We set this as an attribute rather than change the API of the ctor or
|
|
# setup().
|
|
project.arguments = arguments
|
|
|
|
# Complete the configuration of the project.
|
|
project.setup(pyproject, tool, tool_description)
|
|
|
|
return project
|
|
|
|
@abstractmethod
|
|
def build(self):
|
|
""" Build the project in-situ. """
|
|
|
|
@abstractmethod
|
|
def build_sdist(self, sdist_directory):
|
|
""" Build an sdist for the project and return the name of the sdist
|
|
file.
|
|
"""
|
|
|
|
@abstractmethod
|
|
def build_wheel(self, wheel_directory):
|
|
""" Build a wheel for the project and return the name of the wheel
|
|
file.
|
|
"""
|
|
|
|
@staticmethod
|
|
def import_callable(name, base_type):
|
|
""" Import a callable from either a .py file or specified as
|
|
module[:object].
|
|
"""
|
|
|
|
# See if the name refers to a .py file.
|
|
if name.endswith('.py'):
|
|
name = name.replace('/', os.sep)
|
|
|
|
module_name = name[:-3]
|
|
object_name = None
|
|
|
|
# Try and import the .py file.
|
|
spec = importlib.util.spec_from_file_location(module_name, name)
|
|
module = importlib.util.module_from_spec(spec)
|
|
|
|
try:
|
|
spec.loader.exec_module(module)
|
|
except Exception as e:
|
|
raise UserException("Unable to import '{0}'".format(name),
|
|
detail=str(e))
|
|
else:
|
|
# Extract the module and any object name.
|
|
parts = name.split(':')
|
|
if len(parts) > 2:
|
|
raise UserException(
|
|
"The callable '{0}' must be specified as "
|
|
"'module[:name]'".format(name))
|
|
|
|
module_name = parts[0]
|
|
object_name = parts[1] if len(parts) == 2 else None
|
|
|
|
# Try and import the module.
|
|
try:
|
|
module = importlib.import_module(module_name)
|
|
except ImportError as e:
|
|
raise UserException(
|
|
"Unable to import '{0}'".format(module_name),
|
|
detail=str(e))
|
|
|
|
# Get the callable object from the module.
|
|
if object_name is None:
|
|
# Look for a class that is a sub-class of the base type.
|
|
for obj in module.__dict__.values():
|
|
if isinstance(obj, type):
|
|
if issubclass(obj, base_type):
|
|
# Make sure the type is defined in the module and not
|
|
# imported by it.
|
|
if obj.__module__ == module_name:
|
|
break
|
|
else:
|
|
raise UserException(
|
|
"'{0}' does not define a {1} sub-class".format(name,
|
|
base_type.__name__))
|
|
else:
|
|
# We have the name of the callable so just get it.
|
|
obj = getattr(module, object_name)
|
|
if obj is None:
|
|
raise UserException(
|
|
"'{0}' module has no callable '{1}'".format(
|
|
module_name, object_name))
|
|
|
|
return obj
|
|
|
|
@abstractmethod
|
|
def install(self):
|
|
""" Install the project. """
|
|
|
|
@abstractmethod
|
|
def setup(self, pyproject, tool, tool_description):
|
|
""" Complete the configuration of the project. """
|