crackerjack 0.14.3__py3-none-any.whl → 0.14.5__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.
@@ -17,7 +17,7 @@ repos:
17
17
  - id: check-added-large-files
18
18
  name: check-added-large-files
19
19
  - repo: https://github.com/astral-sh/ruff-pre-commit
20
- rev: v0.9.10
20
+ rev: v0.11.2
21
21
  hooks:
22
22
  - id: ruff-format
23
23
  - id: ruff
@@ -65,11 +65,11 @@ repos:
65
65
  - id: bandit
66
66
  args: ["-c", "pyproject.toml"]
67
67
  - repo: https://github.com/RobertCraigie/pyright-python
68
- rev: v1.1.396
68
+ rev: v1.1.397
69
69
  hooks:
70
70
  - id: pyright
71
71
  - repo: https://github.com/astral-sh/ruff-pre-commit
72
- rev: v0.9.10
72
+ rev: v0.11.2
73
73
  hooks:
74
74
  - id: ruff
75
75
  - id: ruff-format
@@ -5,13 +5,13 @@ import subprocess
5
5
  import tokenize
6
6
  import typing as t
7
7
  from contextlib import suppress
8
+ from dataclasses import dataclass, field
8
9
  from pathlib import Path
9
10
  from subprocess import CompletedProcess
10
11
  from subprocess import run as execute
11
12
  from token import STRING
12
13
  from tomllib import loads
13
14
 
14
- from pydantic import BaseModel
15
15
  from rich.console import Console
16
16
  from tomli_w import dumps
17
17
 
@@ -20,7 +20,8 @@ interactive_hooks = ("refurb", "bandit", "pyright")
20
20
  default_python_version = "3.13"
21
21
 
22
22
 
23
- class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
23
+ @dataclass
24
+ class CodeCleaner:
24
25
  console: Console
25
26
 
26
27
  def clean_files(self, pkg_dir: Path | None) -> None:
@@ -35,25 +36,25 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
35
36
  def clean_file(self, file_path: Path) -> None:
36
37
  try:
37
38
  if file_path.resolve() == Path(__file__).resolve():
38
- print(f"Skipping cleaning of {file_path} (self file).")
39
+ self.console.print(f"Skipping cleaning of {file_path} (self file).")
39
40
  return
40
41
  except Exception as e:
41
- print(f"Error comparing file paths: {e}")
42
+ self.console.print(f"Error comparing file paths: {e}")
42
43
  try:
43
44
  code = file_path.read_text()
44
45
  code = self.remove_docstrings(code)
45
46
  code = self.remove_line_comments(code)
46
47
  code = self.remove_extra_whitespace(code)
47
48
  code = self.reformat_code(code)
48
- file_path.write_text(code)
49
- print(f"Cleaned: {file_path}")
49
+ file_path.write_text(code) # type: ignore
50
+ self.console.print(f"Cleaned: {file_path}")
50
51
  except Exception as e:
51
- print(f"Error cleaning {file_path}: {e}")
52
+ self.console.print(f"Error cleaning {file_path}: {e}")
52
53
 
53
54
  def remove_line_comments(self, code: str) -> str:
54
55
  new_lines = []
55
56
  for line in code.splitlines():
56
- if "#" not in line or line.startswith("#"):
57
+ if "#" not in line:
57
58
  new_lines.append(line)
58
59
  continue
59
60
  idx = line.find("#")
@@ -66,31 +67,87 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
66
67
  new_lines.append(code_part)
67
68
  return "\n".join(new_lines)
68
69
 
