freyacli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- freyacli-0.1.0/LICENSE.md +22 -0
- freyacli-0.1.0/PKG-INFO +91 -0
- freyacli-0.1.0/README.md +64 -0
- freyacli-0.1.0/freyacli/__init__.py +18 -0
- freyacli-0.1.0/freyacli/_arguments/__init__.py +0 -0
- freyacli-0.1.0/freyacli/_arguments/arg_count.py +118 -0
- freyacli-0.1.0/freyacli/_arguments/arg_dtype.py +61 -0
- freyacli-0.1.0/freyacli/_arguments/argument_rule.py +217 -0
- freyacli-0.1.0/freyacli/_arguments/subcommand.py +170 -0
- freyacli-0.1.0/freyacli/_core/__init__.py +0 -0
- freyacli-0.1.0/freyacli/_core/app.py +164 -0
- freyacli-0.1.0/freyacli/_misc/__init__.py +0 -0
- freyacli-0.1.0/freyacli/_misc/arg_dtype_error.py +12 -0
- freyacli-0.1.0/freyacli/_misc/freya_syntax_error.py +12 -0
- freyacli-0.1.0/freyacli/_misc/help_str.py +53 -0
- freyacli-0.1.0/freyacli/_parsers/__init__.py +0 -0
- freyacli-0.1.0/freyacli/_parsers/args_parser.py +219 -0
- freyacli-0.1.0/freyacli/_parsers/freya_parser.py +161 -0
- freyacli-0.1.0/freyacli/_utils/__init__.py +0 -0
- freyacli-0.1.0/freyacli/_utils/color.py +69 -0
- freyacli-0.1.0/freyacli/_utils/path_assertion.py +60 -0
- freyacli-0.1.0/freyacli/_version.py +1 -0
- freyacli-0.1.0/freyacli.egg-info/PKG-INFO +91 -0
- freyacli-0.1.0/freyacli.egg-info/SOURCES.txt +27 -0
- freyacli-0.1.0/freyacli.egg-info/dependency_links.txt +1 -0
- freyacli-0.1.0/freyacli.egg-info/top_level.txt +1 -0
- freyacli-0.1.0/pyproject.toml +3 -0
- freyacli-0.1.0/setup.cfg +4 -0
- freyacli-0.1.0/setup.py +26 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Diego Barquero Morera
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
freyacli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: freyacli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI framework for Python
|
|
5
|
+
Home-page: https://github.com/diegobarmor/freya-cli
|
|
6
|
+
Author: DiegoBarMor
|
|
7
|
+
Author-email: diegobarmor42@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: cli terminal user interface
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE.md
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: keywords
|
|
23
|
+
Dynamic: license
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# FREYA CLI
|
|
29
|
+
**Freya CLI** (FRamEwork for pYthon Applications) is, as the name suggests, a Python framework for building Command Line Interface (CLI) applications. It allows developers to use a set of well defined rules and specifications for the intended usage of the CLI (via the `FYR` format), while giving an intuitive idea to the users on how the app's CLI expects to receive.
|
|
30
|
+
|
|
31
|
+
As a bonus, **Freya CLI** provides a simple utility for coloring any printed text, via the `freyacli.Color` class. The colors provided are black, white, red, green, blue, yellow, magenta, cyan; with their respective *bright* variations, for a total of 16 colors.
|
|
32
|
+
|
|
33
|
+
## FYR / FYH format
|
|
34
|
+
- `FYR` (FreYa Rules) specifies a list of **rules** that must be followed when using an application's CLI. These rules are organized inside a hierarchy structure composed of **subcommand** nodes.
|
|
35
|
+
- `FYH` (FreYa Helps) should follow the same structure as its respective `FYR`. Instead of specifying the CLI rules, it stores the help strings that will be displayed whenever an incorrect use of the CLI is detected.
|
|
36
|
+
|
|
37
|
+
Below is an explanation of the argument **rules** and the **subcommand** nodes.
|
|
38
|
+
|
|
39
|
+
### Argument Rules
|
|
40
|
+
Argument rules specify how the arguments should be passed to the CLI, and are specified in the `FYR` format. The simplest rule has the form:
|
|
41
|
+
```
|
|
42
|
+
name[]1.STR
|
|
43
|
+
```
|
|
44
|
+
where `name` is both the name that will be used to fetch the user values, and the name displayed in the automatic help string. The empty brackets represent that this argument is **positional** (e.g. like how the argument is passed to the command `cat README.md`). The expected type is passed after a `.`, in this case `STR` (`1` indicates that one value is expected, more on that [later](#argument-count)). Supported types are:
|
|
45
|
+
- `STR`: string
|
|
46
|
+
- `PATH`: Python `Path` instance
|
|
47
|
+
- `INT`: integer
|
|
48
|
+
- `FLOAT`: float
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
If the argument is intended to be a simple on/off flag instead (e.g. `-a` in `ls -a`), it would instead look like
|
|
52
|
+
```
|
|
53
|
+
name[a,all]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This means that the flag `name` can be toggled on by passing either the flag `-a` (short name) or `--all` (long name). Short names can only be 1-character long; numbers are currently not allowed. If only a short name is to be defined, you can write it as `[a,]`; if only a long name is needed instead, it can be written as `[,all]`.
|
|
57
|
+
|
|
58
|
+
Naturally, toggle flags should be optional by nature. To indicate that an argument is **optional**, a `~``char should be placed in front of the name. This applies to any kind of argument, be it positional or flags. So the previous example would be:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
~name[a,all]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Flags can also have a value attached to them. It will look similar to the positional argument syntax, with the added short/long flag names inside the brackets, e.g.:
|
|
65
|
+
```
|
|
66
|
+
~path_file[i,input]1.PATH
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Finally, default values can be specified inside the `FYR` too. These will be assigned to optional arguments that the user didn't specify. It uses a `=` at the end of the rule (limitation: spaces are not allowed inside default values), e.g.:
|
|
70
|
+
```
|
|
71
|
+
~path_file[i,input]1.PATH=README.md
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Argument Count
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
### Subcommands
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
### Macros
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
### Help String
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
## Current limitations (TODO)
|
|
87
|
+
- Default values shouldn't have spaces or newlines inside
|
|
88
|
+
- Flag --help/-h is currently reserved.
|
|
89
|
+
- Positional arguments / options (flags) aren't currently supported for non-leaf **subcommand** nodes.
|
|
90
|
+
- Execution of the app must currently happen at a leaf **subcommand** node.
|
|
91
|
+
- Short flag names should not be a digit. Otherwise the parser will confuse it as a float value (e.g. `-3`).
|
freyacli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# FREYA CLI
|
|
2
|
+
**Freya CLI** (FRamEwork for pYthon Applications) is, as the name suggests, a Python framework for building Command Line Interface (CLI) applications. It allows developers to use a set of well defined rules and specifications for the intended usage of the CLI (via the `FYR` format), while giving an intuitive idea to the users on how the app's CLI expects to receive.
|
|
3
|
+
|
|
4
|
+
As a bonus, **Freya CLI** provides a simple utility for coloring any printed text, via the `freyacli.Color` class. The colors provided are black, white, red, green, blue, yellow, magenta, cyan; with their respective *bright* variations, for a total of 16 colors.
|
|
5
|
+
|
|
6
|
+
## FYR / FYH format
|
|
7
|
+
- `FYR` (FreYa Rules) specifies a list of **rules** that must be followed when using an application's CLI. These rules are organized inside a hierarchy structure composed of **subcommand** nodes.
|
|
8
|
+
- `FYH` (FreYa Helps) should follow the same structure as its respective `FYR`. Instead of specifying the CLI rules, it stores the help strings that will be displayed whenever an incorrect use of the CLI is detected.
|
|
9
|
+
|
|
10
|
+
Below is an explanation of the argument **rules** and the **subcommand** nodes.
|
|
11
|
+
|
|
12
|
+
### Argument Rules
|
|
13
|
+
Argument rules specify how the arguments should be passed to the CLI, and are specified in the `FYR` format. The simplest rule has the form:
|
|
14
|
+
```
|
|
15
|
+
name[]1.STR
|
|
16
|
+
```
|
|
17
|
+
where `name` is both the name that will be used to fetch the user values, and the name displayed in the automatic help string. The empty brackets represent that this argument is **positional** (e.g. like how the argument is passed to the command `cat README.md`). The expected type is passed after a `.`, in this case `STR` (`1` indicates that one value is expected, more on that [later](#argument-count)). Supported types are:
|
|
18
|
+
- `STR`: string
|
|
19
|
+
- `PATH`: Python `Path` instance
|
|
20
|
+
- `INT`: integer
|
|
21
|
+
- `FLOAT`: float
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
If the argument is intended to be a simple on/off flag instead (e.g. `-a` in `ls -a`), it would instead look like
|
|
25
|
+
```
|
|
26
|
+
name[a,all]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This means that the flag `name` can be toggled on by passing either the flag `-a` (short name) or `--all` (long name). Short names can only be 1-character long; numbers are currently not allowed. If only a short name is to be defined, you can write it as `[a,]`; if only a long name is needed instead, it can be written as `[,all]`.
|
|
30
|
+
|
|
31
|
+
Naturally, toggle flags should be optional by nature. To indicate that an argument is **optional**, a `~``char should be placed in front of the name. This applies to any kind of argument, be it positional or flags. So the previous example would be:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
~name[a,all]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Flags can also have a value attached to them. It will look similar to the positional argument syntax, with the added short/long flag names inside the brackets, e.g.:
|
|
38
|
+
```
|
|
39
|
+
~path_file[i,input]1.PATH
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Finally, default values can be specified inside the `FYR` too. These will be assigned to optional arguments that the user didn't specify. It uses a `=` at the end of the rule (limitation: spaces are not allowed inside default values), e.g.:
|
|
43
|
+
```
|
|
44
|
+
~path_file[i,input]1.PATH=README.md
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### Argument Count
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
### Subcommands
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
### Macros
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
### Help String
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
## Current limitations (TODO)
|
|
60
|
+
- Default values shouldn't have spaces or newlines inside
|
|
61
|
+
- Flag --help/-h is currently reserved.
|
|
62
|
+
- Positional arguments / options (flags) aren't currently supported for non-leaf **subcommand** nodes.
|
|
63
|
+
- Execution of the app must currently happen at a leaf **subcommand** node.
|
|
64
|
+
- Short flag names should not be a digit. Otherwise the parser will confuse it as a float value (e.g. `-3`).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from ._misc.freya_syntax_error import FreyaSyntaxError
|
|
2
|
+
from ._misc.arg_dtype_error import ArgDTypeError
|
|
3
|
+
from ._misc.help_str import HelpStr
|
|
4
|
+
|
|
5
|
+
from ._utils.color import Color
|
|
6
|
+
from ._utils.path_assertion import PathAssertion
|
|
7
|
+
|
|
8
|
+
from ._arguments.arg_count import ArgCount
|
|
9
|
+
from ._arguments.arg_dtype import ArgDType
|
|
10
|
+
from ._arguments.argument_rule import ArgumentRule
|
|
11
|
+
from ._arguments.subcommand import Subcommand
|
|
12
|
+
|
|
13
|
+
from ._parsers.freya_parser import FreyaParser
|
|
14
|
+
from ._parsers.args_parser import ArgsParser
|
|
15
|
+
|
|
16
|
+
from ._core.app import App
|
|
17
|
+
|
|
18
|
+
WIDTH_TERMINAL: int = 1 # automatically set to the current terminal width when App is initialized
|
|
File without changes
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import freyacli as fy
|
|
2
|
+
|
|
3
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
4
|
+
class ArgCount:
|
|
5
|
+
"""
|
|
6
|
+
`FYR` syntax follows a similarity to regex conventions for specifying amount of expected arguments.
|
|
7
|
+
|
|
8
|
+
- `n` -> exactly n
|
|
9
|
+
- `min_nvalues = n`, `val_optional = False`, `unlimited = False`
|
|
10
|
+
- `FYR` example: `~sphere[s,sphere]4.FLOAT`
|
|
11
|
+
- this means that `-s` must be passed with exactly `4` float values.
|
|
12
|
+
- Automatic help string:
|
|
13
|
+
- n = 1: `<value>`
|
|
14
|
+
- n > 1: `<value(n)>`
|
|
15
|
+
|
|
16
|
+
- `n?` -> 0 or n
|
|
17
|
+
- `min_nvalues = n`, `val_optional = True`, `unlimited = False`
|
|
18
|
+
- `FYR` example: `~out_mrc[m,mrc]1?.PATH`
|
|
19
|
+
- this means `-m` can be passed with exactly `1` value or without a value.
|
|
20
|
+
- Automatic help string:
|
|
21
|
+
- n = 1: `[<value>]`
|
|
22
|
+
- n > 1: `[<value(n)>]`
|
|
23
|
+
|
|
24
|
+
- `*` -> 0 or more
|
|
25
|
+
- `min_nvalues = 0`, `val_optional = True`, `unlimited = True`
|
|
26
|
+
- `FYR` example: `~configs[c,config]*.STR`
|
|
27
|
+
- this means that `-c` can be passed with or without values. Amount of values is arbitrary.
|
|
28
|
+
- Automatic help string: `[<value...>]`
|
|
29
|
+
|
|
30
|
+
- `n+` -> n or more
|
|
31
|
+
- `min_nvalues = n`, `val_optional = False`, `unlimited = True`
|
|
32
|
+
- `FYR` example: `~resids[i,resids]1+.STR`
|
|
33
|
+
- this means that `-i` must be passed with *at least* `1` value.
|
|
34
|
+
- Automatic help string:
|
|
35
|
+
- n = 1: `<value...>`
|
|
36
|
+
- n > 1: `<value(n)[...]>`
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
# --------------------------------------------------------------------------
|
|
40
|
+
def __init__(self, min_nvalues: int = 0, val_optional: bool = False, unlimited: bool = False):
|
|
41
|
+
self.min_nvalues = min_nvalues # 0 for *, n otherwise
|
|
42
|
+
self.val_optional = val_optional # True for n? and *, False for n and n+
|
|
43
|
+
self.unlimited = unlimited # True for * and n+, False for n and n?
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --------------------------------------------------------------------------
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_str(cls, string: str) -> "ArgCount":
|
|
49
|
+
def _safe_substr(s: str) -> str:
|
|
50
|
+
substr = s[:-1]
|
|
51
|
+
if not substr: raise fy.FreyaSyntaxError(
|
|
52
|
+
f"Invalid argument count specification: '{s}'."+\
|
|
53
|
+
"Expected format examples: '3', '3?', '*', '3+'."+\
|
|
54
|
+
"Number is mandatory for all formats except '*', which always means '0 or more'."
|
|
55
|
+
)
|
|
56
|
+
return substr
|
|
57
|
+
|
|
58
|
+
def _safe_int(s: str) -> int:
|
|
59
|
+
try: return int(s)
|
|
60
|
+
except ValueError: raise fy.FreyaSyntaxError(
|
|
61
|
+
f"Invalid number in argument count specification: '{s}'."+\
|
|
62
|
+
"Expected format examples: '3', '3?', '*', '3+'."+\
|
|
63
|
+
"Number is mandatory for all formats except '*', which always means '0 or more'."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if string.endswith("?"): return cls(
|
|
67
|
+
min_nvalues = _safe_int(_safe_substr(string)),
|
|
68
|
+
unlimited = False,
|
|
69
|
+
val_optional = True
|
|
70
|
+
)
|
|
71
|
+
if string.endswith("*"): return cls(
|
|
72
|
+
min_nvalues = 0,
|
|
73
|
+
unlimited = True,
|
|
74
|
+
val_optional = True
|
|
75
|
+
)
|
|
76
|
+
if string.endswith("+"): return cls(
|
|
77
|
+
min_nvalues = _safe_int(_safe_substr(string)),
|
|
78
|
+
unlimited = True,
|
|
79
|
+
val_optional = False
|
|
80
|
+
)
|
|
81
|
+
return cls(
|
|
82
|
+
min_nvalues = _safe_int(string),
|
|
83
|
+
unlimited = False,
|
|
84
|
+
val_optional = False
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------------------
|
|
89
|
+
def get_help_str(self, buffer: str) -> str:
|
|
90
|
+
if self.val_optional:
|
|
91
|
+
if self.unlimited: return f"[<{buffer}...>]" # * case
|
|
92
|
+
if self.min_nvalues == 1: return f"[<{buffer}>]" # n? case with n=1
|
|
93
|
+
return f"[<{buffer}({self.min_nvalues})>]" # n? case with n>1
|
|
94
|
+
|
|
95
|
+
if self.unlimited:
|
|
96
|
+
if self.min_nvalues == 1: return f"<{buffer}...>" # n+ case with n=1
|
|
97
|
+
return f"<{buffer}({self.min_nvalues})[...]>" # n+ case with n>1
|
|
98
|
+
|
|
99
|
+
if self.min_nvalues == 1: return f"<{buffer}>" # n case with n=1
|
|
100
|
+
return f"<{buffer}({self.min_nvalues})>" # n case with n>1
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --------------------------------------------------------------------------
|
|
104
|
+
def is_full(self, nvalues: int) -> bool:
|
|
105
|
+
"""Returns `True` for an argument that shouldn't take more values."""
|
|
106
|
+
if self.unlimited: return False
|
|
107
|
+
return nvalues >= self.min_nvalues
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------------------
|
|
111
|
+
def has_enough_values(self, nvalues: int) -> bool:
|
|
112
|
+
"""Returns `True` for an argument that has already received enough values."""
|
|
113
|
+
if self.val_optional: return True
|
|
114
|
+
if self.unlimited: return nvalues >= self.min_nvalues
|
|
115
|
+
return nvalues == self.min_nvalues
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import freyacli as fy
|
|
5
|
+
|
|
6
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
7
|
+
class ArgDType(Enum):
|
|
8
|
+
NONE = auto()
|
|
9
|
+
TOGGLE = auto()
|
|
10
|
+
STR = auto()
|
|
11
|
+
PATH = auto()
|
|
12
|
+
INT = auto()
|
|
13
|
+
FLOAT = auto()
|
|
14
|
+
|
|
15
|
+
# --------------------------------------------------------------------------
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_str(cls, s: str | None) -> "ArgDType":
|
|
18
|
+
if s is None: return cls.NONE
|
|
19
|
+
s = s.lower()
|
|
20
|
+
if not s: return cls.TOGGLE
|
|
21
|
+
if s == "str": return cls.STR
|
|
22
|
+
if s == "path": return cls.PATH
|
|
23
|
+
if s == "float": return cls.FLOAT
|
|
24
|
+
if s == "int": return cls.INT
|
|
25
|
+
raise fy.FreyaSyntaxError(f"Invalid flag type specified: '{s}'")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# --------------------------------------------------------------------------
|
|
29
|
+
def stores_data(self) -> bool:
|
|
30
|
+
return self != ArgDType.NONE and self != ArgDType.TOGGLE
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------------------
|
|
33
|
+
def parse_value(self, arg_name: str, raw_value: str) -> fy.ArgDTypeError |\
|
|
34
|
+
bool|str|Path|int|float:
|
|
35
|
+
|
|
36
|
+
if self == ArgDType.NONE:
|
|
37
|
+
raise fy.FreyaSyntaxError(f"Argument doesn't accept any values, but somehow got '{raw_value}'.")
|
|
38
|
+
|
|
39
|
+
if self == ArgDType.TOGGLE:
|
|
40
|
+
raise fy.FreyaSyntaxError(f"Argument is a flag of type TOGGLE and doesn't accept any values, but somehow got '{raw_value}'.")
|
|
41
|
+
|
|
42
|
+
if self == ArgDType.STR:
|
|
43
|
+
return raw_value.strip('"').strip("'")
|
|
44
|
+
|
|
45
|
+
if self == ArgDType.PATH:
|
|
46
|
+
return Path(raw_value)
|
|
47
|
+
|
|
48
|
+
if self == ArgDType.INT:
|
|
49
|
+
try: return int(raw_value)
|
|
50
|
+
except ValueError: return fy.ArgDTypeError(
|
|
51
|
+
f"Argument '{arg_name}' expected an integer value, but got '{raw_value}'."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if self == ArgDType.FLOAT:
|
|
55
|
+
try: return float(raw_value)
|
|
56
|
+
except ValueError: return fy.ArgDTypeError(
|
|
57
|
+
f"Argument '{arg_name}' expected a float value, but got '{raw_value}'."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import freyacli as fy
|
|
4
|
+
|
|
5
|
+
# //////////////////////////////////////////////////////////////////////////////
|
|
6
|
+
class ArgumentRule:
|
|
7
|
+
_INDENT_LONG_DESC = 8
|
|
8
|
+
|
|
9
|
+
# --------------------------------------------------------------------------
|
|
10
|
+
def __init__(self, raw_rule: str | None):
|
|
11
|
+
self._raw_rule = raw_rule
|
|
12
|
+
self.help_str: fy.HelpStr = fy.HelpStr()
|
|
13
|
+
|
|
14
|
+
self._raw_user_values: list[str] = [] # raw string values provided by the user
|
|
15
|
+
self._n_user_values: int = 0 # amount of values provided by the user so far
|
|
16
|
+
self._was_used: bool = False # True when the flag of this argument was used at least once.
|
|
17
|
+
|
|
18
|
+
self.name: str = "" # name that will be displayed between <> (e.g. "<value>") in the help string, and used to fetch any given values
|
|
19
|
+
self.is_positional: bool = False # True when the argument doesn't need a preceding "flag" (e.g. just "<value>" instead of "--flag <value>")
|
|
20
|
+
self.is_optional: bool = False # display with surrounding [] in the help string (e.g. "[<value>]")
|
|
21
|
+
self.flag_short: str = "" # must be 1 character long. It's always used with a single preceding dash (e.g. "-f")
|
|
22
|
+
self.flag_long: str = "" # used with a double preceding dash (e.g. "--flag")
|
|
23
|
+
self.default_value: str = "" # when a default value is present, it's placed at the beginning of the respective help string, right after the flag type (e.g. "INT (default: 0)").
|
|
24
|
+
self.arg_dtype = fy.ArgDType.NONE # placed at the beginning of the help string. TOGGLE type is used for flags with no arguments, and the respective type (e.g. STR, INT) is used for flags with arguments.
|
|
25
|
+
self.arg_count = fy.ArgCount() # contains information about the expected number of passed values and whether they are optional.
|
|
26
|
+
|
|
27
|
+
if raw_rule is None: return
|
|
28
|
+
|
|
29
|
+
self.is_optional = raw_rule.startswith('~')
|
|
30
|
+
|
|
31
|
+
(buffer,
|
|
32
|
+
self.default_value
|
|
33
|
+
) = self._parse_default_value(raw_rule)
|
|
34
|
+
|
|
35
|
+
(buffer,
|
|
36
|
+
self.name
|
|
37
|
+
) = self._parse_name(buffer)
|
|
38
|
+
|
|
39
|
+
(buffer,
|
|
40
|
+
self.is_positional,
|
|
41
|
+
self.flag_short,
|
|
42
|
+
self.flag_long
|
|
43
|
+
) = self._parse_flags(buffer)
|
|
44
|
+
|
|
45
|
+
(buffer,
|
|
46
|
+
self.arg_count
|
|
47
|
+
) = self._parse_arg_count(buffer)
|
|
48
|
+
|
|
49
|
+
self.arg_dtype = fy.ArgDType.from_str(buffer)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --------------------------------------------------------------------------
|
|
53
|
+
def register_user_value(self, value: str) -> bool:
|
|
54
|
+
"""Returns `True` when the argument is "full" (i.e. won't accept any more user values)"""
|
|
55
|
+
self._raw_user_values.append(value)
|
|
56
|
+
self._n_user_values += 1
|
|
57
|
+
self.touch()
|
|
58
|
+
return self.arg_count.is_full(self._n_user_values)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --------------------------------------------------------------------------
|
|
62
|
+
def touch(self):
|
|
63
|
+
"""
|
|
64
|
+
Marks the argument as used even if it doesn't receive any user values.
|
|
65
|
+
Useful when dealing with flags with optional keyword arguments
|
|
66
|
+
"""
|
|
67
|
+
self._was_used = True
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --------------------------------------------------------------------------
|
|
71
|
+
def parse_user_values(self) -> None | fy.ArgDTypeError |\
|
|
72
|
+
bool|str|Path|int|float |\
|
|
73
|
+
list[bool]|list[str]|list[Path]|list[int]|list[float]:
|
|
74
|
+
"""
|
|
75
|
+
Parses the raw user values according to the argument type (e.g. `INT`, `STR`, etc.) and returns the parsed values in the respective types.
|
|
76
|
+
When the argument accepts only 1 value, the value is returned directly instead of a `list`.
|
|
77
|
+
A boolean is returned for flags of `TOGGLE` type. Alternatively, `True` can be returned when a flag is passed but its optional keyword argument isn't.
|
|
78
|
+
An `ArgDTypeError` instance is returned when parsing fails due to invalid user input.
|
|
79
|
+
`None` is returned for non-used optional arguments, be it positional or non-toggle flags (i.e. those that should receive values).
|
|
80
|
+
"""
|
|
81
|
+
if self._was_used and not self.arg_count.has_enough_values(self._n_user_values):
|
|
82
|
+
s = 's' if self.arg_count.min_nvalues > 1 else ''
|
|
83
|
+
return fy.ArgDTypeError(f"Expected {self.arg_count.min_nvalues} value{s} for argument '{self.name}', but got {len(self._raw_user_values)}.")
|
|
84
|
+
|
|
85
|
+
if self.arg_dtype in (fy.ArgDType.INT, fy.ArgDType.FLOAT):
|
|
86
|
+
### for numeric types, split the raw user values, just in case they are received as a single space-separated string
|
|
87
|
+
self._raw_user_values = [
|
|
88
|
+
v for lst in self._raw_user_values for v in lst.split()
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
parsed_values = [
|
|
92
|
+
self.arg_dtype.parse_value(self.name, raw_value)
|
|
93
|
+
for raw_value in self._raw_user_values
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
if not parsed_values:
|
|
97
|
+
if self._was_used: return True # used flag has value of "True"
|
|
98
|
+
if self.is_positional: return None # unused optional positional argument has value of "None"
|
|
99
|
+
if self.arg_dtype is fy.ArgDType.TOGGLE: return False # unused toggle flag has value of "False"
|
|
100
|
+
return None # unused optional flag with arguments has value of "None"
|
|
101
|
+
|
|
102
|
+
if self.arg_count.min_nvalues == 1: return parsed_values[0]
|
|
103
|
+
return parsed_values
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# --------------------------------------------------------------------------
|
|
107
|
+
def get_usage_str_positional(self) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Usage string is only relevant for positionals.
|
|
110
|
+
Flag arguments are summarized as [options...] in the usage string.
|
|
111
|
+
A more detailed description of them is provided in the longer help string.
|
|
112
|
+
"""
|
|
113
|
+
if not self.is_positional:
|
|
114
|
+
raise fy.FreyaSyntaxError(f"Usage string can only be generated for positional arguments, but got a flag argument ('{self._raw_rule}').")
|
|
115
|
+
|
|
116
|
+
buffer = self.arg_count.get_help_str(self.name)
|
|
117
|
+
return fy.Color.blue(buffer)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# --------------------------------------------------------------------------
|
|
121
|
+
def get_help_string_description(self) -> str:
|
|
122
|
+
preffix = ""
|
|
123
|
+
if self.is_optional: preffix += "[optional] "
|
|
124
|
+
if self.arg_dtype.stores_data():
|
|
125
|
+
preffix += self.arg_dtype.name
|
|
126
|
+
if self.default_value: preffix += f" (default: {self.default_value})"
|
|
127
|
+
preffix += ". "
|
|
128
|
+
|
|
129
|
+
arg_desc = self._get_arg_description()
|
|
130
|
+
long_desc = self.help_str.wrapped_text(
|
|
131
|
+
indent = self._INDENT_LONG_DESC,
|
|
132
|
+
width = fy.WIDTH_TERMINAL - self._INDENT_LONG_DESC,
|
|
133
|
+
preffix = fy.Color.yellow(preffix)
|
|
134
|
+
)
|
|
135
|
+
return f"{arg_desc}\n{self._INDENT_LONG_DESC*' '}{long_desc}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# --------------------------------------------------------------------------
|
|
139
|
+
def _parse_default_value(self, buffer: str) -> tuple[str, str]:
|
|
140
|
+
idx = buffer.find('=')
|
|
141
|
+
if idx == -1: return buffer, ''
|
|
142
|
+
return buffer[:idx], buffer[idx+1:]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# --------------------------------------------------------------------------
|
|
146
|
+
def _parse_name(self, buffer: str) -> tuple[str, str]:
|
|
147
|
+
name, buffer = self._safe_split_into_2(buffer, '[')
|
|
148
|
+
return buffer, name[int(self.is_optional):]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# --------------------------------------------------------------------------
|
|
152
|
+
def _parse_flags(self, buffer: str) -> tuple[str, bool, str, str]:
|
|
153
|
+
flags, buffer = self._safe_split_into_2(buffer, ']')
|
|
154
|
+
if not flags: return buffer, True, '', ''
|
|
155
|
+
|
|
156
|
+
flag_short, flag_long = self._safe_split_into_2(flags, ',')
|
|
157
|
+
if len(flag_short) > 1:
|
|
158
|
+
raise fy.FreyaSyntaxError(f"Short flags should be 1 character long, but got '{self.flag_short}'.")
|
|
159
|
+
|
|
160
|
+
return buffer, False, flag_short, flag_long
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# --------------------------------------------------------------------------
|
|
164
|
+
def _parse_arg_count(self, buffer: str) -> fy.ArgCount:
|
|
165
|
+
if not buffer:
|
|
166
|
+
if self.default_value:
|
|
167
|
+
raise fy.FreyaSyntaxError(f"Flags with no arguments can't have a default value ('{self._raw_rule}')")
|
|
168
|
+
if self.is_positional:
|
|
169
|
+
raise fy.FreyaSyntaxError(f"Positional arguments must specify type and number of arguments ('{self._raw_rule}')")
|
|
170
|
+
|
|
171
|
+
return "", fy.ArgCount()
|
|
172
|
+
|
|
173
|
+
min_nvalues, buffer = self._safe_split_into_2(buffer, '.')
|
|
174
|
+
arg_count = fy.ArgCount.from_str(min_nvalues)
|
|
175
|
+
if self.is_positional:
|
|
176
|
+
### having a distinction between self.is_optional (e.g. "passing -x" is optional)
|
|
177
|
+
### vs arg_count.val_optional (e.g. "passing a value for -x" is optional)
|
|
178
|
+
### is only relevant for flags, not for positionals
|
|
179
|
+
arg_count.val_optional = self.is_optional
|
|
180
|
+
|
|
181
|
+
return buffer, arg_count
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --------------------------------------------------------------------------
|
|
185
|
+
def _safe_split_into_2(self, buffer: str, char_split: str) -> tuple[str, str]:
|
|
186
|
+
splitted = buffer.split(char_split)
|
|
187
|
+
if len(splitted) == 2: return splitted
|
|
188
|
+
raise fy.FreyaSyntaxError(f"Invalid substring found inside argument rule: '{self._raw_rule}'")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# --------------------------------------------------------------------------
|
|
192
|
+
def _get_arg_description(self) -> str:
|
|
193
|
+
buffer = self.arg_count.get_help_str(self.name)
|
|
194
|
+
if self.is_positional: return fy.Color.blue(" " + buffer)
|
|
195
|
+
|
|
196
|
+
buffer = f" {buffer}" if self.arg_dtype.stores_data() else ""
|
|
197
|
+
|
|
198
|
+
if self.is_optional:
|
|
199
|
+
str_flags_0 = "["
|
|
200
|
+
str_flags_1 = "]"
|
|
201
|
+
else:
|
|
202
|
+
str_flags_0 = ""
|
|
203
|
+
str_flags_1 = ""
|
|
204
|
+
|
|
205
|
+
flag_short = f"-{self.flag_short}" if self.flag_short else ""
|
|
206
|
+
flag_long = f"--{self.flag_long}" if self.flag_long else ""
|
|
207
|
+
|
|
208
|
+
str_flags_0 += (
|
|
209
|
+
f"{flag_short}, {flag_long}" if self.flag_long else flag_short
|
|
210
|
+
) if self.flag_short else flag_long
|
|
211
|
+
|
|
212
|
+
buffer = f" {fy.Color.green(str_flags_0)}{fy.Color.blue(buffer)}"
|
|
213
|
+
if str_flags_1: buffer += fy.Color.green(str_flags_1)
|
|
214
|
+
return buffer
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# //////////////////////////////////////////////////////////////////////////////
|