crackerjack 0.21.6__py3-none-any.whl → 0.21.7__py3-none-any.whl

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.
crackerjack/__init__.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import typing as t
2
-
3
2
  from .crackerjack import Crackerjack, create_crackerjack_runner
4
3
  from .errors import (
5
4
  CleaningError,
crackerjack/__main__.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from enum import Enum
2
-
3
2
  import typer
4
3
  from pydantic import BaseModel, field_validator
5
4
  from rich.console import Console
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  from subprocess import CompletedProcess
7
7
  from subprocess import run as execute
8
8
  from tomllib import loads
9
-
10
9
  from pydantic import BaseModel
11
10
  from rich.console import Console
12
11
  from tomli_w import dumps
@@ -86,55 +85,93 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
86
85
  except Exception as e:
87
86
  print(f"Error cleaning {file_path}: {e}")
88
87
 
89
- def remove_docstrings(self, code: str) -> str:
90
- lines = code.split("\n")
91
- cleaned_lines = []
92
- docstring_state = {
88
+ def _initialize_docstring_state(self) -> dict[str, t.Any]:
89
+ return {
93
90
  "in_docstring": False,
94
91
  "delimiter": None,
95
92
  "waiting": False,
96
93
  "function_indent": 0,
97
94
  "removed_docstring": False,
95
+ "in_multiline_def": False,
98
96
  }
99
- for i, line in enumerate(lines):
100
- stripped = line.strip()
101
- if self._is_function_or_class_definition(stripped):
102
- docstring_state["waiting"] = True
103
- docstring_state["function_indent"] = len(line) - len(line.lstrip())
104
- docstring_state["removed_docstring"] = False
105
- cleaned_lines.append(line)
106
- continue
107
- if docstring_state["waiting"] and stripped:
108
- if self._handle_docstring_start(stripped, docstring_state):
109
- if not docstring_state["in_docstring"]:
110
- if self._needs_pass_statement(
111
- lines, i + 1, docstring_state["function_indent"]
112
- ):
113
- pass_line = (
114
- " " * (docstring_state["function_indent"] + 4) + "pass"
115
- )
116
- cleaned_lines.append(pass_line)
117
- docstring_state["removed_docstring"] = True
118
- continue
119
- else:
120
- docstring_state["waiting"] = False
121
- if docstring_state["in_docstring"]:
122
- if self._handle_docstring_end(stripped, docstring_state):
123
- if self._needs_pass_statement(
124
- lines, i + 1, docstring_state["function_indent"]
125
- ):
126
- pass_line = (
127
- " " * (docstring_state["function_indent"] + 4) + "pass"
128
- )
129
- cleaned_lines.append(pass_line)
130
- docstring_state["removed_docstring"] = False
131
- continue
132
- else:
133
- continue
134
- if docstring_state["removed_docstring"] and stripped:
135
- docstring_state["removed_docstring"] = False
136
- cleaned_lines.append(line)
137
97
 
98
+ def _handle_function_definition(
99
+ self, line: str, stripped: str, state: dict[str, t.Any]
100
+ ) -> bool:
101
+ if self._is_function_or_class_definition(stripped):
102
+ state["waiting"] = True
103
+ state["function_indent"] = len(line) - len(line.lstrip())
104
+ state["removed_docstring"] = False
105
+ state["in_multiline_def"] = not stripped.endswith(":")
106
+ return True
107
+ return False
108
+
109
+ def _handle_multiline_definition(
110
+ self, line: str, stripped: str, state: dict[str, t.Any]
111
+ ) -> bool:
112
+ if state["in_multiline_def"]:
113
+ if stripped.endswith(":"):
114
+ state["in_multiline_def"] = False
115
+ return True
116
+ return False
117
+
118
+ def _handle_waiting_docstring(
119
+ self, lines: list[str], i: int, stripped: str, state: dict[str, t.Any]
120
+ ) -> tuple[bool, str | None]:
121
+ if state["waiting"] and stripped:
122
+ if self._handle_docstring_start(stripped, state):
123
+ pass_line = None
124
+ if not state["in_docstring"]:
125
+ function_indent: int = state["function_indent"]
126
+ if self._needs_pass_statement(lines, i + 1, function_indent):
127
+ pass_line = " " * (function_indent + 4) + "pass"
128
+ state["removed_docstring"] = True
129
+ return True, pass_line
130
+ else:
131
+ state["waiting"] = False
132
+ return False, None
133
+
134
+ def _handle_docstring_content(
135
+ self, lines: list[str], i: int, stripped: str, state: dict[str, t.Any]
136
+ ) -> tuple[bool, str | None]:
137
+ if state["in_docstring"]:
138
+ if self._handle_docstring_end(stripped, state):
139
+ pass_line = None
140
+ function_indent: int = state["function_indent"]
141
+ if self._needs_pass_statement(lines, i + 1, function_indent):
142
+ pass_line = " " * (function_indent + 4) + "pass"
143
+ state["removed_docstring"] = False
144
+ return True, pass_line
145
+ else:
146
+ return True, None
147
+ return False, None
148
+
149
+ def _process_line(
150
+ self, lines: list[str], i: int, line: str, state: dict[str, t.Any]
151
+ ) -> tuple[bool, str | None]:
152
+ stripped = line.strip()
153
+ if self._handle_function_definition(line, stripped, state):
154
+ return True, line
155
+ if self._handle_multiline_definition(line, stripped, state):
156
+ return True, line
157
+ handled, pass_line = self._handle_waiting_docstring(lines, i, stripped, state)
158
+ if handled:
159
+ return True, pass_line
160
+ handled, pass_line = self._handle_docstring_content(lines, i, stripped, state)
161
+ if handled:
162
+ return True, pass_line
163
+ if state["removed_docstring"] and stripped:
164
+ state["removed_docstring"] = False
165
+ return False, line
166
+
167
+ def remove_docstrings(self, code: str) -> str:
168
+ lines = code.split("\n")
169
+ cleaned_lines = []
170
+ docstring_state = self._initialize_docstring_state()
171
+ for i, line in enumerate(lines):
172
+ _, result_line = self._process_line(lines, i, line, docstring_state)
173
+ if result_line:
174
+ cleaned_lines.append(result_line)
138
175
  return "\n".join(cleaned_lines)
139
176
 
140
177
  def _is_function_or_class_definition(self, stripped_line: str) -> bool:
@@ -177,9 +214,8 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
177
214
  line_indent = len(line) - len(line.lstrip())
178
215
  if line_indent <= function_indent:
179
216
  return True
180
- if line_indent == function_indent + 4:
217
+ if line_indent > function_indent:
181
218
  return False
182
-
183
219
  return True
184
220
 
185
221
  def remove_line_comments(self, code: str) -> str:
@@ -192,7 +228,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
192
228
  cleaned_line = self._process_line_for_comments(line)
193
229
  if cleaned_line or not line.strip():
194
230
  cleaned_lines.append(cleaned_line or line)
195
-
196
231
  return "\n".join(cleaned_lines)
197
232
 
198
233
  def _process_line_for_comments(self, line: str) -> str:
@@ -205,7 +240,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
205
240
  break
206
241
  else:
207
242
  result.append(char)
208
-
209
243
  return "".join(result).rstrip()
210
244
 
211
245
  def _handle_string_character(
@@ -216,18 +250,14 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
216
250
  string_state: dict[str, t.Any],
217
251
  result: list[str],
218
252
  ) -> bool:
219
- """Handle string quote characters. Returns True if character was handled."""
220
253
  if char not in ("'", '"'):
221
254
  return False
222
-
223
255
  if index > 0 and line[index - 1] == "\\":
224
256
  return False
225
-
226
257
  if string_state["in_string"] is None:
227
258
  string_state["in_string"] = char
228
259
  elif string_state["in_string"] == char:
229
260
  string_state["in_string"] = None
230
-
231
261
  result.append(char)
232
262
  return True
233
263
 
@@ -239,14 +269,11 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
239
269
  string_state: dict[str, t.Any],
240
270
  result: list[str],
241
271
  ) -> bool:
242
- """Handle comment character. Returns True if comment was found."""
243
272
  if char != "#" or string_state["in_string"] is not None:
244
273
  return False
245
-
246
274
  comment = line[index:].strip()
247
275
  if self._is_special_comment_line(comment):
248
276
  result.append(line[index:])
249
-
250
277
  return True
251
278
 
252
279
  def _is_special_comment_line(self, comment: str) -> bool:
@@ -270,13 +297,11 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
270
297
  ):
