├── .coveragerc ├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── CONTRIBUTING.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── Procfile ├── README.rst ├── demo ├── app.py └── templates │ ├── base.html │ └── ndiff.html ├── diffhtml ├── __init__.py └── ndiff.py ├── requirements-test.txt ├── requirements.txt ├── runtime.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_ndiff.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = 3 | conftest.py 4 | */tests.py 5 | */tests/* 6 | 7 | exclude_lines = 8 | def __repr__ 9 | def __str__ 10 | raise AssertionError 11 | raise NotImplementedError 12 | if 0: 13 | 14 | show_missing = true 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = tab 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | end_of_line = crlf 15 | 16 | [*.html] 17 | indent_size = 2 18 | 19 | [*.py] 20 | indent_style = space 21 | 22 | [*.rst] 23 | indent_style = space 24 | 25 | [*.yml] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [LICENSE] 30 | insert_final_newline = false 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * Diff-HTML version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | # pyenv python configuration file 62 | .python-version 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every 8 | little bit helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/uranusjr/diffhtml/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" 30 | and "help wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | Diff-HTML could always use more documentation, whether as part of the 42 | official Diff-HTML docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/uranusjr/diffhtml/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `diffhtml` for local development. 61 | 62 | 1. Fork the `diffhtml` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/diffhtml.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv diffhtml 70 | $ cd diffhtml/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 80 | 81 | $ flake8 diffhtml tests 82 | $ python setup.py test or py.test 83 | $ tox 84 | 85 | To get flake8 and tox, just pip install them into your virtualenv. 86 | 87 | 6. Commit your changes and push your branch to GitHub:: 88 | 89 | $ git add . 90 | $ git commit -m "Your detailed description of your changes." 91 | $ git push origin name-of-your-bugfix-or-feature 92 | 93 | 7. Submit a pull request through the GitHub website. 94 | 95 | Pull Request Guidelines 96 | ----------------------- 97 | 98 | Before you submit a pull request, check that it meets these guidelines: 99 | 100 | 1. The pull request should include tests. 101 | 2. If the pull request adds functionality, the docs should be updated. Put 102 | your new functionality into a function with a docstring, and add the 103 | feature to the list in README.rst. 104 | 3. The pull request should work for Python 2.6, 2.7, 3.3, 3.4 and 3.5, and for PyPy. Check 105 | https://travis-ci.org/uranusjr/diffhtml/pull_requests 106 | and make sure that the tests pass for all supported Python versions. 107 | 108 | Tips 109 | ---- 110 | 111 | To run a subset of tests:: 112 | 113 | $ py.test tests.test_diffhtml 114 | 115 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 6 | 0.1.1 (2017-04-22) 7 | ------------------ 8 | 9 | * `ndiff` now takes a keyword-only argument `cutoff` to specify reaplce line matching ratio cutoff, as `BlockDiffContext` does. 10 | 11 | 12 | 0.1.0 (2017-04-22) 13 | ------------------ 14 | 15 | * First release on PyPI. 16 | * Initial release with just an `ndiff` API. 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017, Tzu-ping Chung 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.rst 2 | include HISTORY.rst 3 | include LICENSE 4 | include README.rst 5 | 6 | recursive-include tests * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | 10 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 11 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: sh -c 'cd demo && exec waitress-serve --port=$PORT app:app' 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Diff-HTML 3 | ========== 4 | 5 | .. .. image:: https://img.shields.io/pypi/v/diffhtml.svg 6 | :target: https://pypi.python.org/pypi/diffhtml 7 | 8 | .. .. image:: https://img.shields.io/travis/uranusjr/diffhtml.svg 9 | :target: https://travis-ci.org/uranusjr/diffhtml 10 | 11 | .. .. image:: https://readthedocs.org/projects/diffhtml/badge/?version=latest 12 | :target: https://diffhtml.readthedocs.io/en/latest/?badge=latest 13 | :alt: Documentation Status 14 | 15 | .. .. image:: https://pyup.io/repos/github/uranusjr/diffhtml/shield.svg 16 | :target: https://pyup.io/repos/github/uranusjr/diffhtml/ 17 | :alt: Updates 18 | 19 | Tools for generating HTML diff output. 20 | 21 | `A simple demo `_. 22 | 23 | * Free software: ISC license 24 | * Documentation (not set up yet): https://diffhtml.readthedocs.io. 25 | 26 | 27 | Features 28 | -------- 29 | 30 | * TODO 31 | 32 | 33 | Credits 34 | --------- 35 | 36 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template. 37 | 38 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 39 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage 40 | -------------------------------------------------------------------------------- /demo/app.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | import diffhtml 4 | import flask 5 | 6 | from flask import request 7 | from markupsafe import Markup 8 | 9 | 10 | app = flask.Flask( 11 | 'Diff-HTML Demo', 12 | template_folder=pathlib.Path(__file__).parent.joinpath('templates'), 13 | ) 14 | 15 | 16 | DEFAULT_A = """ 17 | I am the very model of a modern Major-General, 18 | I've information vegetable, animal, and mineral, 19 | I know the kings of England, and I quote the fights historical, 20 | From Marathon to Waterloo, in order categorical. 21 | """ 22 | 23 | DEFAULT_B = """ 24 | I am the very model of an anime individual, 25 | I've information on comical, unusual, and moe girl, 26 | I know the girls from galgames, and I quote the lines all chuunibyo, 27 | From Neo Eva to SAO, down to the very last detail. 28 | """ 29 | 30 | 31 | @app.route('/ndiff', methods=['GET', 'POST']) 32 | def ndiff(): 33 | a = request.form.get('a', DEFAULT_A) 34 | b = request.form.get('b', DEFAULT_B) 35 | try: 36 | cutoff = float(request.form.get('cutoff', 0.6)) 37 | except ValueError: 38 | cutoff = 0.6 39 | context = { 40 | 'result': None, 41 | 'cutoff': cutoff, 42 | 'input': {'a': a, 'b': b}, 43 | } 44 | if request.method == 'POST': 45 | context['result'] = Markup('
').join(diffhtml.ndiff( 46 | a.splitlines(), b.splitlines(), cutoff=cutoff, 47 | )) 48 | return flask.render_template('ndiff.html', **context) 49 | 50 | 51 | @app.route('/') 52 | def home(): 53 | return flask.redirect(flask.url_for('ndiff')) 54 | 55 | 56 | if __name__ == '__main__': 57 | app.run() 58 | -------------------------------------------------------------------------------- /demo/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% block title %}{{ title }}{% endblock title %} | Diff-HTML Demo 7 | 8 | 76 | 77 | {% block head %} 78 | {% endblock head %} 79 | 80 | 81 | 82 | 83 | 84 |
{% block heading %}{% endblock heading %}
85 |
{% block main %}{% endblock main %}
86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /demo/templates/ndiff.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}ndiff{% endblock title %} 4 | 5 | 6 | {% block head %} 7 | 35 | {% endblock head %} 36 | 37 | 38 | {% block heading %} 39 | 40 |

ndiff Demo

41 |

This is a simple demo of the Diff-HTML project. See project on GitHub.

42 | 43 | {% endblock heading %} 44 | 45 | 46 | {% block main %} 47 | 48 | {% if result %} 49 |
50 |
51 |
52 |

{{ result }}

53 |
54 |
55 |
56 | {% endif %} 57 | 58 |
59 |

Input text below, submit to diff:

60 |
61 |
62 |
63 | 64 |
65 |
66 | 67 |
68 |
69 |
70 | 74 | 75 |
76 |
77 |
78 | 79 | {% endblock main %} 80 | -------------------------------------------------------------------------------- /diffhtml/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "Tzu-ping Chung" 2 | __email__ = 'uranusjr@gmail.com' 3 | __version__ = '0.1.1' 4 | 5 | from .ndiff import ndiff # noqa 6 | -------------------------------------------------------------------------------- /diffhtml/ndiff.py: -------------------------------------------------------------------------------- 1 | """Implements a differ that outputs HTML context diff. 2 | """ 3 | 4 | import collections 5 | import difflib 6 | 7 | import markupsafe 8 | 9 | 10 | DUMP_HANDLERS = { 11 | 'equal': 'dump_equal', 12 | 'delete': 'dump_delete', 13 | 'insert': 'dump_insert', 14 | 'replace': 'dump_replace', 15 | } 16 | 17 | 18 | def format_tag(tag, content): 19 | content = markupsafe.escape(content) 20 | return markupsafe.Markup('{0}{content}{1}'.format(*tag, content=content)) 21 | 22 | 23 | DiffContext = collections.namedtuple('DiffContext', 'a b loa hia lob hib') 24 | 25 | 26 | class InlineDiffContext(DiffContext): 27 | """Context of a inline diff operation. 28 | """ 29 | insert_tag = ('', '') 30 | delete_tag = ('', '') 31 | 32 | @classmethod 33 | def crunch(cls, cruncher, a, b): 34 | cruncher.set_seqs(a, b) 35 | for tag, ai1, ai2, bi1, bi2 in cruncher.get_opcodes(): 36 | subcontext = cls(a, b, ai1, ai2, bi1, bi2) 37 | yield getattr(subcontext, DUMP_HANDLERS[tag])() 38 | 39 | def dump_equal(self): 40 | return ( 41 | markupsafe.escape(self.a[self.loa:self.hia]), 42 | markupsafe.escape(self.b[self.lob:self.hib]), 43 | ) 44 | 45 | def dump_delete(self): 46 | return (format_tag(self.delete_tag, self.a[self.loa:self.hia]), '') 47 | 48 | def dump_insert(self): 49 | return ('', format_tag(self.insert_tag, self.b[self.lob:self.hib])) 50 | 51 | def dump_replace(self): 52 | return (self.dump_delete()[0], self.dump_insert()[1]) 53 | 54 | 55 | class BlockDiffContext(DiffContext): 56 | """Context of a diff operation. 57 | """ 58 | insert_tag = ('', '') 59 | delete_tag = ('', '') 60 | 61 | DEFAULT_CUTOFF = 0.75 62 | 63 | def __new__(cls, *args, cutoff=DEFAULT_CUTOFF, **kwargs): 64 | self = super().__new__(cls, *args, **kwargs) 65 | self.cutoff = cutoff 66 | return self 67 | 68 | @classmethod 69 | def crunch(cls, cruncher, a, b, *, cutoff): 70 | cruncher.set_seqs(a, b) 71 | for tag, loa, hia, lob, hib in cruncher.get_opcodes(): 72 | context = cls(a, b, loa, hia, lob, hib, cutoff=cutoff) 73 | yield from getattr(context, DUMP_HANDLERS[tag])() 74 | 75 | def dump_equal(self): 76 | for line in self.a[self.loa:self.hia]: 77 | yield markupsafe.escape(line) 78 | 79 | def dump_delete(self): 80 | for line in self.a[self.loa:self.hia]: 81 | yield format_tag(self.delete_tag, line) 82 | 83 | def dump_insert(self): 84 | for line in self.b[self.lob:self.hib]: 85 | yield format_tag(self.insert_tag, line) 86 | 87 | def _dump_replace_lines(self): 88 | if self.loa < self.hia: 89 | if self.lob < self.hib: 90 | yield from self.dump_replace() 91 | else: 92 | yield from self.dump_delete() 93 | elif self.lob < self.hib: 94 | yield from self.dump_insert() 95 | 96 | def dump_replace(self): 97 | # Based on `difflib.Differ._fancy_replace`. 98 | # When replacing one block of lines with another, search the blocks 99 | # for *similar* lines; the best-matching pair (if any) is used as a 100 | # sync point, and intraline difference marking is done on the 101 | # similar pair. Lots of work, but often worth it. 102 | best_ratio = 0 103 | cruncher = difflib.SequenceMatcher(difflib.IS_CHARACTER_JUNK) 104 | 105 | eq = None # Indexes of the identical line. 106 | best = None # Indexes of the best non-identical line. 107 | for bi, bline in enumerate(self.b[self.lob:self.hib], self.lob): 108 | cruncher.set_seq2(bline) 109 | for ai, aline in enumerate(self.a[self.loa:self.hia], self.loa): 110 | if aline == bline: 111 | if not eq: 112 | eq = (ai, bi) 113 | continue 114 | cruncher.set_seq1(aline) 115 | 116 | # Ratio calculation is expensive; this raduces the computation 117 | # as much as possible. 118 | if (cruncher.real_quick_ratio() > best_ratio and 119 | cruncher.quick_ratio() > best_ratio): 120 | ratio = cruncher.ratio() 121 | if ratio > best_ratio: 122 | best_ratio = ratio 123 | best = (ai, bi) 124 | 125 | if best_ratio < self.cutoff: # No "pretty close" pair. 126 | if not eq: # No identical pair either. Just dump the lines. 127 | yield from self.dump_delete() 128 | yield from self.dump_insert() 129 | return 130 | # There's an identical line. Use it. 131 | best_ratio = 1.0 132 | best = eq 133 | else: 134 | eq = None 135 | 136 | # Dump lines up to the best pair. 137 | subcontext = self._replace(hia=best[0], hib=best[1]) 138 | subcontext.cutoff = self.cutoff 139 | yield from subcontext._dump_replace_lines() 140 | 141 | # Intraline marking. 142 | if eq: # Identical lines are simple. 143 | yield markupsafe.escape(self.a[eq[0]]) 144 | else: 145 | pairs = InlineDiffContext.crunch( 146 | cruncher, self.a[best[0]], self.b[best[1]], 147 | ) 148 | a, b = map(lambda x: markupsafe.Markup('').join(x), zip(*pairs)) 149 | yield format_tag(self.delete_tag, a) 150 | yield format_tag(self.insert_tag, b) 151 | 152 | # Dump lines after the best pair. 153 | subcontext = self._replace(loa=best[0] + 1, lob=best[1] + 1) 154 | subcontext.cutoff = self.cutoff 155 | yield from subcontext._dump_replace_lines() 156 | 157 | 158 | def ndiff(a, b, *, cutoff=BlockDiffContext.DEFAULT_CUTOFF): 159 | cruncher = difflib.SequenceMatcher(None, a, b) 160 | yield from BlockDiffContext.crunch(cruncher, a, b, cutoff=cutoff) 161 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | markupsafe 2 | pytest>=2.10 3 | pytest-cov 4 | 5 | -e . 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bumpversion 2 | collective.checkdocs 3 | flask 4 | tox 5 | twine 6 | waitress 7 | wheel 8 | 9 | -r requirements-test.txt 10 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.6.1 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.1 3 | commit = True 4 | tag = False 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:diffhtml/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [tools:pytest] 15 | addopts = --no-cov-on-fail --cov-config .coveragerc 16 | python_files = 17 | test_*.py 18 | tests.py 19 | runxfail = true 20 | 21 | [bdist_wheel] 22 | universal = 1 23 | 24 | [flake8] 25 | exclude = docs 26 | 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | with open('README.rst') as readme_file: 7 | readme = readme_file.read() 8 | 9 | with open('HISTORY.rst') as history_file: 10 | history = history_file.read() 11 | 12 | requirements = [ 13 | 'markupsafe', 14 | ] 15 | 16 | test_requirements = [ 17 | 'pytest>=2.10', 18 | 'pytest-cov', 19 | ] 20 | 21 | setup( 22 | name='diffhtml', 23 | version='0.1.1', 24 | description="Tools for generating HTML diff output.", 25 | long_description=readme + '\n\n' + history, 26 | author="Tzu-ping Chung", 27 | author_email='uranusjr@gmail.com', 28 | url='https://github.com/uranusjr/diffhtml', 29 | packages=[ 30 | 'diffhtml', 31 | ], 32 | package_dir={ 33 | 'diffhtml': 'diffhtml', 34 | }, 35 | include_package_data=True, 36 | install_requires=requirements, 37 | license="ISC license", 38 | zip_safe=False, 39 | keywords='diffhtml', 40 | classifiers=[ 41 | 'Development Status :: 2 - Pre-Alpha', 42 | 'Intended Audience :: Developers', 43 | 'License :: OSI Approved :: ISC License (ISCL)', 44 | 'Natural Language :: English', 45 | 'Programming Language :: Python :: 3', 46 | 'Programming Language :: Python :: 3.3', 47 | 'Programming Language :: Python :: 3.4', 48 | 'Programming Language :: Python :: 3.5', 49 | 'Programming Language :: Python :: 3.6', 50 | ], 51 | test_suite='tests', 52 | tests_require=test_requirements 53 | ) 54 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/test_ndiff.py: -------------------------------------------------------------------------------- 1 | import difflib 2 | 3 | import pytest 4 | 5 | from diffhtml.ndiff import ( 6 | format_tag, ndiff, 7 | BlockDiffContext, InlineDiffContext, 8 | ) 9 | 10 | 11 | def test_format_tag(): 12 | result = format_tag(('
', '
'), '
') 13 | assert result == '
<br>
' 14 | 15 | 16 | inline_examples = [ 17 | ('', ''), 18 | ] 19 | 20 | 21 | @pytest.mark.parametrize(('a', 'b'), inline_examples) 22 | def test_inline_equal(a, b): 23 | ctx = InlineDiffContext(a, b, 5, 9, 6, 10) 24 | assert ctx.dump_equal() == ('<bar', '<bar') 25 | 26 | 27 | @pytest.mark.parametrize(('a', 'b'), inline_examples) 28 | def test_inline_delete(a, b): 29 | ctx = InlineDiffContext(a, b, 11, 16, None, None) 30 | assert ctx.dump_delete() == ('<buz>', '') 31 | 32 | 33 | @pytest.mark.parametrize(('a', 'b'), inline_examples) 34 | def test_inline_insert(a, b): 35 | ctx = InlineDiffContext(a, b, None, None, 11, 16) 36 | assert ctx.dump_insert() == ('', '<rex>') 37 | 38 | 39 | @pytest.mark.parametrize(('a', 'b'), inline_examples) 40 | def test_inline_replace(a, b): 41 | ctx = InlineDiffContext(a, b, 11, 16, 11, 16) 42 | result = ctx.dump_replace() 43 | assert result == ('<buz>', '<rex>') 44 | 45 | 46 | @pytest.mark.parametrize(('a', 'b'), inline_examples) 47 | def test_inline_crunch(a, b): 48 | cruncher = difflib.SequenceMatcher(difflib.IS_CHARACTER_JUNK) 49 | result = list(InlineDiffContext.crunch(cruncher, a, b)) 50 | assert result == [ 51 | ('<foo',) * 2, 52 | ('', 'd'), # Insertion. 53 | ('><bar',) * 2, 54 | ('k', ''), # Deletion. 55 | ('><',) * 2, 56 | ('buz', 'rex'), # Replace. 57 | ('>',) * 2, 58 | ] 59 | 60 | 61 | block_examples = [ 62 | ( 63 | ['', '', '', '', '', ''], 64 | ['', '', '', '', '', ''], 65 | ), 66 | ] 67 | 68 | 69 | @pytest.mark.parametrize(('a', 'b'), block_examples) 70 | def test_block_equal(a, b): 71 | ctx = BlockDiffContext(a, b, 0, 1, 0, 1) 72 | assert list(ctx.dump_equal()) == ['<foo>'] 73 | 74 | 75 | @pytest.mark.parametrize(('a', 'b'), block_examples) 76 | def test_block_delete(a, b): 77 | ctx = BlockDiffContext(a, b, 2, 3, None, None) 78 | assert list(ctx.dump_delete()) == ['<rex>'] 79 | 80 | 81 | @pytest.mark.parametrize(('a', 'b'), block_examples) 82 | def test_block_insert(a, b): 83 | ctx = BlockDiffContext(a, b, None, None, 1, 2) 84 | assert list(ctx.dump_insert()) == ['<bar>'] 85 | 86 | 87 | @pytest.mark.parametrize(('a', 'b'), block_examples) 88 | def test_block_replace(a, b): 89 | ctx = BlockDiffContext(a, b, 3, 6, 3, 6, cutoff=0.8) 90 | assert list(ctx.dump_replace()) == [ 91 | # Identical. 92 | '<mos>', 93 | 94 | # Similar enough to trigger inline diff. 95 | '<nod>', 96 | '<rod>', 97 | 98 | # Not similar enough. 99 | '<doy>', 100 | '<dug>', 101 | ] 102 | 103 | 104 | @pytest.mark.parametrize(('a', 'b'), block_examples) 105 | def test_ndiff(a, b): 106 | assert list(ndiff(a, b)) == [ 107 | '<foo>', 108 | '<bar>', 109 | '<buz>', 110 | '<rex>', 111 | '<mos>', 112 | '<nod>', 113 | '<rod>', 114 | '<doy>', 115 | '<dug>', 116 | ] 117 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py33, py34, py35, py36, flake8, docs 3 | 4 | [testenv:flake8] 5 | basepython = python 6 | deps = flake8 7 | commands = flake8 diffhtml 8 | 9 | [testenv:docs] 10 | basepython = python 11 | deps = collective.checkdocs 12 | commands = python setup.py checkdocs 13 | 14 | [testenv] 15 | deps = 16 | -r{toxinidir}/requirements-test.txt 17 | commands = 18 | py.test --basetemp={envtmpdir} 19 | --------------------------------------------------------------------------------