dir-content-diff 1.13.0__tar.gz → 1.14.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.
Files changed (80) hide show
  1. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/CHANGELOG.md +16 -0
  2. {dir_content_diff-1.13.0/dir_content_diff.egg-info → dir_content_diff-1.14.0}/PKG-INFO +85 -4
  3. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/README.md +84 -3
  4. dir_content_diff-1.14.0/dir_content_diff/__init__.py +53 -0
  5. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff/base_comparators.py +1 -1
  6. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff/cli/__init__.py +34 -2
  7. dir_content_diff-1.14.0/dir_content_diff/config.py +225 -0
  8. dir_content_diff-1.14.0/dir_content_diff/core.py +463 -0
  9. dir_content_diff-1.14.0/dir_content_diff/parallel_utils.py +100 -0
  10. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff/pytest_plugin.py +1 -1
  11. dir_content_diff-1.14.0/dir_content_diff/registry.py +114 -0
  12. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff/util.py +1 -1
  13. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0/dir_content_diff.egg-info}/PKG-INFO +85 -4
  14. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff.egg-info/SOURCES.txt +5 -0
  15. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/comparators/test_pandas.py +17 -16
  16. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/comparators/test_voxcell.py +1 -1
  17. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/test_base.py +129 -75
  18. dir_content_diff-1.14.0/tests/test_parallel_execution.py +371 -0
  19. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/test_util.py +1 -1
  20. dir_content_diff-1.13.0/dir_content_diff/__init__.py +0 -613
  21. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.auto-changelog +0 -0
  22. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.auto-changelog-template.hbs +0 -0
  23. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.codespellignorelines +0 -0
  24. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.codespellrc +0 -0
  25. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.copier-answers.yml +0 -0
  26. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.copyright.tmpl +0 -0
  27. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.coveragerc +0 -0
  28. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.coveragerc_min_versions +0 -0
  29. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.gitattributes +0 -0
  30. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.github/ISSUE_TEMPLATE/bug_report.yaml +0 -0
  31. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  32. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.github/ISSUE_TEMPLATE/feature_request.yaml +0 -0
  33. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.github/ISSUE_TEMPLATE/how_to_use.yaml +0 -0
  34. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.github/dependabot.yml +0 -0
  35. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.github/pull_request_template.md +0 -0
  36. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.github/workflows/commitlint.yml +0 -0
  37. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.github/workflows/publish-sdist.yml +0 -0
  38. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.github/workflows/run-tox.yml +0 -0
  39. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.gitignore +0 -0
  40. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.gitreview +0 -0
  41. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.pre-commit-config.yaml +0 -0
  42. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.pylintrc +0 -0
  43. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/.readthedocs.yml +0 -0
  44. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/AUTHORS.md +0 -0
  45. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/CONTRIBUTING.md +0 -0
  46. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/LICENSE.txt +0 -0
  47. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/MANIFEST.in +0 -0
  48. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/codecov.yml +0 -0
  49. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/commitlint.config.js +0 -0
  50. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff/comparators/__init__.py +0 -0
  51. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff/comparators/dependencies.json +0 -0
  52. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff/comparators/morphio.py +0 -0
  53. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff/comparators/pandas.py +0 -0
  54. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff/comparators/voxcell.py +0 -0
  55. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff.egg-info/dependency_links.txt +0 -0
  56. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff.egg-info/entry_points.txt +0 -0
  57. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff.egg-info/requires.txt +0 -0
  58. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/dir_content_diff.egg-info/top_level.txt +0 -0
  59. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/docs/Makefile +0 -0
  60. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/docs/source/.gitignore +0 -0
  61. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/docs/source/api_ref.rst +0 -0
  62. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/docs/source/changelog.rst +0 -0
  63. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/docs/source/conf.py +0 -0
  64. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/docs/source/contributing.rst +0 -0
  65. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/docs/source/index.rst +0 -0
  66. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/package.json +0 -0
  67. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/pyproject.toml +0 -0
  68. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/setup.cfg +0 -0
  69. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/setup.py +0 -0
  70. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/__init__.py +0 -0
  71. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/comparators/__init__.py +0 -0
  72. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/comparators/test_morphio.py +0 -0
  73. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/conftest.py +0 -0
  74. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/generate_test_files.py +0 -0
  75. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/test_attrs_validation.py +0 -0
  76. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/test_cli.py +0 -0
  77. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/test_missing_deps.py +0 -0
  78. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/test_pytest_plugin.py +0 -0
  79. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tests/test_version.py +0 -0
  80. {dir_content_diff-1.13.0 → dir_content_diff-1.14.0}/tox.ini +0 -0
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.14.0](https://github.com/BlueBrain/dir-content-diff/compare/1.13.0..1.14.0)
4
+
5
+ > 13 September 2025
6
+
7
+ ### New Features
8
+
9
+ - Add parallel iteration over compared files (Adrien Berchet - [#89](https://github.com/BlueBrain/dir-content-diff/pull/89))
10
+
11
+ ### Documentation Changes
12
+
13
+ - Improve README to describe parallel execution (Adrien Berchet - [#91](https://github.com/BlueBrain/dir-content-diff/pull/91))
14
+
15
+ ### Refactoring and Updates
16
+
17
+ - Reorganize the code (Adrien Berchet - [#90](https://github.com/BlueBrain/dir-content-diff/pull/90))
18
+
3
19
  ## [1.13.0](https://github.com/BlueBrain/dir-content-diff/compare/1.12.1..1.13.0)
4
20
 
5
21
  > 12 September 2025
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dir-content-diff
3
- Version: 1.13.0
3
+ Version: 1.14.0
4
4
  Summary: Simple tool to compare directory contents.
5
5
  Author: Blue Brain Project, EPFL
6
6
  License: Apache License 2.0
@@ -76,7 +76,7 @@ Dynamic: provides-extra
76
76
  This project provides simple tools to compare the content of a directory against a reference
77
77
  directory.
78
78
 
79
- This is useful to check the results of a process that generate several files, like a luigi
79
+ This is useful to check the results of a process that generates several files, like a luigi
80
80
  workflow for example.
81
81
 
82
82
 
@@ -102,6 +102,8 @@ the files, so it is not able to report which values are different.
102
102
 
103
103
  ### Compare two directories
104
104
 
105
+ #### Basic Directory Comparison
106
+
105
107
  Let's compare two directories with the following structures:
106
108
 
107
109
  ```bash
@@ -142,6 +144,8 @@ was detected. As mentioned previously, this is because `dir-content-diff` is onl
142
144
  in the compared directory that are also present in the reference directory, so the file
143
145
  `sub_file_3.b` is just ignored in this case.
144
146
 
147
+ #### Using Custom Comparators
148
+
145
149
  If ``reference_dir/file_1.c`` is the following JSON-like file:
146
150
 
147
151
  ```json
@@ -183,6 +187,8 @@ The previous code will output the following dictionary:
183
187
  }
184
188
  ```
185
189
 
190
+ #### Assertion-based Comparison
191
+
186
192
  It is also possible to check whether the two directories are equal or not with the following code:
187
193
 
188
194
  ```python
@@ -201,7 +207,9 @@ Changed the value of '[a]' from 1 to 2.
201
207
  Changed the value of '[b][0]' from 1 to 10.
202
208
  ```
203
209
 
204
- Finally, the comparators have parameters that can be passed either to be used for all files of a
210
+ #### Advanced Configuration Options
211
+
212
+ The comparators have parameters that can be passed either to be used for all files of a
205
213
  given extension or only for a specific file:
206
214
 
207
215
  ```python
@@ -229,6 +237,8 @@ dir_content_diff.assert_equal_trees(
229
237
 
230
238
  Each comparator has different arguments that are detailed in the documentation.
231
239
 
240
+ ##### File-specific Comparators
241
+
232
242
  It's also possible to specify a arbitrary comparator for a specific file:
233
243
 
234
244
  ```python
@@ -240,6 +250,8 @@ specific_args = {
240
250
  }
241
251
  ```
242
252
 
253
+ ##### Pattern-based Configuration
254
+
243
255
  Another possibility is to use regular expressions to associate specific arguments to
244
256
  a set of files:
245
257
 
@@ -252,7 +264,9 @@ specific_args = {
252
264
  }
253
265
  ```
254
266
 
255
- And last but not least, it's possible to filter files from the reference directory (for example
267
+ ##### File Filtering
268
+
269
+ Last but not least, it's possible to filter files from the reference directory (for example
256
270
  because the reference directory contains temporary files that should not be compared). For
257
271
  example, the following code will ignore all files whose name does not start with `file_` and does
258
272
  not ends with `_tmp.yaml`:
@@ -269,6 +283,73 @@ dir_content_diff.compare_trees(
269
283
  ```
270
284
 
271
285
 
286
+ ### Parallel Execution
287
+
288
+ By default, `dir-content-diff` runs file comparisons sequentially. However, for improved performance when comparing large numbers of files, parallel execution is available using either thread-based or process-based concurrency.
289
+
290
+ #### Configuration Options
291
+
292
+ Parallel execution can be configured using the following parameters:
293
+
294
+ - **`executor_type`**: Controls the type of parallel execution:
295
+ - `"sequential"` (default): No parallel execution, files are compared one by one
296
+ - `"thread"`: Uses `ThreadPoolExecutor` (recommended for I/O-bound tasks)
297
+ - `"process"`: Uses `ProcessPoolExecutor` (recommended for CPU-intensive comparisons)
298
+
299
+ - **`max_workers`**: Maximum number of worker threads/processes. If `None` (default), it defaults to `min(32, (os.cpu_count() or 1) + 4)`.
300
+
301
+ #### Usage Examples
302
+
303
+ Enable thread-based parallel execution:
304
+
305
+ ```python
306
+ import dir_content_diff
307
+
308
+ dir_content_diff.compare_trees(
309
+ "reference_dir",
310
+ "compared_dir",
311
+ executor_type="thread",
312
+ max_workers=8
313
+ )
314
+ ```
315
+
316
+ Enable process-based parallel execution with automatic worker count:
317
+
318
+ ```python
319
+ import dir_content_diff
320
+
321
+ dir_content_diff.compare_trees(
322
+ "reference_dir",
323
+ "compared_dir",
324
+ executor_type="process"
325
+ )
326
+ ```
327
+
328
+ Using a configuration object:
329
+
330
+ ```python
331
+ import dir_content_diff
332
+
333
+ config = dir_content_diff.ComparisonConfig(
334
+ executor_type="thread",
335
+ max_workers=4
336
+ )
337
+
338
+ dir_content_diff.compare_trees(
339
+ "reference_dir",
340
+ "compared_dir",
341
+ config=config
342
+ )
343
+ ```
344
+
345
+ #### Performance Considerations
346
+
347
+ - **Thread-based execution** (`executor_type="thread"`) is generally recommended for most use cases as file comparisons are typically I/O-bound operations
348
+ - **Process-based execution** (`executor_type="process"`) may be beneficial when using computationally intensive comparators or when dealing with very large files
349
+ - Parallel execution is automatically disabled for single file comparisons and falls back to sequential execution when only one file needs to be compared
350
+ - The optimal number of workers depends on your system's capabilities and the nature of your files; too many workers may actually decrease performance due to overhead
351
+
352
+
272
353
  ### Export formatted data
273
354
 
274
355
  Some comparators have to format the data before comparing them. For example, if one wants to
@@ -10,7 +10,7 @@
10
10
  This project provides simple tools to compare the content of a directory against a reference
11
11
  directory.
12
12
 
13
- This is useful to check the results of a process that generate several files, like a luigi
13
+ This is useful to check the results of a process that generates several files, like a luigi
14
14
  workflow for example.
15
15
 
16
16
 
@@ -36,6 +36,8 @@ the files, so it is not able to report which values are different.
36
36
 
37
37
  ### Compare two directories
38
38
 
39
+ #### Basic Directory Comparison
40
+
39
41
  Let's compare two directories with the following structures:
40
42
 
41
43
  ```bash
@@ -76,6 +78,8 @@ was detected. As mentioned previously, this is because `dir-content-diff` is onl
76
78
  in the compared directory that are also present in the reference directory, so the file
77
79
  `sub_file_3.b` is just ignored in this case.
78
80
 
81
+ #### Using Custom Comparators
82
+
79
83
  If ``reference_dir/file_1.c`` is the following JSON-like file:
80
84
 
81
85
  ```json
@@ -117,6 +121,8 @@ The previous code will output the following dictionary:
117
121
  }
118
122
  ```
119
123
 
124
+ #### Assertion-based Comparison
125
+
120
126
  It is also possible to check whether the two directories are equal or not with the following code:
121
127
 
122
128
  ```python
@@ -135,7 +141,9 @@ Changed the value of '[a]' from 1 to 2.
135
141
  Changed the value of '[b][0]' from 1 to 10.
136
142
  ```
137
143
 
138
- Finally, the comparators have parameters that can be passed either to be used for all files of a
144
+ #### Advanced Configuration Options
145
+
146
+ The comparators have parameters that can be passed either to be used for all files of a
139
147
  given extension or only for a specific file:
140
148
 
141
149
  ```python
@@ -163,6 +171,8 @@ dir_content_diff.assert_equal_trees(
163
171
 
164
172
  Each comparator has different arguments that are detailed in the documentation.
165
173
 
174
+ ##### File-specific Comparators
175
+
166
176
  It's also possible to specify a arbitrary comparator for a specific file:
167
177
 
168
178
  ```python
@@ -174,6 +184,8 @@ specific_args = {
174
184
  }
175
185
  ```
176
186
 
187
+ ##### Pattern-based Configuration
188
+
177
189
  Another possibility is to use regular expressions to associate specific arguments to
178
190
  a set of files:
179
191
 
@@ -186,7 +198,9 @@ specific_args = {
186
198
  }
187
199
  ```
188
200
 
189
- And last but not least, it's possible to filter files from the reference directory (for example
201
+ ##### File Filtering
202
+
203
+ Last but not least, it's possible to filter files from the reference directory (for example
190
204
  because the reference directory contains temporary files that should not be compared). For
191
205
  example, the following code will ignore all files whose name does not start with `file_` and does
192
206
  not ends with `_tmp.yaml`:
@@ -203,6 +217,73 @@ dir_content_diff.compare_trees(
203
217
  ```
204
218
 
205
219
 
220
+ ### Parallel Execution
221
+
222
+ By default, `dir-content-diff` runs file comparisons sequentially. However, for improved performance when comparing large numbers of files, parallel execution is available using either thread-based or process-based concurrency.
223
+
224
+ #### Configuration Options
225
+
226
+ Parallel execution can be configured using the following parameters:
227
+
228
+ - **`executor_type`**: Controls the type of parallel execution:
229
+ - `"sequential"` (default): No parallel execution, files are compared one by one
230
+ - `"thread"`: Uses `ThreadPoolExecutor` (recommended for I/O-bound tasks)
231
+ - `"process"`: Uses `ProcessPoolExecutor` (recommended for CPU-intensive comparisons)
232
+
233
+ - **`max_workers`**: Maximum number of worker threads/processes. If `None` (default), it defaults to `min(32, (os.cpu_count() or 1) + 4)`.
234
+
235
+ #### Usage Examples
236
+
237
+ Enable thread-based parallel execution:
238
+
239
+ ```python
240
+ import dir_content_diff
241
+
242
+ dir_content_diff.compare_trees(
243
+ "reference_dir",
244
+ "compared_dir",
245
+ executor_type="thread",
246
+ max_workers=8
247
+ )
248
+ ```
249
+
250
+ Enable process-based parallel execution with automatic worker count:
251
+
252
+ ```python
253
+ import dir_content_diff
254
+
255
+ dir_content_diff.compare_trees(
256
+ "reference_dir",
257
+ "compared_dir",
258
+ executor_type="process"
259
+ )
260
+ ```
261
+
262
+ Using a configuration object:
263
+
264
+ ```python
265
+ import dir_content_diff
266
+
267
+ config = dir_content_diff.ComparisonConfig(
268
+ executor_type="thread",
269
+ max_workers=4
270
+ )
271
+
272
+ dir_content_diff.compare_trees(
273
+ "reference_dir",
274
+ "compared_dir",
275
+ config=config
276
+ )
277
+ ```
278
+
279
+ #### Performance Considerations
280
+
281
+ - **Thread-based execution** (`executor_type="thread"`) is generally recommended for most use cases as file comparisons are typically I/O-bound operations
282
+ - **Process-based execution** (`executor_type="process"`) may be beneficial when using computationally intensive comparators or when dealing with very large files
283
+ - Parallel execution is automatically disabled for single file comparisons and falls back to sequential execution when only one file needs to be compared
284
+ - The optimal number of workers depends on your system's capabilities and the nature of your files; too many workers may actually decrease performance due to overhead
285
+
286
+
206
287
  ### Export formatted data
207
288
 
208
289
  Some comparators have to format the data before comparing them. For example, if one wants to
@@ -0,0 +1,53 @@
1
+ """dir-content-diff package.
2
+
3
+ Simple tool to compare directory contents.
4
+ """
5
+
6
+ # LICENSE HEADER MANAGED BY add-license-header
7
+ # Copyright (c) 2023-2025 Blue Brain Project, EPFL.
8
+ #
9
+ # This file is part of dir-content-diff.
10
+ # See https://github.com/BlueBrain/dir-content-diff for further info.
11
+ #
12
+ # SPDX-License-Identifier: Apache-2.0
13
+ # LICENSE HEADER MANAGED BY add-license-header
14
+
15
+ import importlib.metadata
16
+
17
+ # Import base comparators
18
+ from dir_content_diff.base_comparators import BaseComparator
19
+ from dir_content_diff.base_comparators import DefaultComparator
20
+
21
+ # Import core comparison functionality
22
+ from dir_content_diff.config import ComparisonConfig
23
+ from dir_content_diff.core import ComparatorType
24
+ from dir_content_diff.core import assert_equal_trees
25
+ from dir_content_diff.core import compare_files
26
+ from dir_content_diff.core import compare_trees
27
+ from dir_content_diff.core import export_formatted_file
28
+ from dir_content_diff.core import pick_comparator
29
+
30
+ # Import comparator registry functionality
31
+ from dir_content_diff.registry import get_comparators
32
+ from dir_content_diff.registry import register_comparator
33
+ from dir_content_diff.registry import reset_comparators
34
+ from dir_content_diff.registry import unregister_comparator
35
+
36
+ __version__ = importlib.metadata.version("dir-content-diff")
37
+
38
+ # For backward compatibility, ensure all public functions are available
39
+ __all__ = [
40
+ "ComparisonConfig",
41
+ "ComparatorType",
42
+ "assert_equal_trees",
43
+ "compare_files",
44
+ "compare_trees",
45
+ "export_formatted_file",
46
+ "pick_comparator",
47
+ "get_comparators",
48
+ "register_comparator",
49
+ "reset_comparators",
50
+ "unregister_comparator",
51
+ "BaseComparator",
52
+ "DefaultComparator",
53
+ ]
@@ -513,7 +513,7 @@ class XmlComparator(DictComparator):
513
513
  elif str(text).lower() == "false":
514
514
  res = False
515
515
  else:
516
- raise ValueError("Bool attributes expect 'true' or 'false'.")
516
+ raise ValueError("Boolean attributes expect 'true' or 'false'.")
517
517
  elif value_type == "list":
518
518
  res = []
519
519
  elif value_type == "dict":
@@ -13,15 +13,17 @@ import json
13
13
  import logging
14
14
  import sys
15
15
  from pathlib import Path
16
+ from typing import Optional
17
+ from typing import Union
16
18
 
17
19
  import click
18
20
  from yaml import safe_load
19
21
 
20
- from dir_content_diff import _DEFAULT_EXPORT_SUFFIX
21
22
  from dir_content_diff import compare_files
22
23
  from dir_content_diff import compare_trees
23
24
  from dir_content_diff import export_formatted_file
24
25
  from dir_content_diff import pick_comparator
26
+ from dir_content_diff.core import _DEFAULT_EXPORT_SUFFIX
25
27
  from dir_content_diff.util import LOGGER
26
28
 
27
29
 
@@ -102,6 +104,19 @@ def load_config(ctx, param, value): # pylint: disable=unused-argument
102
104
  default=False,
103
105
  help="Sort the differences by file name.",
104
106
  )
107
+ @click.option(
108
+ "--executor-type",
109
+ type=click.Choice(["sequential", "thread", "process"]),
110
+ default="sequential",
111
+ help="Type of executor for execution. 'sequential' for single-threaded, "
112
+ "'thread' for I/O-bound tasks, 'process' for CPU-bound tasks.",
113
+ )
114
+ @click.option(
115
+ "--max-workers",
116
+ type=int,
117
+ help="Maximum number of worker threads/processes for parallel execution. "
118
+ "Only used with 'thread' or 'process' executor types.",
119
+ )
105
120
  @click.version_option()
106
121
  @click.pass_context
107
122
  def main(ctx, *args, **kwargs):
@@ -120,16 +135,31 @@ def main(ctx, *args, **kwargs):
120
135
 
121
136
  ref = Path(kwargs.pop("reference_input"))
122
137
  comp = Path(kwargs.pop("compared_input"))
138
+
139
+ # Extract execution options
140
+ executor_type = kwargs.pop("executor_type", "sequential")
141
+ max_workers = kwargs.pop("max_workers", None)
142
+
123
143
  input_diff(
124
144
  ref,
125
145
  comp,
126
146
  ctx.config,
127
147
  kwargs.pop("export_formatted_files", False),
128
148
  kwargs.pop("sort_diffs", False),
149
+ executor_type,
150
+ max_workers,
129
151
  )
130
152
 
131
153
 
132
- def input_diff(ref, comp, config, export_formatted_files=False, sort_diffs=False):
154
+ def input_diff(
155
+ ref: Union[str, Path],
156
+ comp: Union[str, Path],
157
+ config,
158
+ export_formatted_files: Union[bool, str] = False,
159
+ sort_diffs: bool = False,
160
+ executor_type: str = "sequential",
161
+ max_workers: Optional[int] = None,
162
+ ):
133
163
  """Compute and display differences from given inputs."""
134
164
  ref = Path(ref)
135
165
  comp = Path(comp)
@@ -145,6 +175,8 @@ def input_diff(ref, comp, config, export_formatted_files=False, sort_diffs=False
145
175
  comp,
146
176
  specific_args=config,
147
177
  export_formatted_files=export_formatted_files,
178
+ executor_type=executor_type,
179
+ max_workers=max_workers,
148
180
  )
149
181
  else:
150
182
  comparator_name = config.pop("comparator", None)
@@ -0,0 +1,225 @@
1
+ """Configuration classes for directory content comparison.
2
+
3
+ This module contains the configuration classes and validation functions.
4
+ """
5
+
6
+ # LICENSE HEADER MANAGED BY add-license-header
7
+ # Copyright (c) 2023-2025 Blue Brain Project, EPFL.
8
+ #
9
+ # This file is part of dir-content-diff.
10
+ # See https://github.com/BlueBrain/dir-content-diff for further info.
11
+ #
12
+ # SPDX-License-Identifier: Apache-2.0
13
+ # LICENSE HEADER MANAGED BY add-license-header
14
+
15
+ import re
16
+ from collections.abc import Callable
17
+ from typing import Any
18
+ from typing import Dict
19
+ from typing import Iterable
20
+ from typing import Literal
21
+ from typing import Optional
22
+ from typing import Pattern
23
+ from typing import Tuple
24
+ from typing import Union
25
+
26
+ import attrs
27
+
28
+ from dir_content_diff.base_comparators import BaseComparator
29
+ from dir_content_diff.registry import get_comparators
30
+
31
+ # Type alias for comparators
32
+ ComparatorType = Union[BaseComparator, Callable]
33
+
34
+
35
+ def _convert_iterable_to_tuple(
36
+ x: Optional[Iterable[str]],
37
+ ) -> Optional[Tuple[str, ...]]:
38
+ """Convert an iterable to a tuple, or return None."""
39
+ if x is None:
40
+ return None
41
+ return tuple(x)
42
+
43
+
44
+ def _validate_specific_args(instance, attribute, value): # pylint: disable=unused-argument
45
+ """Validate specific_args structure."""
46
+ for file_path, args in value.items():
47
+ if not isinstance(args, dict):
48
+ raise ValueError(f"specific_args['{file_path}'] must be a dictionary")
49
+ # Note: regex patterns in specific_args will be validated during compilation
50
+ # in __attrs_post_init__, so no need to validate them here
51
+
52
+
53
+ def _validate_export_formatted_files(instance, attribute, value): # pylint: disable=unused-argument
54
+ """Validate export_formatted_files is either bool or non-empty string."""
55
+ if isinstance(value, str) and len(value.strip()) == 0:
56
+ raise ValueError(
57
+ "export_formatted_files must be a non-empty string when provided as string"
58
+ )
59
+
60
+
61
+ def _validate_comparators(instance, attribute, value): # pylint: disable=unused-argument
62
+ """Validate comparators are either BaseComparator instances or callable."""
63
+ for ext, comparator in value.items():
64
+ if not (isinstance(comparator, BaseComparator) or callable(comparator)):
65
+ raise ValueError(
66
+ f"Comparator for extension '{ext}' must be a BaseComparator instance "
67
+ "or callable"
68
+ )
69
+
70
+
71
+ @attrs.frozen
72
+ class ComparisonConfig:
73
+ """Configuration class to store comparison settings.
74
+
75
+ Attributes:
76
+ include_patterns: A list of regular expression patterns. If the relative path of a
77
+ file does not match any of these patterns, it is ignored during the comparison. Note
78
+ that this means that any specific arguments for that file will also be ignored.
79
+ exclude_patterns: A list of regular expression patterns. If the relative path of a
80
+ file matches any of these patterns, it is ignored during the comparison. Note that
81
+ this means that any specific arguments for that file will also be ignored.
82
+ comparators: A ``dict`` to override the registered comparators.
83
+ specific_args: A ``dict`` with the args/kwargs that should be given to the
84
+ comparator for a given file. This ``dict`` should be like the following:
85
+
86
+ .. code-block:: Python
87
+
88
+ {
89
+ <relative_file_path>: {
90
+ comparator: ComparatorInstance,
91
+ args: [arg1, arg2, ...],
92
+ kwargs: {
93
+ kwarg_name_1: kwarg_value_1,
94
+ kwarg_name_2: kwarg_value_2,
95
+ }
96
+ },
97
+ <another_file_path>: {...},
98
+ <a name for this category>: {
99
+ "patterns": ["regex1", "regex2", ...],
100
+ ... (other arguments)
101
+ }
102
+ }
103
+
104
+ If the "patterns" entry is present, then the name is not considered and is only used as
105
+ a helper for the user. When a "patterns" entry is detected, the other arguments are
106
+ applied to all files whose relative name matches one of the given regular expression
107
+ patterns. If a file could match multiple patterns of different groups, only the first
108
+ one is considered.
109
+
110
+ Note that all entries in this ``dict`` are optional.
111
+ return_raw_diffs: If set to ``True``, only the raw differences are returned instead
112
+ of a formatted report.
113
+ export_formatted_files: If set to ``True`` or a not empty string, create a
114
+ new directory with formatted compared data files. If a string is passed, this string is
115
+ used as suffix for the new directory. If `True` is passed, the suffix is
116
+ ``_FORMATTED``.
117
+ max_workers: Maximum number of worker threads/processes for parallel execution. If None,
118
+ defaults to min(32, (os.cpu_count() or 1) + 4) as per executor default.
119
+ executor_type: Type of executor to use for parallel execution. 'thread' uses
120
+ ThreadPoolExecutor (better for I/O-bound tasks), 'process' uses ProcessPoolExecutor
121
+ (better for CPU-bound tasks), 'sequential' disables parallel execution.
122
+ """
123
+
124
+ include_patterns: Optional[Iterable[str]] = attrs.field(
125
+ default=None, converter=_convert_iterable_to_tuple
126
+ )
127
+ exclude_patterns: Optional[Iterable[str]] = attrs.field(
128
+ default=None, converter=_convert_iterable_to_tuple
129
+ )
130
+ comparators: Optional[Dict[Optional[str], ComparatorType]] = attrs.field(
131
+ default=None, validator=attrs.validators.optional(_validate_comparators)
132
+ )
133
+ specific_args: Optional[Dict[str, Dict[str, Any]]] = attrs.field(
134
+ default=None, validator=attrs.validators.optional(_validate_specific_args)
135
+ )
136
+ return_raw_diffs: bool = attrs.field(default=False)
137
+ export_formatted_files: Union[bool, str] = attrs.field(
138
+ default=False, validator=_validate_export_formatted_files
139
+ )
140
+ executor_type: Literal["sequential", "thread", "process"] = attrs.field(
141
+ default="sequential"
142
+ )
143
+ max_workers: Optional[int] = attrs.field(default=None)
144
+
145
+ # Compiled patterns - computed once, no caching complexity needed
146
+ compiled_include_patterns: Tuple[Pattern[str], ...] = attrs.field(init=False)
147
+ compiled_exclude_patterns: Tuple[Pattern[str], ...] = attrs.field(init=False)
148
+ pattern_specific_args: Dict[Pattern[str], Dict[str, Any]] = attrs.field(
149
+ init=False, repr=False
150
+ )
151
+
152
+ def __attrs_post_init__(self):
153
+ """Initialize computed fields after attrs initialization."""
154
+ # Validate and compile patterns - with frozen, we compile once and store directly
155
+ try:
156
+ compiled_include = self._compile_patterns(self.include_patterns)
157
+ object.__setattr__(self, "compiled_include_patterns", compiled_include)
158
+ except ValueError as e:
159
+ raise ValueError(f"Error in include_patterns: {e}") from e
160
+
161
+ try:
162
+ compiled_exclude = self._compile_patterns(self.exclude_patterns)
163
+ object.__setattr__(self, "compiled_exclude_patterns", compiled_exclude)
164
+ except ValueError as e:
165
+ raise ValueError(f"Error in exclude_patterns: {e}") from e
166
+
167
+ # Setup specific args and pattern specific args
168
+ if self.specific_args is None:
169
+ # Use object.__setattr__ to modify the field even if it's frozen
170
+ object.__setattr__(self, "specific_args", {})
171
+
172
+ # Setup pattern specific args
173
+ pattern_specific_args = {}
174
+ if self.specific_args: # Check if it's not None
175
+ for file_path, v in self.specific_args.items():
176
+ if "patterns" in v:
177
+ patterns = v.pop("patterns", [])
178
+ for pattern in patterns:
179
+ try:
180
+ compiled_pattern = self._compile_pattern(pattern)
181
+ pattern_specific_args[compiled_pattern] = v
182
+ except ValueError as e:
183
+ raise ValueError(
184
+ f"Error in specific_args['{file_path}']['patterns']: {e}"
185
+ ) from e
186
+
187
+ object.__setattr__(self, "pattern_specific_args", pattern_specific_args)
188
+
189
+ # Setup comparators
190
+ if self.comparators is None:
191
+ object.__setattr__(self, "comparators", get_comparators())
192
+
193
+ def _compile_pattern(self, pattern: str) -> Pattern[str]:
194
+ """Compile a regex pattern."""
195
+ try:
196
+ return re.compile(pattern)
197
+ except re.error as e:
198
+ raise ValueError(f"Invalid regex pattern: '{pattern}'") from e
199
+
200
+ def _compile_patterns(
201
+ self, patterns: Optional[Iterable[str]]
202
+ ) -> Tuple[Pattern[str], ...]:
203
+ """Compile regex patterns from any iterable to tuple."""
204
+ if patterns is None:
205
+ return ()
206
+ return tuple(self._compile_pattern(pattern) for pattern in patterns)
207
+
208
+ # Note: compiled_include_patterns, compiled_exclude_patterns, and pattern_specific_args
209
+ # are now direct attributes set in __attrs_post_init__, no properties needed!
210
+
211
+ def should_ignore_file(self, relative_path: str) -> bool:
212
+ """Check if a file should be ignored."""
213
+ # Check inclusion patterns first
214
+ if self.compiled_include_patterns:
215
+ included = any(
216
+ pattern.match(relative_path)
217
+ for pattern in self.compiled_include_patterns
218
+ )
219
+ if not included:
220
+ return True
221
+
222
+ # Check exclusion patterns
223
+ return any(
224
+ pattern.match(relative_path) for pattern in self.compiled_exclude_patterns
225
+ )