python-redlines 0.2.1__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_redlines-0.2.1 → python_redlines-1.0.0}/.gitignore +3 -0
- {python_redlines-0.2.1 → python_redlines-1.0.0}/PKG-INFO +10 -5
- {python_redlines-0.2.1 → python_redlines-1.0.0}/README.md +3 -0
- {python_redlines-0.2.1 → python_redlines-1.0.0}/pyproject.toml +21 -3
- {python_redlines-0.2.1 → python_redlines-1.0.0}/src/python_redlines/__about__.py +1 -1
- {python_redlines-0.2.1 → python_redlines-1.0.0}/src/python_redlines/engines.py +86 -19
- {python_redlines-0.2.1 → python_redlines-1.0.0}/src/python_redlines/__init__.py +0 -0
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: python-redlines
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Open-source DOCX comparison tool to generate native Word tracked changes (redlines) in Python without MS Word dependencies.
|
|
5
5
|
Project-URL: Homepage, https://github.com/JSv4/Python-Redlines
|
|
6
|
+
Project-URL: Documentation, https://jsv4.github.io/Python-Redlines/
|
|
7
|
+
Project-URL: Demo, https://redlines.opensource.legal
|
|
6
8
|
Project-URL: Issues, https://github.com/JSv4/Python-Redlines/issues
|
|
7
9
|
Project-URL: Source, https://github.com/JSv4/Python-Redlines
|
|
8
10
|
Author: John Scrudato IV
|
|
9
11
|
License-Expression: MIT
|
|
10
|
-
Keywords: diff,docx,openxml,redline,tracked-changes,word
|
|
11
|
-
Classifier: Development Status ::
|
|
12
|
+
Keywords: compare-word-documents,diff,docx,docx-comparison,docxodus,legal-tech,openxml,python-docx-diff,redline,track-changes-api,tracked-changes,word
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
15
|
Classifier: Programming Language :: Python
|
|
14
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -57,6 +59,9 @@ redline_bytes, stdout, stderr = engine.run_redline(
|
|
|
57
59
|
)
|
|
58
60
|
```
|
|
59
61
|
|
|
62
|
+
`DocxodusEngine` accepts `engine="wmlcomparer"` (default) or `engine="docxdiff"` to select the
|
|
63
|
+
comparison algorithm. See the [project README](https://github.com/JSv4/Python-Redlines#choosing-an-engine).
|
|
64
|
+
|
|
60
65
|
If an engine's companion package is not installed, instantiating the engine
|
|
61
66
|
raises `EngineNotInstalledError` with the `pip install` command to fix it.
|
|
62
67
|
|
|
@@ -28,6 +28,9 @@ redline_bytes, stdout, stderr = engine.run_redline(
|
|
|
28
28
|
)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
`DocxodusEngine` accepts `engine="wmlcomparer"` (default) or `engine="docxdiff"` to select the
|
|
32
|
+
comparison algorithm. See the [project README](https://github.com/JSv4/Python-Redlines#choosing-an-engine).
|
|
33
|
+
|
|
31
34
|
If an engine's companion package is not installed, instantiating the engine
|
|
32
35
|
raises `EngineNotInstalledError` with the `pip install` command to fix it.
|
|
33
36
|
|
|
@@ -5,16 +5,29 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-redlines"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "
|
|
8
|
+
description = "Open-source DOCX comparison tool to generate native Word tracked changes (redlines) in Python without MS Word dependencies."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = "MIT"
|
|
12
|
-
keywords = [
|
|
12
|
+
keywords = [
|
|
13
|
+
"docx",
|
|
14
|
+
"redline",
|
|
15
|
+
"diff",
|
|
16
|
+
"tracked-changes",
|
|
17
|
+
"openxml",
|
|
18
|
+
"word",
|
|
19
|
+
"docx-comparison",
|
|
20
|
+
"compare-word-documents",
|
|
21
|
+
"track-changes-api",
|
|
22
|
+
"python-docx-diff",
|
|
23
|
+
"legal-tech",
|
|
24
|
+
"docxodus",
|
|
25
|
+
]
|
|
13
26
|
authors = [
|
|
14
27
|
{ name = "John Scrudato IV" },
|
|
15
28
|
]
|
|
16
29
|
classifiers = [
|
|
17
|
-
"Development Status ::
|
|
30
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
31
|
"License :: OSI Approved :: MIT License",
|
|
19
32
|
"Programming Language :: Python",
|
|
20
33
|
"Programming Language :: Python :: 3.9",
|
|
@@ -31,12 +44,17 @@ dependencies = [
|
|
|
31
44
|
# its extra, e.g. `pip install python-redlines[docxodus]`. All three packages
|
|
32
45
|
# are released together from the same repository on each tagged release.
|
|
33
46
|
[project.optional-dependencies]
|
|
47
|
+
# Deprecated: wraps the archived Open-XML-PowerTools WmlComparer. Kept so that
|
|
48
|
+
# anyone who needs that algorithm's output has somewhere to stand; `all` keeps
|
|
49
|
+
# installing it so the extra does not change meaning under existing pins.
|
|
34
50
|
ooxmlpowertools = ["python-redlines-ooxmlpowertools"]
|
|
35
51
|
docxodus = ["python-redlines-docxodus"]
|
|
36
52
|
all = ["python-redlines-ooxmlpowertools", "python-redlines-docxodus"]
|
|
37
53
|
|
|
38
54
|
[project.urls]
|
|
39
55
|
Homepage = "https://github.com/JSv4/Python-Redlines"
|
|
56
|
+
Documentation = "https://jsv4.github.io/Python-Redlines/"
|
|
57
|
+
Demo = "https://redlines.opensource.legal"
|
|
40
58
|
Issues = "https://github.com/JSv4/Python-Redlines/issues"
|
|
41
59
|
Source = "https://github.com/JSv4/Python-Redlines"
|
|
42
60
|
|
|
@@ -6,6 +6,7 @@ import platform
|
|
|
6
6
|
import subprocess
|
|
7
7
|
import tarfile
|
|
8
8
|
import tempfile
|
|
9
|
+
import warnings
|
|
9
10
|
import zipfile
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Optional, Tuple, Union
|
|
@@ -138,18 +139,26 @@ class BaseEngine(object):
|
|
|
138
139
|
Runs the redline binary. The 'original' and 'modified' arguments can be either bytes or file paths
|
|
139
140
|
(as ``str`` or ``pathlib.Path``). Returns the redline output as bytes.
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
A path the caller supplies is never deleted; only scratch files this
|
|
143
|
+
method creates are cleaned up.
|
|
144
|
+
|
|
145
|
+
Additional keyword arguments are passed to _build_command() for engine-specific
|
|
146
|
+
options. DocxodusEngine supports: case_insensitive, detect_moves,
|
|
147
|
+
move_similarity_threshold, move_minimum_word_count, detect_format_changes,
|
|
148
|
+
conflate_spaces, date_time. It raises ValueError for unrecognised settings and
|
|
149
|
+
for the WmlComparer-era settings removed in Docxodus v11.0.0 (engine,
|
|
150
|
+
detail_threshold, simplify_move_markup). XmlPowerToolsEngine ignores kwargs.
|
|
145
151
|
"""
|
|
146
|
-
|
|
152
|
+
scratch_files = []
|
|
147
153
|
try:
|
|
154
|
+
# mkstemp, not NamedTemporaryFile: we want a path, and NamedTemporaryFile
|
|
155
|
+
# hands back an open file object that nothing here would close (issue #30).
|
|
156
|
+
handle, target_path = tempfile.mkstemp(suffix='.docx')
|
|
157
|
+
os.close(handle)
|
|
158
|
+
scratch_files.append(target_path)
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
modified_path = self._write_to_temp_file(modified) if isinstance(modified, bytes) else modified
|
|
152
|
-
temp_files.extend([target_path, original_path, modified_path])
|
|
160
|
+
original_path = self._as_path(original, scratch_files)
|
|
161
|
+
modified_path = self._as_path(modified, scratch_files)
|
|
153
162
|
|
|
154
163
|
command = self._build_command(author_tag, original_path, modified_path, target_path, **kwargs)
|
|
155
164
|
|
|
@@ -164,14 +173,27 @@ class BaseEngine(object):
|
|
|
164
173
|
return redline_output, stdout_output, stderr_output
|
|
165
174
|
|
|
166
175
|
finally:
|
|
167
|
-
self._cleanup_temp_files(
|
|
176
|
+
self._cleanup_temp_files(scratch_files)
|
|
177
|
+
|
|
178
|
+
def _as_path(self, document, scratch_files):
|
|
179
|
+
"""Return a filesystem path for *document*, writing bytes out if needed.
|
|
180
|
+
|
|
181
|
+
Only a path this method creates is appended to *scratch_files*. A path
|
|
182
|
+
the caller passed in is the caller's own document, and registering it
|
|
183
|
+
for cleanup would delete the file they asked us to compare.
|
|
184
|
+
"""
|
|
185
|
+
if isinstance(document, bytes):
|
|
186
|
+
path = self._write_to_temp_file(document)
|
|
187
|
+
scratch_files.append(path)
|
|
188
|
+
return path
|
|
189
|
+
return os.fspath(document)
|
|
168
190
|
|
|
169
191
|
def _cleanup_temp_files(self, temp_files):
|
|
170
192
|
for file_path in temp_files:
|
|
171
193
|
try:
|
|
172
194
|
os.remove(file_path)
|
|
173
195
|
except OSError as e:
|
|
174
|
-
|
|
196
|
+
logger.warning("Error deleting temp file %s: %s", file_path, e)
|
|
175
197
|
|
|
176
198
|
def _write_to_temp_file(self, data):
|
|
177
199
|
"""
|
|
@@ -188,17 +210,49 @@ class XmlPowerToolsEngine(BaseEngine):
|
|
|
188
210
|
BINARY_BASE_NAME = 'redlines'
|
|
189
211
|
EXTRA_NAME = 'ooxmlpowertools'
|
|
190
212
|
|
|
213
|
+
def __init__(self, target_path: Optional[str] = None):
|
|
214
|
+
warnings.warn(
|
|
215
|
+
"XmlPowerToolsEngine wraps the original, unmaintained Open-XML-PowerTools "
|
|
216
|
+
"WmlComparer and is deprecated; it will be removed in a future major "
|
|
217
|
+
"release. Use DocxodusEngine, which is actively maintained and runs the "
|
|
218
|
+
"DocxDiff algorithm.",
|
|
219
|
+
DeprecationWarning,
|
|
220
|
+
stacklevel=2,
|
|
221
|
+
)
|
|
222
|
+
super().__init__(target_path)
|
|
223
|
+
|
|
191
224
|
|
|
192
225
|
class DocxodusEngine(BaseEngine):
|
|
193
226
|
BINARY_PACKAGE = 'python_redlines_docxodus'
|
|
194
227
|
BINARY_BASE_NAME = 'redline'
|
|
195
228
|
EXTRA_NAME = 'docxodus'
|
|
196
229
|
|
|
230
|
+
# Settings that died with WmlComparer in Docxodus v11.0.0. They are rejected
|
|
231
|
+
# rather than dropped, because dropping them would not be a breaking change
|
|
232
|
+
# but a wrong-answer one: the v12 CLI rejects --engine as an unknown flag,
|
|
233
|
+
# and merely warns-and-ignores the other two, so a caller who passed
|
|
234
|
+
# engine='wmlcomparer' would silently receive DocxDiff output believing they
|
|
235
|
+
# had selected something else.
|
|
236
|
+
_REMOVED_KWARGS = {
|
|
237
|
+
'engine': (
|
|
238
|
+
"the comparison-engine selector was removed in Docxodus v11.0.0, which "
|
|
239
|
+
"deleted WmlComparer. DocxDiff is now the only algorithm — drop the argument"
|
|
240
|
+
),
|
|
241
|
+
'detail_threshold': (
|
|
242
|
+
"it tuned WmlComparer's LCS granularity and went with it in Docxodus "
|
|
243
|
+
"v11.0.0. DocxDiff's granularity is structural, with no equivalent knob — "
|
|
244
|
+
"drop the argument"
|
|
245
|
+
),
|
|
246
|
+
'simplify_move_markup': (
|
|
247
|
+
"it worked around WmlComparer's move markup and went with it in Docxodus "
|
|
248
|
+
"v11.0.0. DocxDiff renders moves natively — drop the argument"
|
|
249
|
+
),
|
|
250
|
+
}
|
|
251
|
+
|
|
197
252
|
# Boolean flags (default False — presence enables)
|
|
198
253
|
_BOOL_FLAGS = [
|
|
199
254
|
('case_insensitive', '--case-insensitive'),
|
|
200
255
|
('detect_moves', '--detect-moves'),
|
|
201
|
-
('simplify_move_markup', '--simplify-move-markup'),
|
|
202
256
|
]
|
|
203
257
|
|
|
204
258
|
# Negatable flags (default True — --no- prefix disables)
|
|
@@ -209,18 +263,31 @@ class DocxodusEngine(BaseEngine):
|
|
|
209
263
|
|
|
210
264
|
# Value flags
|
|
211
265
|
_VALUE_FLAGS = [
|
|
212
|
-
('detail_threshold', '--detail-threshold'),
|
|
213
266
|
('move_similarity_threshold', '--move-similarity-threshold'),
|
|
214
267
|
('move_minimum_word_count', '--move-minimum-word-count'),
|
|
215
268
|
('date_time', '--date-time'),
|
|
216
269
|
]
|
|
217
270
|
|
|
218
|
-
@
|
|
219
|
-
def
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
271
|
+
@classmethod
|
|
272
|
+
def _supported_kwargs(cls):
|
|
273
|
+
"""Every comparison setting this engine still understands."""
|
|
274
|
+
return {name for name, _ in (*cls._BOOL_FLAGS, *cls._NEG_FLAGS, *cls._VALUE_FLAGS)}
|
|
275
|
+
|
|
276
|
+
@classmethod
|
|
277
|
+
def _validate_kwargs(cls, kwargs):
|
|
278
|
+
# Removed settings first, so they get their specific explanation rather
|
|
279
|
+
# than being lumped in with typos below.
|
|
280
|
+
for name, reason in cls._REMOVED_KWARGS.items():
|
|
281
|
+
if name in kwargs:
|
|
282
|
+
raise ValueError(f"{name} is no longer supported: {reason}.")
|
|
283
|
+
|
|
284
|
+
supported = cls._supported_kwargs()
|
|
285
|
+
unknown = sorted(set(kwargs) - supported)
|
|
286
|
+
if unknown:
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"Unknown comparison setting(s): {', '.join(unknown)}. "
|
|
289
|
+
f"Supported settings: {', '.join(sorted(supported))}."
|
|
290
|
+
)
|
|
224
291
|
|
|
225
292
|
if 'move_similarity_threshold' in kwargs:
|
|
226
293
|
val = kwargs['move_similarity_threshold']
|
|
File without changes
|