From 70b26b616af873cf87e126c9d3790e969e69f574 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 15 Jun 2023 17:51:20 +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. --- .github/workflows/ci-cd.yml | 43 +-- .pre-commit-config.yaml | 2 + CHANGES/893.misc.rst | 24 ++ MANIFEST.in | 1 + Makefile | 4 +- README.rst | 7 +- docs/index.rst | 7 +- packaging/README.md | 11 + packaging/pep517_backend/__init__.py | 1 + packaging/pep517_backend/_backend.py | 395 ++++++++++++++++++++++ packaging/pep517_backend/_compat.py | 23 ++ packaging/pep517_backend/_transformers.py | 91 +++++ packaging/pep517_backend/hooks.py | 19 ++ pyproject.toml | 67 +++- requirements/ci.txt | 1 - requirements/test.txt | 1 - setup.cfg | 8 +- setup.py | 51 --- 18 files changed, 666 insertions(+), 90 deletions(-) create mode 100644 CHANGES/893.misc.rst create mode 100644 packaging/README.md create mode 100644 packaging/pep517_backend/__init__.py create mode 100644 packaging/pep517_backend/_backend.py create mode 100644 packaging/pep517_backend/_compat.py create mode 100644 packaging/pep517_backend/_transformers.py create mode 100644 packaging/pep517_backend/hooks.py delete mode 100644 setup.py diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 64339471a..07b97b404 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -38,11 +38,9 @@ jobs: uses: py-actions/py-dependency-install@v4 with: path: requirements/lint.txt - - name: Install itself + - name: Self-install run: | - python setup.py install - env: - YARL_NO_EXTENSIONS: 1 + pip install . - name: Run linters run: | make lint @@ -55,10 +53,8 @@ jobs: make doc-spelling - name: Prepare twine checker run: | - pip install -U twine wheel - python setup.py sdist bdist_wheel - env: - YARL_NO_EXTENSIONS: 1 + pip install -U build twine + python -m build --config-setting=--pure-python=true - name: Run twine checker run: | twine check dist/* @@ -111,23 +107,25 @@ jobs: uses: py-actions/py-dependency-install@v4 with: path: requirements/cython.txt - - name: Cythonize - if: ${{ matrix.no-extensions == '' }} - run: | - make cythonize - name: Install dependencies uses: py-actions/py-dependency-install@v4 with: path: requirements/ci.txt - env: - YARL_NO_EXTENSIONS: ${{ matrix.no-extensions }} + - name: Self-install + run: >- + python -Im pip install -e . + --config-settings=--pure-python=${{ + matrix.no-extensions != '' + && 'true' + || 'false' + }} - name: Run unittests env: COLOR: 'yes' YARL_NO_EXTENSIONS: ${{ matrix.no-extensions }} run: | - python -m pytest tests -vv - python -m coverage xml + python -Im pytest tests -vv + python -Im coverage xml - name: Upload coverage uses: codecov/codecov-action@v3.1.4 with: @@ -167,16 +165,12 @@ jobs: uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 - - name: Install cython - uses: py-actions/py-dependency-install@v4 - with: - path: requirements/cython.txt - - name: Cythonize + - name: Install pypa/build run: | - make cythonize + python -Im pip install build - name: Make sdist run: - python setup.py sdist + python -Im build --sdist - name: Upload artifacts uses: actions/upload-artifact@v3 with: @@ -222,9 +216,6 @@ jobs: uses: py-actions/py-dependency-install@v4 with: path: requirements/cython.txt - - name: Cythonize - run: | - make cythonize - name: Build wheels uses: pypa/cibuildwheel@v2.16.2 env: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 90becc2fd..cc014319e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,8 @@ repos: rev: v1.3.0 hooks: - id: yesqa + additional_dependencies: + - wemake-python-styleguide - repo: https://github.com/PyCQA/isort rev: '5.11.5' hooks: diff --git a/CHANGES/893.misc.rst b/CHANGES/893.misc.rst new file mode 100644 index 000000000..48a3cafa8 --- /dev/null +++ b/CHANGES/893.misc.rst @@ -0,0 +1,24 @@ +Replaced the packaging is replaced from an old-fashioned :file:`setup.py` to an +in-tree :pep:`517` build backend -- by :user:`webknjaz`. + +Whenever the end-users or downstream packagers need to build ``yarl`` from +source (a Git checkout or an sdist), they may pass a ``config_settings`` +flag ``--pure-python``. If this flag is not set, a C-extension will be built +and included into the distribution. + +Here is how this can be done with ``pip``: + +.. code-block:: console + + $ python -m pip install . --config-settings=--pure-python= + +This will also work with ``-e | --editable``. + +The same can be achieved via ``pypa/build``: + +.. code-block:: console + + $ python -m build --config-setting=--pure-python= + +Adding ``-w | --wheel`` can force ``pypa/build`` produce a wheel from source +directly, as opposed to building an ``sdist`` and then building from it. diff --git a/MANIFEST.in b/MANIFEST.in index dab6cb9a0..7a3e427af 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,6 +2,7 @@ include LICENSE include CHANGES.rst include README.rst graft yarl +graft packaging graft docs graft tests include yarl/*.c diff --git a/Makefile b/Makefile index e0e0c7883..8aff708b5 100644 --- a/Makefile +++ b/Makefile @@ -25,8 +25,8 @@ yarl/%.c: yarl/%.pyx cythonize: .cythonize -.develop: .install-deps $(shell find yarl -type f) .cythonize - @pip install -e . +.develop: .install-deps $(shell find yarl -type f) + @pip install -e . --config-settings=--pure-python=false @touch .develop fmt: diff --git a/README.rst b/README.rst index 7ee33bc33..293f97249 100644 --- a/README.rst +++ b/README.rst @@ -110,12 +110,13 @@ manylinux-compliant because of the missing glibc and therefore, cannot be used with our wheels) the the tarball will be used to compile the library from the source code. It requires a C compiler and and Python headers installed. -To skip the compilation you must explicitly opt-in by setting the `YARL_NO_EXTENSIONS` +To skip the compilation you must explicitly opt-in by using a PEP 517 +configuration setting ``--pure-python``, or setting the ``YARL_NO_EXTENSIONS`` environment variable to a non-empty value, e.g.: -.. code-block:: bash +.. code-block:: console - $ YARL_NO_EXTENSIONS=1 pip install yarl + $ pip install yarl --config-settings=--pure-python= Please note that the pure-Python (uncompiled) version is much slower. However, PyPy always uses a pure-Python implementation, and, as such, it is unaffected diff --git a/docs/index.rst b/docs/index.rst index 637acfa1d..873532886 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -101,12 +101,13 @@ manylinux-compliant because of the missing glibc and therefore, cannot be used with our wheels) the the tarball will be used to compile the library from the source code. It requires a C compiler and and Python headers installed. -To skip the compilation you must explicitly opt-in by setting the ``YARL_NO_EXTENSIONS`` +To skip the compilation you must explicitly opt-in by using a PEP 517 +configuration setting ``--pure-python``, or setting the ``YARL_NO_EXTENSIONS`` environment variable to a non-empty value, e.g.: -:: +.. code-block:: console - $ YARL_NO_EXTENSIONS=1 pip install yarl + $ pip install yarl --config-settings=--pure-python= Please note that the pure-Python (uncompiled) version is much slower. However, PyPy always uses a pure-Python implementation, and, as such, it is unaffected 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/_backend.py b/packaging/pep517_backend/_backend.py new file mode 100644 index 000000000..cd6702bce --- /dev/null +++ b/packaging/pep517_backend/_backend.py @@ -0,0 +1,395 @@ +# fmt: off +"""PEP 517 build backend wrapper for pre-building Cython for wheel.""" + +from __future__ import annotations + +import os +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 + +try: + from tomllib import loads as _load_toml_from_string +except ImportError: + from tomli import loads as _load_toml_from_string + +from expandvars import expandvars +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 + + +# 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 ._transformers import ( # noqa: WPS436 + get_cli_kwargs_from_config, + get_enabled_cli_flags_from_config, + sanitize_rst_roles, +) + +__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', + ) + ), +) + + +PURE_PYTHON_CONFIG_SETTING = '--pure-python' +"""Config setting name toggle that is used to opt out of making C-exts.""" + +PURE_PYTHON_ENV_VAR = 'YARL_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 = bool(IS_CPYTHON) +"""A fallback for `--pure-python` is not set.""" + + +def _make_pure_python(config_settings: dict[str, str] | None = None) -> bool: + truthy_values = {'', None, 'true', '1', 'on'} + + user_provided_setting_sources = ( + (config_settings, PURE_PYTHON_CONFIG_SETTING, (KeyError, TypeError)), + (os.environ, PURE_PYTHON_ENV_VAR, KeyError), + ) + for src_mapping, src_key, lookup_errors in user_provided_setting_sources: + with suppress(lookup_errors): + return src_mapping[src_key].lower() in truthy_values + + return PURE_PYTHON_MODE_CLI_FALLBACK + + +def _get_local_cython_config(): + """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'] + + +@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_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 patched_env(env): + """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 os.getenv('YARL_CYTHON_TRACING') == '1': + os.environ['CFLAGS'] = ' '.join(( + os.getenv('CFLAGS', ''), + '-DCYTHON_TRACE=1', + '-DCYTHON_TRACE_NOGIL=1', + )).strip() + try: + yield + finally: + os.environ.clear() + os.environ.update(orig_env) + + +@contextmanager +def _run_in_temporary_directory() -> t.Iterator[Path]: + with TemporaryDirectory(prefix='.tmp-yarl-pep517-') as tmp_dir: + with chdir_cm(tmp_dir): + yield Path(tmp_dir) + + +@contextmanager +def maybe_prebuild_c_extensions( # noqa: WPS210 + 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. + + """ + 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) + 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, + ) + + original_src_dir = Path.cwd().resolve() + build_dir_ctx = ( + nullcontext(original_src_dir) if build_inplace + else _run_in_temporary_directory() + ) + with build_dir_ctx as tmp_dir: + if not build_inplace: + tmp_src_dir = Path(tmp_dir) / 'src' + copytree(original_src_dir, tmp_src_dir, symlinks=True) + os.chdir(tmp_src_dir) + + config = _get_local_cython_config() + + 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']) + + cythonize_args = cli_flags + [py_ver_arg] + cli_kwargs + config['src'] + with patched_env(config['env']): + _cythonize_cli_cmd(cythonize_args) + with patched_distutils_cmd_install(): + with patched_dist_has_ext_modules(): + 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( + 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( + 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..d495b879f --- /dev/null +++ b/packaging/pep517_backend/_compat.py @@ -0,0 +1,23 @@ +"""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 +except ImportError: + + @contextmanager + 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) + + +__all__ = ("chdir_cm",) # noqa: WPS410 diff --git a/packaging/pep517_backend/_transformers.py b/packaging/pep517_backend/_transformers.py new file mode 100644 index 000000000..b19dc5b48 --- /dev/null +++ b/packaging/pep517_backend/_transformers.py @@ -0,0 +1,91 @@ +"""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.""" + 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 >`__" + ) + + role_regex = r"""(?x) + (?::\w+)?:\w+:`(?P[^`]+)(?:\s+(.*))?` + """ + substitution_pattern = r"``\g``" + + substitutions = ( + (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), + (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/hooks.py b/packaging/pep517_backend/hooks.py new file mode 100644 index 000000000..825e6988f --- /dev/null +++ b/packaging/pep517_backend/hooks.py @@ -0,0 +1,19 @@ +"""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 + +from ._backend import ( # noqa: WPS436 # Re-exporting PEP 517 hooks + build_sdist, + build_wheel, + get_requires_for_build_wheel, + prepare_metadata_for_build_wheel, +) + +with _suppress(ImportError): # Only succeeds w/ setuptools implementing PEP 660 + from ._backend import ( # noqa: WPS436 # Re-exporting PEP 660 hooks + build_editable, + get_requires_for_build_editable, + prepare_metadata_for_build_editable, + ) diff --git a/pyproject.toml b/pyproject.toml index 870be55c8..fcfb99673 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,67 @@ [build-system] requires = [ - "setuptools>=40", + # 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'", ] -build-backend = "setuptools.build_meta" +backend-path = ["packaging"] # requires `pip >= 20` or `pep517 >= 0.6.0` +build-backend = "pep517_backend.hooks" # wraps `setuptools.build_meta` + +[tool.local.cythonize] +# This attr can contain multiple globs +src = ["yarl/*.pyx"] + +[tool.local.cythonize.env] +# Env vars provisioned during cythonize call +#CFLAGS = "-DCYTHON_TRACE=1 ${CFLAGS}" +#LDFLAGS = "${LDFLAGS}" + +[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.directive] +# This section can contain compiler directives +# Ref: https://github.com/cython/cython/blob/d6e6de9/Cython/Compiler/Options.py#L170-L242 +embedsignature = "True" +emit_code_comments = "True" +linetrace = "True" +profile = "True" + +[tool.local.cythonize.kwargs.compile-time-env] +# This section can contain compile time env vars + +[tool.local.cythonize.kwargs.option] +# This section can contain cythonize options +# Ref: https://github.com/cython/cython/blob/d6e6de9/Cython/Compiler/Options.py#L694-L730 +#docstrings = "True" +#embed_pos_in_docstring = "True" +#warning_errors = "True" +#error_on_unknown_names = "True" +#error_on_uninitialized = "True" [tool.towncrier] package = "yarl" @@ -13,7 +72,11 @@ issue_format = "`#{issue} `_" [tool.cibuildwheel] +build-frontend = "build" test-requires = "-r requirements/ci.txt" test-command = "pytest {project}/tests" # don't build PyPy wheels, install from source instead skip = "pp*" + +[tool.cibuildwheel.config-settings] +--pure-python = "false" diff --git a/requirements/ci.txt b/requirements/ci.txt index 421b0594e..4e95b36ec 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -1,3 +1,2 @@ --e . -r test.txt -r lint.txt diff --git a/requirements/test.txt b/requirements/test.txt index 040697aa5..f7fb625c0 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,4 +1,3 @@ --e . idna==3.4 multidict==6.0.4 pytest==7.4.3 diff --git a/setup.cfg b/setup.cfg index f9a506234..1ce531ae3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,7 +17,7 @@ project_urls = GitHub: issues = https://github.com/aio-libs/yarl/issues GitHub: repo = https://github.com/aio-libs/yarl description = Yet another URL library -# long_description = file: README.rst, CHANGES.rst +long_description = file: README.rst, CHANGES.rst long_description_content_type = text/x-rst author = Andrew Svetlov author_email = andrew.svetlov@gmail.com @@ -99,6 +99,12 @@ max-line-length=79 ignore = E203,E301,E302,E704,W503,W504,F811 max-line-length = 88 +# Allow certain violations in certain files: +per-file-ignores = + + # F401 imported but unused + packaging/pep517_backend/hooks.py: F401 + [isort] profile=black diff --git a/setup.py b/setup.py deleted file mode 100644 index e9334a2e7..000000000 --- a/setup.py +++ /dev/null @@ -1,51 +0,0 @@ -import os -import pathlib -import re -import sys - -from setuptools import Extension, setup - -NO_EXTENSIONS = bool(os.environ.get("YARL_NO_EXTENSIONS")) # type: bool - -if sys.implementation.name != "cpython": - NO_EXTENSIONS = True - - -extensions = [Extension("yarl._quoting_c", ["yarl/_quoting_c.c"])] - - -here = pathlib.Path(__file__).parent - - -def read(name): - fname = here / name - with fname.open(encoding="utf8") as f: - return f.read() - - -# $ echo ':something:`test `' | sed 's/:\w\+:`\(\w\+\)\(\s\+\(.*\)\)\?`/``\1``/g' -# ``test`` -def sanitize_rst_roles(rst_source_text: str) -> str: - """Replace RST roles with inline highlighting.""" - role_regex = r":\w+:`(?P[^`]+)(\s+(.*))?`" - substitution_pattern = r"``(?P=rendered_text)``" - return re.sub(role_regex, substitution_pattern, rst_source_text) - - -args = dict( - long_description="\n\n".join( - [read("README.rst"), sanitize_rst_roles(read("CHANGES.rst"))] - ), -) - - -if not NO_EXTENSIONS: - print("**********************") - print("* Accelerated build *") - print("**********************") - setup(ext_modules=extensions, **args) -else: - print("*********************") - print("* Pure Python build *") - print("*********************") - setup(**args)