hatchit 0.1.0__py3-none-any.whl

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 (38) hide show
  1. hatchit/__init__.py +13 -0
  2. hatchit/cli.py +32 -0
  3. hatchit/py.typed +0 -0
  4. hatchit/scaffold.py +340 -0
  5. hatchit/templates/docs/Makefile +29 -0
  6. hatchit/templates/docs/_static/.gitignore +1 -0
  7. hatchit/templates/docs/authors.md +4 -0
  8. hatchit/templates/docs/changelog.md +4 -0
  9. hatchit/templates/docs/conf.py +333 -0
  10. hatchit/templates/docs/contributing.md +4 -0
  11. hatchit/templates/docs/index.md +39 -0
  12. hatchit/templates/docs/license.md +5 -0
  13. hatchit/templates/docs/readme.md +4 -0
  14. hatchit/templates/docs/requirements.txt +9 -0
  15. hatchit/templates/github/dependabot.yml +15 -0
  16. hatchit/templates/github_workflows/publish-pypi.yml +52 -0
  17. hatchit/templates/github_workflows/run-tests.yml +73 -0
  18. hatchit/templates/gitignore/gitignore.template +73 -0
  19. hatchit/templates/precommit/pre-commit-config.yaml +55 -0
  20. hatchit/templates/root/.coveragerc +13 -0
  21. hatchit/templates/root/.gitattributes +10 -0
  22. hatchit/templates/root/.readthedocs.yml +21 -0
  23. hatchit/templates/root/.zenodo.json +14 -0
  24. hatchit/templates/root/AUTHORS.md +3 -0
  25. hatchit/templates/root/CHANGELOG.md +11 -0
  26. hatchit/templates/root/CITATION.cff +10 -0
  27. hatchit/templates/root/CONTRIBUTING.md +20 -0
  28. hatchit/templates/root/LICENSE.md +21 -0
  29. hatchit/templates/tox/uv_tox.ini +58 -0
  30. hatchit/templates/uv_github_workflows/pre-commit.yml +16 -0
  31. hatchit/templates/uv_github_workflows/publish-pypi.yml +53 -0
  32. hatchit/templates/uv_github_workflows/run-tests.yml +72 -0
  33. hatchit-0.1.0.dist-info/METADATA +82 -0
  34. hatchit-0.1.0.dist-info/RECORD +38 -0
  35. hatchit-0.1.0.dist-info/WHEEL +4 -0
  36. hatchit-0.1.0.dist-info/entry_points.txt +2 -0
  37. hatchit-0.1.0.dist-info/licenses/AUTHORS.md +3 -0
  38. hatchit-0.1.0.dist-info/licenses/LICENSE.md +21 -0
