crackerjack 0.20.3__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.3 → 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.3 → crackerjack-0.20.7}/crackerjack/crackerjack.py +245 -56
  5. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/pyproject.toml +3 -1
  6. {crackerjack-0.20.3 → crackerjack-0.20.7}/pyproject.toml +3 -1
  7. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/test_crackerjack.py +192 -74
  8. crackerjack-0.20.3/crackerjack/.ruff_cache/0.11.13/1867267426380906393 +0 -0
  9. {crackerjack-0.20.3 → crackerjack-0.20.7}/LICENSE +0 -0
  10. {crackerjack-0.20.3 → crackerjack-0.20.7}/README.md +0 -0
  11. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.gitignore +0 -0
  12. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.libcst.codemod.yaml +0 -0
  13. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.pdm.toml +0 -0
  14. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.pre-commit-config.yaml +0 -0
  15. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.pytest_cache/.gitignore +0 -0
  16. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.pytest_cache/CACHEDIR.TAG +0 -0
  17. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.pytest_cache/README.md +0 -0
  18. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.pytest_cache/v/cache/nodeids +0 -0
  19. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.pytest_cache/v/cache/stepwise +0 -0
  20. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/.gitignore +0 -0
  21. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
  22. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
  23. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
  24. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
  25. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
  26. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
  27. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.11/18187162184424859798 +0 -0
  28. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.12/16869036553936192448 +0 -0
  29. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.12/1867267426380906393 +0 -0
  30. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.12/4240757255861806333 +0 -0
  31. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.12/4441409093023629623 +0 -0
  32. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
  33. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
  34. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
  35. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
  36. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
  37. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
  38. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
  39. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
  40. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
  41. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
  42. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
  43. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
  44. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
  45. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
  46. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
  47. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
  48. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
  49. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
  50. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
  51. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
  52. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
  53. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
  54. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
  55. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
  56. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
  57. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
  58. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
  59. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
  60. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
  61. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
  62. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
  63. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
  64. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
  65. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/.ruff_cache/CACHEDIR.TAG +0 -0
  66. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/__init__.py +0 -0
  67. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/__main__.py +0 -0
  68. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/errors.py +0 -0
  69. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/interactive.py +0 -0
  70. {crackerjack-0.20.3 → crackerjack-0.20.7}/crackerjack/py313.py +0 -0
  71. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/TESTING.md +0 -0
  72. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/__init__.py +0 -0
  73. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/conftest.py +0 -0
  74. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/data/comments_sample.txt +0 -0
  75. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/data/docstrings_sample.txt +0 -0
  76. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/data/expected_comments_sample.txt +0 -0
  77. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/data/init.py +0 -0
  78. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/test_crackerjack_runner.py +0 -0
  79. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/test_errors.py +0 -0
  80. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/test_interactive.py +0 -0
  81. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/test_interactive_run.py +0 -0
  82. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/test_main.py +0 -0
  83. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/test_py313_advanced.py +0 -0
  84. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/test_py313_features.py +0 -0
  85. {crackerjack-0.20.3 → crackerjack-0.20.7}/tests/test_pytest_features.py +0 -0
  86. {crackerjack-0.20.3 → 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.3
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,51 +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
- # First check if keyring is already installed in PDM
1026
- check_keyring = self.execute_command(
1027
- ["pdm", "self", "list"], capture_output=True, text=True
1028
- )
1029
- keyring_installed = "keyring" in check_keyring.stdout
1030
-
1031
- if not keyring_installed:
1032
- # Only attempt to install keyring if it's not already installed
1033
- self.console.print("Installing keyring for PDM...")
1034
- authorize = self.execute_command(
1035
- ["pdm", "self", "add", "keyring"],
1036
- capture_output=True,
1037
- text=True,
1038
- )
1039
- if authorize.returncode > 0:
1040
- error = PublishError(
1041
- message="Authentication setup failed",
1042
- error_code=ErrorCode.AUTHENTICATION_ERROR,
1043
- details=f"Failed to add keyring support to PDM.\nCommand output:\n{authorize.stderr}",
1044
- recovery="Please manually add your keyring credentials to PDM. Run `pdm self add keyring` and try again.",
1045
- exit_code=1,
1046
- )
1047
- handle_error(
1048
- error=error,
1049
- console=self.console,
1050
- verbose=options.verbose,
1051
- ai_agent=options.ai_agent,
1052
- )
1024
+ if platform.system() != "Darwin":
1025
+ return
1053
1026
 
1054
- build = self.execute_command(
1055
- ["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,
1056
1040
  )
1057
- self.console.print(build.stdout)
1058
-
1059
- if build.returncode > 0:
1041
+ if authorize.returncode > 0:
1060
1042
  error = PublishError(
1061
- message="Package build failed",
1062
- error_code=ErrorCode.BUILD_ERROR,
1063
- details=f"Command output:\n{build.stderr}",
1064
- 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.",
1065
1047
  exit_code=1,
1066
1048
  )
1067
1049
  handle_error(
@@ -1071,26 +1053,233 @@ class Crackerjack:
1071
1053
  ai_agent=options.ai_agent,
1072
1054
  )
1073
1055
 
1074
- publish_result = self.execute_command(
1075
- ["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,
1076
1076
  )
1077
1077
 
1078
- if publish_result.returncode > 0:
1079
- error = PublishError(
1080
- message="Package publication failed",
1081
- error_code=ErrorCode.PUBLISH_ERROR,
1082
- details=f"Command output:\n{publish_result.stderr}",
1083
- recovery="Ensure you have the correct PyPI credentials configured. Check your internet connection and that the package name is available on PyPI.",
1084
- 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]"
1085
1094
  )
1086
- handle_error(
1087
- error=error,
1088
- console=self.console,
1089
- verbose=options.verbose,
1090
- 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]"
1091
1111
  )
1092
- else:
1093
- 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
+ )
1094
1283
 
