Repository: spdx/tools-python

Issue:
Add a proper logging framework
Making sure that `stdout` is not cluttered with unwanted output that could go to `stderr`

Discussion:
armintaenzertng: I fixed the system exits' logs, but for the cli tool parser the output is the actual wanted result of the tool and should therefore go to stdout, I think.
armintaenzertng: Concerning the parsing logger, which basically just collects all parsing errors, I think that it is still valid as it is go give helpful information for the `SpdxParsingError` that we raise (this can be caught by library users, which can easily do whatever they want with the `get_messages()` function).
The question would be if we additionally want to separately log all these errors. But right now, I don't see the merit in that.

Patch:
diff --git a/src/spdx/clitools/pyspdxtools.py b/src/spdx/clitools/pyspdxtools.py
index ed32dbb..44a4a3b 100644
--- a/src/spdx/clitools/pyspdxtools.py
+++ b/src/spdx/clitools/pyspdxtools.py
@@ -10,6 +10,7 @@
 # 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 logging
 import sys
 from typing import List
 
@@ -26,8 +27,11 @@ from spdx.writer.write_anything import write_file
 
 @click.command()
 @click.option("--infile", "-i", 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 ("SPDX-2.2" or "SPDX-2.3"). Will be read from the document if not provided.', default=None)
+@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 ("SPDX-2.2" or "SPDX-2.3"). Will be read from the document if not provided.',
+              default=None)
 @click.option("--novalidation", is_flag=True, help="Don't validate the provided document.")
 def main(infile: str, outfile: str, version: str, novalidation: bool):
     """
@@ -46,34 +50,34 @@ def main(infile: str, outfile: str, version: str, novalidation: bool):
                 version = document.creation_info.spdx_version
 
             if not version in ["SPDX-2.2", "SPDX-2.3"]:
-                print(f"This tool only supports SPDX versions SPDX-2.2 and SPDX-2.3, but got: {version}",
-                      file=sys.stderr)
+                logging.error(f"This tool only supports SPDX versions SPDX-2.2 and SPDX-2.3, but got: {version}")
                 sys.exit(1)
 
             validation_messages: List[ValidationMessage] = validate_full_spdx_document(document, version)
             if validation_messages:
-                print("The document is invalid. The following issues have been found:", file=sys.stderr)
-                for message in validation_messages:
-                    print(message.validation_message, file=sys.stderr)
+                log_string = "\n".join(
+                    ["The document is invalid. The following issues have been found:"] +
+                    [message.validation_message for message in validation_messages])
+                logging.error(log_string)
                 sys.exit(1)
             else:
-                print("The document is valid.", file=sys.stderr)
+                logging.info("The document is valid.")
 
         if outfile and outfile != "-":
             write_file(document, outfile, validate=False)
 
     except NotImplementedError as err:
-        print(err.args[0], file=sys.stderr)
-        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.", file=sys.stderr)
+        logging.error(err.args[0] +
+                          "\nPlease 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 current PyPI release version.")
         sys.exit(1)
 
     except SPDXParsingError as err:
-        print("There have been issues while parsing the provided document:", file=sys.stderr)
-        for message in err.get_messages():
-            print(message, file=sys.stderr)
+        log_string = "\n".join(["There have been issues while parsing the provided document:"] +
+                               [message for message in err.get_messages()])
+        logging.error(log_string)
         sys.exit(1)
 
 
diff --git a/src/spdx/parser/rdf/creation_info_parser.py b/src/spdx/parser/rdf/creation_info_parser.py
index 522b21e..7ca2215 100644
--- a/src/spdx/parser/rdf/creation_info_parser.py
+++ b/src/spdx/parser/rdf/creation_info_parser.py
@@ -8,6 +8,7 @@
 # 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 logging
 import sys
 from typing import Tuple
 from urllib.parse import urldefrag
@@ -72,19 +73,23 @@ def parse_namespace_and_spdx_id(graph: Graph) -> (str, str):
     try:
         subject = graph.value(predicate=RDF.type, object=SPDX_NAMESPACE.SpdxDocument, any=False)
     except UniquenessError:
-        sys.exit("Multiple SpdxDocuments found, can't parse rdf file.")
+        logging.error("Multiple SpdxDocuments found, can't parse rdf file.")
+        sys.exit(1)
 
     if not subject:
-        sys.exit("No SpdxDocument found, can't parse rdf file.")
+        logging.error("No SpdxDocument found, can't parse rdf file.")
+        sys.exit(1)
     if not "#" in subject:
-        sys.exit("No '#' found in the URI of SpdxDocument, "
-                 "the URI for the SpdxDocument should be the namespace appended by '#SPDXRef-DOCUMENT.")
+        logging.error("No '#' found in the URI of SpdxDocument, "
+                      "the URI for the SpdxDocument should be the namespace appended by '#SPDXRef-DOCUMENT.")
+        sys.exit(1)
 
     namespace, spdx_id = urldefrag(subject)
 
     if not namespace:
-        sys.exit(
+        logging.error(
             "No namespace found, the URI for the SpdxDocument should be the namespace appended by '#SPDXRef-DOCUMENT.")
+        sys.exit(1)
 
     if not spdx_id:
         spdx_id = None
diff --git a/src/spdx/writer/rdf/writer_utils.py b/src/spdx/writer/rdf/writer_utils.py
index 772f80f..1498b75 100644
--- a/src/spdx/writer/rdf/writer_utils.py
+++ b/src/spdx/writer/rdf/writer_utils.py
@@ -8,7 +8,7 @@
 # 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
+import logging
 from datetime import datetime
 from typing import Any, Optional, Dict
 
@@ -59,8 +59,7 @@ def add_namespace_to_spdx_id(spdx_id: str, doc_namespace: str, external_doc_name
     if ":" in spdx_id:
         external_doc_ref_id = spdx_id.split(":")[0]
         if external_doc_ref_id not in external_doc_namespaces.keys():
-            print(f"No namespace for external document reference with id {external_doc_ref_id} provided.",
-                  file=sys.stderr)
+            logging.warning(f"No namespace for external document reference with id {external_doc_ref_id} provided.")
             return spdx_id
         return f"{external_doc_namespaces[external_doc_ref_id]}#{spdx_id.split(':')[1]}"
 


Test patch:
diff --git a/tests/spdx/parser/rdf/test_creation_info_parser.py b/tests/spdx/parser/rdf/test_creation_info_parser.py
index 516b0ff..71ee6cc 100644
--- a/tests/spdx/parser/rdf/test_creation_info_parser.py
+++ b/tests/spdx/parser/rdf/test_creation_info_parser.py
@@ -60,14 +60,16 @@ def test_parse_namespace_and_spdx_id():
                           ([(URIRef("docNamespace1"), RDF.type, SPDX_NAMESPACE.SpdxDocument),
                             (URIRef("docNamespace2"), RDF.type, SPDX_NAMESPACE.SpdxDocument)],
                            "Multiple SpdxDocuments found")])
-def test_parse_namespace_and_spdx_id_with_system_exit(triples: List[Tuple[Node, Node, Node]], error_message: str):
+def test_parse_namespace_and_spdx_id_with_system_exit(triples: List[Tuple[Node, Node, Node]], error_message: str, caplog):
     graph = Graph()
     for triple in triples:
         graph = graph.add(triple)
 
-    with pytest.raises(SystemExit, match=error_message):
+    with pytest.raises(SystemExit):
         parse_namespace_and_spdx_id(graph)
 
+    assert error_message in caplog.text
+
 
 def test_parse_external_document_refs():
     graph = Graph().parse(os.path.join(os.path.dirname(__file__), "data/file_to_test_rdf_parser.rdf.xml"))