Skip to content
Merged
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: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.16.4"
rev: "v0.16.5"
hooks:
- id: ruff-format
args: [--config=ruff.toml]
Expand All @@ -30,7 +30,7 @@ repos:
- prettier-plugin-toml@2.0.6

- repo: https://github.com/crate-ci/typos
rev: v1.49.1
rev: v1.50.0
hooks:
- id: typos
exclude: |
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,22 +121,22 @@ examples.
```python
#!/usr/bin/env python
"""A simple cmd2 application."""

import cmd2


class FirstApp(cmd2.Cmd):
"""A simple cmd2 application."""

def do_hello_world(self, _: cmd2.Statement):
self.poutput('Hello World')
self.poutput("Hello World")


if __name__ == '__main__':
if __name__ == "__main__":
import sys

c = FirstApp()
sys.exit(c.cmdloop())

```

## Found a bug?
Expand Down
4 changes: 3 additions & 1 deletion docs/examples/alternate_event_loops.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ by using code like the following:
```py
import cmd2


class Cmd2EventBased(cmd2.Cmd):
def __init__(self):
cmd2.Cmd.__init__(self)

# ... your class code here ...

if __name__ == '__main__':

if __name__ == "__main__":
app = Cmd2EventBased()
app.preloop()

Expand Down
32 changes: 19 additions & 13 deletions docs/examples/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ click the **Copy** button in the top-right):

!!! example "getting_started.py"

<!-- fmt:off -->
```py
--8<-- "examples/getting_started.py"
```
<!-- fmt:on -->

## Basic Application

Expand All @@ -31,15 +33,17 @@ following contents:
```py
#!/usr/bin/env python
"""A basic cmd2 application."""

import cmd2


class BasicApp(cmd2.Cmd):
"""Cmd2 application to demonstrate many common features."""


if __name__ == '__main__':
if __name__ == "__main__":
import sys

app = BasicApp()
sys.exit(app.cmdloop())
```
Expand Down Expand Up @@ -69,7 +73,7 @@ def __init__(self):

# Make maxrepeats settable at runtime
self.maxrepeats = 3
self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self))
self.add_settable(cmd2.Settable("maxrepeats", int, "max repetitions for speak command", self))
```

In that initializer, the first thing to do is to make sure we initialize `cmd2`. That's what the
Expand All @@ -94,25 +98,26 @@ that the `speak_parser` attribute and the `do_speak()` method are part of the `B

```py
speak_parser = cmd2.Cmd2ArgumentParser()
speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
speak_parser.add_argument('words', nargs='+', help='words to say')
speak_parser.add_argument("-p", "--piglatin", action="store_true", help="atinLay")
speak_parser.add_argument("-s", "--shout", action="store_true", help="N00B EMULATION MODE")
speak_parser.add_argument("-r", "--repeat", type=int, help="output [n] times")
speak_parser.add_argument("words", nargs="+", help="words to say")


@cmd2.with_argparser(speak_parser)
def do_speak(self, args):
"""Repeats what you tell me to."""
words = []
for word in args.words:
if args.piglatin:
word = '%s%say' % (word[1:], word[0])
word = "%s%say" % (word[1:], word[0])
if args.shout:
word = word.upper()
words.append(word)
repetitions = args.repeat or 1
for _ in range(min(repetitions, self.maxrepeats)):
# .poutput handles newlines, and accommodates output redirection too
self.poutput(' '.join(words))
self.poutput(" ".join(words))
```

Up at the top of the script, you'll also need to add:
Expand Down Expand Up @@ -186,12 +191,12 @@ Let's add a shortcut for our `speak` command. Change the `__init__()` method so
```py
def __init__(self):
shortcuts = cmd2.DEFAULT_SHORTCUTS
shortcuts.update({'&': 'speak'})
shortcuts.update({"&": "speak"})
super().__init__(shortcuts=shortcuts)

