"""All major API points and command-line tools""" import pkg_resources import six from argparse import ArgumentParser, FileType from lxml import etree from xmldiff import diff, formatting, patch __version__ = pkg_resources.require("xmldiff")[0].version FORMATTERS = { 'diff': formatting.DiffFormatter, 'xml': formatting.XMLFormatter, 'old': formatting.XmlDiffFormatter, } def diff_trees(left, right, diff_options=None, formatter=None): """Takes two lxml root elements or element trees""" if formatter is not None: formatter.prepare(left, right) if diff_options is None: diff_options = {} differ = diff.Differ(**diff_options) diffs = differ.diff(left, right) if formatter is None: return list(diffs) return formatter.format(diffs, left) def _diff(parse_method, left, right, diff_options=None, formatter=None): normalize = bool(getattr(formatter, 'normalize', 1) & formatting.WS_TAGS) parser = etree.XMLParser(remove_blank_text=normalize) left_tree = parse_method(left, parser) right_tree = parse_method(right, parser) return diff_trees(left_tree, right_tree, diff_options=diff_options, formatter=formatter) def diff_texts(left, right, diff_options=None, formatter=None): """Takes two Unicode strings containing XML""" return _diff(etree.fromstring, left, right, diff_options=diff_options, formatter=formatter) def diff_files(left, right, diff_options=None, formatter=None): """Takes two filenames or streams, and diffs the XML in those files""" return _diff(etree.parse, left, right, diff_options=diff_options, formatter=formatter) def make_diff_parser(): parser = ArgumentParser(description='Create a diff for two XML files.', add_help=False) parser.add_argument('file1', type=FileType('r'), help='The first input file.') parser.add_argument('file2', type=FileType('r'), help='The second input file.') parser.add_argument('-h', '--help', action='help', help='Show this help message and exit.') parser.add_argument('-v', '--version', action='version', help='Display version and exit.', version='xmldiff %s' % __version__) parser.add_argument('-f', '--formatter', default='diff', choices=list(FORMATTERS.keys()), help='Formatter selection.') parser.add_argument('-w', '--keep-whitespace', action='store_true', help='Do not strip ignorable whitespace.') parser.add_argument('-p', '--pretty-print', action='store_true', help='Try to make XML output more readable.') parser.add_argument('-F', type=float, help='A value between 0 and 1 that determines how ' 'similar nodes must be to match.') parser.add_argument('--unique-attributes', type=str, nargs='?', default='{http://www.w3.org/XML/1998/namespace}id', help='A comma separated list of attributes ' 'that uniquely identify a node. Can be empty. ' 'Unique attributes for certain elements can ' 'be specified in the format {NS}element@attr.') parser.add_argument('--ratio-mode', default='fast', choices={'accurate', 'fast', 'faster'}, help='Choose the node comparison optimization.') parser.add_argument('--fast-match', action='store_true', help='A faster, less optimal match run.') return parser def _parse_uniqueattrs(uniqueattrs): if uniqueattrs is None: return [] return [ attr if '@' not in attr else attr.split('@', 1) for attr in uniqueattrs.split(',') ] def diff_command(args=None): parser = make_diff_parser() args = parser.parse_args(args=args) if args.keep_whitespace: normalize = formatting.WS_NONE else: normalize = formatting.WS_BOTH formatter = FORMATTERS[args.formatter](normalize=normalize, pretty_print=args.pretty_print) diff_options = {'ratio_mode': args.ratio_mode, 'F': args.F, 'fast_match': args.fast_match, 'uniqueattrs': _parse_uniqueattrs(args.unique_attributes), } result = diff_files(args.file1, args.file2, diff_options=diff_options, formatter=formatter) print(result) def patch_tree(actions, tree): """Takes an lxml root element or element tree, and a list of actions""" patcher = patch.Patcher() return patcher.patch(actions, tree) def patch_text(actions, tree): """Takes a string with XML and a string with actions""" tree = etree.fromstring(tree) actions = patch.DiffParser().parse(actions) tree = patch_tree(actions, tree) return etree.tounicode(tree) def patch_file(actions, tree): """Takes two filenames or streams, one with XML the other a diff""" tree = etree.parse(tree) if isinstance(actions, six.string_types): # It's a string, so it's a filename with open(actions) as f: actions = f.read() else: # We assume it's a stream actions = actions.read() actions = patch.DiffParser().parse(actions) tree = patch_tree(actions, tree) return etree.tounicode(tree) def make_patch_parser(): parser = ArgumentParser(description='Patch an XML file with an xmldiff', add_help=False) parser.add_argument('patchfile', type=FileType('r'), help='An xmldiff diff file.') parser.add_argument('xmlfile', type=FileType('r'), help='An unpatched XML file.') parser.add_argument('-h', '--help', action='help', help='Show this help message and exit.') parser.add_argument('-v', '--version', action='version', help='Display version and exit.', version='xmldiff %s' % __version__) return parser def patch_command(args=None): parser = make_patch_parser() args = parser.parse_args(args=args) result = patch_file(args.patchfile, args.xmlfile) print(result)