diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..7e9e9b5
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,3 @@
+[flake8]
+select = E,W,F
+max-line-length = 95
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..1ec5855
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,70 @@
+name: CI
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - python-version: "3.8"
+ # 4.2.5 support is still required but building from source is slow
+ # and has missing dependencies, so we use a prebuilt wheel
+ lxml-spec: "vendor/lxml-4.2.5-cp38-cp38-linux_x86_64.whl"
+ # lxml v4
+ - python-version: "3.8"
+ lxml-spec: "lxml>=4,<5"
+ - python-version: "3.10"
+ lxml-spec: "lxml>=4,<5"
+ - python-version: "3.11"
+ lxml-spec: "lxml>=4,<5"
+ - python-version: "3.12"
+ lxml-spec: "lxml>=4,<5"
+ # lxml v5
+ - python-version: "3.8"
+ lxml-spec: "lxml>=5,<6"
+ - python-version: "3.10"
+ lxml-spec: "lxml>=5,<6"
+ - python-version: "3.11"
+ lxml-spec: "lxml>=5,<6"
+ - python-version: "3.12"
+ lxml-spec: "lxml>=5,<6"
+ # lxml v6
+ - python-version: "3.8"
+ lxml-spec: "lxml>=6,<7"
+ - python-version: "3.10"
+ lxml-spec: "lxml>=6,<7"
+ - python-version: "3.11"
+ lxml-spec: "lxml>=6,<7"
+ - python-version: "3.12"
+ lxml-spec: "lxml>=6,<7"
+ # lxml latest
+ - python-version: "3.12"
+ lxml-spec: "lxml"
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ pip install --upgrade pip
+ pip install "${{ matrix.lxml-spec }}"
+ pip install -r requirements/default.txt -r requirements/testing.txt
+ pip install -e .
+ - name: Run tests
+ run: pytest
+
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
+ with:
+ python-version: "3.12"
+ - run: pip install flake8
+ - run: flake8 htmltreediff
diff --git a/.gitignore b/.gitignore
index f43714d..583d8a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,6 @@ __pycache__/
build
dist
-*.whl
*.egg-info
.tox
.workflow
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 516b989..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-language: python
-install: pip install tox
-script: tox -v -e $TOX_ENV -- --verbose
-dist: xenial
-python:
- - "3.7"
-sudo: false
-env:
- - TOX_ENV=py37-coverage
- - TOX_ENV=py37pep8
- - TOX_ENV=py37
-notifications:
- email:
- - development@policystat.com
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ad2c40a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,73 @@
+# HTML Tree Diff
+
+Structure aware diff of XML and HTML documents.
+
+The intended use is to concisely show the edits that have been made in a
+document, so that authors of html content can review their work.
+
+
+## What do we mean by "HTML Tree Diff"?
+
+* **HTML:** The inputs to the diff function are HTML documents
+* **Tree:** It considers the full XML tree structure of the inputs, not just text based changes.
+* **Diff:** The output is human-readable HTML, using `` and `` tags to show the changes.
+
+
+## Command line interface
+
+You can execute `htmltreediff.cli` directly as a python module, passing it html files to diff:
+
+```
+$ python -m htmltreediff.cli one.html two.html
+
+
+ one
+
+
+ two
+
+
+```
+
+
+## Python API
+
+You can also use htmltreediff from within a python program as a library.
+
+For HTML Changes:
+
+```python
+>>> from htmltreediff import diff
+>>> print(diff('...one...
', '...two...
', pretty=True))
+
+ ...
+
+ one
+
+
+ two
+
+ ...
+
+```
+
+And also for text-only changes:
+
+```python
+>>> print(diff(
+... 'The quick brown fox jumps over the lazy dog.',
+... 'The very quick brown foxes jump over the dog.',
+... plaintext=False,
+... ))
+The very quick brown fox jumpsfoxes jump over the lazy dog.
+```
+
+
+## Running the unit tests
+
+Use `pytest` to run the tests with coverage:
+
+```shell
+$ pip install -r requirements/testing.txt
+$ pytest -v
+```
\ No newline at end of file
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 16bffc5..0000000
--- a/README.rst
+++ /dev/null
@@ -1,74 +0,0 @@
-==============
-HTML Tree Diff
-==============
-
-.. image:: https://travis-ci.org/PolicyStat/htmltreediff.svg?branch=master
- :target: https://travis-ci.org/PolicyStat/htmltreediff
-
-Structure aware diff of XML and HTML documents.
-
-The intended use is to concisely show the edits that have been made in a
-document, so that authors of html content can review their work.
-
-
-What do we mean by "HTML Tree Diff"?
-------------------------------------
-
-* HTML:
- The inputs to the diff function are HTML documents
-* Tree:
- It considers the full XML tree structure of the inputs, not just text based changes.
-* Diff:
- The output is human-readable HTML, using and tags to show the changes.
-
-
-Command line interface
-----------------------
-
-You can execute htmltreediff.cli directly as a python module, passing it html files to diff::
-
- $ python -m htmltreediff.cli one.html two.html
-
-
- one
-
-
- two
-
-
-
-
-Python API
-----------
-
-You can also use htmltreediff from within a python program as a library.
-
-For HTML Changes::
-
- >>> from htmltreediff import diff
- >>> print(diff('...one...
', '...two...
', pretty=True))
-
- ...
-
- one
-
-
- two
-
- ...
-
-
-And also for text-only changes::
-
- >>> print(diff(
- ... 'The quick brown fox jumps over the lazy dog.',
- ... 'The very quick brown foxes jump over the dog.',
- ... plaintext=False,
- ... ))
- The very quick brown fox jumpsfoxes jump over the lazy dog.
-
-
-Running the unit tests
-----------------------
-
-The unit test suite requires the packages ``nose`` and ``coverage`` to run. Just run the ``run_tests.sh`` script, and all the tests will run, with code coverage. Code coverage should always be at 100%.
diff --git a/htmltreediff/diff_core.py b/htmltreediff/diff_core.py
index d279961..124877b 100644
--- a/htmltreediff/diff_core.py
+++ b/htmltreediff/diff_core.py
@@ -1,8 +1,6 @@
import difflib
from xml.dom import Node
-import six
-
from htmltreediff.lcs import matching_blocks_from_hashes
from htmltreediff.text import is_text_junk
from htmltreediff.util import (
@@ -277,7 +275,7 @@ def get_opcodes(matching_blocks):
def _is_junk(hashable_node):
- if isinstance(hashable_node, six.string_types):
+ if isinstance(hashable_node, str):
return is_text_junk(hashable_node)
# Nodes with no text or just whitespace are junk.
for descendant in walk_dom(hashable_node.node):
diff --git a/htmltreediff/test_html.py b/htmltreediff/test_html.py
deleted file mode 100644
index f0d3bab..0000000
--- a/htmltreediff/test_html.py
+++ /dev/null
@@ -1,868 +0,0 @@
-from textwrap import dedent
-
-from nose.tools import assert_equal
-
-from htmltreediff.html import diff
-from htmltreediff.tests import assert_html_equal
-from htmltreediff.changes import distribute
-from htmltreediff.html import add_class_to_empty_del_tags, fix_lists, fix_tables
-from htmltreediff.util import (
- parse_minidom,
- minidom_tostring,
- remove_insignificant_text_nodes,
- remove_xml_declaration,
- get_location,
-)
-from htmltreediff.test_util import collapse
-
-
-# Preprocessing
-
-preprocessing_cases = [
- (
- 'empty document',
- '',
- '',
- ''
- ),
- (
- 'tail text',
- 'one
tail',
- 'one
tail',
- 'one
tail',
- ),
- (
- 'ignore comments',
- '',
- '',
- '',
- ),
- (
- 'ignore style tags',
- '',
- '',
- '',
- ),
- (
- 'style tag in a block of text',
- 'xxxyyy
',
- 'xxxyyy
',
- 'xxxyyy
',
- ),
- (
- 'ignore font tags',
- '',
- '',
- '',
- ),
- (
- 'ignore comment tags',
- '',
- '',
- '',
- ),
- (
- 'illegal text nodes inside tables are not removed',
- '''
-
- illegal text
-
-
- | stuff |
-
-
-
- ''',
- '',
- '', # noqa
- ),
-]
-
-
-def test_preprocessing():
- for description, old_html, target, target_raw, in preprocessing_cases:
- def test():
- dom = parse_minidom(old_html)
- assert_equal(minidom_tostring(dom), target)
- assert_equal(remove_xml_declaration(dom.toxml()), target_raw)
- test.description = description
- yield test
-
-
-def test_remove_insignificant_text_nodes():
- html = dedent('''
-
-
-
- one two three
-
-
-
-
- ''')
- target_html = (' one two three
'
- '')
-
- dom = parse_minidom(html)
- remove_insignificant_text_nodes(dom)
- html = minidom_tostring(dom)
- assert_equal(html, target_html)
-
- # Check that it is idempotent.
- dom = parse_minidom(html)
- remove_insignificant_text_nodes(dom)
- html = minidom_tostring(dom)
- assert_equal(html, target_html)
-
-
-def test_remove_insignificant_text_nodes_nbsp():
- html = dedent('''
-
- ''')
- dom = parse_minidom(html)
- remove_insignificant_text_nodes(dom)
- html = minidom_tostring(dom)
- assert_equal(
- html,
- (''),
- )
-
-
-# Post-processing
-
-def test_other_node_type_inserted():
- changes = diff(
- u'foo
',
- u'foo bar
',
- )
- assert_equal(
- changes,
- 'foo bar
',
- )
-
-
-def test_non_printing_characters():
- changes = diff(
- '',
- '',
- )
- assert_equal(
- changes,
- ''
- )
-
-
-def test_cutoff():
- changes = diff(
- 'totally
',
- 'different
',
- cutoff=0.2,
- )
- assert_equal(
- changes,
- 'The differences from the previous version are too large to show '
- 'concisely.
',
- )
-
-
-def test_html_diff_pretty():
- cases = [
- (
- 'Simple Addition',
- 'one
',
- 'one
two
',
- dedent('''
- one
-
- two
-
- ''').strip(),
- ),
- ]
- for test_name, old_html, new_html, pretty_changes in cases:
- def test():
- changes = diff(old_html, new_html, cutoff=0.0, pretty=True)
- assert_equal(pretty_changes, changes)
- test.description = 'test_html_diff_pretty - %s' % test_name
- yield test
-
-
-def test_distribute():
- cases = [
- ('AB',
- 'AB'),
- ]
- for original, distributed in cases:
- def test(original, distributed):
- original = parse_minidom(original)
- distributed = parse_minidom(distributed)
- node = get_location(original, [0])
- distribute(node)
- assert_html_equal(
- minidom_tostring(original),
- minidom_tostring(distributed),
- )
- yield test, original, distributed
-
-
-def test_get_location():
- html = 'AB'
- original = parse_minidom(html)
- try:
- get_location(original, [10])
- raise AssertionError('ValueError not raised')
- except ValueError:
- pass
-
-
-def test_fix_lists():
- cases = [
- (
- 'simple list item insert',
- '''
-
- - one
- - two
-
- ''',
- '''
-
- - one
- - two
-
- '''
- ),
- (
- 'multiple list item insert',
- '''
-
- - one
-
- - two
- - three
-
-
- ''',
- '''
-
- - one
- - two
- - three
-
- '''
- ),
- (
- 'simple list item delete afterward',
- '''
-
- - one
- - one and a half
-
- ''',
- '''
-
- - one
- one and a half
-
- '''
- ),
- (
- 'simple list item delete first',
- '''
-
- - one half
- - one
-
- ''',
- '''
-
- one half
- - one
-
- '''
- ),
- (
- 'multiple list item delete first',
- '''
-
-
- - one third
- - two thirds
-
- - one
-
- ''',
- '''
-
- one third
- two thirds
- - one
-
- '''
- ),
- (
- 'insert and delete separately',
- '''
-
- - one
- - two
- - three
- - three point five
- - four
-
- ''',
- '''
-
- - one
- - two
- - three
-
three point five
- - four
-
- '''
- ),
- (
- 'multiple list item delete',
- '''
-
- - one
-
- - two
- - three
-
-
- ''',
- '''
-
- - one
- two
- three
-
- '''
- ),
- (
- 'delete only list item',
- '''
-
-
- - one
-
-
- ''',
- '''
-
- one
-
- '''
- ),
- (
- 'LI full content change does not add another LI',
- '''
-
-
- - AAA
-
-
- - BBB
-
-
- ''',
- '''
-
- AAABBB
-
- '''
- ),
- (
- 'LI full content change keeps attrs',
- '''
-
-
- - AAA
-
-
- - BBB
-
-
- ''',
- '''
-
- AAABBB
-
- '''
- ),
- (
- 'LI changes markup internalization fix not done if next tag is not an insert', # noqa
- '''
-
-
- - AAA
-
- - BBB
-
- - CCC
-
-
- ''',
- '''
-
- -
-
AAA
-
- - BBB
- - CCC
-
- ''',
- ),
- (
- 'LI changes markup internalization fix not done if next tag is not an insert', # noqa
- '''
-
-
- - AAA
-
- - BBB
-
- - CCC
-
-
- ''',
- '''
-
- -
-
AAA
-
- - BBB
- - CCC
-
- ''',
- ),
- (
- 'LI after del must be ins',
- '''
-
-
- - AAA
-
-
- - BBB
-
-
- - CCC
-
-
- ''',
- '''
-
- -
-
AAA
-
- BBBCCC
-
- ''',
- ),
- (
- 'LI changes markup internalization fix not performed if next tags child is not li', # noqa
- '''
-
-
- - AAA
-
-
- BBB
-
-
- ''',
- '''
-
- -
-
AAA
-
-
- BBB
-
-
- ''',
- ),
- (
- 'LI changes markup internalization fix not performed if next tags is text', # noqa
- '''
-
-
- - AAA
-
-
- BBB
-
-
- ''',
- '''
-
- -
-
AAA
-
-
- BBB
-
-
- ''',
- ),
- ]
- for test_name, changes, fixed_changes in cases:
- changes = collapse(changes)
- fixed_changes = collapse(fixed_changes)
-
- def test():
- changes_dom = parse_minidom(changes)
- fix_lists(changes_dom)
- assert_html_equal(minidom_tostring(changes_dom), fixed_changes)
- test.description = 'test_fix_lists - %s' % test_name
- yield test
-
-
-def test_fix_tables():
- cases = [
- (
- 'add a table row',
- '''
-
- ''',
- '''
-
- '''
- ),
- (
- 'tbody inside ins is distributed',
- '''
-
- ''',
- '''
-
- '''
- ),
- (
- 'tbody inside del is distributed',
- '''
-
- ''',
- '''
-
- '''
- ),
- (
- 'thead inside ins is distributed',
- '''
-
- ''',
- '''
-
- '''
- ),
- (
- 'thead inside del is distributed',
- '''
-
- ''',
- '''
-
- '''
- ),
- (
- 'tfoot inside ins is distributed',
- '''
-
- ''',
- '''
-
- '''
- ),
- (
- 'tfoot inside del is distributed',
- '''
-
- ''',
- '''
-
- '''
- ),
- (
- 'tbody del and ins pair is internalized',
- '''
-
- ''',
- '''
-
- '''
- ),
- (
- 'thead del and ins pair is internalized',
- '''
-
- | old header |
|---|
- | new header |
- | data |
-
- ''',
- '''
-
- old headernew header |
- | data |
-
- '''
- ),
- (
- 'tfoot del and ins pair is internalized',
- '''
-
- | data |
- | old footer |
- | new footer |
-
- ''',
- '''
-
- | data |
- old footernew footer |
-
- '''
- ),
- (
- 'tr del and ins pair is internalized',
- '''
-
-
- | old row |
- | new row |
-
-
- ''',
- '''
-
- '''
- ),
- (
- 'remove ins and del tags at the wrong level of the table',
- '''
-
-
-
-
-
-
-
-
-
-
-
-
- A |
-
-
-
- ''',
- '''
-
- ''',
- ),
- ]
- for test_name, changes, fixed_changes in cases:
- changes = collapse(changes)
- fixed_changes = collapse(fixed_changes)
-
- def test():
- changes_dom = parse_minidom(changes, strict_xml=True)
- fix_tables(changes_dom)
- assert_html_equal(minidom_tostring(changes_dom), fixed_changes)
- test.description = 'test_fix_tables - %s' % test_name
- yield test
-
-
-def test_diff_focused_on_changed_cells_when_colgroup_added():
- old_html = (
- ''
- '| Alpha | unchanged |
'
- '| Beta | old value |
'
- '
'
- )
- new_html = (
- ''
- '| Alpha | unchanged |
'
- '| Beta | new value |
'
- '
'
- )
- expected = (
- ''
- '| Alpha | unchanged |
'
- '| Beta | oldnew value |
'
- '
'
- )
- assert_equal(diff(old_html, new_html), expected)
-
-
-def test_similar_rows_not_misaligned_with_colgroup():
- """
- When a colgroup is added and rows share boilerplate text, pairwise
- fuzzy matching should align each old row to its positional counterpart
- rather than misaligning due to non-transitive text similarity.
-
- The text lengths here are tuned to trigger SequenceMatcher misalignment
- in the old code path; do not shorten them.
- """
- old_html = (
- ''
- '| Alpha | shared setup text | Rate: check | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
- '| Beta | shared setup text | Rate: check | Rinse. |
'
- '| Gamma | shared setup text | Rate: check | Rinse. |
'
- '
'
- )
- new_html = (
- ''
- '| Alpha | shared setup text | Rate: changed1 | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
- '| Beta | shared setup text | Rate: changed2 | Rinse. |
' # noqa E501
- '| Gamma | shared setup text | Rate: changed3 | Rinse. |
' # noqa E501
- '
'
- )
- expected = (
- ''
- '| Alpha | shared setup text | Rate: checkchanged1 | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
- '| Beta | shared setup text | Rate: checkchanged2 | Rinse. |
' # noqa E501
- '| Gamma | shared setup text | Rate: checkchanged3 | Rinse. |
' # noqa E501
- '
'
- )
- assert_equal(diff(old_html, new_html), expected)
-
-
-def test_similar_rows_not_misaligned_without_colgroup():
- """
- Same non-transitive fuzzy equality problem as above, but triggered
- without a colgroup -- every row has a small change so no exact matches
- exist and all rows go through fuzzy matching.
-
- The text lengths here are tuned to trigger SequenceMatcher misalignment
- in the old code path; do not shorten them.
- """
- old_html = (
- ''
- '| Alpha | shared setup text | Rate: check | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
- '| Beta | shared setup text | Rate: check | Rinse. |
'
- '| Gamma | shared setup text | Rate: check | Rinse. |
'
- '
'
- )
- new_html = (
- ''
- '| Alpha | shared setup text | Rate: changed1 | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
- '| Beta | shared setup text | Rate: changed2 | Rinse. |
' # noqa E501
- '| Gamma | shared setup text | Rate: changed3 | Rinse. |
' # noqa E501
- '
'
- )
- expected = (
- ''
- '| Alpha | shared setup text | Rate: checkchanged1 | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
- '| Beta | shared setup text | Rate: checkchanged2 | Rinse. |
' # noqa E501
- '| Gamma | shared setup text | Rate: checkchanged3 | Rinse. |
' # noqa E501
- '
'
- )
- assert_equal(diff(old_html, new_html), expected)
-
-
-def test_add_class_to_empty_del_tags():
- cases = [
- (
- 'empty del tag',
- '',
- '',
- ),
- (
- 'del tag with space',
- ' ',
- ' ',
- ),
- (
- 'del tag with child',
- '
',
- '
',
- ),
- (
- 'del tag with spaces and characters',
- ' abc ',
- ' abc ',
- ),
-
- ]
- for test_name, test_input, expected_result in cases:
- def test():
- dom = parse_minidom(test_input, strict_xml=True)
- add_class_to_empty_del_tags(dom)
- assert_html_equal(minidom_tostring(dom), expected_result)
- test.description = 'test_add_class_to_empty_del_tags - %s' % test_name
- yield test
diff --git a/htmltreediff/test_text.py b/htmltreediff/test_text.py
deleted file mode 100644
index b0718c5..0000000
--- a/htmltreediff/test_text.py
+++ /dev/null
@@ -1,107 +0,0 @@
-# coding: utf8
-from nose.tools import assert_equal
-
-from htmltreediff.html import diff
-from htmltreediff.text import split_text
-
-
-def test_text_split():
- cases = [
- ('word',
- ['word']),
- ('two words',
- ['two', ' ', 'words']),
- ('abcdef12',
- ['abcdef', '12']),
- ('entity"s',
- ['entity', '"', 's']),
- ('stuff stuff',
- ['stuff', ' ', 'stuff']),
- (
- 'Stuff with an ampersand A&B stuff. Stuff with a semicolon; more stuff.', # noqa
- [
- 'Stuff', ' ', 'with', ' ', 'an', ' ', 'ampersand', ' ', 'A',
- '&', 'B', ' ', 'stuff', '.', ' ', 'Stuff', ' ', 'with', ' ',
- 'a', ' ', 'semicolon', ';', ' ', 'more', ' ', 'stuff', '.',
- ],
- ),
- ("we're excited",
- ["we're", " ", "excited"]),
- ('dial 1-800-555-1234',
- ['dial', ' ', '1-800-555-1234']),
- ('Effective 1/2/2003',
- ['Effective', ' ', '1/2/2003']),
- (u'über français',
- [u'über', u' ', u'français']),
- (u'em dashes \u2013 \u2013',
- [u'em', u' ', u'dashes', u' ', u'\u2013', u' ', u'\u2013']),
- ]
- for text, target in cases:
- def test():
- assert_equal(split_text(text), target)
- yield test
-
-
-def test_text_diff():
- cases = [
- (
- 'sub-word changes',
- 'The quick brown fox jumps over the lazy dog.',
- 'The very quick brown foxes jump over the dog.',
- 'The very quick brown fox jumpsfoxes jump over the lazy dog.', # noqa
- ),
- (
- 'special characters',
- 'Assume that A < B, and A & B = {}',
- 'If we assume that A < B, and A & B = {}',
- 'AssumeIf we assume that A < B, and A & B = {}', # noqa
- ),
- (
- 'contractions',
- "we were excited",
- "we're excited",
- "we werewe're excited",
- ),
- (
- 'dates',
- 'Effective 1/2/2003',
- 'Effective 3/4/2005',
- 'Effective 1/2/20033/4/2005',
- ),
- (
- 'text diff with <',
- 'x',
- '<',
- 'x<',
- ),
- (
- 'text diff with >',
- 'x',
- '>',
- 'x>',
- ),
- (
- 'text diff with &',
- 'x',
- '&',
- 'x&',
- ),
- (
- 'do not remove newlines unless necessary',
- 'one two three\nfour six',
- 'one three\nfour five six',
- 'one two three\nfour five six',
- ),
- # long text diff is broken
- # (
- # 'long text diff',
- # open('htmltreediff/fixtures/long_diff/before.txt').read(),
- # open('htmltreediff/fixtures/long_diff/after.txt').read(),
- # open('htmltreediff/fixtures/long_diff/diff.html').read(),
- # ),
- ]
- for description, old, new, changes in cases:
- def test():
- assert_equal(diff(old, new, plaintext=True), changes)
- test.description = 'test_text_diff - %s' % description
- yield test
diff --git a/htmltreediff/tests/__init__.py b/htmltreediff/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/htmltreediff/test_cli.py b/htmltreediff/tests/test_cli.py
similarity index 65%
rename from htmltreediff/test_cli.py
rename to htmltreediff/tests/test_cli.py
index e5cba8b..b46f288 100644
--- a/htmltreediff/test_cli.py
+++ b/htmltreediff/tests/test_cli.py
@@ -1,10 +1,8 @@
import sys
import tempfile
-from six import StringIO
+from io import StringIO
from textwrap import dedent
-from nose.tools import assert_equal
-
from htmltreediff.cli import main
@@ -21,14 +19,12 @@ def test_main():
try:
sys.stdout = stream = StringIO()
main(argv=('', f1.name, f2.name))
- assert_equal(
- stream.getvalue(),
- dedent('''
- one
-
- two
-
- ''').strip() + '\n',
- )
+ expected = dedent('''
+ one
+
+ two
+
+ ''').strip() + '\n'
+ assert stream.getvalue() == expected
finally:
sys.stdout = old_stdout
diff --git a/htmltreediff/tests.py b/htmltreediff/tests/test_core.py
similarity index 93%
rename from htmltreediff/tests.py
rename to htmltreediff/tests/test_core.py
index 979eed8..071374b 100644
--- a/htmltreediff/tests.py
+++ b/htmltreediff/tests/test_core.py
@@ -1,28 +1,27 @@
-# coding: utf8
-
from pprint import pformat
from xml.dom import Node
-from nose.tools import assert_equal
+import pytest
from htmltreediff.html import diff
from htmltreediff.util import (
parse_minidom,
parse_text,
minidom_tostring,
- html_equal,
is_text,
)
-from htmltreediff.test_util import (
- reverse_edit_script,
- reverse_changes_html,
+
+from htmltreediff.tests.util import (
+ assert_html_equal,
+ assert_html_not_equal,
+ assert_strip_changes,
+ collapse,
get_edit_script,
html_patch,
- strip_changes_old,
- strip_changes_new,
- remove_attributes,
- collapse,
parse_cases,
+ remove_attributes,
+ reverse_changes_html,
+ reverse_edit_script,
)
# since the test cases get automatically reversed, only include insert cases,
@@ -1206,29 +1205,6 @@ def reverse_cases(cases):
all_test_cases = (test_cases + reverse_test_cases + one_way_test_cases + insane_test_cases)
-def assert_html_equal(a_html, b_html):
- assert html_equal(a_html, b_html), (
- u'These html documents are not equal:\n%r\n====\n%r' % (
- a_html,
- b_html,
- )
- )
-
-
-def assert_html_not_equal(a_html, b_html):
- assert not html_equal(a_html, b_html), (
- u'These html documents should not be equal:\n%r\n====\n%r' % (
- a_html,
- b_html,
- )
- )
-
-
-def assert_strip_changes(old_html, new_html, changes):
- assert_html_equal(old_html, strip_changes_old(changes))
- assert_html_equal(new_html, strip_changes_new(changes))
-
-
def test_parse_comments():
assert_html_equal(
minidom_tostring(parse_minidom('')),
@@ -1248,7 +1224,7 @@ def test_parse_text():
text = 'test one two < & > ;'
dom = parse_text(text)
root = dom.documentElement
- assert_equal(len(root.childNodes), 1)
+ assert len(root.childNodes) == 1
child = root.childNodes[0]
assert is_text(child)
assert child.nodeValue == text
@@ -1311,57 +1287,42 @@ def test_remove_attributes():
assert_html_equal(remove_attributes(html), stripped_html)
-def test_edit_script():
- # edit script output does not reverse easily, don't test the reverse cases
- for case in parse_cases(test_cases + one_way_test_cases):
- if not case.edit_script:
- continue
+def _edit_script_cases():
+ return [
+ case for case in parse_cases(test_cases + one_way_test_cases)
+ if case.edit_script
+ ]
+
- def test():
- actual_edit_script = get_edit_script(case.old_html, case.new_html)
- assert_equal(
- case.edit_script,
- actual_edit_script,
- ('These edit scripts do not match:\n%s\n!=\n%s'
- % (pformat(case.edit_script), pformat(actual_edit_script))),
- )
- test.description = 'test_edit_script - %s' % case.name
- yield test
+@pytest.mark.parametrize('case', _edit_script_cases(), ids=lambda c: c.name)
+def test_edit_script(case):
+ actual_edit_script = get_edit_script(case.old_html, case.new_html)
+ assert case.edit_script == actual_edit_script, (
+ 'These edit scripts do not match:\n'
+ f'{pformat(case.edit_script)}\n!=\n{pformat(actual_edit_script)}'
+ )
-def test_html_patch():
- for case in parse_cases(all_test_cases):
- # check that applying the diff gives back the same new_html
- def test():
- edit_script = []
- edit_script = get_edit_script(case.old_html, case.new_html)
- edited_html = html_patch(case.old_html, edit_script)
- assert_html_equal(
- remove_attributes(edited_html),
- remove_attributes(case.new_html),
- )
- test.description = 'test_html_patch - %s' % case.name
- yield test
+@pytest.mark.parametrize('case', list(parse_cases(all_test_cases)), ids=lambda c: c.name)
+def test_html_patch(case):
+ edit_script = get_edit_script(case.old_html, case.new_html)
+ edited_html = html_patch(case.old_html, edit_script)
+ assert_html_equal(
+ remove_attributes(edited_html),
+ remove_attributes(case.new_html),
+ )
-def test_cases_sanity():
- # check that removing the ins and del markup gives the original
- sane_cases = (test_cases + reverse_test_cases + one_way_test_cases)
- for case in parse_cases(sane_cases):
- def test():
- assert_strip_changes(
- case.old_html,
- case.new_html,
- case.target_changes,
- )
- test.description = 'test_cases_sanity - %s' % case.name
- yield test
+@pytest.mark.parametrize(
+ 'case',
+ list(parse_cases(test_cases + reverse_test_cases + one_way_test_cases)),
+ ids=lambda c: c.name,
+)
+def test_cases_sanity(case):
+ assert_strip_changes(case.old_html, case.new_html, case.target_changes)
-def test_html_diff():
- for case in parse_cases(all_test_cases):
- def test():
- changes = diff(case.old_html, case.new_html, cutoff=0.0)
- assert_html_equal(changes, case.target_changes)
- test.description = 'test_html_diff - %s' % case.name
- yield test
+@pytest.mark.parametrize('case', list(parse_cases(all_test_cases)), ids=lambda c: c.name)
+def test_html_diff(case):
+ changes = diff(case.old_html, case.new_html, cutoff=0.0)
+ assert_html_equal(changes, case.target_changes)
diff --git a/htmltreediff/test_diff_core.py b/htmltreediff/tests/test_diff_core.py
similarity index 73%
rename from htmltreediff/test_diff_core.py
rename to htmltreediff/tests/test_diff_core.py
index 9f5f77a..90da59b 100644
--- a/htmltreediff/test_diff_core.py
+++ b/htmltreediff/tests/test_diff_core.py
@@ -1,8 +1,5 @@
-# coding: utf-8
from unittest.mock import patch
-from nose.tools import assert_equal
-
from htmltreediff.diff_core import (
_has_fuzzy_hash_collisions,
fuzzy_match_blocks,
@@ -26,40 +23,37 @@ def get_dom_nodes(html):
def test_has_fuzzy_hash_collisions_no_children():
- assert_equal(_has_fuzzy_hash_collisions([]), False)
+ assert _has_fuzzy_hash_collisions([]) is False
def test_all_distinct_tags_dont_have_collisions():
- assert_equal(_has_fuzzy_hash_collisions(get_dom_nodes('a
b
')), False)
+ assert _has_fuzzy_hash_collisions(get_dom_nodes('a
b
')) is False
def test_same_tag_twice_has_collision():
- assert_equal(_has_fuzzy_hash_collisions(get_dom_nodes('a
b
')), True)
+ assert _has_fuzzy_hash_collisions(get_dom_nodes('a
b
')) is True
def test_text_nodes_ignored_in_collision_check():
- assert_equal(_has_fuzzy_hash_collisions(get_dom_nodes('hello a
b
')), False)
+ assert _has_fuzzy_hash_collisions(get_dom_nodes('hello a
b
')) is False
# --- build_pairwise_match_matrix ---
def test_build_pairwise_match_matrix_no_matches():
- assert_equal(
- build_pairwise_match_matrix(['a', 'b'], ['c', 'd']),
- [[False, False], [False, False]],
- )
+ expected = [[False, False], [False, False]]
+ assert build_pairwise_match_matrix(['a', 'b'], ['c', 'd']) == expected
def test_build_pairwise_match_matrix_some_matches():
- assert_equal(
- build_pairwise_match_matrix(['a', 'b'], ['b', 'c']),
- [[False, False], [True, False]],
- )
+ expected = [[False, False], [True, False]]
+ assert build_pairwise_match_matrix(['a', 'b'], ['b', 'c']) == expected
def test_build_pairwise_match_matrix_empty():
- assert_equal(build_pairwise_match_matrix([], []), [])
+ expected = []
+ assert build_pairwise_match_matrix([], []) == expected
# --- compute_longest_common_subsequence_lengths_table ---
@@ -68,13 +62,13 @@ def test_build_pairwise_match_matrix_empty():
def test_lcs_lengths_no_matches():
match_matrix = [[False, False], [False, False]]
lcs_lengths = compute_longest_common_subsequence_lengths_table(match_matrix)
- assert_equal(lcs_lengths[0][0], 0)
+ assert lcs_lengths[0][0] == 0
def test_lcs_lengths_diagonal_matches_accumulate():
match_matrix = [[True, False], [False, True]]
lcs_lengths = compute_longest_common_subsequence_lengths_table(match_matrix)
- assert_equal(lcs_lengths[0][0], 2)
+ assert lcs_lengths[0][0] == 2
# --- traceback_longest_common_subsequence_matched_pairs ---
@@ -83,19 +77,16 @@ def test_lcs_lengths_diagonal_matches_accumulate():
def test_traceback_no_matches_returns_no_pairs():
match_matrix = [[False, False], [False, False]]
lcs_lengths = compute_longest_common_subsequence_lengths_table(match_matrix)
- assert_equal(
- traceback_longest_common_subsequence_matched_pairs(match_matrix, lcs_lengths),
- [],
- )
+ result = traceback_longest_common_subsequence_matched_pairs(match_matrix, lcs_lengths)
+ assert result == []
def test_traceback_two_diagonal_matches_returns_correct_pairs():
match_matrix = [[True, False], [False, True]]
lcs_lengths = compute_longest_common_subsequence_lengths_table(match_matrix)
- assert_equal(
- traceback_longest_common_subsequence_matched_pairs(match_matrix, lcs_lengths),
- [(0, 0), (1, 1)],
- )
+ result = traceback_longest_common_subsequence_matched_pairs(
+ match_matrix, lcs_lengths)
+ assert result == [(0, 0), (1, 1)]
def test_traceback_skips_unmatched_old_node_to_reach_best_lcs():
@@ -103,64 +94,56 @@ def test_traceback_skips_unmatched_old_node_to_reach_best_lcs():
# old[0] does not match; old[1] does: result should be [(1, 0)], not []
match_matrix = [[False], [True]]
lcs_lengths = compute_longest_common_subsequence_lengths_table(match_matrix)
- assert_equal(
- traceback_longest_common_subsequence_matched_pairs(match_matrix, lcs_lengths),
- [(1, 0)],
- )
+ result = traceback_longest_common_subsequence_matched_pairs(
+ match_matrix, lcs_lengths)
+ assert result == [(1, 0)]
# --- group_consecutive_pairs_into_blocks ---
def test_group_no_pairs_produces_only_sentinel():
- assert_equal(
- group_consecutive_pairs_into_blocks([], 2, 3),
- [(2, 3, 0)],
- )
+ assert group_consecutive_pairs_into_blocks([], 2, 3) == [(2, 3, 0)]
def test_group_consecutive_pairs_merged_into_one_block():
- assert_equal(
- group_consecutive_pairs_into_blocks([(0, 0), (1, 1)], 2, 2),
- [(0, 0, 2), (2, 2, 0)],
- )
+ assert group_consecutive_pairs_into_blocks([(0, 0), (1, 1)], 2, 2) == [
+ (0, 0, 2), (2, 2, 0)]
def test_group_non_consecutive_pairs_become_separate_blocks():
- assert_equal(
- group_consecutive_pairs_into_blocks([(0, 0), (2, 2)], 3, 3),
- [(0, 0, 1), (2, 2, 1), (3, 3, 0)],
- )
+ assert group_consecutive_pairs_into_blocks([(0, 0), (2, 2)], 3, 3) == [
+ (0, 0, 1), (2, 2, 1), (3, 3, 0)]
# --- fuzzy_match_blocks ---
def test_fuzzy_match_blocks_empty_old():
- assert_equal(fuzzy_match_blocks([], get_dom_nodes('hello
')), [(0, 1, 0)])
+ assert fuzzy_match_blocks([], get_dom_nodes('hello
')) == [(0, 1, 0)]
def test_fuzzy_match_blocks_empty_new():
- assert_equal(fuzzy_match_blocks(get_dom_nodes('hello
'), []), [(1, 0, 0)])
+ assert fuzzy_match_blocks(get_dom_nodes('hello
'), []) == [(1, 0, 0)]
def test_fuzzy_match_blocks_similar_text_same_tag_matches():
old = get_dom_nodes('Hello world
')
new = get_dom_nodes('Hello earth
')
- assert_equal(fuzzy_match_blocks(old, new), [(0, 0, 1), (1, 1, 0)])
+ assert fuzzy_match_blocks(old, new) == [(0, 0, 1), (1, 1, 0)]
def test_fuzzy_match_blocks_different_tag_does_not_match():
old = get_dom_nodes('Hello world
')
new = get_dom_nodes('Hello world
')
- assert_equal(fuzzy_match_blocks(old, new), [(1, 1, 0)])
+ assert fuzzy_match_blocks(old, new) == [(1, 1, 0)]
def test_fuzzy_match_blocks_unmatched_node_excluded_from_blocks():
# old[0] and old[1] fuzzy-match new[0] and new[1]; old[2] has no match
old = get_dom_nodes('Hello world
Foo bar
Other
')
new = get_dom_nodes('Hello earth
Foo bar
')
- assert_equal(fuzzy_match_blocks(old, new), [(0, 0, 2), (3, 2, 0)])
+ assert fuzzy_match_blocks(old, new) == [(0, 0, 2), (3, 2, 0)]
# --- table-context restriction for fuzzy_match_blocks ---
diff --git a/htmltreediff/tests/test_html.py b/htmltreediff/tests/test_html.py
new file mode 100644
index 0000000..a2b2f05
--- /dev/null
+++ b/htmltreediff/tests/test_html.py
@@ -0,0 +1,843 @@
+from textwrap import dedent
+
+import pytest
+
+from htmltreediff.changes import distribute
+from htmltreediff.html import add_class_to_empty_del_tags, diff, fix_lists, fix_tables
+from htmltreediff.util import (
+ parse_minidom,
+ minidom_tostring,
+ remove_insignificant_text_nodes,
+ remove_xml_declaration,
+ get_location,
+)
+
+from htmltreediff.tests.util import assert_html_equal, collapse
+
+
+# Preprocessing
+
+preprocessing_cases = [
+ (
+ 'empty document',
+ '',
+ '',
+ 'one
tail',
+ ),
+ (
+ 'ignore comments',
+ '',
+ '',
+ '',
+ ),
+ (
+ 'ignore style tags',
+ '',
+ '',
+ 'xxxyyy
',
+ ),
+ (
+ 'ignore font tags',
+ '',
+ '',
+ '', # noqa E501
+ ),
+]
+
+
+@pytest.mark.parametrize(
+ 'description,old_html,target,target_raw',
+ preprocessing_cases,
+ ids=[case[0] for case in preprocessing_cases],
+)
+def test_preprocessing(description, old_html, target, target_raw):
+ dom = parse_minidom(old_html)
+ assert minidom_tostring(dom) == target
+ assert remove_xml_declaration(dom.toxml()) == target_raw
+
+
+def test_remove_insignificant_text_nodes():
+ html = dedent('''
+
+
+
+ one two three
+
+
+
+
+ ''')
+ target_html = (' one two three
'
+ '')
+
+ dom = parse_minidom(html)
+ remove_insignificant_text_nodes(dom)
+ html = minidom_tostring(dom)
+ assert html == target_html
+
+ # Check that it is idempotent.
+ dom = parse_minidom(html)
+ remove_insignificant_text_nodes(dom)
+ html = minidom_tostring(dom)
+ assert html == target_html
+
+
+def test_remove_insignificant_text_nodes_nbsp():
+ html = dedent('''
+
+ ''')
+ dom = parse_minidom(html)
+ remove_insignificant_text_nodes(dom)
+ html = minidom_tostring(dom)
+ assert html == (
+ ''
+ )
+
+
+# Post-processing
+
+def test_other_node_type_inserted():
+ changes = diff(
+ u'foo
',
+ u'foo bar
',
+ )
+ assert changes == 'foo bar
'
+
+
+def test_non_printing_characters():
+ changes = diff(
+ '',
+ '',
+ )
+ assert changes == ''
+
+
+def test_cutoff():
+ changes = diff(
+ 'totally
',
+ 'different
',
+ cutoff=0.2,
+ )
+ assert changes == (
+ 'The differences from the previous version are too large to show '
+ 'concisely.
')
+
+
+cases = [
+ (
+ 'Simple Addition',
+ 'one
',
+ 'one
two
',
+ dedent('''
+ one
+
+ two
+
+ ''').strip(),
+ ),
+]
+
+
+@pytest.mark.parametrize(
+ 'test_name,old_html,new_html,pretty_changes',
+ cases,
+ ids=[case[0] for case in cases],
+)
+def test_html_diff_pretty(test_name, old_html, new_html, pretty_changes):
+ changes = diff(old_html, new_html, cutoff=0.0, pretty=True)
+ assert pretty_changes == changes
+
+
+@pytest.mark.parametrize('original,distributed', [
+ ('AB',
+ 'AB'),
+])
+def test_distribute(original, distributed):
+ original = parse_minidom(original)
+ distributed = parse_minidom(distributed)
+ node = get_location(original, [0])
+ distribute(node)
+ assert_html_equal(
+ minidom_tostring(original),
+ minidom_tostring(distributed),
+ )
+
+
+def test_get_location():
+ html = 'AB'
+ original = parse_minidom(html)
+ try:
+ get_location(original, [10])
+ raise AssertionError('ValueError not raised')
+ except ValueError:
+ pass
+
+
+cases = [
+ (
+ 'simple list item insert',
+ '''
+
+ - one
+ - two
+
+ ''',
+ '''
+
+ - one
+ - two
+
+ '''
+ ),
+ (
+ 'multiple list item insert',
+ '''
+
+ - one
+
+ - two
+ - three
+
+
+ ''',
+ '''
+
+ - one
+ - two
+ - three
+
+ '''
+ ),
+ (
+ 'simple list item delete afterward',
+ '''
+
+ - one
+ - one and a half
+
+ ''',
+ '''
+
+ - one
+ one and a half
+
+ '''
+ ),
+ (
+ 'simple list item delete first',
+ '''
+
+ - one half
+ - one
+
+ ''',
+ '''
+
+ one half
+ - one
+
+ '''
+ ),
+ (
+ 'multiple list item delete first',
+ '''
+
+
+ - one third
+ - two thirds
+
+ - one
+
+ ''',
+ '''
+
+ one third
+ two thirds
+ - one
+
+ '''
+ ),
+ (
+ 'insert and delete separately',
+ '''
+
+ - one
+ - two
+ - three
+ - three point five
+ - four
+
+ ''',
+ '''
+
+ - one
+ - two
+ - three
+ three point five
+ - four
+
+ '''
+ ),
+ (
+ 'multiple list item delete',
+ '''
+
+ - one
+
+ - two
+ - three
+
+
+ ''',
+ '''
+
+ - one
+ two
+ three
+
+ '''
+ ),
+ (
+ 'delete only list item',
+ '''
+
+
+ - one
+
+
+ ''',
+ '''
+
+ one
+
+ '''
+ ),
+ (
+ 'LI full content change does not add another LI',
+ '''
+
+
+ - AAA
+
+
+ - BBB
+
+
+ ''',
+ '''
+
+ AAABBB
+
+ '''
+ ),
+ (
+ 'LI full content change keeps attrs',
+ '''
+
+
+ - AAA
+
+
+ - BBB
+
+
+ ''',
+ '''
+
+ AAABBB
+
+ '''
+ ),
+ (
+ 'LI changes markup internalization fix not done if next tag is not an insert',
+ '''
+
+
+ - AAA
+
+ - BBB
+
+ - CCC
+
+
+ ''',
+ '''
+
+ -
+
AAA
+
+ - BBB
+ - CCC
+
+ ''',
+ ),
+ (
+ 'LI after del must be ins',
+ '''
+
+
+ - AAA
+
+
+ - BBB
+
+
+ - CCC
+
+
+ ''',
+ '''
+
+ -
+
AAA
+
+ BBBCCC
+
+ ''',
+ ),
+ (
+ 'LI changes markup internalization fix not performed if next tags child is not li', # noqa E501
+ '''
+
+
+ - AAA
+
+
+ BBB
+
+
+ ''',
+ '''
+
+ -
+
AAA
+
+
+ BBB
+
+
+ ''',
+ ),
+ (
+ 'LI changes markup internalization fix not performed if next tags is text',
+ '''
+
+
+ - AAA
+
+
+ BBB
+
+
+ ''',
+ '''
+
+ -
+
AAA
+
+
+ BBB
+
+
+ ''',
+ ),
+]
+
+
+@pytest.mark.parametrize(
+ 'test_name,changes,fixed_changes',
+ cases,
+ ids=[case[0] for case in cases],
+)
+def test_fix_lists(test_name, changes, fixed_changes):
+ changes = collapse(changes)
+ fixed_changes = collapse(fixed_changes)
+ changes_dom = parse_minidom(changes)
+ fix_lists(changes_dom)
+ assert_html_equal(minidom_tostring(changes_dom), fixed_changes)
+
+
+cases = [
+ (
+ 'add a table row',
+ '''
+
+ ''',
+ '''
+
+ '''
+ ),
+ (
+ 'tbody inside ins is distributed',
+ '''
+
+ ''',
+ '''
+
+ '''
+ ),
+ (
+ 'tbody inside del is distributed',
+ '''
+
+ ''',
+ '''
+
+ '''
+ ),
+ (
+ 'thead inside ins is distributed',
+ '''
+
+ ''',
+ '''
+
+ '''
+ ),
+ (
+ 'thead inside del is distributed',
+ '''
+
+ ''',
+ '''
+
+ '''
+ ),
+ (
+ 'tfoot inside ins is distributed',
+ '''
+
+ ''',
+ '''
+
+ '''
+ ),
+ (
+ 'tfoot inside del is distributed',
+ '''
+
+ ''',
+ '''
+
+ '''
+ ),
+ (
+ 'tbody del and ins pair is internalized',
+ '''
+
+ ''',
+ '''
+
+ '''
+ ),
+ (
+ 'thead del and ins pair is internalized',
+ '''
+
+ | old header |
|---|
+ | new header |
+ | data |
+
+ ''',
+ '''
+
+ old headernew header |
+ | data |
+
+ '''
+ ),
+ (
+ 'tfoot del and ins pair is internalized',
+ '''
+
+ | data |
+ | old footer |
+ | new footer |
+
+ ''',
+ '''
+
+ | data |
+ old footernew footer |
+
+ '''
+ ),
+ (
+ 'tr del and ins pair is internalized',
+ '''
+
+
+ | old row |
+ | new row |
+
+
+ ''',
+ '''
+
+ '''
+ ),
+ (
+ 'remove ins and del tags at the wrong level of the table',
+ '''
+
+
+
+
+
+
+
+
+
+
+
+
+ A |
+
+
+
+ ''',
+ '''
+
+ ''',
+ ),
+]
+
+
+@pytest.mark.parametrize(
+ 'test_name,changes,fixed_changes',
+ cases,
+ ids=[case[0] for case in cases],
+)
+def test_fix_tables(test_name, changes, fixed_changes):
+ changes = collapse(changes)
+ fixed_changes = collapse(fixed_changes)
+ changes_dom = parse_minidom(changes, strict_xml=True)
+ fix_tables(changes_dom)
+ assert_html_equal(minidom_tostring(changes_dom), fixed_changes)
+
+
+def test_diff_focused_on_changed_cells_when_colgroup_added():
+ old_html = (
+ ''
+ '| Alpha | unchanged |
'
+ '| Beta | old value |
'
+ '
'
+ )
+ new_html = (
+ ''
+ '| Alpha | unchanged |
'
+ '| Beta | new value |
'
+ '
'
+ )
+ expected = (
+ ''
+ '| Alpha | unchanged |
'
+ '| Beta | oldnew value |
'
+ '
'
+ )
+ assert diff(old_html, new_html) == expected
+
+
+def test_similar_rows_not_misaligned_with_colgroup():
+ """
+ When a colgroup is added and rows share boilerplate text, pairwise
+ fuzzy matching should align each old row to its positional counterpart
+ rather than misaligning due to non-transitive text similarity.
+
+ The text lengths here are tuned to trigger SequenceMatcher misalignment
+ in the old code path; do not shorten them.
+ """
+ old_html = (
+ ''
+ '| Alpha | shared setup text | Rate: check | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
+ '| Beta | shared setup text | Rate: check | Rinse. |
'
+ '| Gamma | shared setup text | Rate: check | Rinse. |
'
+ '
'
+ )
+ new_html = (
+ ''
+ '| Alpha | shared setup text | Rate: changed1 | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
+ '| Beta | shared setup text | Rate: changed2 | Rinse. |
' # noqa E501
+ '| Gamma | shared setup text | Rate: changed3 | Rinse. |
' # noqa E501
+ '
'
+ )
+ expected = (
+ ''
+ '| Alpha | shared setup text | Rate: checkchanged1 | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
+ '| Beta | shared setup text | Rate: checkchanged2 | Rinse. |
' # noqa E501
+ '| Gamma | shared setup text | Rate: checkchanged3 | Rinse. |
' # noqa E501
+ '
'
+ )
+ assert diff(old_html, new_html) == expected
+
+
+def test_similar_rows_not_misaligned_without_colgroup():
+ """
+ Same non-transitive fuzzy equality problem as above, but triggered
+ without a colgroup -- every row has a small change so no exact matches
+ exist and all rows go through fuzzy matching.
+
+ The text lengths here are tuned to trigger SequenceMatcher misalignment
+ in the old code path; do not shorten them.
+ """
+ old_html = (
+ ''
+ '| Alpha | shared setup text | Rate: check | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
+ '| Beta | shared setup text | Rate: check | Rinse. |
'
+ '| Gamma | shared setup text | Rate: check | Rinse. |
'
+ '
'
+ )
+ new_html = (
+ ''
+ '| Alpha | shared setup text | Rate: changed1 | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
+ '| Beta | shared setup text | Rate: changed2 | Rinse. |
' # noqa E501
+ '| Gamma | shared setup text | Rate: changed3 | Rinse. |
' # noqa E501
+ '
'
+ )
+ expected = (
+ ''
+ '| Alpha | shared setup text | Rate: checkchanged1 | Long notes requiring careful monitoring and administration throughout procedure. |
' # noqa E501
+ '| Beta | shared setup text | Rate: checkchanged2 | Rinse. |
' # noqa E501
+ '| Gamma | shared setup text | Rate: checkchanged3 | Rinse. |
' # noqa E501
+ '
'
+ )
+ assert diff(old_html, new_html) == expected
+
+
+cases = [
+ (
+ 'empty del tag',
+ '',
+ '',
+ ),
+ (
+ 'del tag with space',
+ ' ',
+ ' ',
+ ),
+ (
+ 'del tag with child',
+ '
',
+ '
',
+ ),
+ (
+ 'del tag with spaces and characters',
+ ' abc ',
+ ' abc ',
+ ),
+
+]
+
+
+@pytest.mark.parametrize(
+ 'test_name,test_input,expected_result',
+ cases,
+ ids=[case[0] for case in cases],
+)
+def test_add_class_to_empty_del_tags(test_name, test_input, expected_result):
+ dom = parse_minidom(test_input, strict_xml=True)
+ add_class_to_empty_del_tags(dom)
+ assert_html_equal(minidom_tostring(dom), expected_result)
diff --git a/htmltreediff/tests/test_test_util.py b/htmltreediff/tests/test_test_util.py
new file mode 100644
index 0000000..7f6cd85
--- /dev/null
+++ b/htmltreediff/tests/test_test_util.py
@@ -0,0 +1,40 @@
+from unittest.mock import patch
+
+from htmltreediff.text import WordMatcher
+from htmltreediff.util import (
+ check_text_similarity,
+ node_compare,
+ parse_minidom,
+ walk_dom,
+)
+
+
+def test_node_compare():
+ del_node = list(walk_dom(parse_minidom('')))[-1]
+ ins_node = list(walk_dom(parse_minidom('')))[-1]
+ assert -1 == node_compare(del_node, ins_node)
+ assert 1 == node_compare(ins_node, del_node)
+
+
+def _uses_autojunk(html):
+ dom = parse_minidom(html)
+ node = dom.documentElement.firstChild
+ captured = []
+ original_init = WordMatcher.__init__
+
+ def spy_init(self, **kwargs):
+ captured.append(kwargs.get('autojunk', True))
+ original_init(self, **kwargs)
+
+ with patch.object(WordMatcher, '__init__', spy_init):
+ check_text_similarity(node, node, cutoff=0.4)
+
+ return any(captured)
+
+
+def test_check_text_similarity_autojunk_disabled_for_table_element():
+ assert _uses_autojunk('| Some cell text |
') is False
+
+
+def test_check_text_similarity_autojunk_enabled_for_non_table_element():
+ assert _uses_autojunk('Some text here
') is True
diff --git a/htmltreediff/tests/test_text.py b/htmltreediff/tests/test_text.py
new file mode 100644
index 0000000..98ec83a
--- /dev/null
+++ b/htmltreediff/tests/test_text.py
@@ -0,0 +1,95 @@
+import pytest
+
+from htmltreediff.html import diff
+from htmltreediff.text import split_text
+
+
+cases = [
+ ('word', ['word']),
+ ('two words', ['two', ' ', 'words']),
+ ('abcdef12', ['abcdef', '12']),
+ ('entity"s', ['entity', '"', 's']),
+ ('stuff stuff', ['stuff', ' ', 'stuff']),
+ (
+ 'Stuff with an ampersand A&B stuff. Stuff with a semicolon; more stuff.', # noqa
+ [
+ 'Stuff', ' ', 'with', ' ', 'an', ' ', 'ampersand', ' ', 'A',
+ '&', 'B', ' ', 'stuff', '.', ' ', 'Stuff', ' ', 'with', ' ',
+ 'a', ' ', 'semicolon', ';', ' ', 'more', ' ', 'stuff', '.',
+ ],
+ ),
+ ("we're excited", ["we're", " ", "excited"]),
+ ('dial 1-800-555-1234', ['dial', ' ', '1-800-555-1234']),
+ ('Effective 1/2/2003', ['Effective', ' ', '1/2/2003']),
+ (u'über français', [u'über', u' ', u'français']),
+ (u'em dash \u2013 \u2013', [u'em', u' ', u'dash', u' ', u'\u2013', u' ', u'\u2013']),
+]
+
+
+@pytest.mark.parametrize('text,target', cases)
+def test_text_split(text, target):
+ assert split_text(text) == target
+
+
+cases = [
+ (
+ 'sub-word changes',
+ 'The quick brown fox jumps over the lazy dog.',
+ 'The very quick brown foxes jump over the dog.',
+ 'The very quick brown fox jumpsfoxes jump over the lazy dog.', # noqa
+ ),
+ (
+ 'special characters',
+ 'Assume that A < B, and A & B = {}',
+ 'If we assume that A < B, and A & B = {}',
+ 'AssumeIf we assume that A < B, and A & B = {}', # noqa
+ ),
+ (
+ 'contractions',
+ "we were excited",
+ "we're excited",
+ "we werewe're excited",
+ ),
+ (
+ 'dates',
+ 'Effective 1/2/2003',
+ 'Effective 3/4/2005',
+ 'Effective 1/2/20033/4/2005',
+ ),
+ (
+ 'text diff with <',
+ 'x',
+ '<',
+ 'x<',
+ ),
+ (
+ 'text diff with >',
+ 'x',
+ '>',
+ 'x>',
+ ),
+ (
+ 'text diff with &',
+ 'x',
+ '&',
+ 'x&',
+ ),
+ (
+ 'do not remove newlines unless necessary',
+ 'one two three\nfour six',
+ 'one three\nfour five six',
+ 'one two three\nfour five six',
+ ),
+ # long text diff is broken
+ # (
+ # 'long text diff',
+ # open('htmltreediff/fixtures/long_diff/before.txt').read(),
+ # open('htmltreediff/fixtures/long_diff/after.txt').read(),
+ # open('htmltreediff/fixtures/long_diff/diff.html').read(),
+ # ),
+]
+
+
+@pytest.mark.parametrize('description,old,new,changes', cases, ids=[c[0] for c in cases])
+def test_text_diff(description, old, new, changes):
+ assert diff(old, new, plaintext=True) == changes
diff --git a/htmltreediff/test_xml.py b/htmltreediff/tests/test_xml.py
similarity index 94%
rename from htmltreediff/test_xml.py
rename to htmltreediff/tests/test_xml.py
index 7fa2ac4..2df11fb 100644
--- a/htmltreediff/test_xml.py
+++ b/htmltreediff/tests/test_xml.py
@@ -1,8 +1,7 @@
-from nose.tools import assert_equal
-
-from htmltreediff.test_util import collapse
-from htmltreediff.util import parse_minidom, minidom_tostring
from htmltreediff.changes import dom_diff
+from htmltreediff.util import parse_minidom, minidom_tostring
+
+from htmltreediff.tests.util import collapse
test_cases = [
(
@@ -108,4 +107,4 @@ def test_xml_diff():
old_dom = parse_minidom(old_html, strict_xml=True)
new_dom = parse_minidom(new_html, strict_xml=True)
changes_xml = minidom_tostring(dom_diff(old_dom, new_dom))
- assert_equal(changes_xml, target)
+ assert changes_xml == target
diff --git a/htmltreediff/test_util.py b/htmltreediff/tests/util.py
similarity index 75%
rename from htmltreediff/test_util.py
rename to htmltreediff/tests/util.py
index c25c012..a553ef2 100644
--- a/htmltreediff/test_util.py
+++ b/htmltreediff/tests/util.py
@@ -1,17 +1,13 @@
-from unittest.mock import patch
-
from htmltreediff.diff_core import Differ
from htmltreediff.edit_script_runner import EditScriptRunner
from htmltreediff.changes import (
split_text_nodes,
sort_nodes,
)
-from htmltreediff.text import WordMatcher
from htmltreediff.util import (
attribute_dict,
- check_text_similarity,
+ html_equal,
minidom_tostring,
- node_compare,
parse_minidom,
remove_node,
unwrap,
@@ -19,6 +15,23 @@
)
+def assert_html_equal(a_html, b_html):
+ assert html_equal(a_html, b_html), (
+ f'These html documents are not equal:\n{a_html!r}\n====\n{b_html!r}'
+ )
+
+
+def assert_html_not_equal(a_html, b_html):
+ assert not html_equal(a_html, b_html), (
+ f'These html documents should not be equal:\n{a_html!r}\n====\n{b_html!r}'
+ )
+
+
+def assert_strip_changes(old_html, new_html, changes):
+ assert_html_equal(old_html, strip_changes_old(changes))
+ assert_html_equal(new_html, strip_changes_new(changes))
+
+
def reverse_edit_script(edit_script):
if edit_script is None:
return None
@@ -130,34 +143,3 @@ def parse_cases(cases):
else:
raise ValueError('Invalid test spec: %r' % (args,))
yield case
-
-
-def test_node_compare():
- del_node = list(walk_dom(parse_minidom('')))[-1]
- ins_node = list(walk_dom(parse_minidom('')))[-1]
- assert -1 == node_compare(del_node, ins_node)
- assert 1 == node_compare(ins_node, del_node)
-
-
-def _uses_autojunk(html):
- dom = parse_minidom(html)
- node = dom.documentElement.firstChild
- captured = []
- original_init = WordMatcher.__init__
-
- def spy_init(self, **kwargs):
- captured.append(kwargs.get('autojunk', True))
- original_init(self, **kwargs)
-
- with patch.object(WordMatcher, '__init__', spy_init):
- check_text_similarity(node, node, cutoff=0.4)
-
- return any(captured)
-
-
-def test_check_text_similarity_autojunk_disabled_for_table_element():
- assert _uses_autojunk('| Some cell text |
') is False
-
-
-def test_check_text_similarity_autojunk_enabled_for_non_table_element():
- assert _uses_autojunk('Some text here
') is True
diff --git a/htmltreediff/text.py b/htmltreediff/text.py
index 3d2d15c..983e9ad 100644
--- a/htmltreediff/text.py
+++ b/htmltreediff/text.py
@@ -1,7 +1,6 @@
import re
import string
-import six
from difflib import SequenceMatcher, _calculate_ratio
@@ -29,7 +28,7 @@ def full_split(text, regex):
def multi_split(text, regexes):
- """
+ r"""
Split the text by the given regexes, in priority order.
Make sure that the regex is parenthesized so that matches are returned in
@@ -48,7 +47,7 @@ def multi_split(text, regexes):
'one234five| |678'
"""
def make_regex(s):
- return re.compile(s) if isinstance(s, six.string_types) else s
+ return re.compile(s) if isinstance(s, str) else s
regexes = [make_regex(r) for r in regexes]
# Run the list of pieces through the regex split, splitting it into more
diff --git a/htmltreediff/util.py b/htmltreediff/util.py
index a0cc0f3..e128e37 100644
--- a/htmltreediff/util.py
+++ b/htmltreediff/util.py
@@ -2,8 +2,6 @@
from textwrap import dedent
from xml.dom import minidom, Node
-import six
-
from htmltreediff.text import WordMatcher, split_text
# DOM utilities ##
@@ -105,7 +103,7 @@ def remove_non_printing_characters(xml, replace_char=' '):
non_printing_chars = range(32)
replace_chars = len(non_printing_chars) * replace_char
translation_map = dict(zip(non_printing_chars, replace_chars))
- return six.text_type(xml).translate(translation_map)
+ return str(xml).translate(translation_map)
def remove_newlines(xml):
diff --git a/pylint.sh b/pylint.sh
deleted file mode 100755
index 398e8c2..0000000
--- a/pylint.sh
+++ /dev/null
@@ -1,7 +0,0 @@
-#! /bin/sh
-
-files=$@
-if [ -z "$files" ]; then
- files="htmltreediff"
-fi
-pylint --rcfile $(dirname $0)/pylintrc --reports=n $files
diff --git a/pylintrc b/pylintrc
deleted file mode 100644
index 80cf952..0000000
--- a/pylintrc
+++ /dev/null
@@ -1,269 +0,0 @@
-# lint Python modules using external checkers.
-#
-# This is the main checker controling the other ones and the reports
-# generation. It is itself both a raw checker and an astng checker in order
-# to:
-# * handle message activation / deactivation at the module level
-# * handle some basic but necessary stats'data (number of classes, methods...)
-#
-[MASTER]
-
-# Add to the black list. It should be a base name, not a
-# path. You may set this option multiple times.
-## Ignore migrations because they are auto-generated.
-ignore=.svn,migrations
-
-# Pickle collected data for later comparisons.
-persistent=yes
-
-# List of plugins (as comma separated values of python modules names) to load,
-# usually to register additional checkers.
-load-plugins=
-
-
-[MESSAGES CONTROL]
-
-# Enable the message(s) with the given id(s).
-#enable=W0611
-
-# Disable the message(s) with the given id(s).
-disable=C0111,W0142,R0904,E1101,W0613,W0221,W0232,W0212
-#,C0323,C0301,C0103,E0213,C0302,C0203,W0703
-
-
-[REPORTS]
-
-# set the output format. Available formats are text, parseable, colorized and
-# html
-output-format=colorized
-
-# Include message's id in output
-include-ids=yes
-
-# Put messages in a separate file for each module / package specified on the
-# command line instead of printing them on stdout. Reports (if any) will be
-# written in a file name "pylint_global.[txt|html]".
-files-output=no
-
-# Tells wether to display a full report or only the messages
-reports=yes
-
-# Python expression which should return a note less than 10 (10 is the highest
-# note).You have access to the variables errors warning, statement which
-# respectivly contain the number of errors / warnings messages and the total
-# number of statements analyzed. This is used by the global evaluation report
-# (R0004).
-evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
-
-# Add a comment according to your evaluation note. This is used by the global
-# evaluation report (R0004).
-comment=no
-
-# checks for
-# * unused variables / imports
-# * undefined variables
-# * redefinition of variable from builtins or from an outer scope
-# * use of variable before assigment
-#
-[VARIABLES]
-
-# Tells wether we should check for unused import in __init__ files.
-init-import=no
-
-# A regular expression matching names used for dummy variables (i.e. not used).
-## Any variable name ending in an underscore, or a single underscore, is a dummy.
-dummy-variables-rgx=[a-z_][a-z0-9_]{0,30}_|_$
-
-# List of additional names supposed to be defined in builtins. Remember that
-# you should avoid to define new builtins when possible.
-additional-builtins=
-
-
-# try to find bugs in the code using type inference
-#
-[TYPECHECK]
-
-# Tells wether missing members accessed in mixin class should be ignored. A
-# mixin class is detected if its name ends with "mixin" (case insensitive).
-ignore-mixin-members=yes
-
-# When zope mode is activated, consider the acquired-members option to ignore
-# access to some undefined attributes.
-zope=yes
-
-generated-members=objects,is_valid,cleaned_data,media,DoesNotExist
-
-# checks for :
-# * doc strings
-# * modules / classes / functions / methods / arguments / variables name
-# * number of arguments, local variables, branchs, returns and statements in
-# functions, methods
-# * required module attributes
-# * dangerous default values as arguments
-# * redefinition of function / method / class
-# * uses of the global statement
-#
-[BASIC]
-
-# Required attributes for module, separated by a comma
-required-attributes=
-
-# Regular expression which should only match functions or classes name which do
-# not require a docstring
-no-docstring-rgx=__.*__
-
-# Regular expression which should only match correct module names
-module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
-
-# Regular expression which should only match correct module level names
-const-rgx=(([a-zA-Z_][a-zA-Z1-9_]*)|(__.*__))$
-
-# Regular expression which should only match correct class names
-class-rgx=[A-Z_][a-zA-Z0-9]+$
-
-# Regular expression which should only match correct function names
-function-rgx=[a-z_][a-z0-9_]{2,35}$
-
-# Regular expression which should only match correct method names
-# Limit of 30 characters unless they're test_ methods
-method-rgx=([a-z_][a-z0-9_]{2,30}$|(test_[a-z0-9_]+$))
-
-# Regular expression which should only match correct instance attribute names
-## At least 2 characters long.
-attr-rgx=[a-z_][a-z0-9_]{1,30}$
-
-# Regular expression which should only match correct argument names
-## At least 2 characters long.
-argument-rgx=[a-z_][a-z0-9_]{1,30}$
-
-# Regular expression which should only match correct variable names
-## At least 1 character long.
-variable-rgx=[a-z_][a-z0-9_]{0,30}$
-
-# Regular expression which should only match correct list comprehension /
-# generator expression variable names
-## Lowercase only, minimum length 1
-inlinevar-rgx=[a-z_][a-z0-9_]*$
-
-# Good variable names which should always be accepted, separated by a comma
-good-names=i,j,k,_
-
-# Bad variable names which should always be refused, separated by a comma
-bad-names=foo,bar,baz,toto,tutu,tata
-
-# List of builtins function names that should not be used, separated by a comma
-bad-functions=apply,input
-
-
-# checks for sign of poor/misdesign:
-# * number of methods, attributes, local variables...
-# * size, complexity of functions, methods
-#
-[DESIGN]
-
-# Maximum number of arguments for function / method
-max-args=12
-
-# Maximum number of locals for function / method body
-max-locals=30
-
-# Maximum number of return / yield for function / method body
-max-returns=12
-
-# Maximum number of branch for function / method body
-max-branchs=30
-
-# Maximum number of statements in function / method body
-max-statements=60
-
-# Maximum number of parents for a class (see R0901).
-max-parents=7
-
-# Maximum number of attributes for a class (see R0902).
-max-attributes=20
-
-# Minimum number of public methods for a class (see R0903).
-min-public-methods=0
-
-# Maximum number of public methods for a class (see R0904).
-max-public-methods=20
-
-[IMPORTS]
-
-# Deprecated modules which should not be used, separated by a comma
-deprecated-modules=regsub,TERMIOS,Bastion,rexec
-
-# Create a graph of every (i.e. internal and external) dependencies in the
-# given file (report R0402 must not be disabled)
-import-graph=
-
-# Create a graph of external dependencies in the given file (report R0402 must
-# not be disabled)
-ext-import-graph=
-
-# Create a graph of internal dependencies in the given file (report R0402 must
-# not be disabled)
-int-import-graph=
-
-
-# checks for :
-# * methods without self as first argument
-# * overridden methods signature
-# * access only to existant members via self
-# * attributes not defined in the __init__ method
-# * supported interfaces implementation
-# * unreachable code
-#
-[CLASSES]
-
-# List of interface methods to ignore, separated by a comma. This is used for
-# instance to not check methods defines in Zope's Interface base class.
-ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
-
-# List of method names used to declare (i.e. assign) instance attributes.
-defining-attr-methods=__init__,__new__,setUp
-
-
-# checks for similarities and duplicated code. This computation may be
-# memory / CPU intensive, so you should disable it if you experiments some
-# problems.
-#
-[SIMILARITIES]
-
-# Minimum lines number of a similarity.
-min-similarity-lines=10
-
-# Ignore comments when computing similarities.
-ignore-comments=yes
-
-# Ignore docstrings when computing similarities.
-ignore-docstrings=yes
-
-
-# checks for:
-# * warning notes in the code like FIXME, XXX
-# * PEP 263: source code with non ascii character but no encoding declaration
-#
-[MISCELLANEOUS]
-
-# List of note tags to take in consideration, separated by a comma.
-notes=FIXME,XXX,TODO
-
-
-# checks for :
-# * unauthorized constructions
-# * strict indentation
-# * line length
-# * use of <> instead of !=
-#
-[FORMAT]
-
-# Maximum number of characters on a single line.
-max-line-length=90
-
-# Maximum number of lines in a module
-max-module-lines=1000
-
-# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
-# tab).
-indent-string=' '
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..c640904
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,5 @@
+[tool.pytest.ini_options]
+# Previously, coverage was at 100 but that is failing, so it's set to 99 for
+# now. We can increase it back to 100 once we have more tests.
+# TODO (GH#26): Increase coverage back to 100
+addopts = "--doctest-modules --cov=htmltreediff --cov-report=term-missing --cov-fail-under=99"
diff --git a/requirements/default.txt b/requirements/default.txt
index d6c2d19..bba8053 100644
--- a/requirements/default.txt
+++ b/requirements/default.txt
@@ -1,3 +1 @@
-html5lib==0.90
-lxml==4.2.5
-six==1.15.0
+lxml>=4.2.5
diff --git a/requirements/testing.txt b/requirements/testing.txt
index abb1803..9955dec 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -1,3 +1,2 @@
-coverage
-nose
-flake8
+pytest
+pytest-cov
diff --git a/run_tests.sh b/run_tests.sh
deleted file mode 100755
index f48ae3d..0000000
--- a/run_tests.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#! /bin/sh
-
-nosetests --verbose --with-doctest --with-coverage --cover-erase --cover-min-percentage=100 --cover-package htmltreediff $@ && find htmltreediff/ -name '*.py' | xargs flake8
diff --git a/setup.py b/setup.py
index ba9d89c..d279727 100644
--- a/setup.py
+++ b/setup.py
@@ -1,17 +1,11 @@
#! /usr/bin/env python
-# coding: utf-8
import codecs
import os
-try:
- from setuptools import setup, find_packages
-except ImportError:
- from ez_setup import use_setuptools
- use_setuptools()
- from setuptools import setup, find_packages
+from setuptools import setup, find_packages
-long_description = codecs.open("README.rst", "r", "utf-8").read()
+long_description = codecs.open("README.md", "r", "utf-8").read()
def strip_comments(line):
@@ -38,6 +32,7 @@ def get_requirements(path):
scripts=[],
zip_safe=False,
install_requires=list(get_requirements('requirements/default.txt')),
+ python_requires=">=3.8",
tests_require=list(get_requirements('requirements/testing.txt')),
cmdclass={},
classifiers=[
@@ -46,6 +41,12 @@ def get_requirements(path):
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
"Topic :: Text Processing :: Markup :: HTML",
"Topic :: Text Processing :: Markup :: XML",
],
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 62a210c..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,27 +0,0 @@
-# Tox (http://tox.testrun.org/) is a tool for running tests
-# in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it, "pip install tox"
-# and then run "tox" from this directory.
-
-[tox]
-envlist = py37pep8, py37, py37-coverage
-
-[testenv]
-commands =
- nosetests --with-doctest []
-deps =
- -r{toxinidir}/requirements/testing.txt
-
-# Coverage for python 3.7
-[testenv:py37-coverage]
-commands =
- nosetests --with-doctest --with-coverage --cover-package htmltreediff []
-
-[testenv:py37pep8]
-basepython = python3.7
-deps = flake8
-commands = flake8 htmltreediff
-
-[flake8]
-select = E,W,F
-max-line-length = 95
diff --git a/vendor/lxml-4.2.5-cp38-cp38-linux_x86_64.whl b/vendor/lxml-4.2.5-cp38-cp38-linux_x86_64.whl
new file mode 100644
index 0000000..f9d61b0
Binary files /dev/null and b/vendor/lxml-4.2.5-cp38-cp38-linux_x86_64.whl differ