Skip to content

Commit ece2552

Browse files
Add license expression removal
Signed-off-by: bhavesh200628-debug <243262296+bhavesh200628-debug@users.noreply.github.com>
1 parent 2efada2 commit ece2552

2 files changed

Lines changed: 231 additions & 0 deletions

File tree

src/license_expression/__init__.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,100 @@ def dedup(self, expression):
783783
raise ExpressionError(f"Unknown expression type: {expression!r}")
784784
return deduped
785785

786+
def remove(self, expression, licenses_to_remove, **kwargs):
787+
"""
788+
Return a new LicenseExpression with the specified ``licenses_to_remove``
789+
removed from ``expression``, or None if all licenses are removed.
790+
791+
``expression`` is a license expression string or LicenseExpression object.
792+
793+
``licenses_to_remove`` is a license key string, LicenseSymbol,
794+
LicenseExpression object, or an iterable of these.
795+
796+
Composite "WITH" expressions (LicenseWithExceptionSymbol) are treated as
797+
atomic and are removed when matching the exact composite symbol or
798+
expression.
799+
800+
Nested AND/OR expressions are pruned recursively and collapsed when only
801+
a single child remains.
802+
803+
Extra ``kwargs`` are passed down to the parse() function.
804+
"""
805+
if expression is None:
806+
return None
807+
808+
exp = self.parse(expression, **kwargs)
809+
if exp is None:
810+
return None
811+
812+
if licenses_to_remove is None:
813+
return exp
814+
815+
if isinstance(licenses_to_remove, (str, bytes, LicenseExpression, BaseSymbol)):
816+
licenses_to_remove = [licenses_to_remove]
817+
elif isinstance(licenses_to_remove, (list, tuple, set)):
818+
if len(licenses_to_remove) == 0:
819+
return exp
820+
else:
821+
try:
822+
licenses_to_remove = list(licenses_to_remove)
823+
except TypeError:
824+
licenses_to_remove = [licenses_to_remove]
825+
826+
targets = []
827+
for target in licenses_to_remove:
828+
if target is None:
829+
continue
830+
if isinstance(target, (LicenseExpression, BaseSymbol)):
831+
targets.append(target)
832+
else:
833+
parsed_target = self.parse(target, **kwargs)
834+
if parsed_target is not None:
835+
targets.append(parsed_target)
836+
837+
if not targets:
838+
return exp
839+
840+
def _remove(node):
841+
if node is None:
842+
return None
843+
844+
for t in targets:
845+
if node == t:
846+
return None
847+
848+
if isinstance(node, BaseSymbol):
849+
return node
850+
851+
if isinstance(node, (self.AND, self.OR)):
852+
relation = node.__class__.__name__
853+
filtered_args = []
854+
for arg in node.args:
855+
res = _remove(arg)
856+
if res is not None:
857+
filtered_args.append(res)
858+
859+
if relation == "AND":
860+
flattened = []
861+
for e in filtered_args:
862+
if isinstance(e, self.AND):
863+
flattened.extend(e.args)
864+
else:
865+
flattened.append(e)
866+
filtered_args = flattened
867+
868+
unique_args = ordered_unique(filtered_args)
869+
870+
if not unique_args:
871+
return None
872+
if len(unique_args) == 1:
873+
return unique_args[0]
874+
return node.__class__(*unique_args)
875+
876+
raise ExpressionError(f"Unknown expression type: {node!r}")
877+
878+
return _remove(exp)
879+
786880
def validate(self, expression, strict=True, **kwargs):
787881
"""
788882
Return a ExpressionInfo object that contains information about

tests/test_license_expression.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2640,3 +2640,140 @@ def test_combine_expressions_with_duplicated_elements(self):
26402640

26412641
def test_combine_expressions_with_or_relationship(self):
26422642
assert str(combine_expressions(["mit", "apache-2.0"], "OR")) == "mit OR apache-2.0"
2643+
2644+
2645+
class LicensingRemoveTest(TestCase):
2646+
def setUp(self):
2647+
self.licensing = Licensing()
2648+
2649+
def test_remove_from_and_expression(self):
2650+
result = self.licensing.remove("MIT AND Apache-2.0", "Apache-2.0")
2651+
assert result.render() == "MIT"
2652+
2653+
def test_remove_from_or_expression(self):
2654+
result = self.licensing.remove("MIT OR Apache-2.0", "Apache-2.0")
2655+
assert result.render() == "MIT"
2656+
2657+
def test_remove_first_term_from_and(self):
2658+
result = self.licensing.remove("MIT AND Apache-2.0", "MIT")
2659+
assert result.render() == "Apache-2.0"
2660+
2661+
def test_remove_first_term_from_or(self):
2662+
result = self.licensing.remove("MIT OR Apache-2.0", "MIT")
2663+
assert result.render() == "Apache-2.0"
2664+
2665+
def test_remove_nested_and_or_expressions(self):
2666+
expr = "(MIT AND Apache-2.0) OR GPL-2.0"
2667+
result = self.licensing.remove(expr, "Apache-2.0")
2668+
assert result.render() == "MIT OR GPL-2.0"
2669+
2670+
result = self.licensing.remove(expr, "GPL-2.0")
2671+
assert result.render() == "MIT AND Apache-2.0"
2672+
2673+
def test_remove_complete_subexpression_string(self):
2674+
expr = "(MIT AND Apache-2.0) OR GPL-2.0"
2675+
result = self.licensing.remove(expr, "MIT AND Apache-2.0")
2676+
assert result.render() == "GPL-2.0"
2677+
2678+
def test_remove_complete_subexpression_object(self):
2679+
expr = self.licensing.parse("(MIT AND Apache-2.0) OR GPL-2.0")
2680+
subexpr = self.licensing.parse("MIT AND Apache-2.0")
2681+
result = self.licensing.remove(expr, subexpr)
2682+
assert result.render() == "GPL-2.0"
2683+
2684+
def test_remove_nonexistent_license(self):
2685+
expr = "MIT AND Apache-2.0"
2686+
result = self.licensing.remove(expr, "GPL-2.0")
2687+
assert result.render() == "MIT AND Apache-2.0"
2688+
2689+
def test_remove_all_terms_single_license(self):
2690+
result = self.licensing.remove("MIT", "MIT")
2691+
assert result is None
2692+
2693+
def test_remove_all_terms_from_and(self):
2694+
result = self.licensing.remove("MIT AND Apache-2.0", ["MIT", "Apache-2.0"])
2695+
assert result is None
2696+
2697+
def test_remove_all_terms_from_or(self):
2698+
result = self.licensing.remove("MIT OR Apache-2.0", ["MIT", "Apache-2.0"])
2699+
assert result is None
2700+
2701+
def test_remove_all_terms_from_nested(self):
2702+
expr = "(MIT AND Apache-2.0) OR GPL-2.0"
2703+
result = self.licensing.remove(expr, ["MIT", "Apache-2.0", "GPL-2.0"])
2704+
assert result is None
2705+
2706+
def test_remove_with_duplicate_terms(self):
2707+
expr = "MIT AND MIT AND Apache-2.0"
2708+
result = self.licensing.remove(expr, "Apache-2.0")
2709+
assert result.render() == "MIT"
2710+
2711+
def test_remove_with_parenthesized_complex_expressions(self):
2712+
expr = "(MIT OR BSD) AND (Apache-2.0 OR GPL-2.0)"
2713+
result = self.licensing.remove(expr, "BSD")
2714+
assert result.render() == "MIT AND (Apache-2.0 OR GPL-2.0)"
2715+
2716+
result = self.licensing.remove(expr, ["BSD", "GPL-2.0"])
2717+
assert result.render() == "MIT AND Apache-2.0"
2718+
2719+
def test_remove_with_expression_exact_composite_string(self):
2720+
expr = "GPL-2.0 WITH Classpath-exception OR MIT"
2721+
result = self.licensing.remove(expr, "GPL-2.0 WITH Classpath-exception")
2722+
assert result.render() == "MIT"
2723+
2724+
def test_remove_with_expression_exact_composite_object(self):
2725+
lic_sym = LicenseSymbol("GPL-2.0")
2726+
exc_sym = LicenseSymbol("Classpath-exception")
2727+
with_sym = LicenseWithExceptionSymbol(lic_sym, exc_sym)
2728+
expr = "GPL-2.0 WITH Classpath-exception OR MIT"
2729+
result = self.licensing.remove(expr, with_sym)
2730+
assert result.render() == "MIT"
2731+
2732+
def test_remove_with_expression_exact_composite_object_with_known_symbols(self):
2733+
gpl = LicenseSymbol("GPL-2.0")
2734+
exc = LicenseSymbol("Classpath-exception", is_exception=True)
2735+
mit = LicenseSymbol("MIT")
2736+
licensing = Licensing([gpl, exc, mit])
2737+
with_sym = LicenseWithExceptionSymbol(gpl, exc)
2738+
expr = "GPL-2.0 WITH Classpath-exception OR MIT"
2739+
result = licensing.remove(expr, with_sym)
2740+
assert result.render() == "MIT"
2741+
2742+
def test_remove_with_expression_all_removed(self):
2743+
expr = "GPL-2.0 WITH Classpath-exception"
2744+
result = self.licensing.remove(expr, "GPL-2.0 WITH Classpath-exception")
2745+
assert result is None
2746+
2747+
def test_remove_base_license_does_not_affect_composite_with_expression(self):
2748+
expr = "GPL-2.0 WITH Classpath-exception OR GPL-2.0"
2749+
result = self.licensing.remove(expr, "GPL-2.0")
2750+
assert result.render() == "GPL-2.0 WITH Classpath-exception"
2751+
2752+
def test_remove_with_symbol_object_target(self):
2753+
expr = "MIT AND Apache-2.0"
2754+
result = self.licensing.remove(expr, LicenseSymbol("Apache-2.0"))
2755+
assert result.render() == "MIT"
2756+
2757+
def test_remove_with_parsed_expression_input(self):
2758+
expr = self.licensing.parse("MIT AND Apache-2.0")
2759+
result = self.licensing.remove(expr, "Apache-2.0")
2760+
assert result.render() == "MIT"
2761+
2762+
def test_remove_empty_or_none_expression(self):
2763+
assert self.licensing.remove(None, "MIT") is None
2764+
assert self.licensing.remove("", "MIT") is None
2765+
assert self.licensing.remove(" ", "MIT") is None
2766+
2767+
def test_remove_empty_or_none_targets(self):
2768+
expr = "MIT AND Apache-2.0"
2769+
assert self.licensing.remove(expr, None).render() == "MIT AND Apache-2.0"
2770+
assert self.licensing.remove(expr, []).render() == "MIT AND Apache-2.0"
2771+
assert self.licensing.remove(expr, "").render() == "MIT AND Apache-2.0"
2772+
2773+
def test_remove_with_known_symbols_and_aliases(self):
2774+
gpl2 = LicenseSymbol("GPL-2.0", aliases=["gpl v2", "gpl2"])
2775+
mit = LicenseSymbol("MIT", aliases=["mit license"])
2776+
licensing = Licensing([gpl2, mit])
2777+
expr = "gpl v2 OR mit license"
2778+
result = licensing.remove(expr, "gpl2")
2779+
assert result.render() == "MIT"

0 commit comments

Comments
 (0)