70
+ def _is_triple_quoted(self, token_string: str) -> bool:
71
+ triple_quote_patterns = [
72
+ ('"""', '"""'),
73
+ ("'''", "'''"),
74
+ ('r"""', '"""'),
75
+ ("r'''", "'''"),
76
+ ]
77
+ return any(
78
+ token_string.startswith(start) and token_string.endswith(end)
79
+ for start, end in triple_quote_patterns
80
+ )
81
+
82
+ def _is_module_docstring(
83
+ self, tokens: list[tokenize.TokenInfo], i: int, indent_level: int
84
+ ) -> bool:
85
+ if i <= 0 or indent_level != 0:
86
+ return False
87
+ preceding_tokens = tokens[:i]
88
+ return not preceding_tokens
89
+
90
+ def _is_function_or_class_docstring(
91
+ self,
92
+ tokens: list[tokenize.TokenInfo],
93
+ i: int,
94
+ last_token_type: t.Any,
95
+ last_token_string: str,
96
+ ) -> bool:
97
+ if last_token_type != tokenize.OP or last_token_string != ":": # nosec B105
98
+ return False
99
+ for prev_idx in range(i - 1, max(0, i - 20), -1):
100
+ prev_token = tokens[prev_idx]
101
+ if prev_token[1] in ("def", "class") and prev_token[0] == tokenize.NAME:
102
+ return True
103
+ elif prev_token[0] == tokenize.DEDENT:
104
+ break
105
+ return False
106
+
107
+ def _is_variable_docstring(
108
+ self, tokens: list[tokenize.TokenInfo], i: int, indent_level: int
109
+ ) -> bool:
110
+ if indent_level <= 0:
111
+ return False
112
+ for prev_idx in range(i - 1, max(0, i - 10), -1):
113
+ if tokens[prev_idx][0]:
114
+ return True
115
+ return False
116
+
69
117
  def remove_docstrings(self, source: str) -> str:
70
118
  try:
71
119
  io_obj = io.StringIO(source)
72
- output_tokens = []
73
- first_token_stack = [True]
74
120
  tokens = list(tokenize.generate_tokens(io_obj.readline))
75
- for tok in tokens:
76
- token_type = tok.type
121
+ result_tokens = []
122
+ indent_level = 0
123
+ last_non_ws_token_type = None
124
+ last_non_ws_token_string = "" # nosec B105
125
+ for i, token in enumerate(tokens):
126
+ token_type, token_string, _, _, _ = token
77
127
  if token_type == tokenize.INDENT:
78
- first_token_stack.append(True)
128
+ indent_level += 1
79
129
  elif token_type == tokenize.DEDENT:
80
- if len(first_token_stack) > 1:
81
- first_token_stack.pop()
82
- if token_type == STRING and first_token_stack[-1]:
83
- first_token_stack[-1] = False
84
- continue
85
- else:
86
- if token_type not in (
87
- tokenize.NEWLINE,
88
- tokenize.NL,
89
- tokenize.COMMENT,
90
- ):
91
- first_token_stack[-1] = False
92
- output_tokens.append(tok)
93
- return tokenize.untokenize(output_tokens)
130
+ indent_level -= 1
131
+ if token_type == STRING and self._is_triple_quoted(token_string):
132
+ is_docstring = (
133
+ self._is_module_docstring(tokens, i, indent_level)
134
+ or self._is_function_or_class_docstring(
135
+ tokens, i, last_non_ws_token_type, last_non_ws_token_string
136
+ )
137
+ or self._is_variable_docstring(tokens, i, indent_level)
138
+ )
139
+ if is_docstring:
140
+ continue
141
+ if token_type not in (
142
+ tokenize.NL,
143
+ tokenize.NEWLINE,
144
+ tokenize.INDENT,
145
+ tokenize.DEDENT,
146
+ ):
147
+ last_non_ws_token_type = token_type
148
+ last_non_ws_token_string = token_string
149
+ result_tokens.append(token)
150
+ return tokenize.untokenize(result_tokens)
94
151
  except Exception as e:
95
152
  self.console.print(f"Error removing docstrings: {e}")
96
153
  return source
@@ -105,7 +162,7 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
105
162
  cleaned_lines.append(line)
106
163
  return "\n".join(cleaned_lines)
107
164
 
108
- def reformat_code(self, code: str) -> str:
165
+ def reformat_code(self, code: str) -> str | None:
109
166
  try:
