0

I'm trying to correctly type the following function in Python 3.10

from typing import overload

@overload
def joinpath(*path_pieces: str) -> str: ...
@overload
def joinpath(*path_pieces: list[str] | str) -> list[str]: ...
def joinpath(*path_pieces: list[str] | str) -> list[str] | str:
    """Join paths.

    os separators in the path pieces will be remove, except for the trailing
    separator of the last piece which is used to differenciate folders from
    files.

    Parameters
    ----------
    *path_pieces : str | list[str]
        Path pieces to join. If one of the path piece is a list, the function
        will join each element of the list with the other path pieces and return
        a list of path. If all path pieces are strings, the function will return
        a string.

    Returns
    -------
    str | list[str]
        the joined path: str if all the components are strings, list of strings
        otherwise.

    Examples
    --------
    >>> joinpath("a", "b", "c")
    "a/b/c"
    >>> joinpath("a", "", "c")
    "a/c"
    >>> joinpath(["a", "b"], "c/")
    ["a/c/", "b/c/"]
    >>> joinpath(["a", "b"], ["c", "d"], "e")
    ["a/c/e", "a/d/e", "b/c/e", "b/d/e"]
    >>> joinpath(["a", "", "b"], ["c", "d"], "e")
    ["a/c/e", "a/d/e", "b/c/e", "b/d/e"]
    """
    if all(isinstance(piece, str) for piece in path_pieces):
        return join_path_pieces(*path_pieces)

    if len(path_pieces) < 2:
        raise ValueError("At least two path pieces are required.")
    if len(path_pieces) > 2:
        # Recursive call to join the first two pieces and the rest of the pieces.
        # This will slowly reduce the number of pieces until there are only two.
        return joinpath(joinpath(path_pieces[0], path_pieces[1]), *path_pieces[2:])

    first_paths = ensure_in_list(path_pieces[0])
    second_paths = ensure_in_list(path_pieces[1])

    joined_paths = _cartesian_product_join_paths(first_paths, second_paths)

    return joined_paths


def join_path_pieces(*path_pieces: str) -> str:
    """Join path pieces, while stripping leading slashes for all except the first,
    and trailing slashes for all except the last.
    """
    striped_pieces = [path for path in path_pieces if path != ""]
    for i in range(len(striped_pieces) - 1):
        striped_pieces[i] = striped_pieces[i].rstrip("/")
    for i in range(1, len(striped_pieces)):
        striped_pieces[i] = striped_pieces[i].lstrip("/")
    return os.sep.join(striped_pieces)


def ensure_in_list(value: T | list[T]) -> list[T]:
    """Put in a list any scalar value that is not None.

    If it is already a list, do nothing.
    """
    if value is not None and not isinstance(value, list):
        value = [value]

    return value


def _cartesian_product_join_paths(
    first_path_piece: list[str], second_path_piece: list[str]
) -> list[str]:
    """Compute the cartesian product of two lists of paths.

    If one of the path piece is an empty string, it will be ignored.

    The output list will contain the concatenation of each element of the first
    list with each element of the second list.

    Returns
    -------
    list[str]
        List of length len(first_path_piece) * len(second_path_piece) that
        contains the concatenated path.
    """
    ...

Unfortunately, there are two mypy errors:

  1. L4 error: Overloaded function signatures 1 and 2 overlap with incompatible return types
  2. L40 error: Argument 1 to "join_path_pieces" has incompatible type "*tuple[list[str] | str, ...]"; expected "str"

For the first error, the two signatures are meant to reflect that if there is at least one list of str as an input, the output will be a list.

For the second error, I feel like mypy is missing the condition on the type of all components of path_pieces

7
  • have you tried running your code? the signature def func(*arg_one, arg_two) will never be able to run be able to run (whatever signature you give) because *arg will consume everything that's positional; the other error is because logically yes, might make sense to you but your interpreter doesn't care because joinpath(['a'],'b',false) is valid Commented Jun 19 at 12:55
  • @KenzoStaelens ...what? Python has keyword arguments specifically for this. def fn(*args, another) is perfectly valid and can be called as fn(1, 2, 3, another=""). Commented Jun 19 at 13:04
  • right... that's on me for being a dumbass for a second Commented Jun 19 at 13:08
  • 2
    Your overloads do overlap. I think you're trying to use the final argument to discriminate, in which case tell it that: mypy-play.net/… Commented Jun 19 at 13:17
  • 1
    Really silly question: doesn't itertools.product already do what you want here? E.g. ["/".join(x) for x in product(["a", "b"], ["c", "d"], "e")] yields ['a/c/e', 'a/d/e', 'b/c/e', 'b/d/e'], the only "tricky" part is when you have empty strings, in which case looking at your desired outputs, just... remove them from your input, they seem to do nothing compared to "not being there"? E.g. product(["a", "b"], ["c", "d"], "e") rather than product(["a", "", "b"], ["c", "d"], "e") and done, that gives you the result you're showing as desired result. Commented Jun 19 at 15:32

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.