Skip to content

Commit

Permalink
refactor(parser): delete doc-based command configuration parser (#239)
Browse files Browse the repository at this point in the history
* refactor(parser): delete doc-based command configuration parser

* refactor(parser): delete doc-based command configuration parser

* Update README and CHANGELOG

* Apply suggestions from code review

Co-authored-by: Branch Vincent <[email protected]>
  • Loading branch information
Secrus and branchvincent authored Sep 5, 2022
1 parent 37d4e48 commit e7687e9
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 466 deletions.
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
Loading

0 comments on commit e7687e9

Please sign in to comment.