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:
- L4 error: Overloaded function signatures 1 and 2 overlap with incompatible return types
- 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
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 becausejoinpath(['a'],'b',false)is validdef fn(*args, another)is perfectly valid and can be called asfn(1, 2, 3, another="").itertools.productalready 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 thanproduct(["a", "", "b"], ["c", "d"], "e")and done, that gives you the result you're showing as desired result.