dykes 0.2.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.
- dykes-0.2.0/LICENSE.txt +21 -0
- dykes-0.2.0/PKG-INFO +97 -0
- dykes-0.2.0/README.md +82 -0
- dykes-0.2.0/pyproject.toml +31 -0
- dykes-0.2.0/src/dykes/__init__.py +138 -0
dykes-0.2.0/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright © 2025 Piper Thunstrom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the “Software”), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
+
so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
dykes-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dykes
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A tiny declarative Argparse wrapper.
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Piper Thunstrom
|
|
7
|
+
Author-email: pathunstrom@gmail.com
|
|
8
|
+
Requires-Python: >=3.12,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# Do You Know Every Selection?
|
|
16
|
+
|
|
17
|
+
`dykes` helps you get a handle on your tools.
|
|
18
|
+
|
|
19
|
+
It uses `typing.NamedTuples` or `dataclasses` to declaratively set up your argument parser and then returns an instance of your argument class.
|
|
20
|
+
|
|
21
|
+
An example usage, in example_application.py
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Annotated
|
|
26
|
+
|
|
27
|
+
import dykes
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ExampleApplication:
|
|
31
|
+
path: Annotated[Path, "The paths to operate on."]
|
|
32
|
+
dry_run: bool
|
|
33
|
+
prompt: dykes.StoreFalse
|
|
34
|
+
verbosity: dykes.Count
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
arguments = dykes.parse_args(ExampleApplication)
|
|
38
|
+
print(arguments)
|
|
39
|
+
|
|
40
|
+
Use this from the command line:
|
|
41
|
+
|
|
42
|
+
python example_application.py ~ -d -vv
|
|
43
|
+
|
|
44
|
+
And the output looks like:
|
|
45
|
+
|
|
46
|
+
ExampleApplication(path=Path('~'), dry_run=True, prompt=True, verbosity=2)
|
|
47
|
+
|
|
48
|
+
## Inlining `dykes`
|
|
49
|
+
|
|
50
|
+
While `dykes` is packaged to be used with pip, it's operational code is in a single file.
|
|
51
|
+
If you would like to vendor it, take the `src/dykes/__init__.py` and include it in your own project.
|
|
52
|
+
|
|
53
|
+
## What works
|
|
54
|
+
|
|
55
|
+
* Positional parameters
|
|
56
|
+
* Store True flags
|
|
57
|
+
* Two variants: type a `bool` or use `dykes.StoreTrue`
|
|
58
|
+
* Store False flags
|
|
59
|
+
* Count flags
|
|
60
|
+
* A StrEnum of Argparse actions. `dykes.Action` instances can be passed to directly to `argparse.add_parameter`
|
|
61
|
+
* Parameter help strings: provide a bare string via Annotated
|
|
62
|
+
* Application description via your `dataclass` or `NamedTuple`'s docstring.
|
|
63
|
+
* `snake_case` field names converted to `--kebab-case` long options.
|
|
64
|
+
|
|
65
|
+
## What works but is underwhelming
|
|
66
|
+
|
|
67
|
+
* Parameter defaults. (positional parameters can't use them, and the other supported fields have good ones.)
|
|
68
|
+
|
|
69
|
+
## Coming Soon
|
|
70
|
+
|
|
71
|
+
* More actions
|
|
72
|
+
* Store Const
|
|
73
|
+
* Append
|
|
74
|
+
* Append Const
|
|
75
|
+
* Extend
|
|
76
|
+
* Version
|
|
77
|
+
* More Options
|
|
78
|
+
* Defining custom flags (currently derived from names)
|
|
79
|
+
* nargs
|
|
80
|
+
* const
|
|
81
|
+
* choices
|
|
82
|
+
* required
|
|
83
|
+
* deprecated
|
|
84
|
+
* Proper documentation
|
|
85
|
+
|
|
86
|
+
## Coming Maybe
|
|
87
|
+
|
|
88
|
+
* Application framework based on `__call__`
|
|
89
|
+
* Subcommands
|
|
90
|
+
|
|
91
|
+
## Isn't That Name Insensitive?
|
|
92
|
+
|
|
93
|
+
Author and maintainer here: I am a transgender lesbian and I find it funny.
|
|
94
|
+
If you don't want that word in your project, that's fine!
|
|
95
|
+
Please see instructions above on how to inline the project.
|
|
96
|
+
Feel free to rename it.
|
|
97
|
+
|
dykes-0.2.0/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Do You Know Every Selection?
|
|
2
|
+
|
|
3
|
+
`dykes` helps you get a handle on your tools.
|
|
4
|
+
|
|
5
|
+
It uses `typing.NamedTuples` or `dataclasses` to declaratively set up your argument parser and then returns an instance of your argument class.
|
|
6
|
+
|
|
7
|
+
An example usage, in example_application.py
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Annotated
|
|
12
|
+
|
|
13
|
+
import dykes
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ExampleApplication:
|
|
17
|
+
path: Annotated[Path, "The paths to operate on."]
|
|
18
|
+
dry_run: bool
|
|
19
|
+
prompt: dykes.StoreFalse
|
|
20
|
+
verbosity: dykes.Count
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
arguments = dykes.parse_args(ExampleApplication)
|
|
24
|
+
print(arguments)
|
|
25
|
+
|
|
26
|
+
Use this from the command line:
|
|
27
|
+
|
|
28
|
+
python example_application.py ~ -d -vv
|
|
29
|
+
|
|
30
|
+
And the output looks like:
|
|
31
|
+
|
|
32
|
+
ExampleApplication(path=Path('~'), dry_run=True, prompt=True, verbosity=2)
|
|
33
|
+
|
|
34
|
+
## Inlining `dykes`
|
|
35
|
+
|
|
36
|
+
While `dykes` is packaged to be used with pip, it's operational code is in a single file.
|
|
37
|
+
If you would like to vendor it, take the `src/dykes/__init__.py` and include it in your own project.
|
|
38
|
+
|
|
39
|
+
## What works
|
|
40
|
+
|
|
41
|
+
* Positional parameters
|
|
42
|
+
* Store True flags
|
|
43
|
+
* Two variants: type a `bool` or use `dykes.StoreTrue`
|
|
44
|
+
* Store False flags
|
|
45
|
+
* Count flags
|
|
46
|
+
* A StrEnum of Argparse actions. `dykes.Action` instances can be passed to directly to `argparse.add_parameter`
|
|
47
|
+
* Parameter help strings: provide a bare string via Annotated
|
|
48
|
+
* Application description via your `dataclass` or `NamedTuple`'s docstring.
|
|
49
|
+
* `snake_case` field names converted to `--kebab-case` long options.
|
|
50
|
+
|
|
51
|
+
## What works but is underwhelming
|
|
52
|
+
|
|
53
|
+
* Parameter defaults. (positional parameters can't use them, and the other supported fields have good ones.)
|
|
54
|
+
|
|
55
|
+
## Coming Soon
|
|
56
|
+
|
|
57
|
+
* More actions
|
|
58
|
+
* Store Const
|
|
59
|
+
* Append
|
|
60
|
+
* Append Const
|
|
61
|
+
* Extend
|
|
62
|
+
* Version
|
|
63
|
+
* More Options
|
|
64
|
+
* Defining custom flags (currently derived from names)
|
|
65
|
+
* nargs
|
|
66
|
+
* const
|
|
67
|
+
* choices
|
|
68
|
+
* required
|
|
69
|
+
* deprecated
|
|
70
|
+
* Proper documentation
|
|
71
|
+
|
|
72
|
+
## Coming Maybe
|
|
73
|
+
|
|
74
|
+
* Application framework based on `__call__`
|
|
75
|
+
* Subcommands
|
|
76
|
+
|
|
77
|
+
## Isn't That Name Insensitive?
|
|
78
|
+
|
|
79
|
+
Author and maintainer here: I am a transgender lesbian and I find it funny.
|
|
80
|
+
If you don't want that word in your project, that's fine!
|
|
81
|
+
Please see instructions above on how to inline the project.
|
|
82
|
+
Feel free to rename it.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dykes"
|
|
3
|
+
dynamic = []
|
|
4
|
+
description = "A tiny declarative Argparse wrapper."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Piper Thunstrom",email = "pathunstrom@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12,<4.0"
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
version = "0.2.0"
|
|
14
|
+
|
|
15
|
+
[tool.poetry]
|
|
16
|
+
|
|
17
|
+
[tool.poetry.group.dev.dependencies]
|
|
18
|
+
pytest = "^8.4.0"
|
|
19
|
+
sphinx = "^8.2.3"
|
|
20
|
+
ruff = "^0.11.12"
|
|
21
|
+
mypy = "^1.16.0"
|
|
22
|
+
|
|
23
|
+
[tool.poetry.requires-plugins]
|
|
24
|
+
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
|
|
25
|
+
|
|
26
|
+
[tool.poetry-dynamic-versioning]
|
|
27
|
+
enable = false
|
|
28
|
+
|
|
29
|
+
[build-system]
|
|
30
|
+
requires = ["poetry-core>=2.0.0,<3.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
|
31
|
+
build-backend = "poetry_dynamic_versioning.backend"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import dataclasses
|
|
3
|
+
import typing
|
|
4
|
+
from enum import StrEnum, auto
|
|
5
|
+
from inspect import getdoc
|
|
6
|
+
from sys import argv
|
|
7
|
+
from typing import get_type_hints
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass
|
|
11
|
+
class Flags:
|
|
12
|
+
value: list[str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Action(StrEnum):
|
|
16
|
+
"""
|
|
17
|
+
Actions for use with ArgumentParser.add_argument.
|
|
18
|
+
|
|
19
|
+
See https://docs.python.org/3/library/argparse.html#action for what each does.
|
|
20
|
+
|
|
21
|
+
Can be used directly:
|
|
22
|
+
|
|
23
|
+
parser = argparse.ArgumentParser
|
|
24
|
+
parser.add_argument("file_path", type=pathlib.Path, action=simple_parser.Action.STORE)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
STORE = auto()
|
|
28
|
+
STORE_CONST = auto()
|
|
29
|
+
STORE_TRUE = auto()
|
|
30
|
+
STORE_FALSE = auto()
|
|
31
|
+
APPEND = auto()
|
|
32
|
+
APPEND_CONST = auto()
|
|
33
|
+
EXTEND = auto()
|
|
34
|
+
COUNT = auto()
|
|
35
|
+
HELP = auto()
|
|
36
|
+
VERSION = auto()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
Count = typing.Annotated[int, Action.COUNT]
|
|
40
|
+
StoreTrue = typing.Annotated[bool, Action.STORE_TRUE]
|
|
41
|
+
StoreFalse = typing.Annotated[bool, Action.STORE_FALSE]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_args[ArgsType](
|
|
45
|
+
parameter_definition: type[ArgsType], *, args: list | None = None
|
|
46
|
+
) -> ArgsType:
|
|
47
|
+
"""
|
|
48
|
+
Your entry point.
|
|
49
|
+
"""
|
|
50
|
+
if args is None:
|
|
51
|
+
args = argv[1:]
|
|
52
|
+
parser = build_parser(parameter_definition)
|
|
53
|
+
parsed = parser.parse_args(args)
|
|
54
|
+
return parameter_definition(**vars(parsed))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclasses.dataclass
|
|
58
|
+
class Field:
|
|
59
|
+
name: str
|
|
60
|
+
value: typing.Any
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_parser(application_definition: type) -> argparse.ArgumentParser:
|
|
64
|
+
description = getdoc(application_definition)
|
|
65
|
+
parser = argparse.ArgumentParser(description=description)
|
|
66
|
+
hints = get_type_hints(application_definition, include_extras=True)
|
|
67
|
+
fields = _get_fields(application_definition)
|
|
68
|
+
for name, cls in hints.items():
|
|
69
|
+
action: Action | None = None
|
|
70
|
+
flags: typing.Sequence[str] | None = None
|
|
71
|
+
configuration: dict[str, typing.Any] = {
|
|
72
|
+
"help": None,
|
|
73
|
+
"default": fields[name].value,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (meta := getattr(cls, "__metadata__", None)) is not None:
|
|
77
|
+
for datum in meta:
|
|
78
|
+
if isinstance(datum, Action) and action is not None:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"Multiple actions in annotations. Please use only one Action."
|
|
81
|
+
)
|
|
82
|
+
elif isinstance(datum, Action):
|
|
83
|
+
action = datum
|
|
84
|
+
elif isinstance(datum, str) and configuration["help"] is not None:
|
|
85
|
+
raise (
|
|
86
|
+
ValueError(
|
|
87
|
+
"Multiple bare strings in annotation. Please use only one bare string in Annotation."
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
elif isinstance(datum, str):
|
|
91
|
+
configuration["help"] = datum
|
|
92
|
+
if flags is None:
|
|
93
|
+
flags = f"-{name[0]}", f"--{name.replace('_', '-')}"
|
|
94
|
+
if cls is bool or action is Action.STORE_TRUE:
|
|
95
|
+
del configuration["default"]
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
*flags, dest=name, action=Action.STORE_TRUE, **configuration
|
|
98
|
+
) # type:ignore
|
|
99
|
+
elif action is Action.COUNT:
|
|
100
|
+
default = configuration.pop("default") or 0
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
*flags, dest=name, action=action, default=default, **configuration
|
|
103
|
+
) # type:ignore
|
|
104
|
+
elif action is Action.STORE_FALSE:
|
|
105
|
+
del configuration["default"]
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
*flags, dest=name, action=Action.STORE_FALSE, **configuration
|
|
108
|
+
) # type:ignore
|
|
109
|
+
else:
|
|
110
|
+
if configuration["default"] is not None:
|
|
111
|
+
raise ValueError("Positional arguments cannot have defaults.")
|
|
112
|
+
parser.add_argument(name, type=cls, **configuration) # type:ignore
|
|
113
|
+
return parser
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@typing.runtime_checkable
|
|
117
|
+
class _NamedTupleProtocol(typing.Protocol):
|
|
118
|
+
_fields: tuple[str]
|
|
119
|
+
_field_defaults: dict[str, typing.Any]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_fields(cls: type) -> dict["str", Field]:
|
|
123
|
+
fields = {}
|
|
124
|
+
if dataclasses.is_dataclass(cls):
|
|
125
|
+
fields = {
|
|
126
|
+
field.name: Field(
|
|
127
|
+
field.name,
|
|
128
|
+
field.default if field.default is not dataclasses.MISSING else None,
|
|
129
|
+
)
|
|
130
|
+
for field in dataclasses.fields(cls)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return fields
|
|
134
|
+
elif isinstance(cls, _NamedTupleProtocol):
|
|
135
|
+
fields = {
|
|
136
|
+
field: Field(field, cls._field_defaults.get(field)) for field in cls._fields
|
|
137
|
+
}
|
|
138
|
+
return fields
|