docs: def parse(self, expr, simplify=False):
          """
          Return a boolean expression parsed from `expr` either a unicode string
          or tokens iterable.
  
          Optionally simplify the expression if `simplify` is True.
  
          Raise ParseError on errors.
  
          If `expr` is a string, the standard `tokenizer` is used for tokenization
          and the algebra configured Symbol type is used to create Symbol
          instances from Symbol tokens.
  
          If `expr` is an iterable, it should contain 3-tuples of: (token_type,
          token_string, token_position). In this case, the `token_type` can be
          a Symbol instance or one of the TOKEN_* constant types.
          See the `tokenize()` method for detailed specification.
          """
  
          precedence = {self.NOT: 5, self.AND: 10, self.OR: 15, TOKEN_LPAR: 20}
  
          if isinstance(expr, basestring):
              tokenized = self.tokenize(expr)
          else:
              tokenized = iter(expr)
  
          if TRACE_PARSE:
              tokenized = list(tokenized)
              print('tokens:')
              map(print, tokenized)
              tokenized = iter(tokenized)
  
          # the abstract syntax tree for this expression that will be build as we
          # process tokens
          # the first two items are None
          # symbol items are appended to this structure
          ast = [None, None]
  
          def is_sym(_t):
              return isinstance(_t, Symbol) or _t in (TOKEN_TRUE, TOKEN_FALSE, TOKEN_SYMBOL)
  
          def is_operator(_t):
              return _t in (TOKEN_AND, TOKEN_OR)
  
          prev_token = None
          for token_type, token_string, token_position in tokenized:
              if TRACE_PARSE:
                  print('\nprocessing token_type:', repr(token_type), 'token_string:', repr(token_string), 'token_position:', repr(token_position))
  
              if prev_token:
                  prev_token_type, _prev_token_string, _prev_token_position = prev_token
                  if TRACE_PARSE:
                      print('  prev_token:', repr(prev_token))
  
                  if is_sym(prev_token_type) and (is_sym(token_type)):  # or token_type == TOKEN_LPAR) :
                      raise ParseError(token_type, token_string, token_position, PARSE_INVALID_SYMBOL_SEQUENCE)
  
                  if is_operator(prev_token_type) and (is_operator(token_type) or token_type == TOKEN_RPAR):
                      raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE)
  
              else:
                  if is_operator(token_type):
                      raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE)
  
              if token_type == TOKEN_SYMBOL:
                  ast.append(self.Symbol(token_string))
                  if TRACE_PARSE:
                      print(' ast: token_type is TOKEN_SYMBOL: append new symbol', repr(ast))
  
              elif isinstance(token_type, Symbol):
                  ast.append(token_type)
                  if TRACE_PARSE:
                      print(' ast: token_type is Symbol): append existing symbol', repr(ast))
  
              elif token_type == TOKEN_TRUE:
                  ast.append(self.TRUE)
                  if TRACE_PARSE: print(' ast: token_type is TOKEN_TRUE:', repr(ast))
  
              elif token_type == TOKEN_FALSE:
                  ast.append(self.FALSE)
                  if TRACE_PARSE: print(' ast: token_type is TOKEN_FALSE:', repr(ast))
  
              elif token_type == TOKEN_NOT:
                  ast = [ast, self.NOT]
                  if TRACE_PARSE: print(' ast: token_type is TOKEN_NOT:', repr(ast))
  
              elif token_type == TOKEN_AND:
  #                if not prev_token or not is_sym(prev_token_type):
  #                    raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE)
  
                  ast = self._start_operation(ast, self.AND, precedence)
                  if TRACE_PARSE:
                      print('  ast:token_type is TOKEN_AND: start_operation', ast)
  
              elif token_type == TOKEN_OR:
  #                if not prev_token or not is_sym(prev_token_type):
  #                    raise ParseError(token_type, token_string, token_position, PARSE_INVALID_OPERATOR_SEQUENCE)
  
                  ast = self._start_operation(ast, self.OR, precedence)
                  if TRACE_PARSE:
                      print('  ast:token_type is TOKEN_OR: start_operation', ast)
  
              elif token_type == TOKEN_LPAR:
                  if prev_token:
                      # Check that an opening parens is preceded by a function
                      # or an opening parens
                      if prev_token_type not in (TOKEN_NOT, TOKEN_AND, TOKEN_OR, TOKEN_LPAR):
                          raise ParseError(token_type, token_string, token_position, PARSE_INVALID_NESTING)
                  ast = [ast, TOKEN_LPAR]
  
              elif token_type == TOKEN_RPAR:
                  while True:
                      if ast[0] is None:
                          raise ParseError(token_type, token_string, token_position, PARSE_UNBALANCED_CLOSING_PARENS)
  
                      if ast[1] is TOKEN_LPAR:
                          ast[0].append(ast[2])
                          if TRACE_PARSE: print('ast9:', repr(ast))
                          ast = ast[0]
                          if TRACE_PARSE: print('ast10:', repr(ast))
                          break
  
                      if isinstance(ast[1], int):
                          raise ParseError(token_type, token_string, token_position, PARSE_UNBALANCED_CLOSING_PARENS)
  
                      # the parens are properly nested
                      # the top ast node should be a function subclass
                      if not (inspect.isclass(ast[1]) and issubclass(ast[1], Function)):
                          raise ParseError(token_type, token_string, token_position, PARSE_INVALID_NESTING)
  
                      subex = ast[1](*ast[2:])
                      ast[0].append(subex)
                      if TRACE_PARSE: print('ast11:', repr(ast))
                      ast = ast[0]
                      if TRACE_PARSE: print('ast12:', repr(ast))
              else:
                  raise ParseError(token_type, token_string, token_position, PARSE_UNKNOWN_TOKEN)
  
              prev_token = (token_type, token_string, token_position)
  
          try:
              while True:
                  if ast[0] is None:
                      if TRACE_PARSE: print('ast[0] is None:', repr(ast))
                      if ast[1] is None:
                          if TRACE_PARSE: print('  ast[1] is None:', repr(ast))
                          if len(ast) != 3:
                              raise ParseError(error_code=PARSE_INVALID_EXPRESSION)
                          parsed = ast[2]
                          if TRACE_PARSE: print('    parsed = ast[2]:', repr(parsed))
  
                      else:
                          # call the function in ast[1] with the rest of the ast as args
                          parsed = ast[1](*ast[2:])
                          if TRACE_PARSE: print('  parsed = ast[1](*ast[2:]):', repr(parsed))
                      break
                  else:
                      if TRACE_PARSE: print('subex = ast[1](*ast[2:]):', repr(ast))
                      subex = ast[1](*ast[2:])
                      ast[0].append(subex)
                      if TRACE_PARSE: print('  ast[0].append(subex):', repr(ast))
                      ast = ast[0]
                      if TRACE_PARSE: print('    ast = ast[0]:', repr(ast))
          except TypeError:
              raise ParseError(error_code=PARSE_INVALID_EXPRESSION)
  
          if simplify:
              return parsed.simplify()
  
          if TRACE_PARSE: print('final parsed:', repr(parsed))
          return parsed