support `type={pathlib.Path,argparse.FileType}` without explicit `.complete = shtab.FILE`
Requirements:
- When defining arguments with using
pathlib.Path, such asp.add_argument("-s", "--src", type=Path)on anArgumentParserinstance, the autocomplete mechanism should understand the type and emit an autocomplete that is compatible with both a file or directory.
Context/Comments
- Path is commonly used and communicates the fundamental intent that the argument is PathLike.
- Currently, when defining File/Dir, the user needs to add
.complete(e.g.,p.add_argument("-s", "--src", type=Path).complete = shtab.FILE). This is easy to forget and it's not a particulary obvious interface toAction. Also, your text editing won't be able to help or assist you. For example, a simple mistyping of.completion = shtab.FILEcan create confusion. - Adding
.completedoesn't play well with static analysis tools such asmypyin strict mode. Using.complete = ...will yield an error (example.py:29: error: "Action" has no attribute "complete" [attr-defined] - Using
.complete = shtab.FILEmodel requires an explicit dependency onshtab. It's useful to make shtab an optional dependency with a try/catch ImportError and useshtab.add_argument_to(p)when defining aget_parser()function. - If you don't explicitly add
.completestyle for a Path, then you loose any "default" autocomplete mechanics of file/dir behavior in the shell.
A bit hesitant as it's not possible to infer which (FILE or DIR) the user expects type=Path to mean.
A bit hesitant as it's not possible to infer which (
FILEorDIR) the user expectstype=Pathto mean.
I agree, but adding basic support for autocompleting any file or directory would be useful out of the box experience. If you need to further customize it, then you can use the specific file or directory version (e.g, .complete = shtab.DIR).
For example, in zsh, I believe converting :input_file: to :input_file:_files: would be good enough for most cases with Path?
Also should add argparse.FileType too I suppose
Currently, there's two different styles with the usage of choices= and the .completer=.
p = ArgumentParser(description="Desc")
p.add_argument("-s", "--src", help="Source file", choices=shtab.Required.FILE)
p.add_argument("-d", "--dest", help="Dest file").completer = shtab.FILE
It might useful to explore ideas of putting the Completion component into the type= of .add_argument (Only requires Callable[[Any], T]] as an interface) and have something like this:
def example() -> ArgumentParser:
p = ArgumentParser(description="Scratch Pad autocomplete")
p.add_argument("-s", "--src", type=FileCompleter(exts=(".py", ".rb")), help="Source file", required=False)
# the required or optional should be pulled from the keyword
p.add_argument("-o", "--output-dir", type=DirCompleter(), help="Output directory", required=True)
p.add_argument("-m", "--max-records", type=int, help="Max number of records")
return shtab.add_argument_to(p)
With adding .complete to provide a customizable and re-useable hook.
class Completer(Protocol):
@abstractmethod
def __call__(self, args: Any, **kwargs: Any) -> Any:
...
def complete(self, name: str) -> str:
cx = {"zsh": self.complete_zsh,
"bash": self.complete_bash,
"fish": self.complete_fish}
return cx[name]()
@abstractmethod
def complete_zsh(self) -> str:
raise NotImplementedError
@abstractmethod
def complete_bash(self) -> str:
raise NotImplementedError
@abstractmethod
def complete_fish(self) -> str:
raise NotImplementedError
class FileCompleter(Completer):
def __init__(self, exts: tuple[str, ...]):
self.exts = exts
def __call__(self, args: Any, **kwargs: Any) -> Any:
px = Path(args)
if px.is_file() and px.suffix in self.exts:
return px
raise ValueError(f"{px} is not a file")
def complete_zsh(self) -> str:
# do stuff with ext to
return ""
def complete_bash(self) -> str:
return ""
def complete_fish(self) -> str:
return ""
class DirCompleter(Completer):
def __init__(self):
...
def __call__(self, args: Any, **kwargs: Any) -> Any:
px = Path(args)
if px.is_dir():
return px
raise ValueError(f"{px} is not a directory")
def complete_zsh(self) -> str:
# emit
return ""
def complete_bash(self) -> str:
return ""
def complete_fish(self) -> str:
return ""
If we want to piggy-back off of types it might be better to have:
p.add_argument("-o", "--output-dir",
type=typing.Annotated[str, shtab.DIR],
help="Output directory")
Also should add
argparse.FileTypetoo I suppose
It looks like this is starting the depreciated process with a warning in 3.14.
https://github.com/python/cpython/issues/58032