diff --git a/conda_build/api.py b/conda_build/api.py index fb8555c07c..b64c14002e 100644 --- a/conda_build/api.py +++ b/conda_build/api.py @@ -11,11 +11,14 @@ from __future__ import annotations +import inspect + # imports are done locally to keep the api clean and limited strictly # to conda-build's functionality. import os import sys from collections.abc import Iterable +from importlib import import_module from os.path import dirname, expanduser, join from pathlib import Path from typing import TYPE_CHECKING @@ -23,6 +26,7 @@ # make the Config class available in the api namespace from .config import DEFAULT_PREFIX_LENGTH as _prefix_length from .config import Config, get_channel_urls, get_or_merge_config +from .deprecations import deprecated from .metadata import MetaData, MetaDataTuple from .utils import ( CONDA_PACKAGE_EXTENSIONS, @@ -259,6 +263,7 @@ def test( ) +@deprecated("27.3", "27.9", addendum="Use `grayskull` instead.") def list_skeletons() -> list[str]: """List available skeletons for generating conda recipes from external sources. @@ -274,6 +279,7 @@ def list_skeletons() -> list[str]: return files +@deprecated("27.3", "27.9", addendum="Use `grayskull` instead.") def skeletonize( packages: str | Iterable[str], repo: Literal["cpan", "cran", "luarocks", "pypi", "rpm"], @@ -306,18 +312,13 @@ def skeletonize( # off of the config object, and pass it as a keyword argument. This is sort of the # inverse of what we do in the CLI code - there we take CLI arguments and dangle them # all on the config object as attributes. - module = getattr( - __import__( - "conda_build.skeletons", globals=globals(), locals=locals(), fromlist=[repo] - ), - repo, - ) - - func_args = module.skeletonize.__code__.co_varnames + module = import_module(f"conda_build.skeletons.{repo}") + argspec = inspect.signature(module.skeletonize) + func_args = list(argspec.parameters) kwargs = {name: getattr(config, name) for name in dir(config) if name in func_args} kwargs.update({name: value for name, value in kwargs.items() if name in func_args}) # strip out local arguments that we pass directly - for arg in skeletonize.__code__.co_varnames: + for arg in list(inspect.signature(skeletonize).parameters): if arg in kwargs: del kwargs[arg] with config: diff --git a/conda_build/cli/main_skeleton.py b/conda_build/cli/main_skeleton.py index 66a93cef63..eeda7f428f 100644 --- a/conda_build/cli/main_skeleton.py +++ b/conda_build/cli/main_skeleton.py @@ -13,6 +13,7 @@ from .. import api from ..config import Config +from ..deprecations import deprecated if TYPE_CHECKING: from argparse import ArgumentParser, Namespace @@ -21,6 +22,8 @@ thisdir = os.path.dirname(os.path.abspath(__file__)) logging.basicConfig(level=logging.INFO) +deprecated.module("27.3", "27.9", addendum="Use the `grayskull` tool instead.") + def parse_args(args: Sequence[str] | None) -> tuple[ArgumentParser, Namespace]: from conda.cli.conda_argparse import ArgumentParser @@ -30,6 +33,7 @@ def parse_args(args: Sequence[str] | None) -> tuple[ArgumentParser, Namespace]: description=""" Generates a boilerplate/skeleton recipe, which you can then edit to create a full recipe. Some simple skeleton recipes may not even need edits. +Pending deprecation. Please use the `grayskull` tool instead: https://conda.github.io/grayskull """, epilog=""" Run --help on the subcommands like 'conda skeleton pypi --help' to see the @@ -52,6 +56,7 @@ def parse_args(args: Sequence[str] | None) -> tuple[ArgumentParser, Namespace]: return parser, parser.parse_args(args) +@deprecated("27.3", "27.9", addendum="Use `grayskull` instead.") def execute(args: Sequence[str] | None = None) -> int: parser, parsed = parse_args(args) context.__init__(argparse_args=parsed) diff --git a/conda_build/skeletons/cpan.py b/conda_build/skeletons/cpan.py index 58c806493b..9027feec08 100644 --- a/conda_build/skeletons/cpan.py +++ b/conda_build/skeletons/cpan.py @@ -29,6 +29,7 @@ from .. import environ from ..config import Config, get_or_merge_config +from ..deprecations import deprecated from ..utils import check_call_env, on_linux, on_win from ..variants import get_default_variant from ..version import _parse as parse_version @@ -384,6 +385,7 @@ def get_core_modules_for_this_perl_version(version, cache_dir): ) +@deprecated("27.3", "27.9", addendum="Use `grayskull` instead.") # meta_cpan_url="http://api.metacpan.org", def skeletonize( packages: list[str], diff --git a/conda_build/skeletons/cran.py b/conda_build/skeletons/cran.py index 152c3617ad..3248620ea0 100755 --- a/conda_build/skeletons/cran.py +++ b/conda_build/skeletons/cran.py @@ -33,6 +33,8 @@ import requests import yaml +from ..deprecations import deprecated + # try to import C dumper try: from yaml import CSafeDumper as SafeDumper @@ -864,6 +866,7 @@ def remove_comments(template): return "\n".join(lines_no_comments) +@deprecated("27.3", "27.9", addendum="Use `grayskull` instead.") def skeletonize( in_packages: list[str], output_dir: str = ".", diff --git a/conda_build/skeletons/luarocks.py b/conda_build/skeletons/luarocks.py index 41ec499bad..3f6ec35ad8 100644 --- a/conda_build/skeletons/luarocks.py +++ b/conda_build/skeletons/luarocks.py @@ -17,6 +17,8 @@ from glob import glob from sys import platform as _platform +from ..deprecations import deprecated + INDENT = "\n - " rockspec_parser = """ @@ -226,6 +228,7 @@ def ensure_base_deps(deps): return deps +@deprecated("27.3", "27.9", addendum="Use `grayskull` instead.") def skeletonize( packages: list[str], output_dir: str = ".", diff --git a/conda_build/skeletons/pypi.py b/conda_build/skeletons/pypi.py index ed4aeb5a9d..c514706423 100644 --- a/conda_build/skeletons/pypi.py +++ b/conda_build/skeletons/pypi.py @@ -34,6 +34,7 @@ from requests.packages.urllib3.util.url import parse_url from ..config import Config +from ..deprecations import deprecated from ..environ import create_env from ..license_family import allowed_license_families, guess_license_family from ..metadata import MetaData @@ -258,6 +259,7 @@ def _formating_value(attribute_name, attribute_value): return " " + str(attribute_value) + "\n" +@deprecated("27.3", "27.9", addendum="Use `grayskull` instead.") def skeletonize( packages: list[str], output_dir: str = ".", diff --git a/conda_build/skeletons/rpm.py b/conda_build/skeletons/rpm.py index 0ac211a2f8..8b0f0caf22 100644 --- a/conda_build/skeletons/rpm.py +++ b/conda_build/skeletons/rpm.py @@ -15,6 +15,7 @@ from urllib.request import urlopen from xml.etree import ElementTree as ET +from ..deprecations import deprecated from ..license_family import guess_license_family from ..source import download_to_cache from ..utils import ensure_list @@ -714,6 +715,7 @@ def write_conda_recipe( ) +@deprecated("27.3", "27.9", addendum="Use `grayskull` instead.") def skeletonize( packages: list[str], output_dir: str = ".", diff --git a/news/6023-conda-skeleton-deprecation.md b/news/6023-conda-skeleton-deprecation.md new file mode 100644 index 0000000000..465de877fd --- /dev/null +++ b/news/6023-conda-skeleton-deprecation.md @@ -0,0 +1,19 @@ +### Enhancements + +* + +### Bug fixes + +* + +### Deprecations + +* Mark `conda skeleton` as pending deprecation, with removal scheduled in 27.9 release. Users are encouraged to use [grayskull](https://conda.github.io/grayskull) instead of `conda skeleton`. (#4460 via #6023) + +### Docs + +* + +### Other + +* diff --git a/tests/cli/test_main_skeleton.py b/tests/cli/test_main_skeleton.py index a20e91605a..d779922290 100644 --- a/tests/cli/test_main_skeleton.py +++ b/tests/cli/test_main_skeleton.py @@ -1,5 +1,6 @@ # Copyright (C) 2014 Anaconda, Inc # SPDX-License-Identifier: BSD-3-Clause +import importlib import os import re from pathlib import Path @@ -13,7 +14,8 @@ @pytest.mark.sanity def test_skeleton_pypi(testing_workdir, testing_config): args = ["pypi", "peppercorn"] - main_skeleton.execute(args) + with pytest.deprecated_call(): + main_skeleton.execute(args) assert os.path.isdir("peppercorn") # add setuptools to host dependencies @@ -34,7 +36,8 @@ def test_skeleton_pypi(testing_workdir, testing_config): @pytest.mark.sanity def test_skeleton_pypi_compatible_versions(testing_workdir, testing_config): args = ["pypi", "openshift"] - main_skeleton.execute(args) + with pytest.deprecated_call(): + main_skeleton.execute(args) assert os.path.isdir("openshift") @@ -48,7 +51,8 @@ def test_skeleton_pypi_arguments_work(testing_workdir): https://github.com/conda/conda-build/pull/1384 """ args = ["pypi", "fasttext", "--version=0.9.2", "--pin-numpy"] - main_skeleton.execute(args) + with pytest.deprecated_call(): + main_skeleton.execute(args) assert os.path.isdir("fasttext") # Deliberately bypass metadata reading in conda build to get as @@ -63,10 +67,23 @@ def test_skeleton_pypi_arguments_work(testing_workdir): "--setup-options=--offline", "--extra-specs=extension-helpers", ] - main_skeleton.execute(args) + with pytest.deprecated_call(): + main_skeleton.execute(args) assert os.path.isdir("photutils") # Check that the setup option occurs in bld.bat and build.sh. metadata = api.render("photutils")[0][0] assert "--offline" in metadata.meta["build"]["script"] assert metadata.version() == "1.10.0" + + +def test_skeleton_deprecation_warning(): + """ + Verify that importing conda_build.cli.main_skeleton will raise a deprecation warning. + """ + with pytest.deprecated_call( + match=r"conda_build.cli.main_skeleton is pending deprecation.*grayskull.*", + ): + import conda_build.cli.main_skeleton # noqa F401 + + importlib.reload(conda_build.cli.main_skeleton) diff --git a/tests/test_api_consistency.py b/tests/test_api_consistency.py index 9dac14351c..005803c757 100644 --- a/tests/test_api_consistency.py +++ b/tests/test_api_consistency.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause # This file makes sure that our API has not changed. Doing so can not be accidental. Whenever it # happens, we should bump our major build number, because we may have broken someone. +import inspect import sys from inspect import getfullargspec as getargspec @@ -86,22 +87,25 @@ def test_api_test(): def test_api_list_skeletons(): - argspec = getargspec(api.list_skeletons) - assert argspec.args == [] - assert argspec.defaults is None + sig = inspect.signature(api.list_skeletons) + assert list(sig.parameters) == [] + assert all(p.default is inspect._empty for p in sig.parameters.values()) def test_api_skeletonize(): - argspec = getargspec(api.skeletonize) - assert argspec.args == [ + sig = inspect.signature(api.skeletonize) + assert list(sig.parameters) == [ "packages", "repo", "output_dir", "version", "recursive", "config", + "kwargs", ] - assert argspec.defaults == (".", None, False, None) + assert tuple( + p.default for p in sig.parameters.values() if p.default is not inspect._empty + ) == (".", None, False, None) def test_api_develop(): diff --git a/tests/test_api_skeleton.py b/tests/test_api_skeleton.py index 1e605a75a0..aec14be256 100644 --- a/tests/test_api_skeleton.py +++ b/tests/test_api_skeleton.py @@ -202,13 +202,14 @@ def test_repo( tmp_path: Path, testing_config, ): - api.skeletonize( - package, - repo, - version=version, - output_dir=tmp_path, - config=testing_config, - ) + with pytest.deprecated_call(): + api.skeletonize( + package, + repo, + version=version, + output_dir=tmp_path, + config=testing_config, + ) package_name = f"{prefix}{Path(package).stem}".lower() assert len( @@ -228,13 +229,14 @@ def test_repo( ], ) def test_sympy(package: str, version: str | None, tmp_path: Path, testing_config): - api.skeletonize( - packages=package, - repo="pypi", - version=version, - config=testing_config, - output_dir=tmp_path, - ) + with pytest.deprecated_call(): + api.skeletonize( + packages=package, + repo="pypi", + version=version, + config=testing_config, + output_dir=tmp_path, + ) metadata = api.render(str(tmp_path / "sympy" / "meta.yaml"))[0][0] assert metadata.version() == "1.10" @@ -340,15 +342,16 @@ def test_pypi_with_setup_options(tmp_path: Path, testing_config): # occurs by default. # Test that the setup option is used in constructing the skeleton. - api.skeletonize( - packages="photutils", - repo="pypi", - version="1.10.0", - setup_options="--offline", - config=testing_config, - output_dir=tmp_path, - extra_specs=["extension-helpers"], - ) + with pytest.deprecated_call(): + api.skeletonize( + packages="photutils", + repo="pypi", + version="1.10.0", + setup_options="--offline", + config=testing_config, + output_dir=tmp_path, + extra_specs=["extension-helpers"], + ) # Check that the setup option occurs in bld.bat and build.sh. metadata = api.render(str(tmp_path / "photutils"))[0][0] @@ -358,50 +361,55 @@ def test_pypi_with_setup_options(tmp_path: Path, testing_config): def test_pypi_pin_numpy(tmp_path: Path, testing_config: Config): # The package used here must have a numpy dependence for pin-numpy to have # any effect. - api.skeletonize( - packages="fasttext", - repo="pypi", - version="0.9.2", - config=testing_config, - pin_numpy=True, - output_dir=tmp_path, - ) + with pytest.deprecated_call(): + api.skeletonize( + packages="fasttext", + repo="pypi", + version="0.9.2", + config=testing_config, + pin_numpy=True, + output_dir=tmp_path, + ) assert (tmp_path / "fasttext" / "meta.yaml").read_text().count("numpy x.x") == 2 def test_pypi_version_sorting(tmp_path: Path, testing_config: Config): # The package used here must have a numpy dependence for pin-numpy to have # any effect. - api.skeletonize( - packages="fasttext", - repo="pypi", - config=testing_config, - output_dir=tmp_path, - ) + with pytest.deprecated_call(): + api.skeletonize( + packages="fasttext", + repo="pypi", + config=testing_config, + output_dir=tmp_path, + ) metadata = api.render(str(tmp_path / "fasttext"))[0][0] assert parse_version(metadata.version()) >= parse_version("0.9.2") def test_list_skeletons(): - skeletons = api.list_skeletons() + with pytest.deprecated_call(): + skeletons = api.list_skeletons() assert set(skeletons) == {"pypi", "cran", "cpan", "luarocks", "rpm"} def test_pypi_with_entry_points(tmp_path: Path): # planemo 0.75.29 dropped setup.py - api.skeletonize( - "planemo", - repo="pypi", - version="0.75.28", - python_version="3.12", - output_dir=tmp_path, - ) + with pytest.deprecated_call(): + api.skeletonize( + "planemo", + repo="pypi", + version="0.75.28", + python_version="3.12", + output_dir=tmp_path, + ) assert (tmp_path / "planemo").is_dir() def test_pypi_with_version_arg(tmp_path: Path): # regression test for https://github.com/conda/conda-build/issues/1442 - api.skeletonize("PrettyTable", "pypi", version="0.7.2", output_dir=tmp_path) + with pytest.deprecated_call(): + api.skeletonize("PrettyTable", "pypi", version="0.7.2", output_dir=tmp_path) metadata = api.render(str(tmp_path / "prettytable"))[0][0] assert parse_version(metadata.version()) == parse_version("0.7.2") @@ -418,15 +426,16 @@ def test_pypi_with_extra_specs(tmp_path: Path, testing_config): extra_specs = ["cython", "mpi4py"] if not on_win: extra_specs.append("nomkl") - api.skeletonize( - "bigfile", - "pypi", - extra_specs=extra_specs, - version="0.1.24", - python="3.6", - config=testing_config, - output_dir=tmp_path, - ) + with pytest.deprecated_call(): + api.skeletonize( + "bigfile", + "pypi", + extra_specs=extra_specs, + version="0.1.24", + python="3.6", + config=testing_config, + output_dir=tmp_path, + ) metadata = api.render(str(tmp_path / "bigfile"))[0][0] assert parse_version(metadata.version()) == parse_version("0.1.24") assert any("cython" in req for req in metadata.meta["requirements"]["host"]) @@ -445,22 +454,24 @@ def test_pypi_with_version_inconsistency(tmp_path: Path, testing_config): if not on_win: extra_specs.append("nomkl") testing_config.channel_urls.append("https://repo.anaconda.com/pkgs/free") - api.skeletonize( - "mpi4py_test", - "pypi", - extra_specs=extra_specs, - version="0.0.10", - python="3.6", - config=testing_config, - output_dir=tmp_path, - ) + with pytest.deprecated_call(): + api.skeletonize( + "mpi4py_test", + "pypi", + extra_specs=extra_specs, + version="0.0.10", + python="3.6", + config=testing_config, + output_dir=tmp_path, + ) metadata = api.render(str(tmp_path / "mpi4py_test"))[0][0] assert parse_version(metadata.version()) == parse_version("0.0.10") def test_pypi_with_basic_environment_markers(tmp_path: Path): # regression test for https://github.com/conda/conda-build/issues/1974 - api.skeletonize("coconut", "pypi", version="1.2.2", output_dir=tmp_path) + with pytest.deprecated_call(): + api.skeletonize("coconut", "pypi", version="1.2.2", output_dir=tmp_path) metadata = api.render(tmp_path / "coconut")[0][0] build_reqs = str(metadata.meta["requirements"]["host"]) @@ -473,9 +484,10 @@ def test_pypi_with_basic_environment_markers(tmp_path: Path): def test_setuptools_test_requirements(tmp_path: Path): - api.skeletonize( - packages="hdf5storage", repo="pypi", version="0.1.19", output_dir=tmp_path - ) + with pytest.deprecated_call(): + api.skeletonize( + packages="hdf5storage", repo="pypi", version="0.1.19", output_dir=tmp_path + ) metadata = api.render(str(tmp_path / "hdf5storage"))[0][0] assert metadata.meta["test"]["requires"] == ["nose >=1.0"] @@ -492,7 +504,8 @@ def test_pypi_section_order_preserved(tmp_path: Path): REQUIREMENTS_ORDER, ) - api.skeletonize(packages="sympy", repo="pypi", output_dir=tmp_path) + with pytest.deprecated_call(): + api.skeletonize(packages="sympy", repo="pypi", output_dir=tmp_path) # Since we want to check the order of items in the recipe (not whether # the metadata values themselves are sensible), read the file as (ordered) # yaml, and check the order. @@ -532,12 +545,13 @@ def test_pypi_section_order_preserved(tmp_path: Path): def test_build_sh_shellcheck_clean( package: str, repo: str, tmp_path: Path, testing_config ): - api.skeletonize( - packages=package, - repo=repo, - output_dir=tmp_path, - config=testing_config, - ) + with pytest.deprecated_call(): + api.skeletonize( + packages=package, + repo=repo, + output_dir=tmp_path, + config=testing_config, + ) build_sh = next( Path(root, filename) diff --git a/tests/test_api_skeleton_cpan.py b/tests/test_api_skeleton_cpan.py index 5945158023..a32fa02298 100644 --- a/tests/test_api_skeleton_cpan.py +++ b/tests/test_api_skeleton_cpan.py @@ -16,7 +16,10 @@ def test_xs_needs_c_compiler(testing_config): """Perl packages with XS files need a C compiler""" # This uses Sub::Identify=0.14 since it includes no .c files but a .xs file. - api.skeletonize("Sub::Identify", version="0.14", repo="cpan", config=testing_config) + with pytest.deprecated_call(): + api.skeletonize( + "Sub::Identify", version="0.14", repo="cpan", config=testing_config + ) metadata = api.render( "perl-sub-identify/0.14", finalize=False, bypass_env_check=True )[0][0] diff --git a/tests/test_api_skeleton_cran.py b/tests/test_api_skeleton_cran.py index 6344e95c1e..11afe56403 100644 --- a/tests/test_api_skeleton_cran.py +++ b/tests/test_api_skeleton_cran.py @@ -38,9 +38,10 @@ def test_cran_license( tmp_path: Path, testing_config, ): - api.skeletonize( - packages=package, repo="cran", output_dir=tmp_path, config=testing_config - ) + with pytest.deprecated_call(): + api.skeletonize( + packages=package, repo="cran", output_dir=tmp_path, config=testing_config + ) metadata = api.render(str(tmp_path / package / "meta.yaml"))[0][0] assert metadata.get_value("about/license") == license_id @@ -60,9 +61,10 @@ def test_cran_license( ) @pytest.mark.flaky(rerun=5, reruns_delay=2) def test_cran_os_type(package: str, skip_text: str, tmp_path: Path, testing_config): - api.skeletonize( - packages=package, repo="cran", output_dir=tmp_path, config=testing_config - ) + with pytest.deprecated_call(): + api.skeletonize( + packages=package, repo="cran", output_dir=tmp_path, config=testing_config + ) assert skip_text in (tmp_path / f"r-{package.lower()}" / "meta.yaml").read_text() @@ -79,13 +81,14 @@ def test_cran_no_comments(tmp_path: Path, testing_config): assert build_sh_comment in CRAN_BUILD_SH_SOURCE assert build_sh_shebang in CRAN_BUILD_SH_SOURCE - api.skeletonize( - packages=package, - repo="cran", - output_dir=tmp_path, - config=testing_config, - no_comments=True, - ) + with pytest.deprecated_call(): + api.skeletonize( + packages=package, + repo="cran", + output_dir=tmp_path, + config=testing_config, + no_comments=True, + ) # Check that comments got removed meta_yaml_text = (tmp_path / f"r-{package.lower()}" / "meta.yaml").read_text()