diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py index 94ab2358f..5c9fed0a1 100755 --- a/piptools/scripts/compile.py +++ b/piptools/scripts/compile.py @@ -7,8 +7,12 @@ import click from click.utils import safecall from pip._internal.commands import create_command -from pip._internal.req.constructors import install_req_from_line +from pip._internal.req.constructors import ( + install_req_from_line, + install_req_from_req_string, +) from pip._internal.utils.misc import redact_auth_from_url +from pip._vendor.pep517 import meta from .._compat import parse_requirements from ..cache import DependencyCache @@ -330,21 +334,14 @@ def cli( constraints = [] for src_file in src_files: is_setup_file = os.path.basename(src_file) == "setup.py" - if is_setup_file or src_file == "-": + if src_file == "-": # pip requires filenames and not files. Since we want to support # piping from stdin, we need to briefly save the input from stdin # to a temporary file and have pip read that. also used for # reading requirements from install_requires in setup.py. tmpfile = tempfile.NamedTemporaryFile(mode="wt", delete=False) - if is_setup_file: - from distutils.core import run_setup - - dist = run_setup(src_file) - tmpfile.write("\n".join(dist.install_requires)) - comes_from = f"{dist.get_name()} ({src_file})" - else: - tmpfile.write(sys.stdin.read()) - comes_from = "-r -" + tmpfile.write(sys.stdin.read()) + comes_from = "-r -" tmpfile.flush() reqs = list( parse_requirements( @@ -357,6 +354,15 @@ def cli( for req in reqs: req.comes_from = comes_from constraints.extend(reqs) + elif is_setup_file: + dist = meta.load(os.path.dirname(os.path.abspath(src_file))) + comes_from = f"{dist.metadata.get_all('Name')[0]} ({src_file})" + constraints.extend( + [ + install_req_from_req_string(req, comes_from=comes_from) + for req in dist.requires or [] + ] + ) else: constraints.extend( parse_requirements( @@ -378,7 +384,13 @@ def cli( # Filter out pip environment markers which do not match (PEP496) constraints = [ - req for req in constraints if req.markers is None or req.markers.evaluate() + req + for req in constraints + if req.markers is None + # We explicitly set extra=None to filter out optional requirements + # since evaluating an extra marker with no environment raises UndefinedEnvironmentName + # (see https://packaging.pypa.io/en/latest/markers.html#usage) + or req.markers.evaluate({"extra": None}) ] log.debug("Using indexes:") diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py index f893ab737..256064071 100644 --- a/tests/test_cli_compile.py +++ b/tests/test_cli_compile.py @@ -38,7 +38,17 @@ def test_command_line_overrides_pip_conf(pip_with_index_conf, runner): assert "Using indexes:\n http://override.com" in out.stderr -def test_command_line_setuptools_read(pip_conf, runner): +@pytest.mark.network +def test_command_line_setuptools_read(runner, make_pip_conf): + make_pip_conf( + dedent( + """\ + [global] + disable-pip-version-check = True + """ + ) + ) + with open("setup.py", "w") as package: package.write( dedent( @@ -51,16 +61,13 @@ def test_command_line_setuptools_read(pip_conf, runner): """ ) ) - out = runner.invoke(cli, ["--no-emit-find-links"]) + out = runner.invoke( + cli, + ["--no-header", "--no-emit-find-links", "--find-links", MINIMAL_WHEELS_PATH], + ) assert out.stderr == dedent( """\ - # - # This file is autogenerated by pip-compile - # To update, run: - # - # pip-compile --no-emit-find-links - # small-fake-a==0.1 # via fake-setuptools-a (setup.py) """ @@ -70,6 +77,7 @@ def test_command_line_setuptools_read(pip_conf, runner): assert os.path.exists("requirements.txt") +@pytest.mark.network @pytest.mark.parametrize( ("options", "expected_output_file"), ( @@ -85,12 +93,11 @@ def test_command_line_setuptools_read(pip_conf, runner): (["setup.py", "--output-file", "output.txt"], "output.txt"), ), ) -def test_command_line_setuptools_output_file( - pip_conf, runner, options, expected_output_file -): +def test_command_line_setuptools_output_file(runner, options, expected_output_file): """ Test the output files for setup.py as a requirement file. """ + with open("setup.py", "w") as package: package.write( dedent( @@ -106,7 +113,8 @@ def test_command_line_setuptools_output_file( assert os.path.exists(expected_output_file) -def test_command_line_setuptools_nested_output_file(pip_conf, tmpdir, runner): +@pytest.mark.network +def test_command_line_setuptools_nested_output_file(tmpdir, runner): """ Test the output file for setup.py in nested folder as a requirement file. """ @@ -127,6 +135,43 @@ def test_command_line_setuptools_nested_output_file(pip_conf, tmpdir, runner): assert (proj_dir / "requirements.txt").exists() +@pytest.mark.network +def test_setuptools_preserves_environment_markers( + runner, make_package, make_wheel, make_pip_conf, tmpdir +): + make_pip_conf( + dedent( + """\ + [global] + disable-pip-version-check = True + """ + ) + ) + + dists_dir = tmpdir / "dists" + + foo_dir = make_package(name="foo", version="1.0") + make_wheel(foo_dir, dists_dir) + + bar_dir = make_package( + name="bar", version="2.0", install_requires=['foo ; python_version >= "1"'] + ) + out = runner.invoke( + cli, + [ + str(bar_dir / "setup.py"), + "--no-header", + "--no-annotate", + "--no-emit-find-links", + "--find-links", + str(dists_dir), + ], + ) + + assert out.exit_code == 0, out.stderr + assert out.stderr == 'foo==1.0 ; python_version >= "1"\n' + + def test_find_links_option(runner): with open("requirements.in", "w") as req_in: req_in.write("-f ./libs3")