diff --git a/news/6156.feature.rst b/news/6156.feature.rst new file mode 100644 index 000000000..525c98d88 --- /dev/null +++ b/news/6156.feature.rst @@ -0,0 +1 @@ +The ``pipenv requirements`` subcommand now supports the ``--from-pipfile`` flag. When this flag is used, the requirements file will only include the packages explicitly listed in the Pipfile, excluding any sub-packages. diff --git a/pipenv/cli/command.py b/pipenv/cli/command.py index a755346d5..a867aee02 100644 --- a/pipenv/cli/command.py +++ b/pipenv/cli/command.py @@ -744,9 +744,21 @@ def verify(state): default="", help="Only add requirement of the specified categories.", ) +@option( + "--from-pipfile", + is_flag=True, + default=False, + help="Only include dependencies from Pipfile.", +) @pass_state def requirements( - state, dev=False, dev_only=False, hash=False, exclude_markers=False, categories="" + state, + dev=False, + dev_only=False, + hash=False, + exclude_markers=False, + categories="", + from_pipfile=False, ): from pipenv.routines.requirements import generate_requirements @@ -757,6 +769,7 @@ def requirements( include_hashes=hash, include_markers=not exclude_markers, categories=categories, + from_pipfile=from_pipfile, ) diff --git a/pipenv/routines/requirements.py b/pipenv/routines/requirements.py index 32c5d6705..e25c8d0ad 100644 --- a/pipenv/routines/requirements.py +++ b/pipenv/routines/requirements.py @@ -13,8 +13,10 @@ def generate_requirements( include_hashes=False, include_markers=True, categories="", + from_pipfile=False, ): lockfile = project.load_lockfile(expand_env_vars=False) + pipfile_root_package_names = project.pipfile_package_names["combined"] for i, package_index in enumerate(lockfile["_meta"]["sources"]): prefix = "-i" if i == 0 else "--extra-index-url" @@ -26,12 +28,31 @@ def generate_requirements( if categories_list: for category in categories_list: category = get_lockfile_section_using_pipfile_category(category.strip()) - deps.update(lockfile.get(category, {})) + category_deps = lockfile.get(category, {}) + if from_pipfile: + category_deps = { + k: v + for k, v in category_deps.items() + if k in pipfile_root_package_names + } + deps.update(category_deps) else: if dev or dev_only: - deps.update(lockfile["develop"]) + dev_deps = lockfile["develop"] + if from_pipfile: + dev_deps = { + k: v for k, v in dev_deps.items() if k in pipfile_root_package_names + } + deps.update(dev_deps) if not dev_only: - deps.update(lockfile["default"]) + default_deps = lockfile["default"] + if from_pipfile: + default_deps = { + k: v + for k, v in default_deps.items() + if k in pipfile_root_package_names + } + deps.update(default_deps) pip_installable_lines = requirements_from_lockfile( deps, include_hashes=include_hashes, include_markers=include_markers diff --git a/tests/integration/test_requirements.py b/tests/integration/test_requirements.py index ed0cdc5f2..4a64a0bbb 100644 --- a/tests/integration/test_requirements.py +++ b/tests/integration/test_requirements.py @@ -115,6 +115,54 @@ def test_requirements_generates_requirements_from_lockfile_from_categories(pipen assert f'{doc_packages[0]}=={doc_packages[1]}' in d.stdout +@pytest.mark.requirements +def test_requirements_generates_requirements_with_from_pipfile(pipenv_instance_pypi): + with pipenv_instance_pypi() as p: + packages = ('requests', '2.31.0') + sub_packages = ('urllib3', '2.2.1') # subpackages not explicitly written in Pipfile. + dev_packages = ('flask', '0.12.2') + + with open(p.pipfile_path, 'w') as f: + contents = f""" + [packages] + {packages[0]} = "=={packages[1]}" + [dev-packages] + {dev_packages[0]} = "=={dev_packages[1]}" + """.strip() + f.write(contents) + p.pipenv('lock') + + c = p.pipenv('requirements --from-pipfile') + assert c.returncode == 0 + assert f'{packages[0]}=={packages[1]}' in c.stdout + assert f'{sub_packages[0]}=={sub_packages[1]}' not in c.stdout + assert f'{dev_packages[0]}=={dev_packages[1]}' not in c.stdout + + d = p.pipenv('requirements --dev --from-pipfile') + assert d.returncode == 0 + assert f'{packages[0]}=={packages[1]}' in d.stdout + assert f'{sub_packages[0]}=={sub_packages[1]}' not in d.stdout + assert f'{dev_packages[0]}=={dev_packages[1]}' in d.stdout + + e = p.pipenv('requirements --dev-only --from-pipfile') + assert e.returncode == 0 + assert f'{packages[0]}=={packages[1]}' not in e.stdout + assert f'{sub_packages[0]}=={sub_packages[1]}' not in e.stdout + assert f'{dev_packages[0]}=={dev_packages[1]}' in e.stdout + + f = p.pipenv('requirements --categories=dev-packages --from-pipfile') + assert f.returncode == 0 + assert f'{packages[0]}=={packages[1]}' not in f.stdout + assert f'{sub_packages[0]}=={sub_packages[1]}' not in f.stdout + assert f'{dev_packages[0]}=={dev_packages[1]}' in f.stdout + + g = p.pipenv('requirements --categories=packages,dev-packages --from-pipfile') + assert g.returncode == 0 + assert f'{packages[0]}=={packages[1]}' in g.stdout + assert f'{sub_packages[0]}=={sub_packages[1]}' not in g.stdout + assert f'{dev_packages[0]}=={dev_packages[1]}' in g.stdout + + @pytest.mark.requirements def test_requirements_with_git_requirements(pipenv_instance_pypi): req_hash = '3264a0046e1aa3c0a813335286ebdbc651f58b13'