Skip to content

Commit

Permalink
Merge 24656fe into 0b548d8
Browse files Browse the repository at this point in the history
  • Loading branch information
jedie committed Mar 12, 2020
2 parents 0b548d8 + 24656fe commit ff1f29c
Show file tree
Hide file tree
Showing 20 changed files with 279 additions and 313 deletions.
16 changes: 0 additions & 16 deletions .travis.yml
Expand Up @@ -24,39 +24,23 @@ matrix:
#
- env: TOXENV=py36-django22
python: 3.6
- env: TOXENV=py36-django22-dmp
python: 3.6
- env: TOXENV=py36-django30
python: 3.6
- env: TOXENV=py36-django30-dmp
python: 3.6

- env: TOXENV=py37-django22
python: 3.7
- env: TOXENV=py37-django22-dmp
python: 3.7
- env: TOXENV=py37-django30
python: 3.7
- env: TOXENV=py37-django30-dmp
python: 3.7

- env: TOXENV=py38-django22
python: 3.8
- env: TOXENV=py38-django22-dmp
python: 3.8
- env: TOXENV=py38-django30
python: 3.8
- env: TOXENV=py38-django30-dmp
python: 3.8

- env: TOXENV=pypy3-django22
python: pypy3
- env: TOXENV=pypy3-django22-dmp
python: pypy3
- env: TOXENV=pypy3-django30
python: pypy3
- env: TOXENV=pypy3-django30-dmp
python: pypy3


install:
Expand Down
23 changes: 14 additions & 9 deletions README.creole
Expand Up @@ -21,14 +21,6 @@ Just use:
pip install django-reversion-compare
}}}

Optionally you can install [[https://github.com/google/diff-match-patch|google-diff-match-patch]], otherwise difflib would be used. The easiest way is to use the unofficial package [[http://pypi.python.org/pypi/diff-match-patch/|diff-match-patch]], e.g.:
{{{
poetry install -E diff-match-patch
or
pip install diff-match-patch
}}}

=== Setup ===

Expand Down Expand Up @@ -240,6 +232,14 @@ django-reversion-compare-foobar-py3.6) ~/django-reversion-compare$ ./reversion_c
...
}}}


== Backwards-incompatible changes ==

=== v0.12.0 ===

Google "diff-match-patch" is now mandatory and not optional.


== Version compatibility ==

|= Reversion-Compare |=django-reversion |= Django |= Python
Expand All @@ -262,8 +262,13 @@ Maybe other versions are compatible, too.

== Changelog ==

