Repository: spdx/tools-python

Issue:
After installing spdx-tools from pip not able to find pyspdxtools_convertor tool
Navigate to https://pypi.org/project/spdx-tools/ and using the link _pip install spdx-tools_ install spdx-tools. After that type pyspdxtools_convertor in a new cmd (Windows OS). we get nothing. Is there something I am missing.
External License reference is rejected by the validator
A license expression of the form `DocumentRef-..:LicenseRef-..` is rejected by the validator but it should be accepted according to Annex D, both version 2.2 and 2.3.

The implementation does not consider the possibility of a external license reference. Subsequently the check for the correct declaration of the external document is missing.

Discussion:

Patch:
diff --git a/src/spdx_tools/spdx/validation/license_expression_validator.py b/src/spdx_tools/spdx/validation/license_expression_validator.py
index a59aec9..e463aa9 100644
--- a/src/spdx_tools/spdx/validation/license_expression_validator.py
+++ b/src/spdx_tools/spdx/validation/license_expression_validator.py
@@ -7,6 +7,7 @@ from license_expression import ExpressionError, ExpressionParseError, LicenseExp
 
 from spdx_tools.common.spdx_licensing import spdx_licensing
 from spdx_tools.spdx.model import Document, SpdxNoAssertion, SpdxNone
+from spdx_tools.spdx.validation.spdx_id_validators import is_external_doc_ref_present_in_document
 from spdx_tools.spdx.validation.validation_message import SpdxElementType, ValidationContext, ValidationMessage
 
 
@@ -42,7 +43,36 @@ def validate_license_expression(
     license_ref_ids: List[str] = [license_ref.license_id for license_ref in document.extracted_licensing_info]
 
     for non_spdx_token in spdx_licensing.validate(license_expression).invalid_symbols:
-        if non_spdx_token not in license_ref_ids:
+        if ":" in non_spdx_token:
+            split_token: List[str] = non_spdx_token.split(":")
+            if len(split_token) != 2:
+                validation_messages.append(
+                    ValidationMessage(
+                        f"Too many colons in license reference: {non_spdx_token}. "
+                        "A license reference must only contain a single colon to "
+                        "separate an external document reference from the license reference.",
+                        context,
+                    )
+                )
+            else:
+                if not split_token[1].startswith("LicenseRef-"):
+                    validation_messages.append(
+                        ValidationMessage(
+                            f'A license reference must start with "LicenseRef-", but is: {split_token[1]} '
+                            f"in external license reference {non_spdx_token}.",
+                            context,
+                        )
+                    )
+                if not is_external_doc_ref_present_in_document(split_token[0], document):
+                    validation_messages.append(
+                        ValidationMessage(
+                            f'Did not find the external document reference "{split_token[0]}" in the SPDX document. '
+                            f"From the external license reference {non_spdx_token}.",
+                            context,
+                        )
+                    )
+
+        elif non_spdx_token not in license_ref_ids:
             validation_messages.append(
                 ValidationMessage(
                     f"Unrecognized license reference: {non_spdx_token}. license_expression must only use IDs from the "


Test patch:
diff --git a/tests/spdx/validation/test_license_expression_validator.py b/tests/spdx/validation/test_license_expression_validator.py
index cb0ef0c..e965f48 100644
--- a/tests/spdx/validation/test_license_expression_validator.py
+++ b/tests/spdx/validation/test_license_expression_validator.py
@@ -15,9 +15,10 @@ from spdx_tools.spdx.validation.license_expression_validator import (
     validate_license_expressions,
 )
 from spdx_tools.spdx.validation.validation_message import SpdxElementType, ValidationContext, ValidationMessage
-from tests.spdx.fixtures import document_fixture, extracted_licensing_info_fixture
+from tests.spdx.fixtures import document_fixture, external_document_ref_fixture, extracted_licensing_info_fixture
 
 FIXTURE_LICENSE_ID = extracted_licensing_info_fixture().license_id
+EXTERNAL_DOCUMENT_ID = external_document_ref_fixture().document_ref_id
 
 
 @pytest.mark.parametrize(
@@ -26,6 +27,7 @@ FIXTURE_LICENSE_ID = extracted_licensing_info_fixture().license_id
         "MIT",
         FIXTURE_LICENSE_ID,
         f"GPL-2.0-only with GPL-CC-1.0 and {FIXTURE_LICENSE_ID} with 389-exception or Beerware",
+        f"{EXTERNAL_DOCUMENT_ID}:LicenseRef-007",
     ],
 )
 def test_valid_license_expression(expression_string):
@@ -136,3 +138,38 @@ def test_invalid_license_expression_with_invalid_exceptions(expression_string, e
     expected_messages = [ValidationMessage(expected_message, context)]
 
     assert validation_messages == expected_messages
+
+
+@pytest.mark.parametrize(
+    "expression_string, expected_message",
+    [
+        (
+            f"{EXTERNAL_DOCUMENT_ID}:LicenseRef-007:4",
+            f"Too many colons in license reference: {EXTERNAL_DOCUMENT_ID}:LicenseRef-007:4. "
+            "A license reference must only contain a single colon to "
+            "separate an external document reference from the license reference.",
+        ),
+        (
+            f"{EXTERNAL_DOCUMENT_ID}:unknown_license",
+            'A license reference must start with "LicenseRef-", but is: unknown_license '
+            f"in external license reference {EXTERNAL_DOCUMENT_ID}:unknown_license.",
+        ),
+        (
+            "DocumentRef-unknown:LicenseRef-1",
+            'Did not find the external document reference "DocumentRef-unknown" in the SPDX document. '
+            "From the external license reference DocumentRef-unknown:LicenseRef-1.",
+        ),
+    ],
+)
+def test_invalid_license_expression_with_external_reference(expression_string, expected_message):
+    document: Document = document_fixture()
+    license_expression: LicenseExpression = spdx_licensing.parse(expression_string)
+    parent_id = "SPDXRef-File"
+    context = ValidationContext(
+        parent_id=parent_id, element_type=SpdxElementType.LICENSE_EXPRESSION, full_element=license_expression
+    )
+
+    validation_messages: List[ValidationMessage] = validate_license_expression(license_expression, document, parent_id)
+    expected_messages = [ValidationMessage(expected_message, context)]
+
+    assert validation_messages == expected_messages