271
298
  continue
272
299
  cleaned_lines.append(line)
273
-
274
300
  return "\n".join(self._remove_trailing_empty_lines(cleaned_lines))
275
301
 
276
302
  def _update_function_state(
277
303
  self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
278
304
  ) -> None:
279
- """Update function tracking state based on current line."""
280
305
  if stripped_line.startswith(("def ", "async def ")):
281
306
  function_tracker["in_function"] = True
282
307
  function_tracker["function_indent"] = len(line) - len(stripped_line)
@@ -287,7 +312,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
287
312
  def _is_function_end(
288
313
  self, line: str, stripped_line: str, function_tracker: dict[str, t.Any]
289
314
  ) -> bool:
290
- """Check if current line marks the end of a function."""
291
315
  return (
292
316
  function_tracker["in_function"]
293
317
  and bool(line)
@@ -302,13 +326,10 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
302
326
  cleaned_lines: list[str],
303
327
  function_tracker: dict[str, t.Any],
304
328
  ) -> bool:
305
- """Determine if an empty line should be skipped."""
306
329
  if line_idx > 0 and cleaned_lines and (not cleaned_lines[-1]):
307
330
  return True
308
-
309
331
  if function_tracker["in_function"]:
310
332
  return self._should_skip_function_empty_line(line_idx, lines)
