pax_global_header00006660000000000000000000000064131121353010014500gustar00rootroot0000000000000052 comment=2972f0ebcecc274288f359bc001175d1d2dc1f6b python-unidiff-0.5.4/000077500000000000000000000000001311213530100144515ustar00rootroot00000000000000python-unidiff-0.5.4/.gitignore000066400000000000000000000001101311213530100164310ustar00rootroot00000000000000# Python *.py[cod] __pycache__ build dist unidiff.egg-info # Vim *.swp python-unidiff-0.5.4/.travis.yml000066400000000000000000000001541311213530100165620ustar00rootroot00000000000000language: python python: - "2.7" - "3.2" - "3.3" - "3.4" - "3.5" - "3.6" script: ./run_tests.sh python-unidiff-0.5.4/AUTHORS000066400000000000000000000006121311213530100155200ustar00rootroot00000000000000Main developer -------------- * Matias Bordese (`@matiasb`_) Contributors ------------ * Natalia Bidart (`@nessita`_) * Jacobo de Vera (`@jdevera`_) * Lei Zhang (`@antiAgainst`_) * Sumeet Agarwal (`@sumeet`_) * Philipp Kewisch (`@kewisch`_) * Allan Lewis (`@allanlewis`_) * Andrew Lapidas (`@alapidas`_) * Daniel Thompson (`@daniel-thompson`) * Sebastian Kreft (`@sk-`) python-unidiff-0.5.4/HISTORY000066400000000000000000000012731311213530100155400ustar00rootroot00000000000000History ------- 0.5.4 - 2017-05-26 ------------------ * Added PatchSet.from_string helper. * Do not install tests as top-level package. 0.5.3 - 2017-04-10 ------------------ * Re-released 0.5.2 as 0.5.3 because of issues with PyPI. 0.5.2 - 2016-02-02 ------------------ * Added diff line number to Line metadata. * Optimizations for large hunks. * Fix for git empty new lines. * Added (optional) errors parameter to PatchSet.from_filename, to specify how to handle encoding errors. 0.5.1 - 2015-01-18 ------------------ * Added (optional) encoding parameter to PatchSet. * Added support to get any iterable as PatchSet diff argument. 0.5 - 2014-12-14 ---------------- * Release on PyPI. python-unidiff-0.5.4/LICENSE000066400000000000000000000020701311213530100154550ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2012 Matias Bordese Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. python-unidiff-0.5.4/MANIFEST.in000066400000000000000000000001301311213530100162010ustar00rootroot00000000000000include bin/* include tests/samples/* include HISTORY include LICENSE include README.md python-unidiff-0.5.4/README.md000066400000000000000000000066301311213530100157350ustar00rootroot00000000000000Unidiff ======= Simple Python library to parse and interact with unified diff data. [![Build Status](https://travis-ci.org/matiasb/python-unidiff.png?branch=master)](https://travis-ci.org/matiasb/python-unidiff) Installing unidiff ------------------ $ pip install unidiff Quick start ----------- >>> import urllib2 >>> from unidiff import PatchSet >>> diff = urllib2.urlopen('https://github.com/matiasb/python-unidiff/pull/3.diff') >>> encoding = diff.headers.getparam('charset') >>> patch = PatchSet(diff, encoding=encoding) >>> patch , , ]> >>> patch[0] >>> patch[0].is_added_file True >>> patch[0].added 6 >>> patch[1] >>> patch[1].added, patch[1].removed (20, 11) >>> len(patch[1]) 6 >>> patch[1][2] >>> patch[2] >>> print patch[2] --- a/unidiff/utils.py +++ b/unidiff/utils.py @@ -37,4 +37,3 @@ # - deleted line # \ No newline case (ignore) RE_HUNK_BODY_LINE = re.compile(r'^([- \+\\])') - Load unified diff data by instantiating PatchSet with a file-like object as argument, or using PatchSet.from_filename class method to read diff from file. A PatchSet is a list of files updated by the given patch. For each PatchedFile you can get stats (if it is a new, removed or modified file; the source/target lines; etc), besides having access to each hunk (also like a list) and its respective info. At any point you can get the string representation of the current object, and that will return the unified diff data of it. As a quick example of what can be done, check bin/unidiff file. Also, once installed, unidiff provides a command-line program that displays information from diff data (a file, or stdin). For example: $ git diff | unidiff Summary ------- README.md: +6 additions, -0 deletions 1 modified file(s), 0 added file(s), 0 removed file(s) Total: 6 addition(s), 0 deletion(s) Load a local diff file ---------------------- To instantiate PatchSet from a local file, you can use: >>> from unidiff import PatchSet >>> patch = PatchSet.from_filename('tests/samples/bzr.diff', encoding='utf-8') >>> patch , , ]> Notice the (optional) encoding parameter. If not specified, unicode input will be expected. Or alternatively: >>> import codecs >>> from unidiff import PatchSet >>> with codecs.open('tests/samples/bzr.diff', 'r', encoding='utf-8') as diff: ... patch = PatchSet(diff) ... >>> patch , , ]> Finally, you can also instantiate PatchSet passing any iterable (and encoding, if needed): >>> from unidiff import PatchSet >>> with open('tests/samples/bzr.diff', 'r') as diff: ... data = diff.readlines() ... >>> patch = PatchSet(data, encoding='utf-8') >>> patch , , ]> References ---------- * http://en.wikipedia.org/wiki/Diff_utility * http://www.artima.com/weblogs/viewpost.jsp?thread=164293 python-unidiff-0.5.4/bin/000077500000000000000000000000001311213530100152215ustar00rootroot00000000000000python-unidiff-0.5.4/bin/unidiff000077500000000000000000000034101311213530100165710ustar00rootroot00000000000000#!/usr/bin/env python from __future__ import print_function, unicode_literals import argparse import codecs import sys from unidiff import DEFAULT_ENCODING, PatchSet PY2 = sys.version_info[0] == 2 DESCRIPTION = """Unified diff metadata. Examples: $ git diff | unidiff $ hg diff | unidiff --show-diff $ unidiff -f patch.diff """ def get_parser(): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=DESCRIPTION) parser.add_argument('--show-diff', action="store_true", default=False, dest='show_diff', help='output diff to stdout') parser.add_argument('-f', '--file', dest='diff_file', type=argparse.FileType('r'), help='if not specified, read diff data from stdin') return parser if __name__ == '__main__': parser = get_parser() args = parser.parse_args() encoding = DEFAULT_ENCODING if args.diff_file: diff_file = args.diff_file else: encoding = sys.stdin.encoding or encoding diff_file = sys.stdin if PY2: diff_file = codecs.getreader(encoding)(diff_file) patch = PatchSet(diff_file) if args.show_diff: print(patch) print() print('Summary') print('-------') additions = 0 deletions = 0 for f in patch: additions += f.added deletions += f.removed print('%s:' % f.path, '+%d additions,' % f.added, '-%d deletions' % f.removed) print() print('%d modified file(s), %d added file(s), %d removed file(s)' % ( len(patch.modified_files), len(patch.added_files), len(patch.removed_files))) print('Total: %d addition(s), %d deletion(s)' % (additions, deletions)) python-unidiff-0.5.4/run_tests.sh000077500000000000000000000001061311213530100170330ustar00rootroot00000000000000#! /bin/bash PYTHONPATH=unidiff python -m unittest discover -s tests/ python-unidiff-0.5.4/setup.cfg000066400000000000000000000000321311213530100162650ustar00rootroot00000000000000[bdist_wheel] universal=1 python-unidiff-0.5.4/setup.py000066400000000000000000000016271311213530100161710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Author: Matías Bordese from __future__ import unicode_literals from setuptools import setup, find_packages from unidiff import VERSION setup( name='unidiff', version=VERSION, description="Unified diff parsing/metadata extraction library.", keywords='unified diff parse metadata', author='Matias Bordese', author_email='mbordese@gmail.com', url='http://github.com/matiasb/python-unidiff', license='MIT', packages=find_packages(exclude=['tests']), scripts=['bin/unidiff'], classifiers=[ 'Intended Audience :: Developers', 'Development Status :: 4 - Beta', "Programming Language :: Python :: 2", 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', ], test_suite='tests', ) python-unidiff-0.5.4/tests/000077500000000000000000000000001311213530100156135ustar00rootroot00000000000000python-unidiff-0.5.4/tests/__init__.py000066400000000000000000000021671311213530100177320ustar00rootroot00000000000000# The MIT License (MIT) # Copyright (c) 2014 Matias Bordese # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. """Tests for unidiff.""" python-unidiff-0.5.4/tests/samples/000077500000000000000000000000001311213530100172575ustar00rootroot00000000000000python-unidiff-0.5.4/tests/samples/bzr.diff000066400000000000000000000013401311213530100207040ustar00rootroot00000000000000=== added file 'added_file' --- added_file 1970-01-01 00:00:00 +0000 +++ added_file 2013-10-13 23:44:04 +0000 @@ -0,0 +1,4 @@ +This was missing! +Adding it now. + +Only for testing purposes. \ No newline at end of file === modified file 'modified_file' --- modified_file 2013-10-13 23:53:13 +0000 +++ modified_file 2013-10-13 23:53:26 +0000 @@ -1,5 +1,7 @@ This is the original content. -This should be updated. +This is now updated. + +This is a new line. This will stay. \ No newline at end of file === removed file 'removed_file' --- removed_file 2013-10-13 23:53:13 +0000 +++ removed_file 1970-01-01 00:00:00 +0000 @@ -1,3 +0,0 @@ -This content shouldn't be here. - -This file will be removed. \ No newline at end of file python-unidiff-0.5.4/tests/samples/git.diff000066400000000000000000000013311311213530100206720ustar00rootroot00000000000000diff --git a/added_file b/added_file new file mode 100644 index 0000000..9b710f3 --- /dev/null +++ b/added_file @@ -0,0 +1,4 @@ +This was missing! +Adding it now. + +Only for testing purposes. \ No newline at end of file diff --git a/modified_file b/modified_file index c7921f5..8946660 100644 --- a/modified_file +++ b/modified_file @@ -1,5 +1,7 @@ This is the original content. -This should be updated. +This is now updated. + +This is a new line. This will stay. \ No newline at end of file diff --git a/removed_file b/removed_file deleted file mode 100644 index 1f38447..0000000 --- a/removed_file +++ /dev/null @@ -1,3 +0,0 @@ -This content shouldn't be here. - -This file will be removed. \ No newline at end of file python-unidiff-0.5.4/tests/samples/hg.diff000066400000000000000000000014051311213530100205070ustar00rootroot00000000000000diff -r 44299fd3d1a8 added_file --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/added_file Sun Oct 13 20:51:40 2013 -0300 @@ -0,0 +1,4 @@ +This was missing! +Adding it now. + +Only for testing purposes. \ No newline at end of file diff -r 44299fd3d1a8 modified_file --- a/modified_file Sun Oct 13 20:51:07 2013 -0300 +++ b/modified_file Sun Oct 13 20:51:40 2013 -0300 @@ -1,5 +1,7 @@ This is the original content. -This should be updated. +This is now updated. + +This is a new line. This will stay. \ No newline at end of file diff -r 44299fd3d1a8 removed_file --- a/removed_file Sun Oct 13 20:51:07 2013 -0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -This content shouldn't be here. - -This file will be removed. \ No newline at end of file python-unidiff-0.5.4/tests/samples/sample0.diff000066400000000000000000000023411311213530100214520ustar00rootroot00000000000000--- /path/to/original ''timestamp'' +++ /path/to/new ''timestamp'' @@ -1,3 +1,9 @@ Section Header +This is an important +notice! It should +therefore be located at +the beginning of this +document! + This part of the document has stayed the same from version to @@ -5,16 +11,10 @@ be shown if it doesn't change. Otherwise, that would not be helping to -compress the size of the -changes. - -This paragraph contains -text that is outdated. -It will be deleted in the -near future. +compress anything. It is important to spell -check this dokument. On +check this document. On the other hand, a misspelled word isn't the end of the world. @@ -22,3 +22,7 @@ this paragraph needs to be changed. Things can be added after it. + +This paragraph contains +important new additions +to this document. --- /dev/null +++ /path/to/another_new @@ -0,0 +1,9 @@ +This is an important +notice! It should +therefore be located at +the beginning of this +document! + +This part of the +document has stayed the +same from version to --- /path/to/existing +++ /dev/null @@ -1,9 +0,0 @@ -This is an important -notice! It should -therefore be located at -the beginning of this -document! - -This part of the -document has stayed the -same from version to python-unidiff-0.5.4/tests/samples/sample1.diff000066400000000000000000000014311311213530100214520ustar00rootroot00000000000000--- /path/to/original ''timestamp'' +++ /path/to/new ''timestamp'' @@ -1,3 +1,9 @@ +This is an important +notice! It should +therefore be located at +the beginning of this +document! + This part of the document has stayed the same from version to @@ -5,16 +11,13 @@ be shown if it doesn't change. Otherwise, that would not be helping to -compress the size of the -changes. - -This paragraph contains -text that is outdated. -It will be deleted in the -near future. +compress anything. It is important to spell -check this dokument. On +check this document. On the other hand, a misspelled word isn't the end of the world. @@ -22,3 +22,7 @@ this paragraph needs to be changed. Things can be added after it. + +This paragraph contains +important new additions +to this document. python-unidiff-0.5.4/tests/samples/sample2.diff000066400000000000000000000027351311213530100214630ustar00rootroot00000000000000# HG changeset patch # Parent 13ba6cbdb304cd251fbc22466cadb21019ee817f # User Bill McCloskey diff --git a/content/base/src/nsContentUtils.cpp b/content/base/src/nsContentUtils.cpp --- a/content/base/src/nsContentUtils.cpp +++ b/content/base/src/nsContentUtils.cpp @@ -6369,17 +6369,17 @@ public: nsCycleCollectionParticipant* helper) { } NS_IMETHOD_(void) NoteNextEdgeName(const char* name) { } - NS_IMETHOD_(void) NoteWeakMapping(void* map, void* key, void* val) + NS_IMETHOD_(void) NoteWeakMapping(void* map, void* key, void* kdelegate, void* val) { } bool mFound; private: void* mWrapper; }; diff --git a/js/src/jsfriendapi.cpp b/js/src/jsfriendapi.cpp --- a/js/src/jsfriendapi.cpp +++ b/js/src/jsfriendapi.cpp @@ -527,16 +527,24 @@ js::VisitGrayWrapperTargets(JSCompartmen { for (WrapperMap::Enum e(comp->crossCompartmentWrappers); !e.empty(); e.popFront()) { gc::Cell *thing = e.front().key.wrapped; if (thing->isMarked(gc::GRAY)) callback(closure, thing); } } +JS_FRIEND_API(JSObject *) +js::GetWeakmapKeyDelegate(JSObject *key) +{ + if (JSWeakmapKeyDelegateOp op = key->getClass()->ext.weakmapKeyDelegateOp) + return op(key); + return NULL; +} + JS_FRIEND_API(void) JS_SetAccumulateTelemetryCallback(JSRuntime *rt, JSAccumulateTelemetryDataCallback callback) { rt->telemetryCallback = callback; } JS_FRIEND_API(JSObject *)python-unidiff-0.5.4/tests/samples/sample3.diff000066400000000000000000000013571311213530100214630ustar00rootroot00000000000000=== added file 'added_file' --- added_file 1970-01-01 00:00:00 +0000 +++ added_file 2013-10-13 23:44:04 +0000 @@ -0,0 +1,4 @@ +This was missing! +holá mundo! + +Only for testing purposes. \ No newline at end of file === modified file 'modified_file' --- modified_file 2013-10-13 23:53:13 +0000 +++ modified_file 2013-10-13 23:53:26 +0000 @@ -1,5 +1,7 @@ This is the original content. -This should be updated. +This is now updated. + +This is a new line. -This will stay. \ No newline at end of file +This will stay. === removed file 'removed_file' --- removed_file 2013-10-13 23:53:13 +0000 +++ removed_file 1970-01-01 00:00:00 +0000 @@ -1,3 +0,0 @@ -This content shouldn't be here. - -This file will be removed. \ No newline at end of file python-unidiff-0.5.4/tests/samples/sample4.diff000066400000000000000000000013521311213530100214570ustar00rootroot00000000000000=== added file 'added_file' --- added_file 1970-01-01 00:00:00 +0000 +++ added_file 2013-10-13 23:44:04 +0000 @@ -0,0 +1,4 @@ +This was missing! +holá mundo! + +Only for testing purposes. \ No newline at end of file === modified file 'modified_file' --- modified_file 2013-10-13 23:53:13 +0000 +++ modified_file 2013-10-13 23:53:26 +0000 @@ -1,5 +1,7 @@ This is the original content. -This should be updated. +This is now updated. + +This is a new line. This will stay. \ No newline at end of file === removed file 'removed_file' --- removed_file 2013-10-13 23:53:13 +0000 +++ removed_file 1970-01-01 00:00:00 +0000 @@ -1,3 +0,0 @@ -This content shouldn't be here. - -This file will be removed. \ No newline at end of file python-unidiff-0.5.4/tests/samples/svn.diff000066400000000000000000000014771311213530100207300ustar00rootroot00000000000000Index: modified_file =================================================================== --- modified_file (revision 191) +++ modified_file (working copy) @@ -1,5 +1,7 @@ This is the original content. -This should be updated. +This is now updated. +This is a new line. + This will stay. \ No newline at end of file Index: removed_file =================================================================== --- removed_file (revision 188) +++ removed_file (working copy) @@ -1,3 +0,0 @@ -This content shouldn't be here. - -This file will be removed. \ No newline at end of file Index: added_file =================================================================== --- added_file (revision 0) +++ added_file (revision 0) @@ -0,0 +1,4 @@ +This was missing! +Adding it now. + +Only for testing purposes. \ No newline at end of file python-unidiff-0.5.4/tests/test_hunks.py000066400000000000000000000065631311213530100203660ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014 Matias Bordese # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. """Tests for Hunk.""" from __future__ import unicode_literals import unittest from unidiff.patch import ( LINE_TYPE_ADDED, LINE_TYPE_CONTEXT, LINE_TYPE_REMOVED, Hunk, Line, ) class TestHunk(unittest.TestCase): """Tests for Hunk.""" def setUp(self): super(TestHunk, self).setUp() self.context_line = Line('Sample line', line_type=LINE_TYPE_CONTEXT) self.added_line = Line('Sample line', line_type=LINE_TYPE_ADDED) self.removed_line = Line('Sample line', line_type=LINE_TYPE_REMOVED) def test_missing_length(self): hunk = Hunk(src_len=None, tgt_len=None) hunk.append(self.context_line) self.assertTrue(hunk.is_valid()) def test_default_is_valid(self): hunk = Hunk() self.assertTrue(hunk.is_valid()) def test_missing_data_is_not_valid(self): hunk = Hunk(src_len=1, tgt_len=1) self.assertFalse(hunk.is_valid()) def test_append_context(self): hunk = Hunk(src_len=1, tgt_len=1) hunk.append(self.context_line) self.assertTrue(hunk.is_valid()) self.assertEqual(len(hunk.source), 1) self.assertEqual(hunk.target, hunk.source) self.assertIn(str(self.context_line), hunk.source) source_lines = list(hunk.source_lines()) target_lines = list(hunk.target_lines()) self.assertEqual(target_lines, source_lines) self.assertEqual(target_lines, [self.context_line]) def test_append_added_line(self): hunk = Hunk(src_len=0, tgt_len=1) hunk.append(self.added_line) self.assertTrue(hunk.is_valid()) self.assertEqual(len(hunk.target), 1) self.assertEqual(hunk.source, []) self.assertIn(str(self.added_line), hunk.target) target_lines = list(hunk.target_lines()) self.assertEqual(target_lines, [self.added_line]) def test_append_deleted_line(self): hunk = Hunk(src_len=1, tgt_len=0) hunk.append(self.removed_line) self.assertTrue(hunk.is_valid()) self.assertEqual(len(hunk.source), 1) self.assertEqual(hunk.target, []) self.assertIn(str(self.removed_line), hunk.source) source_lines = list(hunk.source_lines()) self.assertEqual(source_lines, [self.removed_line]) python-unidiff-0.5.4/tests/test_line.py000066400000000000000000000051641311213530100201610ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2017 Matias Bordese # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. """Tests for Line.""" from __future__ import unicode_literals import unittest from unidiff.patch import ( LINE_TYPE_ADDED, LINE_TYPE_CONTEXT, LINE_TYPE_REMOVED, Line, ) class TestLine(unittest.TestCase): """Tests for Line.""" def setUp(self): super(TestLine, self).setUp() self.context_line = Line('Sample line', line_type=LINE_TYPE_CONTEXT) self.added_line = Line('Sample line', line_type=LINE_TYPE_ADDED) self.removed_line = Line('Sample line', line_type=LINE_TYPE_REMOVED) def test_str(self): self.assertEqual(str(self.added_line), '+Sample line') def test_repr(self): self.assertEqual(repr(self.added_line), '') def test_equal(self): other = Line('Sample line', line_type=LINE_TYPE_ADDED) self.assertEqual(self.added_line, other) def test_not_equal(self): self.assertNotEqual(self.added_line, self.removed_line) def test_is_added(self): self.assertTrue(self.added_line.is_added) self.assertFalse(self.context_line.is_added) self.assertFalse(self.removed_line.is_added) def test_is_removed(self): self.assertTrue(self.removed_line.is_removed) self.assertFalse(self.added_line.is_removed) self.assertFalse(self.context_line.is_removed) def test_is_context(self): self.assertTrue(self.context_line.is_context) self.assertFalse(self.added_line.is_context) self.assertFalse(self.removed_line.is_context) python-unidiff-0.5.4/tests/test_parser.py000066400000000000000000000260151311213530100205240ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014 Matias Bordese # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. """Tests for the unified diff parser process.""" from __future__ import unicode_literals import codecs import os.path import unittest from unidiff import PatchSet from unidiff.patch import PY2 from unidiff.errors import UnidiffParseError if not PY2: unicode = str class TestUnidiffParser(unittest.TestCase): """Tests for Unified Diff Parser.""" def setUp(self): super(TestUnidiffParser, self).setUp() self.samples_dir = os.path.dirname(os.path.realpath(__file__)) self.sample_file = os.path.join( self.samples_dir, 'samples/sample0.diff') self.sample_bad_file = os.path.join( self.samples_dir, 'samples/sample1.diff') def test_missing_encoding(self): utf8_file = os.path.join(self.samples_dir, 'samples/sample3.diff') # read bytes with open(utf8_file, 'rb') as diff_file: if PY2: self.assertRaises(UnicodeDecodeError, PatchSet, diff_file) else: # unicode expected self.assertRaises(TypeError, PatchSet, diff_file) def test_encoding_param(self): utf8_file = os.path.join(self.samples_dir, 'samples/sample3.diff') with open(utf8_file, 'rb') as diff_file: res = PatchSet(diff_file, encoding='utf-8') # 3 files updated by diff self.assertEqual(len(res), 3) added_unicode_line = res.added_files[0][0][1] self.assertEqual(added_unicode_line.value, 'holá mundo!\n') def test_no_newline_at_end_of_file(self): utf8_file = os.path.join(self.samples_dir, 'samples/sample3.diff') with open(utf8_file, 'rb') as diff_file: res = PatchSet(diff_file, encoding='utf-8') # 3 files updated by diff self.assertEqual(len(res), 3) added_unicode_line = res.added_files[0][0][4] self.assertEqual(added_unicode_line.line_type, '\\') self.assertEqual(added_unicode_line.value, ' No newline at end of file\n') added_unicode_line = res.modified_files[0][0][8] self.assertEqual(added_unicode_line.line_type, '\\') self.assertEqual(added_unicode_line.value, ' No newline at end of file\n') def test_preserve_dos_line_endings(self): utf8_file = os.path.join(self.samples_dir, 'samples/sample4.diff') with open(utf8_file, 'rb') as diff_file: res = PatchSet(diff_file, encoding='utf-8') # 3 files updated by diff self.assertEqual(len(res), 3) added_unicode_line = res.added_files[0][0][1] self.assertEqual(added_unicode_line.value, 'holá mundo!\r\n') def test_print_hunks_without_gaps(self): with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file: res = PatchSet(diff_file) lines = unicode(res).splitlines() self.assertEqual(lines[12], '@@ -5,16 +11,10 @@ ') self.assertEqual(lines[31], '@@ -22,3 +22,7 @@ ') def test_parse_sample(self): """Parse sample file.""" with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file: res = PatchSet(diff_file) # three file in the patch self.assertEqual(len(res), 3) # three hunks self.assertEqual(len(res[0]), 3) # first file is modified self.assertTrue(res[0].is_modified_file) self.assertFalse(res[0].is_removed_file) self.assertFalse(res[0].is_added_file) # Hunk 1: five additions, no deletions, a section header self.assertEqual(res[0][0].added, 6) self.assertEqual(res[0][0].removed, 0) self.assertEqual(res[0][0].section_header, 'Section Header') # Hunk 2: 2 additions, 8 deletions, no section header self.assertEqual(res[0][1].added, 2) self.assertEqual(res[0][1].removed, 8) self.assertEqual(res[0][1].section_header, '') # Hunk 3: four additions, no deletions, no section header self.assertEqual(res[0][2].added, 4) self.assertEqual(res[0][2].removed, 0) self.assertEqual(res[0][2].section_header, '') # Check file totals self.assertEqual(res[0].added, 12) self.assertEqual(res[0].removed, 8) # second file is added self.assertFalse(res[1].is_modified_file) self.assertFalse(res[1].is_removed_file) self.assertTrue(res[1].is_added_file) # third file is removed self.assertFalse(res[2].is_modified_file) self.assertTrue(res[2].is_removed_file) self.assertFalse(res[2].is_added_file) self.assertEqual(res.added, 21) self.assertEqual(res.removed, 17) def test_patchset_compare(self): with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file: ps1 = PatchSet(diff_file) with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file: ps2 = PatchSet(diff_file) other_file = os.path.join(self.samples_dir, 'samples/sample3.diff') with open(other_file, 'rb') as diff_file: ps3 = PatchSet(diff_file, encoding='utf-8') self.assertEqual(ps1, ps2) self.assertNotEqual(ps1, ps3) def test_patchset_from_string(self): with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file: diff_data = diff_file.read() ps1 = PatchSet.from_string(diff_data) with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file: ps2 = PatchSet(diff_file) self.assertEqual(ps1, ps2) def test_patchset_from_bytes_string(self): with codecs.open(self.sample_file, 'rb') as diff_file: diff_data = diff_file.read() ps1 = PatchSet.from_string(diff_data, encoding='utf-8') with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file: ps2 = PatchSet(diff_file) self.assertEqual(ps1, ps2) def test_parse_malformed_diff(self): """Parse malformed file.""" with open(self.sample_bad_file) as diff_file: self.assertRaises(UnidiffParseError, PatchSet, diff_file) def test_diff_lines_linenos(self): with open(self.sample_file, 'rb') as diff_file: res = PatchSet(diff_file, encoding='utf-8') target_line_nos = [] source_line_nos = [] diff_line_nos = [] for diff_file in res: for hunk in diff_file: for line in hunk: target_line_nos.append(line.target_line_no) source_line_nos.append(line.source_line_no) diff_line_nos.append(line.diff_line_no) expected_target_line_nos = [ # File: 1, Hunk: 1 1, 2, 3, 4, 5, 6, 7, 8, 9, # File: 1, Hunk: 2 11, 12, 13, None, None, None, None, None, None, None, 14, 15, 16, None, 17, 18, 19, 20, # File: 1, Hunk: 3 22, 23, 24, 25, 26, 27, 28, # File: 2, Hunk 1 1, 2, 3, 4, 5, 6, 7, 8, 9, # File: 3, Hunk 1 None, None, None, None, None, None, None, None, None, ] expected_source_line_nos = [ # File: 1, Hunk: 1 None, None, None, None, None, None, 1, 2, 3, # File: 1, Hunk: 2 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, None, 15, 16, 17, None, 18, 19, 20, # File: 1, Hunk: 3 22, 23, 24, None, None, None, None, # File: 2, Hunk 1 None, None, None, None, None, None, None, None, None, # File: 3, Hunk 1 1, 2, 3, 4, 5, 6, 7, 8, 9, ] expected_diff_line_nos = [ # File: 1, Hunk: 1 4, 5, 6, 7, 8, 9, 10, 11, 12, # File: 1, Hunk: 2 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, # File: 1, Hunk: 3 33, 34, 35, 36, 37, 38, 39, # File: 2, Hunk 1 43, 44, 45, 46, 47, 48, 49, 50, 51, # File: 3, Hunk 1 55, 56, 57, 58, 59, 60, 61, 62, 63, ] self.assertEqual(target_line_nos, expected_target_line_nos) self.assertEqual(source_line_nos, expected_source_line_nos) self.assertEqual(diff_line_nos, expected_diff_line_nos) class TestVCSSamples(unittest.TestCase): """Tests for real examples from VCS.""" samples = ['bzr.diff', 'git.diff', 'hg.diff', 'svn.diff'] def test_samples(self): tests_dir = os.path.dirname(os.path.realpath(__file__)) for fname in self.samples: file_path = os.path.join(tests_dir, 'samples', fname) with codecs.open(file_path, 'r', encoding='utf-8') as diff_file: res = PatchSet(diff_file) # 3 files updated by diff self.assertEqual(len(res), 3) # 1 added file added_files = res.added_files self.assertEqual(len(added_files), 1) self.assertEqual(added_files[0].path, 'added_file') # 1 hunk, 4 lines self.assertEqual(len(added_files[0]), 1) self.assertEqual(added_files[0].added, 4) self.assertEqual(added_files[0].removed, 0) # 1 removed file removed_files = res.removed_files self.assertEqual(len(removed_files), 1) self.assertEqual(removed_files[0].path, 'removed_file') # 1 hunk, 3 removed lines self.assertEqual(len(removed_files[0]), 1) self.assertEqual(removed_files[0].added, 0) self.assertEqual(removed_files[0].removed, 3) # 1 modified file modified_files = res.modified_files self.assertEqual(len(modified_files), 1) self.assertEqual(modified_files[0].path, 'modified_file') # 1 hunk, 3 added lines, 1 removed line self.assertEqual(len(modified_files[0]), 1) self.assertEqual(modified_files[0].added, 3) self.assertEqual(modified_files[0].removed, 1) self.assertEqual(res.added, 7) self.assertEqual(res.removed, 4) python-unidiff-0.5.4/tests/test_patchedfile.py000066400000000000000000000040301311213530100214710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014 Matias Bordese # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. """Tests for PatchedFile.""" from __future__ import unicode_literals import unittest from unidiff.patch import PatchedFile, Hunk class TestPatchedFile(unittest.TestCase): """Tests for PatchedFile.""" def setUp(self): super(TestPatchedFile, self).setUp() self.patched_file = PatchedFile() def test_is_added_file(self): hunk = Hunk(src_start=0, src_len=0, tgt_start=1, tgt_len=10) self.patched_file.append(hunk) self.assertTrue(self.patched_file.is_added_file) def test_is_removed_file(self): hunk = Hunk(src_start=1, src_len=10, tgt_start=0, tgt_len=0) self.patched_file.append(hunk) self.assertTrue(self.patched_file.is_removed_file) def test_is_modified_file(self): hunk = Hunk(src_start=1, src_len=10, tgt_start=1, tgt_len=8) self.patched_file.append(hunk) self.assertTrue(self.patched_file.is_modified_file) python-unidiff-0.5.4/unidiff/000077500000000000000000000000001311213530100160755ustar00rootroot00000000000000python-unidiff-0.5.4/unidiff/__init__.py000066400000000000000000000026141311213530100202110ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014 Matias Bordese # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. """Unidiff parsing library.""" from __future__ import unicode_literals from unidiff.patch import ( DEFAULT_ENCODING, LINE_TYPE_ADDED, LINE_TYPE_CONTEXT, LINE_TYPE_REMOVED, Hunk, PatchedFile, PatchSet, UnidiffParseError, ) VERSION = '0.5.4' python-unidiff-0.5.4/unidiff/constants.py000066400000000000000000000040601311213530100204630ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014 Matias Bordese # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. """Useful constants and regexes used by the package.""" from __future__ import unicode_literals import re RE_SOURCE_FILENAME = re.compile( r'^--- (?P[^\t\n]+)(?:\t(?P[^\n]+))?') RE_TARGET_FILENAME = re.compile( r'^\+\+\+ (?P[^\t\n]+)(?:\t(?P[^\n]+))?') # @@ (source offset, length) (target offset, length) @@ (section header) RE_HUNK_HEADER = re.compile( r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))?\ @@[ ]?(.*)") # kept line (context) # \n empty line (treat like context) # + added line # - deleted line # \ No newline case RE_HUNK_BODY_LINE = re.compile( r'^(?P[- \n\+\\])(?P.*)', re.DOTALL) RE_NO_NEWLINE_MARKER = re.compile(r'^\\ No newline at end of file') DEFAULT_ENCODING = 'UTF-8' LINE_TYPE_ADDED = '+' LINE_TYPE_REMOVED = '-' LINE_TYPE_CONTEXT = ' ' LINE_TYPE_EMPTY = '\n' LINE_TYPE_NO_NEWLINE = '\\' LINE_VALUE_NO_NEWLINE = ' No newline at end of file' python-unidiff-0.5.4/unidiff/errors.py000066400000000000000000000024621311213530100177670ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014 Matias Bordese # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. """Errors and exceptions raised by the package.""" from __future__ import unicode_literals class UnidiffParseError(Exception): """Exception when parsing the unified diff data.""" python-unidiff-0.5.4/unidiff/patch.py000066400000000000000000000330511311213530100175500ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014 Matias Bordese # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE # OR OTHER DEALINGS IN THE SOFTWARE. """Classes used by the unified diff parser to keep the diff data.""" from __future__ import unicode_literals import codecs import sys from unidiff.constants import ( DEFAULT_ENCODING, LINE_TYPE_ADDED, LINE_TYPE_CONTEXT, LINE_TYPE_EMPTY, LINE_TYPE_REMOVED, LINE_TYPE_NO_NEWLINE, LINE_VALUE_NO_NEWLINE, RE_HUNK_BODY_LINE, RE_HUNK_HEADER, RE_SOURCE_FILENAME, RE_TARGET_FILENAME, RE_NO_NEWLINE_MARKER, ) from unidiff.errors import UnidiffParseError PY2 = sys.version_info[0] == 2 if PY2: from StringIO import StringIO open_file = codecs.open make_str = lambda x: x.encode(DEFAULT_ENCODING) def implements_to_string(cls): cls.__unicode__ = cls.__str__ cls.__str__ = lambda x: x.__unicode__().encode(DEFAULT_ENCODING) return cls else: from io import StringIO open_file = open make_str = str implements_to_string = lambda x: x unicode = str @implements_to_string class Line(object): """A diff line.""" def __init__(self, value, line_type, source_line_no=None, target_line_no=None, diff_line_no=None): super(Line, self).__init__() self.source_line_no = source_line_no self.target_line_no = target_line_no self.diff_line_no = diff_line_no self.line_type = line_type self.value = value def __repr__(self): return make_str("") % (self.line_type, self.value) def __str__(self): return "%s%s" % (self.line_type, self.value) def __eq__(self, other): return (self.source_line_no == other.source_line_no and self.target_line_no == other.target_line_no and self.diff_line_no == other.diff_line_no and self.line_type == other.line_type and self.value == other.value) @property def is_added(self): return self.line_type == LINE_TYPE_ADDED @property def is_removed(self): return self.line_type == LINE_TYPE_REMOVED @property def is_context(self): return self.line_type == LINE_TYPE_CONTEXT @implements_to_string class Hunk(list): """Each of the modified blocks of a file.""" def __init__(self, src_start=0, src_len=0, tgt_start=0, tgt_len=0, section_header=''): if src_len is None: src_len = 1 if tgt_len is None: tgt_len = 1 self.added = 0 # number of added lines self.removed = 0 # number of removed lines self.source = [] self.source_start = int(src_start) self.source_length = int(src_len) self.target = [] self.target_start = int(tgt_start) self.target_length = int(tgt_len) self.section_header = section_header def __repr__(self): value = "" % (self.source_start, self.source_length, self.target_start, self.target_length, self.section_header) return make_str(value) def __str__(self): head = "@@ -%d,%d +%d,%d @@ %s\n" % ( self.source_start, self.source_length, self.target_start, self.target_length, self.section_header) content = ''.join(unicode(line) for line in self) return head + content def append(self, line): """Append the line to hunk, and keep track of source/target lines.""" super(Hunk, self).append(line) s = str(line) if line.is_added: self.added += 1 self.target.append(s) elif line.is_removed: self.removed += 1 self.source.append(s) elif line.is_context: self.target.append(s) self.source.append(s) def is_valid(self): """Check hunk header data matches entered lines info.""" return (len(self.source) == self.source_length and len(self.target) == self.target_length) def source_lines(self): """Hunk lines from source file (generator).""" return (l for l in self if l.is_context or l.is_removed) def target_lines(self): """Hunk lines from target file (generator).""" return (l for l in self if l.is_context or l.is_added) class PatchedFile(list): """Patch updated file, it is a list of Hunks.""" def __init__(self, source='', target='', source_timestamp=None, target_timestamp=None): super(PatchedFile, self).__init__() self.source_file = source self.source_timestamp = source_timestamp self.target_file = target self.target_timestamp = target_timestamp def __repr__(self): return make_str("") % make_str(self.path) def __str__(self): source = "--- %s\n" % self.source_file target = "+++ %s\n" % self.target_file hunks = ''.join(unicode(hunk) for hunk in self) return source + target + hunks def _parse_hunk(self, header, diff, encoding): """Parse hunk details.""" header_info = RE_HUNK_HEADER.match(header) hunk_info = header_info.groups() hunk = Hunk(*hunk_info) source_line_no = hunk.source_start target_line_no = hunk.target_start expected_source_end = source_line_no + hunk.source_length expected_target_end = target_line_no + hunk.target_length for diff_line_no, line in diff: if encoding is not None: line = line.decode(encoding) valid_line = RE_HUNK_BODY_LINE.match(line) if not valid_line: raise UnidiffParseError('Hunk diff line expected: %s' % line) line_type = valid_line.group('line_type') if line_type == LINE_TYPE_EMPTY: line_type = LINE_TYPE_CONTEXT value = valid_line.group('value') original_line = Line(value, line_type=line_type) if line_type == LINE_TYPE_ADDED: original_line.target_line_no = target_line_no target_line_no += 1 elif line_type == LINE_TYPE_REMOVED: original_line.source_line_no = source_line_no source_line_no += 1 elif line_type == LINE_TYPE_CONTEXT: original_line.target_line_no = target_line_no target_line_no += 1 original_line.source_line_no = source_line_no source_line_no += 1 elif line_type == LINE_TYPE_NO_NEWLINE: pass else: original_line = None if original_line: original_line.diff_line_no = diff_line_no hunk.append(original_line) # if hunk source/target lengths are ok, hunk is complete if (source_line_no == expected_source_end and target_line_no == expected_target_end): break self.append(hunk) def _add_no_newline_marker_to_last_hunk(self): if not self: raise UnidiffParseError( 'Unexpected marker:' + LINE_VALUE_NO_NEWLINE) last_hunk = self[-1] last_hunk.append( Line(LINE_VALUE_NO_NEWLINE + '\n', line_type=LINE_TYPE_NO_NEWLINE)) @property def path(self): """Return the file path abstracted from VCS.""" if (self.source_file.startswith('a/') and self.target_file.startswith('b/')): filepath = self.source_file[2:] elif (self.source_file.startswith('a/') and self.target_file == '/dev/null'): filepath = self.source_file[2:] elif (self.target_file.startswith('b/') and self.source_file == '/dev/null'): filepath = self.target_file[2:] else: filepath = self.source_file return filepath @property def added(self): """Return the file total added lines.""" return sum([hunk.added for hunk in self]) @property def removed(self): """Return the file total removed lines.""" return sum([hunk.removed for hunk in self]) @property def is_added_file(self): """Return True if this patch adds the file.""" return (len(self) == 1 and self[0].source_start == 0 and self[0].source_length == 0) @property def is_removed_file(self): """Return True if this patch removes the file.""" return (len(self) == 1 and self[0].target_start == 0 and self[0].target_length == 0) @property def is_modified_file(self): """Return True if this patch modifies the file.""" return not (self.is_added_file or self.is_removed_file) @implements_to_string class PatchSet(list): """A list of PatchedFiles.""" def __init__(self, f, encoding=None): super(PatchSet, self).__init__() # make sure we pass an iterator object to parse data = iter(f) # if encoding is None, assume we are reading unicode data self._parse(data, encoding=encoding) def __repr__(self): return make_str('') % super(PatchSet, self).__repr__() def __str__(self): return '\n'.join(unicode(patched_file) for patched_file in self) def _parse(self, diff, encoding): current_file = None diff = enumerate(diff, 1) for unused_diff_line_no, line in diff: if encoding is not None: line = line.decode(encoding) # check for source file header is_source_filename = RE_SOURCE_FILENAME.match(line) if is_source_filename: source_file = is_source_filename.group('filename') source_timestamp = is_source_filename.group('timestamp') # reset current file current_file = None continue # check for target file header is_target_filename = RE_TARGET_FILENAME.match(line) if is_target_filename: if current_file is not None: raise UnidiffParseError('Target without source: %s' % line) target_file = is_target_filename.group('filename') target_timestamp = is_target_filename.group('timestamp') # add current file to PatchSet current_file = PatchedFile(source_file, target_file, source_timestamp, target_timestamp) self.append(current_file) continue # check for hunk header is_hunk_header = RE_HUNK_HEADER.match(line) if is_hunk_header: if current_file is None: raise UnidiffParseError('Unexpected hunk found: %s' % line) current_file._parse_hunk(line, diff, encoding) # check for no newline marker is_no_newline = RE_NO_NEWLINE_MARKER.match(line) if is_no_newline: if current_file is None: raise UnidiffParseError('Unexpected marker: %s' % line) current_file._add_no_newline_marker_to_last_hunk() @classmethod def from_filename(cls, filename, encoding=DEFAULT_ENCODING, errors=None): """Return a PatchSet instance given a diff filename.""" with open_file(filename, 'r', encoding=encoding, errors=errors) as f: instance = cls(f) return instance @classmethod def from_string(cls, data, encoding=None, errors='strict'): """Return a PatchSet instance given a diff string.""" if encoding is not None: # if encoding is given, assume bytes and decode data = unicode(data, encoding=encoding, errors=errors) return cls(StringIO(data)) @property def added_files(self): """Return patch added files as a list.""" return [f for f in self if f.is_added_file] @property def removed_files(self): """Return patch removed files as a list.""" return [f for f in self if f.is_removed_file] @property def modified_files(self): """Return patch modified files as a list.""" return [f for f in self if f.is_modified_file] @property def added(self): """Return the patch total added lines.""" return sum([f.added for f in self]) @property def removed(self): """Return the patch total removed lines.""" return sum([f.removed for f in self])