From 8a06b34b7f1b05b65ec5fb83eb3169839f113a46 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 5 Apr 2025 14:04:39 +0200 Subject: [PATCH 1/3] feat: deprecate CLI switch `--outfile`; use new `--output-file` instead Signed-off-by: Jan Kowalleck --- cyclonedx_py/_internal/cli.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/cyclonedx_py/_internal/cli.py b/cyclonedx_py/_internal/cli.py index d9ad4c95..ba65f18c 100644 --- a/cyclonedx_py/_internal/cli.py +++ b/cyclonedx_py/_internal/cli.py @@ -49,6 +49,7 @@ else: BooleanOptionalAction = None +OPTION_OUTPUT_STDOUT='-' class Command: @classmethod @@ -74,14 +75,20 @@ def make_argument_parser(cls, sco: ArgumentParser, **kwargs: Any) -> ArgumentPar action='store_true', dest='short_purls', default=False) - op.add_argument('-o', '--outfile', + op.add_argument('--outfile', # DEPRECATED + metavar='', + help='DEPRECATED alias for "--output-file".', + type=FileType('wt', encoding='utf8'), + dest='output_file', + default=OPTION_OUTPUT_STDOUT) + op.add_argument('-o', '--output-file', metavar='', help='Output file path for your SBOM' - ' (set to "-" to output to )' + f' (set to "{OPTION_OUTPUT_STDOUT}" to output to )' ' (default: %(default)s)', type=FileType('wt', encoding='utf8'), - dest='outfile', - default='-') + dest='output_file', + default=OPTION_OUTPUT_STDOUT) op.add_argument('--schema-version', # DEPRECATED metavar='', help='DEPRECATED alias for option "--spec-version".', @@ -159,7 +166,7 @@ def make_argument_parser(cls, sco: ArgumentParser, **kwargs: Any) -> ArgumentPar # the arg keywords from __init__() 'logger', 'short_purls', 'output_format', 'spec_version', 'output_reproducible', 'should_validate', # the arg keywords from __call__() - 'outfile' + 'output_file' } @classmethod @@ -232,10 +239,10 @@ def _validate(self, output: str) -> bool: self._logger.debug('result is schema-valid') return True - def _write(self, output: str, outfile: TextIO) -> int: - self._logger.info('Writing to: %s', outfile.name) - written = outfile.write(output) - self._logger.debug('Wrote %i bytes to %s', written, outfile.name) + def _write(self, output: str, output_file: TextIO) -> int: + self._logger.info('Writing to: %s', output_file.name) + written = output_file.write(output) + self._logger.debug('Wrote %i bytes to %s', written, output_file.name) return written def _make_output(self, bom: 'Bom') -> str: @@ -259,14 +266,14 @@ def _make_bom(self, **kwargs: Any) -> 'Bom': return self._bbc(**self._clean_kwargs(kwargs)) def __call__(self, - outfile: TextIO, + output_file: TextIO, **kwargs: Any) -> None: bom = self._make_bom(**kwargs) self._shorten_purls(bom) output = self._make_output(bom) del bom self._validate(output) - self._write(output, outfile) + self._write(output, output_file) def run(*, argv: Optional[Sequence[str]] = None, **kwargs: Any) -> Union[int, NoReturn]: From 55bec73d29e867d519b540c79cd6e9a74700dbf8 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 5 Apr 2025 14:10:46 +0200 Subject: [PATCH 2/3] wip Signed-off-by: Jan Kowalleck --- docs/usage.rst | 14 +++++++++----- tests/integration/test_cli_environment.py | 18 +++++++++--------- tests/integration/test_cli_pipenv.py | 12 ++++++------ tests/integration/test_cli_poetry.py | 14 +++++++------- tests/integration/test_cli_requirements.py | 10 +++++----- tests/unit/test_cli.py | 6 +++--- 6 files changed, 39 insertions(+), 35 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 61e0f920..a250646b 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -35,7 +35,7 @@ Example usage: save SBOM in CycloneDX 1.6 XML format, generated from current pyt .. code-block:: shell - cyclonedx-py environment --outfile my-sbom.xml --spec-version 1.6 --output-format XML + cyclonedx-py environment --spec-version 1.6 --output-format XML --output-file my-sbom.xml For Python (virtual) environment @@ -81,7 +81,8 @@ The full documentation can be issued by running with ``environment --help``: (default: application) --short-PURLs Omit all qualifiers from PackageURLs. This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings. - -o , --outfile + --outfile DEPRECATED alias for "--output-file". + -o , --output-file Output file path for your SBOM (set to "-" to output to STDOUT) (default: -) @@ -254,7 +255,8 @@ The full documentation can be issued by running with ``pipenv --help``: (default: application) --short-PURLs Omit all qualifiers from PackageURLs. This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings. - -o , --outfile + --outfile DEPRECATED alias for "--output-file". + -o , --output-file Output file path for your SBOM (set to "-" to output to ) (default: -) @@ -331,7 +333,8 @@ The full documentation can be issued by running with ``poetry --help``: (default: application) --short-PURLs Omit all qualifiers from PackageURLs. This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings. - -o , --outfile + --outfile DEPRECATED alias for "--output-file". + -o , --output-file Output file path for your SBOM (set to "-" to output to ) (default: -) @@ -404,7 +407,8 @@ The full documentation can be issued by running with ``requirements --help``: (default: application) --short-PURLs Omit all qualifiers from PackageURLs. This causes information loss in trade-off shorter PURLs, which might improve ingesting these strings. - -o , --outfile + --outfile DEPRECATED alias for "--output-file". + -o , --output-file Output file path for your SBOM (set to "-" to output to ) (default: -) diff --git a/tests/integration/test_cli_environment.py b/tests/integration/test_cli_environment.py index 2f3af3bd..09e8b13e 100644 --- a/tests/integration/test_cli_environment.py +++ b/tests/integration/test_cli_environment.py @@ -79,7 +79,7 @@ def test_fails_with_python_not_found(self, wrong_python: str, expected_error: st '-vvv', f'--sv={sv.to_version()}', f'--of={of.name}', - '--outfile=-', + '-o=-', wrong_python) self.assertNotEqual(0, res, err) self.assertIn(expected_error, err) @@ -96,7 +96,7 @@ def test_fails_with_python_unexpected(self, wrong_python: str, expected_error: s '-vvv', f'--sv={sv.to_version()}', f'--of={of.name}', - '--outfile=-', + '-o=-', wrong_python) self.assertNotEqual(0, res, err) self.assertIn(expected_error, err) @@ -108,7 +108,7 @@ def test_with_pyproject_not_found(self) -> None: '-vvv', '--sv', sv.to_version(), '--of', of.name, - '--outfile=-', + '-o=-', '--pyproject=something-that-must-not-exist.testing', projectdir ) @@ -124,7 +124,7 @@ def test_with_current_python(self) -> None: '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', # no project dir -> search in current python ) self.assertEqual(0, res, err) @@ -134,7 +134,7 @@ def test_with_current_python(self) -> None: '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', executable # explicitly current python ) self.assertEqual(0, res, err) @@ -151,7 +151,7 @@ def test_plain_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputF '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--pyproject', join(projectdir, 'pyproject.toml'), join(projectdir, '.venv')) self.assertEqual(0, res, err) @@ -165,7 +165,7 @@ def test_pep639_as_expected(self, projectdir: str, sv: SchemaVersion, of: Output '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--pyproject', join(projectdir, 'pyproject.toml'), '--PEP-639', join(projectdir, '.venv')) @@ -180,7 +180,7 @@ def test_pep639_texts_as_expected(self, projectdir: str, sv: SchemaVersion, of: '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--pyproject', join(projectdir, 'pyproject.toml'), '--PEP-639', '--gather-license-texts', @@ -196,7 +196,7 @@ def test_texts_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputF '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--pyproject', join(projectdir, 'pyproject.toml'), '--gather-license-texts', join(projectdir, '.venv')) diff --git a/tests/integration/test_cli_pipenv.py b/tests/integration/test_cli_pipenv.py index f45d2ec7..317bff8a 100644 --- a/tests/integration/test_cli_pipenv.py +++ b/tests/integration/test_cli_pipenv.py @@ -56,7 +56,7 @@ def test_fails_with_dir_not_found(self) -> None: '-vvv', '--sv', sv.to_version(), '--of', of.name, - '--outfile=-', + '-o=-', 'something-that-must-not-exist.testing') self.assertNotEqual(0, res, err) self.assertIn('Could not open lock file: something-that-must-not-exist.testing', err) @@ -68,7 +68,7 @@ def test_with_pyproject_not_found(self) -> None: '-vvv', '--sv', sv.to_version(), '--of', of.name, - '--outfile=-', + '-o=-', '--pyproject=something-that-must-not-exist.testing', projectdir) self.assertNotEqual(0, res, err) @@ -82,7 +82,7 @@ def test_plain_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputF '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--pyproject', join(projectdir, 'pyproject.toml'), projectdir) self.assertEqual(0, res, err) @@ -96,7 +96,7 @@ def test_with_categories_as_expected(self, projectdir: str, sv: SchemaVersion, o '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--categories', 'categoryB,groupA packages,dev-packages', projectdir) self.assertEqual(0, res, err) @@ -110,7 +110,7 @@ def test_with_dev_as_expected(self, projectdir: str, sv: SchemaVersion, of: Outp '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--dev', projectdir) self.assertEqual(0, res, err) @@ -124,7 +124,7 @@ def test_with_pypi_mirror_as_expected(self, projectdir: str, sv: SchemaVersion, '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--pypi-mirror', 'https://user:password@pypy-mirror.testing.acme.org/simple/', projectdir) self.assertEqual(0, res, err) diff --git a/tests/integration/test_cli_poetry.py b/tests/integration/test_cli_poetry.py index 1375ccfa..02f19675 100644 --- a/tests/integration/test_cli_poetry.py +++ b/tests/integration/test_cli_poetry.py @@ -56,7 +56,7 @@ def test_fails_with_dir_not_found(self) -> None: '-vvv', '--sv', sv.to_version(), '--of', of.name, - '--outfile=-', + '-o=-', 'something-that-must-not-exist.testing') self.assertNotEqual(0, res, err) self.assertIn('Could not open pyproject file: something-that-must-not-exist.testing', err) @@ -109,7 +109,7 @@ def test_plain_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputF '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', projectdir) self.assertEqual(0, res, err) self.assertEqualSnapshot(out, 'plain', projectdir, sv, of) @@ -124,7 +124,7 @@ def test_with_groups_as_expected(self, projectdir: str, sv: SchemaVersion, of: O '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', projectdir) self.assertEqual(0, res, err) self.assertEqualSnapshot(out, 'some-groups', projectdir, sv, of) @@ -138,7 +138,7 @@ def test_only_groups_as_expected(self, projectdir: str, sv: SchemaVersion, of: O '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', projectdir) self.assertEqual(0, res, err) self.assertEqualSnapshot(out, 'only-groups', projectdir, sv, of) @@ -153,7 +153,7 @@ def test_nodev_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputF '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', projectdir) self.assertEqual(0, res, err) self.assertEqualSnapshot(out, 'no-dev', projectdir, sv, of) @@ -167,7 +167,7 @@ def test_with_extras_as_expected(self, projectdir: str, sv: SchemaVersion, of: O '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', projectdir) self.assertEqual(0, res, err) self.assertEqualSnapshot(out, 'some-extras', projectdir, sv, of) @@ -181,7 +181,7 @@ def test_with_all_extras_as_expected(self, projectdir: str, sv: SchemaVersion, o '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', projectdir) self.assertEqual(0, res, err) self.assertEqualSnapshot(out, 'all-extras', projectdir, sv, of) diff --git a/tests/integration/test_cli_requirements.py b/tests/integration/test_cli_requirements.py index ea9bbe25..454483d7 100644 --- a/tests/integration/test_cli_requirements.py +++ b/tests/integration/test_cli_requirements.py @@ -67,7 +67,7 @@ def test_with_file_not_found(self) -> None: '-vvv', '--sv', sv.to_version(), '--of', of.name, - '--outfile=-', + '-o=-', 'something-that-must-not-exist.testing') self.assertNotEqual(0, res, err) self.assertIn('Could not open requirements file: something-that-must-not-exist.testing', err) @@ -79,7 +79,7 @@ def test_with_pyproject_not_found(self) -> None: '-vvv', '--sv', sv.to_version(), '--of', of.name, - '--outfile=-', + '-o=-', '--pyproject=something-that-must-not-exist.testing', infile ) @@ -94,7 +94,7 @@ def test_with_file_as_expected(self, infile: str, sv: SchemaVersion, of: OutputF '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--pyproject', pyproject_file, infile) self.assertEqual(0, res, err) @@ -109,7 +109,7 @@ def test_with_stream_as_expected(self, infile: str, sv: SchemaVersion, of: Outpu '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', # no pyproject for this case '-', inp=inp) @@ -127,7 +127,7 @@ def test_with_index_auth(self, infile: str, sv: SchemaVersion, of: OutputFormat) '--sv', sv.to_version(), '--of', of.name, '--output-reproducible', - '--outfile=-', + '-o=-', '--pyproject', pyproject_file, infile) self.assertEqual(0, res, err) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index ef09801d..a1d62c3e 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -82,7 +82,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> BomBuilder: output_reproducible=True, _bbc=MyBBC ) - command(outfile=outs) + command(output_file=outs) out = outs.getvalue() @@ -109,7 +109,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> BomBuilder: command._make_output = Mock(return_value=r'["invalid to CDX schema"]') with self.assertRaisesRegex(ValueError, 'is schema-invalid'): - command(outfile=outs) + command(output_file=outs) def test_validation_skip_with_invalid(self) -> None: class MyBBC(BomBuilder): @@ -131,7 +131,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> BomBuilder: ) command._make_output = Mock(return_value=r'["invalid to CDX schema"]') - command(outfile=outs) + command(output_file=outs) log = logs.getvalue() out = outs.getvalue() From 1731dc1d44ac555942c9fafd1f7d252368076260 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Sat, 5 Apr 2025 14:12:50 +0200 Subject: [PATCH 3/3] tidy Signed-off-by: Jan Kowalleck --- cyclonedx_py/_internal/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cyclonedx_py/_internal/cli.py b/cyclonedx_py/_internal/cli.py index ba65f18c..135a65b1 100644 --- a/cyclonedx_py/_internal/cli.py +++ b/cyclonedx_py/_internal/cli.py @@ -49,7 +49,8 @@ else: BooleanOptionalAction = None -OPTION_OUTPUT_STDOUT='-' +OPTION_OUTPUT_STDOUT = '-' + class Command: @classmethod