diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..58f5d49 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,113 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.toml] +indent_style = space +indent_size = 4 + +[*.yml,yaml,json] +indent_style = space +indent_size = 2 + +[{*.py,*.pyw,*.whl}] +indent_style = space +indent_size = 4 +ij_continuation_indent_size = 4 +ij_python_add_indent_inside_injections = false +ij_python_align_collections_and_comprehensions = true +ij_python_align_multiline_imports = true +ij_python_align_multiline_parameters = false +ij_python_align_multiline_parameters_in_calls = false +ij_python_blank_line_at_file_end = true +ij_python_blank_lines_after_imports = 2 +ij_python_blank_lines_after_local_imports = 0 +ij_python_blank_lines_around_class = 1 +ij_python_blank_lines_around_method = 1 +ij_python_blank_lines_around_top_level_classes_functions = 2 +ij_python_blank_lines_before_first_method = 0 +ij_python_call_parameters_new_line_after_left_paren = true +ij_python_call_parameters_right_paren_on_new_line = true +ij_python_call_parameters_wrap = on_every_item +ij_python_dict_alignment = 0 +ij_python_dict_new_line_after_left_brace = true +ij_python_dict_new_line_before_right_brace = true +ij_python_dict_wrapping = on_every_item +ij_python_format_injected_fragments = true +ij_python_from_import_new_line_after_left_parenthesis = true +ij_python_from_import_new_line_before_right_parenthesis = true +ij_python_from_import_parentheses_force_if_multiline = true +ij_python_from_import_trailing_comma_if_multiline = false +ij_python_from_import_wrapping = on_every_item +ij_python_hang_closing_brackets = false +ij_python_keep_blank_lines_in_code = 1 +ij_python_keep_blank_lines_in_declarations = 1 +ij_python_keep_indents_on_empty_lines = false +ij_python_keep_line_breaks = true +ij_python_list_new_line_after_left_bracket = true +ij_python_list_new_line_before_right_bracket = true +ij_python_list_wrapping = on_every_item +ij_python_method_parameters_new_line_after_left_paren = true +ij_python_method_parameters_right_paren_on_new_line = true +ij_python_method_parameters_wrap = on_every_item +ij_python_new_line_after_colon = true +ij_python_new_line_after_colon_multi_clause = true +ij_python_optimize_imports_always_split_from_imports = true +ij_python_optimize_imports_case_insensitive_order = false +ij_python_optimize_imports_join_from_imports_with_same_source = false +ij_python_optimize_imports_sort_by_type_first = true +ij_python_optimize_imports_sort_imports = true +ij_python_optimize_imports_sort_names_in_from_imports = false +ij_python_set_new_line_after_left_brace = true +ij_python_set_new_line_before_right_brace = true +ij_python_set_wrapping = on_every_item +ij_python_space_after_comma = true +ij_python_space_after_number_sign = true +ij_python_space_after_py_colon = true +ij_python_space_before_backslash = true +ij_python_space_before_comma = false +ij_python_space_before_for_semicolon = false +ij_python_space_before_lbracket = false +ij_python_space_before_method_call_parentheses = false +ij_python_space_before_method_parentheses = false +ij_python_space_before_number_sign = true +ij_python_space_before_py_colon = false +ij_python_space_within_empty_method_call_parentheses = false +ij_python_space_within_empty_method_parentheses = false +ij_python_spaces_around_additive_operators = true +ij_python_spaces_around_assignment_operators = true +ij_python_spaces_around_bitwise_operators = true +ij_python_spaces_around_eq_in_keyword_argument = false +ij_python_spaces_around_eq_in_named_parameter = false +ij_python_spaces_around_equality_operators = true +ij_python_spaces_around_multiplicative_operators = true +ij_python_spaces_around_power_operator = true +ij_python_spaces_around_relational_operators = true +ij_python_spaces_around_shift_operators = true +ij_python_spaces_within_braces = false +ij_python_spaces_within_brackets = false +ij_python_spaces_within_method_call_parentheses = false +ij_python_spaces_within_method_parentheses = false +ij_python_tuple_new_line_after_left_parenthesis = true +ij_python_tuple_new_line_before_right_parenthesis = true +ij_python_tuple_wrapping = on_every_item +ij_python_use_continuation_indent_for_arguments = false +ij_python_use_continuation_indent_for_collection_and_comprehensions = false +ij_python_use_continuation_indent_for_parameters = true +ij_python_use_trailing_comma_in_arguments_list = false +ij_python_use_trailing_comma_in_collections = false +ij_python_use_trailing_comma_in_parameter_list = false +ij_python_wrap_long_lines = false diff --git a/.gitignore b/.gitignore index fad6f04..076666a 100644 --- a/.gitignore +++ b/.gitignore @@ -61,5 +61,4 @@ Thumbs.db # Windows thumbnail cache debug.log nohup.out -# Cookiecutter/Cruft metadata (for template updates) -.cruft.json +/.idea/ diff --git a/README.md b/README.md index ba41e93..19851a5 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,10 @@ Any and all advice, support, PR's, etc are welcome and would be greatly apprecia # Why does this project exist? -Unfortunately the [Hypermodern Python Cookiecutter] is no longer maintained nor modern. +Unfortunately, the [Hypermodern Python Cookiecutter] is no longer maintained nor modern. While it will always have a place in my heart, there have been far too many improvements in Python tooling to keep using it as is. -For a whle I maintained [a personal fork](https://github.com/56kyle/cookiecutter-hypermodern-python) that I would update, however when it came time to switch +For a while I maintained [a personal fork](https://github.com/56kyle/cookiecutter-hypermodern-python) that I would update, however when it came time to switch to new tooling such as [ruff], [uv], [maturin], etc, I found the process of updating the existing tooling to be extremly painful. The [Hypermodern Python Cookiecutter] remains as a fantastic sendoff point for devs interested in building a 2021 style Python Package, but there were @@ -34,7 +34,7 @@ The [Robust Python Cookiecutter] exists to solve a few main concerns One of the main issues I encountered with [my personal fork] of the [Hypermodern Python Cookiecutter] was that any change I made to my repos would mean a later conflict if I tried to rerun [cookiecutter] to sync a change from a different project. -Thankfully [cruft] exists specifically to help with this issue. It enables us to periodically create PR's to add in any fixes +Thankfully, [cruft] exists specifically to help with this issue. It enables us to periodically create PR's to add in any fixes the [Robust Python Cookiecutter] may have added. Additionally, extra care is put in to use tooling specific config files whenever possible to help reduce merge conflicts occurring @@ -76,12 +76,12 @@ Overall it's rather rare that people debate over tooling for no reason. Most thi ## CI/CD Vendor Lock Now don't get me wrong, I love [github-actions] and do pretty much everything in my power to avoid [bitbucket-pipelines]. -However not all jobs have the luxury of github, and I would love to be able to just use the same template for both my personal and professional projects. +However, not all jobs have the luxury of github, and I would love to be able to just use the same template for both my personal and professional projects. The [Robust Python Cookiecutter] focuses on being as modular as possible for areas that connect to the CI/CD pipeline. Additionally, there will always be either alternative CI/CD options or at a minimum basic examples of what the translated CI/CD pipeline would look like. -Finally the main reason that this task is even possible is that the [Robust Python Cookiecutter] mirrors all of the CI/CD steps in it's local dev tooling. +Finally, the main reason that this task is even possible is that the [Robust Python Cookiecutter] mirrors all of the CI/CD steps in it's local dev tooling. The local [noxfile] is designed to match up directly with the CI/CD each step of the way. The [Hypermodern Python Cookiecutter] did this where it could afford to also, however the lack of [uv] meant it would significantly increase CI/CD times if done everywhere. diff --git a/noxfile.py b/noxfile.py index 7562ccc..60c6728 100644 --- a/noxfile.py +++ b/noxfile.py @@ -155,10 +155,10 @@ def docs(session: Session): session.log(f"Cleaning template docs build directory: {docs_build_dir}") docs_build_dir.parent.mkdir(parents=True, exist_ok=True) - session.run("uv", "run", "sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-E", external=True) + session.run("sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-E") session.log("Building template documentation.") - session.run("uv", "run", "sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-W", external=True) + session.run("sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-W") session.log(f"Template documentation built in {docs_build_dir.resolve()}.") @@ -199,7 +199,7 @@ def test(session: Session) -> None: session.install("-e", ".", external=True) session.log("Running generated project's default checks...") - session.run("uv", "run", "nox", external=True) + session.run("nox") session.log(f"Cleaning up temporary directory: {temp_dir}") shutil.rmtree(temp_dir) @@ -221,7 +221,7 @@ def release_template(session: Session): session.skip("Git not available.") session.log("Checking Commitizen availability via uvx.") - session.run("uvx", "cz", "--version", successcodes=[0], external=True) + session.run("cz", "--version", successcodes=[0]) increment = session.posargs[0] if session.posargs else None session.log( diff --git a/pyproject.toml b/pyproject.toml index 6dd11b9..4882d9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,12 +10,12 @@ dependencies = [ "loguru>=0.7.3", "platformdirs>=4.3.8", "retrocookie>=0.4.3", - "typer>=0.15.3", + "typer>=0.15.4", ] [dependency-groups] dev = [ - "commitizen>=4.7.0", + "commitizen>=4.8.2", "nox>=2025.5.1", "pre-commit>=4.2.0", ] diff --git a/uv.lock b/uv.lock index 6ea43ff..fd8f794 100644 --- a/uv.lock +++ b/uv.lock @@ -346,7 +346,7 @@ wheels = [ [[package]] name = "commitizen" -version = "4.7.0" +version = "4.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "argcomplete" }, @@ -362,9 +362,9 @@ dependencies = [ { name = "tomlkit" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/7a/2c2a781b3e227f528b19e1144efcfdd61f7414bf9cf84ba6bbdc215f3427/commitizen-4.7.0.tar.gz", hash = "sha256:ef95f2ef354b438dce7c6164e5d47d10cc377df666ee65a116bcfcb146bb0c0a", size = 53254, upload-time = "2025-05-10T13:47:33.585Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/15/c2fe85c0224886109b5061419acea2e20539be1b4bff619a16d7295fe0f2/commitizen-4.8.2.tar.gz", hash = "sha256:4fc73126c7300f715f11b85242550677722c57767b579100e869ccd45143e2c5", size = 53235, upload-time = "2025-05-22T03:16:39.915Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/2d/0ae2129dd1071f16fffa373f9223971f84808a7fee85c0cefc1db2ffaec8/commitizen-4.7.0-py3-none-any.whl", hash = "sha256:2d274e013a6e09bc69f97fe64d6a6389926e2a22f5a4a19b16571d5b4c31083c", size = 76010, upload-time = "2025-05-10T13:47:31.75Z" }, + { url = "https://files.pythonhosted.org/packages/0e/40/2b81df1b3ec24c41004512feba0884895b84748775d21642690120539a30/commitizen-4.8.2-py3-none-any.whl", hash = "sha256:86cae0bd8e1da889389d828b30a5acb79b62f9290f9274b127ee9d8c189eb16c", size = 76074, upload-time = "2025-05-22T03:16:38.431Z" }, ] [[package]] @@ -441,12 +441,12 @@ requires-dist = [ { name = "loguru", specifier = ">=0.7.3" }, { name = "platformdirs", specifier = ">=4.3.8" }, { name = "retrocookie", specifier = ">=0.4.3" }, - { name = "typer", specifier = ">=0.15.3" }, + { name = "typer", specifier = ">=0.15.4" }, ] [package.metadata.requires-dev] dev = [ - { name = "commitizen", specifier = ">=4.7.0" }, + { name = "commitizen", specifier = ">=4.8.2" }, { name = "nox", specifier = ">=2025.5.1" }, { name = "pre-commit", specifier = ">=4.2.0" }, ] @@ -1831,7 +1831,7 @@ wheels = [ [[package]] name = "typer" -version = "0.15.3" +version = "0.15.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1839,9 +1839,9 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/1a/5f36851f439884bcfe8539f6a20ff7516e7b60f319bbaf69a90dc35cc2eb/typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c", size = 101641, upload-time = "2025-04-28T21:40:59.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/89/c527e6c848739be8ceb5c44eb8208c52ea3515c6cf6406aa61932887bf58/typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3", size = 101559, upload-time = "2025-05-14T16:34:57.704Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/20/9d953de6f4367163d23ec823200eb3ecb0050a2609691e512c8b95827a9b/typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd", size = 45253, upload-time = "2025-04-28T21:40:56.269Z" }, + { url = "https://files.pythonhosted.org/packages/c9/62/d4ba7afe2096d5659ec3db8b15d8665bdcb92a3c6ff0b95e99895b335a9c/typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173", size = 45258, upload-time = "2025-05-14T16:34:55.583Z" }, ] [[package]] diff --git a/{{cookiecutter.project_name}}/.cz.toml b/{{cookiecutter.project_name}}/.cz.toml index c18146c..5548a61 100644 --- a/{{cookiecutter.project_name}}/.cz.toml +++ b/{{cookiecutter.project_name}}/.cz.toml @@ -11,6 +11,8 @@ write_version_files = [ "src/{{ cookiecutter.package_name }}/__init__.py:__version__", ] commit_msg_file = ".git/COMMIT_EDITMSG" +retry_after_failure = true +update_changelog_on_bump = true [tool.commitizen.github] release = true diff --git a/{{cookiecutter.project_name}}/.editorconfig b/{{cookiecutter.project_name}}/.editorconfig index a8faee7..58f5d49 100644 --- a/{{cookiecutter.project_name}}/.editorconfig +++ b/{{cookiecutter.project_name}}/.editorconfig @@ -5,11 +5,109 @@ charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false -[*.{py,toml}] +[*.toml] indent_style = space indent_size = 4 [*.yml,yaml,json] indent_style = space indent_size = 2 + +[{*.py,*.pyw,*.whl}] +indent_style = space +indent_size = 4 +ij_continuation_indent_size = 4 +ij_python_add_indent_inside_injections = false +ij_python_align_collections_and_comprehensions = true +ij_python_align_multiline_imports = true +ij_python_align_multiline_parameters = false +ij_python_align_multiline_parameters_in_calls = false +ij_python_blank_line_at_file_end = true +ij_python_blank_lines_after_imports = 2 +ij_python_blank_lines_after_local_imports = 0 +ij_python_blank_lines_around_class = 1 +ij_python_blank_lines_around_method = 1 +ij_python_blank_lines_around_top_level_classes_functions = 2 +ij_python_blank_lines_before_first_method = 0 +ij_python_call_parameters_new_line_after_left_paren = true +ij_python_call_parameters_right_paren_on_new_line = true +ij_python_call_parameters_wrap = on_every_item +ij_python_dict_alignment = 0 +ij_python_dict_new_line_after_left_brace = true +ij_python_dict_new_line_before_right_brace = true +ij_python_dict_wrapping = on_every_item +ij_python_format_injected_fragments = true +ij_python_from_import_new_line_after_left_parenthesis = true +ij_python_from_import_new_line_before_right_parenthesis = true +ij_python_from_import_parentheses_force_if_multiline = true +ij_python_from_import_trailing_comma_if_multiline = false +ij_python_from_import_wrapping = on_every_item +ij_python_hang_closing_brackets = false +ij_python_keep_blank_lines_in_code = 1 +ij_python_keep_blank_lines_in_declarations = 1 +ij_python_keep_indents_on_empty_lines = false +ij_python_keep_line_breaks = true +ij_python_list_new_line_after_left_bracket = true +ij_python_list_new_line_before_right_bracket = true +ij_python_list_wrapping = on_every_item +ij_python_method_parameters_new_line_after_left_paren = true +ij_python_method_parameters_right_paren_on_new_line = true +ij_python_method_parameters_wrap = on_every_item +ij_python_new_line_after_colon = true +ij_python_new_line_after_colon_multi_clause = true +ij_python_optimize_imports_always_split_from_imports = true +ij_python_optimize_imports_case_insensitive_order = false +ij_python_optimize_imports_join_from_imports_with_same_source = false +ij_python_optimize_imports_sort_by_type_first = true +ij_python_optimize_imports_sort_imports = true +ij_python_optimize_imports_sort_names_in_from_imports = false +ij_python_set_new_line_after_left_brace = true +ij_python_set_new_line_before_right_brace = true +ij_python_set_wrapping = on_every_item +ij_python_space_after_comma = true +ij_python_space_after_number_sign = true +ij_python_space_after_py_colon = true +ij_python_space_before_backslash = true +ij_python_space_before_comma = false +ij_python_space_before_for_semicolon = false +ij_python_space_before_lbracket = false +ij_python_space_before_method_call_parentheses = false +ij_python_space_before_method_parentheses = false +ij_python_space_before_number_sign = true +ij_python_space_before_py_colon = false +ij_python_space_within_empty_method_call_parentheses = false +ij_python_space_within_empty_method_parentheses = false +ij_python_spaces_around_additive_operators = true +ij_python_spaces_around_assignment_operators = true +ij_python_spaces_around_bitwise_operators = true +ij_python_spaces_around_eq_in_keyword_argument = false +ij_python_spaces_around_eq_in_named_parameter = false +ij_python_spaces_around_equality_operators = true +ij_python_spaces_around_multiplicative_operators = true +ij_python_spaces_around_power_operator = true +ij_python_spaces_around_relational_operators = true +ij_python_spaces_around_shift_operators = true +ij_python_spaces_within_braces = false +ij_python_spaces_within_brackets = false +ij_python_spaces_within_method_call_parentheses = false +ij_python_spaces_within_method_parentheses = false +ij_python_tuple_new_line_after_left_parenthesis = true +ij_python_tuple_new_line_before_right_parenthesis = true +ij_python_tuple_wrapping = on_every_item +ij_python_use_continuation_indent_for_arguments = false +ij_python_use_continuation_indent_for_collection_and_comprehensions = false +ij_python_use_continuation_indent_for_parameters = true +ij_python_use_trailing_comma_in_arguments_list = false +ij_python_use_trailing_comma_in_collections = false +ij_python_use_trailing_comma_in_parameter_list = false +ij_python_wrap_long_lines = false diff --git a/{{cookiecutter.project_name}}/.github/workflows/release-python.yml b/{{cookiecutter.project_name}}/.github/workflows/release-python.yml index e3eed7b..15f95da 100644 --- a/{{cookiecutter.project_name}}/.github/workflows/release-python.yml +++ b/{{cookiecutter.project_name}}/.github/workflows/release-python.yml @@ -61,7 +61,7 @@ jobs: env: # TestPyPI credentials stored as secrets in GitHub Settings -> Secrets TWINE_USERNAME: __token__ # Standard username when using API tokens - TWINE_PASSWORD: {% raw %}[{% raw %} {{ "${{ secrets.TESTPYPI_API_TOKEN }}" }} {% raw %}]{% endraw %} # Use GitHub Encrypted Secret + TWINE_PASSWORD: {% raw %}${{ secrets.TESTPYPI_API_TOKEN }}{% endraw } # Use GitHub Encrypted Secret # Optional: If uv publish requires different config for repository URL, pass TWINE_REPOSITORY or similar run: uvx nox -s publish-package -- --repository testpypi # Call the publish-package session, passing repository arg @@ -73,7 +73,7 @@ jobs: uses: simple-changelog/action@v3 # Action to parse CHANGELOG.md with: path: CHANGELOG.md # Path to your CHANGELOG.md - tag: {% raw %}${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag }}{% endraw %} # Pass the tag name + tag: { "${{ github.event_name == 'push' && github.ref_name || github.event.inputs.tag }}" } # Pass the tag name # Define outputs from this job that other jobs (like create_github_release) can use. outputs: @@ -91,21 +91,20 @@ jobs: needs: build_and_testpypi # Only run on tag push events, NOT on manual dispatch for the final PyPI publish - if: {% raw %}github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'){% endraw %} - + if: { "github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')" } steps: - - name: Download package artifacts # Get packages built in the previous job + - name: Download package artifacts uses: actions/download-artifact@v4 with: - name: distribution-packages # Must match the artifact name from build job - path: dist/ # Download into the 'dist' directory + name: distribution-packages + path: dist/ - - name: Set up Python # Needed to run uvx/publish task + - name: Set up Python uses: actions/setup-python@v5 with: - python-version: {% raw %}3.x{% endraw %} # Same version as build job for consistency + python-version-file: .python-version - - name: Set up uv # Install uv for this job + - name: Set up uv uses: astral-sh/setup-uv@v6 # --- Publish to Production PyPI Step --- @@ -116,7 +115,7 @@ jobs: env: # Production PyPI credentials stored as secrets in GitHub Settings -> Secrets TWINE_USERNAME: __token__ - TWINE_PASSWORD: {% raw %}[{% raw %} {{ "${{ secrets.PYPI_API_TOKEN }}" }} {% raw %}]{% endraw %} # Use GitHub Encrypted Secret + TWINE_PASSWORD: {% raw %}${{ secrets.PYPI_API_TOKEN }}{% endraw %} # Use GitHub Encrypted Secret # Optional: TWINE_REPOSITORY if publishing to a custom production index run: uvx nox -s publish-package # Call the publish-package session (defaults to pypi.org) @@ -129,7 +128,7 @@ jobs: needs: build_and_testpypi # Only run this job if triggered by a tag push - if: {% raw %}github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'){% endraw %} + if: {% raw %}(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v'){% endraw %} steps: - name: Download package artifacts # Get built artifacts for release assets diff --git a/{{cookiecutter.project_name}}/.gitignore b/{{cookiecutter.project_name}}/.gitignore index fad6f04..1d65b34 100644 --- a/{{cookiecutter.project_name}}/.gitignore +++ b/{{cookiecutter.project_name}}/.gitignore @@ -8,6 +8,8 @@ /docs/_build/ /src/*.egg-info/ *.egg-info/ +target +/.idea/ # Byte-code files *.pyc @@ -60,6 +62,3 @@ Thumbs.db # Windows thumbnail cache # Development logs/outputs debug.log nohup.out - -# Cookiecutter/Cruft metadata (for template updates) -.cruft.json diff --git a/{{cookiecutter.project_name}}/.pre-commit-config.yaml b/{{cookiecutter.project_name}}/.pre-commit-config.yaml index 8556ff9..eff2767 100644 --- a/{{cookiecutter.project_name}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_name}}/.pre-commit-config.yaml @@ -1,23 +1,30 @@ # .pre-commit-config.yaml # See https://pre-commit.com/ -default_stages: [commit] +default_stages: [pre-commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-added-large-files - args: ["--max-size=1000000"] + args: ["--maxkb=2000"] - id: check-yaml - id: check-toml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.5 + rev: v0.11.11 hooks: - id: ruff-format - args: [--config={{ cookiecutter.project_name }}/.ruff.toml] + args: [--config=.ruff.toml] - id: ruff - args: [--fix, --exit-non-zero-on-fix, --config={{ cookiecutter.project_name }}/.ruff.toml] + args: [--fix, --exit-non-zero-on-fix, --config=.ruff.toml] + + - repo: https://github.com/commitizen-tools/commitizen + rev: v4.8.2 + hooks: + - id: commitizen + - id: commitizen-branch + stages: [ commit-msg ] diff --git a/{{cookiecutter.project_name}}/noxfile.py b/{{cookiecutter.project_name}}/noxfile.py index 3981360..d9a97a4 100644 --- a/{{cookiecutter.project_name}}/noxfile.py +++ b/{{cookiecutter.project_name}}/noxfile.py @@ -1,5 +1,10 @@ """Noxfile for the {{cookiecutter.project_name}} project.""" + +import os +import shlex + from pathlib import Path +from textwrap import dedent from typing import List import nox @@ -23,62 +28,138 @@ PACKAGE_NAME: str = "{{cookiecutter.package_name}}" +def activate_virtualenv_in_precommit_hooks(session: Session) -> None: + """Activate virtualenv in hooks installed by pre-commit. + + This function patches git hooks installed by pre-commit to activate the + session's virtual environment. This allows pre-commit to locate hooks in + that environment when invoked from git. + + Args: + session: The Session object. + """ + assert session.bin is not None # nosec + + # Only patch hooks containing a reference to this session's bindir. Support + # quoting rules for Python and bash, but strip the outermost quotes so we + # can detect paths within the bindir, like /python. + bindirs = [ + bindir[1:-1] if bindir[0] in "'\"" else bindir for bindir in (repr(session.bin), shlex.quote(session.bin)) + ] + + virtualenv = session.env.get("VIRTUAL_ENV") + if virtualenv is None: + return + + headers = { + # pre-commit < 2.16.0 + "python": f"""\ + import os + os.environ["VIRTUAL_ENV"] = {virtualenv!r} + os.environ["PATH"] = os.pathsep.join(( + {session.bin!r}, + os.environ.get("PATH", ""), + )) + """, + # pre-commit >= 2.16.0 + "bash": f"""\ + VIRTUAL_ENV={shlex.quote(virtualenv)} + PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH" + """, + # pre-commit >= 2.17.0 on Windows forces sh shebang + "/bin/sh": f"""\ + VIRTUAL_ENV={shlex.quote(virtualenv)} + PATH={shlex.quote(session.bin)}"{os.pathsep}$PATH" + """, + } + + hookdir: Path = Path(".git") / "hooks" + if not hookdir.is_dir(): + return + + for hook in hookdir.iterdir(): + if hook.name.endswith(".sample") or not hook.is_file(): + continue + + if not hook.read_bytes().startswith(b"#!"): + continue + + text: str = hook.read_text() + + if not any((Path("A") == Path("a") and bindir.lower() in text.lower()) or bindir in text for bindir in bindirs): + continue + + lines: list[str] = text.splitlines() + + for executable, header in headers.items(): + if executable in lines[0].lower(): + lines.insert(1, dedent(header)) + hook.write_text("\n".join(lines)) + break + + @nox.session(python=DEFAULT_PYTHON_VERSION, name="pre-commit") -def pre_commit(session: Session) -> None: - """Run pre-commit checks.""" +def precommit(session: Session) -> None: + """Lint using pre-commit.""" + args: list[str] = session.posargs or ["run", "--all-files", "--hook-stage=manual", "--show-diff-on-failure"] + session.log("Installing pre-commit dependencies...") - session.run("uv", "sync", "--locked", "--group", "dev", "--group", "pre-commit", external=True) + session.install("-e", ".", "--group", "dev") + + session.run("pre-commit", *args) + if args and args[0] == "install": + activate_virtualenv_in_precommit_hooks(session) @nox.session(python=DEFAULT_PYTHON_VERSION, name="format-python") def format_python(session: Session) -> None: """Run Python code formatter (Ruff format).""" session.log("Installing formatting dependencies...") - session.run("uv", "sync", "--locked", "--group", "dev", "--group", "lint", external=True) + session.install("-e", ".", "--group", "dev", "--group", "lint") session.log(f"Running Ruff formatter check with py{session.python}.") # Use --check, not fix. Fixing is done by pre-commit or manual run. - session.run("uv", "run", "ruff", "format", *session.posargs, external=True) + session.run("ruff", "format", *session.posargs) @nox.session(python=DEFAULT_PYTHON_VERSION, name="lint-python") def lint_python(session: Session) -> None: """Run Python code linters (Ruff check, Pydocstyle rules).""" session.log("Installing linting dependencies...") - session.run("uv", "sync", "--locked", "--group", "dev", "--group", "lint", external=True) + session.install("-e", ".", "--group", "dev", "--group", "lint") session.log(f"Running Ruff check with py{session.python}.") - session.run("uv", "run", "ruff", "check", "--verbose", external=True) + session.run("ruff", "check", "--verbose") @nox.session(python=PYTHON_VERSIONS) def typecheck(session: Session) -> None: """Run static type checking (Pyright) on Python code.""" session.log("Installing type checking dependencies...") - session.run("uv", "sync", "--locked", "--group", "dev", "--group", "typecheck", external=True) + session.install("-e", ".", "--group", "dev", "--group", "typecheck") session.log(f"Running Pyright check with py{session.python}.") - session.run("uv", "run", "pyright", external=True) + session.run("pyright") @nox.session(python=DEFAULT_PYTHON_VERSION, name="security-python") def security_python(session: Session) -> None: """Run code security checks (Bandit) on Python code.""" session.log("Installing security dependencies...") - session.run("uv", "sync", "--locked", "--group", "dev", "--group", "security", external=True) + session.install("-e", ".", "--group", "dev", "--group", "security") session.log(f"Running Bandit static security analysis with py{session.python}.") - session.run("uv", "run", "bandit", "-r", PACKAGE_NAME, "-c", ".bandit", "-ll", "-s", external=True) + session.run("bandit", "-r", PACKAGE_NAME, "-c", "bandit.yml", "-ll") session.log(f"Running pip-audit dependency security check with py{session.python}.") - session.run("uv", "run", "pip-audit", "--python", str(Path(session.python)), external=True) + session.run("pip-audit") @nox.session(python=PYTHON_VERSIONS, name="tests-python") def tests_python(session: Session) -> None: """Run the Python test suite (pytest with coverage).""" session.log("Installing test dependencies...") - session.run("uv", "sync", "--locked", "--group", "dev", "--group", "test", external=True) + session.install("-e", ".", "--group", "dev", "--group", "test") session.log(f"Running test suite with py{session.python}.") test_results_dir = Path("test-results") @@ -107,16 +188,16 @@ def tests_rust(session: Session) -> None: def docs_build(session: Session) -> None: """Build the project documentation (Sphinx).""" session.log("Installing documentation dependencies...") - session.run("uv", "sync", "--locked", "--group", "dev", "--group", "docs", external=True) + session.install("-e", ".", "--group", "dev", "--group", "docs") session.log(f"Building documentation with py{session.python}.") docs_build_dir = Path("docs") / "_build" / "html" session.log(f"Cleaning build directory: {docs_build_dir}") - session.run("uv", "run", "sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-E", external=True) + session.run("sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-E") session.log("Building documentation.") - session.run("uv", "run", "sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-W", external=True) + session.run("sphinx-build", "-b", "html", "docs", str(docs_build_dir), "-W") @nox.session(python=DEFAULT_PYTHON_VERSION, name="build-python") @@ -124,13 +205,13 @@ def build_python(session: Session) -> None: """Build sdist and wheel packages (uv build).""" session.log("Installing build dependencies...") # Sync core & dev deps are needed for accessing project source code. - session.run("uv", "sync", "--locked", "--group", "dev", external=True) + session.install("-e", ".", "--group", "dev") session.log(f"Building sdist and wheel packages with py{session.python}.") {% if cookiecutter.add_rust_extension == 'y' -%} session.run("uv", "build", "--sdist", "--wheel", "--outdir", "dist/", external=True) {% else -%} - session.run("uvx", "maturin", "develop", "--uv", external=True) + session.run("maturin", "develop", "--uv") {% endif -%} session.log("Built packages in ./dist directory:") @@ -160,7 +241,7 @@ def build_container(session: Session) -> None: current_dir: Path = Path.cwd() session.log(f"Ensuring core dependencies are synced in {current_dir.resolve()} for build context...") - session.run("uv", "sync", "--locked", external=True) + session.run("-e", ".") session.log(f"Building Docker image using {container_cli}.") project_image_name = PACKAGE_NAME.replace("_", "-").lower() @@ -176,10 +257,10 @@ def publish_python(session: Session) -> None: Requires packages to be built first (`nox -s build-python` or `nox -s build`). Requires TWINE_USERNAME/TWINE_PASSWORD or TWINE_API_KEY environment variables set (usually in CI). """ - session.run("uv", "sync", "--locked", "--group", "dev", external=True) + session.install("-e", ".", "--group", "dev") session.log("Checking built packages with Twine.") - session.run("uvx", "twine", "check", "dist/*", external=True) + session.run("twine", "check", "dist/*") session.log("Publishing packages to PyPI.") session.run("uv", "publish", "dist/*", external=True) @@ -202,7 +283,7 @@ def release(session: Session) -> None: Optionally accepts increment (major, minor, patch) after '--'. """ session.log("Running release process using Commitizen...") - session.run("uv", "sync", "--locked", "--group", "dev", external=True) + session.install("-e", ".", "--group", "dev") try: session.run("git", "version", success_codes=[0], external=True, silent=True) @@ -211,7 +292,7 @@ def release(session: Session) -> None: session.skip("Git not available.") session.log("Checking Commitizen availability via uvx.") - session.run("uvx", "cz", "--version", success_codes=[0], external=True) + session.run("cz", "--version", success_codes=[0]) increment = session.posargs[0] if session.posargs else None session.log( @@ -246,7 +327,7 @@ def tox(session: Session) -> None: Accepts tox args after '--' (e.g., `nox -s tox -- -e py39`). """ session.log("Running Tox test matrix via uvx...") - session.run("uv", "sync", "--locked", "--group", "dev", external=True) + session.install("-e", ".", "--group", "dev") tox_ini_path = Path("tox.ini") if not tox_ini_path.exists(): @@ -254,9 +335,9 @@ def tox(session: Session) -> None: session.skip("tox.ini not present.") session.log("Checking Tox availability via uvx.") - session.run("uvx", "tox", "--version", success_codes=[0], external=True) + session.run("tox", "--version", success_codes=[0]) - session.run("uvx", "tox", *session.posargs, external=True) + session.run("tox", *session.posargs) # --- COMBINED/ORCHESTRATION SESSIONS --- @@ -312,13 +393,13 @@ def coverage(session: Session) -> None: session.log("Note: Ensure 'nox -s test-python' was run across all desired Python versions first to generate coverage data.") session.log("Installing dependencies for coverage report session...") - session.run("uv", "sync", "--locked", "--group", "dev", "--group", "test", external=True) + session.install("-e", ".", "--group", "dev", "--group", "test") coverage_combined_file: Path = Path.cwd() / ".coverage" session.log("Combining coverage data.") try: - session.run("uv", "run", "coverage", "combine", external=True) + session.run("coverage", "combine") session.log(f"Combined coverage data into {coverage_combined_file.resolve()}") except CommandFailed as e: if e.returncode == 1: @@ -329,9 +410,9 @@ def coverage(session: Session) -> None: session.log("Generating HTML coverage report.") coverage_html_dir = Path("coverage-html") - session.run("uv", "run", "coverage", "html", "--directory", str(coverage_html_dir), external=True) + session.run("coverage", "html", "--directory", str(coverage_html_dir)) session.log("Running terminal coverage report.") - session.run("uv", "run", "coverage", "report", external=True) + session.run("coverage", "report") session.log(f"Coverage reports generated in ./{coverage_html_dir} and terminal.") diff --git a/{{cookiecutter.project_name}}/pyproject.toml b/{{cookiecutter.project_name}}/pyproject.toml index 4bc1e10..e98158e 100644 --- a/{{cookiecutter.project_name}}/pyproject.toml +++ b/{{cookiecutter.project_name}}/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "{{cookiecutter.package_name}}" -version = "0.1.0" -description = "{{cookiecutter.package_name}}" +name = "{{cookiecutter.project_name}}" +version = "{{cookiecutter.version}}" +description = "{{cookiecutter.project_name}}" authors = [ { name = "{{cookiecutter.author}}", email = "{{cookiecutter.email}}" }, ] @@ -25,8 +25,9 @@ dependencies = [ [dependency-groups] dev = [ "commitizen>=4.7.0", - "nox>={{cookiecutter.copyright_year}}.5.1", + "nox>=2025.5.1", "pre-commit>=4.2.0", + "pre-commit-hooks>=5.0.0", ] docs = [ "furo>=2024.8.6", diff --git a/{{cookiecutter.project_name}}/pyrightconfig.json b/{{cookiecutter.project_name}}/pyrightconfig.json index cd288a4..daf74b1 100644 --- a/{{cookiecutter.project_name}}/pyrightconfig.json +++ b/{{cookiecutter.project_name}}/pyrightconfig.json @@ -21,14 +21,13 @@ "venv": ".venv", "executionEnvironments": [ { - "root": "src", - "extraPaths": [] - }, - { - "root": "tests", + "root": ".", "extraPaths": [] } ], + "defineConstant": { + "DEBUG": true + }, "reportMissingImports": true, "reportMissingTypeStubs": true,