110
167
  import tempfile
111
168
 
@@ -124,21 +181,22 @@ class CodeCleaner(BaseModel, arbitrary_types_allowed=True):
124
181
  if result.returncode == 0:
125
182
  formatted_code = temp_path.read_text()
126
183
  else:
127
- print(f"Ruff formatting failed: {result.stderr}")
184
+ self.console.print(f"Ruff formatting failed: {result.stderr}")
128
185
  formatted_code = code
129
186
  except Exception as e:
130
- print(f"Error running Ruff: {e}")
187
+ self.console.print(f"Error running Ruff: {e}")
131
188
  formatted_code = code
132
189
  finally:
133
190
  with suppress(FileNotFoundError):
134
191
  temp_path.unlink()
135
192
  return formatted_code
136
193
  except Exception as e:
137
- print(f"Error during reformatting: {e}")
194
+ self.console.print(f"Error during reformatting: {e}")
138
195
  return code
139
196
 
140
197
 
141
- class ConfigManager(BaseModel, arbitrary_types_allowed=True):
198
+ @dataclass
199
+ class ConfigManager:
142
200
  our_path: Path
143
201
  pkg_path: Path
144
202
  pkg_name: str
@@ -262,14 +320,15 @@ class ConfigManager(BaseModel, arbitrary_types_allowed=True):
262
320
  return execute(cmd, **kwargs)
263
321
 
264
322
 
265
- class ProjectManager(BaseModel, arbitrary_types_allowed=True):
323
+ @dataclass
324
+ class ProjectManager:
266
325
  our_path: Path
267
326
  pkg_path: Path
268
- pkg_dir: Path | None = None
269
- pkg_name: str = "crackerjack"
270
327
  console: Console
271
328
  code_cleaner: CodeCleaner
272
329
  config_manager: ConfigManager
330
+ pkg_dir: Path | None = None
331
+ pkg_name: str = "crackerjack"
273
332
  dry_run: bool = False
274
333
 
275
334
  def run_interactive(self, hook: str) -> None:
@@ -321,39 +380,42 @@ class ProjectManager(BaseModel, arbitrary_types_allowed=True):
321
380
  return execute(cmd, **kwargs)
322
381
 
323
382
 
324
- class Crackerjack(BaseModel, arbitrary_types_allowed=True):
325
- our_path: Path = Path(__file__).parent
326
- pkg_path: Path = Path(Path.cwd())
383
+ @dataclass
384
+ class Crackerjack:
385
+ our_path: Path = field(default_factory=lambda: Path(__file__).parent)
386
+ pkg_path: Path = field(default_factory=lambda: Path(Path.cwd()))
327
387
  pkg_dir: Path | None = None
328
388
  pkg_name: str = "crackerjack"
329
389
  python_version: str = default_python_version
330
- console: Console = Console(force_terminal=True)
390
+ console: Console = field(default_factory=lambda: Console(force_terminal=True))
331
391
  dry_run: bool = False
332
392
  code_cleaner: CodeCleaner | None = None
333
393
  config_manager: ConfigManager | None = None
334
394
  project_manager: ProjectManager | None = None
335
395
 
