Skip to content

Commit ba32395

Browse files
authored
Merge pull request #13 from graingert/reverse
Support for reverse patching
2 parents 39c8edd + c1c2bc1 commit ba32395

File tree

14 files changed

+1503
-1231
lines changed

14 files changed

+1503
-1231
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ pip-log.txt
3131
.coverage
3232
.tox
3333
nosetests.xml
34+
.noseids
3435

3536
# Translations
3637
*.mo

.travis.yml

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1+
sudo: false
12
language: python
2-
python:
3-
- "2.6"
4-
- "2.7"
5-
- "3.3"
6-
- "3.4"
7-
- "3.5"
3+
python: "3.5"
4+
env:
5+
- TOX_ENV=py27
6+
- TOX_ENV=py33
7+
- TOX_ENV=py34
8+
- TOX_ENV=py35
9+
- TOX_ENV=lint
10+
matrix:
11+
include:
12+
- python: 3.6
13+
env: TOX_ENV=py36
814
addons:
915
apt:
1016
packages:
1117
- ed
12-
install:
13-
- pip install .
14-
script: nosetests
18+
install: pip install tox
19+
script: tox -e $TOX_ENV
20+
cache: pip

README.rst

Lines changed: 43 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ What The Patch!?
44
.. image:: https://travis-ci.org/cscorley/whatthepatch.svg?style=flat
55
:target: https://travis-ci.org/cscorley/whatthepatch
66

7-
What The Patch!? is a library for parsing patch files. Its only purpose is to
8-
read a patch file and get it into some usable form by other programs.
7+
What The Patch!? is a library for both parsing and applying patch files.
98

109
Features
1110
---------
@@ -71,47 +70,41 @@ each diff in the patch:
7170
.. code-block:: python
7271
7372
>>> import whatthepatch
74-
>>> with open('somechanges.patch') as f:
73+
>>> import pprint
74+
>>> with open('tests/casefiles/diff-unified.diff') as f:
7575
... text = f.read()
7676
...
7777
>>> for diff in whatthepatch.parse_patch(text):
78-
... print(diff)
78+
... print(diff) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
7979
...
80-
diff(header=header(
81-
index_path=None,
82-
old_path='lao',
83-
old_version='2012-12-26 23:16:54.000000000 -0600',
84-
new_path='tzu',
85-
new_version='2012-12-26 23:16:50.000000000 -0600'
86-
),
87-
changes=[
88-
(1, None, 'The Way that can be told of is not the eternal Way;'),
89-
(2, None, 'The name that can be named is not the eternal name.'),
90-
(3, 1, 'The Nameless is the origin of Heaven and Earth;'),
91-
(4, None, 'The Named is the mother of all things.'),
92-
(None, 2, 'The named is the mother of all things.'),
93-
(None, 3, ''),
94-
(5, 4, 'Therefore let there always be non-being,'),
95-
(6, 5, ' so we may see their subtlety,'),
96-
(7, 6, 'And let there always be being,'),
97-
(9, 8, 'The two are the same,'),
98-
(10, 9, 'But after they are produced,'),
99-
(11, 10, ' they have different names.'),
100-
(None, 11, 'They both may be called deep and profound.'),
101-
(None, 12, 'Deeper and more profound,'),
102-
(None, 13, 'The door of all subtleties!')
103-
]
104-
)
105-
106-
*Edited to show structure of the results*
80+
diff(header=header(index_path=None,
81+
old_path='lao',
82+
old_version='2013-01-05 16:56:19.000000000 -0600',
83+
new_path='tzu',
84+
new_version='2013-01-05 16:56:35.000000000 -0600'),
85+
changes=[Change(old=1, new=None, hunk=1, line='The Way that can be told of is not the eternal Way;'),
86+
Change(old=2, new=None, hunk=1, line='The name that can be named is not the eternal name.'),
87+
Change(old=3, new=1, hunk=1, line='The Nameless is the origin of Heaven and Earth;'),
88+
Change(old=4, new=None, hunk=1, line='The Named is the mother of all things.'),
89+
Change(old=None, new=2, hunk=1, line='The named is the mother of all things.'),
90+
Change(old=None, new=3, hunk=1, line=''), Change(old=5, new=4, hunk=1, line='Therefore let there always be non-being,'),
91+
Change(old=6, new=5, hunk=1, line=' so we may see their subtlety,'),
92+
Change(old=7, new=6, hunk=1, line='And let there always be being,'),
93+
Change(old=9, new=8, hunk=2, line='The two are the same,'),
94+
Change(old=10, new=9, hunk=2, line='But after they are produced,'),
95+
Change(old=11, new=10, hunk=2, line=' they have different names.'),
96+
Change(old=None, new=11, hunk=2, line='They both may be called deep and profound.'),
97+
Change(old=None, new=12, hunk=2, line='Deeper and more profound,'),
98+
Change(old=None, new=13, hunk=2, line='The door of all subtleties!')],
99+
text='...')
107100
108101
The changes are listed as they are in the patch, but instead of the +/- syntax
109102
of the patch, we get a tuple of two numbers and the text of the line.
110103
What these numbers indicate are as follows:
111104

