pax_global_header00006660000000000000000000000064142000155300014500gustar00rootroot0000000000000052 comment=5294ed13cce7834d4398f81a538a9f6975c86915 python-unidiff-0.7.3/000077500000000000000000000000001420001553000144525ustar00rootroot00000000000000python-unidiff-0.7.3/.gitignore000066400000000000000000000001101420001553000164320ustar00rootroot00000000000000# Python *.py[cod] __pycache__ build dist unidiff.egg-info # Vim *.swp python-unidiff-0.7.3/.travis.yml000066400000000000000000000001421420001553000165600ustar00rootroot00000000000000language: python python: - "2.7" - "3.6" - "3.7" - "3.8" - "3.9" script: ./run_tests.sh python-unidiff-0.7.3/AUTHORS000066400000000000000000000014451420001553000155260ustar00rootroot00000000000000Main 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-`_) * Thomas Grainger (`@graingert`_) * (`snake-scaly`_) * Dan Callaghan (`@danc86`_) * Max Bittker (`@MaxBittker`_) * Volo Zyko (`@volo-zyko`_) * Robert Estelle (`@erydo`_) * Dylan Grafmyre * Povilas Kanapickas (`@p12tic`_) * Snowhite (`@CirQ`_) * earonesty (`@earonesty`_) * Ben Carlsson (`@glacials`_) * (`@huichen-cs`) * Mikhail f. Shiryaev (`@Felixoid`) * Ronuk Raval (`@rraval`) python-unidiff-0.7.3/HISTORY000066400000000000000000000032761420001553000155460ustar00rootroot00000000000000History ------- 0.7.3 - 2022-02-06 ------------------ * Fixed RE_BINARY_DIFF regex to make it a raw string. 0.7.2 - 2022-01-28 ------------------ * Fixed issue when parsing git diff header generated with `--no-prefix`. 0.7.1 - 2022-01-27 ------------------ * Improved git added/deleted file detection. * Added `newline` optional param when parsing `from_filename`. 0.7.0 - 2021-08-16 ------------------ * Fixed issues handling multiple git renames. * Renamed files return target filename as PatchedFile.path. * Fixed error when first change is a binary file. * Added source code type hints. 0.6.0 - 2020-05-07 ---------------- * Updated PatchSet constructor to accept an optional (default to False) metadata_only parameter to only keep diff metadata information without the diff text data (better performance). * Identify and track changed binary files. * Added support for git rename syntax. 0.5.5 - 2018-01-03 ------------------ * Updated PatchSet constructor to accept string data. * Added support to parse extended patch info. 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.7.3/LICENSE000066400000000000000000000020701420001553000154560ustar00rootroot00000000000000The 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.7.3/MANIFEST.in000066400000000000000000000001311420001553000162030ustar00rootroot00000000000000include bin/* include tests/samples/* include HISTORY include LICENSE include README.rst python-unidiff-0.7.3/README.rst000066400000000000000000000076241420001553000161520ustar00rootroot00000000000000Unidiff ======= Simple Python library to parse and interact with unified diff data. .. image:: https://www.travis-ci.com/matiasb/python-unidiff.svg?branch=master :target: https://travis-ci.com/matiasb/python-unidiff Installing unidiff ------------------ :: $ pip install unidiff Quick start ----------- :: >>> import urllib.request >>> from unidiff import PatchSet >>> diff = urllib.request.urlopen('https://github.com/matiasb/python-unidiff/pull/3.diff') >>> encoding = diff.headers.get_charsets()[0] >>> 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]) diff --git a/unidiff/utils.py b/unidiff/utils.py index eae63e6..29c896a 100644 --- 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 :code:`PatchSet` with a file-like object as argument, or using :code:`PatchSet.from_filename` class method to read diff from file. A :code:`PatchSet` is a list of files updated by the given patch. For each :code:`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 :code:`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) :code:`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 :code:`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) >>> patch , , ]> If you don't need to be able to rebuild the original unified diff input, you can pass :code:`metadata_only=True` (defaults to :code:`False`), which should help making the parsing more efficient: :: >>> from unidiff import PatchSet >>> patch = PatchSet.from_filename('tests/samples/bzr.diff', encoding='utf-8', metadata_only=True) References ---------- * http://en.wikipedia.org/wiki/Diff_utility * http://www.artima.com/weblogs/viewpost.jsp?thread=164293 python-unidiff-0.7.3/bin/000077500000000000000000000000001420001553000152225ustar00rootroot00000000000000python-unidiff-0.7.3/bin/unidiff000077500000000000000000000041061420001553000165750ustar00rootroot00000000000000#!/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, metadata_only=(not args.show_diff)) if args.show_diff: print(patch) print() print('Summary') print('-------') additions = 0 deletions = 0 renamed_files = 0 for f in patch: if f.is_binary_file: print('%s:' % f.path, '(binary file)') else: additions += f.added deletions += f.removed print('%s:' % f.path, '+%d additions,' % f.added, '-%d deletions' % f.removed) renamed_files = renamed_files + 1 if f.is_rename else renamed_files 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))) if renamed_files: print('%d file(s) renamed' % renamed_files) print('Total: %d addition(s), %d deletion(s)' % (additions, deletions)) python-unidiff-0.7.3/run_tests.sh000077500000000000000000000001061420001553000170340ustar00rootroot00000000000000#! /bin/bash PYTHONPATH=unidiff python -m unittest discover -s tests/ python-unidiff-0.7.3/setup.cfg000066400000000000000000000000751420001553000162750ustar00rootroot00000000000000[bdist_wheel] universal=1 [metadata] license_file = LICENSE python-unidiff-0.7.3/setup.py000066400000000000000000000027171420001553000161730ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Author: Matías Bordese import codecs import os from setuptools import find_packages, setup # metadata NAME = 'unidiff' DESCRIPTION = 'Unified diff parsing/metadata extraction library.' KEYWORDS = ['unified', 'diff', 'parse', 'metadata'] URL = 'http://github.com/matiasb/python-unidiff' EMAIL = 'mbordese@gmail.com' AUTHOR = 'Matias Bordese' LICENSE = 'MIT' HERE = os.path.abspath(os.path.dirname(__file__)) # use README as the long-description with codecs.open(os.path.join(HERE, 'README.rst'), "rb", "utf-8") as f: long_description = f.read() # load __version__.py module as a dictionary about = {} with open(os.path.join(HERE, 'unidiff/__version__.py')) as f: exec(f.read(), about) setup( name=NAME, version=about['__version__'], description=DESCRIPTION, long_description=long_description, keywords=KEYWORDS, author=AUTHOR, author_email=EMAIL, url=URL, packages=find_packages(exclude=('tests',)), scripts=['bin/unidiff'], include_package_data=True, license=LICENSE, 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.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', ], test_suite='tests', ) python-unidiff-0.7.3/tests/000077500000000000000000000000001420001553000156145ustar00rootroot00000000000000python-unidiff-0.7.3/tests/__init__.py000066400000000000000000000021741420001553000177310ustar00rootroot00000000000000# The MIT License (MIT) # Copyright (c) 2014-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 unidiff.""" python-unidiff-0.7.3/tests/samples/000077500000000000000000000000001420001553000172605ustar00rootroot00000000000000python-unidiff-0.7.3/tests/samples/bzr.diff000066400000000000000000000013401420001553000207050ustar00rootroot00000000000000=== 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.7.3/tests/samples/git.diff000066400000000000000000000013321420001553000206740ustar00rootroot00000000000000diff --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.7.3/tests/samples/git_cr.diff000066400000000000000000000010451420001553000213610ustar00rootroot00000000000000diff --git a/src/test/org/apache/commons/math/util/ExpandableDoubleArrayTest.java b/src/test/org/apache/commons/math/util/ExpandableDoubleArrayTest.java new file mode 100644 index 000000000..2b38fa232 --- /dev/null +++ b/src/test/org/apache/commons/math/util/ExpandableDoubleArrayTest.java @@ -0,0 +1,3 @@ + "This line is broken into two lines by CR. " + "but it should be treated as one line in the text diff file" + "This has no CR" + "This line also has CR. " + "but it should also be treated as one line in the text diff file". python-unidiff-0.7.3/tests/samples/git_no_prefix.diff000066400000000000000000000007011420001553000227440ustar00rootroot00000000000000diff --git file1 file1 deleted file mode 100644 index 42f90fd..0000000 --- file1 +++ /dev/null @@ -1,3 +0,0 @@ -line11 -line12 -line13 diff --git file2 file2 index c337bf1..1cb02b9 100644 --- file2 +++ file2 @@ -4,0 +5,3 @@ line24 +line24n +line24n2 +line24n3 @@ -15,0 +19,3 @@ line215 +line215n +line215n2 +line215n3 diff --git file3 file3 new file mode 100644 index 0000000..632e269 --- /dev/null +++ file3 @@ -0,0 +1,3 @@ +line31 +line32 +line33 python-unidiff-0.7.3/tests/samples/git_rename.diff000066400000000000000000000011351420001553000222240ustar00rootroot00000000000000diff --git a/added b/moved similarity index 85% rename from added rename to moved index a071991..4dbab21 100644 --- a/added +++ b/moved @@ -9,4 +9,4 @@ Some content Some content Some content Some content -Some content +Some modified content diff --git a/oldfile b/newfile similarity index 85% rename from oldfile rename to newfile index a071991..4dbab21 100644 --- a/oldfile +++ b/newfile @@ -9,4 +9,4 @@ Some content Some content Some content Some content -Some content +Some modified content diff --git a/sub/onefile b/sub/otherfile similarity index 100% rename from onefile rename to otherfile python-unidiff-0.7.3/tests/samples/hg.diff000066400000000000000000000014051420001553000205100ustar00rootroot00000000000000diff -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.7.3/tests/samples/sample0.diff000066400000000000000000000023411420001553000214530ustar00rootroot00000000000000--- /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.7.3/tests/samples/sample1.diff000066400000000000000000000014311420001553000214530ustar00rootroot00000000000000--- /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.7.3/tests/samples/sample2.diff000066400000000000000000000027351420001553000214640ustar00rootroot00000000000000# 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.7.3/tests/samples/sample3.diff000066400000000000000000000013571420001553000214640ustar00rootroot00000000000000=== 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.7.3/tests/samples/sample4.diff000066400000000000000000000013521420001553000214600ustar00rootroot00000000000000=== 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.7.3/tests/samples/sample5.diff000066400000000000000000000011171420001553000214600ustar00rootroot00000000000000=== modified file 'modified_file1' --- modified_file1 2013-10-13 23:53:13 +0000 +++ modified_file1 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 === modified file 'modified_file2' --- modified_file2 2013-10-13 23:53:13 +0000 +++ modified_file2 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 python-unidiff-0.7.3/tests/samples/sample6.diff000066400000000000000000000014071420001553000214630ustar00rootroot00000000000000--- /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. this paragraph needs to be changed. Things can be added after it. + +This paragraph contains +important new additions +to this document. python-unidiff-0.7.3/tests/samples/sample7.diff000066400000000000000000000011221420001553000214560ustar00rootroot00000000000000--- /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. +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. python-unidiff-0.7.3/tests/samples/sample8.diff000066400000000000000000000007671420001553000214750ustar00rootroot00000000000000diff --git a/boo.bin b/boo.bin new file mode 100644 index 0000000..ae000000 diff --git a/foo.bin b/foo.bin new file mode 100644 index 0000000..af000000 Binary files /dev/null and b/foo.bin differ diff --git a/bar.bin b/bar.bin index ad000000..ac000000 100644 Binary files a/bar.bin and b/bar.bin differ diff --git a/baz.bin b/baz.bin deleted file mode 100644 index af000000..0000000 Binary files a/baz.bin and /dev/null differ diff --git a/fuz.bin b/fuz.bin new file mode 100644 index 0000000..ae000000 python-unidiff-0.7.3/tests/samples/svn.diff000066400000000000000000000014771420001553000207310ustar00rootroot00000000000000Index: 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.7.3/tests/test_hunks.py000066400000000000000000000065701420001553000203650ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014-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 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.7.3/tests/test_line.py000066400000000000000000000051641420001553000201620ustar00rootroot00000000000000# -*- 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.7.3/tests/test_parser.py000066400000000000000000000455461420001553000205370ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014-2021 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_preserve_dos_line_endings_empty_line_type(self): utf8_file = os.path.join(self.samples_dir, 'samples/sample5.diff') with open(utf8_file, 'rb') as diff_file: res = PatchSet(diff_file, encoding='utf-8') # 2 files updated by diff self.assertEqual(len(res), 2) modified_unicode_line = res.modified_files[0][0][6] self.assertEqual(modified_unicode_line.value, '\r\n') self.assertEqual(modified_unicode_line.line_type, ' ') modified_unicode_line = res.modified_files[1][0][6] self.assertEqual(modified_unicode_line.value, '\n') self.assertEqual(modified_unicode_line.line_type, ' ') 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, metadata_only): """Parse sample file.""" with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file: res = PatchSet(diff_file, metadata_only=metadata_only) # 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) self.assertFalse(res[0].is_binary_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) self.assertFalse(res[1].is_binary_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.assertFalse(res[2].is_binary_file) self.assertEqual(res.added, 21) self.assertEqual(res.removed, 17) def test_parse_sample_full(self): self._test_parse_sample(metadata_only=False) def test_parse_sample_metadata_only(self): self._test_parse_sample(metadata_only=True) 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_patchset_string_input(self): with codecs.open(self.sample_file, 'r', encoding='utf-8') as diff_file: diff_data = diff_file.read() ps1 = PatchSet(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_parse_malformed_diff(self): """Parse malformed file.""" with open(self.sample_bad_file) as diff_file: self.assertRaises(UnidiffParseError, PatchSet, diff_file) def test_parse_malformed_diff_longer_than_expected(self): """Parse malformed file with non-terminated hunk.""" utf8_file = os.path.join(self.samples_dir, 'samples/sample6.diff') with open(utf8_file, 'r') as diff_file: self.assertRaises(UnidiffParseError, PatchSet, diff_file) def test_parse_malformed_diff_shorter_than_expected(self): """Parse malformed file with non-terminated hunk.""" utf8_file = os.path.join(self.samples_dir, 'samples/sample7.diff') with open(utf8_file, 'r') as diff_file: self.assertRaises(UnidiffParseError, PatchSet, diff_file) def test_from_filename_with_cr_in_diff_text_files(self): """Parse git diff text files that contain CR""" utf8_file = os.path.join(self.samples_dir, 'samples/git_cr.diff') self.assertRaises(UnidiffParseError, PatchSet.from_filename, utf8_file) ps1 = PatchSet.from_filename(utf8_file, newline='\n') import io with io.open(utf8_file, 'r', newline='\n') as diff_file: ps2 = PatchSet(diff_file) self.assertEqual(ps1, ps2) def test_parse_diff_with_new_and_modified_binary_files(self): """Parse git diff file with newly added and modified binaries files.""" utf8_file = os.path.join(self.samples_dir, 'samples/sample8.diff') with open(utf8_file, 'r') as diff_file: res = PatchSet(diff_file) # three file in the patch self.assertEqual(len(res), 5) # first empty file is added self.assertFalse(res[0].is_modified_file) self.assertFalse(res[0].is_removed_file) self.assertTrue(res[0].is_added_file) self.assertFalse(res[0].is_binary_file) # 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) self.assertTrue(res[1].is_binary_file) # third file is modified self.assertTrue(res[2].is_modified_file) self.assertFalse(res[2].is_removed_file) self.assertFalse(res[2].is_added_file) self.assertTrue(res[2].is_binary_file) # fourth file is removed self.assertFalse(res[3].is_modified_file) self.assertTrue(res[3].is_removed_file) self.assertFalse(res[3].is_added_file) self.assertTrue(res[3].is_binary_file) # fifth empty file is added self.assertFalse(res[4].is_modified_file) self.assertFalse(res[4].is_removed_file) self.assertTrue(res[4].is_added_file) self.assertFalse(res[4].is_binary_file) def test_parse_round_trip_with_binary_files_in_diff(self): """Parse git diff with binary files though round trip""" utf8_file = os.path.join(self.samples_dir, 'samples/sample8.diff') with open(utf8_file, 'r') as diff_file: res1 = PatchSet(diff_file) res2 = PatchSet(str(res1)) self.assertEqual(res1, res2) def test_parse_diff_git_no_prefix(self): utf8_file = os.path.join(self.samples_dir, 'samples/git_no_prefix.diff') with open(utf8_file, 'r') as diff_file: res = PatchSet(diff_file) self.assertEqual(len(res), 3) self.assertEqual(res[0].source_file, 'file1') self.assertEqual(res[0].target_file, '/dev/null') self.assertTrue(res[0].is_removed_file) self.assertEqual(res[0].path, 'file1') self.assertEqual(res[1].source_file, 'file2') self.assertEqual(res[1].target_file, 'file2') self.assertTrue(res[1].is_modified_file) self.assertEqual(res[1].path, 'file2') self.assertEqual(res[2].source_file, '/dev/null') self.assertEqual(res[2].target_file, 'file3') self.assertTrue(res[2].is_added_file) self.assertEqual(res[2].path, 'file3') 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) def test_diff_hunk_positions(self): with open(self.sample_file, 'rb') as diff_file: res = PatchSet(diff_file, encoding='utf-8') self.do_test_diff_hunk_positions(res) def test_diff_metadata_only(self): with open(self.sample_file, 'rb') as diff_file: res = PatchSet(diff_file, encoding='utf-8', metadata_only=True) self.do_test_diff_hunk_positions(res) def do_test_diff_hunk_positions(self, res): hunk_positions = [] for diff_file in res: for hunk in diff_file: hunk_positions.append((hunk.source_start, hunk.target_start, hunk.source_length, hunk.target_length)) expected_hunk_positions = [ # File: 1, Hunk: 1 (1, 1, 3, 9), # File: 1, Hunk: 2 (5, 11, 16, 10), # File: 1, Hunk: 3 (22, 22, 3, 7), # File: 2, Hunk: 1 (0, 1, 0, 9), # File: 3, Hunk: 1 (1, 0, 9, 0) ] self.assertEqual(hunk_positions, expected_hunk_positions) 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) # check that original diffs and those produced # by unidiff are the same with codecs.open(file_path, 'r', encoding='utf-8') as diff_file: self.assertEqual(diff_file.read(), str(res)) def test_git_renaming(self): tests_dir = os.path.dirname(os.path.realpath(__file__)) file_path = os.path.join(tests_dir, 'samples/git_rename.diff') with codecs.open(file_path, 'r', encoding='utf-8') as diff_file: res = PatchSet(diff_file) self.assertEqual(len(res), 3) self.assertEqual(len(res.modified_files), 3) self.assertEqual(len(res.added_files), 0) self.assertEqual(len(res.removed_files), 0) # renamed and modified files for patch in res[:2]: self.assertTrue(patch.is_rename) self.assertEqual(patch.added, 1) self.assertEqual(patch.removed, 1) # renamed file under sub-path patch = res[2] self.assertTrue(patch.is_rename) self.assertEqual(patch.added, 0) self.assertEqual(patch.removed, 0) # confirm the full path is in source/target filenames self.assertEqual(patch.source_file, 'a/sub/onefile') self.assertEqual(patch.target_file, 'b/sub/otherfile') # check path is the target path self.assertEqual(patch.path, 'sub/otherfile') # check that original diffs and those produced # by unidiff are the same with codecs.open(file_path, 'r', encoding='utf-8') as diff_file: self.assertEqual(diff_file.read(), str(res)) python-unidiff-0.7.3/tests/test_patchedfile.py000066400000000000000000000040351420001553000214770ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014-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 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.7.3/unidiff/000077500000000000000000000000001420001553000160765ustar00rootroot00000000000000python-unidiff-0.7.3/unidiff/__init__.py000066400000000000000000000027001420001553000202060ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014-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. """Unidiff parsing library.""" from __future__ import unicode_literals from unidiff import __version__ from unidiff.patch import ( DEFAULT_ENCODING, LINE_TYPE_ADDED, LINE_TYPE_CONTEXT, LINE_TYPE_REMOVED, Hunk, PatchedFile, PatchSet, UnidiffParseError, ) VERSION = __version__.__version__ python-unidiff-0.7.3/unidiff/__version__.py000066400000000000000000000022221420001553000207270ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014-2022 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. __version__ = '0.7.3' python-unidiff-0.7.3/unidiff/constants.py000066400000000000000000000054661420001553000204770ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014-2022 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]+))?') # check diff git line for git renamed files support RE_DIFF_GIT_HEADER = re.compile( r'^diff --git (?P(a/)?[^\t\n]+) (?P(b/)?[^\t\n]+)') # check diff git new file marker `deleted file mode 100644` RE_DIFF_GIT_DELETED_FILE = re.compile(r'^deleted file mode \d+\n$') # check diff git new file marker `new file mode 100644` RE_DIFF_GIT_NEW_FILE = re.compile(r'^new file mode \d+\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[- \+\\])(?P.*)', re.DOTALL) RE_HUNK_EMPTY_BODY_LINE = re.compile( r'^(?P[- \+\\]?)(?P[\r\n]{1,2})', re.DOTALL) RE_NO_NEWLINE_MARKER = re.compile(r'^\\ No newline at end of file') RE_BINARY_DIFF = re.compile( r'^Binary files? ' r'(?P[^\t]+?)(?:\t(?P[\s0-9:\+-]+))?' r'(?: and (?P[^\t]+?)(?:\t(?P[\s0-9:\+-]+))?)? (differ|has changed)') DEFAULT_ENCODING = 'UTF-8' DEV_NULL = '/dev/null' LINE_TYPE_ADDED = '+' LINE_TYPE_REMOVED = '-' LINE_TYPE_CONTEXT = ' ' LINE_TYPE_EMPTY = '' LINE_TYPE_NO_NEWLINE = '\\' LINE_VALUE_NO_NEWLINE = ' No newline at end of file' python-unidiff-0.7.3/unidiff/errors.py000066400000000000000000000024671420001553000177750ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014-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. """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.7.3/unidiff/patch.py000066400000000000000000000557741420001553000175710ustar00rootroot00000000000000# -*- coding: utf-8 -*- # The MIT License (MIT) # Copyright (c) 2014-2022 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, DEV_NULL, LINE_TYPE_ADDED, LINE_TYPE_CONTEXT, LINE_TYPE_EMPTY, LINE_TYPE_REMOVED, LINE_TYPE_NO_NEWLINE, LINE_VALUE_NO_NEWLINE, RE_DIFF_GIT_DELETED_FILE, RE_DIFF_GIT_HEADER, RE_DIFF_GIT_NEW_FILE, RE_HUNK_BODY_LINE, RE_HUNK_EMPTY_BODY_LINE, RE_HUNK_HEADER, RE_SOURCE_FILENAME, RE_TARGET_FILENAME, RE_NO_NEWLINE_MARKER, RE_BINARY_DIFF, ) from unidiff.errors import UnidiffParseError PY2 = sys.version_info[0] == 2 if PY2: import io from StringIO import StringIO open_file = io.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 from typing import Iterable, Optional, Union open_file = open make_str = str implements_to_string = lambda x: x unicode = str basestring = 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): # type: (str, str, Optional[int], Optional[int], Optional[int]) -> 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): # type: () -> str return make_str("") % (self.line_type, self.value) def __str__(self): # type: () -> str return "%s%s" % (self.line_type, self.value) def __eq__(self, other): # type: (Line) -> bool 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): # type: () -> bool return self.line_type == LINE_TYPE_ADDED @property def is_removed(self): # type: () -> bool return self.line_type == LINE_TYPE_REMOVED @property def is_context(self): # type: () -> bool return self.line_type == LINE_TYPE_CONTEXT @implements_to_string class PatchInfo(list): """Lines with extended patch info. Format of this info is not documented and it very much depends on patch producer. """ def __repr__(self): # type: () -> str value = "" % self[0].strip() return make_str(value) def __str__(self): # type: () -> str return ''.join(unicode(line) for line in self) @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=''): # type: (int, int, int, int, str) -> None super(Hunk, self).__init__() if src_len is None: src_len = 1 if tgt_len is None: tgt_len = 1 self.source_start = int(src_start) self.source_length = int(src_len) self.target_start = int(tgt_start) self.target_length = int(tgt_len) self.section_header = section_header self._added = None # Optional[int] self._removed = None # Optional[int] def __repr__(self): # type: () -> str value = "" % (self.source_start, self.source_length, self.target_start, self.target_length, self.section_header) return make_str(value) def __str__(self): # type: () -> str # section header is optional and thus we output it only if it's present head = "@@ -%d,%d +%d,%d @@%s\n" % ( self.source_start, self.source_length, self.target_start, self.target_length, ' ' + self.section_header if self.section_header else '') content = ''.join(unicode(line) for line in self) return head + content def append(self, line): # type: (Line) -> None """Append the line to hunk, and keep track of source/target lines.""" # Make sure the line is encoded correctly. This is a no-op except for # potentially raising a UnicodeDecodeError. str(line) super(Hunk, self).append(line) @property def added(self): # type: () -> Optional[int] if self._added is not None: return self._added # re-calculate each time to allow for hunk modifications # (which should mean metadata_only switch wasn't used) return sum(1 for line in self if line.is_added) @property def removed(self): # type: () -> Optional[int] if self._removed is not None: return self._removed # re-calculate each time to allow for hunk modifications # (which should mean metadata_only switch wasn't used) return sum(1 for line in self if line.is_removed) def is_valid(self): # type: () -> bool """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): # type: () -> Iterable[Line] """Hunk lines from source file (generator).""" return (l for l in self if l.is_context or l.is_removed) @property def source(self): # type: () -> Iterable[str] return [str(l) for l in self.source_lines()] def target_lines(self): # type: () -> Iterable[Line] """Hunk lines from target file (generator).""" return (l for l in self if l.is_context or l.is_added) @property def target(self): # type: () -> Iterable[str] return [str(l) for l in self.target_lines()] class PatchedFile(list): """Patch updated file, it is a list of Hunks.""" def __init__(self, patch_info=None, source='', target='', source_timestamp=None, target_timestamp=None, is_binary_file=False): # type: (Optional[PatchInfo], str, str, Optional[str], Optional[str], bool, bool) -> None super(PatchedFile, self).__init__() self.patch_info = patch_info self.source_file = source self.source_timestamp = source_timestamp self.target_file = target self.target_timestamp = target_timestamp self.is_binary_file = is_binary_file def __repr__(self): # type: () -> str return make_str("") % make_str(self.path) def __str__(self): # type: () -> str source = '' target = '' # patch info is optional info = '' if self.patch_info is None else str(self.patch_info) if not self.is_binary_file and self: source = "--- %s%s\n" % ( self.source_file, '\t' + self.source_timestamp if self.source_timestamp else '') target = "+++ %s%s\n" % ( self.target_file, '\t' + self.target_timestamp if self.target_timestamp else '') hunks = ''.join(unicode(hunk) for hunk in self) return info + source + target + hunks def _parse_hunk(self, header, diff, encoding, metadata_only): # type: (str, enumerate[str], Optional[str], bool) -> None """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 added = 0 removed = 0 for diff_line_no, line in diff: if encoding is not None: line = line.decode(encoding) if metadata_only: # quick line type detection, no regex required line_type = line[0] if line else LINE_TYPE_CONTEXT if line_type not in (LINE_TYPE_ADDED, LINE_TYPE_REMOVED, LINE_TYPE_CONTEXT, LINE_TYPE_NO_NEWLINE): raise UnidiffParseError( 'Hunk diff line expected: %s' % line) if line_type == LINE_TYPE_ADDED: target_line_no += 1 added += 1 elif line_type == LINE_TYPE_REMOVED: source_line_no += 1 removed += 1 elif line_type == LINE_TYPE_CONTEXT: target_line_no += 1 source_line_no += 1 # no file content tracking original_line = None else: # parse diff line content valid_line = RE_HUNK_BODY_LINE.match(line) if not valid_line: valid_line = RE_HUNK_EMPTY_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') # type: str 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 original_line.source_line_no = source_line_no target_line_no += 1 source_line_no += 1 elif line_type == LINE_TYPE_NO_NEWLINE: pass else: original_line = None # stop parsing if we got past expected number of lines if (source_line_no > expected_source_end or target_line_no > expected_target_end): raise UnidiffParseError('Hunk is longer than expected') 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 # report an error if we haven't got expected number of lines if (source_line_no < expected_source_end or target_line_no < expected_target_end): raise UnidiffParseError('Hunk is shorter than expected') if metadata_only: # HACK: set fixed calculated values when metadata_only is enabled hunk._added = added hunk._removed = removed self.append(hunk) def _add_no_newline_marker_to_last_hunk(self): # type: () -> None 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)) def _append_trailing_empty_line(self): # type: () -> None if not self: raise UnidiffParseError('Unexpected trailing newline character') last_hunk = self[-1] last_hunk.append(Line('\n', line_type=LINE_TYPE_EMPTY)) @property def path(self): # type: () -> str """Return the file path abstracted from VCS.""" filepath = self.source_file if filepath in (None, DEV_NULL) or ( self.is_rename and self.target_file not in (None, DEV_NULL)): # if this is a rename, prefer the target filename filepath = self.target_file if filepath.startswith('a/') or filepath.startswith('b/'): filepath = filepath[2:] return filepath @property def added(self): # type: () -> int """Return the file total added lines.""" return sum([hunk.added for hunk in self]) @property def removed(self): # type: () -> int """Return the file total removed lines.""" return sum([hunk.removed for hunk in self]) @property def is_rename(self): return (self.source_file != DEV_NULL and self.target_file != DEV_NULL and self.source_file[2:] != self.target_file[2:]) @property def is_added_file(self): # type: () -> bool """Return True if this patch adds the file.""" if self.source_file == DEV_NULL: return True return (len(self) == 1 and self[0].source_start == 0 and self[0].source_length == 0) @property def is_removed_file(self): # type: () -> bool """Return True if this patch removes the file.""" if self.target_file == DEV_NULL: return True return (len(self) == 1 and self[0].target_start == 0 and self[0].target_length == 0) @property def is_modified_file(self): # type: () -> bool """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, metadata_only=False): # type: (Union[StringIO, str], Optional[str], bool) -> None super(PatchSet, self).__init__() # convert string inputs to StringIO objects if isinstance(f, basestring): f = self._convert_string(f, encoding) # type: StringIO # make sure we pass an iterator object to parse data = iter(f) # if encoding is None, assume we are reading unicode data # when metadata_only is True, only perform a minimal metadata parsing # (ie. hunks without content) which is around 2.5-6 times faster; # it will still validate the diff metadata consistency and get counts self._parse(data, encoding=encoding, metadata_only=metadata_only) def __repr__(self): # type: () -> str return make_str('') % super(PatchSet, self).__repr__() def __str__(self): # type: () -> str return ''.join(unicode(patched_file) for patched_file in self) def _parse(self, diff, encoding, metadata_only): # type: (StringIO, Optional[str], bool) -> None current_file = None patch_info = None diff = enumerate(diff, 1) for unused_diff_line_no, line in diff: if encoding is not None: line = line.decode(encoding) # check for a git file rename is_diff_git_header = RE_DIFF_GIT_HEADER.match(line) if is_diff_git_header: patch_info = PatchInfo() source_file = is_diff_git_header.group('source') target_file = is_diff_git_header.group('target') current_file = PatchedFile( patch_info, source_file, target_file, None, None) self.append(current_file) patch_info.append(line) continue # check for a git new file is_diff_git_new_file = RE_DIFF_GIT_NEW_FILE.match(line) if is_diff_git_new_file: if current_file is None or patch_info is None: raise UnidiffParseError('Unexpected new file found: %s' % line) current_file.source_file = DEV_NULL patch_info.append(line) continue # check for a git deleted file is_diff_git_deleted_file = RE_DIFF_GIT_DELETED_FILE.match(line) if is_diff_git_deleted_file: if current_file is None or patch_info is None: raise UnidiffParseError('Unexpected deleted file found: %s' % line) current_file.target_file = DEV_NULL patch_info.append(line) continue # 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, unless we are processing a rename # (in that case, source files should match) if current_file is not None and not ( current_file.source_file == source_file): current_file = None elif current_file is not None: current_file.source_timestamp = source_timestamp continue # check for target file header is_target_filename = RE_TARGET_FILENAME.match(line) if is_target_filename: target_file = is_target_filename.group('filename') target_timestamp = is_target_filename.group('timestamp') if current_file is not None and not (current_file.target_file == target_file): raise UnidiffParseError('Target without source: %s' % line) if current_file is None: # add current file to PatchSet current_file = PatchedFile( patch_info, source_file, target_file, source_timestamp, target_timestamp) self.append(current_file) patch_info = None else: current_file.target_timestamp = target_timestamp continue # check for hunk header is_hunk_header = RE_HUNK_HEADER.match(line) if is_hunk_header: patch_info = None if current_file is None: raise UnidiffParseError('Unexpected hunk found: %s' % line) current_file._parse_hunk(line, diff, encoding, metadata_only) continue # 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() continue # sometimes hunks can be followed by empty lines if line == '\n' and current_file is not None: current_file._append_trailing_empty_line() continue # if nothing has matched above then this line is a patch info if patch_info is None: current_file = None patch_info = PatchInfo() is_binary_diff = RE_BINARY_DIFF.match(line) if is_binary_diff: source_file = is_binary_diff.group('source_filename') target_file = is_binary_diff.group('target_filename') patch_info.append(line) if current_file is not None: current_file.is_binary_file = True else: current_file = PatchedFile( patch_info, source_file, target_file, is_binary_file=True) self.append(current_file) patch_info = None current_file = None continue patch_info.append(line) @classmethod def from_filename(cls, filename, encoding=DEFAULT_ENCODING, errors=None, newline=None): # type: (str, str, Optional[str]) -> PatchSet """Return a PatchSet instance given a diff filename.""" with open_file(filename, 'r', encoding=encoding, errors=errors, newline=newline) as f: instance = cls(f) return instance @staticmethod def _convert_string(data, encoding=None, errors='strict'): # type: (Union[str, bytes], str, str) -> StringIO if encoding is not None: # if encoding is given, assume bytes and decode data = unicode(data, encoding=encoding, errors=errors) return StringIO(data) @classmethod def from_string(cls, data, encoding=None, errors='strict'): # type: (str, str, Optional[str]) -> PatchSet """Return a PatchSet instance given a diff string.""" return cls(cls._convert_string(data, encoding, errors)) @property def added_files(self): # type: () -> list[PatchedFile] """Return patch added files as a list.""" return [f for f in self if f.is_added_file] @property def removed_files(self): # type: () -> list[PatchedFile] """Return patch removed files as a list.""" return [f for f in self if f.is_removed_file] @property def modified_files(self): # type: () -> list[PatchedFile] """Return patch modified files as a list.""" return [f for f in self if f.is_modified_file] @property def added(self): # type: () -> int """Return the patch total added lines.""" return sum([f.added for f in self]) @property def removed(self): # type: () -> int """Return the patch total removed lines.""" return sum([f.removed for f in self])