From 5138892edb1bce56deca7b60707464888b6f63a8 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 10 Sep 2024 02:16:29 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=A6=20Move=20packaging=20to=20PEP=2051?= =?UTF-8?q?7=20in-tree=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This essentially allows the cythonization opt-out be controlled by the `pure-python` PEP 517 config setting that can be passed to the corresponding build frontends via their respective CLIs. --- MANIFEST.in | 1 + packaging/README.md | 11 + packaging/pep517_backend/__init__.py | 1 + packaging/pep517_backend/__main__.py | 6 + packaging/pep517_backend/_backend.py | 410 ++++++++++++++++++ packaging/pep517_backend/_compat.py | 29 ++ .../pep517_backend/_cython_configuration.py | 107 +++++ packaging/pep517_backend/_transformers.py | 105 +++++ packaging/pep517_backend/cli.py | 53 +++ packaging/pep517_backend/hooks.py | 21 + pyproject.toml | 30 +- setup.cfg | 15 + setup.py | 44 -- 13 files changed, 787 insertions(+), 46 deletions(-) create mode 100644 packaging/README.md create mode 100644 packaging/pep517_backend/__init__.py create mode 100644 packaging/pep517_backend/__main__.py create mode 100644 packaging/pep517_backend/_backend.py create mode 100644 packaging/pep517_backend/_compat.py create mode 100644 packaging/pep517_backend/_cython_configuration.py create mode 100644 packaging/pep517_backend/_transformers.py create mode 100644 packaging/pep517_backend/cli.py create mode 100644 packaging/pep517_backend/hooks.py delete mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in index 43625782e..d13f3c19c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ include CHANGES.rst include README.rst include Makefile graft multidict +graft packaging graft docs graft CHANGES graft requirements diff --git a/packaging/README.md b/packaging/README.md new file mode 100644 index 000000000..9940dc56f --- /dev/null +++ b/packaging/README.md @@ -0,0 +1,11 @@ +# `pep517_backend` in-tree build backend + +The `pep517_backend.hooks` importable exposes callables declared by PEP 517 +and PEP 660 and is integrated into `pyproject.toml`'s +`[build-system].build-backend` through `[build-system].backend-path`. + +# Design considerations + +`__init__.py` is to remain empty, leaving `hooks.py` the only entrypoint +exposing the callables. The logic is contained in private modules. This is +to prevent import-time side effects. diff --git a/packaging/pep517_backend/__init__.py b/packaging/pep517_backend/__init__.py new file mode 100644 index 000000000..74ae43697 --- /dev/null +++ b/packaging/pep517_backend/__init__.py @@ -0,0 +1 @@ +"""PEP 517 build backend for optionally pre-building Cython.""" diff --git a/packaging/pep517_backend/__main__.py b/packaging/pep517_backend/__main__.py new file mode 100644 index 000000000..7ad33e74d --- /dev/null +++ b/packaging/pep517_backend/__main__.py @@ -0,0 +1,6 @@ +import sys + +from . import cli + +if __name__ == "__main__": + sys.exit(cli.run_main_program(argv=sys.argv)) diff --git a/packaging/pep517_backend/_backend.py b/packaging/pep517_backend/_backend.py new file mode 100644 index 000000000..fa3d773bb --- /dev/null +++ b/packaging/pep517_backend/_backend.py @@ -0,0 +1,410 @@ +# fmt: off +"""PEP 517 build backend wrapper for pre-building Cython for wheel.""" + +from __future__ import annotations + +import os +import platform +import typing as t +from contextlib import contextmanager, nullcontext, suppress +from pathlib import Path +from shutil import copytree +from sys import implementation as _system_implementation +from sys import stderr as _standard_error_stream +from sys import version_info as _python_version_tuple +from tempfile import TemporaryDirectory +from warnings import warn as _warn_that + +from setuptools import Extension +from setuptools.build_meta import build_sdist as _setuptools_build_sdist +from setuptools.build_meta import build_wheel as _setuptools_build_wheel +# from setuptools.build_meta import ( +# get_requires_for_build_wheel as _setuptools_get_requires_for_build_wheel, +# ) +from setuptools.build_meta import ( + prepare_metadata_for_build_wheel as _setuptools_prepare_metadata_for_build_wheel, +) + +try: + from setuptools.build_meta import build_editable as _setuptools_build_editable +except ImportError: + _setuptools_build_editable = None # type: ignore[assignment] + + +# isort: split +from distutils.command.install import install as _distutils_install_cmd +from distutils.core import Distribution as _DistutilsDistribution +from distutils.dist import DistributionMetadata as _DistutilsDistributionMetadata + +# with suppress(ImportError): +# # NOTE: Only available for wheel builds that bundle C-extensions. Declared +# # NOTE: by `get_requires_for_build_wheel()` and +# # NOTE: `get_requires_for_build_editable()`, when `pure-python` +# # NOTE: is not passed. +# from Cython.Build.Cythonize import main as _cythonize_cli_cmd + +from ._compat import chdir_cm +# from ._cython_configuration import ( # noqa: WPS436 +# get_local_cython_config as _get_local_cython_config, +# ) +# from ._cython_configuration import ( +# make_cythonize_cli_args_from_config as _make_cythonize_cli_args_from_config, +# ) +# from ._cython_configuration import patched_env as _patched_cython_env +from ._transformers import sanitize_rst_roles # noqa: WPS436 + +__all__ = ( # noqa: WPS410 + 'build_sdist', + 'build_wheel', + # 'get_requires_for_build_wheel', + 'prepare_metadata_for_build_wheel', + *( + () if _setuptools_build_editable is None + else ( + 'build_editable', + # 'get_requires_for_build_editable', + 'prepare_metadata_for_build_editable', + ) + ), +) + + +TRACING_CONFIG_SETTING = 'with-c-extension-tracing' +"""Config setting name toggle to include line tracing to C-exts.""" + +TRACING_ENV_VAR = 'MULTIDICT_EXTENSION_TRACING' +"""Environment variable name toggle used to opt out of making C-exts.""" + +PURE_PYTHON_CONFIG_SETTING = 'pure-python' +"""Config setting name toggle that is used to opt out of making C-exts.""" + +PURE_PYTHON_ENV_VAR = 'MULTIDICT_NO_EXTENSIONS' +"""Environment variable name toggle used to opt out of making C-exts.""" + +# IS_PY3_12_PLUS = _python_version_tuple[:2] >= (3, 12) +# """A flag meaning that the current runtime is Python 3.12 or higher.""" + +IS_CPYTHON = _system_implementation.name == "cpython" +"""A flag meaning that the current interpreter implementation is CPython.""" + +PURE_PYTHON_MODE_CLI_FALLBACK = not IS_CPYTHON +"""A fallback for ``pure-python`` is not set.""" + + +def _is_truthy_setting_value(setting_value) -> bool: + truthy_values = {'', None, 'true', '1', 'on'} + return setting_value.lower() in truthy_values + + +def _get_setting_value( + config_settings: dict[str, str] | None = None, + config_setting_name: str | None = None, + env_var_name: str | None = None, + *, + default: bool = False, +) -> bool: + user_provided_setting_sources = ( + (config_settings, config_setting_name, (KeyError, TypeError)), + (os.environ, env_var_name, KeyError), + ) + for src_mapping, src_key, lookup_errors in user_provided_setting_sources: + if src_key is None: + continue + + with suppress(lookup_errors): # type: ignore[arg-type] + return _is_truthy_setting_value(src_mapping[src_key]) # type: ignore[index] + + return default + + +def _make_pure_python(config_settings: dict[str, str] | None = None) -> bool: + return _get_setting_value( + config_settings, + PURE_PYTHON_CONFIG_SETTING, + PURE_PYTHON_ENV_VAR, + default=PURE_PYTHON_MODE_CLI_FALLBACK, + ) + + +def _include_cython_line_tracing( + config_settings: dict[str, str] | None = None, + *, + default=False, +) -> bool: + return _get_setting_value( + config_settings, + TRACING_CONFIG_SETTING, + TRACING_ENV_VAR, + default=default, + ) + + +@contextmanager +def patched_distutils_cmd_install(): + """Make `install_lib` of `install` cmd always use `platlib`. + + :yields: None + """ + # Without this, build_lib puts stuff under `*.data/purelib/` folder + orig_finalize = _distutils_install_cmd.finalize_options + + def new_finalize_options(self): # noqa: WPS430 + self.install_lib = self.install_platlib + orig_finalize(self) + + _distutils_install_cmd.finalize_options = new_finalize_options + try: + yield + finally: + _distutils_install_cmd.finalize_options = orig_finalize + + +@contextmanager +def patched_dist_has_ext_modules(): + """Make `has_ext_modules` of `Distribution` always return `True`. + + :yields: None + """ + # Without this, build_lib puts stuff under `*.data/platlib/` folder + orig_func = _DistutilsDistribution.has_ext_modules + + _DistutilsDistribution.has_ext_modules = lambda *args, **kwargs: True + try: + yield + finally: + _DistutilsDistribution.has_ext_modules = orig_func + + +@contextmanager +def patched_dist_ext_modules(*, ext_modules): + """Make `has_ext_modules` of `Distribution` always return `True`. + + :yields: None + """ + _DistutilsDistribution.ext_modules = ext_modules + try: + yield + finally: + _DistutilsDistribution.ext_modules = None + + +@contextmanager +def patched_dist_get_long_description(): + """Make `has_ext_modules` of `Distribution` always return `True`. + + :yields: None + """ + # Without this, build_lib puts stuff under `*.data/platlib/` folder + _orig_func = _DistutilsDistributionMetadata.get_long_description + + def _get_sanitized_long_description(self): + return sanitize_rst_roles(self.long_description) + + _DistutilsDistributionMetadata.get_long_description = ( + _get_sanitized_long_description + ) + try: + yield + finally: + _DistutilsDistributionMetadata.get_long_description = _orig_func + + +@contextmanager +def _in_temporary_directory(src_dir: Path) -> t.Iterator[None]: + with TemporaryDirectory(prefix='.tmp-multidict-pep517-') as tmp_dir: + with chdir_cm(tmp_dir): + tmp_src_dir = Path(tmp_dir) / 'src' + copytree(src_dir, tmp_src_dir, symlinks=True) + os.chdir(tmp_src_dir) + yield + + +@contextmanager +def maybe_prebuild_c_extensions( + line_trace_cython_when_unset: bool = False, + build_inplace: bool = False, + config_settings: dict[str, str] | None = None, +) -> t.Generator[None, t.Any, t.Any]: + """Pre-build C-extensions in a temporary directory, when needed. + + This context manager also patches metadata, setuptools and distutils. + + :param build_inplace: Whether to copy and chdir to a temporary location. + :param config_settings: :pep:`517` config settings mapping. + + """ + line_tracing_requested = _include_cython_line_tracing( + config_settings, + default=line_trace_cython_when_unset, + ) + is_pure_python_build = _make_pure_python(config_settings) + + if is_pure_python_build: + print("*********************", file=_standard_error_stream) + print("* Pure Python build *", file=_standard_error_stream) + print("*********************", file=_standard_error_stream) + + if line_tracing_requested: + _warn_that( + f'The `{TRACING_CONFIG_SETTING !s}` setting requesting ' + 'Cython line tracing is set, but building C-extensions is not. ' + 'This option will not have any effect for in the pure-python ' + 'build mode.', + RuntimeWarning, + stacklevel=999, + ) + + with patched_dist_ext_modules(ext_modules=[]): + yield + return + + print("**********************", file=_standard_error_stream) + print("* Accelerated build *", file=_standard_error_stream) + print("**********************", file=_standard_error_stream) + if not IS_CPYTHON: + _warn_that( + 'Building C-extensions under the runtimes other than CPython is ' + 'unsupported and will likely fail. Consider passing the ' + f'`{PURE_PYTHON_CONFIG_SETTING !s}` PEP 517 config setting.', + RuntimeWarning, + stacklevel=999, + ) + + print(f'{line_tracing_requested=}') + CFLAGS = ["-O2"] + # if line_tracing_requested: + # # CFLAGS.extend( + # # [ + # # "--coverage", + # # "-g", + # # "-O0", # FIXME: should `-O2` be dropped? + # # ] + # # ) + # # https://gcovr.com/en/stable/getting-started.html#getting-started + # # CFLAGS = [ + # # "--coverage", + # # "-g", + # # "-O0", + # # ] + if platform.system() != "Windows": + CFLAGS.extend( + [ + "-std=c99", + "-Wall", + "-Wsign-compare", + "-Wconversion", + "-fno-strict-aliasing", + "-pedantic", + ] + ) + c_extensions = [ + Extension( + "multidict._multidict", + ["multidict/_multidict.c"], + extra_compile_args=CFLAGS, + ), + ] + build_dir_ctx = ( + nullcontext() if build_inplace + else _in_temporary_directory(src_dir=Path.cwd().resolve()) + ) + with build_dir_ctx: + # config = _get_local_cython_config() + + # cythonize_args = _make_cythonize_cli_args_from_config(config) + # with _patched_cython_env(config['env'], line_tracing_requested): + # _cythonize_cli_cmd(cythonize_args) + with patched_distutils_cmd_install(): + with patched_dist_has_ext_modules(), patched_dist_ext_modules(ext_modules=c_extensions): + yield + + +@patched_dist_get_long_description() +def build_wheel( + wheel_directory: str, + config_settings: dict[str, str] | None = None, + metadata_directory: str | None = None, +) -> str: + """Produce a built wheel. + + This wraps the corresponding ``setuptools``' build backend hook. + + :param wheel_directory: Directory to put the resulting wheel in. + :param config_settings: :pep:`517` config settings mapping. + :param metadata_directory: :file:`.dist-info` directory path. + + """ + with maybe_prebuild_c_extensions( + line_trace_cython_when_unset=False, + build_inplace=False, + config_settings=config_settings, + ): + return _setuptools_build_wheel( + wheel_directory=wheel_directory, + config_settings=config_settings, + metadata_directory=metadata_directory, + ) + + +@patched_dist_get_long_description() +def build_editable( + wheel_directory: str, + config_settings: dict[str, str] | None = None, + metadata_directory: str | None = None, +) -> str: + """Produce a built wheel for editable installs. + + This wraps the corresponding ``setuptools``' build backend hook. + + :param wheel_directory: Directory to put the resulting wheel in. + :param config_settings: :pep:`517` config settings mapping. + :param metadata_directory: :file:`.dist-info` directory path. + + """ + with maybe_prebuild_c_extensions( + line_trace_cython_when_unset=True, + build_inplace=True, + config_settings=config_settings, + ): + return _setuptools_build_editable( + wheel_directory=wheel_directory, + config_settings=config_settings, + metadata_directory=metadata_directory, + ) + + +# def get_requires_for_build_wheel( +# config_settings: dict[str, str] | None = None, +# ) -> list[str]: +# """Determine additional requirements for building wheels. +# +# :param config_settings: :pep:`517` config settings mapping. +# +# """ +# is_pure_python_build = _make_pure_python(config_settings) +# +# if not is_pure_python_build and not IS_CPYTHON: +# _warn_that( +# 'Building C-extensions under the runtimes other than CPython is ' +# 'unsupported and will likely fail. Consider passing the ' +# f'`{PURE_PYTHON_CONFIG_SETTING !s}` PEP 517 config setting.', +# RuntimeWarning, +# stacklevel=999, +# ) +# +# c_ext_build_deps = [] if is_pure_python_build else [ +# 'Cython >= 3.0.0b3' if IS_PY3_12_PLUS # Only Cython 3+ is compatible +# else 'Cython', +# ] +# +# return _setuptools_get_requires_for_build_wheel( +# config_settings=config_settings, +# ) + c_ext_build_deps + + +build_sdist = patched_dist_get_long_description()(_setuptools_build_sdist) +# get_requires_for_build_editable = get_requires_for_build_wheel +prepare_metadata_for_build_wheel = patched_dist_get_long_description()( + _setuptools_prepare_metadata_for_build_wheel, +) +prepare_metadata_for_build_editable = prepare_metadata_for_build_wheel diff --git a/packaging/pep517_backend/_compat.py b/packaging/pep517_backend/_compat.py new file mode 100644 index 000000000..5e2ffe096 --- /dev/null +++ b/packaging/pep517_backend/_compat.py @@ -0,0 +1,29 @@ +"""Cross-python stdlib shims.""" + +import os +import typing as t +from contextlib import contextmanager +from pathlib import Path + +try: + from contextlib import chdir as chdir_cm # type: ignore[attr-defined] +except ImportError: + + @contextmanager # type: ignore[no-redef] + def chdir_cm(path: os.PathLike) -> t.Iterator[None]: + """Temporarily change the current directory, recovering on exit.""" + original_wd = Path.cwd() + os.chdir(path) + try: + yield + finally: + os.chdir(original_wd) + + +try: + from tomllib import loads as load_toml_from_string +except ImportError: + from tomli import loads as load_toml_from_string # type: ignore[no-redef] + + +__all__ = ("chdir_cm", "load_toml_from_string") # noqa: WPS410 diff --git a/packaging/pep517_backend/_cython_configuration.py b/packaging/pep517_backend/_cython_configuration.py new file mode 100644 index 000000000..316b85fcf --- /dev/null +++ b/packaging/pep517_backend/_cython_configuration.py @@ -0,0 +1,107 @@ +# fmt: off + +from __future__ import annotations + +import os +from contextlib import contextmanager +from pathlib import Path +from sys import version_info as _python_version_tuple + +from expandvars import expandvars + +from ._compat import load_toml_from_string # noqa: WPS436 +from ._transformers import ( # noqa: WPS436 + get_cli_kwargs_from_config, + get_enabled_cli_flags_from_config, +) + + +def get_local_cython_config() -> dict: + """Grab optional build dependencies from pyproject.toml config. + + :returns: config section from ``pyproject.toml`` + :rtype: dict + + This basically reads entries from:: + + [tool.local.cythonize] + # Env vars provisioned during cythonize call + src = ["src/**/*.pyx"] + + [tool.local.cythonize.env] + # Env vars provisioned during cythonize call + LDFLAGS = "-lssh" + + [tool.local.cythonize.flags] + # This section can contain the following booleans: + # * annotate — generate annotated HTML page for source files + # * build — build extension modules using distutils + # * inplace — build extension modules in place using distutils (implies -b) + # * force — force recompilation + # * quiet — be less verbose during compilation + # * lenient — increase Python compat by ignoring some compile time errors + # * keep-going — compile as much as possible, ignore compilation failures + annotate = false + build = false + inplace = true + force = true + quiet = false + lenient = false + keep-going = false + + [tool.local.cythonize.kwargs] + # This section can contain args that have values: + # * exclude=PATTERN exclude certain file patterns from the compilation + # * parallel=N run builds in N parallel jobs (default: calculated per system) + exclude = "**.py" + parallel = 12 + + [tool.local.cythonize.kwargs.directives] + # This section can contain compiler directives + # NAME = "VALUE" + + [tool.local.cythonize.kwargs.compile-time-env] + # This section can contain compile time env vars + # NAME = "VALUE" + + [tool.local.cythonize.kwargs.options] + # This section can contain cythonize options + # NAME = "VALUE" + """ + config_toml_txt = (Path.cwd().resolve() / 'pyproject.toml').read_text() + config_mapping = load_toml_from_string(config_toml_txt) + return config_mapping['tool']['local']['cythonize'] + + +def make_cythonize_cli_args_from_config(config) -> list[str]: + py_ver_arg = f'-{_python_version_tuple.major!s}' + + cli_flags = get_enabled_cli_flags_from_config(config['flags']) + cli_kwargs = get_cli_kwargs_from_config(config['kwargs']) + + return cli_flags + [py_ver_arg] + cli_kwargs + ['--'] + config['src'] + + +@contextmanager +def patched_env(env: dict[str, str], cython_line_tracing_requested: bool): + """Temporary set given env vars. + + :param env: tmp env vars to set + :type env: dict + + :yields: None + """ + orig_env = os.environ.copy() + expanded_env = {name: expandvars(var_val) for name, var_val in env.items()} + os.environ.update(expanded_env) + + if cython_line_tracing_requested: + os.environ['CFLAGS'] = ' '.join(( + os.getenv('CFLAGS', ''), + '-DCYTHON_TRACE_NOGIL=1', # Implies CYTHON_TRACE=1 + )).strip() + try: + yield + finally: + os.environ.clear() + os.environ.update(orig_env) diff --git a/packaging/pep517_backend/_transformers.py b/packaging/pep517_backend/_transformers.py new file mode 100644 index 000000000..a106dcba1 --- /dev/null +++ b/packaging/pep517_backend/_transformers.py @@ -0,0 +1,105 @@ +"""Data conversion helpers for the in-tree PEP 517 build backend.""" + +from itertools import chain +from re import sub as _substitute_with_regexp + + +def _emit_opt_pairs(opt_pair): + flag, flag_value = opt_pair + flag_opt = f"--{flag!s}" + if isinstance(flag_value, dict): + sub_pairs = flag_value.items() + else: + sub_pairs = ((flag_value,),) + + yield from ("=".join(map(str, (flag_opt,) + pair)) for pair in sub_pairs) + + +def get_cli_kwargs_from_config(kwargs_map): + """Make a list of options with values from config.""" + return list(chain.from_iterable(map(_emit_opt_pairs, kwargs_map.items()))) + + +def get_enabled_cli_flags_from_config(flags_map): + """Make a list of enabled boolean flags from config.""" + return [f"--{flag}" for flag, is_enabled in flags_map.items() if is_enabled] + + +def sanitize_rst_roles(rst_source_text: str) -> str: + """Replace RST roles with inline highlighting.""" + pep_role_regex = r"""(?x) + :pep:`(?P\d+)` + """ + pep_substitution_pattern = ( + r"`PEP \g >`__" + ) + + user_role_regex = r"""(?x) + :user:`(?P[^`]+)(?:\s+(.*))?` + """ + user_substitution_pattern = ( + r"`@\g " + r">`__" + ) + + issue_role_regex = r"""(?x) + :issue:`(?P[^`]+)(?:\s+(.*))?` + """ + issue_substitution_pattern = ( + r"`#\g " + r">`__" + ) + + pr_role_regex = r"""(?x) + :pr:`(?P[^`]+)(?:\s+(.*))?` + """ + pr_substitution_pattern = ( + r"`PR #\g " + r">`__" + ) + + commit_role_regex = r"""(?x) + :commit:`(?P[^`]+)(?:\s+(.*))?` + """ + commit_substitution_pattern = ( + r"`\g " + r">`__" + ) + + gh_role_regex = r"""(?x) + :gh:`(?P[^`]+)(?:\s+(.*))?` + """ + gh_substitution_pattern = ( + r"`GitHub: \g >`__" + ) + + meth_role_regex = r"""(?x) + (?::py)?:meth:`~?(?P[^`<]+)(?:\s+([^`]*))?` + """ + meth_substitution_pattern = r"``\g()``" + + role_regex = r"""(?x) + (?::\w+)?:\w+:`(?P[^`<]+)(?:\s+([^`]*))?` + """ + substitution_pattern = r"``\g``" + + substitutions = ( + (pep_role_regex, pep_substitution_pattern), + (user_role_regex, user_substitution_pattern), + (issue_role_regex, issue_substitution_pattern), + (pr_role_regex, pr_substitution_pattern), + (commit_role_regex, commit_substitution_pattern), + (gh_role_regex, gh_substitution_pattern), + (meth_role_regex, meth_substitution_pattern), + (role_regex, substitution_pattern), + ) + + rst_source_normalized_text = rst_source_text + for regex, substitution in substitutions: + rst_source_normalized_text = _substitute_with_regexp( + regex, + substitution, + rst_source_normalized_text, + ) + + return rst_source_normalized_text diff --git a/packaging/pep517_backend/cli.py b/packaging/pep517_backend/cli.py new file mode 100644 index 000000000..f3a1c85cc --- /dev/null +++ b/packaging/pep517_backend/cli.py @@ -0,0 +1,53 @@ +# fmt: off + +from __future__ import annotations + +import sys +from itertools import chain +from pathlib import Path + +from Cython.Compiler.Main import compile as _translate_cython_cli_cmd +from Cython.Compiler.Main import parse_command_line as _split_cython_cli_args + +from ._cython_configuration import get_local_cython_config as _get_local_cython_config +from ._cython_configuration import ( + make_cythonize_cli_args_from_config as _make_cythonize_cli_args_from_config, +) +from ._cython_configuration import patched_env as _patched_cython_env + +_PROJECT_PATH = Path(__file__).parents[2] + + +def run_main_program(argv) -> int | str: + """Invoke ``translate-cython`` or fail.""" + if len(argv) != 2: + return 'This program only accepts one argument -- "translate-cython"' + + if argv[1] != 'translate-cython': + return 'This program only implements the "translate-cython" subcommand' + + config = _get_local_cython_config() + config['flags'] = {'keep-going': config['flags']['keep-going']} + config['src'] = list( + map( + str, + chain.from_iterable( + map(_PROJECT_PATH.glob, config['src']), + ), + ), + ) + translate_cython_cli_args = _make_cythonize_cli_args_from_config(config) + + cython_options, cython_sources = _split_cython_cli_args( + translate_cython_cli_args, + ) + + with _patched_cython_env(config['env'], cython_line_tracing_requested=True): + return _translate_cython_cli_cmd( + cython_sources, + cython_options, + ).num_errors + + +if __name__ == '__main__': + sys.exit(run_main_program(argv=sys.argv)) diff --git a/packaging/pep517_backend/hooks.py b/packaging/pep517_backend/hooks.py new file mode 100644 index 000000000..c3c8edd2e --- /dev/null +++ b/packaging/pep517_backend/hooks.py @@ -0,0 +1,21 @@ +"""PEP 517 build backend for optionally pre-building Cython.""" + +from contextlib import suppress as _suppress + +from setuptools.build_meta import * # Re-exporting PEP 517 hooks # pylint: disable=unused-wildcard-import,wildcard-import # noqa: E501, F401, F403 + +# Re-exporting PEP 517 hooks +from ._backend import ( # type: ignore[assignment] # noqa: WPS436 + build_sdist, + build_wheel, + # get_requires_for_build_wheel, + prepare_metadata_for_build_wheel, +) + +with _suppress(ImportError): # Only succeeds w/ setuptools implementing PEP 660 + # Re-exporting PEP 660 hooks + from ._backend import ( # type: ignore[assignment] # noqa: WPS436 + build_editable, + # get_requires_for_build_editable, + prepare_metadata_for_build_editable, + ) diff --git a/pyproject.toml b/pyproject.toml index b2de51005..f9a6fe98d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,33 @@ [build-system] -requires = ["setuptools >= 40"] -build-backend = "setuptools.build_meta" +requires = [ + # NOTE: The following build dependencies are necessary for initial + # NOTE: provisioning of the in-tree build backend located under + # NOTE: `packaging/pep517_backend/`. + "expandvars", + "setuptools >= 47", # Minimum required for `version = attr:` + "tomli; python_version < '3.11'", +] +backend-path = ["packaging"] # requires `pip >= 20` or `pep517 >= 0.6.0` +build-backend = "pep517_backend.hooks" # wraps `setuptools.build_meta` +[[tool.local.extensions."multidict._multidict"]] +c-sources = ["multidict/_multidict.c"] +extra_compile_args = "${CFLAGS}" + + +[tool.local.cythonize] + +[tool.local.cythonize.env] + +[tool.local.cythonize.flags] + +[tool.local.cythonize.kwargs] + +[tool.local.cythonize.kwargs.directive] + +[tool.local.cythonize.kwargs.compile-time-env] + +[tool.local.cythonize.kwargs.option] [tool.towncrier] package = "multidict" diff --git a/setup.cfg b/setup.cfg index c3b7cab5b..983ff36a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,21 @@ install_requires = typing-extensions >= 4.1.0; python_version < '3.11' packages = multidict +# https://setuptools.pypa.io/en/latest/deprecated/zip_safe.html +zip_safe = False +include_package_data = True + +[options.package_data] +# Ref: +# https://setuptools.pypa.io/en/latest/userguide/datafiles.html#package-data +# (see notes for the asterisk/`*` meaning) +* = + *.so + +[options.exclude_package_data] +* = + *.c + *.h [isort] diff --git a/setup.py b/setup.py deleted file mode 100644 index fcf200516..000000000 --- a/setup.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -import platform -import sys - -from setuptools import Extension, setup - -NO_EXTENSIONS = bool(os.environ.get("MULTIDICT_NO_EXTENSIONS")) - -if sys.implementation.name != "cpython": - NO_EXTENSIONS = True - -CFLAGS = ["-O2"] -# CFLAGS = ['-g'] -if platform.system() != "Windows": - CFLAGS.extend( - [ - "-std=c99", - "-Wall", - "-Wsign-compare", - "-Wconversion", - "-fno-strict-aliasing", - "-pedantic", - ] - ) - -extensions = [ - Extension( - "multidict._multidict", - ["multidict/_multidict.c"], - extra_compile_args=CFLAGS, - ), -] - - -if not NO_EXTENSIONS: - print("*********************") - print("* Accelerated build *") - print("*********************") - setup(ext_modules=extensions) -else: - print("*********************") - print("* Pure Python build *") - print("*********************") - setup()