pythonwrench 0.6.3__tar.gz → 0.6.4__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.
Files changed (133) hide show
  1. pythonwrench-0.6.4/.github/workflows/test.yaml +194 -0
  2. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/CHANGELOG.md +6 -0
  3. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/CITATION.cff +2 -2
  4. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/PKG-INFO +2 -2
  5. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/__init__.py +1 -1
  6. pythonwrench-0.6.4/src/pythonwrench/argparse/dataclass_.py +284 -0
  7. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/argparse/parsers.py +50 -20
  8. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/checksum.py +9 -9
  9. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/functools.py +1 -2
  10. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/re.py +1 -1
  11. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/typing/checks.py +0 -26
  12. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_argparse.py +58 -4
  13. pythonwrench-0.6.3/.github/workflows/test.yaml +0 -69
  14. pythonwrench-0.6.3/src/pythonwrench/argparse/dataclass_.py +0 -190
  15. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/.gitignore +0 -0
  16. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/.pre-commit-config.yaml +0 -0
  17. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/.readthedocs.yaml +0 -0
  18. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/LICENSE +0 -0
  19. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/README.md +0 -0
  20. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/Makefile +0 -0
  21. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/conf.py +0 -0
  22. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/index.rst +0 -0
  23. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/make.bat +0 -0
  24. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/modules.rst +0 -0
  25. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.abc.rst +0 -0
  26. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.argparse.dataclass_.rst +0 -0
  27. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.argparse.parsers.rst +0 -0
  28. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.argparse.rst +0 -0
  29. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.cast.rst +0 -0
  30. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.checksum.rst +0 -0
  31. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.collections.collections.rst +0 -0
  32. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.collections.prop.rst +0 -0
  33. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.collections.reducers.rst +0 -0
  34. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.collections.rst +0 -0
  35. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.concurrent.rst +0 -0
  36. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.csv.rst +0 -0
  37. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.dataclasses.rst +0 -0
  38. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.datetime.rst +0 -0
  39. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.difflib.rst +0 -0
  40. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.disk_cache.rst +0 -0
  41. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.enum.rst +0 -0
  42. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.functools.rst +0 -0
  43. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.hashlib.rst +0 -0
  44. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.importlib.rst +0 -0
  45. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.inspect.rst +0 -0
  46. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.json.rst +0 -0
  47. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.jsonl.rst +0 -0
  48. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.logging.rst +0 -0
  49. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.math.rst +0 -0
  50. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.os.rst +0 -0
  51. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.pickle.rst +0 -0
  52. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.random.rst +0 -0
  53. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.re.rst +0 -0
  54. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.rst +0 -0
  55. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.semver.rst +0 -0
  56. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.serialization.csv.rst +0 -0
  57. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.serialization.json.rst +0 -0
  58. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.serialization.jsonl.rst +0 -0
  59. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.serialization.pickle.rst +0 -0
  60. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.serialization.rst +0 -0
  61. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.time.rst +0 -0
  62. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.typing.checks.rst +0 -0
  63. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.typing.classes.rst +0 -0
  64. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.typing.rst +0 -0
  65. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/pythonwrench.warnings.rst +0 -0
  66. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/docs/update.bash +0 -0
  67. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/pyproject.toml +0 -0
  68. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/setup.py +0 -0
  69. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/__main__.py +0 -0
  70. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/_core.py +0 -0
  71. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/abc.py +0 -0
  72. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/argparse/__init__.py +0 -0
  73. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/cast.py +0 -0
  74. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/collections/__init__.py +0 -0
  75. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/collections/collections.py +0 -0
  76. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/collections/prop.py +0 -0
  77. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/collections/reducers.py +0 -0
  78. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/concurrent.py +0 -0
  79. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/csv.py +0 -0
  80. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/dataclasses.py +0 -0
  81. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/datetime.py +0 -0
  82. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/difflib.py +0 -0
  83. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/disk_cache.py +0 -0
  84. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/entrypoints/info.py +0 -0
  85. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/entrypoints/safe_rmdir.py +0 -0
  86. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/entrypoints/tree.py +0 -0
  87. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/enum.py +0 -0
  88. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/hashlib.py +0 -0
  89. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/importlib.py +0 -0
  90. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/inspect.py +0 -0
  91. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/json.py +0 -0
  92. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/jsonl.py +0 -0
  93. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/logging.py +0 -0
  94. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/math.py +0 -0
  95. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/os.py +0 -0
  96. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/pickle.py +0 -0
  97. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/random.py +0 -0
  98. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/semver.py +0 -0
  99. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/serialization/__init__.py +0 -0
  100. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/serialization/_core.py +0 -0
  101. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/serialization/csv.py +0 -0
  102. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/serialization/json.py +0 -0
  103. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/serialization/jsonl.py +0 -0
  104. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/serialization/pickle.py +0 -0
  105. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/time.py +0 -0
  106. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/typing/__init__.py +0 -0
  107. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/typing/classes.py +0 -0
  108. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/src/pythonwrench/warnings.py +0 -0
  109. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_abc.py +0 -0
  110. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_cast.py +0 -0
  111. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_checksum.py +0 -0
  112. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_collections.py +0 -0
  113. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_core.py +0 -0
  114. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_csv.py +0 -0
  115. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_dataclasses.py +0 -0
  116. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_difflib.py +0 -0
  117. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_disk_cache.py +0 -0
  118. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_entrypoints.py +0 -0
  119. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_enum.py +0 -0
  120. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_functools.py +0 -0
  121. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_hashlib.py +0 -0
  122. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_importlib.py +0 -0
  123. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_inspect.py +0 -0
  124. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_json.py +0 -0
  125. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_jsonl.py +0 -0
  126. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_logging.py +0 -0
  127. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_math.py +0 -0
  128. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_os.py +0 -0
  129. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_random.py +0 -0
  130. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_readme.py +0 -0
  131. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_semver.py +0 -0
  132. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/tests/test_typing.py +0 -0
  133. {pythonwrench-0.6.3 → pythonwrench-0.6.4}/uv.lock +0 -0