hatchit/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ import sys
2
+ from importlib.metadata import PackageNotFoundError, version # pragma: no cover
3
+
4
+ try:
5
+ dist_name = __name__
6
+ __version__ = version(dist_name)
7
+ except PackageNotFoundError: # pragma: no cover
8
+ __version__ = "unknown"
9
+ finally:
10
+ del version, PackageNotFoundError
11
+
12
+ # exports
13
+ from .scaffold import create_hatchit_repository
hatchit/cli.py ADDED
@@ -0,0 +1,32 @@
1
+ import argparse
2
+ import os
3
+ import sys
4
+
5
+ from .scaffold import create_hatchit_repository
6
+
7
+ __author__ = "Jayaram Kancherla"
8
+ __copyright__ = "Jayaram Kancherla"
9
+ __license__ = "MIT"
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="hatchit: Modern Python project scaffolder combining uv and hatch.")
14
+ parser.add_argument("project_path", help="Path where the new project should be created.")
15
+ parser.add_argument("--description", default="Add a short description here!", help="Optional project description.")
16
+ parser.add_argument("--license", default="MIT", help="License to use. Defaults to MIT.")
17
+
18
+ args = parser.parse_args()
19
+
20
+ project_path = args.project_path
21
+
22
+ if os.path.exists(project_path) and os.listdir(project_path):
23
+ print(f"Error: Directory {project_path} is not empty.")
24
+ sys.exit(1)
25
+
26
+ print(f"Hatching project at {project_path}...")
27
+ create_hatchit_repository(project_path=project_path, description=args.description, license=args.license)
28
+ print("Success! 🚀 💥")
29
+
30
+
31
+ if __name__ == "__main__":
32
+ main()
hatchit/py.typed ADDED
File without changes
hatchit/scaffold.py ADDED
@@ -0,0 +1,340 @@
1
+ import datetime
2
+ import shutil
3
+ import subprocess
4
+ from pathlib import Path
5
+ from warnings import warn
6
+
7
+ __author__ = "Jayaram Kancherla"
8
+ __copyright__ = "Jayaram Kancherla"
9
+ __license__ = "MIT"
10
+
11
+
12
+ def create_hatchit_repository(project_path: str, description: str, license: str) -> None:
13
+ """Create a new Python project using uv and hatch.
14
+
15
+ This function initializes a new Python project at the specified `project_path`
16
+ using the `uv` tool and `hatch` for version management and project scaffolding.
17
+ It sets up a basic project structure, including source code, tests, documentation,
18
+ and configuration files. The function also customizes certain files based on the
19
+ provided `description` and `license`.
20
+
21
+ Args:
22
+ project_path:
23
+ The path where the new project should be created.
24
+
25
+ description:
26
+ A short description of the project.
27
+
28
+ license:
29
+ The license to use for the project.
30
+
31
+ Example:
32
+ >>> create_hatchit_repository(
33
+ ... "my_project",
34
+ ... description="My new project",
35
+ ... license="MIT",
36
+ ... )
37
+
38
+ CLI usage:
39
+ $ hatchit my_project --description "My new project" --license MIT
40
+ """
41
+ if description is None:
42
+ description = "Add a short description here!"
43
+
44
+ subprocess.run(["uv", "init", "--no-workspace", "--lib", project_path], check=True)
45
+
46
+ # Clear out the dummy hello function generated by uv init
47
+ init_path = Path(project_path) / "src" / project_path.split("/")[-1].replace("-", "_") / "__init__.py"
48
+ if init_path.exists():
49
+ with open(init_path, "w") as init_f:
50
+ init_content = """from importlib.metadata import PackageNotFoundError, version # pragma: no cover
51
+
52
+ try:
53
+ dist_name = __name__
54
+ __version__ = version(dist_name)
55
+ except PackageNotFoundError: # pragma: no cover
56
+ __version__ = "unknown"
57
+ finally:
58
+ del version, PackageNotFoundError
59
+ """
60
+ init_f.write(init_content)
61
+
62
+ modified_files = []
63
+ template_dir = Path(__file__).parent / "templates"
64
+
65
+ # Add GitHub Actions
66
+ gh_actions_dir = Path(project_path) / ".github" / "workflows"
67
+ gh_actions_dir.mkdir(parents=True, exist_ok=True)
68
+
69
+ for workflow in ["run-tests.yml", "publish-pypi.yml", "pre-commit.yml"]:
70
+ src = template_dir / "uv_github_workflows" / workflow
71
+ dst = gh_actions_dir / workflow
72
+ shutil.copy2(src, dst)
73
+ modified_files.append(dst)
74
+
75
+ # Add dependabot.yml
76
+ dependabot_src = template_dir / "github" / "dependabot.yml"
77
+ dependabot_dst = Path(project_path) / ".github" / "dependabot.yml"
78
+ shutil.copy2(dependabot_src, dependabot_dst)
79
+ modified_files.append(dependabot_dst)
80
+
81
+ # Add tox.ini
82
+ tox_src = template_dir / "tox" / "uv_tox.ini"
83
+ tox_dst = Path(project_path) / "tox.ini"
84
+ shutil.copy2(tox_src, tox_dst)
85
+ modified_files.append(tox_dst)
86
+
87
+ # Add comprehensive .gitignore
88
+ gitignore_src = template_dir / "gitignore" / "gitignore.template"
89
+ gitignore_dst = Path(project_path) / ".gitignore"
90
+ shutil.copy2(gitignore_src, gitignore_dst)
91
+ modified_files.append(gitignore_dst)
92
+
93
+ proj_name = Path(project_path).parts[-1]
94
+
95
+ try:
96
+ git_name = subprocess.check_output(["git", "config", "user.name"], text=True).strip()
97
+ git_email = subprocess.check_output(["git", "config", "user.email"], text=True).strip()
98
+ except Exception as _:
99
+ warn("Git user.name and user.email not configured. Using default values.")
100
+ git_name = "First Author"
101
+ git_email = "first.author@example.com"
102
+
103
+ # Add top-level root files
104
+ for root_file in [
105
+ "CHANGELOG.md",
106
+ "CONTRIBUTING.md",
107
+ "LICENSE.md",
108
+ "AUTHORS.md",
109
+ ".coveragerc",
110
+ ".readthedocs.yml",
111
+ "CITATION.cff",
112
+ ".gitattributes",
113
+ ".zenodo.json",
114
+ ]:
115
+ src = template_dir / "root" / root_file
116
+ dst = Path(project_path) / root_file
117
+ if src.exists():
118
+ if root_file == "LICENSE.md":
119
+ with open(src, "r") as f:
120
+ content = f.read()
121
+
122
+ with open(dst, "w") as f:
123
+ f.write(content.replace("REPLACE_PROJECT_NAME", proj_name))
124
+ elif root_file == "AUTHORS.md":
125
+ with open(src, "r") as f:
126
+ content = f.read()
127
+
128
+ with open(dst, "w") as f:
129
+ f.write(content.replace("First Author <first.author@example.com>", f"{git_name} <{git_email}>"))
130
+ elif root_file == "CITATION.cff":
131
+ name_parts = git_name.split(" ", 1)
132
+ given_names = name_parts[0]
133
+ family_names = name_parts[1] if len(name_parts) > 1 else ""
134
+ today_str = datetime.date.today().strftime("%Y-%m-%d")
135
+
136
+ with open(src, "r") as f:
137
+ content = f.read()
138
+
139
+ with open(dst, "w") as f:
140
+ f.write(
141
+ content
142
+ .replace("REPLACE_PROJECT_NAME", proj_name)
143
+ .replace("YOUR_FIRST_NAME", given_names)
144
+ .replace("YOUR_LAST_NAME", family_names)
145
+ .replace("REPLACE_DATE", today_str)
146
+ )
147
+ elif root_file == ".zenodo.json":
148
+ with open(src, "r") as f:
149
+ content = f.read()
150
+
151
+ with open(dst, "w") as f:
152
+ f.write(
153
+ content
154
+ .replace("REPLACE_PROJECT_NAME", proj_name)
155
+ .replace("REPLACE_DESCRIPTION", description)
156
+ .replace("YOUR_NAME", git_name)
157
+ )
158
+ else:
159
+ shutil.copy2(src, dst)
160
+
161
+ modified_files.append(dst)
162
+
163
+ # Add pre-commit config
164
+ precommit_src = template_dir / "precommit" / "pre-commit-config.yaml"
165
+ precommit_dst = Path(project_path) / ".pre-commit-config.yaml"
166
+ shutil.copy2(precommit_src, precommit_dst)
167
+ modified_files.append(precommit_dst)
168
+
169
+ # Modify README
170
+ readme_path = Path(project_path) / "README.md"
171
+
172
+ new_readme = f"""[![PyPI-Server](https://img.shields.io/pypi/v/{proj_name}.svg)](https://pypi.org/project/{proj_name}/)
173
+ ![Unit tests](https://github.com/YOUR_ORG_OR_USERNAME/{proj_name}/actions/workflows/run-tests.yml/badge.svg)
174
+
175
+ # {proj_name}
176
+
177
+ > {description}
178
+
179
+ A longer description of your project goes here...
180
+
181
+ ## Install
182
+
183
+ To get started, install the package from [PyPI](https://pypi.org/project/{proj_name}/)
184
+
185
+ ```bash
186
+ pip install {proj_name}
187
+ ```
188
+
189
+ <!-- hatchit-notes -->
190
+
191
+ ## Note
192
+
193
+ This project has been set up using [hatchit](https://github.com/biocpy/hatchit)
194
+ and [uv](https://docs.astral.sh/uv/).
195
+ """
196
+
197
+ with open(readme_path, "w") as f:
198
+ f.write(new_readme)
199
+ modified_files.append(readme_path)
200
+
201
+ # Add docs
202
+ docs_dir = Path(project_path) / "docs"
203
+ src_docs_dir = template_dir / "docs"
204
+ shutil.copytree(src_docs_dir, docs_dir, dirs_exist_ok=True)
205
+
206
+ pkg_name = proj_name.replace("-", "_")
207
+
208
+ conf_py_path = docs_dir / "conf.py"
209
+ if conf_py_path.exists():
210
+ with open(conf_py_path, "r") as f:
211
+ content = f.read()
212
+
213
+ with open(conf_py_path, "w") as f:
214
+ f.write(content.replace("REPLACE_PROJECT_NAME", pkg_name).replace("YOUR_NAME", git_name))
215
+
216
+ modified_files.append(conf_py_path)
217
+
218
+ index_md_path = docs_dir / "index.md"
219
+ if index_md_path.exists():
220
+ with open(index_md_path, "r") as f:
221
+ content = f.read()
222
+
223
+ with open(index_md_path, "w") as f:
224
+ f.write(content.replace("REPLACE_PROJECT_NAME", proj_name))
225
+
226
+ modified_files.append(index_md_path)
227
+
228
+ for f in docs_dir.glob("**/*"):
229
+ if f.is_file() and f not in modified_files:
230
+ modified_files.append(f)
231
+
232
+ # Modify pyproject.toml
233
+ pyprj_path = Path(project_path) / "pyproject.toml"
234
+ with open(pyprj_path, "r") as f:
235
+ pyprj_content = f.read()
236
+
237
+ ruff_config = """
238
+ [tool.ruff]
239
+ line-length = 120
240
+ src = ["src"]
241
+ exclude = ["tests", "docs"]
242
+
243
+ [tool.ruff.lint]
244
+ extend-ignore = ["F821"]
245
+
246
+ [tool.ruff.lint.pydocstyle]
247
+ convention = "google"
248
+
249
+ [tool.ruff.lint.per-file-ignores]
250
+ "__init__.py" = ["E402", "F401"]
251
+
252
+ [tool.ruff.format]
253
+ docstring-code-format = true
254
+ docstring-code-line-length = 20
255
+
256
+ [tool.mypy]
257
+ strict = true
258
+
259
+ [tool.pytest.ini_options]
260
+ addopts = "--cov --cov-report term-missing"
261
+ testpaths = ["tests"]
262
+ """
263
+
264
+ import re
265
+
266
+ # Remove hardcoded version and replace with dynamic versioning
267
+ pyprj_content = re.sub(r'version\s*=\s*"[^"]+"\n', 'dynamic = ["version"]\n', pyprj_content)
268
+
269
+ # Replace build-system with hatchling and hatch-vcs
270
+ build_system = """[build-system]
271
+ requires = ["hatchling", "hatch-vcs"]
272
+ build-backend = "hatchling.build"
273
+
274
+ [tool.hatch.version]
275
+ source = "vcs"
276
+ fallback-version = "0.1.0"
277
+ """
278
+ pyprj_content = re.sub(
279
+ r'\[build-system\].*?build-backend\s*=\s*"[^"]+"\n', build_system, pyprj_content, flags=re.DOTALL
280
+ )
281
+
282
+ pyprj_content = pyprj_content.replace('description = "Add your description here"', f'description = "{description}"')
283
+
284
+ with open(pyprj_path, "w") as f:
285
+ f.write(pyprj_content + ruff_config)
286
+ modified_files.append(pyprj_path)
287
+
288
+ # tests directory
289
+ tests_dir = Path(project_path) / "tests"
290
+ tests_dir.mkdir(parents=True, exist_ok=True)
291
+ test_file = tests_dir / "test_basic.py"
292
+
293
+ pkg_name = proj_name.replace("-", "_")
294
+ test_content = f"""import {pkg_name}
295
+
296
+ def test_import():
297
+ '''Test that the package can be imported.'''
298
+ assert {pkg_name}.__name__ == "{pkg_name}"
299
+ """
300
+ with open(test_file, "w") as f:
301
+ f.write(test_content)
302
+
303
+ modified_files.append(test_file)
304
+
305
+ # Install dev dependencies
306
+ subprocess.run(
307
+ [
308
+ "uv",
309
+ "add",
310
+ "--dev",
311
+ "pytest",
312
+ "pytest-cov",
313
+ "sphinx",
314
+ "myst-nb",
315
+ "furo",
316
+ "sphinx-autodoc-typehints",
317
+ "linkify-it-py",
318
+ "mypy",
319
+ ],
320
+ cwd=project_path,
321
+ check=True,
322
+ )
323
+
324
+ try:
325
+ subprocess.run(["git", "init"], cwd=project_path, check=True, capture_output=True)
326
+ subprocess.run(["git", "add", "."], cwd=project_path, check=True, capture_output=True)
327
+
328
+ # Check if git user config exists before committing
329
+ name_configured = subprocess.run(["git", "config", "user.name"], capture_output=True).returncode == 0
330
+ email_configured = subprocess.run(["git", "config", "user.email"], capture_output=True).returncode == 0
331
+
332
+ if name_configured and email_configured:
333
+ subprocess.run(
334
+ ["git", "commit", "-m", "chore: initial setup"], cwd=project_path, check=True, capture_output=True
335
+ )
336
+ except Exception:
337
+ warn("Git is not installed or not configured. Skipping git initialization and commit.")
338
+ pass
339
+
340
+ print("hatchit complete! 🚀 💥")
@@ -0,0 +1,29 @@
1
+ # Makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = .
9
+ BUILDDIR = _build
10
+ AUTODOCDIR = api
11
+
12
+ # User-friendly check for sphinx-build
13
+ ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $?), 1)
14
+ $(error "The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://sphinx-doc.org/")
15
+ endif
16
+
17
+ .PHONY: help clean Makefile
18
+
19
+ # Put it first so that "make" without argument is like "make help".
20
+ help:
21
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
22
+
23
+ clean:
24
+ rm -rf $(BUILDDIR)/* $(AUTODOCDIR)
25
+
26
+ # Catch-all target: route all unknown targets to Sphinx using the new
27
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
28
+ %: Makefile
29
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@@ -0,0 +1 @@
1
+ # Empty directory
@@ -0,0 +1,4 @@
1
+ ```{include} ../AUTHORS.md
2
+ :relative-docs: docs/
3
+ :relative-images:
4
+ ```
@@ -0,0 +1,4 @@
1
+ ```{include} ../CHANGELOG.md
2
+ :relative-docs: docs/
3
+ :relative-images:
4
+ ```