311
-
312
333
  return False
313
334
 
314
335
  def _should_skip_function_empty_line(self, line_idx: int, lines: list[str]) -> bool:
@@ -323,7 +344,6 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
323
344
  return True
324
345
  if next_line in ("pass", "break", "continue", "raise"):
325
346
  return True
326
-
327
347
  return self._is_special_comment(next_line)
328
348
 
329
349
  def _is_special_comment(self, line: str) -> bool:
@@ -463,15 +483,12 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
463
483
  for tool, settings in our_toml_config.get("tool", {}).items():
464
484
  if tool not in pkg_toml_config["tool"]:
465
485
  pkg_toml_config["tool"][tool] = {}
466
-
467
486
  pkg_tool_config = pkg_toml_config["tool"][tool]
468
-
469
487
  self._merge_tool_config(settings, pkg_tool_config, tool)
470
488
 
471
489
  def _merge_tool_config(
472
490
  self, our_config: dict[str, t.Any], pkg_config: dict[str, t.Any], tool: str
473
491
  ) -> None:
474
- """Recursively merge tool configuration, preserving existing project settings."""
475
492
  for setting, value in our_config.items():
476
493
  if isinstance(value, dict):
477
494
  self._merge_nested_config(setting, value, pkg_config)
@@ -481,21 +498,17 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
481
498
  def _merge_nested_config(
482
499
  self, setting: str, value: dict[str, t.Any], pkg_config: dict[str, t.Any]
483
500
  ) -> None:
484
- """Handle nested configuration merging."""
485
501
  if setting not in pkg_config:
486
502
  pkg_config[setting] = {}
487
503
  elif not isinstance(pkg_config[setting], dict):
488
504
  pkg_config[setting] = {}
489
-
490
505
  self._merge_tool_config(value, pkg_config[setting], "")
491
-
492
506
  for k, v in value.items():
493
507
  self._merge_nested_value(k, v, pkg_config[setting])
494
508
 
495
509
  def _merge_nested_value(
496
510
  self, key: str, value: t.Any, nested_config: dict[str, t.Any]
497
511
  ) -> None:
498
- """Merge individual nested values."""
499
512
  if isinstance(value, str | list) and "crackerjack" in str(value):
500
513
  nested_config[key] = self.swap_package_name(value)
501
514
  elif self._is_mergeable_list(key, value):
@@ -510,7 +523,6 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
510
523
  def _merge_direct_config(
511
524
  self, setting: str, value: t.Any, pkg_config: dict[str, t.Any]
512
525
  ) -> None:
513
- """Handle direct configuration merging."""
514
526
  if isinstance(value, str | list) and "crackerjack" in str(value):
515
527
  pkg_config[setting] = self.swap_package_name(value)
516
528
  elif self._is_mergeable_list(setting, value):