336
- def __init__(self, **data: t.Any) -> None:
337
- super().__init__(**data)
338
- self.code_cleaner = CodeCleaner(console=self.console)
339
- self.config_manager = ConfigManager(
340
- our_path=self.our_path,
341
- pkg_path=self.pkg_path,
342
- pkg_name=self.pkg_name,
343
- console=self.console,
344
- python_version=self.python_version,
345
- dry_run=self.dry_run,
346
- )
347
- self.project_manager = ProjectManager(
348
- our_path=self.our_path,
349
- pkg_path=self.pkg_path,
350
- pkg_dir=self.pkg_dir,
351
- pkg_name=self.pkg_name,
352
- console=self.console,
353
- code_cleaner=self.code_cleaner,
354
- config_manager=self.config_manager,
355
- dry_run=self.dry_run,
356
- )
396
+ def __post_init__(self) -> None:
397
+ if self.code_cleaner is None:
398
+ self.code_cleaner = CodeCleaner(console=self.console)
399
+ if self.config_manager is None:
400
+ self.config_manager = ConfigManager(
401
+ our_path=self.our_path,
402
+ pkg_path=self.pkg_path,
403
+ pkg_name=self.pkg_name,
404
+ console=self.console,
405
+ python_version=self.python_version,
406
+ dry_run=self.dry_run,
407
+ )
408
+ if self.project_manager is None:
409
+ self.project_manager = ProjectManager(
410
+ our_path=self.our_path,
411
+ pkg_path=self.pkg_path,
412
+ pkg_dir=self.pkg_dir,
413
+ pkg_name=self.pkg_name,
414
+ console=self.console,
415
+ code_cleaner=self.code_cleaner,
416
+ config_manager=self.config_manager,
417
+ dry_run=self.dry_run,
418
+ )
357
419
 
358
420
  def _setup_package(self) -> None:
359
421
  self.pkg_name = self.pkg_path.stem.lower().replace("-", "_")
@@ -1,6 +1,34 @@
1
1
  [tool.pytest.ini_options]
2
2
  addopts = "--cov=crackerjack"
3
3
  asyncio_default_fixture_loop_scope = "function"
4
+ python_files = ["test_*.py", "*_test.py"]
5
+ python_classes = "Test*"
6
+ python_functions = "test_*"
7
+ asyncio_mode = "auto"
8
+
9
+ [tool.coverage.run]
10
+ branch = true
11
+ source = ["crackerjack"]
12
+ omit = [
13
+ "*/tests/*",
14
+ "*/site-packages/*",
15
+ "*/__pycache__/*",
16
+ "*/__init__.py",
17
+ ]
18
+
19
+ [tool.coverage.report]
20
+ exclude_also = [
21
+ "pragma: no cover",
22
+ "def __repr__",
23
+ "raise NotImplementedError",
24
+ "if __name__ == .__main__.:",
25
+ "pass",
26
+ "raise ImportError",
27
+ "except ImportError",
28
+ "def __str__",
29
+ "@abstractmethod",
30
+ ]
31
+ ignore_errors = false
4
32
 
5
33
  [tool.codespell]
6
34
  skip = "*/data/*"
@@ -119,7 +147,7 @@ pythonPlatform = "Darwin"
119
147
 
120
148
  [project]
121
149
  name = "crackerjack"
122
- version = "0.14.2"
150
+ version = "0.14.4"
123
151
  description = "Default template for PDM package"
124
152
  requires-python = ">=3.13"
125
153
  readme = "README.md"
@@ -146,12 +174,12 @@ classifiers = [
146
174
  ]