112-
#. ``( 1, None, ... )`` indicates line 1 of the file lao was **removed**.
113-
#. ``( None, 2, ... )`` indicates line 2 of the file tzu was **inserted**.
114-
#. ``( 5, 4, ... )`` indicates that line 5 of lao and line 4 of tzu are **equal**.
105+
#. ``( old=1, new=None, ... )`` indicates line 1 of the file lao was **removed**.
106+
#. ``( old=None, new=2, ... )`` indicates line 2 of the file tzu was **inserted**.
107+
#. ``( old=5, new=4, ... )`` indicates that line 5 of lao and line 4 of tzu are **equal**.
115108

116109
Please note that not all patch formats provide the actual lines modified, so some
117110
results will have the text portion of the tuple set to ``None``.
@@ -124,15 +117,29 @@ To apply a diff to some lines of text, first read the patch and parse it.
124117
.. code-block:: python
125118
126119
>>> import whatthepatch
127-
>>> with open('somechanges.patch') as f:
120+
>>> with open('tests/casefiles/diff-default.diff') as f:
128121
... text = f.read()
129122
...
130-
>>> with open('lao') as f:
123+
>>> with open('tests/casefiles/lao') as f:
131124
... lao = f.read()
132125
...
133126
>>> diff = [x for x in whatthepatch.parse_patch(text)]
134127
>>> diff = diff[0]
135128
>>> tzu = whatthepatch.apply_diff(diff, lao)
129+
>>> tzu # doctest: +NORMALIZE_WHITESPACE
130+
['The Nameless is the origin of Heaven and Earth;',
131+
'The named is the mother of all things.',
132+
'',
133+
'Therefore let there always be non-being,',
134+
' so we may see their subtlety,',
135+
'And let there always be being,',
136+
' so we may see their outcome.',
137+
'The two are the same,',
138+
'But after they are produced,',
139+
' they have different names.',
140+
'They both may be called deep and profound.',
141+
'Deeper and more profound,',
142+
'The door of all subtleties!']
136143
137144
138145
Contribute

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@
3737
"Topic :: Text Processing",
3838
"Programming Language :: Python",
3939
"Programming Language :: Python :: 2",
40-
"Programming Language :: Python :: 2.6",
4140
"Programming Language :: Python :: 2.7",
4241
"Programming Language :: Python :: 3",
4342
"Programming Language :: Python :: 3.3",
4443
"Programming Language :: Python :: 3.4",
4544
"Programming Language :: Python :: 3.5",
45+
"Programming Language :: Python :: 3.6"
4646
],
4747
)
4848

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--- lao 2013-01-05 16:56:19.000000000 -0600
2+
+++ tzu 2013-01-05 16:56:35.000000000 -0600
3+
@@ -1,7 +1,6 @@
4+
-The Way that can be told of is not the eternal Way;
5+
-The name that can be named is not the eternal name.
6+
The Nameless is the origin of Heaven and Earth;
7+
-The Named is the mother of all tings.
8+
+The named is the mother of all things.
9+
+
10+
Therefore let there always be non-being,
11+
so we may see their subtlety,
12+
And let there always be being,
13+
@@ -9,3 +8,6 @@
14+
The two are the same,
15+
But after they are produced,
16+
they have different names.
17+
+They both may be called deep and profound.
18+
+Deeper and more profound,
19+
+The door of all subtleties!
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
--- lao 2013-01-05 16:56:19.000000000 -0600
2+
+++ tzu 2013-01-05 16:56:35.000000000 -0600
3+
@@ -1,7 +1,6 @@
4+
-The Way that can be told of is not the eternal Way;
5+
-The name that can be named is not the eternal name.
6+
The Nameless is the origin of Heaven and Earth;
7+
-The Named is the mother of all things.
8+
+The named is the mother of all things.
9+
+
10+
Therefore let there always be non-being,
11+
so we may see their subtlety,
12+
And let there always be being,
13+
@@ -9,3 +8,6 @@
14+
The two are te same,
15+
But after they are produced,
16+
they have different names.
17+
+They both may be called deep and profound.
18+
+Deeper and more profound,
19+
+The door of all subtleties!