@@ -773,7 +785,6 @@ class Crackerjack(BaseModel, arbitrary_types_allowed=True):
773
785
  self._add_benchmark_flags(test, options)
774
786
  else:
775
787
  self._add_worker_flags(test, options, project_size)
776
-
777
788
  return test
778
789
 
779
790
  def _detect_project_size(self) -> str:
crackerjack/errors.py CHANGED
@@ -2,7 +2,6 @@ import sys
2
2
  import typing as t
3
3
  from enum import Enum
4
4
  from pathlib import Path
5
-
6
5
  from rich.console import Console
7
6
  from rich.panel import Panel
8
7
 
@@ -2,7 +2,6 @@ import time
2
2
  import typing as t
3
3
  from enum import Enum, auto
4
4
  from pathlib import Path
5
-
6
5
  from rich.box import ROUNDED
7
6
  from rich.console import Console
8
7
  from rich.layout import Layout
@@ -19,7 +18,6 @@ from rich.prompt import Confirm, Prompt
19
18
  from rich.table import Table
20
19
  from rich.text import Text
21
20
  from rich.tree import Tree
22
-
23
21
  from .errors import CrackerjackError, ErrorCode, handle_error
24
22
 
25
23
 
@@ -289,7 +287,6 @@ class InteractiveCLI:
289
287
  )
290
288
  total_tasks = len(self.workflow.tasks)
291
289
  progress_task = progress.add_task("Running workflow", total=total_tasks)
