crackerjack 0.20.2__tar.gz → 0.20.7__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 (86) hide show
  1. {crackerjack-0.20.2 → crackerjack-0.20.7}/PKG-INFO +2 -1
  2. crackerjack-0.20.7/crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
  3. crackerjack-0.20.7/crackerjack/.ruff_cache/0.11.13/4240757255861806333 +0 -0
  4. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/crackerjack.py +245 -45
  5. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/pyproject.toml +3 -1
  6. {crackerjack-0.20.2 → crackerjack-0.20.7}/pyproject.toml +3 -1
  7. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_crackerjack.py +270 -49
  8. crackerjack-0.20.2/crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
  9. {crackerjack-0.20.2 → crackerjack-0.20.7}/LICENSE +0 -0
  10. {crackerjack-0.20.2 → crackerjack-0.20.7}/README.md +0 -0
  11. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.gitignore +0 -0
  12. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.libcst.codemod.yaml +0 -0
  13. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.pdm.toml +0 -0
  14. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.pre-commit-config.yaml +0 -0
  15. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.pytest_cache/.gitignore +0 -0
  16. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.pytest_cache/CACHEDIR.TAG +0 -0
  17. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.pytest_cache/README.md +0 -0
  18. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.pytest_cache/v/cache/nodeids +0 -0
  19. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.pytest_cache/v/cache/stepwise +0 -0
  20. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/.gitignore +0 -0
  21. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
  22. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
  23. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
  24. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
  25. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
  26. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
  27. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.11/18187162184424859798 +0 -0
  28. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.12/16869036553936192448 +0 -0
  29. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.12/1867267426380906393 +0 -0
  30. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.12/4240757255861806333 +0 -0
  31. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.12/4441409093023629623 +0 -0
  32. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
  33. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
  34. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
  35. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
  36. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
  37. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
  38. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
  39. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
  40. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
  41. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
  42. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
  43. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
  44. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
  45. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
  46. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
  47. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
  48. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
  49. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
  50. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
  51. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
  52. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
  53. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
  54. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
  55. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
  56. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
  57. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
  58. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
  59. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
  60. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
  61. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
  62. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
  63. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
  64. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
  65. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/.ruff_cache/CACHEDIR.TAG +0 -0
  66. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/__init__.py +0 -0
  67. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/__main__.py +0 -0
  68. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/errors.py +0 -0
  69. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/interactive.py +0 -0
  70. {crackerjack-0.20.2 → crackerjack-0.20.7}/crackerjack/py313.py +0 -0
  71. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/TESTING.md +0 -0
  72. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/__init__.py +0 -0
  73. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/conftest.py +0 -0
  74. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/data/comments_sample.txt +0 -0
  75. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/data/docstrings_sample.txt +0 -0
  76. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/data/expected_comments_sample.txt +0 -0
  77. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/data/init.py +0 -0
  78. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_crackerjack_runner.py +0 -0
  79. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_errors.py +0 -0
  80. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_interactive.py +0 -0
  81. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_interactive_run.py +0 -0
  82. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_main.py +0 -0
  83. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_py313_advanced.py +0 -0
  84. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_py313_features.py +0 -0
  85. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_pytest_features.py +0 -0
  86. {crackerjack-0.20.2 → crackerjack-0.20.7}/tests/test_structured_errors.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: crackerjack
3
- Version: 0.20.2
3
+ Version: 0.20.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>
@@ -23,6 +23,7 @@ Project-URL: homepage, 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
26
+ Requires-Dist: keyring>=25.6
26
27
  Requires-Dist: pdm>=2.24.2
27
28
  Requires-Dist: pdm-bump>=0.9.12
28
29
  Requires-Dist: pre-commit>=4.2
@@ -1017,40 +1017,33 @@ class Crackerjack:
1017
1017
  self.execute_command(["pdm", "bump", option])
1018
1018
  break
1019
1019
 
1020
- def _publish_project(self, options: OptionsProtocol) -> None:
1020
+ def _ensure_keyring_installed(self, options: OptionsProtocol) -> None:
1021
+ """Ensure keyring is installed for PDM on macOS."""
1021
1022
  from .errors import ErrorCode, PublishError, handle_error
1022
1023
 
1023
- if options.publish:
1024
- if platform.system() == "Darwin":
1025
- authorize = self.execute_command(
1026
- ["pdm", "self", "add", "keyring"], capture_output=True, text=True
1027
- )
1028
- if authorize.returncode > 0:
1029
- error = PublishError(
1030
- message="Authentication setup failed",
1031
- error_code=ErrorCode.AUTHENTICATION_ERROR,
1032
- details=f"Failed to add keyring support to PDM.\nCommand output:\n{authorize.stderr}",
1033
- recovery="Please manually add your keyring credentials to PDM. Run `pdm self add keyring` and try again.",
1034
- exit_code=1,
1035
- )
1036
- handle_error(
1037
- error=error,
1038
- console=self.console,
1039
- verbose=options.verbose,
1040
- ai_agent=options.ai_agent,
1041
- )
1024
+ if platform.system() != "Darwin":
1025
+ return
1042
1026
 
1043
- build = self.execute_command(
1044
- ["pdm", "build"], capture_output=True, text=True
1027
+ # Check if keyring is already installed in PDM
1028
+ check_keyring = self.execute_command(
1029
+ ["pdm", "self", "list"], capture_output=True, text=True
1030
+ )
1031
+ keyring_installed = "keyring" in check_keyring.stdout
1032
+
1033
+ if not keyring_installed:
1034
+ # Only attempt to install keyring if it's not already installed
1035
+ self.console.print("Installing keyring for PDM...")
1036
+ authorize = self.execute_command(
1037
+ ["pdm", "self", "add", "keyring"],
1038
+ capture_output=True,
1039
+ text=True,
1045
1040
  )
1046
- self.console.print(build.stdout)
1047
-
1048
- if build.returncode > 0:
1041
+ if authorize.returncode > 0:
1049
1042
  error = PublishError(
1050
- message="Package build failed",
1051
- error_code=ErrorCode.BUILD_ERROR,
1052
- details=f"Command output:\n{build.stderr}",
1053
- recovery="Review the error message above for details. Common issues include missing dependencies, invalid project structure, or incorrect metadata in pyproject.toml.",
1043
+ message="Authentication setup failed",
1044
+ error_code=ErrorCode.AUTHENTICATION_ERROR,
1045
+ details=f"Failed to add keyring support to PDM.\nCommand output:\n{authorize.stderr}",
1046
+ recovery="Please manually add your keyring credentials to PDM. Run `pdm self add keyring` and try again.",
1054
1047
  exit_code=1,
1055
1048
  )
1056
1049
  handle_error(
@@ -1060,26 +1053,233 @@ class Crackerjack:
1060
1053
  ai_agent=options.ai_agent,
1061
1054
  )
1062
1055
 
1063
- publish_result = self.execute_command(
1064
- ["pdm", "publish", "--no-build"], capture_output=True, text=True
1056
+ def _build_package(self, options: OptionsProtocol) -> None:
1057
+ """Build the package using PDM."""
1058
+ from .errors import ErrorCode, PublishError, handle_error
1059
+
1060
+ build = self.execute_command(["pdm", "build"], capture_output=True, text=True)
1061
+ self.console.print(build.stdout)
1062
+
1063
+ if build.returncode > 0:
1064
+ error = PublishError(
1065
+ message="Package build failed",
1066
+ error_code=ErrorCode.BUILD_ERROR,
1067
+ details=f"Command output:\n{build.stderr}",
1068
+ recovery="Review the error message above for details. Common issues include missing dependencies, invalid project structure, or incorrect metadata in pyproject.toml.",
1069
+ exit_code=1,
1070
+ )
1071
+ handle_error(
1072
+ error=error,
1073
+ console=self.console,
1074
+ verbose=options.verbose,
1075
+ ai_agent=options.ai_agent,
1065
1076
  )
1066
1077
 
1067
- if publish_result.returncode > 0:
1068
- error = PublishError(
1069
- message="Package publication failed",
1070
- error_code=ErrorCode.PUBLISH_ERROR,
1071
- details=f"Command output:\n{publish_result.stderr}",
1072
- recovery="Ensure you have the correct PyPI credentials configured. Check your internet connection and that the package name is available on PyPI.",
1073
- exit_code=1,
1078
+ def _prepare_publish_command(self) -> list[str]:
1079
+ """Prepare the PDM publish command with appropriate flags."""
1080
+ # Prepare the publish command
1081
+ publish_cmd = ["pdm", "publish", "--no-build"]
1082
+
1083
+ # Check if we're running in a CI environment
1084
+ is_ci = any(env in os.environ for env in ("CI", "GITHUB_ACTIONS", "GITLAB_CI"))
1085
+
1086
+ # If in CI environment, check if OIDC is likely to be the issue
1087
+ if is_ci:
1088
+ self.console.print("[yellow]Detected CI environment[/yellow]")
1089
+
1090
+ # Check for required OIDC-related environment variables
1091
+ if "GITHUB_ACTIONS" not in os.environ and "GITLAB_CI" not in os.environ:
1092
+ self.console.print(
1093
+ "[yellow]Non-GitHub/GitLab CI environment detected[/yellow]"
1074
1094
  )
1075
- handle_error(
1076
- error=error,
1077
- console=self.console,
1078
- verbose=options.verbose,
1079
- ai_agent=options.ai_agent,
1095
+ # Don't add --no-oidc flag as it may not be supported by all PDM versions
1096
+ elif "ACTIONS_ID_TOKEN_REQUEST_URL" not in os.environ:
1097
+ self.console.print("[yellow]OIDC token request URL not found[/yellow]")
1098
+ # Don't add --no-oidc flag as it may not be supported by all PDM versions
1099
+
1100
+ # Check for API token in environment regardless of OIDC status
1101
+ self._check_for_token_env_vars()
1102
+
1103
+ return publish_cmd
1104
+
1105
+ def _check_for_token_env_vars(self) -> None:
1106
+ """Check for token environment variables."""
1107
+ for token_var in ("PYPI_TOKEN", "PYPI_API_TOKEN", "TWINE_PASSWORD"):
1108
+ if token_var in os.environ:
1109
+ self.console.print(
1110
+ f"[yellow]Found {token_var} environment variable, will use for authentication[/yellow]"
1080
1111
  )
1081
- else:
1082
- self.console.print("[green]✅ Package published successfully![/green]")
1112
+ # If we have a token, PDM will use it automatically
1113
+ break
1114
+ else:
1115
+ self.console.print(
1116
+ "[yellow]No PyPI token found in environment variables[/yellow]"
1117
+ )
1118
+
1119
+ def _check_keyring_credentials(self) -> None:
1120
+ """Check for PyPI credentials in keyring."""
1121
+ # First try to verify if keyring has PyPI credentials, wrapped in double try/except
1122
+ # to handle both ImportError and keyring backend errors
1123
+ try:
1124
+ try:
1125
+ import keyring
1126
+
1127
+ username = keyring.get_password("pdm-publish", "username")
1128
+ token = (
1129
+ keyring.get_password("pdm-publish", username) if username else None
1130
+ )
1131
+
1132
+ if username and token:
1133
+ self.console.print(
1134
+ f"[green]Found PyPI credentials in keyring for user: {username}[/green]"
1135
+ )
1136
+ else:
1137
+ self.console.print(
1138
+ "[yellow]Warning: Could not find PyPI credentials in keyring.[/yellow]"
1139
+ )
1140
+ self.console.print(
1141
+ "[yellow]You might be prompted for username and password.[/yellow]"
1142
+ )
1143
+ except ImportError:
1144
+ self.console.print(
1145
+ "[yellow]Warning: Could not import keyring module to check credentials.[/yellow]"
1146
+ )
1147
+ except Exception as e:
1148
+ # Catch any keyring-related exceptions, including NoKeyringError
1149
+ self.console.print(f"[yellow]Warning: Keyring error: {e}[/yellow]")
1150
+ self.console.print(
1151
+ "[yellow]Will use PDM's built-in credential handling.[/yellow]"
1152
+ )
1153
+
1154
+ def _execute_publish_command(
1155
+ self, publish_cmd: list[str], options: OptionsProtocol
1156
+ ) -> None:
1157
+ """Execute the publish command and handle OIDC errors."""
1158
+ from .errors import (
1159
+ ErrorCode,
1160
+ ExecutionError,
1161
+ PublishError,
1162
+ check_command_result,
1163
+ handle_error,
1164
+ )
1165
+
1166
+ # Log the exact command we're running
1167
+ self.console.print(f"[yellow]Running command: {' '.join(publish_cmd)}[/yellow]")
1168
+
1169
+ # Execute the publish command with detailed output
1170
+ publish_result = self.execute_command(
1171
+ publish_cmd, capture_output=True, text=True
1172
+ )
1173
+
1174
+ # Print the command's stdout and stderr for debugging
1175
+ self.console.print(
1176
+ f"[yellow]Debug: publish stdout:[/yellow]\n{publish_result.stdout}"
1177
+ )
1178
+ self.console.print(
1179
+ f"[yellow]Debug: publish stderr:[/yellow]\n{publish_result.stderr}"
1180
+ )
1181
+ self.console.print(
1182
+ f"[yellow]Debug: publish return code: {publish_result.returncode}[/yellow]"
1183
+ )
1184
+
1185
+ # If first attempt failed with OIDC error, try an alternative approach
1186
+ if publish_result.returncode > 0 and "OIDC" in publish_result.stderr:
1187
+ self.console.print(
1188
+ "[yellow]Detected OIDC error, OIDC may not be supported on this platform[/yellow]"
1189
+ )
1190
+
1191
+ # Provide a more helpful error message
1192
+ error = PublishError(
1193
+ message="Package publication failed - OIDC authentication issue",
1194
+ error_code=ErrorCode.AUTHENTICATION_ERROR,
1195
+ details=f"OIDC authentication is not supported on this platform or configuration.\n\nCommand output:\n{publish_result.stderr}",
1196
+ recovery="Consider manually setting PyPI credentials using the keyring or environment variables. Run 'pdm config pypi.username YOUR_USERNAME' and 'pdm config pypi.password YOUR_PASSWORD' to configure credentials.",
1197
+ exit_code=1,
1198
+ )
1199
+ handle_error(
1200
+ error=error,
1201
+ console=self.console,
1202
+ verbose=options.verbose,
1203
+ ai_agent=options.ai_agent,
1204
+ )
1205
+ return
1206
+
1207
+ # Check the command result and raise an error if it failed
1208
+ try:
1209
+ cmd_str = " ".join(publish_cmd)
1210
+ check_command_result(
1211
+ publish_result,
1212
+ cmd_str,
1213
+ "Package publication failed",
1214
+ ErrorCode.PUBLISH_ERROR,
1215
+ "Ensure you have the correct PyPI credentials configured. Check your internet connection and that the package name is available on PyPI.",
1216
+ )
1217
+ except ExecutionError as e:
1218
+ # Convert to PublishError for consistent error handling
1219
+ error = PublishError(
1220
+ message=e.message,
1221
+ error_code=ErrorCode.PUBLISH_ERROR,
1222
+ details=e.details,
1223
+ recovery=e.recovery,
1224
+ exit_code=1,
1225
+ )
1226
+ handle_error(
1227
+ error=error,
1228
+ console=self.console,
1229
+ verbose=options.verbose,
1230
+ ai_agent=options.ai_agent,
1231
+ )
1232
+
1233
+ def _publish_project(self, options: OptionsProtocol) -> None:
1234
+ """Publish the package to PyPI."""
1235
+ from .errors import ErrorCode, PublishError, handle_error
1236
+
1237
+ if not options.publish:
1238
+ return
1239
+
1240
+ try:
1241
+ # Step 1: Ensure keyring is installed (on macOS)
1242
+ self._ensure_keyring_installed(options)
1243
+
1244
+ # Step 2: Build the package
1245
+ self._build_package(options)
1246
+
1247
+ # Print debug info before executing publish command
1248
+ self.console.print(
1249
+ "[yellow]Debug: About to execute PDM publish command[/yellow]"
1250
+ )
1251
+
1252
+ # Step 3: Setup environment variables for authentication
1253
+ os.environ["PDM_USE_KEYRING"] = "1"
1254
+
1255
+ # Step 4: Prepare publish command with appropriate flags
1256
+ publish_cmd = self._prepare_publish_command()
1257
+
1258
+ # Step 5: Check keyring credentials
1259
+ self._check_keyring_credentials()
1260
+
1261
+ # Step 6: Execute publish command and handle OIDC errors
1262
+ self._execute_publish_command(publish_cmd, options)
1263
+
1264
+ # Success message
1265
+ self.console.print("[green]✅ Package published successfully![/green]")
1266
+
1267
+ except Exception as e:
1268
+ # Catch any other unexpected errors
1269
+ self.console.print(f"[red]Debug: Unexpected error: {e}[/red]")
1270
+ error = PublishError(
1271
+ message="Package publication failed",
1272
+ error_code=ErrorCode.PUBLISH_ERROR,
1273
+ details=f"Unexpected error: {e}",
1274
+ recovery="This is an unexpected error. Please report this issue.",
1275
+ exit_code=1,
1276
+ )
1277
+ handle_error(
1278
+ error=error,
1279
+ console=self.console,
1280
+ verbose=options.verbose,
1281
+ ai_agent=options.ai_agent,
1282
+ )
1083
1283
 
1084
1284
  def _commit_and_push(self, options: OptionsProtocol) -> None:
1085
1285
  if options.commit:
@@ -4,7 +4,7 @@ requires = [ "pdm-backend" ]
4
4
 
5
5
  [project]
6
6
  name = "crackerjack"
7
- version = "0.20.1"
7
+ version = "0.20.6"
8
8
  description = "Crackerjack: code quality toolkit"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -42,6 +42,7 @@ classifiers = [
42
42
  ]
43
43
  dependencies = [
44
44
  "autotyping>=24.9",
45
+ "keyring>=25.6",
45
46
  "pdm>=2.24.2",
46
47
  "pdm-bump>=0.9.12",
47
48
  "pre-commit>=4.2",
@@ -216,6 +217,7 @@ exclude-deps = [
216
217
  "tomli-w",
217
218
  "google-crc32c",
218
219
  "pytest-timeout",
220
+ "keyring",
219
221
  ]
220
222
 
221
223
  [tool.refurb]
@@ -6,7 +6,7 @@ requires = [
6
6
 
7
7
  [project]
8
8
  name = "crackerjack"
9
- version = "0.20.2"
9
+ version = "0.20.7"
10
10
  description = "Crackerjack: code quality toolkit"
11
11
  readme = "README.md"
12
12
  keywords = [
@@ -42,6 +42,7 @@ classifiers = [
42
42
  ]
43
43
  dependencies = [
44
44
  "autotyping>=24.9",
45
+ "keyring>=25.6",
45
46
  "pdm>=2.24.2",
46
47
  "pdm-bump>=0.9.12",
47
48
  "pre-commit>=4.2",
@@ -241,6 +242,7 @@ exclude-deps = [
241
242
  "tomli-w",
242
243
  "google-crc32c",
243
244
  "pytest-timeout",
245
+ "keyring",
244
246
  ]
245
247
 
246
248
  [tool.refurb]
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import subprocess
2
3
  import typing as t
3
4
  from contextlib import suppress
4
5
  from dataclasses import dataclass
@@ -12,8 +13,10 @@ from crackerjack.crackerjack import (
12
13
  CodeCleaner,
13
14
  ConfigManager,
14
15
  Crackerjack,
16
+ OptionsProtocol,
15
17
  ProjectManager,
16
18
  )
19
+ from crackerjack.errors import ErrorCode
17
20
 
18
21
 
19
22
  class BumpOption(str, Enum):
@@ -180,7 +183,7 @@ class TestCrackerjackProcess:
180
183
  options = options_factory(commit=True, no_config_updates=True)
181
184
  with patch.object(Crackerjack, "_update_project") as mock_update_project:
182
185
 
183
- def side_effect(opts: t.Any) -> None:
186
+ def side_effect(opts: OptionsProtocol) -> None:
184
187
  if opts.no_config_updates:
185
188
  mock_console_print("Skipping config updates.")
186
189
 
@@ -580,7 +583,9 @@ class TestCrackerjackProcess:
580
583
  with patch("platform.system", return_value="Linux"):
581
584
  with patch.object(Crackerjack, "execute_command") as mock_cj_execute:
582
585
 
583
- def mock_execute_side_effect(*args: t.Any, **kwargs: t.Any):
586
+ def mock_execute_side_effect(
587
+ *args: t.Any, **kwargs: t.Any
588
+ ) -> subprocess.CompletedProcess[str]:
584
589
  cmd = args[0][0]
585
590
  if cmd == "pdm" and "build" in args[0]:
586
591
  return MagicMock(
@@ -596,55 +601,271 @@ class TestCrackerjackProcess:
596
601
 
597
602
  mock_handle_error.assert_called()
598
603
 
599
- def test_process_with_darwin_platform(
600
- self,
601
- mock_execute: MagicMock,
602
- mock_console_print: MagicMock,
603
- tmp_path: Path,
604
- tmp_path_package: Path,
605
- create_package_dir: None,
606
- options_factory: t.Callable[..., OptionsForTesting],
607
- ) -> None:
608
- options = options_factory(publish="micro", no_config_updates=True)
609
- with patch("platform.system", return_value="Darwin"):
610
- with patch.object(Crackerjack, "execute_command") as mock_cj_execute:
611
- mock_cj_execute.side_effect = [
612
- MagicMock(returncode=0, stdout="Success"),
613
- MagicMock(returncode=0, stdout="Success"),
614
- MagicMock(returncode=0, stdout="Success"),
615
- MagicMock(returncode=0, stdout="Success"),
616
- ]
617
- with patch.object(Crackerjack, "_update_project"):
618
- cj = Crackerjack(dry_run=True)
619
- cj.process(options)
620
- mock_cj_execute.assert_any_call(
621
- ["pdm", "self", "add", "keyring"],
622
- capture_output=True,
623
- text=True,
624
- )
604
+ def test_keyring_check_install_darwin(self) -> None:
605
+ """Test checking for keyring on Darwin (macOS) and installing it."""
606
+ # Create a minimal options object
607
+ options = OptionsForTesting(publish=BumpOption.micro)
625
608
 
626
- def test_process_with_darwin_platform_keyring_failure(
627
- self,
628
- mock_execute: MagicMock,
629
- mock_console_print: MagicMock,
630
- tmp_path: Path,
631
- tmp_path_package: Path,
632
- create_package_dir: None,
633
- options_factory: t.Callable[..., OptionsForTesting],
634
- ) -> None:
635
- with patch("crackerjack.errors.handle_error") as mock_handle_error:
636
- options = options_factory(publish="micro", no_config_updates=True)
637
- with patch("platform.system", return_value="Darwin"):
638
- with patch.object(Crackerjack, "execute_command") as mock_cj_execute:
639
- mock_cj_execute.return_value = MagicMock(
640
- returncode=1, stdout="", stderr="Authorization failed"
641
- )
642
- with patch.object(Crackerjack, "_update_project"):
643
- with suppress(SystemExit):
644
- cj = Crackerjack(dry_run=True)
645
- cj.process(options)
609
+ # Use a list to track actual calls
610
+ actual_calls = []
646
611
 
647
- mock_handle_error.assert_called()
612
+ # Create a Crackerjack instance for testing
613
+ crackerjack = Crackerjack(dry_run=False)
614
+
615
+ # Mock platform.system to return 'Darwin'
616
+ with patch("platform.system", return_value="Darwin"):
617
+ # Mock keyring module to avoid NoKeyringError
618
+ mock_keyring = MagicMock()
619
+ # Make get_password return None to simulate no credentials
620
+ mock_keyring.get_password.return_value = None
621
+
622
+ with patch.dict("sys.modules", {"keyring": mock_keyring}):
623
+ # Mock environment variables
624
+ with patch.dict("os.environ", {}, clear=True):
625
+ # Create a custom execute_command function that tracks calls
626
+ def mock_execute_side_effect(
627
+ *args: t.Any, **kwargs: t.Any
628
+ ) -> subprocess.CompletedProcess[str]:
629
+ cmd = args[0]
630
+ actual_calls.append(cmd) # Track each command
631
+
632
+ if cmd == ["pdm", "self", "list"]:
633
+ # Important: This MUST NOT contain the string "keyring" to trigger installation
634
+ return MagicMock(
635
+ returncode=0, stdout="packages installed: pytest, black"
636
+ )
637
+ elif cmd == ["pdm", "self", "add", "keyring"]:
638
+ return MagicMock(returncode=0, stdout="installed keyring")
639
+ elif cmd == ["pdm", "build"]:
640
+ return MagicMock(returncode=0, stdout="build output")
641
+ elif "pdm" in cmd and "publish" in cmd:
642
+ return MagicMock(returncode=0, stdout="publish output")
643
+ return MagicMock(returncode=0, stdout="")
644
+
645
+ # Mock execute_command with our custom side_effect
646
+ with patch.object(
647
+ crackerjack,
648
+ "execute_command",
649
+ side_effect=mock_execute_side_effect,
650
+ ):
651
+ # Mock console.print
652
+ with patch.object(crackerjack.console, "print"):
653
+ # Call the method we're testing
654
+ crackerjack._publish_project(options)
655
+
656
+ # Check that all the expected commands were called
657
+ assert ["pdm", "self", "list"] in actual_calls
658
+ assert ["pdm", "self", "add", "keyring"] in actual_calls
659
+ assert ["pdm", "build"] in actual_calls
660
+
661
+ # The publish command might have additional flags now
662
+ publish_cmd_found = False
663
+ for cmd in actual_calls:
664
+ if "pdm" in cmd and "publish" in cmd and "--no-build" in cmd:
665
+ publish_cmd_found = True
666
+ break
667
+ assert publish_cmd_found, "Expected pdm publish command was not called"
668
+
669
+ def test_keyring_already_installed_darwin(self) -> None:
670
+ """Test behavior when keyring is already installed on Darwin (macOS)."""
671
+ # Create a minimal options object
672
+ options = OptionsForTesting(publish=BumpOption.micro)
673
+
674
+ # Use a list to track actual calls
675
+ actual_calls = []
676
+
677
+ # Create a Crackerjack instance for testing
678
+ crackerjack = Crackerjack(dry_run=False)
679
+
680
+ # Mock platform.system to return 'Darwin'
681
+ with patch("platform.system", return_value="Darwin"):
682
+ # Mock keyring module to avoid NoKeyringError
683
+ mock_keyring = MagicMock()
684
+ # Make get_password return None to simulate no credentials
685
+ mock_keyring.get_password.return_value = None
686
+
687
+ with patch.dict("sys.modules", {"keyring": mock_keyring}):
688
+ # Mock environment variables
689
+ with patch.dict("os.environ", {}, clear=True):
690
+ # Create a custom execute_command function that tracks calls
691
+ def mock_execute_side_effect(
692
+ *args: t.Any, **kwargs: t.Any
693
+ ) -> subprocess.CompletedProcess[str]:
694
+ cmd = args[0]
695
+ actual_calls.append(cmd) # Track each command
696
+
697
+ if cmd == ["pdm", "self", "list"]:
698
+ return MagicMock(
699
+ returncode=0, stdout="keyring 25.6.0"
700
+ )
701
+ elif cmd == ["pdm", "build"]:
702
+ return MagicMock(returncode=0, stdout="build output")
703
+ elif "pdm" in cmd and "publish" in cmd:
704
+ return MagicMock(returncode=0, stdout="publish output")
705
+ return MagicMock(returncode=0, stdout="")
706
+
707
+ # Mock execute_command with our custom side_effect
708
+ with patch.object(
709
+ crackerjack,
710
+ "execute_command",
711
+ side_effect=mock_execute_side_effect,
712
+ ):
713
+ # Mock console.print
714
+ with patch.object(crackerjack.console, "print"):
715
+ # Call the method we're testing
716
+ crackerjack._publish_project(options)
717
+
718
+ # Check that keyring installation was skipped
719
+ assert ["pdm", "self", "list"] in actual_calls
720
+ assert ["pdm", "self", "add", "keyring"] not in actual_calls
721
+ assert ["pdm", "build"] in actual_calls
722
+
723
+ # The publish command might have additional flags now
724
+ publish_cmd_found = False
725
+ for cmd in actual_calls:
726
+ if "pdm" in cmd and "publish" in cmd and "--no-build" in cmd:
727
+ publish_cmd_found = True
728
+ break
729
+ assert publish_cmd_found, "Expected pdm publish command was not called"
730
+
731
+ def test_keyring_install_failure_darwin(self) -> None:
732
+ """Test handling of keyring installation failure on Darwin (macOS)."""
733
+ # Create a minimal options object with ai_agent False to avoid extra mocking
734
+ options = OptionsForTesting(publish=BumpOption.micro, ai_agent=False)
735
+
736
+ # Create a Crackerjack instance for testing
737
+ crackerjack = Crackerjack(dry_run=False)
738
+
739
+ # Mock the PublishError class to capture its creation
740
+ with patch("crackerjack.errors.PublishError") as mock_publish_error:
741
+ # Set up the mock to return a MagicMock that we can track
742
+ error_instance = MagicMock()
743
+ mock_publish_error.return_value = error_instance
744
+
745
+ # Mock handle_error to avoid actual exit
746
+ with patch("crackerjack.errors.handle_error") as mock_handle_error:
747
+ # Mock platform.system to return 'Darwin'
748
+ with patch("platform.system", return_value="Darwin"):
749
+ # Mock keyring module to avoid NoKeyringError
750
+ mock_keyring = MagicMock()
751
+ # Make get_password return None to simulate no credentials
752
+ mock_keyring.get_password.return_value = None
753
+
754
+ with patch.dict("sys.modules", {"keyring": mock_keyring}):
755
+ # Mock environment variables
756
+ with patch.dict("os.environ", {}, clear=True):
757
+ # Create a custom execute_command function with failure scenario
758
+ def mock_execute_side_effect(
759
+ *args: t.Any, **kwargs: t.Any
760
+ ) -> subprocess.CompletedProcess[str]:
761
+ cmd = args[0]
762
+
763
+ if cmd == ["pdm", "self", "list"]:
764
+ # This must NOT contain the string "keyring" to trigger installation
765
+ return MagicMock(
766
+ returncode=0,
767
+ stdout="packages installed: pytest, black",
768
+ )
769
+ elif cmd == ["pdm", "self", "add", "keyring"]:
770
+ # Return a failure for keyring installation
771
+ mock_result = MagicMock()
772
+ mock_result.returncode = 1
773
+ mock_result.stdout = ""
774
+ mock_result.stderr = "Installation failed"
775
+ return mock_result
776
+ return MagicMock(returncode=0, stdout="")
777
+
778
+ # Mock execute_command with our custom side_effect
779
+ with patch.object(
780
+ crackerjack,
781
+ "execute_command",
782
+ side_effect=mock_execute_side_effect,
783
+ ):
784
+ # Mock console.print to avoid terminal output
785
+ with patch.object(crackerjack.console, "print"):
786
+ # Call the method we're testing with error suppression
787
+ with suppress(SystemExit):
788
+ crackerjack._publish_project(options)
789
+
790
+ # Check that the error handler was called with the right error
791
+ mock_handle_error.assert_called_once_with(
792
+ error=error_instance,
793
+ console=crackerjack.console,
794
+ verbose=options.verbose,
795
+ ai_agent=options.ai_agent,
796
+ )
797
+
798
+ def test_publish_oidc_error_handling(self) -> None:
799
+ """Test OIDC error detection and handling."""
800
+ # Create a minimal options object
801
+ options = OptionsForTesting(publish=BumpOption.micro)
802
+
803
+ # Use a list to track actual calls
804
+ actual_calls = []
805
+
806
+ # Create a Crackerjack instance for testing
807
+ crackerjack = Crackerjack(dry_run=False)
808
+
809
+ # Mock platform.system to return a non-Darwin platform to skip keyring install
810
+ with patch("platform.system", return_value="Linux"):
811
+ # Mock keyring module to avoid NoKeyringError
812
+ mock_keyring = MagicMock()
813
+ # Make get_password return None to simulate no credentials
814
+ mock_keyring.get_password.return_value = None
815
+
816
+ with patch.dict("sys.modules", {"keyring": mock_keyring}):
817
+ with patch.dict("os.environ", {}, clear=True):
818
+ # Create a custom execute_command function that simulates OIDC failure
819
+ def mock_execute_side_effect(
820
+ *args: t.Any, **kwargs: t.Any
821
+ ) -> subprocess.CompletedProcess[str]:
822
+ cmd = args[0]
823
+ actual_calls.append(cmd) # Track each command
824
+
825
+ if cmd == ["pdm", "build"]:
826
+ return MagicMock(returncode=0, stdout="build output")
827
+ elif "pdm" in cmd and "publish" in cmd:
828
+ # Simulate OIDC error
829
+ return MagicMock(
830
+ returncode=1,
831
+ stdout="",
832
+ stderr="Getting PyPI token via OIDC...\nThis platform is not supported for trusted publishing via OIDC\n[PdmUsageError]: Username and password are required",
833
+ )
834
+ return MagicMock(returncode=0, stdout="")
835
+
836
+ # Mock execute_command with our custom side_effect
837
+ with patch.object(
838
+ crackerjack,
839
+ "execute_command",
840
+ side_effect=mock_execute_side_effect,
841
+ ):
842
+ # Mock console.print
843
+ with patch.object(crackerjack.console, "print"):
844
+ # Mock handle_error to prevent sys.exit
845
+ with patch(
846
+ "crackerjack.errors.handle_error"
847
+ ) as mock_handle_error:
848
+ # Call the method we're testing
849
+ crackerjack._publish_project(options)
850
+
851
+ # Verify the error handler was called with the right error type
852
+ mock_handle_error.assert_called_once()
853
+ error = mock_handle_error.call_args[1]["error"]
854
+ assert (
855
+ error.error_code == ErrorCode.AUTHENTICATION_ERROR
856
+ )
857
+ assert "OIDC authentication" in error.message
858
+
859
+ # Check that the build command was called
860
+ assert ["pdm", "build"] in actual_calls
861
+
862
+ # There should be exactly one publish command called (no retry)
863
+ publish_cmds = [
864
+ cmd for cmd in actual_calls if "pdm" in cmd and "publish" in cmd
865
+ ]
866
+ assert len(publish_cmds) == 1, (
867
+ "Expected only one publish command (no retry)"
868
+ )
648
869
 
649
870
  def test_process_with_commit_input(
650
871
  self,
File without changes
File without changes