1515from licensedcode import cache
1616from licensedcode import models
1717from licensedcode import match_hash
18+ from licensedcode import frontmatter
19+ from license_expression import Licensing
1820
1921"""
2022A script to generate license detection rules from a simple text data file.
@@ -172,10 +174,112 @@ def find_rule_base_loc(license_expression):
172174 )
173175
174176
177+ def validate_and_dump_rules (rules , licenses_by_key , licenses_file_path ):
178+ valid_rules_text , invalid_rules_text = validate_rules_with_details (rules , licenses_by_key )
179+ if invalid_rules_text :
180+ valid_rules_file = licenses_file_path + ".valid"
181+ with open (valid_rules_file , "w" ) as o :
182+ o .write (valid_rules_text )
183+
184+ invalid_rules_file = licenses_file_path + ".invalid"
185+ with open (invalid_rules_file , "w" ) as o :
186+ o .write (invalid_rules_text )
187+
188+ message = [
189+ 'Errors while validating rules:' ,
190+ f'See valid rules file: { valid_rules_file } ' ,
191+ f'See invalid rules file: { invalid_rules_file } ' ,
192+ ]
193+ raise models .InvalidRule ('\n ' .join (message ))
194+
195+
196+ def validate_rules_with_details (rules , licenses_by_key ):
197+ """
198+ Return a tuple of (text of valid rules, text of invalid rules) in the format
199+ expected by this tool. Invalid rules have a YAML comment text added to their
200+ metadata section that describes the issue.
201+ """
202+
203+ licensing = Licensing (symbols = licenses_by_key .values ())
204+
205+ valid_rules_texts = []
206+ invalid_rules_texts = []
207+
208+ for rule in rules :
209+ error_messages = list (rule .validate (licensing , thorough = True ))
210+ rule_as_text = dump_skinny_rule (rule , error_messages = error_messages )
211+
212+ if error_messages :
213+ invalid_rules_texts .append (rule_as_text )
214+ else :
215+ valid_rules_texts .append (rule_as_text )
216+
217+ valid_rules_text = "\n " .join (valid_rules_texts ) + start_delimiter
218+
219+ if invalid_rules_texts :
220+ invalid_rules_text = "\n " .join (invalid_rules_texts ) + start_delimiter
221+ else :
222+ invalid_rules_text = ""
223+
224+ return valid_rules_text , invalid_rules_text
225+
226+
227+ SKINNY_RULE_TEMPLATE = """\
228+ {start_delimiter}
229+ {metadata}
230+ {end_delimiter}
231+ {content}
232+ """
233+
234+ start_delimiter = "----------------------------------------"
235+
236+
237+ def dump_skinny_rule (rule , error_messages = ()):
238+ """
239+ Return a string that dumps the ``rule`` Rule in the input format used by
240+ this tool. Add a comment with the ``error_message`` to the metadata if any.
241+ """
242+ metadata = rule .to_dict ()
243+ if error_messages :
244+ # add missing metadata for sanity
245+ if "license_expression" not in metadata :
246+ m = {"license_expression" : None }
247+ m .update (metadata )
248+ metadata = m
249+
250+ if "notes" not in metadata :
251+ metadata ["notes" ] = None
252+
253+ if "referenced_filenames" not in metadata :
254+ metadata ["referenced_filenames" ] = None
255+
256+ msgs = "\n " .join (f"# { m } " for m in error_messages )
257+ end_delimiter = f"{ msgs } \n ---"
258+ else :
259+ end_delimiter = "---"
260+
261+ return frontmatter .dumps_frontmatter (
262+ content = rule .text ,
263+ metadata = metadata ,
264+ template = SKINNY_RULE_TEMPLATE ,
265+ start_delimiter = start_delimiter ,
266+ end_delimiter = end_delimiter )
267+
268+
175269@click .command ()
176- @click .argument ("licenses_file" , type = click .Path (), metavar = "FILE" )
270+ @click .argument (
271+ "licenses_file" , type = click .Path (), metavar = "FILE" ,
272+ )
273+ @click .option (
274+ "-d" , "--dump-to-file-on-errors" ,
275+ is_flag = True ,
276+ default = False ,
277+ help = "On errors, dump the valid rules and the invalid rules in text files "
278+ "named after the input FILE with a .valid and a .invalid extension." ,
279+ )
280+
177281@click .help_option ("-h" , "--help" )
178- def cli (licenses_file ):
282+ def cli (licenses_file , dump_to_file_on_errors = False ):
179283 """
180284 Create rules from a text file with delimited blocks of metadata and texts.
181285
@@ -191,6 +295,12 @@ def cli(licenses_file):
191295 it under the terms of the GNU Lesser General Public License
192296 version 2.1 as published by the Free Software Foundation;
193297 ----------------------------------------
298+ license_expression: lgpl-2.1
299+ relevance: 100
300+ is_license_reference: yes
301+ ---
302+ LGPL-2.1
303+ ----------------------------------------
194304 """
195305
196306 rules_data = load_data (licenses_file )
@@ -213,7 +323,11 @@ def cli(licenses_file):
213323 rl = models .BasicRule (text = rdata .text , ** rdata .data )
214324 skinny_rules .append (rl )
215325
216- models .validate_rules (skinny_rules , licenses_by_key , with_text = True , thorough = True )
326+ # these will raise an exception and exit on errors
327+ if not dump_to_file_on_errors :
328+ models .validate_rules (rules = skinny_rules , licenses_by_key = licenses_by_key , with_text = True , thorough = True )
329+ else :
330+ validate_and_dump_rules (rules = skinny_rules , licenses_by_key = licenses_by_key , licenses_file_path = licenses_file )
217331
218332 print ()
219333 for rule in skinny_rules :
0 commit comments