147
175
  dependencies = [
148
176
  "autotyping>=24.9.0",
149
- "pre-commit>=4.1.0",
177
+ "pre-commit>=4.2.0",
150
178
  "pytest>=8.3.5",
151
179
  "pydantic>=2.10.6",
152
180
  "pdm-bump>=0.9.10",
153
181
  "pdm>=2.22.4",
154
- "uv>=0.6.6",
182
+ "uv>=0.6.9",
155
183
  "pytest-cov>=6.0.0",
156
184
  "pytest-mock>=3.14.0",
157
185
  "tomli-w>=1.2.0",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: crackerjack
3
- Version: 0.14.3
3
+ Version: 0.14.5
4
4
  Summary: Default template for PDM package
5
5
  Keywords: black,ruff,mypy,creosote,refurb
6
6
  Author-Email: lesleslie <les@wedgwoodwebworks.com>
@@ -23,12 +23,12 @@ Project-URL: documentation, https://github.com/lesleslie/crackerjack
23
23
  Project-URL: repository, https://github.com/lesleslie/crackerjack
24
24
  Requires-Python: >=3.13
25
25
  Requires-Dist: autotyping>=24.9.0
26
- Requires-Dist: pre-commit>=4.1.0
26
+ Requires-Dist: pre-commit>=4.2.0
27
27
  Requires-Dist: pytest>=8.3.5
28
28
  Requires-Dist: pydantic>=2.10.6
29
29
  Requires-Dist: pdm-bump>=0.9.10
30
30
  Requires-Dist: pdm>=2.22.4
31
- Requires-Dist: uv>=0.6.6
31
+ Requires-Dist: uv>=0.6.9
32
32
  Requires-Dist: pytest-cov>=6.0.0
33
33
  Requires-Dist: pytest-mock>=3.14.0
34
34
  Requires-Dist: tomli-w>=1.2.0
@@ -1,12 +1,12 @@
1
- crackerjack-0.14.3.dist-info/METADATA,sha256=d1KMIGQ4kl0qLVeLrcwufpSOg_IDVQAxSYmv23RN6Gk,11033
2
- crackerjack-0.14.3.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
- crackerjack-0.14.3.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
- crackerjack-0.14.3.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
1
+ crackerjack-0.14.5.dist-info/METADATA,sha256=kft0v_xwfOXdAYFhGrtaflsYXvnq5UfGLWJ2oZ7G6Js,11033
2
+ crackerjack-0.14.5.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
+ crackerjack-0.14.5.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
+ crackerjack-0.14.5.dist-info/licenses/LICENSE,sha256=fDt371P6_6sCu7RyqiZH_AhT1LdN3sN1zjBtqEhDYCk,1531
5
5
  crackerjack/.coverage,sha256=dLzPzp72qZEXohNfxnOAlRwvM9dqF06-HoFqfvXZd1U,53248
6
6
  crackerjack/.gitignore,sha256=l8ErBAypC3rI6N9lhc7ZMdOw87t0Tz69ZW5C6uj15Wg,214
7
7
  crackerjack/.libcst.codemod.yaml,sha256=a8DlErRAIPV1nE6QlyXPAzTOgkB24_spl2E9hphuf5s,772
8
8
  crackerjack/.pdm.toml,sha256=dZe44HRcuxxCFESGG8SZIjmc-cGzSoyK3Hs6t4NYA8w,23
9
- crackerjack/.pre-commit-config.yaml,sha256=cLIzulHerHmCHjo5UWA2JAMm5d5PjwGpIp5Lx-nL3FQ,2267
9
+ crackerjack/.pre-commit-config.yaml,sha256=rb_3cBRHFSzWYC1OW8mLkFC7A8Retb0zdyeIM5FUQ1I,2267
10
10
  crackerjack/.pytest_cache/.gitignore,sha256=Ptcxtl0GFQwTji2tsL4Gl1UIiKa0frjEXsya26i46b0,37
11
11
  crackerjack/.pytest_cache/CACHEDIR.TAG,sha256=N9yI75oKvt2-gQU6bdj9-xOvthMEXqHrSlyBWnSjveQ,191
12
12
  crackerjack/.pytest_cache/README.md,sha256=c_1vzN2ALEGaay2YPWwxc7fal1WKxLWJ7ewt_kQ9ua0,302
@@ -19,6 +19,7 @@ crackerjack/.ruff_cache/0.1.4/10355199064880463147,sha256=kmqNg5WySQYPeAqa5elfaV
19
19
  crackerjack/.ruff_cache/0.1.6/15140459877605758699,sha256=oQy5boAXeskdm5M0Abh_nyBtitWj5N5wtx_4gsDgu7c,248
20
20
  crackerjack/.ruff_cache/0.1.7/1790508110482614856,sha256=De7Puq32XF0925xrGehWSKX6cw5Wi2bpt1cnqh__f54,248
21
21
  crackerjack/.ruff_cache/0.1.9/17041001205004563469,sha256=tKP_k8HaHhQJyrHbDfJ93kM7vahjrU8cKQ1f_-OUzZY,248
22
+ crackerjack/.ruff_cache/0.11.2/4070660268492669020,sha256=5oEl9zpN_cL4MPdtF1_mLFYyeFT5Wp6UI4h5twWAZ04,224
22
23
  crackerjack/.ruff_cache/0.2.0/10047773857155985907,sha256=j9LNa_RQ4Plor7go1uTYgz17cEENKvZQ-dP6b9MX0ik,248
23
24
  crackerjack/.ruff_cache/0.2.1/8522267973936635051,sha256=u_aPBMibtAp_iYvLwR88GMAECMcIgHezxMyuapmU2P4,248
24
25
  crackerjack/.ruff_cache/0.2.2/18053836298936336950,sha256=Xb_ebP0pVuUfSqPEZKlhQ70so_vqkEfMYpuHQ06iR5U,248
@@ -41,13 +42,13 @@ crackerjack/.ruff_cache/0.7.1/285614542852677309,sha256=mOHKRzKoSvW-1sHtqI_LHWRt
41
42
  crackerjack/.ruff_cache/0.7.3/16061516852537040135,sha256=AWJR9gmaO7-wpv8mY1homuwI8CrMPI3VrnbXH-wRPlg,224
42
43
  crackerjack/.ruff_cache/0.8.4/16354268377385700367,sha256=Ksz4X8N6Z1i83N0vV1PxmBRlqgjrtzmDCOg7VBF4baQ,224
43
44
  crackerjack/.ruff_cache/0.9.10/12813592349865671909,sha256=6yRYi5XvzLtzUymRBm6-DozDE48eJtLmaLGSghgY3Wo,224
44
- crackerjack/.ruff_cache/0.9.10/923908772239632759,sha256=ZS7bKHVE-HdGkSqM231c0JwcelE8GskD5ymNEjZmal0,224
45
+ crackerjack/.ruff_cache/0.9.10/923908772239632759,sha256=XNakPJR4lyXTBP_I8A0Lpa8zo2WSQhbDnlRKD2lCNrI,224
45
46
  crackerjack/.ruff_cache/0.9.3/13948373885254993391,sha256=kGhtIkzPUtKAgvlKs3D8j4QM4qG8RhsHrmQJI69Sv3o,224
46
47
  crackerjack/.ruff_cache/0.9.9/12813592349865671909,sha256=tmr8_vhRD2OxsVuMfbJPdT9fDFX-d5tfC5U9jgziyho,224
47
48
  crackerjack/.ruff_cache/0.9.9/8843823720003377982,sha256=e4ymkXfQsUg5e_mtO34xTsaTvs1uA3_fI216Qq9qCAM,136
48
49
  crackerjack/.ruff_cache/CACHEDIR.TAG,sha256=WVMVbX4MVkpCclExbq8m-IcOZIOuIZf5FrYw5Pk-Ma4,43
49
50
  crackerjack/__init__.py,sha256=XTWW_XQkWR6dSydFSLg-T--eY3TPKUp4jUwZP11kgwY,142
50
51
  crackerjack/__main__.py,sha256=7SHrcRFYhMaSV0S-EG8q2w8udURwdvIsS7vi9AW4naU,3784
51
- crackerjack/crackerjack.py,sha256=KInDrWX--rXaL4EL7DYhceA6WiRDUOWgKC7rFS8VpIY,18951
52
- crackerjack/pyproject.toml,sha256=kcDxCcODGdFEYAjf42Oo7ufCa3skZJQeBIYUbda1i6A,3418
53
- crackerjack-0.14.3.dist-info/RECORD,,
52
+ crackerjack/crackerjack.py,sha256=u3ulfgYbnuPGT7shM_upeJUWO_bB4ReC693bKncvkWg,21247
53
+ crackerjack/pyproject.toml,sha256=Br1rZoz0srCo047TjrHgpuyUhcTw2AwbXCB61NEdmDI,3981
54
+ crackerjack-0.14.5.dist-info/RECORD,,