Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions cyclonedx_py/_internal/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from os import getcwd, name as os_name
from os.path import exists, isdir, join
from subprocess import run # nosec
from sys import path as sys_path
from sys import executable, path as sys_path
from textwrap import dedent
from typing import TYPE_CHECKING, Any, Optional

Expand Down Expand Up @@ -115,14 +115,14 @@ def make_argument_parser(**kwargs: Any) -> 'ArgumentParser':
action='store_false',
dest='import_site',
help='Do not implicitly import site during Python path detection.\n'
'Prevents evaluation of `*.pth` files, but may lead to incomplete component detection.')
'Prevents evaluation of `*.pth` files, but may lead to incomplete component detection.\n'
'Without a target `<python>`, the current interpreter is probed this way.')
p.add_argument('-E', # mimic `python -E`
action='store_false',
dest='honor_py_env_vars',
help='Run the target interpreter with `-E` (ignore PYTHON* environment\n'
'variables such as PYTHONPATH) when detecting its path.\n'
'Only applies when a target `<python>` is given; has no effect when\n'
'analyzing the current environment.\n'
'Without a target `<python>`, the current interpreter is probed this way.\n'
'Interpreters that require PYTHONHOME to start may fail with this flag.')
p.add_argument('--gather-license-texts',
action='store_true',
Expand Down Expand Up @@ -171,6 +171,10 @@ def __call__(self, *, # type:ignore[override]
path: list[str]
if python:
path = self.__path4python(python, import_site, honor_py_env_vars)
elif not import_site or not honor_py_env_vars:
# the already-running interpreter imported site and honored PYTHON* vars at startup;
# probe a fresh one with the requested flags
path = self.__path4python(executable, import_site, honor_py_env_vars)
else:
path = sys_path.copy()
if path[0] in ('', getcwd()):
Expand Down
9 changes: 5 additions & 4 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ The full documentation can be issued by running with ``environment --help``:
-h, --help show this help message and exit
-S Do not implicitly import site during Python path detection.
Prevents evaluation of `*.pth` files, but may lead to incomplete component detection.
Without a target `<python>`, the current interpreter is probed this way.
-E Run the target interpreter with `-E` (ignore PYTHON*
environment variables such as PYTHONPATH) when
detecting its path. Only applies when a target
`<python>` is given; has no effect when analyzing the
current environment. Interpreters that require
PYTHONHOME to start may fail with this flag.
detecting its path. Without a target `<python>`, the
current interpreter is probed this way. Interpreters
that require PYTHONHOME to start may fail with this
flag.
--gather-license-texts
Enable license text gathering.
--pyproject <file> Path to the root component's `pyproject.toml` file.
Expand Down
57 changes: 56 additions & 1 deletion tests/integration/test_cli_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from os import environ, mkdir, name as os_name
from os.path import basename, dirname, join
from subprocess import run # nosec:B404
from sys import executable, stderr
from sys import executable, path as sys_path, stderr
from tempfile import TemporaryDirectory
from typing import Any
from unittest import TestCase, skipIf
Expand Down Expand Up @@ -194,6 +194,61 @@ def test_no_python_env_ignores_parent_pythonpath(self) -> None:
self.assertIn(foreign_name, names1)
self.assertNotIn(foreign_name, names2)

def test_no_python_given_env_ignores_parent_pythonpath(self) -> None:
"""Follow-up for #1045: with -E and no target, the current interpreter is
probed via `python -E` instead of reusing the already-imported sys.path."""
foreign_name = 'leakypkg'
with TemporaryDirectory() as foreign_root:
dist_info = join(foreign_root, f'{foreign_name}-9.9.9.dist-info')
mkdir(dist_info)
with open(join(dist_info, 'METADATA'), 'w', encoding='utf8') as fh:
fh.write('Metadata-Version: 2.1\n'
f'Name: {foreign_name}\n'
'Version: 9.9.9\n')
# simulate a parent PYTHONPATH that the running interpreter already honored
leaky_path = [foreign_root, *sys_path]
with patch('cyclonedx_py._internal.environment.sys_path', leaky_path), \
patch.dict(environ, {'PYTHONPATH': foreign_root}):
res, sbom1, err = run_cli(
'environment', '--sv', '1.6', '--of', 'JSON', '--no-validate', '-o=-')
self.assertEqual(0, res, err)
res, sbom2, err = run_cli(
'environment', '--sv', '1.6', '--of', 'JSON', '--no-validate', '-o=-',
'-E') # the important part
self.assertEqual(0, res, err)
names1 = {c['name'] for c in json_loads(sbom1).get('components') or ()}
names2 = {c['name'] for c in json_loads(sbom2).get('components') or ()}
self.assertIn(foreign_name, names1)
self.assertNotIn(foreign_name, names2)

def test_no_python_given_site_flags_probe_current_interpreter(self) -> None:
"""Follow-up for #1045: with -S and/or -E and no target, the current interpreter
is probed with the corresponding flags instead of reusing the already-imported sys.path."""
sentinel = ['/sentinel/not/probed']
for flags, expected in (
(('-S',), ['-S']),
(('-E',), ['-E']),
(('-S', '-E'), ['-E', '-S']),
):
with self.subTest(flags=flags), \
patch('cyclonedx_py._internal.environment.sys_path', sentinel), \
patch('cyclonedx_py._internal.environment.run', wraps=run) as run_spy:
res, sbom, err = run_cli(
'environment', '--sv', '1.6', '--of', 'JSON', '--no-validate', '-o=-',
*flags)
self.assertEqual(0, res, err)
self.assertEqual(1, run_spy.call_count)
cmd = run_spy.call_args.args[0]
self.assertEqual(executable, cmd[0])
self.assertEqual(expected, cmd[1:-2])

def test_no_python_given_no_flags_reuses_sys_path(self) -> None:
with patch('cyclonedx_py._internal.environment.run') as run_mock:
res, sbom, err = run_cli(
'environment', '--sv', '1.6', '--of', 'JSON', '--no-validate', '-o=-')
self.assertEqual(0, res, err)
run_mock.assert_not_called()

def test_with_current_python(self) -> None:
sv = SchemaVersion.V1_6
of = random.choice((OutputFormat.XML, OutputFormat.JSON)) # nosec B311
Expand Down
Loading