py-arguments 1.0.1__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,29 @@
1
+ name: Claude PR Review
2
+ on:
3
+ pull_request:
4
+ types: [opened, synchronize, reopened]
5
+ jobs:
6
+ review:
7
+ runs-on: ubuntu-latest
8
+ permissions:
9
+ contents: read
10
+ pull-requests: write
11
+ issues: write
12
+ id-token: write
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ with:
16
+ fetch-depth: 1
17
+
18
+ - uses: anthropics/claude-code-action@v1
19
+ with:
20
+ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
21
+ prompt: |
22
+ REPO: ${{ github.repository }}
23
+ PR NUMBER: ${{ github.event.pull_request.number }}
24
+
25
+ Review this PR focusing on bugs, security issues, code performance and readability.
26
+ Be objective and answer in english. Comment only what is really important.
27
+
28
+ claude_args: |
29
+ --allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
@@ -0,0 +1,16 @@
1
+ name: Pre-Commit
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches: [main, develop]
6
+ jobs:
7
+ pre-commit:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-python@v5
12
+ with:
13
+ python-version: '3.x'
14
+ - uses: pre-commit/action@v3.0.1
15
+ env:
16
+ SKIP: no-commit-to-branch
@@ -0,0 +1,26 @@
1
+ name: Publish
2
+ on:
3
+ release:
4
+ types: [published]
5
+ workflow_dispatch:
6
+ inputs:
7
+ tag:
8
+ description: Tag to be published
9
+ required: true
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+ environment: pypi
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0
21
+ ref: ${{ inputs.tag || github.ref }}
22
+ - uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.12"
25
+ - run: pipx run build
26
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,33 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ permissions:
7
+ contents: write
8
+ issues: write
9
+ pull-requests: write
10
+ id-token: write
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Generate app token
16
+ id: app-token
17
+ uses: actions/create-github-app-token@v2
18
+ with:
19
+ app-id: ${{ vars.APP_ID }}
20
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
21
+ - uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0
24
+ token: ${{ steps.app-token.outputs.token }}
25
+ - uses: actions/setup-node@v4
26
+ with:
27
+ node-version: 20
28
+ - name: Install semantic-release
29
+ run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog
30
+ - name: Release
31
+ env:
32
+ GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
33
+ run: npx semantic-release
@@ -0,0 +1,61 @@
1
+ # Python.
2
+ *.pyc
3
+ __pycache__/
4
+
5
+ # Distribution / packaging.
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+
10
+ # Node.
11
+ node_modules/
12
+ npm-debug.*
13
+ yarn-debug.*
14
+ yarn-error.*
15
+ pnpm-debug.log*
16
+ lerna-debug.log*
17
+ *.log
18
+ logs
19
+
20
+ # Vite.
21
+ dist-ssr
22
+ *.local
23
+
24
+ # Expo / React Native.
25
+ .expo/
26
+ web-build/
27
+ expo-env.d.ts
28
+ .metro-health-check*
29
+
30
+ # Native (iOS/Android).
31
+ .kotlin/
32
+ /ios
33
+ /android
34
+ *.jks
35
+ *.p8
36
+ *.p12
37
+ *.key
38
+ *.mobileprovision
39
+
40
+ # TypeScript.
41
+ *.tsbuildinfo
42
+
43
+ # Environment.
44
+ *.env*
45
+ *.venv*
46
+ env/
47
+ venv/
48
+
49
+ # Ruff.
50
+ .ruff_cache/
51
+
52
+ # MyPy.
53
+ .mypy_cache/
54
+
55
+ # IDEs and code editors.
56
+ .idea/
57
+ .vscode/
58
+
59
+ # macOS.
60
+ .DS_Store
61
+ *.pem
@@ -0,0 +1,79 @@
1
+ default_install_hook_types: [pre-commit, commit-msg]
2
+ default_stages: [pre-commit]
3
+ repos:
4
+ # Overall hooks.
5
+ - repo: https://github.com/pre-commit/pre-commit-hooks
6
+ rev: v6.0.0
7
+ hooks:
8
+ - id: check-added-large-files
9
+ name: Check for Large Files
10
+ args: [--maxkb=512]
11
+ - id: check-illegal-windows-names
12
+ name: Check for Illegal Windows Names
13
+ - id: check-json
14
+ name: Check JSON Files
15
+ - id: check-symlinks
16
+ name: Check for Symlinks
17
+ - id: check-toml
18
+ name: Check TOML Files
19
+ - id: check-xml
20
+ name: Check XML Files
21
+ - id: check-yaml
22
+ name: Check YAML Files
23
+ args: [--allow-multiple-documents]
24
+ - id: detect-private-key
25
+ name: Detect Private Keys
26
+ - id: end-of-file-fixer
27
+ name: Fix End of Files
28
+ - id: name-tests-test
29
+ name: Ensure Test Files Start with "test"
30
+ args: [--pytest-test-first]
31
+ - id: no-commit-to-branch
32
+ name: Prevent Commit to `main` and `develop` Branch
33
+ args: [--branch=main, --branch=develop]
34
+ - id: pretty-format-json
35
+ name: Pretty Format JSON Files
36
+ args: [--autofix, --indent=4]
37
+ - id: requirements-txt-fixer
38
+ name: Fix Requirements.txt Files
39
+ - id: trailing-whitespace
40
+ name: Trim Trailing Whitespace
41
+ - repo: https://github.com/compilerla/conventional-pre-commit
42
+ rev: v4.4.0
43
+ hooks:
44
+ - id: conventional-pre-commit
45
+ name: Conventional Commit Message Validator
46
+ stages: [commit-msg]
47
+ args: [feat, fix, docs, style, refactor, test, chore, revert, ci, build, perf]
48
+ - repo: local
49
+ hooks:
50
+ - id: format-commit-message
51
+ name: Format Commit Message
52
+ language: python
53
+ stages: [commit-msg]
54
+ entry: python3 hooks/format_commit_message.py
55
+ additional_dependencies: [conventional-pre-commit==4.4.0] # Same version as hook.
56
+ # Python hooks.
57
+ - repo: https://github.com/astral-sh/ruff-pre-commit
58
+ rev: v0.15.16
59
+ hooks:
60
+ - id: ruff-check
61
+ name: Lint Python Files
62
+ args: [--fix]
63
+ - id: ruff-format
64
+ name: Format Python Files
65
+ - repo: https://github.com/pre-commit/mirrors-mypy
66
+ rev: v2.1.0
67
+ hooks:
68
+ - id: mypy
69
+ name: Static Python Type Checker
70
+ additional_dependencies: [pydantic==2.13.5] # Add additional mypy plugins for type validation if needed.
71
+ args: [--config-file=pyproject.toml]
72
+ # Node hooks.
73
+ - repo: https://github.com/pre-commit/mirrors-prettier
74
+ rev: v4.0.0-alpha.8
75
+ hooks:
76
+ - id: prettier
77
+ name: Format JS and CSS Files
78
+ files: \.(js|jsx|ts|tsx|css|html)$
79
+ args: [--write]
@@ -0,0 +1,45 @@
1
+ {
2
+ "branches": [
3
+ "main"
4
+ ],
5
+ "plugins": [
6
+ [
7
+ "@semantic-release/commit-analyzer",
8
+ {
9
+ "parserOpts": {
10
+ "headerCorrespondence": [
11
+ "type",
12
+ "scope",
13
+ "subject"
14
+ ],
15
+ "headerPattern": "^(?:.*?\\s)?(\\w*)(?:\\(([\\w$.\\-* ]*)\\))?: (.*)$"
16
+ },
17
+ "preset": "angular"
18
+ }
19
+ ],
20
+ [
21
+ "@semantic-release/release-notes-generator",
22
+ {
23
+ "parserOpts": {
24
+ "headerCorrespondence": [
25
+ "type",
26
+ "scope",
27
+ "subject"
28
+ ],
29
+ "headerPattern": "^(?:.*?\\s)?(\\w*)(?:\\(([\\w$.\\-* ]*)\\))?: (.*)$"
30
+ },
31
+ "preset": "angular"
32
+ }
33
+ ],
34
+ "@semantic-release/changelog",
35
+ "@semantic-release/github",
36
+ [
37
+ "@semantic-release/git",
38
+ {
39
+ "assets": [
40
+ "CHANGELOG.md"
41
+ ]
42
+ }
43
+ ]
44
+ ]
45
+ }
@@ -0,0 +1,14 @@
1
+ ## [1.0.1](https://github.com/SmilingTech/pyarg/compare/v1.0.0...v1.0.1) (2026-11-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Accept manual publish via UI. ([1396176](https://github.com/SmilingTech/pyarg/commit/139617633f7d533aaad4923a5016af80c2a9a651))
7
+
8
+ # 1.0.0 (2026-11-08)
9
+
10
+
11
+ ### Features
12
+
13
+ * Add main lib folder and it utilities. ([31bb920](https://github.com/SmilingTech/pyarg/commit/31bb920c5d484b7eb4ad74ef81f94915078be334))
14
+ * Drop all unnecessary files and directories for the project. ([36733f9](https://github.com/SmilingTech/pyarg/commit/36733f9d8085b0fb7e59acd5691c90d4a2bc3ba6))
@@ -0,0 +1,46 @@
1
+ # Developers
2
+ > [!IMPORTANT]
3
+ > Before starting development on this repository, make sure you install all local development requirements.
4
+ ```bash
5
+ $ python3 -m venv .env # Create a virtual environment.
6
+ $ source .env/bin/activate # Activate the virtual environment.
7
+ $ make setup-local-development # Install all development requirements.
8
+ ```
9
+
10
+ ### Folder structure.
11
+ > [!WARNING]
12
+ > *This structure can be modified during the development of any project.*
13
+ ```txt
14
+ .github/
15
+ ᒪ workflows/
16
+ ᒪ ... # YAML files to be executed in GitHub workflows.
17
+ pyarg/ # Main library directory.
18
+ hooks/
19
+ ᒪ ... # Python scripts to be used as hooks in pre-commit.
20
+ .gitignore # File with all files and folders to be ignored by Git.
21
+ .pre-commit-config.yaml # Configuration file to set up local hooks for pre-commit.
22
+ .releaserc.json # Plugins needed for generate the Release Notes.
23
+ DEVELOPERS.md # Documentation for developers.
24
+ Makefile # Used to define keywords to execute long commands.
25
+ pyproject.toml # Main Python configuration file.
26
+ README.md # Main project documentation.
27
+ ```
28
+
29
+ ### Conventional Commit Message.
30
+ All projects in this organizations must to use the conventional commit message format based on the following pattern:
31
+ ```
32
+ ^(feat|fix|docs|styles|refactor|test|chore|revert|ci|build|perf): .+$
33
+ ```
34
+ |Type|Description|
35
+ |-|-|
36
+ |`feat`|Add a new functionality to the project.|
37
+ |`fix`|Fix a bug in the project.|
38
+ |`docs`|Changes in documentation. (README, comments, wikis, etc.)|
39
+ |`style`|Formatting, spacing, semicolons. Without changing the logic.|
40
+ |`refactor`|Rewriting/restructuring code without adding feature or fixing bug.|
41
+ |`test`|Add or fix tests. Without touching the main production code.|
42
+ |`chore`|Maintenance tasks: update dependencies, configs, build scripts.|
43
+ |`revert`|Revert a previous commit.|
44
+ |`ci`|Pipeline changes and CI/CD configurations. (GitHub Actions, etc.)|
45
+ |`build`|Changes in build system or external dependencies. (Dockerfile, Makefile, etc.)|
46
+ |`perf`|Code changes that improve performance without changing behavior.|
@@ -0,0 +1,4 @@
1
+ setup-local-development:
2
+ @echo "Setting up local development environment..."
3
+ pip install .[development] && pre-commit install
4
+ @echo "Local development environment setup complete."
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-arguments
3
+ Version: 1.0.1
4
+ Description-Content-Type: text/markdown
5
+ Provides-Extra: development
6
+ Requires-Dist: pre-commit; extra == "development"
7
+
8
+ # PyArg
9
+
10
+ A small layer on top of `argparse` to build CLIs with sub-commands by **reusing parser classes**.
11
+
12
+ Each parser is a class. You define its arguments in one method, and then plug that class into another parser as a
13
+ command, without rewriting the arguments.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install .
19
+ ```
20
+
21
+ For development (changes apply without reinstalling):
22
+
23
+ ```bash
24
+ pip install -e .
25
+ ```
26
+
27
+ Requires Python 3.10+ and has no dependencies.
28
+
29
+ ## Quick start
30
+
31
+ Create a class that inherits from `PyArg` and define the arguments in `setup_arguments`:
32
+
33
+ ```python
34
+ from pyarg import PyArg
35
+
36
+
37
+ class Login(PyArg):
38
+ def setup_arguments(self) -> None:
39
+ self.add_argument("--username", "-u")
40
+ self.add_argument("--password", "-p")
41
+
42
+
43
+ parser = Login(name="Login")
44
+ print(parser.get_arguments())
45
+ ```
46
+
47
+ ```console
48
+ $ python main.py -u ana -p 123
49
+ {'username': 'ana', 'password': '123'}
50
+ ```
51
+
52
+ Since `PyArg` inherits from `argparse.ArgumentParser`, everything from `argparse` still works
53
+ (`add_argument`, `add_mutually_exclusive_group`, `type`, `nargs`, `choices`, etc.).
54
+
55
+ ## Sub-commands
56
+
57
+ Use `add_command_parser` to attach parsers to a main one. Each command is named after the parser `name`, converted to
58
+ lowercase with spaces replaced by `-` (`"Make Directory"` becomes `make-directory`).
59
+
60
+ ```python
61
+ from pyarg import PyArg
62
+
63
+
64
+ class Login(PyArg):
65
+ def setup_arguments(self) -> None:
66
+ self.add_argument("--username", "-u")
67
+ self.add_argument("--password", "-p")
68
+
69
+
70
+ class Logout(PyArg):
71
+ def setup_arguments(self) -> None:
72
+ self.add_argument("--all", action="store_true")
73
+
74
+
75
+ class App(PyArg):
76
+ def setup_arguments(self) -> None:
77
+ pass # The main parser can have its own arguments too.
78
+
79
+
80
+ app = App(name="My App", description="Demo app.")
81
+ app.add_command_parser(Login(name="Login", description="Sign in."))
82
+ app.add_command_parser(Logout(name="Logout"), description="Sign out.")
83
+
84
+ print(app.get_arguments())
85
+ ```
86
+
87
+ ```console
88
+ $ python main.py login -u ana -p 123
89
+ {'command': 'login', 'username': 'ana', 'password': '123'}
90
+
91
+ $ python main.py logout --all
92
+ {'command': 'logout', 'all': True}
93
+
94
+ $ python main.py -h
95
+ usage: My App [-h] {login,logout} ...
96
+ ```
97
+
98
+ The chosen command is available under the `command` key.
99
+
100
+ ## Reusing arguments
101
+
102
+ Arguments from other parsers can be inherited with `parents`:
103
+
104
+ ```python
105
+ class Common(PyArg):
106
+ def setup_arguments(self) -> None:
107
+ self.add_argument("--verbose", "-v", action="store_true")
108
+
109
+
110
+ class Build(PyArg):
111
+ def setup_arguments(self) -> None:
112
+ self.add_argument("--target")
113
+
114
+
115
+ build = Build(name="Build", parents=[Common(name="Common", add_help=False)])
116
+ ```
117
+
118
+ Use `add_help=False` on the parent parser so the `-h` option is not registered twice.
119
+
120
+ ## API
121
+
122
+ ### `PyArg(name, description=None, parents=[], **kwargs)`
123
+
124
+ Abstract class. It cannot be instantiated directly, you must inherit from it and implement `setup_arguments`.
125
+
126
+ - `name`: parser name. Used as `prog` in the help text and to build the command name.
127
+ - `description`: text shown in `-h`.
128
+ - `parents`: other parsers whose arguments are copied into this one.
129
+ - `**kwargs`: forwarded to `argparse.ArgumentParser`. The `prog`, `description`, `parents` and `conflict_handler`
130
+ keys are ignored, as `conflict_handler` is always `"resolve"`.
131
+
132
+ ### `setup_arguments(self) -> None`
133
+
134
+ Abstract method. Define all the arguments of the parser here. It runs at the end of `__init__`.
135
+
136
+ ### `command_name`
137
+
138
+ Read-only property with the command name derived from `name`.
139
+
140
+ ### `add_command_parser(parser, description=None, script_entrypoint=None)`
141
+
142
+ Adds `parser` as a sub-command. If `description` is not set, the parser's own description is used.
143
+
144
+ `script_entrypoint` stores a callable on the command, but `get_arguments` does not call it and removes it from the
145
+ result.
146
+
147
+ ### `get_arguments() -> dict[str, Any]`
148
+
149
+ Parses `sys.argv` and returns the arguments passed by the user as a dictionary.
150
+
151
+ ## Development
152
+
153
+ See [DEVELOPERS.md](DEVELOPERS.md).
@@ -0,0 +1,146 @@
1
+ # PyArg
2
+
3
+ A small layer on top of `argparse` to build CLIs with sub-commands by **reusing parser classes**.
4
+
5
+ Each parser is a class. You define its arguments in one method, and then plug that class into another parser as a
6
+ command, without rewriting the arguments.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install .
12
+ ```
13
+
14
+ For development (changes apply without reinstalling):
15
+
16
+ ```bash
17
+ pip install -e .
18
+ ```
19
+
20
+ Requires Python 3.10+ and has no dependencies.
21
+
22
+ ## Quick start
23
+
24
+ Create a class that inherits from `PyArg` and define the arguments in `setup_arguments`:
25
+
26
+ ```python
27
+ from pyarg import PyArg
28
+
29
+
30
+ class Login(PyArg):
31
+ def setup_arguments(self) -> None:
32
+ self.add_argument("--username", "-u")
33
+ self.add_argument("--password", "-p")
34
+
35
+
36
+ parser = Login(name="Login")
37
+ print(parser.get_arguments())
38
+ ```
39
+
40
+ ```console
41
+ $ python main.py -u ana -p 123
42
+ {'username': 'ana', 'password': '123'}
43
+ ```
44
+
45
+ Since `PyArg` inherits from `argparse.ArgumentParser`, everything from `argparse` still works
46
+ (`add_argument`, `add_mutually_exclusive_group`, `type`, `nargs`, `choices`, etc.).
47
+
48
+ ## Sub-commands
49
+
50
+ Use `add_command_parser` to attach parsers to a main one. Each command is named after the parser `name`, converted to
51
+ lowercase with spaces replaced by `-` (`"Make Directory"` becomes `make-directory`).
52
+
53
+ ```python
54
+ from pyarg import PyArg
55
+
56
+
57
+ class Login(PyArg):
58
+ def setup_arguments(self) -> None:
59
+ self.add_argument("--username", "-u")
60
+ self.add_argument("--password", "-p")
61
+
62
+
63
+ class Logout(PyArg):
64
+ def setup_arguments(self) -> None:
65
+ self.add_argument("--all", action="store_true")
66
+
67
+
68
+ class App(PyArg):
69
+ def setup_arguments(self) -> None:
70
+ pass # The main parser can have its own arguments too.
71
+
72
+
73
+ app = App(name="My App", description="Demo app.")
74
+ app.add_command_parser(Login(name="Login", description="Sign in."))
75
+ app.add_command_parser(Logout(name="Logout"), description="Sign out.")
76
+
77
+ print(app.get_arguments())
78
+ ```
79
+
80
+ ```console
81
+ $ python main.py login -u ana -p 123
82
+ {'command': 'login', 'username': 'ana', 'password': '123'}
83
+
84
+ $ python main.py logout --all
85
+ {'command': 'logout', 'all': True}
86
+
87
+ $ python main.py -h
88
+ usage: My App [-h] {login,logout} ...
89
+ ```
90
+
91
+ The chosen command is available under the `command` key.
92
+
93
+ ## Reusing arguments
94
+
95
+ Arguments from other parsers can be inherited with `parents`:
96
+
97
+ ```python
98
+ class Common(PyArg):
99
+ def setup_arguments(self) -> None:
100
+ self.add_argument("--verbose", "-v", action="store_true")
101
+
102
+
103
+ class Build(PyArg):
104
+ def setup_arguments(self) -> None:
105
+ self.add_argument("--target")
106
+
107
+
108
+ build = Build(name="Build", parents=[Common(name="Common", add_help=False)])
109
+ ```
110
+
111
+ Use `add_help=False` on the parent parser so the `-h` option is not registered twice.
112
+
113
+ ## API
114
+
115
+ ### `PyArg(name, description=None, parents=[], **kwargs)`
116
+
117
+ Abstract class. It cannot be instantiated directly, you must inherit from it and implement `setup_arguments`.
118
+
119
+ - `name`: parser name. Used as `prog` in the help text and to build the command name.
120
+ - `description`: text shown in `-h`.
121
+ - `parents`: other parsers whose arguments are copied into this one.
122
+ - `**kwargs`: forwarded to `argparse.ArgumentParser`. The `prog`, `description`, `parents` and `conflict_handler`
123
+ keys are ignored, as `conflict_handler` is always `"resolve"`.
124
+
125
+ ### `setup_arguments(self) -> None`
126
+
127
+ Abstract method. Define all the arguments of the parser here. It runs at the end of `__init__`.
128
+
129
+ ### `command_name`
130
+
131
+ Read-only property with the command name derived from `name`.
132
+
133
+ ### `add_command_parser(parser, description=None, script_entrypoint=None)`
134
+
135
+ Adds `parser` as a sub-command. If `description` is not set, the parser's own description is used.
136
+
137
+ `script_entrypoint` stores a callable on the command, but `get_arguments` does not call it and removes it from the
138
+ result.
139
+
140
+ ### `get_arguments() -> dict[str, Any]`
141
+
142
+ Parses `sys.argv` and returns the arguments passed by the user as a dictionary.
143
+
144
+ ## Development
145
+
146
+ See [DEVELOPERS.md](DEVELOPERS.md).
@@ -0,0 +1,114 @@
1
+ """
2
+ This script is a custom hook for pre-commit that checks if the commit message follows the expected
3
+ format. Then, it formats the commit message to match the project conventions.
4
+ """
5
+
6
+ import re
7
+ import sys
8
+ from enum import Enum
9
+ from pathlib import Path
10
+
11
+ from conventional_pre_commit.format import ConventionalCommit
12
+
13
+
14
+ # Enums.
15
+ class CommitTypeEmoji(Enum):
16
+ """
17
+ Emoji representations for different commit types.
18
+ """
19
+
20
+ FEAT = "🔥"
21
+ FIX = "🐛"
22
+ DOCS = "📝"
23
+ STYLE = "💄"
24
+ REFACTOR = "✨"
25
+ TEST = "🔍"
26
+ CHORE = "⚙️"
27
+ REVERT = "⏪"
28
+ CI = "🚀"
29
+ BUILD = "🚧"
30
+ PERF = "⚡️"
31
+
32
+ @classmethod
33
+ def types(cls) -> list[str]:
34
+ """
35
+ Get the list of available commit types.
36
+
37
+ Returns:
38
+ list[str]: A list of commit types.
39
+ """
40
+ return [name.lower() for name in cls.__members__]
41
+
42
+
43
+ # Functions.
44
+ def main(log: bool = True) -> None:
45
+ """
46
+ Entry point for the script.
47
+
48
+ Args:
49
+ log (bool): Whether to log the results to the console. Defaults to True.
50
+ """
51
+
52
+ # Utils.
53
+ def logger(message: str) -> None:
54
+ print(message) if log else None
55
+
56
+ def capitalize_after_final_dot(match: re.Match[str]) -> str:
57
+ match_str: str = match.group(0)
58
+ return match_str.capitalize()
59
+
60
+ # Get the commit message from arguments.
61
+ try:
62
+ commit_msg_filepath = sys.argv[1]
63
+ commit_message = Path(commit_msg_filepath).read_text(encoding="utf-8").strip()
64
+ except Exception as e:
65
+ logger("Unable to read the commit message file. " + str(e))
66
+ sys.exit(1)
67
+
68
+ # Get commit type.
69
+ conventional_commit_pattern = ConventionalCommit().r_types
70
+ commit_type_re = re.compile(pattern=conventional_commit_pattern)
71
+ commit_type_match = commit_type_re.search(string=commit_message)
72
+ if not commit_type_match:
73
+ logger("Could not parse commit type from message.")
74
+ sys.exit(1)
75
+ commit_type: str = commit_type_match.group(0)
76
+
77
+ # Get main commit message.
78
+ main_message_pattern = ConventionalCommit().r_subject
79
+ main_message_re = re.compile(pattern=main_message_pattern)
80
+ main_message_match = main_message_re.search(string=commit_message)
81
+ if not main_message_match:
82
+ logger("Could not parse main commit message from message.")
83
+ sys.exit(1)
84
+ main_commit_message: str = main_message_match.group(0).strip()
85
+
86
+ # Capitalize the first letter of the commit description and
87
+ # after any final dot.
88
+ new_main_commit_message = main_commit_message[0].upper() + main_commit_message[1:]
89
+ capitalize_after_final_dot_re = re.compile(pattern=r"(?<=\.\s)(\w)")
90
+ new_main_commit_message_str = capitalize_after_final_dot_re.sub(
91
+ repl=capitalize_after_final_dot, string=new_main_commit_message
92
+ )
93
+
94
+ # Add only one final dot at the end of the commit description if it doesn't already have one.
95
+ final_dot_re = re.compile(pattern=r"(?<![!?])\.*$")
96
+ new_main_commit_message_str = final_dot_re.sub(repl="", string=new_main_commit_message_str)
97
+ new_main_commit_message_str += "."
98
+
99
+ # Get emoji for commit message.
100
+ if commit_type not in CommitTypeEmoji.types():
101
+ logger("Could not find emoji for the commit type: " + commit_type)
102
+ sys.exit(1)
103
+ commit_type_emoji = CommitTypeEmoji[commit_type.upper()].value
104
+
105
+ # Format final commit message.
106
+ formatted_commit_message = f"{commit_type_emoji} {commit_message}"
107
+ formatted_commit_message = formatted_commit_message.replace(main_commit_message, new_main_commit_message_str, 1)
108
+
109
+ # Write the formatted commit message back to the file.
110
+ Path(commit_msg_filepath).write_text(formatted_commit_message, encoding="utf-8")
111
+
112
+
113
+ if __name__ == "__main__":
114
+ main()
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-arguments
3
+ Version: 1.0.1
4
+ Description-Content-Type: text/markdown
5
+ Provides-Extra: development
6
+ Requires-Dist: pre-commit; extra == "development"
7
+
8
+ # PyArg
9
+
10
+ A small layer on top of `argparse` to build CLIs with sub-commands by **reusing parser classes**.
11
+
12
+ Each parser is a class. You define its arguments in one method, and then plug that class into another parser as a
13
+ command, without rewriting the arguments.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pip install .
19
+ ```
20
+
21
+ For development (changes apply without reinstalling):
22
+
23
+ ```bash
24
+ pip install -e .
25
+ ```
26
+
27
+ Requires Python 3.10+ and has no dependencies.
28
+
29
+ ## Quick start
30
+
31
+ Create a class that inherits from `PyArg` and define the arguments in `setup_arguments`:
32
+
33
+ ```python
34
+ from pyarg import PyArg
35
+
36
+
37
+ class Login(PyArg):
38
+ def setup_arguments(self) -> None:
39
+ self.add_argument("--username", "-u")
40
+ self.add_argument("--password", "-p")
41
+
42
+
43
+ parser = Login(name="Login")
44
+ print(parser.get_arguments())
45
+ ```
46
+
47
+ ```console
48
+ $ python main.py -u ana -p 123
49
+ {'username': 'ana', 'password': '123'}
50
+ ```
51
+
52
+ Since `PyArg` inherits from `argparse.ArgumentParser`, everything from `argparse` still works
53
+ (`add_argument`, `add_mutually_exclusive_group`, `type`, `nargs`, `choices`, etc.).
54
+
55
+ ## Sub-commands
56
+
57
+ Use `add_command_parser` to attach parsers to a main one. Each command is named after the parser `name`, converted to
58
+ lowercase with spaces replaced by `-` (`"Make Directory"` becomes `make-directory`).
59
+
60
+ ```python
61
+ from pyarg import PyArg
62
+
63
+
64
+ class Login(PyArg):
65
+ def setup_arguments(self) -> None:
66
+ self.add_argument("--username", "-u")
67
+ self.add_argument("--password", "-p")
68
+
69
+
70
+ class Logout(PyArg):
71
+ def setup_arguments(self) -> None:
72
+ self.add_argument("--all", action="store_true")
73
+
74
+
75
+ class App(PyArg):
76
+ def setup_arguments(self) -> None:
77
+ pass # The main parser can have its own arguments too.
78
+
79
+
80
+ app = App(name="My App", description="Demo app.")
81
+ app.add_command_parser(Login(name="Login", description="Sign in."))
82
+ app.add_command_parser(Logout(name="Logout"), description="Sign out.")
83
+
84
+ print(app.get_arguments())
85
+ ```
86
+
87
+ ```console
88
+ $ python main.py login -u ana -p 123
89
+ {'command': 'login', 'username': 'ana', 'password': '123'}
90
+
91
+ $ python main.py logout --all
92
+ {'command': 'logout', 'all': True}
93
+
94
+ $ python main.py -h
95
+ usage: My App [-h] {login,logout} ...
96
+ ```
97
+
98
+ The chosen command is available under the `command` key.
99
+
100
+ ## Reusing arguments
101
+
102
+ Arguments from other parsers can be inherited with `parents`:
103
+
104
+ ```python
105
+ class Common(PyArg):
106
+ def setup_arguments(self) -> None:
107
+ self.add_argument("--verbose", "-v", action="store_true")
108
+
109
+
110
+ class Build(PyArg):
111
+ def setup_arguments(self) -> None:
112
+ self.add_argument("--target")
113
+
114
+
115
+ build = Build(name="Build", parents=[Common(name="Common", add_help=False)])
116
+ ```
117
+
118
+ Use `add_help=False` on the parent parser so the `-h` option is not registered twice.
119
+
120
+ ## API
121
+
122
+ ### `PyArg(name, description=None, parents=[], **kwargs)`
123
+
124
+ Abstract class. It cannot be instantiated directly, you must inherit from it and implement `setup_arguments`.
125
+
126
+ - `name`: parser name. Used as `prog` in the help text and to build the command name.
127
+ - `description`: text shown in `-h`.
128
+ - `parents`: other parsers whose arguments are copied into this one.
129
+ - `**kwargs`: forwarded to `argparse.ArgumentParser`. The `prog`, `description`, `parents` and `conflict_handler`
130
+ keys are ignored, as `conflict_handler` is always `"resolve"`.
131
+
132
+ ### `setup_arguments(self) -> None`
133
+
134
+ Abstract method. Define all the arguments of the parser here. It runs at the end of `__init__`.
135
+
136
+ ### `command_name`
137
+
138
+ Read-only property with the command name derived from `name`.
139
+
140
+ ### `add_command_parser(parser, description=None, script_entrypoint=None)`
141
+
142
+ Adds `parser` as a sub-command. If `description` is not set, the parser's own description is used.
143
+
144
+ `script_entrypoint` stores a callable on the command, but `get_arguments` does not call it and removes it from the
145
+ result.
146
+
147
+ ### `get_arguments() -> dict[str, Any]`
148
+
149
+ Parses `sys.argv` and returns the arguments passed by the user as a dictionary.
150
+
151
+ ## Development
152
+
153
+ See [DEVELOPERS.md](DEVELOPERS.md).
@@ -0,0 +1,22 @@
1
+ .gitignore
2
+ .pre-commit-config.yaml
3
+ .releaserc.json
4
+ CHANGELOG.md
5
+ DEVELOPERS.md
6
+ Makefile
7
+ README.md
8
+ pyproject.toml
9
+ .github/workflows/claude-review.yml
10
+ .github/workflows/pre-commit.yml
11
+ .github/workflows/publish.yml
12
+ .github/workflows/release.yml
13
+ hooks/format_commit_message.py
14
+ py_arguments.egg-info/PKG-INFO
15
+ py_arguments.egg-info/SOURCES.txt
16
+ py_arguments.egg-info/dependency_links.txt
17
+ py_arguments.egg-info/requires.txt
18
+ py_arguments.egg-info/scm_file_list.json
19
+ py_arguments.egg-info/scm_version.json
20
+ py_arguments.egg-info/top_level.txt
21
+ pyarg/__init__.py
22
+ pyarg/parser.py
@@ -0,0 +1,3 @@
1
+
2
+ [development]
3
+ pre-commit
@@ -0,0 +1,19 @@
1
+ {
2
+ "files": [
3
+ ".github/workflows/claude-review.yml",
4
+ ".github/workflows/pre-commit.yml",
5
+ ".github/workflows/publish.yml",
6
+ ".github/workflows/release.yml",
7
+ ".gitignore",
8
+ ".pre-commit-config.yaml",
9
+ ".releaserc.json",
10
+ "CHANGELOG.md",
11
+ "DEVELOPERS.md",
12
+ "Makefile",
13
+ "README.md",
14
+ "hooks/format_commit_message.py",
15
+ "pyarg/__init__.py",
16
+ "pyarg/parser.py",
17
+ "pyproject.toml"
18
+ ]
19
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.0.1",
3
+ "distance": 0,
4
+ "node": "gc762b88cff95943831ae7a14f6558fa998ce34e9",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-11-08"
8
+ }
@@ -0,0 +1,5 @@
1
+ """Entrypoint module file for pyarg library."""
2
+
3
+ from pyarg.parser import PyArg
4
+
5
+ __all__ = ["PyArg"]
@@ -0,0 +1,118 @@
1
+ import argparse
2
+ import re
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Callable, Iterable
5
+ from typing import Any
6
+
7
+
8
+ # Classes.
9
+ class PyArg(ABC, argparse.ArgumentParser):
10
+ """
11
+ **PyArg** is an abstract class that uses `argparse.ArgumentParser` class that allows you to set up easily sub
12
+ parsers/commands from a parent parser.
13
+
14
+ Creates a class inhenrance from **PyArg**, set up all arguments in `setup_arguments` method, and then, you can
15
+ re-use the same parser definition to build your complex structure of parser arguments and CLI commands.
16
+
17
+ Usage:
18
+ ```
19
+ class Forms(PyArg):
20
+ def setup_arguments(self):
21
+ self.add_argument("--username", "-u")
22
+ self.add_argument("--password", "-p")
23
+
24
+ parser = Forms(name="Forms")
25
+ arguments = parser.get_arguments()
26
+ ```
27
+
28
+ Extra commands that trigger a function:
29
+ ```
30
+ class Application(PyArg):
31
+ def setup_arguments(self):
32
+ ...
33
+
34
+ class MakeDir(PyArg):
35
+ @staticmethod
36
+ def create_directory(location, name_list):
37
+ for name in name_list:
38
+ os.makedirs(os.path.join(location, name))
39
+
40
+ def setup_arguments(self):
41
+ self.add_argument("--location", "-l")
42
+ self.add_argument("--name", "-n", type=str, nargs="+")
43
+
44
+ class DeleteDir(PyArg):
45
+ @staticmethod
46
+ def delete_directory(path):
47
+ shutil.rmtree(path)
48
+
49
+ def setup_arguments(self):
50
+ self.add_argument("--path", "-p")
51
+
52
+ make_dir_parser = MakeDir(name="Make Directory")
53
+ delete_dir_parser = DeleteDir(name="Delete Directory")
54
+
55
+ app = Application(name="Directory Manager")
56
+ app.add_command_parser(parser=make_dir_parser, script_entrypoint=MakeDir.create_directory)
57
+ app.add_command_parser(parser=delete_dir_parser, script_entrypoint=DeleteDir.delete_directory)
58
+ app.get_arguments()
59
+ ```
60
+ """
61
+
62
+ # Special methods.
63
+ def __init__(
64
+ self, name: str, description: str | None = None, parents: Iterable["PyArg"] | None = None, **kwargs: Any
65
+ ) -> None:
66
+ # Remove parameters.
67
+ for to_remove in ("prog", "description", "parents", "conflict_handler"):
68
+ kwargs.pop(to_remove, None)
69
+ parents = [] if parents is None else parents
70
+ super().__init__(prog=name, description=description, parents=parents, conflict_handler="resolve", **kwargs)
71
+
72
+ # Attributes.
73
+ self.__command_name: str = self.__get_program_name(name=name)
74
+
75
+ # Set up extra sub parsers.
76
+ self.__subparser: None | argparse._SubParsersAction[argparse.ArgumentParser] = None
77
+
78
+ # Set up arguments.
79
+ self.setup_arguments()
80
+
81
+ # Properties.
82
+ @property
83
+ def command_name(self) -> str:
84
+ return self.__command_name
85
+
86
+ # Abstract methods.
87
+ @abstractmethod
88
+ def setup_arguments(self) -> None:
89
+ """Main method used to define all parameters of the parser."""
90
+
91
+ # Private methods.
92
+ def __get_program_name(self, name: str) -> str:
93
+ """Convert the parser name to a CLI command name."""
94
+ name = name.lower()
95
+ name = re.sub(pattern=r"\s+", repl="-", string=name.strip())
96
+ return name
97
+
98
+ # Methods.
99
+ def add_command_parser(
100
+ self, parser: "PyArg", description: str | None = None, script_entrypoint: Callable[..., Any] | None = None
101
+ ) -> None:
102
+ """Add an extra parser to a parent **PyArg** class to be called by `name`."""
103
+ if self.__subparser is None:
104
+ self.__subparser = self.add_subparsers(
105
+ dest="command", parser_class=argparse.ArgumentParser, help="Available commands."
106
+ )
107
+ subparser: argparse.ArgumentParser = self.__subparser.add_parser(
108
+ parents=[parser], name=parser.command_name, description=description or parser.description, add_help=False
109
+ )
110
+ # Add script.
111
+ if script_entrypoint:
112
+ subparser.set_defaults(_entrypoint=script_entrypoint)
113
+
114
+ def get_arguments(self) -> dict[str, Any]:
115
+ """Get the CLI arguments."""
116
+ args: dict[str, Any] = vars(self.parse_args())
117
+ args.pop("_entrypoint", None)
118
+ return args
@@ -0,0 +1,55 @@
1
+ # Build system.
2
+ [build-system]
3
+ requires = ["setuptools>=64", "setuptools-scm>=8"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ # Project metadata.
7
+ [project]
8
+ name = "py-arguments"
9
+ readme = "README.md"
10
+ authors = []
11
+ dynamic = ["version"]
12
+ dependencies = []
13
+
14
+ # Dependencies.
15
+ [project.optional-dependencies]
16
+ development = [
17
+ "pre-commit"
18
+ ]
19
+
20
+ [tool.setuptools]
21
+ packages = ["pyarg"]
22
+
23
+ # Version comes from the git tag created by semantic-release.
24
+ [tool.setuptools_scm]
25
+
26
+ # Ruff.
27
+ [tool.ruff]
28
+ line-length = 120
29
+
30
+ [tool.ruff.lint]
31
+ select = [
32
+ "E", # All pycodestyle errors
33
+ "F", # Pyflakes
34
+ "I", # All isort errors
35
+ "B", # All flake8-bugbear errors
36
+ "C4", # flake8-comprehensions
37
+ "UP", # All pyupgrade errors
38
+ "SIM", # flake8-simplify
39
+ ]
40
+ ignore = ["B008"] # Valid for FastAPI `Depends` call.
41
+
42
+ [tool.ruff.format]
43
+ quote-style = "double"
44
+ indent-style = "space"
45
+ line-ending = "auto"
46
+
47
+ # Mypy.
48
+ [tool.mypy]
49
+ mypy_path = "."
50
+ strict = true
51
+ ignore_missing_imports = true
52
+ explicit_package_bases = true
53
+ disallow_untyped_decorators = false
54
+ disallow_subclassing_any = false
55
+ plugins = ["pydantic.mypy"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+