# Make maxrepeats settable at runtime
self.maxrepeats = 3
self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self))
self.add_settable(cmd2.Settable("maxrepeats", int, "max repetitions for speak command", self))
```

Shortcuts are passed to the `cmd2` initializer, and if you want the built-in shortcuts of `cmd2` you
Expand Down Expand Up @@ -224,14 +229,15 @@ def do_speak(self, args):
words = []
for word in args.words:
if args.piglatin:
word = '%s%say' % (word[1:], word[0])
word = "%s%say" % (word[1:], word[0])
if args.shout:
word = word.upper()
words.append(word)
repetitions = args.repeat or 1
for _ in range(min(repetitions, self.maxrepeats)):
# .poutput handles newlines, and accommodates output redirection too
self.poutput(' '.join(words))
self.poutput(" ".join(words))


# orate is a synonym for speak which takes multiline input
do_orate = do_speak
Expand All @@ -241,7 +247,7 @@ With the new command created, we need to tell `cmd2` to treat that command as a
Modify the super initialization line to look like this:

```py
super().__init__(multiline_commands=['orate'], shortcuts=shortcuts)
super().__init__(multiline_commands=["orate"], shortcuts=shortcuts)
```

Now when you run the example, you can type something like this:
Expand Down
55 changes: 40 additions & 15 deletions docs/features/annotated.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ The two decorators are interchangeable -- here is the same command written both

```py
parser = Cmd2ArgumentParser()
parser.add_argument('name', help='person to greet')
parser.add_argument('--count', type=int, default=1, help='repetitions')
parser.add_argument('--loud', action='store_true', help='shout')
parser.add_argument("name", help="person to greet")
parser.add_argument("--count", type=int, default=1, help="repetitions")
parser.add_argument("--loud", action="store_true", help="shout")


@with_argparser(parser)
def do_greet(self, args):
Expand Down Expand Up @@ -61,6 +62,7 @@ Underscores in parameter names are converted to dashes in the generated flag, so
```py
from cmd2.annotated import with_annotated


class MyApp(cmd2.Cmd):
@with_annotated
def do_greet(self, name: str, count: int = 1, loud: bool = False):
Expand Down Expand Up @@ -132,22 +134,30 @@ For finer control, use `typing.Annotated` with [Argument][cmd2.annotated.Argumen
from typing import Annotated
from cmd2.annotated import Argument, Option, with_annotated


class MyApp(cmd2.Cmd):
def sport_choices(self) -> cmd2.Choices:
return cmd2.Choices.from_values(["football", "basketball"])

@with_annotated
def do_play(
self,
sport: Annotated[str, Argument(
choices_provider=sport_choices,
help_text="Sport to play",
)],
venue: Annotated[str, Option(
"--venue", "-v",
help_text="Where to play",
completer=cmd2.Cmd.path_complete,
)] = "home",
sport: Annotated[
str,
Argument(
choices_provider=sport_choices,
help_text="Sport to play",
),
],
venue: Annotated[
str,
Option(
"--venue",
"-v",
help_text="Where to play",
completer=cmd2.Cmd.path_complete,
),
] = "home",
):
self.poutput(f"Playing {sport} at {venue}")
```
Expand All @@ -173,6 +183,7 @@ import enum
from typing import Annotated
from cmd2.annotated import Argument, with_annotated


class Color(enum.Enum):
red = "red"
green = "green"
Expand All @@ -183,6 +194,7 @@ class Color(enum.Enum):
# map a special keyword onto a real member; return None to reject
return cls.red if str(value).lower() == "auto" else None


class MyApp(cmd2.Cmd):
@with_annotated
def do_theme(self, choice: Annotated[Color, Argument(allow_unknown_entry=True)]) -> None:
Expand Down Expand Up @@ -265,6 +277,7 @@ class UpperAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, values.upper())


@with_annotated
def do_shout(self, name: Annotated[str, Option("--name", action=UpperAction)] = ""):
self.poutput(name)
Expand Down Expand Up @@ -298,6 +311,7 @@ forms are equivalent:
# Signature default
def do_x(self, name: Annotated[str, Option("--name")] = "HI"): ...


# Metadata default (same behaviour)
def do_x(self, name: Annotated[str, Option("--name", default="HI")]): ...
```
Expand Down Expand Up @@ -359,11 +373,13 @@ import datetime
from typing import Annotated
from cmd2.annotated import Argument, Option, with_annotated


def parse_size(value: str) -> int:
"""Parse an integer with an optional K/M/G suffix."""
multiplier = {"K": 1_000, "M": 1_000_000, "G": 1_000_000_000}.get(value[-1:].upper(), 1)
return int(value[:-1] if multiplier != 1 else value) * multiplier


class MyApp(cmd2.Cmd):
@with_annotated
def do_alloc(self, size: Annotated[int, Argument(converter=parse_size)]) -> None:
Expand All @@ -382,9 +398,11 @@ instead infer `nargs` and split the input across several tokens:
```py
from typing import Annotated, Any


def parse_intset(value: str) -> set[int]:
return {int(piece) for piece in value.split(",")}


@with_annotated
def do_select(self, idx: Annotated[Any, Option("--idx", converter=parse_intset)]) -> None:
self.poutput(sorted(idx)) # `select --idx 1,3,5` -> [1, 3, 5]
Expand All @@ -404,6 +422,7 @@ import os
from typing import Annotated
from cmd2.annotated import Argument, with_annotated


class MyApp(cmd2.Cmd):
@with_annotated
def do_tag(self, color: Annotated[Color, Argument(preprocess=str.lower)]) -> None:
Expand Down Expand Up @@ -486,6 +505,7 @@ and `description` for a titled help section (omit them for an untitled group):
```py
from cmd2.annotated import Group, with_annotated


class App(cmd2.Cmd):
@with_annotated(
description="Open a network connection.",
Expand All @@ -509,6 +529,8 @@ def do_greet(self, name: str):
:param name: who to greet
"""
self.poutput(f"hello {name}")


# parser.description == "Greet someone by name."
```

Expand All @@ -532,9 +554,7 @@ choice reads as `[--json | --csv]` instead of expanding to `--json`/`--no-json`

```py
@with_annotated(
mutually_exclusive_groups=(
Group("json", "csv", title="output", description="how to write results"),
),
mutually_exclusive_groups=(Group("json", "csv", title="output", description="how to write results"),),
)
def do_render(
self,
Expand All @@ -560,6 +580,7 @@ class Conn(ArgumentBlock):
host: Annotated[str, Option("--host")] = "localhost"
port: Annotated[int, Option("--port")] = 8080


@with_annotated(groups=(Group("host", "port", title="connection"),))
def do_connect(self, conn: Conn) -> None: ...
```
Expand Down Expand Up @@ -611,6 +632,7 @@ def do_manage(self, *, cmd2_subcommand_func):
if cmd2_subcommand_func:
cmd2_subcommand_func()


@with_annotated(subcommand_to="manage", help="list projects")
def manage_list(self):
self.poutput("listing")
Expand All @@ -626,6 +648,7 @@ def manage_project(self, *, cmd2_subcommand_func):
if cmd2_subcommand_func:
cmd2_subcommand_func()


@with_annotated(subcommand_to="manage project", help="add a project")
def manage_project_add(self, name: str):
self.poutput(f"added {name}")
Expand Down Expand Up @@ -770,9 +793,11 @@ decorator, it skips the first parameter as the method receiver (`self`/`cls`).
```py
from cmd2.annotated import build_parser_from_function


def greet(self, name: str, count: int = 1):
"""Greet someone."""


parser = build_parser_from_function(greet)
namespace = parser.parse_args(["Alice", "--count", "3"])
# namespace.name == "Alice", namespace.count == 3
Expand Down
Loading
Loading