* *dev* [[https://github.com/jedie/django-reversion-compare/compare/v0.11.0...master|compare v0.11.0...master]]
* *dev* [[https://github.com/jedie/django-reversion-compare/compare/v0.12.0...master|compare v0.12.0...master]]
** TBC
* v0.12.0 - 12.03.2020 [[https://github.com/jedie/django-reversion-compare/compare/v0.11.0...v0.12.0|compare v0.11.0...v0.12.0]]
** [[https://github.com/google/diff-match-patch|google-diff-match-patch]] is now mandatory!
** Diff html code are now unified to {{{<pre class="highlight">...</pre>}}}
** Bugfix {{{make run-test-server}}}
** Switch between Google "diff-match-patch" and {{{difflib.ndiff()}}} by size: ndiff makes more human readable diffs with small values.
* v0.11.0 - 12.03.2020 [[https://github.com/jedie/django-reversion-compare/compare/v0.10.0...v0.11.0|compare v0.10.0...v0.11.0]]
** CHANGE output of diff generated with "diff-match-patch":
*** cleanup html by implement a own html pretty function instead of {{{diff_match_patch.diff_prettyHtml}}} usage
Expand Down
33 changes: 21 additions & 12 deletions README.rst
Expand Up @@ -42,16 +42,6 @@ Just use:

pip install django-reversion-compare

Optionally you can install `google-diff-match-patch <https://github.com/google/diff-match-patch>`_, otherwise difflib would be used. The easiest way is to use the unofficial package `diff-match-patch <http://pypi.python.org/pypi/diff-match-patch/>`_, e.g.:

::

poetry install -E diff-match-patch
or
pip install diff-match-patch

Setup
=====

Expand Down Expand Up @@ -289,6 +279,15 @@ Call manage commands from test project, e.g.:
django-reversion-compare-foobar-py3.6) ~/django-reversion-compare$ ./reversion_compare_tests/manage.py --help
...

------------------------------
Backwards-incompatible changes
------------------------------

v0.12.0
=======

Google "diff-match-patch" is now mandatory and not optional.

---------------------
Version compatibility
---------------------
Expand Down Expand Up @@ -328,10 +327,20 @@ Maybe other versions are compatible, too.
Changelog
---------

* *dev* `compare v0.11.0...master <https://github.com/jedie/django-reversion-compare/compare/v0.11.0...master>`_
* *dev* `compare v0.12.0...master <https://github.com/jedie/django-reversion-compare/compare/v0.12.0...master>`_

* TBC

* v0.12.0 - 12.03.2020 `compare v0.11.0...v0.12.0 <https://github.com/jedie/django-reversion-compare/compare/v0.11.0...v0.12.0>`_

* `google-diff-match-patch <https://github.com/google/diff-match-patch>`_ is now mandatory!

* Diff html code are now unified to ``<pre class="highlight">...</pre>``

* Bugfix ``make run-test-server``

* Switch between Google "diff-match-patch" and ``difflib.ndiff()`` by size: ndiff makes more human readable diffs with small values.

* v0.11.0 - 12.03.2020 `compare v0.10.0...v0.11.0 <https://github.com/jedie/django-reversion-compare/compare/v0.10.0...v0.11.0>`_

* CHANGE output of diff generated with "diff-match-patch":
Expand Down Expand Up @@ -605,4 +614,4 @@ Donation

------------

``Note: this file is generated from README.creole 2020-03-12 11:40:46 with "python-creole"``
``Note: this file is generated from README.creole 2020-03-12 15:28:17 with "python-creole"``
7 changes: 2 additions & 5 deletions pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-reversion-compare"
version = "0.11.0"
version = "0.12.0.rc1"
description = "history compare for django-reversion"

# Will be generated from README.creole with: "poetry run update_rst_readme"
Expand Down Expand Up @@ -43,10 +43,7 @@ python = "^3.6"
# v2.2 LTS - extended support until April 2022
django = "2.2.*"
django-reversion = "*"
diff-match-patch = { version = "*", optional = true }

[tool.poetry.extras]
diff-match-patch = ["diff-match-patch"]
diff-match-patch = "*"

[tool.poetry.dev-dependencies]
pytest = "*"
Expand Down
2 changes: 1 addition & 1 deletion reversion_compare/__init__.py
@@ -1 +1 @@
__version__ = "0.11.0"
__version__ = "0.12.0.rc1"
145 changes: 54 additions & 91 deletions reversion_compare/helpers.py
Expand Up @@ -24,23 +24,27 @@
from django.utils.html import escape
from django.utils.safestring import mark_safe

from diff_match_patch import diff_match_patch

logger = logging.getLogger(__name__)


try:
# https://github.com/google/diff-match-patch
from diff_match_patch import diff_match_patch
except ImportError:
dmp = None
else:
dmp = diff_match_patch()
SEMANTIC = 1
EFFICIENCY = 2

# Change from diff-match-patch to ndiff if old/new values are less than X characters:
CHANGE_DIFF_THRESHOLD = 20


# https://github.com/google/diff-match-patch
dmp = diff_match_patch()


def highlight_diff(diff_text):
"""
Simple highlight a diff text in the way pygments do it ;)
"""
html = ['<pre class="highlight">']
lines = []
for line in diff_text.splitlines():
line = escape(line)

Expand All @@ -49,85 +53,61 @@ def highlight_diff(diff_text):
elif line.startswith("-"):
line = f"<del>{line}</del>"

html.append(line)
lines.append(line)

html.append('</pre>')
return '\n'.join(html)
html = '\n'.join(lines)
return f'<pre class="highlight">{html}</pre>'


def diff_match_patch_pretty_html(diff_text):
def diff_match_patch_pretty_html(diff):
"""
Similar to diff_match_patch.diff_prettyHtml but generated the same html as our
reversion_compare.helpers.highlight_diff
"""
html = ['<pre class="highlight">']
for (op, lines) in diff_text:
lines = escape(lines)
for (op, line) in diff:
line = escape(line)

if op == diff_match_patch.DIFF_INSERT:
lines = ''.join(f'+ {line}' for line in lines.splitlines(keepends=True))
lines = f'<ins>{lines}</ins>'
line = f'<ins>{line}</ins>'
elif op == diff_match_patch.DIFF_DELETE:
lines = ''.join(f'- {line}' for line in lines.splitlines(keepends=True))
lines = f'<del>{lines}</del>'
line = f'<del>{line}</del>'
elif op != diff_match_patch.DIFF_EQUAL:
raise TypeError(f'Unknown op: {op!r}')

html.append(lines)
html.append(line)

html.append('</pre>')
return '\n'.join(html)

return ''.join(html)

SEMANTIC = 1
EFFICIENCY = 2

# Change from ndiff to unified_diff if old/new values are more than X lines:
LINE_COUNT_4_UNIFIED_DIFF = 4

def generate_dmp_diff(value1, value2, cleanup=SEMANTIC):
"""
Generate the diff with Google diff-match-patch
"""
diff = dmp.diff_main(
value1, value2,
checklines=False # run a line-level diff first to identify the changed areas
)
if cleanup == SEMANTIC:
dmp.diff_cleanupSemantic(diff)
elif cleanup == EFFICIENCY:
dmp.diff_cleanupEfficiency(diff)
elif cleanup is not None:
raise ValueError("cleanup parameter should be one of SEMANTIC, EFFICIENCY or None.")

html = diff_match_patch_pretty_html(diff)

def unified_diff(a, b, n=3, lineterm="\n"):
r"""
simmilar to the original difflib.unified_diff except:
- no fromfile/tofile and no fromfiledate/tofiledate info lines
- newline before diff control lines and not after
return html

Example:

>>> for line in unified_diff('one two three four'.split(),
... 'zero one tree four'.split(), lineterm=''):
... print(line)
@@ -1,4 +1,4 @@
+zero
one
-two
-three
+tree
four
"""
started = False
for group in difflib.SequenceMatcher(None, a, b).get_grouped_opcodes(n):
first, last = group[0], group[-1]
file1_range = difflib._format_range_unified(first[1], last[2])
file2_range = difflib._format_range_unified(first[3], last[4])

if not started:
started = True
yield f"@@ -{file1_range} +{file2_range} @@"
else:
yield f"{lineterm}@@ -{file1_range} +{file2_range} @@"

for tag, i1, i2, j1, j2 in group:
if tag == "equal":
for line in a[i1:i2]:
yield " " + line
continue
if tag in ("replace", "delete"):
for line in a[i1:i2]:
yield "-" + line
if tag in ("replace", "insert"):
for line in b[j1:j2]:
yield "+" + line
def generate_ndiff(value1, value2):
value1 = value1.splitlines()
value2 = value2.splitlines()
diff = difflib.ndiff(value1, value2)
diff_text = "\n".join(diff)
html = highlight_diff(diff_text)
return html


def html_diff(value1, value2, cleanup=SEMANTIC):
Expand All @@ -137,34 +117,17 @@ def html_diff(value1, value2, cleanup=SEMANTIC):
The cleanup parameter can be SEMANTIC, EFFICIENCY or None to clean up the diff
for greater human readibility.
"""
value1 = force_text(value1)
value2 = force_text(value2)
if dmp is not None:
# Generate the diff with google-diff-match-patch
diff = dmp.diff_main(value1, value2)
if cleanup == SEMANTIC:
dmp.diff_cleanupSemantic(diff)
elif cleanup == EFFICIENCY:
dmp.diff_cleanupEfficiency(diff)
elif cleanup is not None:
raise ValueError("cleanup parameter should be one of SEMANTIC, EFFICIENCY or None.")

html = diff_match_patch_pretty_html(diff)
else:
# fallback: use built-in difflib
value1 = value1.splitlines()
value2 = value2.splitlines()
value1 = force_text(value1, errors='replace')
value2 = force_text(value2, errors='replace')

if len(value1) > LINE_COUNT_4_UNIFIED_DIFF or len(value2) > LINE_COUNT_4_UNIFIED_DIFF:
diff = unified_diff(value1, value2, n=2)
else:
diff = difflib.ndiff(value1, value2)

diff_text = "\n".join(diff)
html = highlight_diff(diff_text)
if len(value1) > CHANGE_DIFF_THRESHOLD or len(value2) > CHANGE_DIFF_THRESHOLD:
# Bigger values -> use Google diff-match-patch
html = generate_dmp_diff(value1, value2, cleanup)
else:
# For small content use ndiff
html = generate_ndiff(value1, value2)

html = mark_safe(html)

return html


Expand Down
Expand Up @@ -3,7 +3,7 @@
<h3>{% firstof field_diff.field.verbose_name field_diff.field.related_name %}{% if field_diff.is_related and not field_diff.follow %}<sup class="follow">*</sup>{% endif %}</h3>
{% if field_diff.field.help_text %}<p class="help">{{ field_diff.field.help_text }}</p>{% endif %}
<div class="module">
<p>{{ field_diff.diff }}</p>
{{ field_diff.diff }}
</div>
{% empty %}
<div class="module">
Expand Down
Expand Up @@ -37,4 +37,4 @@ def handle(self, *args, **options):
# insert all unittest data into database:
Fixtures(verbose=True).create_all()

self.verbose_call("runserver", use_threading=False, use_reloader=True, verbosity=2)
self.verbose_call("runserver", use_threading=True, use_reloader=True, verbosity=2)

0 comments on commit ff1f29c

Please sign in to comment.