Skip to content

Conversation

@strickvl
Copy link
Contributor

@strickvl strickvl commented Oct 10, 2025

Fixes Click 8.2+ compatibility issues in the ZenML CLI. Click 8.2 introduced breaking changes: removed the mix_stderr parameter from CliRunner, tightened validation requiring definition list entries to be pairs (not triples), changed no_args_is_help behavior, and can set width to None in non-interactive contexts.

Changes:

  • Refactored 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.
  • Added _safe_width() helper for robust width handling when Click returns None.
  • Updated CLI root to manually display help when invoked without subcommands, ensuring consistent behavior across Click versions.
  • Created cli_runner() helper in test utils that handles Click version differences.
  • Updated all CLI tests to use the helper and verify exit codes.
  • Renamed tests/integration/functional/utils/ to tests/integration/functional/utils_functional/ to avoid name shadowing with the zenml.utils module.
  • Added CLI testing best practices to CLAUDE.md.
  • Fixed test_reusing_private_secret_name_succeeds to properly handle REST store authentication by creating a fresh client/store after exiting UserContext, preventing credential state pollution.

@strickvl strickvl added internal To filter out internal PRs and issues run-slow-ci labels Oct 10, 2025
@github-actions github-actions bot added the enhancement New feature or request label Oct 10, 2025
strickvl and others added 8 commits October 10, 2025 17:15
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.
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.
@github-actions
Copy link
Contributor

github-actions bot commented Oct 20, 2025

Documentation Link Check Results

Absolute links check failed
There are broken absolute links in the documentation. See workflow logs for details
Relative links check passed
Last checked: 2025-10-23 05:12:48 UTC

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.
@strickvl strickvl requested a review from Copilot October 20, 2025 20:20
Copy link
Contributor

Copilot AI left a 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 CliRunner initialization

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.
@strickvl strickvl requested a review from schustmi October 21, 2025 09:23
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.
Comment on lines 726 to 736
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.
Copy link
Contributor

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.

Copy link
Contributor Author

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:
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor

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.
@strickvl strickvl requested a review from schustmi October 23, 2025 05:09
@strickvl
Copy link
Contributor Author

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request internal To filter out internal PRs and issues run-slow-ci

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants