argparse-type-helper 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.
@@ -0,0 +1,16 @@
1
+ name: Conventional Commit Checker
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, edited, synchronize]
6
+
7
+ jobs:
8
+ check-for-cc:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Conventional Commit Checker
12
+ id: conventional-commit-checker
13
+ uses: agenthunt/conventional-commit-checker-action@v2.0.0
14
+ with:
15
+ pr-title-regex: '^((build|ci|chore|docs|feat|fix|perf|refactor|revert|style|test)!?(\([a-z0-9-]+\))?: .+)$'
16
+ pr-body-regex: '^((?!null)|null.*)\S'
@@ -0,0 +1,18 @@
1
+ name: format
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ push:
7
+ branches:
8
+ - main
9
+
10
+ jobs:
11
+ python-format:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: isort/isort-action@v1
16
+ - uses: psf/black@stable
17
+ with: # see: https://black.readthedocs.io/en/stable/integrations/github_actions.html
18
+ version: "~= 25.0"
@@ -0,0 +1,26 @@
1
+ name: publish-on-tag
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v[0-9]+.[0-9]+.[0-9]+*"
7
+
8
+ jobs:
9
+ pypi-publish-on-tag:
10
+ name: Upload release to PyPI
11
+ runs-on: ubuntu-latest
12
+ environment:
13
+ name: pypi
14
+ url: https://pypi.org/p/argparse-type-helper
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+ - run: |
23
+ python3 -m pip install --upgrade build
24
+ python3 -m build
25
+ - name: Publish package distributions to PyPI
26
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,23 @@
1
+ name: python
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ push:
7
+ branches:
8
+ - main
9
+
10
+ jobs:
11
+ basedpyright:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v5
18
+
19
+ - name: Install the project
20
+ run: uv sync --all-extras --dev
21
+
22
+ - name: Run basedpyright
23
+ run: uv run basedpyright **/*.py
@@ -0,0 +1,269 @@
1
+ ### Custom ###
2
+ uv.lock
3
+
4
+ ### Python ###
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ lib/
22
+ lib64/
23
+ parts/
24
+ sdist/
25
+ var/
26
+ wheels/
27
+ share/python-wheels/
28
+ *.egg-info/
29
+ .installed.cfg
30
+ *.egg
31
+ MANIFEST
32
+
33
+ # PyInstaller
34
+ # Usually these files are written by a python script from a template
35
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
36
+ *.manifest
37
+ *.spec
38
+
39
+ # Installer logs
40
+ pip-log.txt
41
+ pip-delete-this-directory.txt
42
+
43
+ # Unit test / coverage reports
44
+ htmlcov/
45
+ .tox/
46
+ .nox/
47
+ .coverage
48
+ .coverage.*
49
+ .cache
50
+ nosetests.xml
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+ .hypothesis/
55
+ .pytest_cache/
56
+ cover/
57
+
58
+ # Translations
59
+ *.mo
60
+ *.pot
61
+
62
+ # Django stuff:
63
+ *.log
64
+ local_settings.py
65
+ db.sqlite3
66
+ db.sqlite3-journal
67
+
68
+ # Flask stuff:
69
+ instance/
70
+ .webassets-cache
71
+
72
+ # Scrapy stuff:
73
+ .scrapy
74
+
75
+ # Sphinx documentation
76
+ docs/_build/
77
+
78
+ # PyBuilder
79
+ .pybuilder/
80
+ target/
81
+
82
+ # Jupyter Notebook
83
+ .ipynb_checkpoints
84
+
85
+ # IPython
86
+ profile_default/
87
+ ipython_config.py
88
+
89
+ # pyenv
90
+ # For a library or package, you might want to ignore these files since the code is
91
+ # intended to run in multiple environments; otherwise, check them in:
92
+ # .python-version
93
+
94
+ # pipenv
95
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
96
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
97
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
98
+ # install all needed dependencies.
99
+ #Pipfile.lock
100
+
101
+ # poetry
102
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
103
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
104
+ # commonly ignored for libraries.
105
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
106
+ #poetry.lock
107
+
108
+ # pdm
109
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
110
+ #pdm.lock
111
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
112
+ # in version control.
113
+ # https://pdm.fming.dev/#use-with-ide
114
+ .pdm.toml
115
+
116
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
117
+ __pypackages__/
118
+
119
+ # Celery stuff
120
+ celerybeat-schedule
121
+ celerybeat.pid
122
+
123
+ # SageMath parsed files
124
+ *.sage.py
125
+
126
+ # Environments
127
+ .env
128
+ .venv
129
+ env/
130
+ venv/
131
+ ENV/
132
+ env.bak/
133
+ venv.bak/
134
+
135
+ # Spyder project settings
136
+ .spyderproject
137
+ .spyproject
138
+
139
+ # Rope project settings
140
+ .ropeproject
141
+
142
+ # mkdocs documentation
143
+ /site
144
+
145
+ # mypy
146
+ .mypy_cache/
147
+ .dmypy.json
148
+ dmypy.json
149
+
150
+ # Pyre type checker
151
+ .pyre/
152
+
153
+ # pytype static type analyzer
154
+ .pytype/
155
+
156
+ # Cython debug symbols
157
+ cython_debug/
158
+
159
+ # PyCharm
160
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
161
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
162
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
163
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
164
+ #.idea/
165
+
166
+ ### Python Patch ###
167
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
168
+ poetry.toml
169
+
170
+ # ruff
171
+ .ruff_cache/
172
+
173
+ # LSP config files
174
+ pyrightconfig.json
175
+
176
+ ### Vim ###
177
+ # Swap
178
+ [._]*.s[a-v][a-z]
179
+ !*.svg # comment out if you don't need vector files
180
+ [._]*.sw[a-p]
181
+ [._]s[a-rt-v][a-z]
182
+ [._]ss[a-gi-z]
183
+ [._]sw[a-p]
184
+
185
+ # Session
186
+ Session.vim
187
+ Sessionx.vim
188
+
189
+ # Temporary
190
+ .netrwhist
191
+ *~
192
+ # Auto-generated tag files
193
+ tags
194
+ # Persistent undo
195
+ [._]*.un~
196
+
197
+ ### Linux ###
198
+ *~
199
+
200
+ # temporary files which can be created if a process still has a handle open of a deleted file
201
+ .fuse_hidden*
202
+
203
+ # KDE directory preferences
204
+ .directory
205
+
206
+ # Linux trash folder which might appear on any partition or disk
207
+ .Trash-*
208
+
209
+ # .nfs files are created when an open file is removed but is still being accessed
210
+ .nfs*
211
+
212
+ ### macOS ###
213
+ # General
214
+ .DS_Store
215
+ .AppleDouble
216
+ .LSOverride
217
+
218
+ # Icon must end with two \r
219
+ Icon
220
+
221
+ # Thumbnails
222
+ ._*
223
+
224
+ # Files that might appear in the root of a volume
225
+ .DocumentRevisions-V100
226
+ .fseventsd
227
+ .Spotlight-V100
228
+ .TemporaryItems
229
+ .Trashes
230
+ .VolumeIcon.icns
231
+ .com.apple.timemachine.donotpresent
232
+
233
+ # Directories potentially created on remote AFP share
234
+ .AppleDB
235
+ .AppleDesktop
236
+ Network Trash Folder
237
+ Temporary Items
238
+ .apdisk
239
+
240
+ ### macOS Patch ###
241
+ # iCloud generated files
242
+ *.icloud
243
+
244
+ ### Windows ###
245
+ # Windows thumbnail cache files
246
+ Thumbs.db
247
+ Thumbs.db:encryptable
248
+ ehthumbs.db
249
+ ehthumbs_vista.db
250
+
251
+ # Dump file
252
+ *.stackdump
253
+
254
+ # Folder config file
255
+ [Dd]esktop.ini
256
+
257
+ # Recycle Bin used on file shares
258
+ $RECYCLE.BIN/
259
+
260
+ # Windows Installer files
261
+ *.cab
262
+ *.msi
263
+ *.msix
264
+ *.msm
265
+ *.msp
266
+
267
+ # Windows shortcuts
268
+ *.lnk
269
+
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Lingjie
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do 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.
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: argparse-type-helper
3
+ Version: 0.1.0
4
+ Summary: an easy-to-integrate typed argument parser
5
+ Author-email: lljbash <lljbash@gmail.com>
6
+ Project-URL: Homepage, https://github.com/lljbash/targs
7
+ Project-URL: Bug Tracker, https://github.com/lljbash/targs/issues
8
+ Keywords: argparse,typing,argument parser,type hints,typed arguments
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: POSIX
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # Argparse Type Helper
22
+
23
+ A lightweight helper that lets you leverage type hints with `argparse`.
24
+
25
+ ## Features
26
+
27
+ - **Class-based schema**
28
+ Bundle all your arguments in a single `@targs`-decorated class.
29
+ - **Identical API**
30
+ Each field uses the same parameters as `argparse.add_argument` (`help`, `action`, `nargs`, etc.).
31
+ - **Automatic registration**
32
+ One call to `register_targs(parser, YourArgs)` wires up all arguments on your `ArgumentParser`.
33
+ - **Typed extraction**
34
+ After `parse_args()`, call `extract_targs()` to get a fully-typed instance of your class.
35
+ - **Hybrid usage**
36
+ Mix native `parser.add_argument(...)` calls with class-based definitions in the same parser.
37
+ - **Docstring support**
38
+ Use docstrings to automatically generate help text for your arguments.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install argparse-type-helper
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ See [example.py](/tests/example.py).
49
+
50
+ ## Why not [typed-argparse](https://typed-argparse.github.io/typed-argparse/)?
51
+
52
+ typed-argparse is a great library, but it replaces the familiar `argparse.add_argument` API with its own argument-definition interface, which can be a hurdle when integrating into an existing codebase.
53
+
54
+ argparse-type-helper, by contrast, is a simple helper that allows you to use type hints with argparse with minimal learning curve. It uses the same `argparse` API you’re already familiar with, and you can even mix native `argparse` usage with class-based definitions in the same parser.
@@ -0,0 +1,34 @@
1
+ # Argparse Type Helper
2
+
3
+ A lightweight helper that lets you leverage type hints with `argparse`.
4
+
5
+ ## Features
6
+
7
+ - **Class-based schema**
8
+ Bundle all your arguments in a single `@targs`-decorated class.
9
+ - **Identical API**
10
+ Each field uses the same parameters as `argparse.add_argument` (`help`, `action`, `nargs`, etc.).
11
+ - **Automatic registration**
12
+ One call to `register_targs(parser, YourArgs)` wires up all arguments on your `ArgumentParser`.
13
+ - **Typed extraction**
14
+ After `parse_args()`, call `extract_targs()` to get a fully-typed instance of your class.
15
+ - **Hybrid usage**
16
+ Mix native `parser.add_argument(...)` calls with class-based definitions in the same parser.
17
+ - **Docstring support**
18
+ Use docstrings to automatically generate help text for your arguments.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pip install argparse-type-helper
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ See [example.py](/tests/example.py).
29
+
30
+ ## Why not [typed-argparse](https://typed-argparse.github.io/typed-argparse/)?
31
+
32
+ typed-argparse is a great library, but it replaces the familiar `argparse.add_argument` API with its own argument-definition interface, which can be a hurdle when integrating into an existing codebase.
33
+
34
+ argparse-type-helper, by contrast, is a simple helper that allows you to use type hints with argparse with minimal learning curve. It uses the same `argparse` API you’re already familiar with, and you can even mix native `argparse` usage with class-based definitions in the same parser.
@@ -0,0 +1,3 @@
1
+ from .targs import *
2
+
3
+ __all__ = ["Name", "Flag", "targ", "targs", "register_targs", "extract_targs"]
@@ -0,0 +1,184 @@
1
+ import argparse
2
+ from dataclasses import asdict, dataclass, field
3
+ from typing import Any, Callable, Literal, cast, dataclass_transform, get_type_hints
4
+
5
+ from argparse_type_helper.utils import (
6
+ Sentry,
7
+ copy_signature,
8
+ get_attr_docstrings,
9
+ inst_sentry,
10
+ is_sentry,
11
+ logger,
12
+ )
13
+
14
+ __all__ = ["Name", "Flag", "targ", "targs", "register_targs", "extract_targs"]
15
+
16
+
17
+ class Unset:
18
+ pass
19
+
20
+
21
+ class Name:
22
+ pass
23
+
24
+
25
+ @dataclass
26
+ class Flag:
27
+ short: str | None = None
28
+
29
+
30
+ type NameOrFlag = str | tuple[str, str]
31
+
32
+ type Action = Literal[
33
+ "store",
34
+ "store_const",
35
+ "store_true",
36
+ "store_false",
37
+ "append",
38
+ "append_const",
39
+ "extend",
40
+ "count",
41
+ "help",
42
+ "version",
43
+ ]
44
+
45
+
46
+ @dataclass
47
+ class TArg:
48
+ name_or_flag: NameOrFlag | Sentry[Name] | Sentry[Flag]
49
+ action: Action | argparse.BooleanOptionalAction | None | Sentry[Unset] = Unset
50
+ nargs: int | Literal["?", "*", "+"] | None | Sentry[Unset] = Unset
51
+ const: Any | Sentry[Unset] = Unset
52
+ default: Any | Sentry[Unset] = Unset
53
+ type: Callable[[str], Any] | None | Sentry[Unset] = Unset
54
+ choices: list[str] | None | Sentry[Unset] = Unset
55
+ required: bool | None | Sentry[Unset] = Unset
56
+ help: str | None | Sentry[Unset] = Unset
57
+ metavar: str | None | Sentry[Unset] = Unset
58
+ dest: str | None | Sentry[Unset] = Unset
59
+ deprecated: bool | None | Sentry[Unset] = Unset
60
+
61
+ _real_name_or_flag: NameOrFlag | None = field(default=None, init=False)
62
+
63
+ def dump(self) -> dict[str, Any]:
64
+ return {
65
+ k: v
66
+ for k, v in asdict(self).items()
67
+ if k != "name_or_flag" and not k.startswith("_") and not is_sentry(v, Unset)
68
+ }
69
+
70
+ def _init_real_name_or_flag(self, name: str) -> None:
71
+ if is_sentry(self.name_or_flag, Name):
72
+ self._real_name_or_flag = name
73
+ elif is_sentry(self.name_or_flag, Flag):
74
+ flag = inst_sentry(self.name_or_flag, Flag)
75
+ name = name.replace("_", "-") # Convert underscores to dashes for flags
76
+ self._real_name_or_flag = (
77
+ (flag.short, f"--{name}") if flag.short else f"--{name}"
78
+ )
79
+ else:
80
+ self._real_name_or_flag = cast(NameOrFlag, self.name_or_flag)
81
+
82
+ def name_or_flag_tuple(self) -> tuple[str] | tuple[str, str]:
83
+ assert self._real_name_or_flag is not None, "name_or_flag must be initialized"
84
+ if isinstance(self._real_name_or_flag, str):
85
+ return (self._real_name_or_flag,)
86
+ return self._real_name_or_flag
87
+
88
+ def _get_dest_from_one_name_or_flag(self, name_or_flag: str) -> str:
89
+ return name_or_flag.lstrip("-").replace("-", "_")
90
+
91
+ def get_dest(self) -> str:
92
+ assert self._real_name_or_flag is not None, "name_or_flag must be initialized"
93
+ if isinstance(self.dest, str):
94
+ return self.dest
95
+ if isinstance(self._real_name_or_flag, str):
96
+ return self._get_dest_from_one_name_or_flag(self._real_name_or_flag)
97
+ assert all(
98
+ nf.startswith("-") for nf in self._real_name_or_flag
99
+ ), "only one name is allowed for positional arguments"
100
+ first_long = next(
101
+ (nf for nf in self._real_name_or_flag if nf.startswith("--")),
102
+ self._real_name_or_flag[0],
103
+ )
104
+ return self._get_dest_from_one_name_or_flag(first_long)
105
+
106
+ def __set_name__(self, owner: "type", name: str) -> None:
107
+ self._init_real_name_or_flag(name)
108
+ get_targs(owner, check=False)[name] = self
109
+
110
+
111
+ @copy_signature(TArg)
112
+ def targ(*args: Any, **kwargs: Any) -> Any:
113
+ return TArg(*args, **kwargs)
114
+
115
+
116
+ _TARGS_ATTR = "_targs"
117
+ _TARGS_FLAG_ATTR = "_targs_flag"
118
+
119
+
120
+ @dataclass_transform(kw_only_default=True, field_specifiers=(targ, TArg))
121
+ def targs[T](cls: type[T]) -> type[T]:
122
+ def __init__(self: T, **kwargs: Any) -> None:
123
+ targs_dict = get_targs(self.__class__)
124
+ for attr, arg_config in targs_dict.items():
125
+ if attr in kwargs:
126
+ setattr(self, attr, kwargs[attr])
127
+ elif is_sentry(arg_config.default, Unset):
128
+ raise ValueError(f"Missing required argument: {attr}")
129
+ else:
130
+ setattr(self, attr, arg_config.default)
131
+
132
+ def __repr__(self: T) -> str:
133
+ targs_attrs = get_targs(self.__class__).keys()
134
+ return f"{self.__class__.__name__}({', '.join(f'{attr}={getattr(self, attr)!r}' for attr in targs_attrs)})"
135
+
136
+ cls.__init__ = __init__
137
+ cls.__repr__ = __repr__
138
+ setattr(cls, _TARGS_FLAG_ATTR, True)
139
+ return cls
140
+
141
+
142
+ def get_targs(cls: type[object], *, check: bool = True) -> dict[str, TArg]:
143
+ if check and not getattr(cls, _TARGS_FLAG_ATTR, False):
144
+ raise TypeError(f"{cls.__name__} is not a targs class. Use @targs decorator.")
145
+ if not hasattr(cls, _TARGS_ATTR):
146
+ setattr(cls, _TARGS_ATTR, {})
147
+ return getattr(cls, _TARGS_ATTR)
148
+
149
+
150
+ def register_targs(
151
+ parser: argparse.ArgumentParser, cls: type[object], *, verbose: bool = False
152
+ ) -> None:
153
+ targs_dict = get_targs(cls)
154
+ type_hints = get_type_hints(cls)
155
+ docstrings = get_attr_docstrings(cls)
156
+ for attr, arg_config in targs_dict.items():
157
+ name_part = arg_config.name_or_flag_tuple()
158
+ config_part = arg_config.dump()
159
+
160
+ type_hint = type_hints.get(attr, None)
161
+ if type_hint is None:
162
+ raise TypeError(f"Type hint for argument '{attr}' is missing.")
163
+ if callable(type_hint) and config_part.get("action") is None:
164
+ config_part.setdefault("type", type_hint)
165
+
166
+ doc = docstrings.get(attr)
167
+ if doc is not None:
168
+ config_part.setdefault("help", doc)
169
+
170
+ if verbose:
171
+ logger.debug(f"Registering argument {name_part} with config: {config_part}")
172
+ parser.add_argument(*name_part, **config_part)
173
+
174
+
175
+ def extract_targs[T](args: argparse.Namespace, cls: type[T]) -> T:
176
+ targs_dict = get_targs(cls)
177
+ targs_instance = cls.__new__(cls)
178
+ for attr, arg_config in targs_dict.items():
179
+ dest = arg_config.get_dest()
180
+ if hasattr(args, dest):
181
+ setattr(targs_instance, attr, getattr(args, dest))
182
+ else:
183
+ raise AttributeError(f"Argument '{dest}' not found in parsed args.")
184
+ return targs_instance
@@ -0,0 +1,62 @@
1
+ import ast
2
+ import inspect
3
+ import logging
4
+ from typing import Any, Callable, Concatenate, TypeGuard
5
+
6
+ __all__ = ["logger", "Sentry", "is_sentry", "inst_sentry", "get_attr_docstrings"]
7
+
8
+ _formatter = logging.Formatter(fmt="[%(module)s|%(levelname)s] %(message)s")
9
+ _handler = logging.StreamHandler()
10
+ _handler.setFormatter(_formatter)
11
+ logger = logging.getLogger("targs")
12
+ logger.setLevel(logging.DEBUG)
13
+ logger.addHandler(_handler)
14
+
15
+ type Sentry[T] = type[T] | T
16
+
17
+
18
+ def is_sentry[T](value: Any, sentry_type: type[T]) -> TypeGuard[Sentry[T]]:
19
+ return isinstance(value, sentry_type) or value is sentry_type
20
+
21
+
22
+ def inst_sentry[T](value: Sentry[T], sentry_type: type[T]) -> T:
23
+ return value if isinstance(value, sentry_type) else sentry_type()
24
+
25
+
26
+ def copy_signature[**P, R1, R2](
27
+ fn: Callable[P, R1],
28
+ ) -> Callable[[Callable[P, R2]], Callable[P, R2]]:
29
+ """Copy the signature of a function, allowing easier function wrapping with type hints."""
30
+ del fn # Unused parameter
31
+ return lambda fn: fn
32
+
33
+
34
+ def copy_signature_remove_first[**P, R1, R2, F](
35
+ fn: Callable[Concatenate[F, P], R1],
36
+ ) -> Callable[[Callable[P, R2]], Callable[P, R2]]:
37
+ """Like `copy_signature`, but removes the first parameter from the signature."""
38
+ del fn # Unused parameter
39
+ return lambda fn: fn
40
+
41
+
42
+ def get_attr_docstrings(cls: type[object]) -> dict[str, str]:
43
+ """Best effort to get docstrings for attributes in a class."""
44
+ source = inspect.getsource(cls)
45
+ tree = ast.parse(source)
46
+ classdef = tree.body[0]
47
+ assert isinstance(classdef, ast.ClassDef), "Expected a class definition"
48
+
49
+ attr_docstrings: dict[str, str] = {}
50
+ cur_attr: str | None = None
51
+ for stmt in classdef.body:
52
+ if isinstance(stmt, ast.AnnAssign):
53
+ if isinstance(stmt.target, ast.Name):
54
+ cur_attr = stmt.target.id
55
+ elif isinstance(stmt, ast.Assign):
56
+ if len(stmt.targets) == 1 and isinstance(stmt.targets[0], ast.Name):
57
+ cur_attr = stmt.targets[0].id
58
+ elif isinstance(stmt, ast.Expr):
59
+ if isinstance(stmt.value, ast.Constant) and cur_attr is not None:
60
+ attr_docstrings[cur_attr] = inspect.cleandoc(str(stmt.value.value))
61
+ cur_attr = None
62
+ return attr_docstrings
@@ -0,0 +1,54 @@
1
+ Metadata-Version: 2.4
2
+ Name: argparse-type-helper
3
+ Version: 0.1.0
4
+ Summary: an easy-to-integrate typed argument parser
5
+ Author-email: lljbash <lljbash@gmail.com>
6
+ Project-URL: Homepage, https://github.com/lljbash/targs
7
+ Project-URL: Bug Tracker, https://github.com/lljbash/targs/issues
8
+ Keywords: argparse,typing,argument parser,type hints,typed arguments
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: POSIX
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # Argparse Type Helper
22
+
23
+ A lightweight helper that lets you leverage type hints with `argparse`.
24
+
25
+ ## Features
26
+
27
+ - **Class-based schema**
28
+ Bundle all your arguments in a single `@targs`-decorated class.
29
+ - **Identical API**
30
+ Each field uses the same parameters as `argparse.add_argument` (`help`, `action`, `nargs`, etc.).
31
+ - **Automatic registration**
32
+ One call to `register_targs(parser, YourArgs)` wires up all arguments on your `ArgumentParser`.
33
+ - **Typed extraction**
34
+ After `parse_args()`, call `extract_targs()` to get a fully-typed instance of your class.
35
+ - **Hybrid usage**
36
+ Mix native `parser.add_argument(...)` calls with class-based definitions in the same parser.
37
+ - **Docstring support**
38
+ Use docstrings to automatically generate help text for your arguments.
39
+
40
+ ## Installation
41
+
42
+ ```bash
43
+ pip install argparse-type-helper
44
+ ```
45
+
46
+ ## Usage
47
+
48
+ See [example.py](/tests/example.py).
49
+
50
+ ## Why not [typed-argparse](https://typed-argparse.github.io/typed-argparse/)?
51
+
52
+ typed-argparse is a great library, but it replaces the familiar `argparse.add_argument` API with its own argument-definition interface, which can be a hurdle when integrating into an existing codebase.
53
+
54
+ argparse-type-helper, by contrast, is a simple helper that allows you to use type hints with argparse with minimal learning curve. It uses the same `argparse` API you’re already familiar with, and you can even mix native `argparse` usage with class-based definitions in the same parser.
@@ -0,0 +1,17 @@
1
+ .gitignore
2
+ .python-version
3
+ LICENSE
4
+ README.md
5
+ pyproject.toml
6
+ .github/workflows/conventional_commit_checker.yml
7
+ .github/workflows/format.yml
8
+ .github/workflows/publish.yml
9
+ .github/workflows/python.yml
10
+ argparse_type_helper/__init__.py
11
+ argparse_type_helper/targs.py
12
+ argparse_type_helper/utils.py
13
+ argparse_type_helper.egg-info/PKG-INFO
14
+ argparse_type_helper.egg-info/SOURCES.txt
15
+ argparse_type_helper.egg-info/dependency_links.txt
16
+ argparse_type_helper.egg-info/top_level.txt
17
+ tests/example.py
@@ -0,0 +1 @@
1
+ argparse_type_helper
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "setuptools-scm>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "argparse-type-helper"
7
+ dynamic = ["version"]
8
+ dependencies = []
9
+ requires-python = ">=3.12"
10
+ authors = [{ name = "lljbash", email = "lljbash@gmail.com" }]
11
+ description = "an easy-to-integrate typed argument parser"
12
+ readme = "README.md"
13
+ keywords = ["argparse", "typing", "argument parser", "type hints", "typed arguments"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: POSIX",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Topic :: Software Development :: Libraries",
22
+ ]
23
+
24
+ [project.urls]
25
+ "Homepage" = "https://github.com/lljbash/targs"
26
+ "Bug Tracker" = "https://github.com/lljbash/targs/issues"
27
+
28
+ [tool.setuptools_scm]
29
+
30
+ [tool.black]
31
+ include = '\.pyi?$'
32
+ required-version = "25"
33
+
34
+ [tool.isort]
35
+ profile = "black"
36
+
37
+ [tool.basedpyright]
38
+ include = ["argparse_type_helper", "tests"]
39
+ pythonVersion = "3.12"
40
+ pythonPlatform = "Linux"
41
+ typeCheckingMode = "strict"
42
+ deprecateTypingAliases = true
43
+
44
+ [dependency-groups]
45
+ dev = [
46
+ "basedpyright>=1.29.2",
47
+ "black>=25.1.0",
48
+ "isort>=6.0.1",
49
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,94 @@
1
+ import argparse
2
+ import sys
3
+ from typing import Never
4
+
5
+ from argparse_type_helper import Flag, Name, extract_targs, register_targs, targ, targs
6
+
7
+
8
+ # Define your typed arguments as a targ class
9
+ @targs
10
+ class MyArgs:
11
+ # This example will show the common usage of targ.
12
+
13
+ positional: str = targ(Name, help="A positional argument (positional).")
14
+ custom_name_pos: str = targ(
15
+ "my_positional", help="A custom named positional argument."
16
+ )
17
+
18
+ optional: str = targ(Flag, help="An optional argument (--optional).")
19
+ optional_dash: str = targ(
20
+ Flag, help="underscore is replaced with dash (--optional-dash)."
21
+ )
22
+ optional_short: str = targ(
23
+ Flag("-s"), help="You can also add a short name (-s, --optional-short)."
24
+ )
25
+ custom_name_opt: str = targ(
26
+ "--my-optional",
27
+ help="A custom named optional argument.",
28
+ )
29
+ custom_name_opt_short: str = targ(
30
+ ("-c", "--my-short-optional"),
31
+ help="A custom named optional argument with a short name. (note the tuple)",
32
+ )
33
+
34
+ options: list[str] = targ(
35
+ Flag,
36
+ action="extend",
37
+ nargs="+",
38
+ default=[],
39
+ help="All options (`help`, `action`, `nargs`, etc.) are the same as argparse.",
40
+ )
41
+ choices: str = targ(
42
+ Flag,
43
+ choices=["option1", "option2", "option3"],
44
+ help="Another example argument with choices.",
45
+ )
46
+ flag: bool = targ(
47
+ Flag("-d"), action="store_true", help="Another example boolean flag."
48
+ )
49
+
50
+ default_type: int = targ(
51
+ Flag,
52
+ default=42,
53
+ help="if type is not specified, it defaults to the type hint. (type=int in this case)",
54
+ )
55
+ custom_type: float = targ(
56
+ Flag,
57
+ type=lambda x: round(float(x), 1),
58
+ default=3.14,
59
+ help="You can also specify a custom type",
60
+ )
61
+
62
+ docstring_as_help: str = targ(Flag, default="default value")
63
+ """
64
+ If you don't specify a help, it will use the docstring as the help text.
65
+ This is useful for documentation purposes.
66
+ Your LSP will also pick this up.
67
+ """
68
+
69
+
70
+ # You can register the targs with a custom parser
71
+ class MyParser(argparse.ArgumentParser):
72
+ def error(self, message: str) -> Never:
73
+ sys.stderr.write("error: %s\n" % message)
74
+ self.print_help()
75
+ sys.exit(2)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ # Create a parser
80
+ parser = MyParser(description="Process some data arguments.")
81
+
82
+ # Register the targs with the parser
83
+ # verbose=True will print the registered arguments
84
+ register_targs(parser, MyArgs, verbose=True)
85
+
86
+ # Hybrid usage example
87
+ parser.add_argument("--version", action="version", version="MyArgs 1.0.0")
88
+
89
+ # Parse the arguments
90
+ args = parser.parse_args()
91
+
92
+ # Extract the targs from the parsed arguments
93
+ my_args = extract_targs(args, MyArgs)
94
+ print(f"Parsed arguments: {my_args}")