diff --git a/README.md b/README.md index d467ea3b7..e4a12ad49 100644 --- a/README.md +++ b/README.md @@ -58,34 +58,22 @@ This is the result of an initial GSoC contribution by @[ah450](https://github.co ## Command-line usage: -1. **PARSER** (for parsing any format): +1. **PARSING/VALIDATING** (for parsing any format): -* Use `pyspdxtools_parser --file ` where `` is the location of the file. -If you are using a source distribution, try running: `pyspdxtools_parser --file tests/data/formats/SPDXRdfExample.rdf`. +* Use `pyspdxtools -i ` where `` is the location of the file. + If you are using a source distribution, try running: `pyspdxtools -i tests/data/formats/SPDXJSONExample-v2.3.spdx.json`. -* Or you can use `pyspdxtools_parser` only, and it will automatically prompt/ask for `filename`. +* Or you can use `pyspdxtools` only, and it will automatically prompt/ask for the `input file path`. -* For help use `pyspdxtools_parser --help` +2. **CONVERTING** (for converting one format to another): +* Use `pyspdxtools -i -o ` where `` is the location of the file to be converted + and `` is the location of the output file. The output format is inferred automatically from the file ending. + If you are using a source distribution, try running : `pyspdxtools -i tests/data/formats/SPDXJSONExample-v2.3.spdx.json -o output.tag` -2. **CONVERTOR** (for converting one format to another): - -* If I/O formats are known: - - * Use `pyspdxtools_convertor --infile/-i --outfile/-o ` where `` is the location of the file to be converted - and `` is the location of the output file. - If you are using a source distribution, try running : `pyspdxtools_convertor --infile tests/data/formats/SPDXRdfExample.rdf --outfile output.json` - -* If I/O formats are not known: - - * Use `pyspdxtools_convertor --from/-f --to/-t ` where `` is the manually entered format of the input file - and `` is the manually entered format of the output file. - If you are using a source distribution, try running : `pyspdxtools_convertor --from tag tests/data/formats/SPDXTagExample.in --to yaml output.out` - -* If one of the formats is known and the other is not, you can use a mixture of the above two points. -Example (if you are using a source distribution): `pyspdxtools_convertor -f rdf tests/data/formats/SPDXRdfExample.xyz -o output.xml` - -* For help use `pyspdxtools_convertor --help` +* If you want to skip the validation process, provide the `--novalidation` flag, like so: + `pyspdxtools -i tests/data/formats/SPDXJSONExample-v2.3.spdx.json -o output.tag --novalidation` +* For help use `pyspdxtools --help` # Installation diff --git a/pyproject.toml b/pyproject.toml index 8177df578..4d03a65d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,7 @@ dynamic = ["version"] test = ["pytest"] [project.scripts] -pyspdxtools_convertor = "src.clitools.convertor:main" -pyspdxtools_parser = "src.clitools.parser:main" +pyspdxtools = "src.clitools.pyspdxtools:main" [tool.setuptools] zip-safe = false # because of the uses of __file__: https://github.com/spdx/tools-python/issues/257 diff --git a/src/clitools/convertor.py b/src/clitools/convertor.py deleted file mode 100644 index 2a9ecb0f5..000000000 --- a/src/clitools/convertor.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2020 Yash Varshney -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import click - - -def print_help_msg(command): - with click.Context(command) as ctx: - click.echo(command.get_help(ctx)) - - -def determine_infile_and_outfile(infile, outfile, src, from_, to): - if infile is not None and outfile is not None: - """ - when the CLI is of given format: - ' pyspdxtools_convertor ---infile ---outfile . - """ - return infile, outfile - - elif infile is None and outfile is None and len(src) == 2: - """ - ' pyspdxtools_convertor -f/--from -t/--to . - """ - infile = src[0] - outfile = src[1] - if from_ is not None: - infile_path = os.path.splitext(infile)[0] - infile = infile_path + "." + from_ - if to is not None: - outfile_path = os.path.splitext(outfile)[0] - outfile = outfile_path + "." + to - return infile, outfile - - elif infile is None and outfile is not None: - """ - ' pyspdxtools_convertor -f/--from --outfile ' - """ - infile = src[0] - if from_ is not None: - infile_path = os.path.splitext(infile)[0] - infile = infile_path + "." + from_ - return infile, outfile - - elif infile is not None and outfile is None: - """ - ' pyspdxtools_convertor --infile -t/--to ' - """ - outfile = src[0] - if to is not None: - outfile_path = os.path.splitext(outfile)[0] - outfile = outfile_path + "." + to - return infile, outfile - - else: - raise ValueError("Given arguments for convertor are invalid.") - - -@click.command() -@click.argument("src", nargs=-1) -@click.option("--infile", "-i", help="The file to be converted ") -@click.option("--outfile", "-o", help="The file after converting") -@click.option( - "--to", - "-t", - type=click.Choice(["json", "rdf", "yaml", "xml", "tag"], case_sensitive=False) -) -@click.option( - "--from", - "-f", - "from_", - type=click.Choice(["tag", "rdf"], case_sensitive=False)) -@click.option("--force", is_flag=True, help="convert even if there are some parsing errors or inconsistencies") -def main(infile, outfile, src, from_, to, force): - """ - CLI-TOOL for converting a RDF or TAG file to RDF, JSON, YAML, TAG or XML format. - - To use : run 'pyspdxtools_convertor -f -t ' command on terminal - or use ' pyspdxtools_convertor --infile --outfile ' - - """ - try: - infile, outfile = determine_infile_and_outfile(infile, outfile, src, from_, to) - except ValueError as err: - print(err) - print_help_msg(main) - return - - raise NotImplementedError("Currently, conversion is not implemented") - - # Parse document from infile - # First one to implement is the Json parser: https://github.com/spdx/tools-python/issues/305 - - # Write document to outfile - # First writer to implement is the Json writer: https://github.com/spdx/tools-python/issues/359 - - -if __name__ == "__main__": - main() diff --git a/src/clitools/parser.py b/src/clitools/parser.py deleted file mode 100755 index e4340115b..000000000 --- a/src/clitools/parser.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2020 Yash Varshney -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import click - -from src.model.spdx_no_assertion import SpdxNoAssertion -from src.model.spdx_none import SpdxNone - - -@click.command() -@click.option("--file", prompt="File name", help="The file to be parsed") -@click.option("--force", is_flag=True, help="print information even if there are some parsing errors") -def main(file, force): - """ - COMMAND-LINE TOOL for parsing file of RDF, XML, JSON, YAML and XML format. - - To use : run `pyspdxtools_parser` using terminal or run `pyspdxtools_parser --file ` - - """ - raise NotImplementedError("Currently, no parsers are implemented") - - # Parse document - # First one to implement is the Json parser: https://github.com/spdx/tools-python/issues/305 - - # Print all document properties - or possibly a selection of them. Should be human-readable, so using indentation - # for nested properties is probably a good idea. - - -if __name__ == "__main__": - main() diff --git a/src/clitools/pyspdxtools.py b/src/clitools/pyspdxtools.py new file mode 100644 index 000000000..d9dbed945 --- /dev/null +++ b/src/clitools/pyspdxtools.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2020 Yash Varshney +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +from typing import List + +import click + +from src.model.document import Document +from src.parser.parse_anything import parse_file +from src.validation.document_validator import validate_full_spdx_document +from src.validation.validation_message import ValidationMessage +from src.writer.tagvalue import tagvalue_writer +from src.writer.write_anything import write_file + + +@click.command() +@click.option("--infile", "-i", prompt="input file path", help="The file containing the document to be validated or converted.") +@click.option("--outfile", "-o", help="The file to write the converted document to (write a dash for output to stdout or omit for no conversion).") +@click.option("--version", help='The SPDX version to be used during parsing and validation (format "SPDX-2.3").', default="SPDX-2.3") +@click.option("--novalidation", is_flag=True, help="Don't validate the provided document.") +def main(infile: str, outfile: str, version: str, novalidation: bool): + """ + CLI-tool for validating SPDX documents and converting between RDF, TAG-VALUE, JSON, YAML and XML formats. + Formats are determined by the file endings. + To use, run: 'pyspdxtools --infile --outfile ' + """ + try: + document: Document = parse_file(infile) + + if outfile == "-": + tagvalue_writer.write_document(document, sys.stdout) + print("") + + if not novalidation: + validation_messages: List[ValidationMessage] = validate_full_spdx_document(document, version) + if validation_messages: + print("The document is invalid. The following issues have been found:") + for message in validation_messages: + print(message.validation_message) + sys.exit(1) + else: + print("The document is valid.") + + if outfile and outfile != "-": + write_file(document, outfile, validate=False) + + except NotImplementedError as err: + print(err.args[0]) + print("Please note that this project is currently undergoing a major refactoring and therefore missing " + "a few features which will be added in time (refer to https://github.com/spdx/tools-python/issues " + "for insights into the current status).\n" + "In the meantime, please use the PyPI release version 0.7.0.") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/src/formats.py b/src/formats.py new file mode 100644 index 000000000..fecaf2c24 --- /dev/null +++ b/src/formats.py @@ -0,0 +1,36 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import Enum, auto + +from src.parser.error import SPDXParsingError + + +class FileFormat(Enum): + JSON = auto() + YAML = auto() + XML = auto() + TAG_VALUE = auto() + RDF_XML = auto() + + +def file_name_to_format(file_name: str) -> FileFormat: + if file_name.endswith(".rdf") or file_name.endswith(".rdf.xml"): + return FileFormat.RDF_XML + elif file_name.endswith(".tag") or file_name.endswith(".spdx"): + return FileFormat.TAG_VALUE + elif file_name.endswith(".json"): + return FileFormat.JSON + elif file_name.endswith(".xml"): + return FileFormat.XML + elif file_name.endswith(".yaml") or file_name.endswith(".yml"): + return FileFormat.YAML + else: + raise SPDXParsingError(["Unsupported SPDX file type: " + str(file_name)]) diff --git a/src/parser/parse_anything.py b/src/parser/parse_anything.py new file mode 100644 index 000000000..541188a59 --- /dev/null +++ b/src/parser/parse_anything.py @@ -0,0 +1,26 @@ +# Copyright (c) spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from src.formats import file_name_to_format, FileFormat +from src.parser.json.json_parser import JsonParser + + +def parse_file(file_name: str): + input_format = file_name_to_format(file_name) + if input_format == FileFormat.RDF_XML: + raise NotImplementedError("Currently, the rdf parser is not implemented") + elif input_format == FileFormat.TAG_VALUE: + raise NotImplementedError("Currently, the tag-value parser is not implemented") + elif input_format == FileFormat.JSON: + return JsonParser().parse(file_name) + elif input_format == FileFormat.XML: + raise NotImplementedError("Currently, the xml parser is not implemented") + elif input_format == FileFormat.YAML: + raise NotImplementedError("Currently, the yaml parser is not implemented") diff --git a/src/writer/write_anything.py b/src/writer/write_anything.py new file mode 100644 index 000000000..ca01ba86e --- /dev/null +++ b/src/writer/write_anything.py @@ -0,0 +1,28 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from src.formats import file_name_to_format, FileFormat +from src.model.document import Document +from src.writer.json import json_writer +from src.writer.tagvalue import tagvalue_writer + + +def write_file(document: Document, file_name: str, validate: bool = True): + output_format = file_name_to_format(file_name) + if output_format == FileFormat.JSON: + json_writer.write_document(document, file_name, validate) + elif output_format == FileFormat.YAML: + raise NotImplementedError("Currently, the yaml writer is not implemented") + elif output_format == FileFormat.XML: + raise NotImplementedError("Currently, the xml writer is not implemented") + elif output_format == FileFormat.TAG_VALUE: + tagvalue_writer.write_document_to_file(document, file_name) + elif output_format == FileFormat.RDF_XML: + raise NotImplementedError("Currently, the rdf writer is not implemented") diff --git a/tests/clitools/__init__.py b/tests/clitools/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/clitools/test_cli_convertor.py b/tests/clitools/test_cli_convertor.py deleted file mode 100644 index d4ea5c760..000000000 --- a/tests/clitools/test_cli_convertor.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright (c) 2022 spdx tool contributors -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from unittest import TestCase - -import pytest - -from src.clitools.convertor import determine_infile_and_outfile - - -class TestConvertor(TestCase): - maxDiff = None - - def test_determine_input_with_known_i_o_format(self): - infile_given = 'infile.rdf' - outfile_given = 'outfile.json' - src = () - from_ = None - to = None - - infile, outfile = determine_infile_and_outfile(infile_given, outfile_given, src, from_, to) - - assert infile == infile_given - assert outfile == outfile_given - - def test_determine_input_with_unknown_i_o_format(self): - infile_given = None - outfile_given = None - src = ('infile.in', 'outfile.out') - from_ = 'rdf' - to = 'json' - expected_infile = 'infile.rdf' - expected_outfile = 'outfile.json' - - infile, outfile = determine_infile_and_outfile(infile_given, outfile_given, src, from_, to) - - assert infile == expected_infile - assert outfile == expected_outfile - - def test_determine_input_with_known_i_format_unknown_o_format(self): - infile_given = 'infile.rdf' - outfile_given = None - src = ('outfile',) - from_ = None - to = 'json' - expected_outfile = 'outfile.json' - - infile, outfile = determine_infile_and_outfile(infile_given, outfile_given, src, from_, to) - - assert infile == infile_given - assert outfile == expected_outfile - - def test_determine_input_with_unknown_i_format_known_o_format(self): - infile_given = None - outfile_given = 'outfile.json' - src = ('infile',) - from_ = 'rdf' - to = None - expected_infile = 'infile.rdf' - - infile, outfile = determine_infile_and_outfile(infile_given, outfile_given, src, from_, to) - - assert infile == expected_infile - assert outfile == outfile_given - - def test_determine_input_with_invalid_arguments(self): - infile_given = None - outfile_given = None - src = () - from_ = None - to = None - - with pytest.raises(ValueError): - determine_infile_and_outfile(infile_given, outfile_given, src, from_, to)