Repository: conda-forge/conda-smithy

Issue:
Allowing `Other (...)` for atypical `license` fields
For some atypical licenses, SPDX is not sufficient. It would help to have some syntax that allows users to specify the unusual nature of the license without running afoul of the linter. [One suggestion]( https://github.com/conda-forge/conda-smithy/pull/1220#issuecomment-600924602 ) is to allow `Other (...)`. Though really any variant of this should be fine.

cc @isuruf


Discussion:
We could just stay within the SPDX spec and use the `LicenseRef-` prefix for this.

---
https://github.com/spdx/spdx-spec/blame/25c499eadbb2ffa1ab4e7dda7a2e6b364e876fe2/chapters/appendix-V-using-SPDX-short-identifiers-in-source-files.md#L57-L59 :
> Alternatively, you can use a `LicenseRef-` custom license identifier to refer to a license that is not on the SPDX License List, such as the following:
> 
>    SPDX-License-Identifier: LicenseRef-my-special-license

`my-special-license` is an "`idstring`" consisting of alphanumeric characters, `.` and `-`.
https://github.com/spdx/spdx-spec/blame/25c499eadbb2ffa1ab4e7dda7a2e6b364e876fe2/chapters/appendix-IV-SPDX-license-expressions.md#L11 :
> The exact syntax of license expressions is described below in [ABNF](http://tools.ietf.org/html/rfc5234).
> 
> idstring = 1*(ALPHA / DIGIT / "-" / "." )

----
Sure, `Other (free form text)` is more eye-pleasing than `LicenseRef-kebap-case--with.dots`, but
- this is for exceptions only,
- defined by a simple grammar, i.e. easily parsable by machine,
- we avoid future maintenance burden by not "rolling our own thing".

Plus, we can build on SPDX's future work, e.g., https://github.com/spdx/spdx-spec/issues/113 .
I can implement either. @jakirkham ?
I don’t have strong feelings about direction here. Just as long as we have the option 🙂

Resolving patch:
diff --git a/conda_smithy/lint_recipe.py b/conda_smithy/lint_recipe.py
--- a/conda_smithy/lint_recipe.py
+++ b/conda_smithy/lint_recipe.py
@@ -616,6 +616,12 @@ def lintify(meta, recipe_dir=None, conda_forge=False):
     except license_expression.ExpressionError:
         parsed_licenses = [license]
 
+    licenseref_regex = re.compile("^LicenseRef[a-zA-Z0-9\-.]*$")
+    filtered_licenses = []
+    for license in parsed_licenses:
+        if not licenseref_regex.match(license):
+            filtered_licenses.append(license)
+
     with open(
         os.path.join(os.path.dirname(__file__), "licenses.txt"), "r"
     ) as f:
@@ -626,9 +632,9 @@ def lintify(meta, recipe_dir=None, conda_forge=False):
     ) as f:
         expected_exceptions = f.readlines()
         expected_exceptions = set([l.strip() for l in expected_exceptions])
-    if set(parsed_licenses) - expected_licenses:
+    if set(filtered_licenses) - expected_licenses:
         hints.append(
-            "License is not an SPDX identifier (or Other) nor an SPDX license expression."
+            "License is not an SPDX identifier (or a custom LicenseRef) nor an SPDX license expression."
         )
     if set(parsed_exceptions) - expected_exceptions:
         hints.append("License exception is not an SPDX exception.")


Test patch:
diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py
--- a/tests/test_lint_recipe.py
+++ b/tests/test_lint_recipe.py
@@ -585,18 +585,23 @@ def test_redundant_license(self):
         self.assertIn(expected_message, lints)
 
     def test_spdx_license(self):
-        msg = "License is not an SPDX identifier (or Other) nor an SPDX license expression."
+        msg = "License is not an SPDX identifier (or a custom LicenseRef) nor an SPDX license expression."
         licenses = {
             "BSD-100": False,
-            "GPL-2.0": True,
-            "Other": True,
-            "GPL-2.0 or MIT": True,
-            "GPL-2.0 | MIT": False,
+            "GPL-2.0": False,
+            "GPL-2.0-only": True,
+            "Other": False,
+            "GPL-2.0-or-later or MIT": True,
+            "LGPL-2.0-only | MIT": False,
             "LLVM-exception": False,
+            "LicenseRef-kebab-case-2--with.dots OR MIT": True,
+            "LicenseRef-HDF5": True,
+            "LicenseRef-@HDF5": False,
         }
         for license, good in licenses.items():
             meta = {"about": {"license": license}}
             lints, hints = linter.lintify(meta)
+            print(license, good)
             if good:
                 self.assertNotIn(msg, hints)
             else: