Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(parser): delete doc-based command configuration parser #239

Merged
merged 6 commits into from
Sep 5, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

- Replaced `Terminal` class with `shutil.get_terminal_size()` from standard library
[#175](https://github.com/python-poetry/cleo/pull/175).

- Removed doc comment-based command configuration notation
[#239](https://github.com/python-poetry/cleo/pull/175).

## [0.8.1] - 2020-04-17

Expand Down
190 changes: 79 additions & 111 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,26 @@ To make a command that greets you from the command line, create

```python
from cleo.commands.command import Command

from cleo.helpers import argument, option

class GreetCommand(Command):
"""
Greets someone

greet
{name? : Who do you want to greet?}
{--y|yell : If set, the task will yell in uppercase letters}
"""
name = "greet"
description = "Greets someone"
arguments = [
argument(
"name",
description="Who do you want to greet?",
optional=True
)
]
options = [
option(
"yell",
"y",
description="If set, the task will yell in uppercase letters",
flag=True
)
]

def handle(self):
name = self.argument("name")
Expand Down Expand Up @@ -84,35 +94,6 @@ This prints:
HELLO JOHN
```

As you may have already seen, Cleo uses the command docstring to
determine the command definition. The docstring must be in the following
form :

```python
"""
Command description

Command signature
"""
```

The signature being in the following form:

```python
"""
command:name {argument : Argument description} {--option : Option description}
"""
```

The signature can span multiple lines.

```python
"""
command:name
{argument : Argument description}
{--option : Option description}
"""
```

### Coloring the Output

Expand Down Expand Up @@ -211,14 +192,27 @@ argument to the command and make the `name` argument required:

```python
class GreetCommand(Command):
"""
Greets someone

greet
{name : Who do you want to greet?}
{last_name? : Your last name?}
{--y|yell : If set, the task will yell in uppercase letters}
"""
name = "greet"
description = "Greets someone"
arguments = [
argument(
"name",
description="Who do you want to greet?",
),
argument(
"last_name",
description="Your last name?",
optional=True
)
]
options = [
option(
"yell",
"y",
description="If set, the task will yell in uppercase letters",
flag=True
)
]
```

You now have access to a `last_name` argument in your command:
Expand All @@ -242,13 +236,23 @@ the end of the argument list:

```python
class GreetCommand(Command):
"""
Greets someone

greet
{names* : Who do you want to greet?}
{--y|yell : If set, the task will yell in uppercase letters}
"""
name = "greet"
description = "Greets someone"
arguments = [
argument(
"names",
description="Who do you want to greet?",
multiple=True
)
]
options = [
option(
"yell",
"y",
description="If set, the task will yell in uppercase letters",
flag=True
)
]
```

To use this, just specify as many names as you want:
Expand All @@ -265,34 +269,6 @@ if names:
text = "Hello " + ", ".join(names)
```

There are 3 argument variants you can use:

| Mode | Notation | Value |
| -------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| Required | none (just write the argument name) | The argument is required |
| Optional | `argument?` | The argument is optional and therefore can be omitted |
| List | `argument*` | The argument can contain an indefinite number of arguments and must be used at the end of the argument list |

You can combine them like this:

```python
class GreetCommand(Command):
"""
Greets someone

greet
{names?* : Who do you want to greet?}
{--y|yell : If set, the task will yell in uppercase letters}
"""
```

If you want to set a default value, you can it like so:

```text
argument=default
```

The argument will then be considered optional.

### Using Options

Expand All @@ -312,20 +288,34 @@ how many times in a row the message should be printed:

```python
class GreetCommand(Command):
"""
Greets someone

greet
{name? : Who do you want to greet?}
{--y|yell : If set, the task will yell in uppercase letters}
{--iterations=1 : How many times should the message be printed?}
"""
name = "greet"
description = "Greets someone"
arguments = [
argument(
"name",
description="Who do you want to greet?",
optional=True
)
]
options = [
option(
"yell",
"y",
description="If set, the task will yell in uppercase letters",
flag=True
),
option(
"iterations",
description="How many times should the message be printed?",
default=1
)
]
```

Next, use this in the command to print the message multiple times:

```python
for _ in range(0, int(self.option("iterations"))):
for _ in range(int(self.option("iterations"))):
self.line(text)
```

Expand All @@ -348,28 +338,6 @@ $ python application.py greet John --iterations=5 --yell
$ python application.py greet John --yell --iterations=5
```

There are 4 option variants you can use:

| Option | Notation | Value |
| -------------- | ------------ | ----------------------------------------------------------------------------------- |
| List | `--option=*` | This option accepts multiple values (e.g. `--dir=/foo --dir=/bar`) |
| Flag | `--option` | Do not accept input for this option (e.g. `--yell`) |
| Requires value | `--option=` | This value is required (e.g. `--iterations=5`), the option itself is still optional |
| Optional value | `--option=?` | This option may or may not have a value (e.g. `--yell` or `--yell=loud`) |

You can combine them like this:

```python
class GreetCommand(Command):
"""
Greets someone

greet
{name? : Who do you want to greet?}
{--y|yell : If set, the task will yell in uppercase letters}
{--iterations=?*1 : How many times should the message be printed?}
"""
```

### Testing Commands

Expand Down
31 changes: 0 additions & 31 deletions src/cleo/commands/command.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
from __future__ import annotations

import inspect
import re

from typing import TYPE_CHECKING
from typing import Any
from typing import ContextManager
Expand All @@ -13,7 +10,6 @@
from cleo.io.inputs.string_input import StringInput
from cleo.io.null_io import NullIO
from cleo.io.outputs.output import Verbosity
from cleo.parser import Parser
from cleo.ui.table_separator import TableSeparator


Expand Down Expand Up @@ -47,40 +43,13 @@ def io(self) -> IO:
return self._io

def configure(self) -> None:
if not self.name:
doc = self.__doc__

if not doc:
for base in inspect.getmro(self.__class__):
if base.__doc__ is not None:
doc = base.__doc__
break

if doc:
self._parse_doc(doc)

for argument in self.arguments:
self._definition.add_argument(argument)

for option in self.options:
self._definition.add_option(option)

def _parse_doc(self, doc: str) -> None:
lines = doc.strip().split("\n", 1)
if len(lines) > 1:
self.description = lines[0].strip()
signature = re.sub(r"\s{2,}", " ", lines[1].strip())
definition = Parser.parse(signature)
self.name = definition["name"]

for argument in definition["arguments"]:
self._definition.add_argument(argument)

for option in definition["options"]:
self._definition.add_option(option)
else:
self.description = lines[0].strip()

def execute(self, io: IO) -> int:
self._io = io

Expand Down