Version 1.0
Copyright © 2022 Lowell D. Thomas
Python APG
 … an ABNF Parser Generator
exp.py
Go to the documentation of this file.
1 ''' @file apg_py/exp/exp.py
2 @brief ApgExp - a RegExp-like pattern matching engine.
3 '''
4 import sys
5 import types
6 import copy
7 from apg_py.lib import identifiers as id
8 from apg_py.lib import utilities as utils
9 from apg_py.lib.parser import Parser
10 from apg_py.lib.ast import Ast
11 from apg_py.lib.trace import Trace
12 from apg_py.api.api import Api
13 
14 
15 class Result():
16  '''A class for returning the results of a pattern match.
17  '''
18 
19  def __init__(
20  self,
21  source,
22  begin,
23  length,
24  node_hits,
25  tree_depth,
26  rules,
27  names,
28  ast,
29  codes=False):
30  '''The Result class constructor. Only called internally by ApgExp'''
31  ending = begin + length
32  self.sourcesource = source
33  self.indexindex = begin
34  self.indicesindices = [begin, ending]
35  self.matchmatch = source[begin:ending]
36  self.left_contextleft_context = source[:begin]
37  self.right_contextright_context = source[ending:]
38  self.codescodes = codes
39  self.rulesrules = rules
40  self.namesnames = names
41  self.astast = ast.copy() if(ast) else None
42  self.node_hitsnode_hits = node_hits
43  self.max_tree_depthmax_tree_depth = tree_depth
44 
45  def __str__(self):
46  '''print(Result) will call this function for a display of the object.
47  @returns Returns the display string.'''
48  string = ' match: ' + str(self.matchmatch)
49  string += '\n index: ' + str(self.indexindex)
50  string += '\n indices: ' + str(self.indicesindices)
51  string += '\n left_context: ' + str(self.left_contextleft_context)
52  string += '\n right_context: ' + str(self.right_contextright_context)
53  string += '\n node hits: ' + str(self.node_hitsnode_hits)
54  string += '\nmax tree depth: ' + str(self.max_tree_depthmax_tree_depth)
55  if(len(self.rulesrules)):
56  string += '\n\nrules: ' + str(len(self.rulesrules))
57  for key, value in self.rulesrules.items():
58  if(len(value) == 0):
59  string += '\n' + self.namesnames[key]
60  string += '[0]: <undefined>'
61  for i in range(len(value)):
62  # print('key: ' + key, end=' ')
63  # print('value[' + str(i) + ']: ', end='')
64  # print(value[i])
65  string += '\n' + self.namesnames[key]
66  string += '[' + str(i) + ']: '
67  if(self.codescodes):
68  string += '('
69  for val in value[i]:
70  string += str(val) + ','
71  string += ')'
72  i = 0
73  else:
74  string += utils.tuple_to_string(value[i])
75  # string += '\n'
76  return string
77 
78 
79 class ApgExp():
80  '''The ApgExp class provides a pattern-matching engine similar
81  to JavaScript's [RegExp](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp)'''
82 
83  def __init__(self, pattern, flags=''):
84  '''The ApgExp constructor.
85  @param pattern The SABNF pattern to match.
86  @param flags A string of characters specifying
87  the operation characteristics. May be any of the following:
88  - c display results as lists of integer "character" codes
89  - g global matching mode - last_index follows
90  the previous matched phrase
91  - t trace a trace of each match attempt is displayed
92  - y sticky similar to global except that the next match
93  must be at exactly last_index
94 
95  Note that:
96  - letters may be in any order
97  - multiple occurrances of letters allowed, the last occurrance wins
98  - the global, g, and sticky, y, flags are mutually exclusive
99  - any letter not in this list will raise and exception
100  '''
101  self._character_codes__character_codes_ = False
102  self._global__global_ = False
103  self._trace__trace_ = False
104  self._sticky__sticky_ = False
105  for ll in flags:
106  if(ll == 'c'):
107  self._character_codes__character_codes_ = True
108  elif(ll == 'g'):
109  self._global__global_ = True
110  self._sticky__sticky_ = False
111  elif(ll == 't'):
112  self._trace__trace_ = True
113  elif(ll == 'y'):
114  self._sticky__sticky_ = True
115  self._global__global_ = False
116  else:
117  raise Exception('flag ' + ll + ' not recognized')
118  self.flagsflags = ''
119  if(self._character_codes__character_codes_):
120  self.flagsflags += 'c'
121  if(self._global__global_):
122  self.flagsflags += 'g'
123  if(self._trace__trace_):
124  self.flagsflags += 't'
125  if(self._sticky__sticky_):
126  self.flagsflags += 'y'
127 
128  api = Api()
129  api.generate(pattern)
130  if(api.errors):
131  raise Exception('Pattern syntax error: \n' + api.display_errors())
132  self.patternpattern = pattern
133  self.grammargrammar = api.grammar
134  self.parserparser = Parser(self.grammargrammar)
135  self.__ast__ast = None
136  if(self._trace__trace_):
137  mode = 'd' if(self._character_codes__character_codes_) else 'dc'
138  Trace(self.parserparser, mode=mode)
139  self.rulesrules = {}
140  self.namesnames = {}
141  self.last_indexlast_index = 0
142  self.max_tree_depthmax_tree_depth = 0
143  self.max_node_hitsmax_node_hits = 0
144 
145  def __callback_factory(self, name):
146  def fn(state, source, index, length, data):
147  if(state == id.SEM_POST):
148  self.rulesrules[name.lower()].append(source[index:index + length])
149  return fn
150 
151  def __get_input(self, input):
152  if(self._character_codes__character_codes_):
153  if(isinstance(input, tuple)):
154  input_tuple = input
155  else:
156  # input must be a list or tuple of integers
157  msg = '"c" flag is set - '
158  msg += 'input must be a tuple of integers'
159  raise Exception(msg)
160  else:
161  if(isinstance(input, str)):
162  input_tuple = utils.string_to_tuple(input)
163  else:
164  # input must be a string
165  msg = '"c" flag is not set - '
166  msg += 'input must be a string'
167  raise Exception(msg)
168  return input_tuple
169 
170  def set_tree_depth(self, depth):
171  '''Limit the maximum tree depth that the parser may make.
172  @param depth The maximum allowed tree node depth.
173  If the parser exceeds this limit an exception is raised.'''
174  self.max_tree_depthmax_tree_depth = max(0, depth)
175  self.parserparser.set_node_hit_limit(self.max_tree_depthmax_tree_depth)
176 
177  def set_node_hits(self, hits):
178  '''Limit the maximum number of parse tree nodes that the parser may visit.
179  @param hits The maximum allowed number of node hits the parser can make.
180  If the parser exceeds this limit an exception is raised.'''
181  self.max_node_hitsmax_node_hits = max(0, hits)
182  self.parserparser.set_node_hit_limit(self.max_node_hitsmax_node_hits)
183 
184  def define_udts(self, callbacks):
185  '''UDTs are user-written callback functions for specialized pattern matching.
186  Callback functions must be defined for all UDTs in the SABNF grammar syntax.
187  @param callbacks A dictionary defining one or more callbacks.
188  Multiple calls may be made until all UDT callback are defined.
189  callbacks = {'udt1': func[[, 'udt2': func2], etc.]}
190  '''
191  if(len(self.parserparser.udts)):
192  items = callbacks.items()
193  for item in items:
194  name = item[0].lower()
195  name_found = False
196  for udt in self.parserparser.udts:
197  if(udt['lower'] == name):
198  name_found = True
199  break
200  if(not name_found):
201  raise(Exception('UDT name ' + name + ' not found'))
202  # if we get here, all names ok
203  self.parserparser.add_callbacks(callbacks)
204  else:
205  raise Exception('pattern has no UDTs')
206 
207  def include(self, names=[]):
208  '''Define the list of rule/UDT name phrases to be included
209  in the matched results.
210  @param names A list of rule/UDT names.
211  An empty list will include ALL rules and UDTs.
212  Invalid names will
213  raise an Exception.
214  '''
215  self.__ast__ast = Ast(self.parserparser)
216  if(len(names) == 0):
217  # add all names
218  for rule in self.parserparser.rules:
219  self.namesnames[rule['lower']] = rule['name']
220  self.rulesrules[rule['lower']] = []
221  self.__ast__ast.add_callback(
222  rule['name'], self.__callback_factory__callback_factory(
223  rule['name']))
224  for udt in self.parserparser.udts:
225  self.namesnames[udt['lower']] = udt['name']
226  self.rulesrules[udt['lower']] = []
227  self.__ast__ast.add_callback(
228  udt['name'], self.__callback_factory__callback_factory(
229  udt['name']))
230  else:
231  # add only names in list
232  for name in names:
233  # find the name
234  name_found = False
235  name_lower = name.lower()
236  for rule in self.parserparser.rules:
237  if(rule['lower'] == name_lower):
238  self.namesnames[rule['lower']] = rule['name']
239  self.rulesrules[rule['lower']] = []
240  self.__ast__ast.add_callback(
241  rule['name'], self.__callback_factory__callback_factory(
242  rule['name']))
243  name_found = True
244  break
245  if(not name_found and len(self.parserparser.udts)):
246  for udt in self.parserparser.udts:
247  if(udt['lower'] == name_lower):
248  self.namesnames[udt['lower']] = udt['name']
249  self.rulesrules[udt['lower']] = []
250  self.__ast__ast.add_callback(
251  udt['name'], self.__callback_factory__callback_factory(
252  udt['name']))
253  name_found = True
254  break
255  if(not name_found):
256  raise Exception(name + ' not a rule or UDT name')
257 
258  def exclude(self, names=[]):
259  '''Define the list of rule/UDT name phrases to be excluded
260  from the matched results.
261  @param names A list of rule/UDT names.
262  An empty list will include ALL rules and UDTs.
263  Invalid names will
264  raise an Exception.
265  '''
266  self.__ast__ast = Ast(self.parserparser)
267  if(len(names) == 0):
268  # include all names
269  self.includeinclude(names)
270  return
271  # generate a list of all names
272  names_lower = []
273  for name in names:
274  names_lower.append(name.lower())
275  for rule in self.parserparser.rules:
276  if(rule['lower'] not in names_lower):
277  self.namesnames[rule['lower']] = rule['name']
278  self.rulesrules[rule['name']] = []
279  self.__ast__ast.add_callback(
280  rule['name'], self.__callback_factory__callback_factory(
281  rule['name']))
282  for udt in self.parserparser.udts:
283  if(udt['lower'] not in names_lower):
284  self.namesnames[udt['lower']] = udt['name']
285  self.rulesrules[udt['name']] = []
286  self.__ast__ast.add_callback(
287  udt['name'], self.__callback_factory__callback_factory(
288  udt['name']))
289 
290  def exec(self, input):
291  '''Execute the pattern match.
292  Search for a match begins at last_index.
293  (Note: last_index can be set prior to calling exec()
294  with ApgExp.last_index = value.)
295  If the g or y flag is set, last_index is set to the
296  next character beyond the matched pattern
297  or incremented by one if the matched pattern is empty.
298  If the pattern is not matched, last_index is always set to 0.
299  @param input The input as a string or tuple of character codes
300  if the "c" flag is set.
301  @returns Returns the result object if pattern is matched.
302  None otherwise.
303  '''
304  input_tuple = self.__get_input__get_input(input)
305  sub_beg = self.last_indexlast_index
306  sub_end = len(input_tuple)
307  if(sub_beg >= sub_end):
308  # user may have set bad value for last_index
309  return None
310  match_result = None
311  if(self._sticky__sticky_):
312  if(self._trace__trace_):
313  print()
314  print('trace beginning at sticky character ' + str(sub_beg))
315  parser_result = self.parserparser.parse(input_tuple, sub_begin=sub_beg)
316  if(parser_result.state == id.MATCH
317  or parser_result.state == id.EMPTY):
318  # set up return result
319  self.last_indexlast_index = sub_beg + \
320  max(1, parser_result.phrase_length)
321  if(len(self.rulesrules)):
322  data = {}
323  self.__ast__ast.translate(data)
324  match_result = Result(
325  input, sub_beg,
326  parser_result.phrase_length,
327  parser_result.node_hits,
328  parser_result.max_tree_depth,
329  self.rulesrules, self.namesnames, self.__ast__ast, self._character_codes__character_codes_)
330  else:
331  while sub_beg < sub_end:
332  if(self._trace__trace_):
333  print()
334  print('trace beginning at character ' + str(sub_beg))
335  parser_result = self.parserparser.parse(
336  input_tuple, sub_begin=sub_beg)
337  self.last_indexlast_index = 0
338  if(parser_result.state == id.MATCH
339  or parser_result.state == id.EMPTY):
340  # set up return result
341  if(self._global__global_):
342  self.last_indexlast_index = sub_beg + \
343  max(1, parser_result.phrase_length)
344  if(len(self.rulesrules)):
345  for key in self.rulesrules.keys():
346  # clear the rules
347  self.rulesrules[key] = []
348  data = {}
349  self.__ast__ast.translate(data)
350  match_result = Result(
351  input, sub_beg,
352  parser_result.phrase_length,
353  parser_result.node_hits,
354  parser_result.max_tree_depth,
355  self.rulesrules, self.namesnames, self.__ast__ast,
356  self._character_codes__character_codes_)
357  break
358  sub_beg += 1
359  return match_result
360 
361  def test(self, input):
362  '''Same as @ref exec() except for the return.
363  @returns Returns True if a pattern match is found, False otherwise.'''
364  input_tuple = self.__get_input__get_input(input)
365  sub_beg = self.last_indexlast_index
366  sub_end = len(input_tuple)
367  if(sub_beg >= sub_end):
368  # user may have set bad value for last_index
369  return False
370  test_result = False
371  if(self._sticky__sticky_):
372  if(self._trace__trace_):
373  print()
374  print('trace beginning at sticky character ' + str(sub_beg))
375  parser_result = self.parserparser.parse(input_tuple, sub_begin=sub_beg)
376  if(parser_result.state == id.MATCH
377  or parser_result.state == id.EMPTY):
378  self.last_indexlast_index = sub_beg + \
379  max(1, parser_result.phrase_length)
380  test_result = True
381  else:
382  self.last_indexlast_index = 0
383  else:
384  while sub_beg < sub_end:
385  if(self._trace__trace_):
386  print()
387  print('trace beginning at character ' + str(sub_beg))
388  parser_result = self.parserparser.parse(
389  input_tuple, sub_begin=sub_beg)
390  self.last_indexlast_index = 0
391  if(parser_result.state == id.MATCH
392  or parser_result.state == id.EMPTY):
393  # set up return result
394  if(self._global__global_):
395  self.last_indexlast_index = sub_beg + \
396  max(1, parser_result.phrase_length)
397  test_result = True
398  break
399  sub_beg += 1
400  return test_result
401 
402  def split(self, input, limit=0):
403  '''Split the input string on the matched delimiters.
404  The ApgExp pattern defines the delimiters.
405  Works similar to the
406  [JavaScript String.split()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split)
407  function.
408  All flags except the character code flag "c" are ignored.
409  If the "c" flag is set, substitute "tuple of character codes" for string.
410  - if the input string is empty, the output list contains
411  a single empty string
412  - if the pattern matches the entire string, the output list contains
413  a single empty string.
414  - if no pattern matches are found in the input string,
415  the output list contains a single string which
416  is a copy of the input string.
417  - if the pattern finds multiple matches, the output list contains
418  a each of the strings between the matches
419  - if the pattern matches the empty string, the output will be a list
420  of the single characters.
421  @param input The input string or tuple of character codes.
422  @param limit If limit > 0 only limit delimiters are matched.
423  The trailing string suffix, if any, is ignored.
424  @returns Returns a list of strings or character code tuples.
425  '''
426  def gen_output(intervals):
427  # from a list of index intervals, generate the
428  # list of output strings or tuples
429  if(len(intervals) == 0):
430  if(self._character_codes__character_codes_):
431  return [()]
432  return ['']
433  gen = []
434  for interval in intervals:
435  tup = input_tuple[interval[0]:interval[1]]
436  if(self._character_codes__character_codes_):
437  gen.append(tup)
438  else:
439  gen.append(utils.tuple_to_string(tup))
440  return gen
441 
442  input_tuple = self.__get_input__get_input(input)
443  if(len(input_tuple) == 0):
444  # input is empty, return empty string or character code array
445  return gen_output([])
446  if(limit <= 0):
447  limit = sys.maxsize
448  sub_beg = 0
449  sub_end = len(input_tuple)
450  intervals = []
451  while(sub_beg < sub_end and limit > 0):
452  parser_result = self.parserparser.parse(
453  input_tuple, sub_begin=sub_beg)
454  if(parser_result.state == id.MATCH):
455  limit -= 1
456  intervals.append(
457  [sub_beg, sub_beg + parser_result.phrase_length])
458  sub_beg += parser_result.phrase_length
459  elif(parser_result.state == id.EMPTY):
460  limit -= 1
461  intervals.append([sub_beg, sub_beg])
462  sub_beg += 1
463  else:
464  sub_beg += 1
465  len_intervals = len(intervals)
466  if(len_intervals == 0):
467  # no match, return original string
468  return gen_output([[0, sub_end]])
469  beg = 0
470  out_put = []
471  for interval in intervals:
472  if(beg < interval[0]):
473  out_put.append([beg, interval[0]])
474  beg = interval[1]
475  intervals_end = intervals[len_intervals - 1][1]
476  if(limit > 0):
477  # add the remainder, if any
478  if(intervals_end < sub_end):
479  out_put.append([intervals_end, sub_end])
480  return gen_output(out_put)
481 
482  def replace(self, input, replacement):
483  '''Replace matched patterns. If a pattern match is found in "input"
484  it will be replaced with "replacement".
485  Works similar to the
486  [JavaScript String.replace()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace)
487  function.
488  If the "g" or "y" flags are set, all matched patterns are replaced.
489  Otherwise, only the first match is replaced.
490  @param input The string to look for pattern matches in.
491  If the "c" flag is set, input must be a tuple of integers.
492  Otherwise, it is a string.
493  @param replacement This may be a simple replacement string,
494  a complex replacement string with special characters or
495  a function that returns the replacement string.
496  If the "c" flag is not set, replacement must be a string,
497  possibly with special characters or
498  a function that returns a string. Special string characters are:
499  - $$ substitute $
500  - $` substitute the matched left context
501  - $& substitute the matched pattern itself
502  - $' substitute the matched right context
503  - ${name} substitue the matched rule/UDT name(case insensitive),
504  note that if this rule has no match an empty string will be used.
505  <br>
506  <br>
507  The function must have the prototype
508  - fn(input, result) where input is the original input string
509  and result is the pattern matching result object.
510  The function must return a string
511  <br>
512  <br>
513  If the "c" flag is set, replacement must be a tuple of integers
514  or a function that returns a tuple of integers.
515  In this case there are no special characters comparable to the string
516  special characters. However, since the function gets the result
517  as an argument, it can be used for the same purpose.
518  The function must have the prototype:
519  - fn(input, result) where input is the original input tuple
520  and result is the pattern matching result object.
521  The function must return a tuple of integers.
522  @returns Returns the input string/tuple "input" with one or all
523  matched patterns replaced with the replacement string,
524  tuple or function.
525  '''
526  # get the result(s)
527  self.last_indexlast_index = 0
528  res = self.execexec(input)
529  if(not res):
530  # no pattern matches to replace
531  return copy.copy(input)
532  results = []
533  results.append(copy.deepcopy(res))
534  if(self._global__global_ or self._sticky__sticky_):
535  while(res):
536  res = self.execexec(input)
537  if(res):
538  results.append(copy.deepcopy(res))
539 
540  if(isinstance(replacement, types.FunctionType)):
541  # handle callable function for replacement
542  output = copy.copy(input)
543  diff = 0
544  for result in results:
545  pref = output[:diff + result.indices[0]]
546  suff = output[diff + result.indices[1]:]
547  repl = replacement(input, result)
548  if(self._character_codes__character_codes_):
549  if(not isinstance(repl, tuple)):
550  msg = 'replacement function must return'
551  msg += ' a tuple of integers'
552  raise Exception(msg)
553  else:
554  if(not isinstance(repl, str)):
555  msg = 'replacement function must return'
556  msg += ' a string'
557  raise Exception(msg)
558  diff += len(repl) + result.indices[0] - result.indices[1]
559  output = pref + repl + suff
560  elif(self._character_codes__character_codes_):
561  if(not isinstance(replacement, tuple)):
562  raise Exception(
563  'replace(): "c" flag set - input must be a tuple')
564  # handle tuple replacement
565  output = copy.copy(input)
566  diff = 0
567  for result in results:
568  pref = output[:diff + result.indices[0]]
569  suff = output[diff + result.indices[1]:]
570  diff += len(replacement) + \
571  result.indices[0] - result.indices[1]
572  output = pref + replacement + suff
573  else:
574  if(not isinstance(replacement, str)):
575  raise Exception(
576  'replace(): "c" flag not set - input must be a string')
577  # handle string replacement
578  output = copy.copy(input)
579  diff = 0
580  for result in results:
581  pref = output[:diff + result.indices[0]]
582  suff = output[diff + result.indices[1]:]
583  repl = replace_special_chars(replacement, result)
584  diff += len(repl) + result.indices[0] - result.indices[1]
585  output = pref + repl + suff
586  return output
587 
588 
589 def replace_special_chars(replacement, result):
590  # handle replacing special characters in replacement string
591  special = copy.copy(replacement)
592  special_found = True
593  start = 0
594  while(special_found):
595  special_found = False
596  for i in range(start, len(special)):
597  if(special[i] == '$'):
598  pref = special[:i]
599  suf = special[i + 2:]
600  special_found = True
601  start = i + 2
602  if(special[i + 1] == '$'):
603  special = pref + '$' + suf
604  break
605  if(special[i + 1] == '&'):
606  special = pref + result.match + suf
607  break
608  if(special[i + 1] == '`'):
609  special = pref + result.left_context + suf
610  break
611  if(special[i + 1] == "'"):
612  special = pref + result.right_context + suf
613  break
614  if(special[i + 1] == "{"):
615  name = None
616  for j in range(i + 2, len(special)):
617  if(special[j] == '}'):
618  name = special[i + 2:j]
619  suf = special[j + 1:]
620  # start = j + 1
621  break
622  if(not name):
623  msg = 'replace(): ${name}, name or closing bracket '
624  msg += 'not found'
625  raise Exception(msg)
626  lower = name.lower()
627  if(result.rules.get(lower)):
628  last_match = len(result.rules[lower]) - 1
629  name_string = utils.tuple_to_string(
630  result.rules[lower][last_match])
631  else:
632  # rule name did not match a string
633  name_string = ''
634  special = pref + name_string + suf
635  start = len(pref) + len(name_string)
636  break
637  return special
The API class.
Definition: api.py:61
The ApgExp class provides a pattern-matching engine similar to JavaScript's RegExp
Definition: exp.py:79
def __get_input(self, input)
Definition: exp.py:151
def __callback_factory(self, name)
Definition: exp.py:145
def define_udts(self, callbacks)
UDTs are user-written callback functions for specialized pattern matching.
Definition: exp.py:184
def exec(self, input)
Execute the pattern match.
Definition: exp.py:290
def include(self, names=[])
Define the list of rule/UDT name phrases to be included in the matched results.
Definition: exp.py:207
def set_tree_depth(self, depth)
Limit the maximum tree depth that the parser may make.
Definition: exp.py:170
def replace(self, input, replacement)
Replace matched patterns.
Definition: exp.py:482
def set_node_hits(self, hits)
Limit the maximum number of parse tree nodes that the parser may visit.
Definition: exp.py:177
def exclude(self, names=[])
Define the list of rule/UDT name phrases to be excluded from the matched results.
Definition: exp.py:258
def split(self, input, limit=0)
Split the input string on the matched delimiters.
Definition: exp.py:402
def test(self, input)
Same as exec() except for the return.
Definition: exp.py:361
def __init__(self, pattern, flags='')
The ApgExp constructor.
Definition: exp.py:83
A class for returning the results of a pattern match.
Definition: exp.py:15
def __init__(self, source, begin, length, node_hits, tree_depth, rules, names, ast, codes=False)
The Result class constructor.
Definition: exp.py:29
def __str__(self)
print(Result) will call this function for a display of the object.
Definition: exp.py:45
A class for capturing the AST as the parser traverses the parse tree.
Definition: ast.py:69
The Parser class for parsing an APG grammar.
Definition: parser.py:60
Class for tracing and displaying the progress of the parser through the parse tree.
Definition: trace.py:15
def replace_special_chars(replacement, result)
Definition: exp.py:589
def fn(input, result)
Python APG, Version 1.0, is licensed under the 2-Clause BSD License,
an Open Source Initiative Approved License.