python-redlines 0.3.0__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.3.0 → python_redlines-1.0.0}/.gitignore +3 -0
- {python_redlines-0.3.0 → python_redlines-1.0.0}/PKG-INFO +7 -5
- {python_redlines-0.3.0 → python_redlines-1.0.0}/pyproject.toml +21 -3
- {python_redlines-0.3.0 → python_redlines-1.0.0}/src/python_redlines/__about__.py +1 -1
- {python_redlines-0.3.0 → python_redlines-1.0.0}/src/python_redlines/engines.py +80 -54
- {python_redlines-0.3.0 → python_redlines-1.0.0}/README.md +0 -0
- {python_redlines-0.3.0 → 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
|
|
@@ -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,23 +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
|
-
simplify_move_markup, move_similarity_threshold, move_minimum_word_count,
|
|
144
|
-
detect_format_changes, conflate_spaces, date_time.
|
|
142
|
+
A path the caller supplies is never deleted; only scratch files this
|
|
143
|
+
method creates are cleaned up.
|
|
145
144
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
150
151
|
"""
|
|
151
|
-
|
|
152
|
+
scratch_files = []
|
|
152
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)
|
|
153
159
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
modified_path = self._write_to_temp_file(modified) if isinstance(modified, bytes) else modified
|
|
157
|
-
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)
|
|
158
162
|
|
|
159
163
|
command = self._build_command(author_tag, original_path, modified_path, target_path, **kwargs)
|
|
160
164
|
|
|
@@ -169,14 +173,27 @@ class BaseEngine(object):
|
|
|
169
173
|
return redline_output, stdout_output, stderr_output
|
|
170
174
|
|
|
171
175
|
finally:
|
|
172
|
-
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)
|
|
173
190
|
|
|
174
191
|
def _cleanup_temp_files(self, temp_files):
|
|
175
192
|
for file_path in temp_files:
|
|
176
193
|
try:
|
|
177
194
|
os.remove(file_path)
|
|
178
195
|
except OSError as e:
|
|
179
|
-
|
|
196
|
+
logger.warning("Error deleting temp file %s: %s", file_path, e)
|
|
180
197
|
|
|
181
198
|
def _write_to_temp_file(self, data):
|
|
182
199
|
"""
|
|
@@ -193,24 +210,49 @@ class XmlPowerToolsEngine(BaseEngine):
|
|
|
193
210
|
BINARY_BASE_NAME = 'redlines'
|
|
194
211
|
EXTRA_NAME = 'ooxmlpowertools'
|
|
195
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
|
+
|
|
196
224
|
|
|
197
225
|
class DocxodusEngine(BaseEngine):
|
|
198
226
|
BINARY_PACKAGE = 'python_redlines_docxodus'
|
|
199
227
|
BINARY_BASE_NAME = 'redline'
|
|
200
228
|
EXTRA_NAME = 'docxodus'
|
|
201
229
|
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
|
|
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
|
+
}
|
|
208
251
|
|
|
209
252
|
# Boolean flags (default False — presence enables)
|
|
210
253
|
_BOOL_FLAGS = [
|
|
211
254
|
('case_insensitive', '--case-insensitive'),
|
|
212
255
|
('detect_moves', '--detect-moves'),
|
|
213
|
-
('simplify_move_markup', '--simplify-move-markup'),
|
|
214
256
|
]
|
|
215
257
|
|
|
216
258
|
# Negatable flags (default True — --no- prefix disables)
|
|
@@ -221,43 +263,31 @@ class DocxodusEngine(BaseEngine):
|
|
|
221
263
|
|
|
222
264
|
# Value flags
|
|
223
265
|
_VALUE_FLAGS = [
|
|
224
|
-
('detail_threshold', '--detail-threshold'),
|
|
225
266
|
('move_similarity_threshold', '--move-similarity-threshold'),
|
|
226
267
|
('move_minimum_word_count', '--move-minimum-word-count'),
|
|
227
268
|
('date_time', '--date-time'),
|
|
228
269
|
]
|
|
229
270
|
|
|
230
271
|
@classmethod
|
|
231
|
-
def
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
return None
|
|
235
|
-
|
|
236
|
-
engine = kwargs['engine']
|
|
237
|
-
if not isinstance(engine, str):
|
|
238
|
-
raise ValueError(f"engine must be a string, got {engine!r}")
|
|
239
|
-
|
|
240
|
-
normalized = engine.strip().lower()
|
|
241
|
-
if normalized not in cls.ENGINES:
|
|
242
|
-
raise ValueError(
|
|
243
|
-
f"engine must be one of {', '.join(cls.ENGINES)}, got {engine!r}"
|
|
244
|
-
)
|
|
245
|
-
return normalized
|
|
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)}
|
|
246
275
|
|
|
247
276
|
@classmethod
|
|
248
277
|
def _validate_kwargs(cls, kwargs):
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
)
|
|
261
291
|
|
|
262
292
|
if 'move_similarity_threshold' in kwargs:
|
|
263
293
|
val = kwargs['move_similarity_threshold']
|
|
@@ -271,14 +301,10 @@ class DocxodusEngine(BaseEngine):
|
|
|
271
301
|
|
|
272
302
|
def _build_command(self, author_tag, original_path, modified_path, target_path, **kwargs):
|
|
273
303
|
self._validate_kwargs(kwargs)
|
|
274
|
-
engine = self._normalize_engine(kwargs)
|
|
275
304
|
|
|
276
305
|
cmd = [self.extracted_binaries_path, original_path, modified_path, target_path,
|
|
277
306
|
f'--author={author_tag}']
|
|
278
307
|
|
|
279
|
-
if engine is not None:
|
|
280
|
-
cmd.append(f'--engine={engine}')
|
|
281
|
-
|
|
282
308
|
for kwarg, flag in self._BOOL_FLAGS:
|
|
283
309
|
if kwargs.get(kwarg):
|
|
284
310
|
cmd.append(flag)
|
|
File without changes
|
|
File without changes
|