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.
@@ -13,6 +13,9 @@ docxodus/**/obj/*
13
13
  packages/*/src/*/_binaries/*.tar.gz
14
14
  packages/*/src/*/_binaries/*.zip
15
15
 
16
+ # MkDocs build output
17
+ site/
18
+
16
19
  # C extensions
17
20
  *.so
18
21
 
@@ -1,14 +1,16 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: python-redlines
3
- Version: 0.2.1
4
- Summary: Generate tracked-change redline .docx documents by comparing Word files.
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 :: 4 - Beta
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 = "Generate tracked-change redline .docx documents by comparing Word files."
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 = ["docx", "redline", "diff", "tracked-changes", "openxml", "word"]
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 :: 4 - Beta",
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
 
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present U.N. Owen <void@some.where>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "0.2.1"
4
+ __version__ = "1.0.0"
@@ -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
- Additional keyword arguments are passed to _build_command() for engine-specific options.
142
- DocxodusEngine supports: detail_threshold, case_insensitive, detect_moves,
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.
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
- temp_files = []
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
- target_path = tempfile.NamedTemporaryFile(delete=False).name
150
- original_path = self._write_to_temp_file(original) if isinstance(original, bytes) else original
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(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
- print(f"Error deleting temp file {file_path}: {e}")
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
- @staticmethod
219
- def _validate_kwargs(kwargs):
220
- if 'detail_threshold' in kwargs:
221
- val = kwargs['detail_threshold']
222
- if not isinstance(val, (int, float)) or val < 0.0 or val > 1.0:
223
- raise ValueError(f"detail_threshold must be a float between 0.0 and 1.0, got {val!r}")
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']