exdrf-util 0.1.17__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.
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: exdrf-util
3
+ Version: 0.1.17
4
+ Summary: Utilities for Ex-DRF.
5
+ Author-email: Nicu Tofan <nicu.tofan@gmail.com>
6
+ License-Expression: MIT
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.12.2
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: exdrf>=0.1.17
14
+ Provides-Extra: dev
15
+ Requires-Dist: autoflake; extra == "dev"
16
+ Requires-Dist: black; extra == "dev"
17
+ Requires-Dist: build; extra == "dev"
18
+ Requires-Dist: flake8; extra == "dev"
19
+ Requires-Dist: isort; extra == "dev"
20
+ Requires-Dist: mypy; extra == "dev"
21
+ Requires-Dist: pre-commit; extra == "dev"
22
+ Requires-Dist: pyproject-flake8; extra == "dev"
23
+ Requires-Dist: pytest-cov; extra == "dev"
24
+ Requires-Dist: pytest-mock; extra == "dev"
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: twine; extra == "dev"
27
+ Requires-Dist: wheel; extra == "dev"
28
+ Requires-Dist: openpyxl-stubs; extra == "dev"
29
+ Requires-Dist: reportlab-stubs; extra == "dev"
30
+
31
+ # Utilities for Ex-DRF
32
+
33
+ **exdrf-util** is a grab bag of **small, optional helpers** built on **exdrf**
34
+ types and attrs. It is useful for desktop or back-office scripts that already
35
+ depend on **exdrf** and need shared table/export or task scaffolding. It is not
36
+ a required dependency for minimal API or ORM-only stacks.
37
+
38
+ ## Contents (high level)
39
+
40
+ Modules under **`exdrf_util`** include:
41
+
42
+ - **Tabular export**: turn **openpyxl** cell ranges into **PDF** or **Word**
43
+ documents (`table2pdf`, `table2doc`, `table_writer`, `table2base`) using
44
+ **reportlab** / **python-docx** when those libraries are available in the
45
+ environment.
46
+ - **PDF utilities**: merge and manipulate PDFs (`merge_pdfs`), backup rotation
47
+ (`rotate_backups`).
48
+ - **Tasks**: attrs-based **`Task`** state machine tied to **`ExFieldBase`** for
49
+ form-like workflows (`task`).
50
+ - **Misc**: verbose checking helpers, shared typedefs for translation/context
51
+ protocols.
52
+
53
+ Runtime **`dependencies`** in `pyproject.toml` list only **exdrf**; features
54
+ that import **openpyxl**, **reportlab**, or **docx** expect you to install those
55
+ packages next to **exdrf-util** when you use the corresponding modules.
56
+
57
+ Python **3.12.2+** is required.
@@ -0,0 +1,27 @@
1
+ # Utilities for Ex-DRF
2
+
3
+ **exdrf-util** is a grab bag of **small, optional helpers** built on **exdrf**
4
+ types and attrs. It is useful for desktop or back-office scripts that already
5
+ depend on **exdrf** and need shared table/export or task scaffolding. It is not
6
+ a required dependency for minimal API or ORM-only stacks.
7
+
8
+ ## Contents (high level)
9
+
10
+ Modules under **`exdrf_util`** include:
11
+
12
+ - **Tabular export**: turn **openpyxl** cell ranges into **PDF** or **Word**
13
+ documents (`table2pdf`, `table2doc`, `table_writer`, `table2base`) using
14
+ **reportlab** / **python-docx** when those libraries are available in the
15
+ environment.
16
+ - **PDF utilities**: merge and manipulate PDFs (`merge_pdfs`), backup rotation
17
+ (`rotate_backups`).
18
+ - **Tasks**: attrs-based **`Task`** state machine tied to **`ExFieldBase`** for
19
+ form-like workflows (`task`).
20
+ - **Misc**: verbose checking helpers, shared typedefs for translation/context
21
+ protocols.
22
+
23
+ Runtime **`dependencies`** in `pyproject.toml` list only **exdrf**; features
24
+ that import **openpyxl**, **reportlab**, or **docx** expect you to install those
25
+ packages next to **exdrf-util** when you use the corresponding modules.
26
+
27
+ Python **3.12.2+** is required.
File without changes
@@ -0,0 +1,24 @@
1
+ """Package version from PEP 621 or installed metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from exdrf.pep621_version import distribution_version, version_tuple_from_string
8
+
9
+ _PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml"
10
+ _DIST_NAME = "exdrf-util"
11
+
12
+ __version__ = version = distribution_version(_DIST_NAME, _PYPROJECT)
13
+ __version_tuple__ = version_tuple = version_tuple_from_string(__version__)
14
+
15
+ __commit_id__ = commit_id = None
16
+
17
+ __all__ = [
18
+ "__version__",
19
+ "__version_tuple__",
20
+ "version",
21
+ "version_tuple",
22
+ "__commit_id__",
23
+ "commit_id",
24
+ ]
@@ -0,0 +1,499 @@
1
+ import logging
2
+ from collections import OrderedDict, defaultdict
3
+ from enum import StrEnum
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Dict,
8
+ Generic,
9
+ List,
10
+ Literal,
11
+ Optional,
12
+ Tuple,
13
+ TypeVar,
14
+ Union,
15
+ )
16
+
17
+ import pluggy
18
+ from attrs import define, field
19
+
20
+ from .task import Task, TaskParameter
21
+
22
+ if TYPE_CHECKING:
23
+ from exdrf_util.typedefs import HasBasicContext, HasTranslate
24
+
25
+ # T represents the type of the item that the check is being performed on
26
+ # on each step.
27
+ T = TypeVar("T")
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class ResultState(StrEnum):
33
+ """The state of a check result.
34
+
35
+ Attributes:
36
+ INITIAL: The check result was not yet computed.
37
+ PASSED: The check passed.
38
+ SKIPPED: The check was skipped.
39
+ FIXED: The check found a problem which was fixed.
40
+ PARTIALLY_FIXED: The check found a problem which was partially fixed.
41
+ NOT_FIXED: The check found a problem which was not fixed.
42
+ FAILED: The check failed to execute (raised an exception).
43
+ """
44
+
45
+ INITIAL = "initial"
46
+ PASSED = "passed"
47
+ SKIPPED = "skipped"
48
+ FIXED = "fixed"
49
+ PARTIALLY_FIXED = "partially_fixed"
50
+ NOT_FIXED = "not_fixed"
51
+ FAILED = "failed"
52
+
53
+
54
+ @define(slots=True, kw_only=True)
55
+ class CheckResult:
56
+ """The result of a check over a particular record or a record set.
57
+
58
+ Attributes:
59
+ check_id: The ID of the check that produced the result.
60
+ state: Whether the check passed, skipped (for cases when the filtration
61
+ could not be fully applied through the query and the runtime check
62
+ if required to determine if a particular record should be checked),
63
+ was fully or partially fixed or was detected but not fixed.
64
+ issue_hash: A pseudo-unique identifier for the issue. When possible we
65
+ should compute the hash so that the behavior of the issue
66
+ can be tracked across time. This may include the record ID,
67
+ the field ID, the check that was performed.
68
+ t_key: The translation code that can be used, along with the parameters,
69
+ to recreate the description.
70
+ params: Additional information about the result in dictionary format.
71
+ When recreating the description the code and params are used,
72
+ but additional information can be added here. Note that these
73
+ are NOT the parameters of the check.
74
+ description: A description of the result.
75
+ links: A list of links related to this result. Each link is a tuple of
76
+ (resource_name, record_id, label, address) where resource_name is
77
+ the name of the resource, record_id is either an int or a tuple of
78
+ ints, label is the text to display for the link, and address is
79
+ the URL/path to navigate to when the link is clicked.
80
+ """
81
+
82
+ check_id: str
83
+ state: ResultState = field(default=ResultState.INITIAL)
84
+ issue_hash: Optional[str] = field(default=None)
85
+ t_key: str = field(default="", repr=False)
86
+ params: Dict[str, Any] = field(factory=dict)
87
+ description: str = field(default="", repr=False)
88
+ links: List[Tuple[str, Union[int, Tuple[int, ...]], str, str]] = field(
89
+ factory=list, repr=False
90
+ )
91
+
92
+
93
+ @define(slots=True, kw_only=True)
94
+ class CheckTask(Task, Generic[T]):
95
+ """A task that executes a check."""
96
+
97
+ check: "Check"
98
+ results: List[CheckResult] = field(factory=list)
99
+
100
+ def __attrs_post_init__(self):
101
+ if self.title == "":
102
+ self.title = self.check.title
103
+ if self.description == "":
104
+ self.description = self.check.description
105
+ if not self.parameters:
106
+ self.parameters = self.check.parameters
107
+
108
+ def results_by_type(self) -> Dict[ResultState, List[CheckResult]]:
109
+ """Split the list of results by their state."""
110
+ results_by_type = defaultdict(list)
111
+ for result in self.results:
112
+ results_by_type[result.state].append(result)
113
+ return results_by_type
114
+
115
+ def result_count_by_type(self) -> Dict[ResultState, int]:
116
+ """Count the number of results by their state."""
117
+ results_by_type = self.results_by_type()
118
+ return {
119
+ state: len(results) for state, results in results_by_type.items()
120
+ }
121
+
122
+ def get_success_message(self, ctx: "HasTranslate") -> str:
123
+ if len(self.results) == 0:
124
+ return ctx.t(
125
+ "check.no-results", "The check did not find any results."
126
+ )
127
+ elif self.check.is_global:
128
+ assert len(self.results) == 1, (
129
+ "Global check should have exactly one result."
130
+ )
131
+
132
+ result = self.results[0]
133
+ state = result.state
134
+ if state == ResultState.PASSED:
135
+ return ctx.t("check.global.success", "The check passed.")
136
+ elif state == ResultState.SKIPPED:
137
+ return ctx.t("check.global.skipped", "The check was skipped.")
138
+ elif state == ResultState.FIXED:
139
+ return ctx.t(
140
+ "check.global.fixed",
141
+ "The check detected an issue and fixed it.",
142
+ )
143
+ elif state == ResultState.NOT_FIXED:
144
+ return ctx.t(
145
+ result.t_key,
146
+ "The check detected an issue but did not fix it.",
147
+ **result.params,
148
+ )
149
+ elif state == ResultState.PARTIALLY_FIXED:
150
+ return ctx.t(
151
+ "check.global.partially-fixed",
152
+ "The check detected an issue and partially fixed it but "
153
+ "it requires further manual action.",
154
+ )
155
+ elif state == ResultState.FAILED:
156
+ return ctx.t(
157
+ result.t_key,
158
+ "The check failed to execute.",
159
+ **result.params,
160
+ )
161
+ else:
162
+ raise ValueError(f"Invalid result state: {state}")
163
+ else:
164
+ final = []
165
+ results_by_type = self.result_count_by_type()
166
+ passed = results_by_type.get(ResultState.PASSED, 0)
167
+ skipped = results_by_type.get(ResultState.SKIPPED, 0)
168
+ fixed = results_by_type.get(ResultState.FIXED, 0)
169
+ not_fixed = results_by_type.get(ResultState.NOT_FIXED, 0)
170
+ partially_fixed = results_by_type.get(
171
+ ResultState.PARTIALLY_FIXED, 0
172
+ )
173
+ failed = results_by_type.get(ResultState.FAILED, 0)
174
+ if passed > 0:
175
+ final.append(
176
+ ctx.t(
177
+ "check.success.count",
178
+ "{count} checks passed.",
179
+ count=passed,
180
+ )
181
+ )
182
+ if skipped > 0:
183
+ final.append(
184
+ ctx.t(
185
+ "check.skipped.count",
186
+ "{count} checks skipped.",
187
+ count=skipped,
188
+ )
189
+ )
190
+ if fixed > 0:
191
+ final.append(
192
+ ctx.t(
193
+ "check.fixed.count",
194
+ "{count} issues were fixed.",
195
+ count=fixed,
196
+ )
197
+ )
198
+ if not_fixed > 0:
199
+ final.append(
200
+ ctx.t(
201
+ "check.not-fixed.count",
202
+ "{count} issues were not fixed.",
203
+ count=not_fixed,
204
+ )
205
+ )
206
+ if partially_fixed > 0:
207
+ final.append(
208
+ ctx.t(
209
+ "check.partially-fixed.count",
210
+ "{count} issues were partially fixed and require "
211
+ "further manual action.",
212
+ count=partially_fixed,
213
+ )
214
+ )
215
+ if failed > 0:
216
+ final.append(
217
+ ctx.t(
218
+ "check.failed.count",
219
+ "{count} checks failed to execute.",
220
+ count=failed,
221
+ )
222
+ )
223
+ return "\n".join(final)
224
+
225
+ def prepare_task(self, ctx: "HasBasicContext") -> bool: # type: ignore
226
+ """Prepare the task."""
227
+ result = self.check.prepare_check(ctx)
228
+ if result is not None:
229
+ # Add any private data to the data.
230
+ assert isinstance(result, dict)
231
+ for k, v in result.items():
232
+ if k in self.data:
233
+ logger.warning(
234
+ "Private data key %s overrides preset with same name. "
235
+ "Avoid naming the preset data keys the same as the "
236
+ "private data keys to avoid unexpected behavior.",
237
+ k,
238
+ )
239
+ self.data[k] = v
240
+
241
+ # Move the parameters to the data.
242
+ for k, v in self.check.parameters.items():
243
+ if k in self.data:
244
+ logger.warning(
245
+ "Parameter %s overrides private data with same name. "
246
+ "Avoid naming the private data keys the same as the "
247
+ "parameters to avoid unexpected behavior.",
248
+ k,
249
+ )
250
+ self.data[k] = v.value
251
+
252
+ # If the result has a records member, we assume its length to be
253
+ # the number of records to check.
254
+ if "records" in result:
255
+ self.max_steps = len(result["records"])
256
+ return True
257
+ return False
258
+
259
+ def cleanup_task(self, ctx: "HasBasicContext") -> bool: # type: ignore
260
+ """Cleanup the task."""
261
+ return self.check.cleanup_check(ctx)
262
+
263
+ def get_failed_message(self, ctx: "HasTranslate") -> str:
264
+ return self.get_success_message(ctx)
265
+
266
+ def prepare_step(self, ctx: "HasTranslate") -> None:
267
+ """Prepare the task."""
268
+ self.results.append(CheckResult(check_id=self.check.check_id))
269
+
270
+ def handle_exception(
271
+ self,
272
+ ctx: "HasTranslate",
273
+ e: Exception,
274
+ stage: Literal["prepare", "execute", "cleanup"],
275
+ ) -> bool:
276
+ """Handle an exception that occurred during the task execution."""
277
+ if stage in ("prepare", "cleanup"):
278
+ return super().handle_exception(ctx, e, stage)
279
+
280
+ t_key = "check.failed.exception"
281
+ result = self.results[-1]
282
+ result.check_id = self.check.check_id
283
+ result.state = ResultState.FAILED
284
+ result.params["exception"] = str(e)
285
+ result.params["e_class"] = e.__class__.__name__
286
+ result.description = ctx.t(
287
+ t_key,
288
+ "The check failed to execute: {exception} ({e_class}).",
289
+ **result.params,
290
+ )
291
+ return True
292
+
293
+ def execute_step(self, ctx: "HasBasicContext") -> None: # type: ignore
294
+ """Execute one step in the task."""
295
+ item = self.check.get_record_to_check(self.step, self.data)
296
+ result = self.results[-1]
297
+ record_id = self.check.get_record_id(self.step, self.data, item)
298
+ result.params["id"] = record_id
299
+ result.issue_hash = self.check.compute_hash(self.step, self.data, item)
300
+ result = self.check.execute(ctx, item, result)
301
+ self.results[-1] = result
302
+
303
+
304
+ @define(slots=True, frozen=True, kw_only=True)
305
+ class Check(Generic[T]):
306
+ """A check that can be performed on a record or a record set.
307
+
308
+ Attributes:
309
+ check_id: A unique identifier for the check.
310
+ description: A description of the check.
311
+ detailed_description: A longer, beginner-friendly description of the
312
+ check, suitable for tooltips or help text.
313
+ category: The category of the check.
314
+ tags: The tags of the check.
315
+ is_global: Whether the check is global. A global check is one that
316
+ cannot iterate over the members so reporting the progress is
317
+ not possible.
318
+ has_fix: Whether this check provides an automatic fix for the problem
319
+ it detects. This does not mean that all issues will be fixed
320
+ or that the fix will be complete. It just means that the
321
+ class implements the fix method< wether a particular issue was
322
+ fixed or not will be reflected in the result.
323
+ parameters: Definition of the parameters that the check accepts
324
+ and their values.
325
+ """
326
+
327
+ check_id: str
328
+ title: str = field(default="", repr=False)
329
+ description: str = field(default="", repr=False)
330
+ detailed_description: str = field(default="", repr=False)
331
+ category: str = field(default="", repr=False)
332
+ tags: List[str] = field(factory=list, repr=False)
333
+ is_global: bool = field(default=False, repr=False)
334
+ has_fix: bool = field(default=False, repr=False)
335
+ parameters: Dict[str, "TaskParameter"] = field(
336
+ factory=OrderedDict, repr=False
337
+ )
338
+
339
+ def prepare_check(self, ctx: "HasBasicContext") -> Optional[Dict[str, Any]]:
340
+ """Prepare the check.
341
+
342
+ Returns a (possibly empty) dictionary of private data that
343
+ the check needs to store between the prepare and cleanup steps.
344
+
345
+ This is the best place to obtain and store the list of records that
346
+ will be checked.
347
+
348
+ If the result is None this indicates that the preparation step failed.
349
+ The check will be aborted and the user will be notified.
350
+ """
351
+ del ctx
352
+ return {}
353
+
354
+ def cleanup_check(self, ctx: "HasBasicContext") -> bool:
355
+ """Cleanup the check.
356
+
357
+ The result indicates whether the cleanup step was successful.
358
+ """
359
+ with ctx.same_session() as s:
360
+ s.commit()
361
+ return True
362
+
363
+ def get_record_to_check(self, step: int, data: Dict[str, Any]) -> T:
364
+ """Get the record to check out of the prepared data."""
365
+ raise NotImplementedError("Subclasses must implement this method.")
366
+
367
+ def get_record_id(self, step: int, data: Dict[str, Any], item: T) -> str:
368
+ """Get the record ID of the item."""
369
+ return str(item.id) # type: ignore
370
+
371
+ def compute_hash(self, step: int, data: Dict[str, Any], item: T) -> str:
372
+ """Compute the hash of the item.
373
+
374
+ The hash is used to track the changes of the item with respect
375
+ to this check through time.
376
+ """
377
+ del step, data, item
378
+ return ""
379
+
380
+ def execute(
381
+ self,
382
+ ctx: "HasBasicContext",
383
+ item: Union[T, List[T]],
384
+ result: Optional["CheckResult"] = None,
385
+ ) -> "CheckResult":
386
+ """Execute the check on the given item.
387
+
388
+ For global checks the `item` is a list of all the records that were
389
+ found for the query. For non-global checks the `item` is a single
390
+ record.
391
+
392
+ Args:
393
+ item: The item to check.
394
+ result: The result to update. If not provided a new result will be
395
+ created.
396
+
397
+ Returns:
398
+ The result of the check.
399
+ """
400
+ raise NotImplementedError
401
+
402
+ def create_task(self) -> CheckTask[T]:
403
+ return CheckTask[T](
404
+ check=self,
405
+ title=self.title,
406
+ description=self.description,
407
+ parameters=self.parameters,
408
+ )
409
+
410
+
411
+ PROJECT_NAME = "exdrf-checks"
412
+ exdrf_check_spec = pluggy.HookspecMarker(PROJECT_NAME)
413
+ exdrf_check_impl = pluggy.HookimplMarker(PROJECT_NAME)
414
+
415
+
416
+ class HookSpecs:
417
+ """Hook specifications for the check registry."""
418
+
419
+ @exdrf_check_spec
420
+ def exdrf_checks(ctx: "HasTranslate", for_gui: bool) -> List["Check"]:
421
+ """Get the checks that should be made available to the user.
422
+
423
+ Args:
424
+ ctx: The context of the application.
425
+ for_gui: Whether the checks are being requested for the GUI.
426
+ Some parameters will use this information to import
427
+ different data for the GUI and the CLI.
428
+ """
429
+ raise NotImplementedError
430
+
431
+
432
+ exdrf_checks_pm = pluggy.PluginManager(PROJECT_NAME)
433
+ exdrf_checks_pm.add_hookspecs(HookSpecs)
434
+
435
+
436
+ def get_all_checks(ctx: "HasTranslate", for_gui: bool) -> List["Check"]:
437
+ """Get all the checks that should be made available to the user.
438
+
439
+ For your check to be noticed by the registry you need to:
440
+
441
+ 1. Create a Check class that implements the check logic.
442
+ 2. Implement the hook using the @exdrf_check_impl decorator.
443
+ 3. Register your plugin with the plugin manager.
444
+
445
+ Example::
446
+
447
+ from exdrf_util.check import (
448
+ Check, CheckResult, ResultState, exdrf_check_impl, exdrf_checks_pm
449
+ )
450
+ from exdrf_util.typedefs import HasBasicContext
451
+
452
+ class MyCustomCheck(Check[str]):
453
+ \"\"\"A custom check that validates string length.\"\"\"
454
+
455
+ def __init__(self):
456
+ super().__init__(
457
+ check_id="my_custom_check",
458
+ title="Custom String Check",
459
+ description="Checks if string length is valid",
460
+ category="validation"
461
+ )
462
+
463
+ def execute(
464
+ self,
465
+ ctx: HasBasicContext,
466
+ item: str,
467
+ result: Optional[CheckResult] = None,
468
+ ) -> CheckResult:
469
+ if result is None:
470
+ result = CheckResult()
471
+
472
+ if len(item) < 5:
473
+ result.state = ResultState.NOT_FIXED
474
+ result.t_key = "check.string.too_short"
475
+ result.params = {"length": len(item), "min_length": 5}
476
+ result.description = (
477
+ f"String is too short: {len(item)} < 5"
478
+ )
479
+ else:
480
+ result.state = ResultState.PASSED
481
+
482
+ return result
483
+
484
+ class MyChecksPlugin:
485
+ \"\"\"Plugin that provides custom checks.\"\"\"
486
+
487
+ @exdrf_check_impl
488
+ def exdrf_checks(self) -> List[Check]:
489
+ \"\"\"Return the list of checks provided by this plugin.\"\"\"
490
+ return [MyCustomCheck()]
491
+
492
+ # Register the plugin
493
+ exdrf_checks_pm.register(MyChecksPlugin())
494
+ """
495
+ result = []
496
+ for hookimpl in exdrf_checks_pm.hook.exdrf_checks(ctx=ctx, for_gui=for_gui):
497
+ result.extend(hookimpl)
498
+ logger.debug(f"Found {len(result)} checks")
499
+ return result
@@ -0,0 +1,89 @@
1
+ """Pre-commit helper: ensure any .py file defining VERBOSE uses VERBOSE = 1.
2
+
3
+ Usage:
4
+ python -m exdrf_util.check_verbose [--fix] <file.py> ...
5
+ With --fix, rewrites VERBOSE = n to VERBOSE = 1 in place (autofix).
6
+ """
7
+
8
+ import re
9
+ import sys
10
+
11
+ # Match VERBOSE = <integer> at line start (optional leading/trailing
12
+ # space/comments).
13
+ VERBOSE_PATTERN = re.compile(
14
+ r"^\s*VERBOSE\s*=\s*(\d+)(?:\s*#.*)?\s*$", re.MULTILINE
15
+ )
16
+ # Capture up to the number for substitution (fix mode).
17
+ VERBOSE_FIX_PATTERN = re.compile(r"^(\s*VERBOSE\s*=\s*)\d+", re.MULTILINE)
18
+
19
+
20
+ def check_file(path: str) -> list[tuple[int, int]]:
21
+ """Find lines where VERBOSE is set to a value other than 1.
22
+
23
+ Args:
24
+ path: Path to a Python source file.
25
+
26
+ Returns:
27
+ List of (line_number, value) for each VERBOSE = x with x != 1.
28
+ Empty if file is ok or not readable.
29
+ """
30
+ try:
31
+ with open(path, encoding="utf-8-sig") as f:
32
+ text = f.read()
33
+ except OSError as e:
34
+ print("%s: could not read file: %s" % (path, e), file=sys.stderr)
35
+ return []
36
+
37
+ violations = []
38
+ for match in VERBOSE_PATTERN.finditer(text):
39
+ value = int(match.group(1))
40
+ if value != 1:
41
+ line_no = text[: match.start()].count("\n") + 1
42
+ violations.append((line_no, value))
43
+ return violations
44
+
45
+
46
+ def fix_file(path: str) -> bool:
47
+ """Rewrite VERBOSE = n to VERBOSE = 1 in place. Return True if changed."""
48
+ try:
49
+ with open(path, encoding="utf-8-sig") as f:
50
+ text = f.read()
51
+ except OSError as e:
52
+ print("%s: could not read file: %s" % (path, e), file=sys.stderr)
53
+ return False
54
+
55
+ new_text = VERBOSE_FIX_PATTERN.sub(r"\g<1>1", text)
56
+ if new_text == text:
57
+ return False
58
+ with open(path, "w", encoding="utf-8", newline="") as f:
59
+ f.write(new_text)
60
+ return True
61
+
62
+
63
+ def main() -> int:
64
+ """Run the check on files passed as arguments. Exit 1 if any violation."""
65
+ argv = sys.argv[1:]
66
+ fix_mode = bool(argv and argv[0] == "--fix")
67
+ if fix_mode:
68
+ argv = argv[1:]
69
+ paths = [p for p in argv if p.endswith(".py")]
70
+
71
+ if fix_mode:
72
+ for path in paths:
73
+ if fix_file(path):
74
+ print("%s: VERBOSE set to 1" % path)
75
+ return 0
76
+
77
+ failed = False
78
+ for path in paths:
79
+ for line_no, value in check_file(path):
80
+ print(
81
+ "%s:%d: VERBOSE = %d (must be VERBOSE = 1)"
82
+ % (path, line_no, value)
83
+ )
84
+ failed = True
85
+ return 1 if failed else 0
86
+
87
+
88
+ if __name__ == "__main__":
89
+ sys.exit(main())