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.
@@ -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