292
-
293
290
  return {
294
291
  "progress": progress,
295
292
  "progress_task": progress_task,
@@ -299,14 +296,11 @@ class InteractiveCLI:
299
296
  def _execute_workflow_loop(
300
297
  self, layout: Layout, progress_tracker: dict[str, t.Any], live: Live
301
298
  ) -> None:
302
- """Execute the main workflow loop."""
303
299
  while not self.workflow.all_tasks_completed():
304
300
  layout["tasks"].update(self.show_task_table())
305
301
  next_task = self.workflow.get_next_task()
306
-
307
302
  if not next_task:
308
303
  break
309
-
310
304
  if self._should_execute_task(layout, next_task, live):
311
305
  self._execute_task(layout, next_task, progress_tracker)
312
306
  else:
@@ -322,20 +316,16 @@ class InteractiveCLI:
322
316
  def _execute_task(
323
317
  self, layout: Layout, task: Task, progress_tracker: dict[str, t.Any]
324
318
  ) -> None:
325
- """Execute a single task and update progress."""
326
319
  task.start()
327
320
  layout["details"].update(self.show_task_status(task))
328
321
  time.sleep(1)
329
-
330
322
  success = self._simulate_task_execution()
331
-
332
323
  if success:
333
324
  task.complete()
334
325
  progress_tracker["completed_tasks"] += 1
335
326
  else:
336
327
  error = self._create_task_error(task.name)
337
328
  task.fail(error)
338
-
339
329
  progress_tracker["progress"].update(
340
330
  progress_tracker["progress_task"],
341
331
  completed=progress_tracker["completed_tasks"],
@@ -4,7 +4,7 @@ requires = [ "pdm-backend" ]
4
4
 
5
5
  [project]
6
6
  name = "crackerjack"
7
- version = "0.21.5"
7
+ version = "0.21.6"
8
8
  description = "Crackerjack: code quality toolkit"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: crackerjack
3
- Version: 0.21.6
3
+ Version: 0.21.7
4
4
  Summary: Crackerjack: code quality toolkit
5
5
  Keywords: bandit,black,creosote,mypy,pyright,pytest,refurb,ruff
6
6
  Author-Email: lesleslie <les@wedgwoodwebworks.com>
@@ -102,7 +102,7 @@ If you're new to Crackerjack, follow these steps:
102
102
 
103
103
  Or use the interactive Rich UI:
104
104
  ```
105
- python -m crackerjack --rich-ui
105
+ python -m crackerjack --interactive
106
106
  ```
107
107
 
108
108
  ---
@@ -269,7 +269,7 @@ python -m crackerjack -t --benchmark-regression --benchmark-regression-threshold
269
269
 
270
270
  Or with the interactive Rich UI:
271
271
  ```
272
- python -m crackerjack --rich-ui
272
+ python -m crackerjack --interactive
273
273
  ```
274
274
 
275
275
  ## Usage
@@ -361,7 +361,7 @@ runner.process(MyOptions())
361
361
  - `--benchmark-regression`: Fail tests if benchmarks regress beyond threshold.
362
362
  - `--benchmark-regression-threshold`: Set threshold percentage for benchmark regression (default 5.0%).
363
363
  - `-a`, `--all`: Run with `-x -t -p <micro|minor|major> -c` development options.
364
- - `--rich-ui`: Enable the interactive Rich UI for a more user-friendly experience with visual progress tracking and interactive prompts.
364
+ - `--interactive`: Enable the interactive Rich UI for a more user-friendly experience with visual progress tracking and interactive prompts.
365
365
  - `--ai-agent`: Enable AI agent mode with structured output (see [AI Agent Integration](#ai-agent-integration)).
366
366
  - `--help`: Display help.
367
367
 
@@ -461,7 +461,7 @@ runner.process(MyOptions())
461
461
 
462
462
  - **Rich Interactive Mode** - Run with the interactive Rich UI:
463
463
  ```bash
464
- python -m crackerjack --rich-ui
464
+ python -m crackerjack --interactive
465
465
  ```
466
466
 
467
467
  - **AI Integration** - Run with structured output for AI tools:
@@ -500,10 +500,10 @@ Crackerjack now offers an enhanced interactive experience through its Rich UI:
500
500
  - **Error Visualization:** Errors are presented in a structured, easy-to-understand format with recovery suggestions
501
501
  - **File Selection:** Interactive file browser for operations that require selecting files
502
502
 
503
- To use the Rich UI, run Crackerjack with the `--rich-ui` flag:
503
+ To use the Rich UI, run Crackerjack with the `--interactive` flag:
504
504
 
505
505
  ```bash
506
- python -m crackerjack --rich-ui
506
+ python -m crackerjack --interactive
507
507
  ```
508
508
 
509
509
  This launches an interactive terminal interface where you can:
@@ -542,7 +542,7 @@ python -m crackerjack -v
542
542
  For the most comprehensive error details with visual formatting, combine verbose mode with the Rich UI:
543
543
 
544
544
  ```bash
545
- python -m crackerjack --rich-ui -v
545
+ python -m crackerjack --interactive -v
546
546
  ```
547
547
 
548
548
  ## Python 3.13+ Features
@@ -1,7 +1,7 @@
1
- crackerjack-0.21.6.dist-info/METADATA,sha256=YcmoMu7VODbnvoQf63uApMapVEOpKbHjdeFs0Qutza4,26414
2
- crackerjack-0.21.6.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
- crackerjack-0.21.6.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
- crackerjack-0.21.6.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
1
+ crackerjack-0.21.7.dist-info/METADATA,sha256=suf-qK6MQQTYL6v8ZXcTUT70x6ADX6u1Dd8YvhQSV60,26442
2
+ crackerjack-0.21.7.dist-info/WHEEL,sha256=tSfRZzRHthuv7vxpI4aehrdN9scLjk-dCJkPLzkHxGg,90
3
+ crackerjack-0.21.7.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
+ crackerjack-0.21.7.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
5
5
  crackerjack/.gitignore,sha256=n8cD6U16L3XZn__PvhYm_F7-YeFHFucHCyxWj2NZCGs,259
6
6
  crackerjack/.libcst.codemod.yaml,sha256=a8DlErRAIPV1nE6QlyXPAzTOgkB24_spl2E9hphuf5s,772
7
7
  crackerjack/.pdm.toml,sha256=dZe44HRcuxxCFESGG8SZIjmc-cGzSoyK3Hs6t4NYA8w,23
@@ -34,7 +34,7 @@ crackerjack/.ruff_cache/0.11.7/10386934055395314831,sha256=lBNwN5zAgM4OzbkXIOzCc
34
34
  crackerjack/.ruff_cache/0.11.7/3557596832929915217,sha256=fKlwUbsvT3YIKV6UR-aA_i64lLignWeVfVu-MMmVbU0,207
35
35
  crackerjack/.ruff_cache/0.11.8/530407680854991027,sha256=xAMAL3Vu_HR6M-h5ojCTaak0By5ii8u-14pXULLgLqw,224
36
36
  crackerjack/.ruff_cache/0.12.0/5056746222905752453,sha256=MqrIT5qymJcgAOBZyn-TvYoGCFfDFCgN9IwSULq8n14,256
37
- crackerjack/.ruff_cache/0.12.1/5056746222905752453,sha256=ksDcvUi9t_w2SUbEhH8Yk-ArZbLaWKIkwnKqJ0Q9ceA,256
37
+ crackerjack/.ruff_cache/0.12.1/5056746222905752453,sha256=CHy2IV23GK0EauutmCmddO7Ck-fHMzbvpvZ1ZO9Cp8Y,256
38
38
  crackerjack/.ruff_cache/0.2.0/10047773857155985907,sha256=j9LNa_RQ4Plor7go1uTYgz17cEENKvZQ-dP6b9MX0ik,248
39
39
  crackerjack/.ruff_cache/0.2.1/8522267973936635051,sha256=u_aPBMibtAp_iYvLwR88GMAECMcIgHezxMyuapmU2P4,248
40
40
  crackerjack/.ruff_cache/0.2.2/18053836298936336950,sha256=Xb_ebP0pVuUfSqPEZKlhQ70so_vqkEfMYpuHQ06iR5U,248
@@ -62,11 +62,11 @@ crackerjack/.ruff_cache/0.9.3/13948373885254993391,sha256=kGhtIkzPUtKAgvlKs3D8j4
62
62
  crackerjack/.ruff_cache/0.9.9/12813592349865671909,sha256=tmr8_vhRD2OxsVuMfbJPdT9fDFX-d5tfC5U9jgziyho,224
63
63
  crackerjack/.ruff_cache/0.9.9/8843823720003377982,sha256=e4ymkXfQsUg5e_mtO34xTsaTvs1uA3_fI216Qq9qCAM,136
64
64
  crackerjack/.ruff_cache/CACHEDIR.TAG,sha256=WVMVbX4MVkpCclExbq8m-IcOZIOuIZf5FrYw5Pk-Ma4,43
65
- crackerjack/__init__.py,sha256=8tBSPAru_YDuPpjz05cL7pNbZjYFoRT_agGd_FWa3gY,839
66
- crackerjack/__main__.py,sha256=AknITUlFjq3YUK9s2xeL62dM0GN82JBQyDkPzQ_hCUg,6561
67
- crackerjack/crackerjack.py,sha256=p5079e4I_pMHKiTFm0ckiuKJ1jvYCq8Na_0Vk-R12xA,35155
68
- crackerjack/errors.py,sha256=QEPtVuMtKtQHuawgr1ToMaN1KbUg5h9-4mS33YB5Znk,4062
69
- crackerjack/interactive.py,sha256=5KXKSvWKttLvHcI1L4VEDc3Rb-ZpHBOl_Qr7lhD-O4Q,16262
65
+ crackerjack/__init__.py,sha256=LyIPEfjBcqWp7UVuJ-b7cc8oiFxrQqyL8RJ7g6c17Qs,838
66
+ crackerjack/__main__.py,sha256=GP5GBbWK5lDTQanCHKVEyS3hDWvshwlrhWagzQlRuD4,6560
67
+ crackerjack/crackerjack.py,sha256=jlepiHgMkvTyl83O8HUqC4mCsQbZIejerOHNQD15JcU,35854
68
+ crackerjack/errors.py,sha256=xvXO3bk0VrxQz0lysIuVOju6SxxDgf04Wne1BedLFB8,4061
69
+ crackerjack/interactive.py,sha256=ierA2jeUsigTJhKOcJJsjhBZm2WwsQpvow3Y5ppF7IE,16151
70
70
  crackerjack/py313.py,sha256=buYE7LO11Q64ffowEhTZRFQoAGj_8sg3DTlZuv8M9eo,5890
71
- crackerjack/pyproject.toml,sha256=w4ssNTiRx9zQqQuywBGMIISalKLwg59DPSza9eGXuGk,4988
72
- crackerjack-0.21.6.dist-info/RECORD,,
71
+ crackerjack/pyproject.toml,sha256=OIoNmIktC1abFPgePb95Zg9BDTGfRxXftK-KccWgalc,4988
72
+ crackerjack-0.21.7.dist-info/RECORD,,