1095
1284
  def _commit_and_push(self, options: OptionsProtocol) -> None:
1096
1285
  if options.commit:
@@ -4,7 +4,7 @@ requires = [ "pdm-backend" ]
4
4
 
5
5
  [project]
6
6
  name = "crackerjack"
7
- version = "0.20.2"
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.3"
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]
@@ -16,6 +16,7 @@ from crackerjack.crackerjack import (
16
16
  OptionsProtocol,
17
17
  ProjectManager,
18
18
  )
19
+ from crackerjack.errors import ErrorCode
19
20
 
20
21
 
21
22
  class BumpOption(str, Enum):
@@ -613,40 +614,57 @@ class TestCrackerjackProcess:
613
614
 
614
615
  # Mock platform.system to return 'Darwin'
615
616
  with patch("platform.system", return_value="Darwin"):
616
- # Create a custom execute_command function that tracks calls
617
- def mock_execute_side_effect(
618
- *args: t.Any, **kwargs: t.Any
619
- ) -> subprocess.CompletedProcess[str]:
620
- cmd = args[0]
621
- actual_calls.append(cmd) # Track each command
622
-
623
- if cmd == ["pdm", "self", "list"]:
624
- # Important: This MUST NOT contain the string "keyring" to trigger installation
625
- return MagicMock(
626
- returncode=0, stdout="packages installed: pytest, black"
627
- )
628
- elif cmd == ["pdm", "self", "add", "keyring"]:
629
- return MagicMock(returncode=0, stdout="installed keyring")
630
- elif cmd == ["pdm", "build"]:
631
- return MagicMock(returncode=0, stdout="build output")
632
- elif cmd == ["pdm", "publish", "--no-build"]:
633
- return MagicMock(returncode=0, stdout="publish output")
634
- return MagicMock(returncode=0, stdout="")
635
-
636
- # Mock execute_command with our custom side_effect
637
- with patch.object(
638
- crackerjack, "execute_command", side_effect=mock_execute_side_effect
639
- ):
640
- # Mock console.print
641
- with patch.object(crackerjack.console, "print"):
642
- # Call the method we're testing
643
- crackerjack._publish_project(options)
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)
644
655
 
645
656
  # Check that all the expected commands were called
646
657
  assert ["pdm", "self", "list"] in actual_calls
647
658
  assert ["pdm", "self", "add", "keyring"] in actual_calls
648
659
  assert ["pdm", "build"] in actual_calls
