Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/click/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,10 @@ def invoke(

if isinstance(args, str):
args = shlex.split(args)
elif args is not None:
# Convert path-like objects to strings to prevent parser errors
# See: https://github.com/pallets/click/issues/1324
args = [str(arg) if hasattr(arg, "__fspath__") else arg for arg in args]

try:
prog_name = extra.pop("prog_name")
Expand Down
123 changes: 123 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import sys
from io import BytesIO
from pathlib import Path

import pytest

Expand Down Expand Up @@ -469,3 +470,125 @@ def cli():

result = runner.invoke(cli)
assert result.stderr == "gyarados gyarados gyarados"


def test_pathlib_path_in_args_works_with_fix():
"""
Test that passing pathlib.Path objects to CliRunner.invoke() now works correctly.

This verifies the fix for: "object of type 'PosixPath' has no len()" when Path
objects were passed to Click's argument parser instead of strings.

See: https://github.com/pallets/click/issues/1324
"""

@click.command()
@click.argument("path")
def cmd(path):
click.echo(f"Path: {path}")

runner = CliRunner()

# This should work fine with strings
result = runner.invoke(cmd, ["/tmp/test"])
assert result.exit_code == 0
assert "Path: /tmp/test" in result.output

# This should now work with Path objects too (automatic conversion)
test_path = Path("/tmp/test")
result = runner.invoke(cmd, [test_path])

# CliRunner should automatically convert Path to string
assert result.exit_code == 0
assert result.exception is None
assert f"Path: {str(test_path)}" in result.output


def test_pathlib_path_string_conversion_works():
"""
Test that pathlib.Path objects work when converted to strings.

This demonstrates the correct workaround for the bug.
"""

@click.command()
@click.argument("path")
def cmd(path):
click.echo(f"Path: {path}")

runner = CliRunner()
test_path = Path("/tmp/test")

# The correct approach: convert Path to string explicitly
result = runner.invoke(cmd, [str(test_path)])
assert result.exit_code == 0
assert f"Path: {str(test_path)}" in result.output


@pytest.mark.parametrize(
"path_input",
[
pytest.param(Path("/tmp/test"), id="absolute"),
pytest.param(Path("/tmp/test/file.txt"), id="absolute_file"),
pytest.param(Path("relative/path"), id="relative"),
pytest.param(Path("."), id="current_dir"),
pytest.param(Path(".."), id="parent_dir"),
],
)
def test_various_pathlib_objects_work_consistently(path_input):
"""
Test that all types of pathlib.Path objects work consistently with the fix.

This ensures automatic Path-to-string conversion works for different Path
object types.
"""

@click.command()
@click.argument("path")
def cmd(path):
click.echo(f"Path: {path}")

runner = CliRunner()

# All Path objects should now work correctly (automatic conversion)
result = runner.invoke(cmd, [path_input])
assert result.exit_code == 0
assert result.exception is None
assert f"Path: {str(path_input)}" in result.output


def test_direct_parser_still_fails_with_pathlib():
"""
Test that Click's parser still fails when given Path objects directly.

This ensures we haven't changed the core parser behavior - only CliRunner
now handles Path objects gracefully by converting them to strings.
"""
from click.core import Argument
from click.core import Context
from click.parser import _OptionParser

@click.command()
@click.argument("path")
def cmd(path):
click.echo(f"Path: {path}")

ctx = Context(cmd)
parser = _OptionParser(ctx)

# Add argument to parser
arg = Argument(["path"])
arg.add_to_parser(parser, ctx)

# This should still work with strings
result = parser.parse_args(["/tmp/test"])
assert result[0]["path"] == "/tmp/test"

# This should still fail with Path objects (parser behavior unchanged)
test_path = Path("/tmp/test")
with pytest.raises(
TypeError,
match=r"(object of type '(Posix|Windows)Path' has no len"
r"|'(Posix|Windows)Path' has no length)",
):
parser.parse_args([test_path])
Loading