tests/test_apply.py

Lines changed: 88 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
# -*- coding: utf-8 -*-
22

33
import whatthepatch as wtp
4+
from whatthepatch import exceptions
45

5-
6+
from nose.tools import assert_raises
67
import unittest
7-
from io import StringIO
8+
9+
10+
def _apply(src, diff_text, reverse=False, use_patch=False):
11+
diff = next(wtp.parse_patch(diff_text))
12+
return wtp.apply.apply_diff(diff, src, reverse, use_patch)
13+
14+
15+
def _apply_r(src, diff_text, reverse=True, use_patch=False):
16+
return _apply(src, diff_text, reverse, use_patch)
17+
818

919
class ApplyTestSuite(unittest.TestCase):
1020
"""Basic test cases."""
@@ -26,62 +36,116 @@ def test_diff_default(self):
2636
with open('tests/casefiles/diff-default.diff') as f:
2737
diff_text = f.read()
2838

29-
diff = next(wtp.parse_patch(diff_text))
30-
31-
new_text = wtp.apply.apply_diff(diff, self.lao)
32-
self.assertEqual(new_text, self.tzu)
39+
self.assertEqual(_apply(self.lao, diff_text), self.tzu)
40+
self.assertEqual(_apply_r(self.tzu, diff_text), self.lao)
3341

3442
def test_diff_context(self):
3543
with open('tests/casefiles/diff-context.diff') as f:
3644
diff_text = f.read()
3745

38-
diff = next(wtp.parse_patch(diff_text))
39-
40-
new_text = wtp.apply.apply_diff(diff, self.lao)
41-
self.assertEqual(new_text, self.tzu)
46+
self.assertEqual(_apply(self.lao, diff_text), self.tzu)
47+
self.assertEqual(_apply_r(self.tzu, diff_text), self.lao)
4248

4349
def test_diff_unified(self):
4450
with open('tests/casefiles/diff-unified.diff') as f:
4551
diff_text = f.read()
4652

47-
diff = next(wtp.parse_patch(diff_text))
53+
self.assertEqual(_apply(self.lao, diff_text), self.tzu)
54+
self.assertEqual(_apply_r(self.tzu, diff_text), self.lao)
4855

49-
new_text = wtp.apply.apply_diff(diff, self.lao)
56+
def test_diff_unified_bad(self):
57+
with open('tests/casefiles/diff-unified-bad.diff') as f:
58+
diff_text = f.read()
5059