@@ -0,0 +1,194 @@
1
+ # Template: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
2
+
3
+ name: Python package using Pip
4
+
5
+ on:
6
+ push:
7
+ branches: [ main, dev** ]
8
+ pull_request:
9
+ branches: [ main, dev** ]
10
+
11
+ env:
12
+ CACHE_NUMBER: 0 # increase to reset cache manually
13
+ UV_NO_SYNC: 1
14
+
15
+ # Cancel workflow if a new push occurs
16
+ concurrency:
17
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
18
+ # Never interrupt a main-branch publication between PyPI and GitHub.
19
+ cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
20
+
21
+ jobs:
22
+ build-and-test:
23
+ runs-on: ${{ matrix.os }}
24
+
25
+ strategy:
26
+ matrix:
27
+ os: [ubuntu-24.04]
28
+ python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
29
+ include:
30
+ - os: windows-2022
31
+ python-version: "3.8"
32
+ - os: windows-latest
33
+ python-version: "3.14"
34
+ - os: macos-15
35
+ python-version: "3.8"
36
+ - os: macos-latest
37
+ python-version: "3.14"
38
+
39
+ steps:
40
+ # --- INSTALLATIONS ---
41
+ - name: Checkout repository
42
+ uses: actions/checkout@v5
43
+
44
+ - name: Install uv and set Python ${{ matrix.python-version }}
45
+ uses: astral-sh/setup-uv@v9.0.0
46
+ with:
47
+ python-version: ${{ matrix.python-version }}
48
+ enable-cache: true
49
+ cache-dependency-glob: "pyproject.toml"
50
+
51
+ - name: Install the project
52
+ run: |
53
+ uv sync --extra tests
54
+
55
+ # --- TESTS ---
56
+ - name: Print install info
57
+ run: |
58
+ uv run pythonwrench-info
59
+
60
+ - name: Check format with Ruff
61
+ run: |
62
+ uv run ruff check
63
+
64
+ - name: Test with coverage + pytest
65
+ run: |
66
+ uv run coverage run -m pytest -v
67
+
68
+ - name: Show coverage results
69
+ run: |
70
+ uv run coverage report -m
71
+
72
+ release-check:
73
+ name: Check whether a release is needed
74
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
75
+ needs: build-and-test
76
+ runs-on: ubuntu-24.04
77
+ permissions:
78
+ contents: read
79
+ outputs:
80
+ version: ${{ steps.release.outputs.version }}
81
+ should-publish: ${{ steps.release.outputs.should-publish }}
82
+
83
+ steps:
84
+ - name: Checkout repository and tags
85
+ uses: actions/checkout@v5
86
+ with:
87
+ fetch-depth: 0
88
+
89
+ - name: Validate release metadata
90
+ id: release
91
+ shell: python
92
+ run: |
93
+ import os
94
+ import pathlib
95
+ import re
96
+
97
+ init_text = pathlib.Path("src/pythonwrench/__init__.py").read_text(encoding="utf-8")
98
+ version_match = re.search(
99
+ r'^__version__\s*=\s*["\'](\d+\.\d+\.\d+)["\']\s*$',
100
+ init_text,
101
+ flags=re.MULTILINE,
102
+ )
103
+ if version_match is None:
104
+ raise SystemExit(
105
+ "__version__ must be a stable X.Y.Z release in "
106
+ "src/pythonwrench/__init__.py"
107
+ )
108
+
109
+ version = version_match.group(1)
110
+ changelog = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8")
111
+ heading = rf"^## \[{re.escape(version)}\](?:[ \t]+[^\r\n]*)?$"
112
+ if re.search(heading, changelog, flags=re.MULTILINE) is None:
113
+ raise SystemExit(f"CHANGELOG.md has no release section for {version}")
114
+
115
+ tag_ref = pathlib.Path(".git/refs/tags") / f"v{version}"
116
+ tag_exists = tag_ref.exists()
117
+ if not tag_exists:
118
+ import subprocess
119
+
120
+ tag_exists = subprocess.run(
121
+ ["git", "rev-parse", "--verify", "--quiet", f"refs/tags/v{version}"],
122
+ check=False,
123
+ ).returncode == 0
124
+
125
+ with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
126
+ print(f"version={version}", file=output)
127
+ print(f"should-publish={'false' if tag_exists else 'true'}", file=output)
128
+
129
+ if tag_exists:
130
+ print(f"v{version} already exists; publication will be skipped.")
131
+ else:
132
+ print(f"v{version} is ready to publish.")
133
+
134
+ publish:
135
+ name: Publish to PyPI and GitHub Releases
136
+ if: needs.release-check.outputs.should-publish == 'true'
137
+ needs: release-check
138
+ runs-on: ubuntu-24.04
139
+ environment:
140
+ name: pypi
141
+ url: https://pypi.org/project/pythonwrench/${{ needs.release-check.outputs.version }}/
142
+ permissions:
143
+ contents: write
144
+ id-token: write
145
+ env:
146
+ RELEASE_VERSION: ${{ needs.release-check.outputs.version }}
147
+
148
+ steps:
149
+ - name: Checkout tested commit
150
+ uses: actions/checkout@v5
151
+
152
+ - name: Install uv
153
+ uses: astral-sh/setup-uv@v9.0.0
154
+
155
+ - name: Build distributions
156
+ run: uv build
157
+
158
+ - name: Validate distributions
159
+ run: uvx twine check dist/*
160
+
161
+ - name: Extract release notes from changelog
162
+ shell: python
163
+ run: |
164
+ import os
165
+ import pathlib
166
+ import re
167
+
168
+ version = os.environ["RELEASE_VERSION"]
169
+ changelog = pathlib.Path("CHANGELOG.md").read_text(encoding="utf-8")
170
+ section = re.search(
171
+ rf"^## \[{re.escape(version)}\](?:[ \t]+[^\r\n]*)?\n(.*?)(?=^## \[|\Z)",
172
+ changelog,
173
+ flags=re.MULTILINE | re.DOTALL,
174
+ )
175
+ if section is None:
176
+ raise SystemExit(f"Could not extract changelog notes for {version}")
177
+ pathlib.Path("release-notes.md").write_text(
178
+ section.group(1).strip() + "\n",
179
+ encoding="utf-8",
180
+ )
181
+
182
+ - name: Publish distributions to PyPI
183
+ uses: pypa/gh-action-pypi-publish@release/v1
184
+ with:
185
+ skip-existing: true
186
+
187
+ - name: Create GitHub Release and tag
188
+ env:
189
+ GH_TOKEN: ${{ github.token }}
190
+ run: |
191
+ gh release create "v${RELEASE_VERSION}" dist/* \
192
+ --target "${GITHUB_SHA}" \
193
+ --title "v${RELEASE_VERSION}" \
194
+ --notes-file release-notes.md
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.6.4] 2026-08-17
6
+ ### Added
7
+ - Function `parse_args_using_dataclass` can now handle multiple dataclass at once.
8
+ - Function `parse_args_using_dataclass` now has option `bool_action`.
9
+
10
+
5
11
  ## [0.6.3] 2026-08-06
6
12
  ### Added
7
13
  - Handle `Enum`, `Path`, and add `register_parser_fn` decorator for `parse_to_type`.
@@ -17,5 +17,5 @@ keywords:
17
17
  - tools
18
18
  - utilities
19
19
  license: MIT
20
- version: 0.6.3
21
- date-released: '2026-08-06'
20
+ version: 0.6.4
21
+ date-released: '2026-08-17'
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pythonwrench
3
- Version: 0.6.3
3
+ Version: 0.6.4
4
4
  Summary: Python library with tools for typing, manipulating collections, and more!
5
5
  Project-URL: Homepage, https://pypi.org/project/pythonwrench/
6
6
  Project-URL: Documentation, https://pythonwrench.readthedocs.io/
@@ -9,7 +9,7 @@ __author_email__ = "labbeti.pub@gmail.com"
9
9
  __license__ = "MIT"
10
10
  __maintainer__ = "Étienne Labbé (Labbeti)"
11
11
  __status__ = "Development"
12
- __version__ = "0.6.3"
12
+ __version__ = "0.6.4"
13
13
 
14
14
 
15
15
  from typing import TYPE_CHECKING
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from argparse import ArgumentParser
5
+ from dataclasses import MISSING, fields
6
+ from typing import (
7
+ Any,
8
+ Dict,
9
+ Iterable,
10
+ Literal,
11
+ Optional,
12
+ Tuple,
13
+ Type,
14
+ TypeVar,
15
+ Union,
16
+ get_args,
17
+ overload,
18
+ )
19
+
20
+ from pythonwrench.argparse.parsers import (
21
+ ListParsing,
22
+ _is_iterable_type_like,
23
+ _is_literal_type,
24
+ _search_parse_fn,
25
+ )
26
+ from pythonwrench.functools import filter_and_call
27
+ from pythonwrench.typing.classes import (
28
+ Dataclass,
29
+ DataclassInstance,
30
+ )
31
+ from pythonwrench.warnings import deprecated_alias
32
+
33
+ try:
34
+ from argparse import BooleanOptionalAction # type: ignore
35
+ except ImportError:
36
+
37
+ class BooleanOptionalAction: ...
38
+
39
+
40
+ T_Dataclass = TypeVar("T_Dataclass", bound=Dataclass)
41
+ T_DataclassInstance = TypeVar("T_DataclassInstance", bound=DataclassInstance)
42
+ T_DataclassInstance_2 = TypeVar("T_DataclassInstance_2", bound=DataclassInstance)
43
+ T_DataclassInstance_3 = TypeVar("T_DataclassInstance_3", bound=DataclassInstance)
44
+ T_DataclassInstance_4 = TypeVar("T_DataclassInstance_4", bound=DataclassInstance)
45
+ T_DataclassInstance_5 = TypeVar("T_DataclassInstance_5", bound=DataclassInstance)
46
+
47
+ _BoolActionName = Literal["store", "store_true", "store_false", "bool_optional"]
48
+ BoolAction = Union[_BoolActionName, Type[BooleanOptionalAction]]
49
+
50
+
51
+ @overload
52
+ def parse_args_using_dataclass(
53
+ dataclass_type: Type[T_DataclassInstance],
54
+ *,
55
+ args: Optional[Iterable[str]] = None,
56
+ parser: Optional[ArgumentParser] = None,
57
+ list_parsing: ListParsing = "argparse",
58
+ bool_action: BoolAction = "store",
59
+ add_dashed_arg: bool = True,
60
+ ) -> T_DataclassInstance: ...
61
+
62
+
63
+ @overload
64
+ def parse_args_using_dataclass(
65
+ dataclass_type: Type[T_DataclassInstance],
66
+ dataclass_type_2: Type[T_DataclassInstance_2],
67
+ /,
68
+ *,
69
+ args: Optional[Iterable[str]] = None,
70
+ parser: Optional[ArgumentParser] = None,
71
+ list_parsing: ListParsing = "argparse",
72
+ bool_action: BoolAction = "store",
73
+ add_dashed_arg: bool = True,
74
+ ) -> Tuple[
75
+ T_DataclassInstance,
76
+ T_DataclassInstance_2,
77
+ ]: ...
78
+
79
+
80
+ @overload
81
+ def parse_args_using_dataclass(
82
+ dataclass_type: Type[T_DataclassInstance],
83
+ dataclass_type_2: Type[T_DataclassInstance_2],
84
+ dataclass_type_3: Type[T_DataclassInstance_3],
85
+ /,
86
+ *,
87
+ args: Optional[Iterable[str]] = None,
88
+ parser: Optional[ArgumentParser] = None,
89
+ list_parsing: ListParsing = "argparse",
90
+ bool_action: BoolAction = "store",
91
+ add_dashed_arg: bool = True,
92
+ ) -> Tuple[
93
+ T_DataclassInstance,
94
+ T_DataclassInstance_2,
95
+ T_DataclassInstance_3,
96
+ ]: ...
97
+
98
+
99
+ @overload
100
+ def parse_args_using_dataclass(
101
+ dataclass_type: Type[T_DataclassInstance],
102
+ dataclass_type_2: Type[T_DataclassInstance_2],
103
+ dataclass_type_3: Type[T_DataclassInstance_3],
104
+ dataclass_type_4: Type[T_DataclassInstance_4],
105
+ /,
106
+ *,
107
+ args: Optional[Iterable[str]] = None,
108
+ parser: Optional[ArgumentParser] = None,
109
+ list_parsing: ListParsing = "argparse",
110
+ bool_action: BoolAction = "store",
111
+ add_dashed_arg: bool = True,
112
+ ) -> Tuple[
113
+ T_DataclassInstance,
114
+ T_DataclassInstance_2,
115
+ T_DataclassInstance_3,
116
+ T_DataclassInstance_4,
117
+ ]: ...
118
+
119
+
120
+ @overload
121
+ def parse_args_using_dataclass(
122
+ dataclass_type: Type[T_DataclassInstance],
123
+ dataclass_type_2: Type[T_DataclassInstance_2],
124
+ dataclass_type_3: Type[T_DataclassInstance_3],
125
+ dataclass_type_4: Type[T_DataclassInstance_4],
126
+ dataclass_type_5: Type[T_DataclassInstance_5],
127
+ /,
128
+ *,
129
+ args: Optional[Iterable[str]] = None,
130
+ parser: Optional[ArgumentParser] = None,
131
+ list_parsing: ListParsing = "argparse",
132
+ bool_action: BoolAction = "store",
133
+ add_dashed_arg: bool = True,
134
+ ) -> Tuple[
135
+ T_DataclassInstance,
136
+ T_DataclassInstance_2,
137
+ T_DataclassInstance_3,
138
+ T_DataclassInstance_4,
139
+ T_DataclassInstance_5,
140
+ ]: ...
141
+
142
+
143
+ def parse_args_using_dataclass(
144
+ dataclass_type: Type[DataclassInstance],
145
+ *dataclass_types: Type[DataclassInstance],
146
+ args: Optional[Iterable[str]] = None,
147
+ parser: Optional[ArgumentParser] = None,
148
+ list_parsing: ListParsing = "argparse",
149
+ bool_action: BoolAction = "store",
150
+ add_dashed_arg: bool = True,
151
+ ) -> Union[
152
+ DataclassInstance,
153
+ Tuple[DataclassInstance, ...],
154
+ ]:
155
+ """Converts prog args to a typed dataclass using argparse.
156
+
157
+ Currently only supports dataclasses that contains only builtin scalars: str, int, float, None, bool OR list of builtin scalars.
158
+ """
159
+ init_parser = parser
160
+ dataclass_types = (dataclass_type,) + dataclass_types
161
+ del dataclass_type
162
+
163
+ for dataclass_type_i in dataclass_types:
164
+ parser = add_dataclass_fields_to_parser(
165
+ dataclass_type_i,
166
+ parser=parser,
167
+ list_parsing=list_parsing,
168
+ bool_action=bool_action,
169
+ add_dashed_arg=add_dashed_arg,
170
+ )
171
+ assert parser is not None
172
+
173
+ parsed, argv = parser.parse_known_args(args)
174
+ if len(argv) > 0:
175
+ msg = f"Found {len(argv)} unknown arguments: {argv}."
176
+ raise ValueError(msg)
177
+
178
+ dataclass_insts = []
179
+ for dataclass_type_i in dataclass_types:
180
+ if init_parser is None and len(dataclass_types) == 1:
181
+ instance = dataclass_type_i(**parsed.__dict__)
182
+ else:
183
+ instance = filter_and_call(
184
+ dataclass_type_i,
185
+ _fill_all_arguments=True,
186
+ **parsed.__dict__,
187
+ )
188
+ dataclass_insts.append(instance)
189
+
190
+ if len(dataclass_insts) == 1:
191
+ return dataclass_insts[0]
192
+ else:
193
+ return tuple(dataclass_insts)
194
+
195
+
196
+ def add_dataclass_fields_to_parser(
197
+ dataclass_type: Type[T_DataclassInstance],
198
+ *,
199
+ parser: Optional[ArgumentParser],
200
+ list_parsing: ListParsing = "argparse",
201
+ bool_action: BoolAction = "store",
202
+ add_dashed_arg: bool = True,
203
+ ) -> ArgumentParser:
204
+ """Perform the add dataclass fields to parser operation."""
205
+ if parser is None:
206
+ parser = ArgumentParser()
207
+
208
+ for field in fields(dataclass_type):
209
+ kwds = {}
210
+ posargs = [f"--{field.name}"]
211
+ if add_dashed_arg and "_" in field.name:
212
+ dashed_arg_name = field.name.replace("_", "-")
213
+ posargs.append(f"--{dashed_arg_name}")
214
+
215
+ if field.default is MISSING and field.default_factory is MISSING:
216
+ if bool_action != "store" and field.type is bool:
217
+ msg = f"Invalid arguments: boolean '{field.name}' without default value is incompatible with {bool_action=}."
218
+ raise RuntimeError(msg)
219
+ kwds["required"] = True
220
+
221
+ elif field.default is not MISSING:
222
+ kwds["default"] = field.default
223
+ elif field.default_factory is not MISSING:
224
+ kwds["default"] = field.default_factory() # type: ignore
225
+ else:
226
+ msg = f"Invalid field {field.name}: found values for default and default_factory."
227
+ raise ValueError(msg)
228
+
229
+ try:
230
+ inner_kwds = _get_kwds_for_type(field.type, list_parsing, bool_action)
231
+ except (ValueError, TypeError, RuntimeError) as err:
232
+ msg = f"Invalid field {field.name}: field type '{field.type}' is not supported."
233
+ raise type(err)(msg) from err
234
+
235
+ kwds.update(inner_kwds)
236
+ parser.add_argument(*posargs, **kwds)
237
+
238
+ return parser
239
+
240
+
241
+ def _get_kwds_for_type(
242
+ field_type: Any,
243
+ list_parsing: Optional[ListParsing],
244
+ bool_action: BoolAction,
245
+ ) -> Dict[str, Any]:
246
+ """Perform the get kwds for type operation."""
247
+ if bool_action == "bool_optional":
248
+ bool_action = BooleanOptionalAction
249
+ kwds = {}
250
+
251
+ if bool_action != "store" and field_type is bool:
252
+ kwds["action"] = bool_action
253
+ return kwds
254
+
255
+ elif list_parsing == "argparse" and _is_iterable_type_like(field_type):
256
+ type_args = get_args(field_type)
257
+ if isinstance(type_args, tuple) and len(type_args) == 1:
258
+ item_type = type_args[0]
259
+ kwds = _get_kwds_for_type(
260
+ item_type, list_parsing=None, bool_action=bool_action
261
+ )
262
+ kwds["nargs"] = "*"
263
+ return kwds
264
+
265
+ parse_fn = _search_parse_fn(field_type, list_parsing=list_parsing)
266
+
267
+ if parse_fn is not None:
268
+ kwds["type"] = parse_fn
269
+ if _is_literal_type(field_type):
270
+ kwds["choices"] = get_args(field_type)
271
+ return kwds
272
+
273
+ else:
274
+ msg = (
275
+ f"Unsupported type {field_type}. (with {list_parsing=} and {bool_action=})"
276
+ )
277
+ raise TypeError(msg)
278
+
279
+
280
+ # ALIASES
281
+ @deprecated_alias(add_dataclass_fields_to_parser)
282
+ def new_parser_from_dataclass(*args, **kwargs):
283
+ """Perform the new parser from dataclass operation."""
284
+ ...
@@ -2,6 +2,7 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  import re
5
+ from collections.abc import Iterable as _RuntimeIterable
5
6
  from enum import Enum
6
7
  from functools import partial
7
8
  from pathlib import Path
@@ -17,16 +18,11 @@ from typing import (
17
18
  TypeVar,
18
19
  Union,
19
20
  get_args,
21
+ get_origin,
20
22
  overload,
21
23
  )
22
24
 
23
25
  from pythonwrench._core import Predicate
24
- from pythonwrench.typing.checks import (
25
- _is_iterable_type_like,
26
- _is_literal_type,
27
- _is_optional_type,
28
- _is_union_type,
29
- )
30
26
  from pythonwrench.typing.classes import NoneType, UnionType
31
27
  from pythonwrench.warnings import deprecated_alias
32
28
 
@@ -38,7 +34,6 @@ TargetType = Union[
38
34
  UnionType,
39
35
  "Type[Literal]",
40
36
  "Type[Optional]",
41
- Tuple[type, ...],
42
37
  ]
43
38
 
44
39
  ListParsing = Literal["argparse", "brackets"]
@@ -51,12 +46,9 @@ DEFAULT_NONE_VALUES = ("None", "null")
51
46
  _PARSER_REGISTRY: List[Tuple[Union[TargetType, Predicate], Callable]] = []
52
47
 
53
48
 
54
- class ParseError(ValueError): ...
55
-
56
-
57
49
  @overload
58
50
  def register_parser_fn(
59
- type_: Union[TargetType, Predicate, None],
51
+ type_: Union[TargetType[T], Predicate, None],
60
52
  fn: None = None,
61
53
  ) -> Callable[[T_Callable], T_Callable]:
62
54
  """Perform the register parser fn operation."""
@@ -65,7 +57,7 @@ def register_parser_fn(
65
57
 
66
58
  @overload
67
59
  def register_parser_fn(
68
- type_: Union[TargetType, Predicate, None],
60
+ type_: Union[TargetType[T], Predicate, None],
69
61
  fn: T_Callable,
70
62
  ) -> T_Callable:
71
63
  """Perform the register parser fn operation."""
@@ -73,7 +65,7 @@ def register_parser_fn(
73
65
 
74
66
 
75
67
  def register_parser_fn(
76
- type_: Union[TargetType, Predicate, None],
68
+ type_: Union[TargetType[T], Predicate, None],
77
69
  fn: Optional[Callable] = None,
78
70
  ) -> Callable:
79
71
  """Perform the register parser fn operation."""
@@ -107,7 +99,7 @@ def parse_to_type(
107
99
  - True values: 'True', 'T', 'yes', 'y', '1'.
108
100
  - False values: 'False', 'F', 'no', 'n', '0'.
109
101
  - None values: 'None', 'null'
110
- - Other raises ParseError.
102
+ - Other raises ValueError.
111
103
  """
112
104
  parse_fn = get_parse_fn(
113
105
  target_type,
@@ -144,6 +136,16 @@ def get_parse_fn(
144
136
  list_parsing=list_parsing,
145
137
  handle_exception=handle_exception,
146
138
  )
139
+ parse_fn = _search_parse_fn(type_, **kwds)
140
+
141
+ if parse_fn is None:
142
+ msg = f"Invalid argument {type_=}. (no valid type or typing found in registry)"
143
+ raise ValueError(msg)
144
+
145
+ return parse_fn
146
+
147
+
148
+ def _search_parse_fn(type_: TargetType[T], **kwds) -> Optional[Callable[[str], T]]:
147
149
  if type_ is None:
148
150
  type_ = NoneType
149
151
 
@@ -161,11 +163,9 @@ def get_parse_fn(
161
163
  msg = f"Invalid argument {type_or_pred_i=}. (excepted type or predicate function)"
162
164
  raise ValueError(msg)
163
165
 
164
- if parse_fn is None:
165
- msg = f"Invalid argument {type_=}. (no valid type or typing found in registry)"
166
- raise ValueError(msg)
166
+ if parse_fn is not None:
167
+ parse_fn = partial(parse_fn, **kwds)
167
168
 
168
- parse_fn = partial(parse_fn, **kwds)
169
169
  return parse_fn
170
170
 
171
171
 
@@ -189,7 +189,7 @@ def parse_to_bool(
189
189
  return False
190
190
 
191
191
  values = tuple(true_values + false_values)
192
- output = ParseError(f"Invalid argument '{x}'. (expected one of {values})")
192
+ output = ValueError(f"Invalid argument '{x}'. (expected one of {values})")
193
193
  return _handle_output(x, handle_exception, output)
194
194
 
195
195
 
@@ -251,9 +251,39 @@ def _parse_to_str(x: str, **kwds) -> str:
251
251
  return x
252
252
 
253
253
 
254
+ def _is_enum_type(x: Any, **kwds) -> bool:
255
+ """Perform the is enum type operation."""
256
+ return isinstance(x, type) and issubclass(x, Enum)
257
+
258
+
259
+ def _is_iterable_type_like(x: Any) -> bool:
260
+ """Perform the is iterable type like operation."""
261
+ return any(xi in (list, Iterable, _RuntimeIterable) for xi in (x, get_origin(x)))
262
+
263
+
264
+ def _is_literal_type(x: Any) -> bool:
265
+ """Perform the is literal type operation."""
266
+ origin = get_origin(x)
267
+ return origin is Literal
268
+
269
+
270
+ def _is_optional_type(x: Any) -> bool:
271
+ """Perform the is optional type operation."""
272
+ return getattr(x, "__name__", None) == "Optional"
273
+
274
+
275
+ def _is_union_type(x: Any) -> bool:
276
+ """Perform the is union type operation."""
277
+ origin = get_origin(x)
278
+ return origin == Union or getattr(origin, "__name__", None) in (
279
+ "Union",
280
+ "UnionType",
281
+ )
282
+
283
+
254
284
  def _is_enum_for_parsing(x: Any, **kwds) -> bool:
255
285
  """Perform the is enum for parsing operation."""
256
- return isinstance(x, type) and issubclass(x, Enum)
286
+ return _is_enum_type(x)
257
287
 
258
288
 
259
289
  def _is_iterable_type_like_for_parsing(