649
- assert ["pdm", "publish", "--no-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"
650
668
 
651
669
  def test_keyring_already_installed_darwin(self) -> None:
652
670
  """Test behavior when keyring is already installed on Darwin (macOS)."""
@@ -661,35 +679,54 @@ class TestCrackerjackProcess:
661
679
 
662
680
  # Mock platform.system to return 'Darwin'
663
681
  with patch("platform.system", return_value="Darwin"):
664
- # Create a custom execute_command function that tracks calls
665
- def mock_execute_side_effect(
666
- *args: t.Any, **kwargs: t.Any
667
- ) -> subprocess.CompletedProcess[str]:
668
- cmd = args[0]
669
- actual_calls.append(cmd) # Track each command
670
-
671
- if cmd == ["pdm", "self", "list"]:
672
- return MagicMock(returncode=0, stdout="keyring 25.6.0")
673
- elif cmd == ["pdm", "build"]:
674
- return MagicMock(returncode=0, stdout="build output")
675
- elif cmd == ["pdm", "publish", "--no-build"]:
676
- return MagicMock(returncode=0, stdout="publish output")
677
- return MagicMock(returncode=0, stdout="")
678
-
679
- # Mock execute_command with our custom side_effect
680
- with patch.object(
681
- crackerjack, "execute_command", side_effect=mock_execute_side_effect
682
- ):
683
- # Mock console.print
684
- with patch.object(crackerjack.console, "print"):
685
- # Call the method we're testing
686
- crackerjack._publish_project(options)
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)
687
717
 
688
718
  # Check that keyring installation was skipped
689
719
  assert ["pdm", "self", "list"] in actual_calls
690
720
  assert ["pdm", "self", "add", "keyring"] not in actual_calls
691
721
  assert ["pdm", "build"] in actual_calls
692
- assert ["pdm", "publish", "--no-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"
693
730
 
694
731
  def test_keyring_install_failure_darwin(self) -> None:
695
732
  """Test handling of keyring installation failure on Darwin (macOS)."""
@@ -709,24 +746,91 @@ class TestCrackerjackProcess:
709
746
  with patch("crackerjack.errors.handle_error") as mock_handle_error:
710
747
  # Mock platform.system to return 'Darwin'
711
748
  with patch("platform.system", return_value="Darwin"):
712
- # Create a custom execute_command function with failure scenario
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
713
819
  def mock_execute_side_effect(
714
820
  *args: t.Any, **kwargs: t.Any
715
821
  ) -> subprocess.CompletedProcess[str]:
716
822
  cmd = args[0]
823
+ actual_calls.append(cmd) # Track each command
717
824
 
718
- if cmd == ["pdm", "self", "list"]:
719
- # This must NOT contain the string "keyring" to trigger installation
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
720
829
  return MagicMock(
721
- returncode=0, stdout="packages installed: pytest, black"
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",
722
833
  )
723
- elif cmd == ["pdm", "self", "add", "keyring"]:
724
- # Return a failure for keyring installation
725
- mock_result = MagicMock()
726
- mock_result.returncode = 1
727
- mock_result.stdout = ""
728
- mock_result.stderr = "Installation failed"
729
- return mock_result
730
834
  return MagicMock(returncode=0, stdout="")
731
835
 
732
836
  # Mock execute_command with our custom side_effect
@@ -735,19 +839,33 @@ class TestCrackerjackProcess:
735
839
  "execute_command",
736
840
  side_effect=mock_execute_side_effect,
737
841
  ):
738
- # Mock console.print to avoid terminal output
842
+ # Mock console.print
739
843
  with patch.object(crackerjack.console, "print"):
740
- # Call the method we're testing with error suppression
741
- with suppress(SystemExit):
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
742
849
  crackerjack._publish_project(options)
743
850
 
744
- # Check that the error handler was called with the right error
745
- mock_handle_error.assert_called_once_with(
746
- error=error_instance,
747
- console=crackerjack.console,
748
- verbose=options.verbose,
749
- ai_agent=options.ai_agent,
750
- )
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
+ )
751
869
 
752
870
  def test_process_with_commit_input(
753
871
  self,
File without changes
File without changes