51-
self.assertEqual(new_text, self.tzu)
60+
with assert_raises(exceptions.ApplyException) as ec:
61+
_apply(self.lao, diff_text)
62+
63+
e = ec.exception
64+
e_str = str(e)
65+
assert 'line 4' in e_str
66+
assert 'The Named is the mother of all tings.' in e_str
67+
assert 'The Named is the mother of all things.' in e_str
68+
assert e.hunk == 1
69+
70+
def test_diff_unified_bad2(self):
71+
with open('tests/casefiles/diff-unified-bad2.diff') as f:
72+
diff_text = f.read()
73+
74+
with assert_raises(exceptions.ApplyException) as ec:
75+
_apply(self.lao, diff_text)
76+
77+
e = ec.exception
78+
e_str = str(e)
79+
assert 'line 9' in e_str
80+
assert 'The two are te same,' in e_str
81+
assert 'The two are the same,' in e_str
82+
assert e.hunk == 2
83+
84+
def test_diff_unified_bad_backward(self):
85+
with open('tests/casefiles/diff-unified-bad2.diff') as f:
86+
diff_text = f.read()
87+
88+
with assert_raises(exceptions.ApplyException) as ec:
89+
_apply(self.tzu, diff_text)
90+
91+
e = ec.exception
92+
e_str = str(e)
93+
assert 'line 1' in e_str
94+
assert 'The Way that can be told of is not the eternal Way;' in e_str
95+
assert 'The Nameless is the origin of Heaven and Earth;' in e_str
96+
assert e.hunk == 1
97+
98+
def test_diff_unified_bad_empty_source(self):
99+
with open('tests/casefiles/diff-unified-bad2.diff') as f:
100+
diff_text = f.read()
101+
102+
with assert_raises(exceptions.ApplyException) as ec:
103+
_apply('', diff_text)
104+
105+
e = ec.exception
106+
e_str = str(e)
107+
assert 'line 1' in e_str
108+
assert 'The Way that can be told of is not the eternal Way;' in e_str
109+
assert 'does not exist in source'
110+
assert e.hunk == 1
52111

53112
def test_diff_unified_patchutil(self):
54113
with open('tests/casefiles/diff-unified.diff') as f:
55114
diff_text = f.read()
56115

57-
diff = next(wtp.parse_patch(diff_text))
116+
self.assertEqual(
117+
_apply(self.lao, diff_text, use_patch=True),
118+
(self.tzu, None),
119+
)
120+
self.assertEqual(
121+
_apply_r(self.tzu, diff_text, use_patch=True),
122+
(self.lao, None),
123+
)
58124

59-
new_text = wtp.apply.apply_diff(diff, self.lao, use_patch=True)
125+
new_text = _apply(self.lao, diff_text, use_patch=True)
60126
self.assertEqual(new_text, (self.tzu, None))
61127

62-
self.assertRaises(AssertionError, wtp.apply.apply_diff, diff, [''] + self.lao, use_patch=True)
128+
with assert_raises(exceptions.ApplyException):
129+
_apply([''] + self.lao, diff_text, use_patch=True)
63130

64131
def test_diff_rcs(self):
65132
with open('tests/casefiles/diff-rcs.diff') as f:
66133
diff_text = f.read()
67134

68-
diff = next(wtp.parse_patch(diff_text))
135+
new_text = _apply(self.lao, diff_text)
69136

70-
new_text = wtp.apply.apply_diff(diff, self.lao)
71137
self.assertEqual(new_text, self.tzu)
72138

73139
def test_diff_ed(self):
74-
self.maxDiff = None
75140
with open('tests/casefiles/diff-ed.diff') as f:
76141
diff_text = f.read()
77142

78-
diff = next(wtp.parse_patch(diff_text))
79-
80-
new_text = wtp.apply.apply_diff(diff, self.lao)
81-
self.assertEqual(self.tzu,new_text)
143+
new_text = _apply(self.lao, diff_text)
144+
self.assertEqual(self.tzu, new_text)
82145

83-
new_text = wtp.apply.apply_diff(diff, self.lao, use_patch=True)
146+
new_text = _apply(self.lao, diff_text, use_patch=True)
84147
self.assertEqual(new_text, (self.tzu, None))
85148

149+
86150
if __name__ == '__main__':
87151
unittest.main()

0 commit comments

Comments
 (0)