-
Couldn't load subscription status.
- Fork 550
Fix for Click 8.2 breaking change #4061
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/update-modal-step-operator
Are you sure you want to change the base?
Fix for Click 8.2 breaking change #4061
Conversation
Updated the type hint for term_len to ensure proper type checking during development. This change enhances code clarity and maintains compatibility with mypy checks.
…-click-cli-ci-failures
Tests were directly instantiating CliRunner() which fails with Click 8.2+ since the mix_stderr parameter was removed. The codebase already had a cli_runner() compatibility helper in utils.py that handles version differences, but tests weren't using it. This fix updates test_base.py and test_cli.py to use the helper function and adds exit code checks to catch failures early with clear error messages. Resolves template init test failures in CI where Click 8.2.1 is installed.
Add guidance for future CLI test development to use the cli_runner() helper for Click version compatibility, while clarifying that existing tests with plain CliRunner() calls are fine as-is.
Documentation Link Check Results❌ Absolute links check failed |
The init command prompts for email during initialization, which causes CI tests to fail with "Email:Aborted!" when running non-interactively. The --test flag (hidden CLI option) specifically bypasses this prompt for testing scenarios. This fixes test_init_creates_zen_folder and test_init_creates_from_templates which were failing in CI due to the interactive email prompt introduced when the command runs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR addresses a breaking change introduced in Click 8.2 where the mix_stderr parameter was removed from CliRunner initialization and HelpFormatter.write_dl now requires definition list entries to be pairs rather than triples. The changes ensure compatibility with Click 8.2+ while maintaining the custom grouped command help format.
Key changes:
- Updated CLI formatter to detect and handle both two-column and three-column definition lists
- Modified CLI command to manually handle help display when invoked without subcommands
- Refactored tests to use a compatibility wrapper for
CliRunnerinitialization
Reviewed Changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/zenml/cli/formatter.py | Refactored write_dl to validate row structure and delegate triple-column rendering to a separate method, added width calculation helper |
| src/zenml/cli/cli.py | Added fallback formatting for non-ZenFormatter instances and manual help handling for Click 8.2+ compatibility |
| tests/integration/functional/cli/test_cli.py | Replaced direct CliRunner() instantiation with create_cli_runner() helper |
| tests/integration/functional/cli/test_base.py | Replaced direct CliRunner() instantiation with cli_runner() helper and added exit code assertions |
| CLAUDE.md | Added CLI testing best practices documentation |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
- Use ImportError instead of broad Exception when catching Click import failures This is more precise and only catches the expected failure case when term_len is unavailable in older Click versions, avoiding accidentally hiding other errors. - Add type: ignore[attr-defined] comment with explanation for max_width access Documents why we're bypassing type checking - max_width may not be present on all formatter types, which is intentional behavior.
The --yes flag is defined as is_flag=True in Click, which means it's a boolean flag that works by presence/absence, not by value assignment. Changed from ["--yes", True] to ["--yes"] to fix the TypeError where Click was trying to process the boolean True as a string argument.
Mypy no longer requires the attr-defined suppression for the getattr call on max_width. The explanatory comment remains to document the intentional use of getattr for compatibility.
Previously, tests in tests/integration/functional/utils/ were failing during pipeline execution with ModuleNotFoundError. The directory was missing __init__.py, preventing ZenML from importing step definitions. Adding __init__.py created a namespace collision between the utils.py module (exporting sample_name) and the utils/ package directory. This broke imports across multiple test files that depend on sample_name. Renamed utils/ to utils_functional/ to eliminate the shadowing issue while preserving the utils.py module for shared test utilities.
Pytest collection was failing due to import-time Client and materializer resolution before fixtures could configure the test environment. Changes: - Move Client import under TYPE_CHECKING guard to prevent import-time initialization - Remove output_materializers from @step decorator to avoid import-time resolution - Apply materializers at runtime using .with_options() inside test functions - Add clean_client fixture to all pipeline-running tests for proper setup This ensures materializers can access StepContext during save/load operations without triggering "No active project configured" errors during collection.
src/zenml/config/global_config.py
Outdated
| close_fn = getattr(store, "close", None) | ||
| if callable(close_fn): | ||
| try: | ||
| close_fn() | ||
| except Exception as e: | ||
| logger.debug( | ||
| "Error while closing zen store during cleanup: %s", | ||
| e, | ||
| exc_info=True, | ||
| ) | ||
| # Clear the cached store reference irrespective of cleanup result. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no real reason to make this code so ugly:
If we want a close (or maybe disconnect?) method, it should be defined on the ZenStoreInterface, which means you can simply call store.close() here. Additionally, you'll also be able to implement that functionality for the Rest store, which can potentially also drain it's connections. Or it simply does nothing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
| # no_args_is_help=True. By relying solely on invoke_without_command=True | ||
| # and handling help here, we ensure consistent behavior across Click | ||
| # versions while leveraging our custom help formatter in ZenMLCLI.get_help(). | ||
| if ctx.invoked_subcommand is None and not ctx.resilient_parsing: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I really get why this is necessary. Why wouldn't click display the help anymore in newer versions with our previous setup?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue isn't that Click won't display help in newer versions (it will). The problem is that Click 8.2+ would display help using its default formatter instead of our custom ZenMLCLI.get_help() formatter.
(As far as I understand it, without invoke_without_command=True, Click defaults to no_args_is_help=True. In Click 8.2+, this causes Click to raise NoArgsIsHelpError before our cli() callback runs, meaning it handles the help display internally with its default formatter, completely bypassing our custom formatting (categories, rich colors, custom layout). By using invoke_without_command=True and manually checking for no subcommand in the callback, we ensure our custom formatter is always used across all Click versions.)
I will improve the comment to make it clearer maybe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's just that I can't reproduce it. I have click 8.2+, I run zenml or zenml --help, and everything shows up correct (on develop). So it uses the correct formatter I assume?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also if the code that defined invoke_without_command=True gets executed, at that point anyway I should also know about our custom class and use that context/formatter I assume?
Clarified that the issue with Click 8.2+ isn't that help won't display, but that it would use Click's default formatter instead of ZenML's custom formatter, losing the custom categories, colors, and layout.
The isinstance check for ZenFormatter was overly defensive. Since ZenMLCLI defines context_class = ZenContext and ZenContext defines formatter_class = ZenFormatter, the formatter passed to format_commands() is guaranteed to be a ZenFormatter instance through Click's class hierarchy. The fallback branch was unreachable in practice.
After review feedback, the formatter refactoring was not needed to fix Click 8.2+ compatibility. The formatter works fine on both old and new Click versions. The minimal fix required is: 1. Tests use cli_runner() helper (already in place) 2. CLI uses invoke_without_command=True for consistent help display (already in place) This commit reverts the formatter.py changes to keep the PR focused on the actual issue.
The base branch had a type: ignore[arg-type] comment because we pass 3-column tuples to write_dl which expects 2-column tuples. The old formatter implementation handled this internally. Since we reverted the formatter refactoring, we need to restore this type ignore comment.
The test was failing on docker-server-mysql configurations because it reused a store object captured before entering a UserContext. In REST store setups, the store reads credentials from GlobalConfiguration on every API call. When the old store variable was used inside the UserContext, it inadvertently authenticated as User 2 instead of User 1. The fix creates a fresh Client/store after exiting the UserContext, ensuring it picks up the restored User 1 credentials from the cleaned-up GlobalConfiguration. This allows the test to correctly verify that User 1 can still see their own private secret after User 2's context exits.
|
Ok the only failing test now is this windows DLL failure on integration tests, but I've seen this failing on other PRs that were merged in, so I guess it can be ignored? |
Fixes Click 8.2+ compatibility issues in the ZenML CLI. Click 8.2 introduced breaking changes: removed the
mix_stderrparameter fromCliRunner, tightened validation requiring definition list entries to be pairs (not triples), changedno_args_is_helpbehavior, and can setwidthtoNonein non-interactive contexts.Changes:
ZenFormatter.write_dl()to detect 2-column vs 3-column rows and handle each appropriately, delegating standard pairs to Click while rendering tagged triples manually._safe_width()helper for robust width handling when Click returnsNone.cli_runner()helper in test utils that handles Click version differences.tests/integration/functional/utils/totests/integration/functional/utils_functional/to avoid name shadowing with thezenml.utilsmodule.CLAUDE.md.test_reusing_private_secret_name_succeedsto properly handle REST store authentication by creating a fresh client/store after exiting UserContext, preventing credential state pollution.