Repository: pypa/hatch Branch: master Commit: b998d2b755bc Files: 499 Total size: 2.6 MB Directory structure: gitextract_qo3g_3ii/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── 1-hatch_bug_report.yml │ │ └── 2-feature_request.yml │ ├── dependabot.yml │ └── workflows/ │ ├── auto-merge.yml │ ├── build-distributions.yml │ ├── build-hatch.yml │ ├── build-hatchling.yml │ ├── cli.yml │ ├── docs-dev.yml │ ├── docs-release.yml │ └── test.yml ├── .gitignore ├── .linkcheckerrc ├── LICENSE.txt ├── README.md ├── backend/ │ ├── LICENSE.txt │ ├── README.md │ ├── pyproject.toml │ ├── src/ │ │ └── hatchling/ │ │ ├── __about__.py │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── bridge/ │ │ │ ├── __init__.py │ │ │ └── app.py │ │ ├── build.py │ │ ├── builders/ │ │ │ ├── __init__.py │ │ │ ├── app.py │ │ │ ├── binary.py │ │ │ ├── config.py │ │ │ ├── constants.py │ │ │ ├── custom.py │ │ │ ├── hooks/ │ │ │ │ ├── __init__.py │ │ │ │ ├── custom.py │ │ │ │ ├── plugin/ │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── hooks.py │ │ │ │ │ └── interface.py │ │ │ │ └── version.py │ │ │ ├── macos.py │ │ │ ├── plugin/ │ │ │ │ ├── __init__.py │ │ │ │ ├── hooks.py │ │ │ │ └── interface.py │ │ │ ├── sdist.py │ │ │ ├── utils.py │ │ │ └── wheel.py │ │ ├── cli/ │ │ │ ├── __init__.py │ │ │ ├── build/ │ │ │ │ └── __init__.py │ │ │ ├── dep/ │ │ │ │ ├── __init__.py │ │ │ │ └── core.py │ │ │ ├── metadata/ │ │ │ │ └── __init__.py │ │ │ └── version/ │ │ │ └── __init__.py │ │ ├── dep/ │ │ │ ├── __init__.py │ │ │ └── core.py │ │ ├── licenses/ │ │ │ ├── __init__.py │ │ │ └── supported.py │ │ ├── metadata/ │ │ │ ├── __init__.py │ │ │ ├── core.py │ │ │ ├── custom.py │ │ │ ├── plugin/ │ │ │ │ ├── __init__.py │ │ │ │ ├── hooks.py │ │ │ │ └── interface.py │ │ │ ├── spec.py │ │ │ └── utils.py │ │ ├── ouroboros.py │ │ ├── plugin/ │ │ │ ├── __init__.py │ │ │ ├── exceptions.py │ │ │ ├── manager.py │ │ │ ├── specs.py │ │ │ └── utils.py │ │ ├── py.typed │ │ ├── utils/ │ │ │ ├── __init__.py │ │ │ ├── constants.py │ │ │ ├── context.py │ │ │ └── fs.py │ │ └── version/ │ │ ├── __init__.py │ │ ├── core.py │ │ ├── scheme/ │ │ │ ├── __init__.py │ │ │ ├── plugin/ │ │ │ │ ├── __init__.py │ │ │ │ ├── hooks.py │ │ │ │ └── interface.py │ │ │ └── standard.py │ │ └── source/ │ │ ├── __init__.py │ │ ├── code.py │ │ ├── env.py │ │ ├── plugin/ │ │ │ ├── __init__.py │ │ │ ├── hooks.py │ │ │ └── interface.py │ │ └── regex.py │ └── tests/ │ ├── __init__.py │ └── downstream/ │ ├── datadogpy/ │ │ ├── data.json │ │ └── pyproject.toml │ ├── hatch-showcase/ │ │ └── data.json │ ├── integrate.py │ └── requirements.txt ├── docs/ │ ├── .hooks/ │ │ ├── expand_blocks.py │ │ ├── inject_version.py │ │ ├── plugin_register.py │ │ ├── render_default_test_env.py │ │ ├── render_ruff_defaults.py │ │ └── title_from_content.py │ ├── .overrides/ │ │ └── partials/ │ │ └── copyright.html │ ├── .snippets/ │ │ ├── abbrs.txt │ │ └── links.txt │ ├── assets/ │ │ ├── badge/ │ │ │ └── v0.json │ │ └── css/ │ │ └── custom.css │ ├── blog/ │ │ ├── .authors.yml │ │ ├── index.md │ │ └── posts/ │ │ ├── release-hatch-1100.md │ │ ├── release-hatch-1160.md │ │ ├── release-hatch-160.md │ │ ├── release-hatch-180.md │ │ └── release-hatch-190.md │ ├── build.md │ ├── cli/ │ │ ├── about.md │ │ └── reference.md │ ├── community/ │ │ ├── contributing.md │ │ ├── highlights.md │ │ └── users.md │ ├── config/ │ │ ├── build.md │ │ ├── context.md │ │ ├── dependency.md │ │ ├── environment/ │ │ │ ├── advanced.md │ │ │ └── overview.md │ │ ├── hatch.md │ │ ├── internal/ │ │ │ ├── build.md │ │ │ ├── static-analysis.md │ │ │ └── testing.md │ │ ├── metadata.md │ │ └── project-templates.md │ ├── environment.md │ ├── history/ │ │ ├── hatch.md │ │ └── hatchling.md │ ├── how-to/ │ │ ├── config/ │ │ │ └── dynamic-metadata.md │ │ ├── environment/ │ │ │ ├── dependency-resolution.md │ │ │ ├── select-installer.md │ │ │ └── workspace.md │ │ ├── integrate/ │ │ │ └── vscode.md │ │ ├── meta/ │ │ │ └── report-issues.md │ │ ├── plugins/ │ │ │ └── testing-builds.md │ │ ├── publish/ │ │ │ ├── auth.md │ │ │ └── repo.md │ │ ├── python/ │ │ │ └── custom.md │ │ ├── run/ │ │ │ └── python-scripts.md │ │ └── static-analysis/ │ │ └── behavior.md │ ├── index.md │ ├── install.md │ ├── intro.md │ ├── meta/ │ │ ├── authors.md │ │ └── faq.md │ ├── next-steps.md │ ├── plugins/ │ │ ├── about.md │ │ ├── build-hook/ │ │ │ ├── custom.md │ │ │ ├── reference.md │ │ │ └── version.md │ │ ├── builder/ │ │ │ ├── binary.md │ │ │ ├── custom.md │ │ │ ├── reference.md │ │ │ ├── sdist.md │ │ │ └── wheel.md │ │ ├── environment/ │ │ │ ├── reference.md │ │ │ └── virtual.md │ │ ├── environment-collector/ │ │ │ ├── custom.md │ │ │ ├── default.md │ │ │ └── reference.md │ │ ├── metadata-hook/ │ │ │ ├── custom.md │ │ │ └── reference.md │ │ ├── publisher/ │ │ │ ├── package-index.md │ │ │ └── reference.md │ │ ├── utilities.md │ │ ├── version-scheme/ │ │ │ ├── reference.md │ │ │ └── standard.md │ │ └── version-source/ │ │ ├── code.md │ │ ├── env.md │ │ ├── reference.md │ │ └── regex.md │ ├── publish.md │ ├── tutorials/ │ │ ├── environment/ │ │ │ └── basic-usage.md │ │ ├── python/ │ │ │ └── manage.md │ │ └── testing/ │ │ └── overview.md │ ├── version.md │ └── why.md ├── hatch.toml ├── mkdocs.insiders.yml ├── mkdocs.yml ├── pyoxidizer.bzl ├── pyproject.toml ├── release/ │ ├── README.md │ ├── macos/ │ │ ├── build_pkg.py │ │ └── pkg/ │ │ └── distribution.xml │ ├── unix/ │ │ └── make_scripts_portable.py │ └── windows/ │ └── make_scripts_portable.py ├── ruff.toml ├── ruff_defaults.toml ├── scripts/ │ ├── bump.py │ ├── generate_coverage_summary.py │ ├── install_mkdocs_material_insiders.py │ ├── release_github.py │ ├── set_release_version.py │ ├── update_distributions.py │ ├── update_ruff.py │ ├── utils.py │ ├── validate_history.py │ └── write_coverage_summary_report.py ├── src/ │ └── hatch/ │ ├── __init__.py │ ├── __main__.py │ ├── cli/ │ │ ├── __init__.py │ │ ├── application.py │ │ ├── build/ │ │ │ └── __init__.py │ │ ├── clean/ │ │ │ └── __init__.py │ │ ├── config/ │ │ │ └── __init__.py │ │ ├── dep/ │ │ │ └── __init__.py │ │ ├── env/ │ │ │ ├── __init__.py │ │ │ ├── create.py │ │ │ ├── find.py │ │ │ ├── prune.py │ │ │ ├── remove.py │ │ │ ├── run.py │ │ │ └── show.py │ │ ├── fmt/ │ │ │ ├── __init__.py │ │ │ └── core.py │ │ ├── new/ │ │ │ ├── __init__.py │ │ │ └── migrate.py │ │ ├── project/ │ │ │ ├── __init__.py │ │ │ └── metadata.py │ │ ├── publish/ │ │ │ └── __init__.py │ │ ├── python/ │ │ │ ├── __init__.py │ │ │ ├── find.py │ │ │ ├── install.py │ │ │ ├── remove.py │ │ │ ├── show.py │ │ │ └── update.py │ │ ├── run/ │ │ │ └── __init__.py │ │ ├── self/ │ │ │ ├── __init__.py │ │ │ ├── report.py │ │ │ ├── restore.py │ │ │ └── update.py │ │ ├── shell/ │ │ │ └── __init__.py │ │ ├── status/ │ │ │ └── __init__.py │ │ ├── terminal.py │ │ ├── test/ │ │ │ ├── __init__.py │ │ │ └── core.py │ │ └── version/ │ │ └── __init__.py │ ├── config/ │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── model.py │ │ ├── user.py │ │ └── utils.py │ ├── dep/ │ │ ├── __init__.py │ │ ├── core.py │ │ └── sync.py │ ├── env/ │ │ ├── __init__.py │ │ ├── collectors/ │ │ │ ├── __init__.py │ │ │ ├── custom.py │ │ │ ├── default.py │ │ │ └── plugin/ │ │ │ ├── __init__.py │ │ │ ├── hooks.py │ │ │ └── interface.py │ │ ├── context.py │ │ ├── internal/ │ │ │ ├── __init__.py │ │ │ ├── build.py │ │ │ ├── static_analysis.py │ │ │ ├── test.py │ │ │ └── uv.py │ │ ├── plugin/ │ │ │ ├── __init__.py │ │ │ ├── hooks.py │ │ │ └── interface.py │ │ ├── system.py │ │ ├── utils.py │ │ └── virtual.py │ ├── errors/ │ │ └── __init__.py │ ├── index/ │ │ ├── __init__.py │ │ ├── core.py │ │ ├── errors.py │ │ └── publish.py │ ├── plugin/ │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── manager.py │ │ ├── specs.py │ │ └── utils.py │ ├── project/ │ │ ├── __init__.py │ │ ├── config.py │ │ ├── constants.py │ │ ├── core.py │ │ ├── env.py │ │ ├── frontend/ │ │ │ ├── __init__.py │ │ │ ├── core.py │ │ │ └── scripts/ │ │ │ ├── __init__.py │ │ │ ├── build_deps.py │ │ │ ├── core_metadata.py │ │ │ └── standard.py │ │ └── utils.py │ ├── publish/ │ │ ├── __init__.py │ │ ├── auth.py │ │ ├── index.py │ │ └── plugin/ │ │ ├── __init__.py │ │ ├── hooks.py │ │ └── interface.py │ ├── py.typed │ ├── python/ │ │ ├── __init__.py │ │ ├── core.py │ │ ├── distributions.py │ │ └── resolve.py │ ├── template/ │ │ ├── __init__.py │ │ ├── default.py │ │ ├── files_default.py │ │ ├── files_feature_ci.py │ │ ├── files_feature_cli.py │ │ ├── files_feature_tests.py │ │ └── plugin/ │ │ ├── __init__.py │ │ ├── hooks.py │ │ └── interface.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── ci.py │ │ ├── dep.py │ │ ├── env.py │ │ ├── fs.py │ │ ├── metadata.py │ │ ├── network.py │ │ ├── platform.py │ │ ├── runner.py │ │ ├── shells.py │ │ ├── structures.py │ │ └── toml.py │ └── venv/ │ ├── __init__.py │ ├── core.py │ └── utils.py └── tests/ ├── __init__.py ├── backend/ │ ├── __init__.py │ ├── builders/ │ │ ├── __init__.py │ │ ├── hooks/ │ │ │ ├── __init__.py │ │ │ ├── test_custom.py │ │ │ └── test_version.py │ │ ├── plugin/ │ │ │ ├── __init__.py │ │ │ └── test_interface.py │ │ ├── test_binary.py │ │ ├── test_config.py │ │ ├── test_custom.py │ │ ├── test_sdist.py │ │ ├── test_wheel.py │ │ └── utils.py │ ├── metadata/ │ │ ├── __init__.py │ │ ├── test_build.py │ │ ├── test_core.py │ │ ├── test_custom_hook.py │ │ ├── test_hatch.py │ │ └── test_spec.py │ ├── test_build.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── test_context.py │ │ ├── test_fs.py │ │ └── test_macos.py │ └── version/ │ ├── __init__.py │ ├── scheme/ │ │ ├── __init__.py │ │ └── test_standard.py │ └── source/ │ ├── __init__.py │ ├── test_code.py │ ├── test_env.py │ └── test_regex.py ├── cli/ │ ├── __init__.py │ ├── build/ │ │ ├── __init__.py │ │ └── test_build.py │ ├── clean/ │ │ ├── __init__.py │ │ └── test_clean.py │ ├── config/ │ │ ├── __init__.py │ │ ├── test_explore.py │ │ ├── test_find.py │ │ ├── test_restore.py │ │ ├── test_set.py │ │ └── test_show.py │ ├── dep/ │ │ ├── __init__.py │ │ ├── show/ │ │ │ ├── __init__.py │ │ │ ├── test_requirements.py │ │ │ └── test_table.py │ │ └── test_hash.py │ ├── env/ │ │ ├── __init__.py │ │ ├── test_create.py │ │ ├── test_find.py │ │ ├── test_prune.py │ │ ├── test_remove.py │ │ ├── test_run.py │ │ └── test_show.py │ ├── fmt/ │ │ ├── __init__.py │ │ └── test_fmt.py │ ├── new/ │ │ ├── __init__.py │ │ └── test_new.py │ ├── project/ │ │ ├── __init__.py │ │ └── test_metadata.py │ ├── publish/ │ │ ├── __init__.py │ │ └── test_publish.py │ ├── python/ │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_find.py │ │ ├── test_install.py │ │ ├── test_remove.py │ │ ├── test_show.py │ │ └── test_update.py │ ├── run/ │ │ ├── __init__.py │ │ └── test_run.py │ ├── self/ │ │ ├── __init__.py │ │ ├── test_report.py │ │ └── test_self.py │ ├── status/ │ │ ├── __init__.py │ │ └── test_status.py │ ├── test/ │ │ ├── __init__.py │ │ └── test_test.py │ ├── test_root.py │ └── version/ │ ├── __init__.py │ └── test_version.py ├── config/ │ ├── __init__.py │ └── test_model.py ├── conftest.py ├── dep/ │ ├── __init__.py │ └── test_sync.py ├── env/ │ ├── __init__.py │ ├── collectors/ │ │ ├── __init__.py │ │ └── test_custom.py │ └── plugin/ │ ├── __init__.py │ └── test_interface.py ├── helpers/ │ ├── __init__.py │ ├── helpers.py │ └── templates/ │ ├── __init__.py │ ├── licenses/ │ │ └── __init__.py │ ├── new/ │ │ ├── __init__.py │ │ ├── basic.py │ │ ├── default.py │ │ ├── feature_ci.py │ │ ├── feature_cli.py │ │ ├── feature_no_src_layout.py │ │ ├── licenses_empty.py │ │ ├── licenses_multiple.py │ │ ├── projects_urls_empty.py │ │ └── projects_urls_space_in_label.py │ ├── sdist/ │ │ ├── __init__.py │ │ ├── standard_default.py │ │ ├── standard_default_build_script_artifacts.py │ │ ├── standard_default_build_script_extra_dependencies.py │ │ ├── standard_default_support_legacy.py │ │ ├── standard_default_vcs_git_exclusion_files.py │ │ ├── standard_default_vcs_mercurial_exclusion_files.py │ │ ├── standard_include.py │ │ └── standard_include_config_file.py │ └── wheel/ │ ├── __init__.py │ ├── standard_default_build_script.py │ ├── standard_default_build_script_artifacts.py │ ├── standard_default_build_script_artifacts_with_src_layout.py │ ├── standard_default_build_script_configured_build_hooks.py │ ├── standard_default_build_script_extra_dependencies.py │ ├── standard_default_build_script_force_include.py │ ├── standard_default_build_script_force_include_no_duplication.py │ ├── standard_default_extra_metadata.py │ ├── standard_default_license_multiple.py │ ├── standard_default_license_single.py │ ├── standard_default_namespace_package.py │ ├── standard_default_python_constraint.py │ ├── standard_default_python_constraint_three_components.py │ ├── standard_default_sbom.py │ ├── standard_default_shared_data.py │ ├── standard_default_shared_scripts.py │ ├── standard_default_single_module.py │ ├── standard_default_symlink.py │ ├── standard_editable_exact.py │ ├── standard_editable_exact_extra_dependencies.py │ ├── standard_editable_exact_force_include.py │ ├── standard_editable_pth.py │ ├── standard_editable_pth_extra_dependencies.py │ ├── standard_editable_pth_force_include.py │ ├── standard_entry_points.py │ ├── standard_no_strict_naming.py │ ├── standard_only_packages_artifact_override.py │ ├── standard_tests.py │ └── utils.py ├── index/ │ ├── __init__.py │ ├── server/ │ │ ├── devpi/ │ │ │ ├── Dockerfile │ │ │ └── entrypoint.sh │ │ ├── docker-compose.yaml │ │ └── nginx/ │ │ └── nginx.conf │ └── test_core.py ├── project/ │ ├── __init__.py │ ├── test_config.py │ ├── test_core.py │ ├── test_frontend.py │ └── test_utils.py ├── publish/ │ ├── __init__.py │ └── plugin/ │ ├── __init__.py │ └── test_interface.py ├── python/ │ ├── __init__.py │ ├── test_core.py │ └── test_resolve.py ├── utils/ │ ├── __init__.py │ ├── test_auth.py │ ├── test_fs.py │ ├── test_platform.py │ ├── test_runner.py │ └── test_structures.py ├── venv/ │ ├── __init__.py │ ├── test_core.py │ └── test_utils.py └── workspaces/ ├── __init__.py └── test_config.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/FUNDING.yml ================================================ github: - ofek custom: - https://ofek.dev/donate/ ================================================ FILE: .github/ISSUE_TEMPLATE/1-hatch_bug_report.yml ================================================ --- name: Hatch Bug report description: Problems and issues with code in Hatch body: - type: markdown attributes: # yamllint disable rule:line-length value: " Thank you for finding the time to report the problem, we ask that you use the command `hatch self report` to report bugs instead of using this form.
" # yamllint enable rule:line-length ================================================ FILE: .github/ISSUE_TEMPLATE/2-feature_request.yml ================================================ --- name: Hatch feature request description: Suggest an idea for this project labels: ["kind:feature"] body: - type: markdown attributes: # yamllint disable rule:line-length value: | Thank you for finding the time to propose new feature! We really appreciate the community efforts to improve Hatch. Please keep feature requests to smaller incremental changes that do not make changes to the assumptions about hatch. If unsure - open a [discussion](https://github.com/pypa/hatch/discussions) first to gather an initial feedback on your idea.
# yamllint enable rule:line-length - type: textarea attributes: label: Description description: A short description of your feature - type: textarea attributes: label: Use case/motivation description: What would you like to happen? placeholder: > Rather than telling us how you might implement this feature, try to take a step back and describe what you are trying to achieve. - type: textarea attributes: label: Related issues description: Is there currently another issue associated with this? - type: checkboxes attributes: label: Are you willing to submit a PR? description: > If want to submit a PR you do not need to open feature request, just create the PR!. Especially if you already have a good understanding of how to implement the feature. Hatch is a PyPA managed project but we love to bring new contributors in. Find us on the PyPA Discord Server under #hatch It's optional though - if you have good idea for small feature, others might implement it if they pick an interest in it, so feel free to leave that checkbox unchecked. options: - label: Yes I am willing to submit a PR! - type: checkboxes attributes: label: Code of Conduct description: The Code of Conduct helps create a safe space for everyone. We require that everyone agrees to it. options: - label: > I agree to follow the Python Software Foundation's [Code of Conduct](https://policies.python.org/python.org/code-of-conduct/) required: true - type: markdown attributes: value: Thanks for completing our form! ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: monthly ================================================ FILE: .github/workflows/auto-merge.yml ================================================ name: auto-merge on: pull_request_target: types: - opened - reopened - synchronize branches: - master jobs: dependabot: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - name: Wait for tests to succeed uses: lewagon/wait-on-check-action@v1.3.4 with: ref: ${{ github.ref }} check-name: check wait-interval: 10 repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Enable auto-merge for Dependabot PRs run: gh pr merge --auto --squash ${{ github.event.pull_request.html_url }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/build-distributions.yml ================================================ name: build distributions on: workflow_call: inputs: version: required: false type: string defaults: run: shell: bash env: DIST_URL: "https://github.com/indygreg/python-build-standalone/releases/download" DIST_VERSION: "20240415" DIST_PYTHON_VERSION: "3.12.3" PYTHONDONTWRITEBYTECODE: "1" PIP_ONLY_BINARY: ":all:" # Some pip environment variables are weird, this means do not compile PIP_NO_COMPILE: "0" jobs: ensure-installable: name: Ensure Hatch is installable runs-on: ubuntu-22.04 steps: - name: Set up Python ${{ env.DIST_PYTHON_VERSION }} uses: actions/setup-python@v5 with: python-version: ${{ env.DIST_PYTHON_VERSION }} - name: Install UV uses: astral-sh/setup-uv@v3 - name: Install Hatch if: inputs.version # Try to install the specific version of Hatch that was just released until successful run: |- for i in {1..20}; do uv pip install --system hatch==${{ inputs.version }} && break || sleep 5 done linux: name: Distribution ${{ matrix.job.target }} needs: ensure-installable runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: job: - target: x86_64-unknown-linux-gnu image: quay.io/pypa/manylinux2014_x86_64 target-override: x86_64_v3-unknown-linux-gnu - target: aarch64-unknown-linux-gnu image: quay.io/pypa/manylinux_2_28_aarch64 emulation: arm64 steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: ${{ inputs.version && 1 || 0 }} - name: Set up QEMU if: matrix.job.emulation uses: docker/setup-qemu-action@v3 - name: Set up Docker container run: >- docker run --rm -d --name builder --workdir /home --env PYTHONDONTWRITEBYTECODE --env PIP_ONLY_BINARY --env PIP_NO_COMPILE --volume ${{ github.workspace }}:/home/hatch ${{ matrix.job.image }} sleep infinity - name: Download distribution run: >- docker exec builder curl -LO ${{ env.DIST_URL }}/${{ env.DIST_VERSION }}/cpython-${{ env.DIST_PYTHON_VERSION }}+${{ env.DIST_VERSION }}-${{ matrix.job.target-override || matrix.job.target }}-install_only.tar.gz - name: Unpack distribution run: >- docker exec builder tar xzf cpython-${{ env.DIST_PYTHON_VERSION }}+${{ env.DIST_VERSION }}-${{ matrix.job.target-override || matrix.job.target }}-install_only.tar.gz - name: Install Hatch run: >- docker exec builder /home/python/bin/python -m pip install ${{ inputs.version && format('hatch=={0}', inputs.version) || '/home/hatch' }} - name: Make scripts portable run: >- docker exec builder /home/python/bin/python /home/hatch/release/unix/make_scripts_portable.py - name: Strip debug symbols run: >- docker exec builder sh -c "find /home/python -name '*.so' | xargs strip -S" - name: Archive distribution run: >- docker exec builder tar czf hatch-dist-${{ matrix.job.target }}.tar.gz python - name: Move to host run: docker cp builder:/home/hatch-dist-${{ matrix.job.target }}.tar.gz . - name: Check original size run: >- docker exec builder ls -lh cpython-${{ env.DIST_PYTHON_VERSION }}+${{ env.DIST_VERSION }}-${{ matrix.job.target-override || matrix.job.target }}-install_only.tar.gz - name: Check final size run: ls -lh hatch-dist-${{ matrix.job.target }}.tar.gz - name: Upload archive uses: actions/upload-artifact@v4 with: name: distribution-${{ matrix.job.target }} path: hatch-dist-${{ matrix.job.target }}.tar.gz windows-macos: name: Distribution ${{ matrix.job.target }} needs: ensure-installable runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: - target: x86_64-pc-windows-msvc os: windows-2022 - target: aarch64-apple-darwin os: macos-14 - target: x86_64-apple-darwin os: macos-14 steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: ${{ inputs.version && 1 || 0 }} - name: Download distribution run: curl -LO ${{ env.DIST_URL }}/${{ env.DIST_VERSION }}/cpython-${{ env.DIST_PYTHON_VERSION }}+${{ env.DIST_VERSION }}-${{ matrix.job.target }}-install_only.tar.gz - name: Unpack distribution run: tar xzf cpython-${{ env.DIST_PYTHON_VERSION }}+${{ env.DIST_VERSION }}-${{ matrix.job.target }}-install_only.tar.gz - name: Install Hatch run: >- ${{ startsWith(matrix.job.os, 'windows-') && '.\\python\\python.exe' || './python/bin/python' }} -m pip install ${{ inputs.version && format('hatch=={0}', inputs.version) || '.' }} - name: Make scripts portable run: >- ${{ startsWith(matrix.job.os, 'windows-') && '.\\python\\python.exe' || './python/bin/python' }} release/${{ startsWith(matrix.job.os, 'windows-') && 'windows' || 'unix' }}/make_scripts_portable.py - name: Strip debug symbols if: startsWith(matrix.job.os, 'macos-') run: find python -name '*.so' | xargs strip -S - name: Remove debug symbols if: startsWith(matrix.job.os, 'windows-') run: Get-ChildItem -Path python -Filter "*.pdb" -Recurse | Remove-Item shell: pwsh - name: Archive distribution run: tar czf hatch-dist-${{ matrix.job.target }}.tar.gz python - name: Check original size run: ls -lh cpython-${{ env.DIST_PYTHON_VERSION }}+${{ env.DIST_VERSION }}-${{ matrix.job.target }}-install_only.tar.gz - name: Check final size run: ls -lh hatch-dist-${{ matrix.job.target }}.tar.gz - name: Upload archive uses: actions/upload-artifact@v4 with: name: distribution-${{ matrix.job.target }} path: hatch-dist-${{ matrix.job.target }}.tar.gz ================================================ FILE: .github/workflows/build-hatch.yml ================================================ name: build hatch on: push: tags: - hatch-v* branches: - master pull_request: branches: - master concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true defaults: run: shell: bash env: APP_NAME: hatch PYTHON_VERSION: "3.12" PYOXIDIZER_VERSION: "0.24.0" DIST_URL: "https://github.com/pypa/hatch/releases/download" jobs: python-artifacts: name: Build wheel and source distribution runs-on: ubuntu-latest outputs: old-version: ${{ steps.version.outputs.old-version }} version: ${{ steps.version.outputs.version }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV uses: astral-sh/setup-uv@v3 - name: Install tools run: |- uv pip install --system build uv pip install --system . hatch env create # Windows installers don't accept non-integer versions so we ubiquitously # perform the following transformation: X.Y.Z.devN -> X.Y.Z.N - name: Set project version id: version run: |- old_version="$(hatch version)" version="${old_version/dev/}" echo "old-version=$old_version" >> $GITHUB_OUTPUT echo "version=$version" >> $GITHUB_OUTPUT echo "$version" - name: Build run: python -m build - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: python-artifacts path: dist/* if-no-files-found: error publish-pypi: name: Publish to PyPI if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') needs: python-artifacts runs-on: ubuntu-latest permissions: id-token: write steps: - name: Download Python artifacts uses: actions/download-artifact@v4 with: name: python-artifacts path: dist - name: Push Python artifacts to PyPI uses: pypa/gh-action-pypi-publish@v1.12.3 with: skip-existing: true binaries: name: Binary ${{ matrix.job.target }} (${{ matrix.job.os }}) needs: - python-artifacts runs-on: ${{ matrix.job.os }} strategy: fail-fast: false matrix: job: # Linux - target: aarch64-unknown-linux-gnu os: ubuntu-22.04 use-dist: true cross: true - target: x86_64-unknown-linux-gnu os: ubuntu-22.04 use-dist: true cross: true - target: x86_64-unknown-linux-musl os: ubuntu-22.04 cross: true - target: powerpc64le-unknown-linux-gnu os: ubuntu-22.04 cross: true # Windows - target: x86_64-pc-windows-msvc os: windows-2022 use-dist: true - target: i686-pc-windows-msvc os: windows-2022 # macOS - target: aarch64-apple-darwin os: macos-14 use-dist: true - target: x86_64-apple-darwin os: macos-14 use-dist: true env: CARGO: cargo CARGO_BUILD_TARGET: ${{ matrix.job.target }} PYAPP_REPO: pyapp PYAPP_VERSION: "0.22.0" PYAPP_UV_ENABLED: "true" PYAPP_PASS_LOCATION: "true" steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Fetch PyApp run: >- mkdir $PYAPP_REPO && curl -L https://github.com/ofek/pyapp/releases/download/v$PYAPP_VERSION/source.tar.gz | tar --strip-components=1 -xzf - -C $PYAPP_REPO - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV uses: astral-sh/setup-uv@v3 - name: Install Hatch run: |- uv pip install --system -e . hatch env create - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.job.target }} - name: Set up cross compiling if: matrix.job.cross uses: taiki-e/install-action@v2 with: tool: cross - name: Configure cross compiling if: matrix.job.cross run: echo "CARGO=cross" >> $GITHUB_ENV - name: Configure target run: |- config_file="$PYAPP_REPO/.cargo/config_${{ matrix.job.target }}.toml" if [[ -f "$config_file" ]]; then mv "$config_file" "$PYAPP_REPO/.cargo/config.toml" fi - name: Download Python artifacts if: ${{ !startsWith(github.event.ref, 'refs/tags') }} uses: actions/download-artifact@v4 with: name: python-artifacts path: dist - name: Configure embedded project if: ${{ !startsWith(github.event.ref, 'refs/tags') }} run: |- cd dist wheel="$(echo *.whl)" mv "$wheel" "../$PYAPP_REPO" echo "PYAPP_PROJECT_PATH=$wheel" >> $GITHUB_ENV - name: Configure release with distribution if: startsWith(github.event.ref, 'refs/tags') && matrix.job.use-dist run: |- echo "PYAPP_SKIP_INSTALL=true" >> $GITHUB_ENV echo "PYAPP_FULL_ISOLATION=true" >> $GITHUB_ENV echo "PYAPP_DISTRIBUTION_SOURCE=${{ env.DIST_URL }}/hatch-v${{ needs.python-artifacts.outputs.version }}/hatch-dist-${{ matrix.job.target }}.tar.gz" >> $GITHUB_ENV echo "PYAPP_DISTRIBUTION_PATH_PREFIX=python" >> $GITHUB_ENV echo "PYAPP_ALLOW_UPDATES=true" >> $GITHUB_ENV # Disable in the case of self updates echo "PYAPP_UV_ENABLED=false" >> $GITHUB_ENV - name: Build binary run: hatch build --target binary - name: Correct binary version run: |- old_version="${{ needs.python-artifacts.outputs.old-version }}" version="${{ needs.python-artifacts.outputs.version }}" if [[ "$version" != "$old_version" ]]; then cd dist/binary old_binary="$(ls)" binary="${old_binary/$old_version/$version}" mv "$old_binary" "$binary" fi - name: Archive binary run: |- mkdir packaging cd dist/binary old_binary="$(ls)" if [[ "$old_binary" =~ -pc-windows- ]]; then new_binary="${{ env.APP_NAME }}.exe" mv "$old_binary" "$new_binary" 7z a "../../packaging/${{ env.APP_NAME }}-${{ matrix.job.target }}.zip" "$new_binary" else new_binary="${{ env.APP_NAME }}" mv "$old_binary" "$new_binary" chmod +x "$new_binary" tar -czf "../../packaging/${{ env.APP_NAME }}-${{ matrix.job.target }}.tar.gz" "$new_binary" fi - name: Upload staged archive if: runner.os != 'Linux' uses: actions/upload-artifact@v4 with: name: staged-${{ runner.os }}-${{ matrix.job.target }} path: packaging/* if-no-files-found: error - name: Upload archive if: runner.os == 'Linux' uses: actions/upload-artifact@v4 with: name: standalone-${{ matrix.job.target }} path: packaging/* if-no-files-found: error windows-packaging: name: Build Windows installers if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository needs: - binaries - python-artifacts runs-on: windows-2022 env: VERSION: ${{ needs.python-artifacts.outputs.version }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV uses: astral-sh/setup-uv@v3 - name: Install PyOxidizer ${{ env.PYOXIDIZER_VERSION }} run: uv pip install --system pyoxidizer==${{ env.PYOXIDIZER_VERSION }} - name: Download staged binaries uses: actions/download-artifact@v4 with: pattern: staged-${{ runner.os }}-* path: archives merge-multiple: true - name: Extract staged binaries run: |- mkdir bin cd archives for f in *; do binary_id=${f:0:-4} 7z e "$f" -o../bin mv "../bin/${{ env.APP_NAME }}.exe" "../bin/$binary_id.exe" done # bin/-.exe -> targets//.exe - name: Prepare binaries run: |- mkdir targets for f in bin/*; do if [[ "$f" =~ ${{ env.APP_NAME }}-(.+).exe$ ]]; then target="${BASH_REMATCH[1]}" mkdir "targets/$target" mv "$f" "targets/$target/${{ env.APP_NAME }}.exe" fi done - name: Build installers run: >- pyoxidizer build windows_installers --release --var version ${{ env.VERSION }} - name: Prepare installers run: |- mkdir installers mv build/*/release/*/*.{exe,msi} installers cd installers universal_installer="$(ls *.exe)" mv "$universal_installer" "${{ env.APP_NAME }}-universal.exe" - name: Upload binaries uses: actions/upload-artifact@v4 with: name: standalone-${{ runner.os }} path: archives/* if-no-files-found: error - name: Upload installers uses: actions/upload-artifact@v4 with: name: installers-${{ runner.os }} path: installers/* if-no-files-found: error macos-packaging: name: Build macOS installer and sign/notarize artifacts if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository needs: - binaries - python-artifacts runs-on: macos-14 env: VERSION: ${{ needs.python-artifacts.outputs.version }} NOTARY_WAIT_TIME: "3600" # 1 hour steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV uses: astral-sh/setup-uv@v3 - name: Install PyOxidizer ${{ env.PYOXIDIZER_VERSION }} run: uv pip install --system pyoxidizer==${{ env.PYOXIDIZER_VERSION }} - name: Install rcodesign env: ARCHIVE_NAME: "apple-codesign-0.27.0-x86_64-apple-darwin" run: >- curl -L "https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.27.0/$ARCHIVE_NAME.tar.gz" | tar --strip-components=1 -xzf - -C /usr/local/bin "$ARCHIVE_NAME/rcodesign" - name: Download staged binaries uses: actions/download-artifact@v4 with: pattern: staged-${{ runner.os }}-* path: archives merge-multiple: true - name: Extract staged binaries run: |- mkdir bin cd archives for f in *; do binary_id=${f:0:${#f}-7} tar -xzf "$f" -C ../bin mv "../bin/${{ env.APP_NAME }}" "../bin/$binary_id" done - name: Write credentials env: APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE: "${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE }}" APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY: "${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY }}" APPLE_DEVELOPER_ID_INSTALLER_CERTIFICATE: "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERTIFICATE }}" APPLE_DEVELOPER_ID_INSTALLER_PRIVATE_KEY: "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_PRIVATE_KEY }}" APPLE_APP_STORE_CONNECT_API_DATA: "${{ secrets.APPLE_APP_STORE_CONNECT_API_DATA }}" run: |- echo "$APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE" > /tmp/certificate-application.pem echo "$APPLE_DEVELOPER_ID_APPLICATION_PRIVATE_KEY" > /tmp/private-key-application.pem echo "$APPLE_DEVELOPER_ID_INSTALLER_CERTIFICATE" > /tmp/certificate-installer.pem echo "$APPLE_DEVELOPER_ID_INSTALLER_PRIVATE_KEY" > /tmp/private-key-installer.pem echo "$APPLE_APP_STORE_CONNECT_API_DATA" > /tmp/app-store-connect.json # https://developer.apple.com/documentation/security/hardened_runtime - name: Sign binaries run: |- for f in bin/*; do rcodesign sign -vv \ --pem-source /tmp/certificate-application.pem \ --pem-source /tmp/private-key-application.pem \ --code-signature-flags runtime \ "$f" done # https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution - name: Notarize binaries run: |- mkdir notarize-bin cd bin for f in *; do zip "../notarize-bin/$f.zip" "$f" done cd ../notarize-bin for f in *; do rcodesign notary-submit -vv \ --max-wait-seconds ${{ env.NOTARY_WAIT_TIME }} \ --api-key-path /tmp/app-store-connect.json \ "$f" done - name: Archive binaries run: |- rm archives/* cd bin for f in *; do mv "$f" "${{ env.APP_NAME }}" tar -czf "../archives/$f.tar.gz" "${{ env.APP_NAME }}" mv "${{ env.APP_NAME }}" "$f" done # bin/- -> targets// - name: Prepare binaries run: |- mkdir targets for f in bin/*; do if [[ "$f" =~ ${{ env.APP_NAME }}-(.+)$ ]]; then target="${BASH_REMATCH[1]}" mkdir "targets/$target" mv "$f" "targets/$target/${{ env.APP_NAME }}" fi done - name: Build universal binary run: >- pyoxidizer build macos_universal_binary --release --var version ${{ env.VERSION }} - name: Prepare universal binary id: binary run: |- binary=$(echo build/*/release/*/${{ env.APP_NAME }}) chmod +x "$binary" echo "path=$binary" >> "$GITHUB_OUTPUT" - name: Build PKG run: >- python release/macos/build_pkg.py --binary ${{ steps.binary.outputs.path }} --version ${{ env.VERSION }} staged - name: Stage PKG id: pkg run: |- mkdir signed pkg_file="$(ls staged)" echo "path=$pkg_file" >> "$GITHUB_OUTPUT" - name: Sign PKG run: >- rcodesign sign -vv --pem-source /tmp/certificate-installer.pem --pem-source /tmp/private-key-installer.pem "staged/${{ steps.pkg.outputs.path }}" "signed/${{ steps.pkg.outputs.path }}" - name: Notarize PKG run: >- rcodesign notary-submit -vv --max-wait-seconds ${{ env.NOTARY_WAIT_TIME }} --api-key-path /tmp/app-store-connect.json --staple "signed/${{ steps.pkg.outputs.path }}" - name: Upload binaries uses: actions/upload-artifact@v4 with: name: standalone-${{ runner.os }} path: archives/* if-no-files-found: error - name: Upload installer uses: actions/upload-artifact@v4 with: name: installers-${{ runner.os }} path: signed/${{ steps.pkg.outputs.path }} if-no-files-found: error distributions-dev: name: Build development distributions if: ${{ !startsWith(github.event.ref, 'refs/tags') }} uses: ./.github/workflows/build-distributions.yml # This actually does not need the binary jobs but we want to prioritize # resources for the test jobs therefore this forces these later on needs: binaries distributions-release: name: Build release distributions needs: - python-artifacts - publish-pypi if: startsWith(github.event.ref, 'refs/tags') uses: ./.github/workflows/build-distributions.yml with: version: ${{ needs.python-artifacts.outputs.version }} publish-release: name: Publish distributions if: startsWith(github.event.ref, 'refs/tags') needs: - binaries - windows-packaging - macos-packaging - distributions-release runs-on: ubuntu-latest permissions: contents: write id-token: write steps: - name: Download distributions uses: actions/download-artifact@v4 with: pattern: distribution-* path: distributions merge-multiple: true - name: Download binaries uses: actions/download-artifact@v4 with: pattern: standalone-* path: archives merge-multiple: true - name: Download installers uses: actions/download-artifact@v4 with: pattern: installers-* path: installers merge-multiple: true - name: Add assets to draft release uses: softprops/action-gh-release@v2 with: files: |- archives/* distributions/* installers/* ================================================ FILE: .github/workflows/build-hatchling.yml ================================================ name: build hatchling on: push: tags: - hatchling-v* env: PYTHON_VERSION: "3.12" jobs: build: name: Build wheels and source distribution runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python ${{ env.PYTHON_VERSION }} uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install UV uses: astral-sh/setup-uv@v3 - name: Install build dependencies run: uv pip install --system --upgrade build - name: Build source distribution run: python -m build backend - uses: actions/upload-artifact@v4 with: name: artifacts path: backend/dist if-no-files-found: error publish: name: Publish release needs: - build runs-on: ubuntu-latest permissions: contents: write id-token: write steps: - uses: actions/download-artifact@v4 with: name: artifacts path: dist - name: Push build artifacts to PyPI uses: pypa/gh-action-pypi-publish@v1.12.3 with: skip-existing: true - name: Add assets to draft release uses: softprops/action-gh-release@v2 with: files: dist/* ================================================ FILE: .github/workflows/cli.yml ================================================ name: CLI experience on: push: branches: - master pull_request: branches: - master concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: STABLE_PYTHON_VERSION: '3.12' HYPERFINE_VERSION: '1.18.0' jobs: response-time: name: CLI responsiveness with latest Python runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python ${{ env.STABLE_PYTHON_VERSION }} uses: actions/setup-python@v5 with: python-version: ${{ env.STABLE_PYTHON_VERSION }} - name: Install UV uses: astral-sh/setup-uv@v3 - name: Install hyperfine uses: taiki-e/install-action@v2 with: tool: hyperfine@${{ env.HYPERFINE_VERSION }} - name: Install other tools run: uv pip install --system --upgrade flit poetry pipenv - name: Install ourself run: | uv pip install --system . - name: Benchmark run: | hyperfine -m 100 --warmup 10 -i pipenv hyperfine -m 100 --warmup 10 poetry hyperfine -m 100 --warmup 10 -i flit hyperfine -m 100 --warmup 10 hatch ================================================ FILE: .github/workflows/docs-dev.yml ================================================ name: dev docs on: push: branches: - master pull_request: branches: - master concurrency: group: docs-deploy env: FORCE_COLOR: "1" jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # Fetch all history for applying timestamps to every page fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Validate history run: python scripts/validate_history.py - name: Install UV uses: astral-sh/setup-uv@v3 - name: Install ourself run: | uv pip install --system -e . hatch env create - name: Configure Git for GitHub Actions bot run: | git config --local user.name 'github-actions[bot]' git config --local user.email 'github-actions[bot]@users.noreply.github.com' - name: Build documentation run: hatch -v run docs:build-check env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN_MKDOCS_MATERIAL_INSIDERS: ${{ secrets.GH_TOKEN_MKDOCS_MATERIAL_INSIDERS }} - name: Commit documentation run: hatch -v run docs:ci-build dev env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN_MKDOCS_MATERIAL_INSIDERS: ${{ secrets.GH_TOKEN_MKDOCS_MATERIAL_INSIDERS }} - name: Create archive run: git archive -o site.zip gh-pages - uses: actions/upload-artifact@v4 with: name: documentation path: site.zip publish: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/master' needs: - build steps: - uses: actions/download-artifact@v4 with: name: documentation - name: Unpack archive run: python -m zipfile -e site.zip site - uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: site commit_message: ${{ github.event.head_commit.message }} # Write .nojekyll at the root, see: # https://help.github.com/en/github/working-with-github-pages/about-github-pages#static-site-generators enable_jekyll: false # Only deploy if there were changes allow_empty_commit: false ================================================ FILE: .github/workflows/docs-release.yml ================================================ name: release docs on: push: tags: - hatch-v* workflow_dispatch: concurrency: group: docs-deploy env: FORCE_COLOR: "1" jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # Fetch all history for applying timestamps to every page fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Validate history run: python scripts/validate_history.py - name: Install UV uses: astral-sh/setup-uv@v3 - name: Install ourself run: | uv pip install --system -e . hatch env create - name: Display full version run: hatch version - name: Set the version of docs to publish run: python scripts/set_release_version.py - name: Configure Git for GitHub Actions bot run: | git config --local user.name 'github-actions[bot]' git config --local user.email 'github-actions[bot]@users.noreply.github.com' - name: Build documentation run: hatch run docs:build-check env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN_MKDOCS_MATERIAL_INSIDERS: ${{ secrets.GH_TOKEN_MKDOCS_MATERIAL_INSIDERS }} - name: Commit documentation run: hatch run docs:ci-build $HATCH_DOCS_VERSION latest env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN_MKDOCS_MATERIAL_INSIDERS: ${{ secrets.GH_TOKEN_MKDOCS_MATERIAL_INSIDERS }} - name: Create archive run: git archive -o site.zip gh-pages - uses: actions/upload-artifact@v4 with: name: documentation path: site.zip publish: runs-on: ubuntu-latest needs: - build steps: - uses: actions/download-artifact@v4 with: name: documentation - name: Unpack archive run: python -m zipfile -e site.zip site - uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: site commit_message: ${{ github.event.head_commit.message }} # Write .nojekyll at the root, see: # https://help.github.com/en/github/working-with-github-pages/about-github-pages#static-site-generators enable_jekyll: false # Only deploy if there were changes allow_empty_commit: false ================================================ FILE: .github/workflows/test.yml ================================================ name: test on: push: branches: - master pull_request: branches: - master concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true env: PYTHONUNBUFFERED: "1" FORCE_COLOR: "1" jobs: run: name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install uv uses: astral-sh/setup-uv@v3 - name: Install ourself run: | uv pip install --system -e . hatch env create - name: Run static analysis run: hatch fmt --check - name: Check types run: hatch run types:check - name: Run tests run: hatch test --python ${{ matrix.python-version }} --cover-quiet --randomize --parallel --retries 5 --retry-delay 3 - name: Disambiguate coverage filename run: mv .coverage ".coverage.${{ matrix.os }}.${{ matrix.python-version }}" - name: Upload coverage data uses: actions/upload-artifact@v4 with: include-hidden-files: true name: coverage-${{ matrix.os }}-${{ matrix.python-version }} path: .coverage* coverage: name: Report coverage runs-on: ubuntu-latest needs: - run steps: - uses: actions/checkout@v4 - name: Install Hatch uses: pypa/hatch@install - name: Trigger build for auto-generated files run: hatch build --hooks-only - name: Download coverage data uses: actions/download-artifact@v4 with: pattern: coverage-* merge-multiple: true - name: Combine coverage data run: hatch run coverage:combine - name: Export coverage reports run: | hatch run coverage:report-xml hatch run coverage:report-uncovered-html - name: Upload uncovered HTML report uses: actions/upload-artifact@v4 with: name: uncovered-html-report path: htmlcov - name: Generate coverage summary run: hatch run coverage:generate-summary - name: Write coverage summary report if: github.event_name == 'pull_request' run: hatch run coverage:write-summary-report - name: Update coverage pull request comment if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork uses: marocchino/sticky-pull-request-comment@v2 with: path: coverage-report.md downstream: name: Downstream builds with Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install tools run: pip install --upgrade -r backend/tests/downstream/requirements.txt - name: Build downstream projects run: python backend/tests/downstream/integrate.py # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection if: always() needs: - coverage - downstream runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} ================================================ FILE: .gitignore ================================================ # Global directories __pycache__/ # Global files *.py[cod] *.dll *.so *.log *.swp # Root directories /.benchmarks/ /.cache/ /.env/ /.idea/ /.mypy_cache/ /.pytest_cache/ /.ruff_cache/ /.vscode/ /backend/dist/ /dist/ /site/ # Root files /.coverage* # Auto-generated during builds /src/hatch/_version.py ================================================ FILE: .linkcheckerrc ================================================ # https://linkchecker.github.io/linkchecker/man/linkcheckerrc.html [filtering] ignore= https://docs.astral.sh/ruff/rules/.+ https://github.com/pypa/hatch/releases/tag/hatch-v.+ [AnchorCheck] ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) 2017-present Ofek Lev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Hatch
Hatch logo | | | | --- | --- | | CI/CD | [![CI - Test](https://github.com/pypa/hatch/actions/workflows/test.yml/badge.svg)](https://github.com/pypa/hatch/actions/workflows/test.yml) [![CD - Build Hatch](https://github.com/pypa/hatch/actions/workflows/build-hatch.yml/badge.svg)](https://github.com/pypa/hatch/actions/workflows/build-hatch.yml) [![CD - Build Hatchling](https://github.com/pypa/hatch/actions/workflows/build-hatchling.yml/badge.svg)](https://github.com/pypa/hatch/actions/workflows/build-hatchling.yml) | | Docs | [![Docs - Release](https://github.com/pypa/hatch/actions/workflows/docs-release.yml/badge.svg)](https://github.com/pypa/hatch/actions/workflows/docs-release.yml) [![Docs - Dev](https://github.com/pypa/hatch/actions/workflows/docs-dev.yml/badge.svg)](https://github.com/pypa/hatch/actions/workflows/docs-dev.yml) | | Package | [![PyPI - Version](https://img.shields.io/pypi/v/hatch.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/hatch/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/hatch.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/hatch/) [![PyPI - Installs](https://img.shields.io/pypi/dm/hatchling.svg?color=blue&label=Installs&logo=pypi&logoColor=gold)](https://pypi.org/project/hatch/) [![Release - Downloads](https://img.shields.io/github/downloads/pypa/hatch/total?label=Downloads)](https://github.com/pypa/hatch/releases) | | Meta | [![Hatch project](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pypa/hatch/master/docs/assets/badge/v0.json)](https://github.com/pypa/hatch) [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/python/mypy) [![License - MIT](https://img.shields.io/badge/license-MIT-9400d3.svg)](https://spdx.org/licenses/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ofek?logo=GitHub%20Sponsors&style=social)](https://github.com/sponsors/ofek) |
----- Hatch is a modern, extensible Python project manager. ## Features - Standardized [build system](https://hatch.pypa.io/latest/config/build/#build-system) with reproducible builds by default - Robust [environment management](https://hatch.pypa.io/latest/environment/) with support for custom scripts and UV - Configurable [Python distribution management](https://hatch.pypa.io/latest/tutorials/python/manage/) - [Test execution](https://hatch.pypa.io/latest/tutorials/testing/overview/) with known best practices - [Static analysis](https://hatch.pypa.io/latest/config/static-analysis/) with sane defaults - Built-in Python [script runner](https://hatch.pypa.io/latest/how-to/run/python-scripts/) - Easy [publishing](https://hatch.pypa.io/latest/publish/) to PyPI or other indices - [Version](https://hatch.pypa.io/latest/version/) management - Best practice [project generation](https://hatch.pypa.io/latest/config/project-templates/) - Responsive [CLI](https://hatch.pypa.io/latest/cli/about/), ~2-3x [faster](https://github.com/pypa/hatch/actions/workflows/cli.yml) than equivalent tools See the [Why Hatch?](https://hatch.pypa.io/latest/why/) page for more information. ## Documentation The [documentation](https://hatch.pypa.io/) is made with [Material for MkDocs](https://github.com/squidfunk/mkdocs-material) and is hosted by [GitHub Pages](https://docs.github.com/en/pages). ## License Hatch is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. ================================================ FILE: backend/LICENSE.txt ================================================ MIT License Copyright (c) 2021-present Ofek Lev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: backend/README.md ================================================ # Hatchling
Hatch logo | | | | --- | --- | | Package | [![PyPI - Version](https://img.shields.io/pypi/v/hatchling.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/hatchling/) [![PyPI - Downloads](https://img.shields.io/pypi/dm/hatchling.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold)](https://pypi.org/project/hatchling/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/hatchling.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/hatchling/) | | Meta | [![Hatch project](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pypa/hatch/master/docs/assets/badge/v0.json)](https://github.com/pypa/hatch) [![linting - Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![code style - Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![types - Mypy](https://img.shields.io/badge/types-Mypy-blue.svg)](https://github.com/python/mypy) [![License - MIT](https://img.shields.io/badge/license-MIT-9400d3.svg)](https://spdx.org/licenses/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/ofek?logo=GitHub%20Sponsors&style=social)](https://github.com/sponsors/ofek) |
----- This is the extensible, standards compliant build backend used by [Hatch](https://github.com/pypa/hatch). ## Usage The following snippet must be present in your project's `pyproject.toml` file in order to use Hatchling as your build backend: ```toml [build-system] requires = ["hatchling"] build-backend = "hatchling.build" ``` Then a build frontend like [pip](https://github.com/pypa/pip), [build](https://github.com/pypa/build), or Hatch itself can build or install your project automatically: ```console # install using pip pip install /path/to/project # build python -m build /path/to/project # build with Hatch hatch build /path/to/project ``` ## Documentation - [Project metadata](https://hatch.pypa.io/latest/config/metadata/) - [Dependencies](https://hatch.pypa.io/latest/config/dependency/) - [Packaging](https://hatch.pypa.io/latest/config/build/) ================================================ FILE: backend/pyproject.toml ================================================ [build-system] requires = [] build-backend = 'hatchling.ouroboros' backend-path = ['src'] [project] name = "hatchling" dynamic = ["version"] description = "Modern, extensible Python build backend" readme = "README.md" license = "MIT" license-files = ["LICENSE.txt"] requires-python = ">=3.10" keywords = [ "build", "hatch", "packaging", ] authors = [ { name = "Ofek Lev", email = "oss@ofek.dev" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Build Tools", "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ "packaging>=24.2", "pathspec>=0.10.1", "pluggy>=1.0.0", "tomli>=1.2.2; python_version < '3.11'", "trove-classifiers", ] [project.urls] Homepage = "https://hatch.pypa.io/latest/" Sponsor = "https://github.com/sponsors/ofek" History = "https://hatch.pypa.io/dev/history/hatchling/" Tracker = "https://github.com/pypa/hatch/issues" Source = "https://github.com/pypa/hatch/tree/master/backend" [project.scripts] hatchling = "hatchling.cli:hatchling" [tool.hatch.version] path = "src/hatchling/__about__.py" ================================================ FILE: backend/src/hatchling/__about__.py ================================================ __version__ = "1.29.0" ================================================ FILE: backend/src/hatchling/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/__main__.py ================================================ import sys if __name__ == "__main__": from hatchling.cli import hatchling sys.exit(hatchling()) ================================================ FILE: backend/src/hatchling/bridge/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/bridge/app.py ================================================ from __future__ import annotations import os import sys from typing import Any class Application: """ The way output is displayed can be [configured](../config/hatch.md#terminal) by users. !!! important Never import this directly; Hatch judiciously decides if a type of plugin requires the capabilities herein and will grant access via an attribute. """ def __init__(self) -> None: self.__verbosity = int(os.environ.get("HATCH_VERBOSE", "0")) - int(os.environ.get("HATCH_QUIET", "0")) @property def verbosity(self) -> int: """ The verbosity level of the application, with 0 as the default. """ return self.__verbosity @staticmethod def display(message: str = "", **kwargs: Any) -> None: # noqa: ARG004 # Do not document _display(message, always=True) def display_info(self, message: str = "", **kwargs: Any) -> None: # noqa: ARG002 """ Meant to be used for messages conveying basic information. """ if self.__verbosity >= 0: _display(message) def display_waiting(self, message: str = "", **kwargs: Any) -> None: # noqa: ARG002 """ Meant to be used for messages shown before potentially time consuming operations. """ if self.__verbosity >= 0: _display(message) def display_success(self, message: str = "", **kwargs: Any) -> None: # noqa: ARG002 """ Meant to be used for messages indicating some positive outcome. """ if self.__verbosity >= 0: _display(message) def display_warning(self, message: str = "", **kwargs: Any) -> None: # noqa: ARG002 """ Meant to be used for messages conveying important information. """ if self.__verbosity >= -1: _display(message) def display_error(self, message: str = "", **kwargs: Any) -> None: # noqa: ARG002 """ Meant to be used for messages indicating some unrecoverable error. """ if self.__verbosity >= -2: # noqa: PLR2004 _display(message) def display_debug(self, message: str = "", level: int = 1, **kwargs: Any) -> None: # noqa: ARG002 """ Meant to be used for messages that are not useful for most user experiences. The `level` option must be between 1 and 3 (inclusive). """ if not 1 <= level <= 3: # noqa: PLR2004 error_message = "Debug output can only have verbosity levels between 1 and 3 (inclusive)" raise ValueError(error_message) if self.__verbosity >= level: _display(message) def display_mini_header(self, message: str = "", **kwargs: Any) -> None: # noqa: ARG002 if self.__verbosity >= 0: _display(f"[{message}]") def abort(self, message: str = "", code: int = 1, **kwargs: Any) -> None: # noqa: ARG002 """ Terminate the program with the given return code. """ if message and self.__verbosity >= -2: # noqa: PLR2004 _display(message) sys.exit(code) def get_safe_application(self) -> SafeApplication: return SafeApplication(self) class SafeApplication: def __init__(self, app: Application) -> None: self.abort = app.abort self.verbosity = app.verbosity self.display = app.display self.display_info = app.display_info self.display_error = app.display_error self.display_success = app.display_success self.display_waiting = app.display_waiting self.display_warning = app.display_warning self.display_debug = app.display_debug self.display_mini_header = app.display_mini_header def _display(message: str, *, always: bool = False) -> None: print(message, file=None if always else sys.stderr) ================================================ FILE: backend/src/hatchling/build.py ================================================ from __future__ import annotations import os from typing import Any __all__ = [ "build_editable", "build_sdist", "build_wheel", "get_requires_for_build_editable", "get_requires_for_build_sdist", "get_requires_for_build_wheel", ] __all__ += ["__all__"] def get_requires_for_build_sdist(config_settings: dict[str, Any] | None = None) -> list[str]: # noqa: ARG001 """ https://peps.python.org/pep-0517/#get-requires-for-build-sdist """ from hatchling.builders.sdist import SdistBuilder builder = SdistBuilder(os.getcwd()) return builder.config.dependencies def build_sdist(sdist_directory: str, config_settings: dict[str, Any] | None = None) -> str: # noqa: ARG001 """ https://peps.python.org/pep-0517/#build-sdist """ from hatchling.builders.sdist import SdistBuilder builder = SdistBuilder(os.getcwd()) return os.path.basename(next(builder.build(directory=sdist_directory, versions=["standard"]))) def get_requires_for_build_wheel(config_settings: dict[str, Any] | None = None) -> list[str]: # noqa: ARG001 """ https://peps.python.org/pep-0517/#get-requires-for-build-wheel """ from hatchling.builders.wheel import WheelBuilder builder = WheelBuilder(os.getcwd()) return builder.config.dependencies def build_wheel( wheel_directory: str, config_settings: dict[str, Any] | None = None, # noqa: ARG001 metadata_directory: str | None = None, # noqa: ARG001 ) -> str: """ https://peps.python.org/pep-0517/#build-wheel """ from hatchling.builders.wheel import WheelBuilder builder = WheelBuilder(os.getcwd()) return os.path.basename(next(builder.build(directory=wheel_directory, versions=["standard"]))) def get_requires_for_build_editable(config_settings: dict[str, Any] | None = None) -> list[str]: # noqa: ARG001 """ https://peps.python.org/pep-0660/#get-requires-for-build-editable """ from hatchling.builders.constants import EDITABLES_REQUIREMENT from hatchling.builders.wheel import WheelBuilder builder = WheelBuilder(os.getcwd()) return [*builder.config.dependencies, EDITABLES_REQUIREMENT] def build_editable( wheel_directory: str, config_settings: dict[str, Any] | None = None, # noqa: ARG001 metadata_directory: str | None = None, # noqa: ARG001 ) -> str: """ https://peps.python.org/pep-0660/#build-editable """ from hatchling.builders.wheel import WheelBuilder builder = WheelBuilder(os.getcwd()) return os.path.basename(next(builder.build(directory=wheel_directory, versions=["editable"]))) # Any builder that has build-time hooks like Hatchling and setuptools cannot technically keep PEP 517's identical # metadata promise e.g. C extensions would require different tags in the `WHEEL` file. Therefore, we consider the # methods as mostly being for non-frontend tools like tox and dependency updaters. So Hatchling only writes the # `METADATA` file to the metadata directory and continues to ignore that directory itself. # # An issue we encounter by supporting this metadata-only access is that for installations with pip the required # dependencies of the project are read at this stage. This means that build hooks that add to the `dependencies` # build data or modify the built wheel have no effect on what dependencies are or are not installed. # # There are legitimate use cases in which this is required, so we only define these when no pip build is detected. # See: https://github.com/pypa/pip/blob/22.2.2/src/pip/_internal/operations/build/build_tracker.py#L41-L51 # Example use case: https://github.com/pypa/hatch/issues/532 if "PIP_BUILD_TRACKER" not in os.environ: __all__ += ["prepare_metadata_for_build_editable", "prepare_metadata_for_build_wheel"] def prepare_metadata_for_build_wheel( metadata_directory: str, config_settings: dict[str, Any] | None = None, # noqa: ARG001 ) -> str: """ https://peps.python.org/pep-0517/#prepare-metadata-for-build-wheel """ from hatchling.builders.wheel import WheelBuilder builder = WheelBuilder(os.getcwd()) directory = os.path.join(metadata_directory, f"{builder.artifact_project_id}.dist-info") if not os.path.isdir(directory): os.mkdir(directory) with open(os.path.join(directory, "METADATA"), "w", encoding="utf-8") as f: f.write(builder.config.core_metadata_constructor(builder.metadata)) return os.path.basename(directory) def prepare_metadata_for_build_editable( metadata_directory: str, config_settings: dict[str, Any] | None = None, # noqa: ARG001 ) -> str: """ https://peps.python.org/pep-0660/#prepare-metadata-for-build-editable """ from hatchling.builders.constants import EDITABLES_REQUIREMENT from hatchling.builders.wheel import WheelBuilder builder = WheelBuilder(os.getcwd()) directory = os.path.join(metadata_directory, f"{builder.artifact_project_id}.dist-info") if not os.path.isdir(directory): os.mkdir(directory) extra_dependencies = [] if not builder.config.dev_mode_dirs and builder.config.dev_mode_exact: extra_dependencies.append(EDITABLES_REQUIREMENT) with open(os.path.join(directory, "METADATA"), "w", encoding="utf-8") as f: f.write(builder.config.core_metadata_constructor(builder.metadata, extra_dependencies=extra_dependencies)) return os.path.basename(directory) ================================================ FILE: backend/src/hatchling/builders/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/builders/app.py ================================================ from __future__ import annotations from typing import Any from hatchling.builders.binary import BinaryBuilder class AppBuilder(BinaryBuilder): PLUGIN_NAME = "app" def build_bootstrap( self, directory: str, **build_data: Any, ) -> str: self.app.display_warning( "The `app` build target is deprecated and will be removed in a future release. " "Use the `binary` build target instead." ) return super().build_bootstrap(directory, **build_data) ================================================ FILE: backend/src/hatchling/builders/binary.py ================================================ from __future__ import annotations import os import sys from typing import TYPE_CHECKING, Any from hatchling.builders.config import BuilderConfig from hatchling.builders.plugin.interface import BuilderInterface if TYPE_CHECKING: from collections.abc import Callable class BinaryBuilderConfig(BuilderConfig): SUPPORTED_VERSIONS = ("3.12", "3.11", "3.10", "3.9", "3.8", "3.7") def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.__scripts: list[str] | None = None self.__python_version: str | None = None self.__pyapp_version: str | None = None @property def scripts(self) -> list[str]: if self.__scripts is None: known_scripts = self.builder.metadata.core.scripts scripts = self.target_config.get("scripts", []) if not isinstance(scripts, list): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.scripts` must be an array" raise TypeError(message) for i, script in enumerate(scripts, 1): if not isinstance(script, str): message = ( f"Script #{i} of field `tool.hatch.build.targets.{self.plugin_name}.scripts` must be a string" ) raise TypeError(message) if script not in known_scripts: message = f"Unknown script in field `tool.hatch.build.targets.{self.plugin_name}.scripts`: {script}" raise ValueError(message) self.__scripts = sorted(set(scripts)) if scripts else list(known_scripts) return self.__scripts @property def python_version(self) -> str: if self.__python_version is None: python_version = self.target_config.get("python-version", "") if not isinstance(python_version, str): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.python-version` must be a string" raise TypeError(message) if not python_version and "PYAPP_DISTRIBUTION_SOURCE" not in os.environ: for supported_version in self.SUPPORTED_VERSIONS: if self.builder.metadata.core.python_constraint.contains(supported_version): python_version = supported_version break else: message = "Field `project.requires-python` is incompatible with the known distributions" raise ValueError(message) self.__python_version = python_version return self.__python_version @property def pyapp_version(self) -> str: if self.__pyapp_version is None: pyapp_version = self.target_config.get("pyapp-version", "") if not isinstance(pyapp_version, str): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.pyapp-version` must be a string" raise TypeError(message) self.__pyapp_version = pyapp_version return self.__pyapp_version class BinaryBuilder(BuilderInterface): """ Build binaries """ PLUGIN_NAME = "binary" def get_version_api(self) -> dict[str, Callable]: return {"bootstrap": self.build_bootstrap} def get_default_versions(self) -> list[str]: # noqa: PLR6301 return ["bootstrap"] def clean( self, directory: str, versions: list[str], # noqa: ARG002 ) -> None: import shutil app_dir = os.path.join(directory, self.PLUGIN_NAME) if os.path.isdir(app_dir): shutil.rmtree(app_dir) def build_bootstrap( self, directory: str, **build_data: Any, # noqa: ARG002 ) -> str: import shutil import tempfile cargo_path = os.environ.get("CARGO", "") if not cargo_path: if not shutil.which("cargo"): message = "Executable `cargo` could not be found on PATH" raise OSError(message) cargo_path = "cargo" app_dir = os.path.join(directory, self.PLUGIN_NAME) if not os.path.isdir(app_dir): os.makedirs(app_dir) on_windows = sys.platform == "win32" base_env = dict(os.environ) base_env["PYAPP_PROJECT_NAME"] = self.metadata.name base_env["PYAPP_PROJECT_VERSION"] = self.metadata.version if self.config.python_version: base_env["PYAPP_PYTHON_VERSION"] = self.config.python_version # https://doc.rust-lang.org/cargo/reference/config.html#buildtarget build_target = os.environ.get("CARGO_BUILD_TARGET", "") # This will determine whether we install from crates.io or build locally and is currently required for # cross compilation: https://github.com/cross-rs/cross/issues/1215 repo_path = os.environ.get("PYAPP_REPO", "") with tempfile.TemporaryDirectory() as temp_dir: exe_name = "pyapp.exe" if on_windows else "pyapp" if repo_path: context_dir = repo_path target_dir = os.path.join(temp_dir, "build") if build_target: temp_exe_path = os.path.join(target_dir, build_target, "release", exe_name) else: temp_exe_path = os.path.join(target_dir, "release", exe_name) install_command = [cargo_path, "build", "--release", "--target-dir", target_dir] else: context_dir = temp_dir temp_exe_path = os.path.join(temp_dir, "bin", exe_name) install_command = [cargo_path, "install", "pyapp", "--force", "--root", temp_dir] if self.config.pyapp_version: install_command.extend(["--version", self.config.pyapp_version]) if self.config.scripts: for script in self.config.scripts: env = dict(base_env) env["PYAPP_EXEC_SPEC"] = self.metadata.core.scripts[script] self.cargo_build(install_command, cwd=context_dir, env=env) exe_stem = ( f"{script}-{self.metadata.version}-{build_target}" if build_target else f"{script}-{self.metadata.version}" ) exe_path = os.path.join(app_dir, f"{exe_stem}.exe" if on_windows else exe_stem) shutil.move(temp_exe_path, exe_path) else: self.cargo_build(install_command, cwd=context_dir, env=base_env) exe_stem = ( f"{self.metadata.name}-{self.metadata.version}-{build_target}" if build_target else f"{self.metadata.name}-{self.metadata.version}" ) exe_path = os.path.join(app_dir, f"{exe_stem}.exe" if on_windows else exe_stem) shutil.move(temp_exe_path, exe_path) return app_dir def cargo_build(self, *args: Any, **kwargs: Any) -> None: import subprocess if self.app.verbosity < 0: kwargs["stdout"] = subprocess.PIPE kwargs["stderr"] = subprocess.STDOUT process = subprocess.run(*args, **kwargs) # noqa: PLW1510 if process.returncode: message = f"Compilation failed (code {process.returncode})" raise OSError(message) @classmethod def get_config_class(cls) -> type[BinaryBuilderConfig]: return BinaryBuilderConfig ================================================ FILE: backend/src/hatchling/builders/config.py ================================================ from __future__ import annotations import os from contextlib import contextmanager from functools import cached_property from typing import TYPE_CHECKING, Any, TypeVar import pathspec from hatchling.builders.constants import DEFAULT_BUILD_DIRECTORY, EXCLUDED_DIRECTORIES, BuildEnvVars from hatchling.builders.utils import normalize_inclusion_map, normalize_relative_directory, normalize_relative_path from hatchling.metadata.utils import normalize_project_name from hatchling.utils.fs import locate_file if TYPE_CHECKING: from collections.abc import Generator from hatchling.builders.plugin.interface import BuilderInterface class BuilderConfig: def __init__( self, builder: BuilderInterface, root: str, plugin_name: str, build_config: dict[str, Any], target_config: dict[str, Any], ) -> None: self.__builder = builder self.__root = root self.__plugin_name = plugin_name self.__build_config = build_config self.__target_config = target_config # This is used when the only file selection is based on forced inclusion or build-time artifacts. This # instructs to `exclude` every encountered path without doing pattern matching that matches everything. self.__exclude_all: bool = False # Modified at build time self.build_artifact_spec: pathspec.GitIgnoreSpec | None = None self.build_force_include: dict[str, str] = {} self.build_reserved_paths: set[str] = set() @property def builder(self) -> BuilderInterface: return self.__builder @property def root(self) -> str: return self.__root @property def plugin_name(self) -> str: return self.__plugin_name @property def build_config(self) -> dict[str, Any]: return self.__build_config @property def target_config(self) -> dict[str, Any]: return self.__target_config def include_path(self, relative_path: str, *, explicit: bool = False, is_package: bool = True) -> bool: return ( self.path_is_build_artifact(relative_path) or self.path_is_artifact(relative_path) or ( not (self.only_packages and not is_package) and not self.path_is_excluded(relative_path) and (explicit or self.path_is_included(relative_path)) ) ) def path_is_included(self, relative_path: str) -> bool: if self.include_spec is None: return True return self.include_spec.match_file(relative_path) def path_is_excluded(self, relative_path: str) -> bool: if self.__exclude_all: return True if self.exclude_spec is None: return False return self.exclude_spec.match_file(relative_path) def path_is_artifact(self, relative_path: str) -> bool: if self.artifact_spec is None: return False return self.artifact_spec.match_file(relative_path) def path_is_build_artifact(self, relative_path: str) -> bool: if self.build_artifact_spec is None: return False return self.build_artifact_spec.match_file(relative_path) def path_is_reserved(self, relative_path: str) -> bool: return relative_path in self.build_reserved_paths def directory_is_excluded(self, name: str, relative_path: str) -> bool: if name in EXCLUDED_DIRECTORIES: return True relative_directory = os.path.join(relative_path, name) return ( self.path_is_reserved(relative_directory) # The trailing slash is necessary so e.g. `bar/` matches `foo/bar` or (self.skip_excluded_dirs and self.path_is_excluded(f"{relative_directory}/")) ) @cached_property def include_spec(self) -> pathspec.GitIgnoreSpec | None: if "include" in self.target_config: include_config = self.target_config include_location = f"tool.hatch.build.targets.{self.plugin_name}.include" else: include_config = self.build_config include_location = "tool.hatch.build.include" all_include_patterns = [] include_patterns = include_config.get("include", self.default_include()) if not isinstance(include_patterns, list): message = f"Field `{include_location}` must be an array of strings" raise TypeError(message) for i, include_pattern in enumerate(include_patterns, 1): if not isinstance(include_pattern, str): message = f"Pattern #{i} in field `{include_location}` must be a string" raise TypeError(message) if not include_pattern: message = f"Pattern #{i} in field `{include_location}` cannot be an empty string" raise ValueError(message) all_include_patterns.append(include_pattern) # Matching only at the root requires a forward slash, back slashes do not work. As such, # normalize to forward slashes for consistency. all_include_patterns.extend(f"/{relative_path.replace(os.sep, '/')}/" for relative_path in self.packages) if all_include_patterns: return pathspec.GitIgnoreSpec.from_lines(all_include_patterns) return None @cached_property def exclude_spec(self) -> pathspec.GitIgnoreSpec | None: if "exclude" in self.target_config: exclude_config = self.target_config exclude_location = f"tool.hatch.build.targets.{self.plugin_name}.exclude" else: exclude_config = self.build_config exclude_location = "tool.hatch.build.exclude" all_exclude_patterns = self.default_global_exclude() if not self.ignore_vcs: all_exclude_patterns.extend(self.load_vcs_exclusion_patterns()) exclude_patterns = exclude_config.get("exclude", self.default_exclude()) if not isinstance(exclude_patterns, list): message = f"Field `{exclude_location}` must be an array of strings" raise TypeError(message) for i, exclude_pattern in enumerate(exclude_patterns, 1): if not isinstance(exclude_pattern, str): message = f"Pattern #{i} in field `{exclude_location}` must be a string" raise TypeError(message) if not exclude_pattern: message = f"Pattern #{i} in field `{exclude_location}` cannot be an empty string" raise ValueError(message) all_exclude_patterns.append(exclude_pattern) if all_exclude_patterns: return pathspec.GitIgnoreSpec.from_lines(all_exclude_patterns) return None @property def artifact_spec(self) -> pathspec.GitIgnoreSpec | None: if "artifacts" in self.target_config: artifact_config = self.target_config artifact_location = f"tool.hatch.build.targets.{self.plugin_name}.artifacts" else: artifact_config = self.build_config artifact_location = "tool.hatch.build.artifacts" all_artifact_patterns = [] artifact_patterns = artifact_config.get("artifacts", []) if not isinstance(artifact_patterns, list): message = f"Field `{artifact_location}` must be an array of strings" raise TypeError(message) for i, artifact_pattern in enumerate(artifact_patterns, 1): if not isinstance(artifact_pattern, str): message = f"Pattern #{i} in field `{artifact_location}` must be a string" raise TypeError(message) if not artifact_pattern: message = f"Pattern #{i} in field `{artifact_location}` cannot be an empty string" raise ValueError(message) all_artifact_patterns.append(artifact_pattern) if all_artifact_patterns: return pathspec.GitIgnoreSpec.from_lines(all_artifact_patterns) return None @cached_property def hook_config(self) -> dict[str, Any]: hook_config: dict[str, dict[str, Any]] = {} global_hook_config = self.build_config.get("hooks", {}) if not isinstance(global_hook_config, dict): message = "Field `tool.hatch.build.hooks` must be a table" raise TypeError(message) for hook_name, config in global_hook_config.items(): if not isinstance(config, dict): message = f"Field `tool.hatch.build.hooks.{hook_name}` must be a table" raise TypeError(message) hook_config.setdefault(hook_name, config) target_hook_config = self.target_config.get("hooks", {}) if not isinstance(target_hook_config, dict): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.hooks` must be a table" raise TypeError(message) for hook_name, config in target_hook_config.items(): if not isinstance(config, dict): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.hooks.{hook_name}` must be a table" raise TypeError(message) hook_config[hook_name] = config if not env_var_enabled(BuildEnvVars.NO_HOOKS): all_hooks_enabled = env_var_enabled(BuildEnvVars.HOOKS_ENABLE) final_hook_config = { hook_name: config for hook_name, config in hook_config.items() if ( all_hooks_enabled or config.get("enable-by-default", True) or env_var_enabled(f"{BuildEnvVars.HOOK_ENABLE_PREFIX}{hook_name.upper()}") ) } else: final_hook_config = {} return final_hook_config @cached_property def directory(self) -> str: if "directory" in self.target_config: directory = self.target_config["directory"] if not isinstance(directory, str): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.directory` must be a string" raise TypeError(message) else: directory = self.build_config.get("directory", DEFAULT_BUILD_DIRECTORY) if not isinstance(directory, str): message = "Field `tool.hatch.build.directory` must be a string" raise TypeError(message) return self.normalize_build_directory(directory) @cached_property def skip_excluded_dirs(self) -> bool: if "skip-excluded-dirs" in self.target_config: skip_excluded_dirs = self.target_config["skip-excluded-dirs"] if not isinstance(skip_excluded_dirs, bool): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.skip-excluded-dirs` must be a boolean" raise TypeError(message) else: skip_excluded_dirs = self.build_config.get("skip-excluded-dirs", False) if not isinstance(skip_excluded_dirs, bool): message = "Field `tool.hatch.build.skip-excluded-dirs` must be a boolean" raise TypeError(message) return skip_excluded_dirs @cached_property def ignore_vcs(self) -> bool: if "ignore-vcs" in self.target_config: ignore_vcs = self.target_config["ignore-vcs"] if not isinstance(ignore_vcs, bool): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.ignore-vcs` must be a boolean" raise TypeError(message) else: ignore_vcs = self.build_config.get("ignore-vcs", False) if not isinstance(ignore_vcs, bool): message = "Field `tool.hatch.build.ignore-vcs` must be a boolean" raise TypeError(message) return ignore_vcs @cached_property def require_runtime_dependencies(self) -> bool: if "require-runtime-dependencies" in self.target_config: require_runtime_dependencies = self.target_config["require-runtime-dependencies"] if not isinstance(require_runtime_dependencies, bool): message = ( f"Field `tool.hatch.build.targets.{self.plugin_name}.require-runtime-dependencies` " f"must be a boolean" ) raise TypeError(message) else: require_runtime_dependencies = self.build_config.get("require-runtime-dependencies", False) if not isinstance(require_runtime_dependencies, bool): message = "Field `tool.hatch.build.require-runtime-dependencies` must be a boolean" raise TypeError(message) return require_runtime_dependencies @cached_property def require_runtime_features(self) -> list[str]: if "require-runtime-features" in self.target_config: features_config = self.target_config features_location = f"tool.hatch.build.targets.{self.plugin_name}.require-runtime-features" else: features_config = self.build_config features_location = "tool.hatch.build.require-runtime-features" require_runtime_features = features_config.get("require-runtime-features", []) if not isinstance(require_runtime_features, list): message = f"Field `{features_location}` must be an array" raise TypeError(message) all_features: dict[str, None] = {} for i, raw_feature in enumerate(require_runtime_features, 1): if not isinstance(raw_feature, str): message = f"Feature #{i} of field `{features_location}` must be a string" raise TypeError(message) if not raw_feature: message = f"Feature #{i} of field `{features_location}` cannot be an empty string" raise ValueError(message) feature = normalize_project_name(raw_feature) if feature not in self.builder.metadata.core.optional_dependencies: message = ( f"Feature `{feature}` of field `{features_location}` is not defined in " f"field `project.optional-dependencies`" ) raise ValueError(message) all_features[feature] = None return list(all_features) @cached_property def only_packages(self) -> bool: """ Whether or not the target should ignore non-artifact files that do not reside within a Python package. """ if "only-packages" in self.target_config: only_packages = self.target_config["only-packages"] if not isinstance(only_packages, bool): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.only-packages` must be a boolean" raise TypeError(message) else: only_packages = self.build_config.get("only-packages", False) if not isinstance(only_packages, bool): message = "Field `tool.hatch.build.only-packages` must be a boolean" raise TypeError(message) return only_packages @cached_property def reproducible(self) -> bool: """ Whether or not the target should be built in a reproducible manner, defaulting to true. """ if "reproducible" in self.target_config: reproducible = self.target_config["reproducible"] if not isinstance(reproducible, bool): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.reproducible` must be a boolean" raise TypeError(message) else: reproducible = self.build_config.get("reproducible", True) if not isinstance(reproducible, bool): message = "Field `tool.hatch.build.reproducible` must be a boolean" raise TypeError(message) return reproducible @cached_property def dev_mode_dirs(self) -> list[str]: """ Directories which must be added to Python's search path in [dev mode](../config/environment/overview.md#dev-mode). """ if "dev-mode-dirs" in self.target_config: dev_mode_dirs_config = self.target_config dev_mode_dirs_location = f"tool.hatch.build.targets.{self.plugin_name}.dev-mode-dirs" else: dev_mode_dirs_config = self.build_config dev_mode_dirs_location = "tool.hatch.build.dev-mode-dirs" all_dev_mode_dirs = [] dev_mode_dirs = dev_mode_dirs_config.get("dev-mode-dirs", []) if not isinstance(dev_mode_dirs, list): message = f"Field `{dev_mode_dirs_location}` must be an array of strings" raise TypeError(message) for i, dev_mode_dir in enumerate(dev_mode_dirs, 1): if not isinstance(dev_mode_dir, str): message = f"Directory #{i} in field `{dev_mode_dirs_location}` must be a string" raise TypeError(message) if not dev_mode_dir: message = f"Directory #{i} in field `{dev_mode_dirs_location}` cannot be an empty string" raise ValueError(message) all_dev_mode_dirs.append(dev_mode_dir) return all_dev_mode_dirs @cached_property def dev_mode_exact(self) -> bool: if "dev-mode-exact" in self.target_config: dev_mode_exact = self.target_config["dev-mode-exact"] if not isinstance(dev_mode_exact, bool): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.dev-mode-exact` must be a boolean" raise TypeError(message) else: dev_mode_exact = self.build_config.get("dev-mode-exact", False) if not isinstance(dev_mode_exact, bool): message = "Field `tool.hatch.build.dev-mode-exact` must be a boolean" raise TypeError(message) return dev_mode_exact @cached_property def versions(self) -> list[str]: # Used as an ordered set all_versions: dict[str, None] = {} versions = self.target_config.get("versions", []) if not isinstance(versions, list): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.versions` must be an array of strings" raise TypeError(message) for i, version in enumerate(versions, 1): if not isinstance(version, str): message = ( f"Version #{i} in field `tool.hatch.build.targets.{self.plugin_name}.versions` must be a string" ) raise TypeError(message) if not version: message = ( f"Version #{i} in field `tool.hatch.build.targets.{self.plugin_name}.versions` " f"cannot be an empty string" ) raise ValueError(message) all_versions[version] = None if not all_versions: default_versions = self.__builder.get_default_versions() for version in default_versions: all_versions[version] = None else: unknown_versions = set(all_versions) - set(self.__builder.get_version_api()) if unknown_versions: message = ( f"Unknown versions in field `tool.hatch.build.targets.{self.plugin_name}.versions`: " f"{', '.join(map(str, sorted(unknown_versions)))}" ) raise ValueError(message) return list(all_versions) @cached_property def dependencies(self) -> list[str]: # Used as an ordered set dependencies: dict[str, None] = {} target_dependencies = self.target_config.get("dependencies", []) if not isinstance(target_dependencies, list): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.dependencies` must be an array" raise TypeError(message) for i, dependency in enumerate(target_dependencies, 1): if not isinstance(dependency, str): message = ( f"Dependency #{i} of field `tool.hatch.build.targets.{self.plugin_name}.dependencies` " f"must be a string" ) raise TypeError(message) dependencies[dependency] = None global_dependencies = self.build_config.get("dependencies", []) if not isinstance(global_dependencies, list): message = "Field `tool.hatch.build.dependencies` must be an array" raise TypeError(message) for i, dependency in enumerate(global_dependencies, 1): if not isinstance(dependency, str): message = f"Dependency #{i} of field `tool.hatch.build.dependencies` must be a string" raise TypeError(message) dependencies[dependency] = None require_runtime_dependencies = self.require_runtime_dependencies require_runtime_features = dict.fromkeys(self.require_runtime_features) for hook_name, config in self.hook_config.items(): hook_require_runtime_dependencies = config.get("require-runtime-dependencies", False) if not isinstance(hook_require_runtime_dependencies, bool): message = f"Option `require-runtime-dependencies` of build hook `{hook_name}` must be a boolean" raise TypeError(message) if hook_require_runtime_dependencies: require_runtime_dependencies = True hook_require_runtime_features = config.get("require-runtime-features", []) if not isinstance(hook_require_runtime_features, list): message = f"Option `require-runtime-features` of build hook `{hook_name}` must be an array" raise TypeError(message) for i, raw_feature in enumerate(hook_require_runtime_features, 1): if not isinstance(raw_feature, str): message = ( f"Feature #{i} of option `require-runtime-features` of build hook `{hook_name}` " f"must be a string" ) raise TypeError(message) if not raw_feature: message = ( f"Feature #{i} of option `require-runtime-features` of build hook `{hook_name}` " f"cannot be an empty string" ) raise ValueError(message) feature = normalize_project_name(raw_feature) if feature not in self.builder.metadata.core.optional_dependencies: message = ( f"Feature `{feature}` of option `require-runtime-features` of build hook `{hook_name}` " f"is not defined in field `project.optional-dependencies`" ) raise ValueError(message) require_runtime_features[feature] = None hook_dependencies = config.get("dependencies", []) if not isinstance(hook_dependencies, list): message = f"Option `dependencies` of build hook `{hook_name}` must be an array" raise TypeError(message) for i, dependency in enumerate(hook_dependencies, 1): if not isinstance(dependency, str): message = f"Dependency #{i} of option `dependencies` of build hook `{hook_name}` must be a string" raise TypeError(message) dependencies[dependency] = None if require_runtime_dependencies: for dependency in self.builder.metadata.core.dependencies: dependencies[dependency] = None if require_runtime_features: for feature in require_runtime_features: for dependency in self.builder.metadata.core.optional_dependencies[feature]: dependencies[dependency] = None for dependency in self.dynamic_dependencies: dependencies[dependency] = None return list(dependencies) @cached_property def dynamic_dependencies(self) -> list[str]: dependencies = [] for hook_name, config in self.hook_config.items(): build_hook_cls = self.builder.plugin_manager.build_hook.get(hook_name) if build_hook_cls is None: continue # Hook exists but dynamic dependencies are not imported lazily. # This happens for example when using the `custom` build hook. try: build_hook = build_hook_cls( self.root, config, self, self.builder.metadata, "", self.builder.PLUGIN_NAME, self.builder.app ) except ImportError: continue dependencies.extend(build_hook.dependencies()) return dependencies @cached_property def sources(self) -> dict[str, str]: if "sources" in self.target_config: sources_config = self.target_config sources_location = f"tool.hatch.build.targets.{self.plugin_name}.sources" else: sources_config = self.build_config sources_location = "tool.hatch.build.sources" sources = {} raw_sources = sources_config.get("sources", []) if isinstance(raw_sources, list): for i, source in enumerate(raw_sources, 1): if not isinstance(source, str): message = f"Source #{i} in field `{sources_location}` must be a string" raise TypeError(message) if not source: message = f"Source #{i} in field `{sources_location}` cannot be an empty string" raise ValueError(message) sources[normalize_relative_directory(source)] = "" elif isinstance(raw_sources, dict): for source, path in raw_sources.items(): if not isinstance(path, str): message = f"Path for source `{source}` in field `{sources_location}` must be a string" raise TypeError(message) normalized_path = normalize_relative_path(path) if normalized_path == ".": normalized_path = "" else: normalized_path += os.sep sources[normalize_relative_directory(source) if source else source] = normalized_path else: message = f"Field `{sources_location}` must be a mapping or array of strings" raise TypeError(message) for relative_path in self.packages: source, _package = os.path.split(relative_path) if source and normalize_relative_directory(relative_path) not in sources: sources[normalize_relative_directory(source)] = "" return dict(sorted(sources.items())) @cached_property def packages(self) -> list[str]: if "packages" in self.target_config: package_config = self.target_config package_location = f"tool.hatch.build.targets.{self.plugin_name}.packages" else: package_config = self.build_config package_location = "tool.hatch.build.packages" packages = package_config.get("packages", self.default_packages()) if not isinstance(packages, list): message = f"Field `{package_location}` must be an array of strings" raise TypeError(message) for i, package in enumerate(packages, 1): if not isinstance(package, str): message = f"Package #{i} in field `{package_location}` must be a string" raise TypeError(message) if not package: message = f"Package #{i} in field `{package_location}` cannot be an empty string" raise ValueError(message) return sorted(normalize_relative_path(package) for package in packages) @cached_property def force_include(self) -> dict[str, str]: if "force-include" in self.target_config: force_include_config = self.target_config force_include_location = f"tool.hatch.build.targets.{self.plugin_name}.force-include" else: force_include_config = self.build_config force_include_location = "tool.hatch.build.force-include" force_include = force_include_config.get("force-include", {}) if not isinstance(force_include, dict): message = f"Field `{force_include_location}` must be a mapping" raise TypeError(message) for i, (source, relative_path) in enumerate(force_include.items(), 1): if not source: message = f"Source #{i} in field `{force_include_location}` cannot be an empty string" raise ValueError(message) if not isinstance(relative_path, str): message = f"Path for source `{source}` in field `{force_include_location}` must be a string" raise TypeError(message) if not relative_path: message = f"Path for source `{source}` in field `{force_include_location}` cannot be an empty string" raise ValueError(message) return normalize_inclusion_map(force_include, self.root) @cached_property def only_include(self) -> dict[str, str]: if "only-include" in self.target_config: only_include_config = self.target_config only_include_location = f"tool.hatch.build.targets.{self.plugin_name}.only-include" else: only_include_config = self.build_config only_include_location = "tool.hatch.build.only-include" only_include = only_include_config.get("only-include", self.default_only_include()) or self.packages if not isinstance(only_include, list): message = f"Field `{only_include_location}` must be an array" raise TypeError(message) inclusion_map = {} for i, relative_path in enumerate(only_include, 1): if not isinstance(relative_path, str): message = f"Path #{i} in field `{only_include_location}` must be a string" raise TypeError(message) normalized_path = normalize_relative_path(relative_path) if not normalized_path or normalized_path.startswith(("~", "..")): message = f"Path #{i} in field `{only_include_location}` must be relative: {relative_path}" raise ValueError(message) if normalized_path in inclusion_map: message = f"Duplicate path in field `{only_include_location}`: {normalized_path}" raise ValueError(message) inclusion_map[normalized_path] = normalized_path return normalize_inclusion_map(inclusion_map, self.root) def get_distribution_path(self, relative_path: str) -> str: # src/foo/bar.py -> foo/bar.py for source, replacement in self.sources.items(): if not source: return replacement + relative_path if relative_path.startswith(source): return relative_path.replace(source, replacement, 1) return relative_path @cached_property def vcs_exclusion_files(self) -> dict[str, list[str]]: exclusion_files: dict[str, list[str]] = {"git": [], "hg": []} local_gitignore = locate_file(self.root, ".gitignore", boundary=".git") if local_gitignore is not None: exclusion_files["git"].append(local_gitignore) local_hgignore = locate_file(self.root, ".hgignore", boundary=".hg") if local_hgignore is not None: exclusion_files["hg"].append(local_hgignore) return exclusion_files def load_vcs_exclusion_patterns(self) -> list[str]: patterns = [] # https://git-scm.com/docs/gitignore#_pattern_format for exclusion_file in self.vcs_exclusion_files["git"]: with open(exclusion_file, encoding="utf-8") as f: patterns.extend(f.readlines()) # https://linux.die.net/man/5/hgignore for exclusion_file in self.vcs_exclusion_files["hg"]: with open(exclusion_file, encoding="utf-8") as f: glob_mode = False for line in f: exact_line = line.strip() if exact_line == "syntax: glob": glob_mode = True continue if exact_line.startswith("syntax: "): glob_mode = False continue if glob_mode: patterns.append(line) # validate project root is not excluded by vcs exclude_spec = pathspec.GitIgnoreSpec.from_lines(patterns) if exclude_spec.match_file(self.root): return [] return patterns def normalize_build_directory(self, build_directory: str) -> str: if not os.path.isabs(build_directory): build_directory = os.path.join(self.root, build_directory) return os.path.normpath(build_directory) def default_include(self) -> list: # noqa: PLR6301 return [] def default_exclude(self) -> list: # noqa: PLR6301 return [] def default_packages(self) -> list: # noqa: PLR6301 return [] def default_only_include(self) -> list: # noqa: PLR6301 return [] def default_global_exclude(self) -> list[str]: # noqa: PLR6301 patterns = ["*.py[cdo]", f"/{DEFAULT_BUILD_DIRECTORY}"] patterns.sort() return patterns def set_exclude_all(self) -> None: self.__exclude_all = True def get_force_include(self) -> dict[str, str]: force_include = self.force_include.copy() force_include.update(self.build_force_include) return force_include @contextmanager def set_build_data(self, build_data: dict[str, Any]) -> Generator: try: # Include anything the hooks indicate build_artifacts = build_data["artifacts"] if build_artifacts: self.build_artifact_spec = pathspec.GitIgnoreSpec.from_lines(build_artifacts) self.build_force_include.update(normalize_inclusion_map(build_data["force_include"], self.root)) for inclusion_map in (self.force_include, self.build_force_include): for source, target in inclusion_map.items(): # Ignore source # old/ -> new/ # old.ext -> new.ext if source.startswith(f"{self.root}{os.sep}"): self.build_reserved_paths.add(self.get_distribution_path(os.path.relpath(source, self.root))) # Ignore target files only # ../out.ext -> ../in.ext elif os.path.isfile(source): self.build_reserved_paths.add(self.get_distribution_path(target)) yield finally: self.build_artifact_spec = None self.build_force_include.clear() self.build_reserved_paths.clear() def env_var_enabled(env_var: str, *, default: bool = False) -> bool: if env_var in os.environ: return os.environ[env_var] in {"1", "true"} return default BuilderConfigBound = TypeVar("BuilderConfigBound", bound=BuilderConfig) ================================================ FILE: backend/src/hatchling/builders/constants.py ================================================ DEFAULT_BUILD_DIRECTORY = "dist" EXCLUDED_DIRECTORIES = frozenset(( # Python bytecode "__pycache__", # Single virtual environment ".venv", # Git ".git", # Mercurial ".hg", # Hatch ".hatch", # tox ".tox", # nox ".nox", # Ruff ".ruff_cache", # pytest ".pytest_cache", # Mypy ".mypy_cache", # pixi ".pixi", )) EXCLUDED_FILES = frozenset(( # https://en.wikipedia.org/wiki/.DS_Store ".DS_Store", )) class BuildEnvVars: LOCATION = "HATCH_BUILD_LOCATION" HOOKS_ONLY = "HATCH_BUILD_HOOKS_ONLY" NO_HOOKS = "HATCH_BUILD_NO_HOOKS" HOOKS_ENABLE = "HATCH_BUILD_HOOKS_ENABLE" HOOK_ENABLE_PREFIX = "HATCH_BUILD_HOOK_ENABLE_" CLEAN = "HATCH_BUILD_CLEAN" CLEAN_HOOKS_AFTER = "HATCH_BUILD_CLEAN_HOOKS_AFTER" EDITABLES_REQUIREMENT = "editables~=0.3" ================================================ FILE: backend/src/hatchling/builders/custom.py ================================================ from __future__ import annotations import os from typing import TYPE_CHECKING, Any, Generic from hatchling.builders.plugin.interface import BuilderInterface from hatchling.metadata.core import ProjectMetadata from hatchling.plugin.manager import PluginManagerBound from hatchling.plugin.utils import load_plugin_from_script from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT if TYPE_CHECKING: from hatchling.bridge.app import Application class CustomBuilder(Generic[PluginManagerBound]): PLUGIN_NAME = "custom" def __new__( # type: ignore[misc] cls, root: str, plugin_manager: PluginManagerBound | None = None, config: dict[str, Any] | None = None, metadata: ProjectMetadata | None = None, app: Application | None = None, ) -> BuilderInterface: project_metadata = ProjectMetadata(root, plugin_manager, config) target_config = project_metadata.hatch.build_targets.get(cls.PLUGIN_NAME, {}) if not isinstance(target_config, dict): message = f"Field `tool.hatch.build.targets.{cls.PLUGIN_NAME}` must be a table" raise TypeError(message) build_script = target_config.get("path", DEFAULT_BUILD_SCRIPT) if not isinstance(build_script, str): message = f"Option `path` for builder `{cls.PLUGIN_NAME}` must be a string" raise TypeError(message) if not build_script: message = f"Option `path` for builder `{cls.PLUGIN_NAME}` must not be empty if defined" raise ValueError(message) path = os.path.normpath(os.path.join(root, build_script)) if not os.path.isfile(path): message = f"Build script does not exist: {build_script}" raise OSError(message) hook_class = load_plugin_from_script(path, build_script, BuilderInterface, "builder") # type: ignore[type-abstract] hook = hook_class(root, plugin_manager=plugin_manager, config=config, metadata=metadata, app=app) # Always keep the name to avoid confusion hook.PLUGIN_NAME = cls.PLUGIN_NAME return hook ================================================ FILE: backend/src/hatchling/builders/hooks/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/builders/hooks/custom.py ================================================ from __future__ import annotations import os from typing import Any from hatchling.builders.hooks.plugin.interface import BuildHookInterface from hatchling.plugin.utils import load_plugin_from_script from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT class CustomBuildHook: PLUGIN_NAME = "custom" def __new__( # type: ignore[misc] cls, root: str, config: dict[str, Any], *args: Any, **kwargs: Any, ) -> BuildHookInterface: build_script = config.get("path", DEFAULT_BUILD_SCRIPT) if not isinstance(build_script, str): message = f"Option `path` for build hook `{cls.PLUGIN_NAME}` must be a string" raise TypeError(message) if not build_script: message = f"Option `path` for build hook `{cls.PLUGIN_NAME}` must not be empty if defined" raise ValueError(message) path = os.path.normpath(os.path.join(root, build_script)) if not os.path.isfile(path): message = f"Build script does not exist: {build_script}" raise OSError(message) hook_class = load_plugin_from_script(path, build_script, BuildHookInterface, "build_hook") hook = hook_class(root, config, *args, **kwargs) # Always keep the name to avoid confusion hook.PLUGIN_NAME = cls.PLUGIN_NAME return hook ================================================ FILE: backend/src/hatchling/builders/hooks/plugin/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/builders/hooks/plugin/hooks.py ================================================ from __future__ import annotations import typing from hatchling.builders.hooks.custom import CustomBuildHook from hatchling.builders.hooks.version import VersionBuildHook from hatchling.plugin import hookimpl if typing.TYPE_CHECKING: from hatchling.builders.hooks.plugin.interface import BuildHookInterface @hookimpl def hatch_register_build_hook() -> list[type[BuildHookInterface]]: return [CustomBuildHook, VersionBuildHook] ================================================ FILE: backend/src/hatchling/builders/hooks/plugin/interface.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING, Any, Generic, cast from hatchling.builders.config import BuilderConfigBound if TYPE_CHECKING: from hatchling.bridge.app import Application from hatchling.metadata.core import ProjectMetadata class BuildHookInterface(Generic[BuilderConfigBound]): # no cov """ Example usage: ```python tab="plugin.py" from hatchling.builders.hooks.plugin.interface import BuildHookInterface class SpecialBuildHook(BuildHookInterface): PLUGIN_NAME = "special" ... ``` ```python tab="hooks.py" from hatchling.plugin import hookimpl from .plugin import SpecialBuildHook @hookimpl def hatch_register_build_hook(): return SpecialBuildHook ``` """ PLUGIN_NAME = "" """The name used for selection.""" def __init__( self, root: str, config: dict[str, Any], build_config: BuilderConfigBound, metadata: ProjectMetadata, directory: str, target_name: str, app: Application | None = None, ) -> None: self.__root = root self.__config = config self.__build_config = build_config self.__metadata = metadata self.__directory = directory self.__target_name = target_name self.__app = app @property def app(self) -> Application: """ An instance of [Application](../utilities.md#hatchling.bridge.app.Application). """ if self.__app is None: from hatchling.bridge.app import Application self.__app = cast(Application, Application().get_safe_application()) return self.__app @property def root(self) -> str: """ The root of the project tree. """ return self.__root @property def config(self) -> dict[str, Any]: """ The cumulative hook configuration. ```toml config-example [tool.hatch.build.hooks.] [tool.hatch.build.targets..hooks.] ``` """ return self.__config @property def metadata(self) -> ProjectMetadata: # Undocumented for now return self.__metadata @property def build_config(self) -> BuilderConfigBound: """ An instance of [BuilderConfig](../utilities.md#hatchling.builders.config.BuilderConfig). """ return self.__build_config @property def directory(self) -> str: """ The build directory. """ return self.__directory @property def target_name(self) -> str: """ The plugin name of the build target. """ return self.__target_name def dependencies(self) -> list[str]: # noqa: PLR6301 """ A list of extra [dependencies](../../config/dependency.md) that must be installed prior to builds. !!! warning - For this to have any effect the hook dependency itself cannot be dynamic and must always be defined in `build-system.requires`. - As the hook must be imported to call this method, imports that require these dependencies must be evaluated lazily. """ return [] def clean(self, versions: list[str]) -> None: """ This occurs before the build process if the `-c`/`--clean` flag was passed to the [`build`](../../cli/reference.md#hatch-build) command, or when invoking the [`clean`](../../cli/reference.md#hatch-clean) command. """ def initialize(self, version: str, build_data: dict[str, Any]) -> None: """ This occurs immediately before each build. Any modifications to the build data will be seen by the build target. """ def finalize(self, version: str, build_data: dict[str, Any], artifact_path: str) -> None: """ This occurs immediately after each build and will not run if the `--hooks-only` flag was passed to the [`build`](../../cli/reference.md#hatch-build) command. The build data will reflect any modifications done by the target during the build. """ ================================================ FILE: backend/src/hatchling/builders/hooks/version.py ================================================ from __future__ import annotations from typing import Any from hatchling.builders.hooks.plugin.interface import BuildHookInterface from hatchling.version.core import VersionFile class VersionBuildHook(BuildHookInterface): PLUGIN_NAME = "version" def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.__config_path: str | None = None self.__config_template: str | None = None self.__config_pattern: str | bool | None = None @property def config_path(self) -> str: if self.__config_path is None: path = self.config.get("path", "") if not isinstance(path, str): message = f"Option `path` for build hook `{self.PLUGIN_NAME}` must be a string" raise TypeError(message) if not path: message = f"Option `path` for build hook `{self.PLUGIN_NAME}` is required" raise ValueError(message) self.__config_path = path return self.__config_path @property def config_template(self) -> str: if self.__config_template is None: template = self.config.get("template", "") if not isinstance(template, str): message = f"Option `template` for build hook `{self.PLUGIN_NAME}` must be a string" raise TypeError(message) self.__config_template = template return self.__config_template @property def config_pattern(self) -> str | bool: if self.__config_pattern is None: pattern = self.config.get("pattern", "") if not isinstance(pattern, (str, bool)): message = f"Option `pattern` for build hook `{self.PLUGIN_NAME}` must be a string or a boolean" raise TypeError(message) self.__config_pattern = pattern return self.__config_pattern def initialize( self, version: str, # noqa: ARG002 build_data: dict[str, Any], ) -> None: version_file = VersionFile(self.root, self.config_path) if self.config_pattern: version_file.read(pattern=self.config_pattern) version_file.set_version(self.metadata.version) else: version_file.write(self.metadata.version, self.config_template) build_data["artifacts"].append(f"/{self.config_path}") ================================================ FILE: backend/src/hatchling/builders/macos.py ================================================ from __future__ import annotations import os import platform import re __all__ = ["process_macos_plat_tag"] def process_macos_plat_tag(plat: str, /, *, compat: bool) -> str: """ Process the macOS platform tag. This will normalize the macOS version to 10.16 if compat=True. If the MACOSX_DEPLOYMENT_TARGET environment variable is set, then it will be used instead for the target version. If archflags is set, then the archs will be respected, including a universal build. """ # Default to a native build current_arch = platform.machine() arm = current_arch == "arm64" # Look for cross-compiles archflags = os.environ.get("ARCHFLAGS", "") if archflags and (archs := re.findall(r"-arch (\S+)", archflags)): new_arch = "universal2" if set(archs) == {"x86_64", "arm64"} else archs[0] arm = archs == ["arm64"] plat = f"{plat[: plat.rfind(current_arch)]}{new_arch}" # Process macOS version if sdk_match := re.search(r"macosx_(\d+_\d+)", plat): macos_version = sdk_match.group(1) target = os.environ.get("MACOSX_DEPLOYMENT_TARGET", None) try: new_version = normalize_macos_version(target or macos_version, arm=arm, compat=compat) except ValueError: new_version = normalize_macos_version(macos_version, arm=arm, compat=compat) return plat.replace(macos_version, new_version, 1) return plat def normalize_macos_version(version: str, *, arm: bool, compat: bool) -> str: """ Set minor version to 0 if major is 11+. Enforces 11+ if arm=True. 11+ is converted to 10.16 if compat=True. Version is always returned in "major_minor" format. """ version = version.replace(".", "_") if "_" not in version: version = f"{version}_0" major, minor = (int(d) for d in version.split("_")[:2]) major = max(major, 11) if arm else major minor = 0 if major >= 11 else minor # noqa: PLR2004 if compat and major >= 11: # noqa: PLR2004 major = 10 minor = 16 return f"{major}_{minor}" ================================================ FILE: backend/src/hatchling/builders/plugin/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/builders/plugin/hooks.py ================================================ from __future__ import annotations import typing from hatchling.builders.app import AppBuilder from hatchling.builders.binary import BinaryBuilder from hatchling.builders.custom import CustomBuilder from hatchling.builders.sdist import SdistBuilder from hatchling.builders.wheel import WheelBuilder from hatchling.plugin import hookimpl if typing.TYPE_CHECKING: from hatchling.builders.plugin.interface import BuilderInterface @hookimpl def hatch_register_builder() -> list[type[BuilderInterface]]: return [AppBuilder, BinaryBuilder, CustomBuilder, SdistBuilder, WheelBuilder] # type: ignore[list-item] ================================================ FILE: backend/src/hatchling/builders/plugin/interface.py ================================================ from __future__ import annotations import os import re from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Generic, cast from hatchling.builders.config import BuilderConfig, BuilderConfigBound, env_var_enabled from hatchling.builders.constants import EXCLUDED_DIRECTORIES, EXCLUDED_FILES, BuildEnvVars from hatchling.builders.utils import get_relative_path, safe_walk from hatchling.plugin.manager import PluginManagerBound if TYPE_CHECKING: from collections.abc import Callable, Generator, Iterable from hatchling.bridge.app import Application from hatchling.builders.hooks.plugin.interface import BuildHookInterface from hatchling.metadata.core import ProjectMetadata class IncludedFile: __slots__ = ("distribution_path", "path", "relative_path") def __init__(self, path: str, relative_path: str, distribution_path: str) -> None: self.path = path self.relative_path = relative_path self.distribution_path = distribution_path class BuilderInterface(ABC, Generic[BuilderConfigBound, PluginManagerBound]): """ Example usage: ```python tab="plugin.py" from hatchling.builders.plugin.interface import BuilderInterface class SpecialBuilder(BuilderInterface): PLUGIN_NAME = "special" ... ``` ```python tab="hooks.py" from hatchling.plugin import hookimpl from .plugin import SpecialBuilder @hookimpl def hatch_register_builder(): return SpecialBuilder ``` """ PLUGIN_NAME = "" """The name used for selection.""" def __init__( self, root: str, plugin_manager: PluginManagerBound | None = None, config: dict[str, Any] | None = None, metadata: ProjectMetadata | None = None, app: Application | None = None, ) -> None: self.__root = root self.__plugin_manager = cast(PluginManagerBound, plugin_manager) self.__raw_config = config self.__metadata = metadata self.__app = app self.__config = cast(BuilderConfigBound, None) self.__project_config: dict[str, Any] | None = None self.__hatch_config: dict[str, Any] | None = None self.__build_config: dict[str, Any] | None = None self.__build_targets: list[str] | None = None self.__target_config: dict[str, Any] | None = None # Metadata self.__project_id: str | None = None def build( self, *, directory: str | None = None, versions: list[str] | None = None, hooks_only: bool | None = None, clean: bool | None = None, clean_hooks_after: bool | None = None, clean_only: bool | None = False, ) -> Generator[str, None, None]: # Fail early for invalid project metadata self.metadata.validate_fields() if directory is None: directory = ( self.config.normalize_build_directory(os.environ[BuildEnvVars.LOCATION]) if BuildEnvVars.LOCATION in os.environ else self.config.directory ) if not os.path.isdir(directory): os.makedirs(directory) version_api = self.get_version_api() versions = versions or self.config.versions if versions: unknown_versions = set(versions) - set(version_api) if unknown_versions: message = ( f"Unknown versions for target `{self.PLUGIN_NAME}`: {', '.join(map(str, sorted(unknown_versions)))}" ) raise ValueError(message) if hooks_only is None: hooks_only = env_var_enabled(BuildEnvVars.HOOKS_ONLY) configured_build_hooks = self.get_build_hooks(directory) build_hooks = list(configured_build_hooks.values()) if clean_only: clean = True elif clean is None: clean = env_var_enabled(BuildEnvVars.CLEAN) if clean: if not hooks_only: self.clean(directory, versions) for build_hook in build_hooks: build_hook.clean(versions) if clean_only: return if clean_hooks_after is None: clean_hooks_after = env_var_enabled(BuildEnvVars.CLEAN_HOOKS_AFTER) for version in versions: self.app.display_debug(f"Building `{self.PLUGIN_NAME}` version `{version}`") build_data = self.get_default_build_data() self.set_build_data_defaults(build_data) # Allow inspection of configured build hooks and the order in which they run build_data["build_hooks"] = tuple(configured_build_hooks) # Execute all `initialize` build hooks for build_hook in build_hooks: build_hook.initialize(version, build_data) if hooks_only: self.app.display_debug(f"Only ran build hooks for `{self.PLUGIN_NAME}` version `{version}`") continue # Build the artifact with self.config.set_build_data(build_data): artifact = version_api[version](directory, **build_data) # Execute all `finalize` build hooks for build_hook in build_hooks: build_hook.finalize(version, build_data, artifact) if clean_hooks_after: for build_hook in build_hooks: build_hook.clean([version]) yield artifact def recurse_included_files(self) -> Iterable[IncludedFile]: """ Returns a consistently generated series of file objects for every file that should be distributed. Each file object has three `str` attributes: - `path` - the absolute path - `relative_path` - the path relative to the project root; will be an empty string for external files - `distribution_path` - the path to be distributed as """ yield from self.recurse_selected_project_files() yield from self.recurse_forced_files(self.config.get_force_include()) def recurse_selected_project_files(self) -> Iterable[IncludedFile]: if self.config.only_include: yield from self.recurse_explicit_files(self.config.only_include) else: yield from self.recurse_project_files() def recurse_project_files(self) -> Iterable[IncludedFile]: for root, dirs, files in safe_walk(self.root): relative_path = get_relative_path(root, self.root) dirs[:] = sorted(d for d in dirs if not self.config.directory_is_excluded(d, relative_path)) files.sort() is_package = "__init__.py" in files for f in files: if f in EXCLUDED_FILES: continue relative_file_path = os.path.join(relative_path, f) distribution_path = self.config.get_distribution_path(relative_file_path) if self.config.path_is_reserved(distribution_path): continue if self.config.include_path(relative_file_path, is_package=is_package): yield IncludedFile( os.path.join(root, f), relative_file_path, self.config.get_distribution_path(relative_file_path) ) def recurse_forced_files(self, inclusion_map: dict[str, str]) -> Iterable[IncludedFile]: for source, target_path in inclusion_map.items(): external = not source.startswith(self.root) if os.path.isfile(source): yield IncludedFile( source, "" if external else os.path.relpath(source, self.root), self.config.get_distribution_path(target_path), ) elif os.path.isdir(source): for root, dirs, files in safe_walk(source): relative_directory = get_relative_path(root, source) dirs[:] = sorted(d for d in dirs if d not in EXCLUDED_DIRECTORIES) files.sort() for f in files: if f in EXCLUDED_FILES: continue relative_file_path = os.path.join(target_path, relative_directory, f) distribution_path = self.config.get_distribution_path(relative_file_path) if not self.config.path_is_reserved(distribution_path): yield IncludedFile( os.path.join(root, f), "" if external else relative_file_path, distribution_path, ) else: msg = f"Forced include not found: {source}" raise FileNotFoundError(msg) def recurse_explicit_files(self, inclusion_map: dict[str, str]) -> Iterable[IncludedFile]: for source, target_path in inclusion_map.items(): external = not source.startswith(self.root) if os.path.isfile(source): distribution_path = self.config.get_distribution_path(target_path) if not self.config.path_is_reserved(distribution_path): yield IncludedFile( source, "" if external else os.path.relpath(source, self.root), self.config.get_distribution_path(target_path), ) elif os.path.isdir(source): for root, dirs, files in safe_walk(source): relative_directory = get_relative_path(root, source) dirs[:] = sorted(d for d in dirs if d not in EXCLUDED_DIRECTORIES) files.sort() is_package = "__init__.py" in files for f in files: if f in EXCLUDED_FILES: continue relative_file_path = os.path.join(target_path, relative_directory, f) distribution_path = self.config.get_distribution_path(relative_file_path) if self.config.path_is_reserved(distribution_path): continue if self.config.include_path(relative_file_path, explicit=True, is_package=is_package): yield IncludedFile( os.path.join(root, f), "" if external else relative_file_path, distribution_path ) @property def root(self) -> str: """ The root of the project tree. """ return self.__root @property def plugin_manager(self) -> PluginManagerBound: if self.__plugin_manager is None: from hatchling.plugin.manager import PluginManager self.__plugin_manager = PluginManager() return self.__plugin_manager @property def metadata(self) -> ProjectMetadata: if self.__metadata is None: from hatchling.metadata.core import ProjectMetadata self.__metadata = ProjectMetadata(self.root, self.plugin_manager, self.__raw_config) return self.__metadata @property def app(self) -> Application: """ An instance of [Application](../utilities.md#hatchling.bridge.app.Application). """ if self.__app is None: from hatchling.bridge.app import Application self.__app = cast(Application, Application().get_safe_application()) return self.__app @property def raw_config(self) -> dict[str, Any]: if self.__raw_config is None: self.__raw_config = self.metadata.config return self.__raw_config @property def project_config(self) -> dict[str, Any]: if self.__project_config is None: self.__project_config = self.metadata.core.config return self.__project_config @property def hatch_config(self) -> dict[str, Any]: if self.__hatch_config is None: self.__hatch_config = self.metadata.hatch.config return self.__hatch_config @property def config(self) -> BuilderConfigBound: """ An instance of [BuilderConfig](../utilities.md#hatchling.builders.config.BuilderConfig). """ if self.__config is None: self.__config = self.get_config_class()( self, self.root, self.PLUGIN_NAME, self.build_config, self.target_config ) return self.__config @property def build_config(self) -> dict[str, Any]: """ ```toml config-example [tool.hatch.build] ``` """ if self.__build_config is None: self.__build_config = self.metadata.hatch.build_config return self.__build_config @property def target_config(self) -> dict[str, Any]: """ ```toml config-example [tool.hatch.build.targets.] ``` """ if self.__target_config is None: target_config: dict[str, Any] = self.metadata.hatch.build_targets.get(self.PLUGIN_NAME, {}) if not isinstance(target_config, dict): message = f"Field `tool.hatch.build.targets.{self.PLUGIN_NAME}` must be a table" raise TypeError(message) self.__target_config = target_config return self.__target_config @property def project_id(self) -> str: if self.__project_id is None: self.__project_id = f"{self.normalize_file_name_component(self.metadata.core.name)}-{self.metadata.version}" return self.__project_id def get_build_hooks(self, directory: str) -> dict[str, BuildHookInterface]: configured_build_hooks = {} for hook_name, config in self.config.hook_config.items(): build_hook = self.plugin_manager.build_hook.get(hook_name) if build_hook is None: from hatchling.plugin.exceptions import UnknownPluginError message = f"Unknown build hook: {hook_name}" raise UnknownPluginError(message) configured_build_hooks[hook_name] = build_hook( self.root, config, self.config, self.metadata, directory, self.PLUGIN_NAME, self.app ) return configured_build_hooks @abstractmethod def get_version_api(self) -> dict[str, Callable]: """ A mapping of `str` versions to a callable that is used for building. Each callable must have the following signature: ```python def ...(build_dir: str, build_data: dict) -> str: ``` The return value must be the absolute path to the built artifact. """ def get_default_versions(self) -> list[str]: """ A list of versions to build when users do not specify any, defaulting to all versions. """ return list(self.get_version_api()) def get_default_build_data(self) -> dict[str, Any]: # noqa: PLR6301 """ A mapping that can be modified by [build hooks](../build-hook/reference.md) to influence the behavior of builds. """ return {} def set_build_data_defaults(self, build_data: dict[str, Any]) -> None: # noqa: PLR6301 build_data.setdefault("artifacts", []) build_data.setdefault("force_include", {}) def clean(self, directory: str, versions: list[str]) -> None: """ Called before builds if the `-c`/`--clean` flag was passed to the [`build`](../../cli/reference.md#hatch-build) command. """ @classmethod def get_config_class(cls) -> type[BuilderConfig]: """ Must return a subclass of [BuilderConfig](../utilities.md#hatchling.builders.config.BuilderConfig). """ return BuilderConfig @staticmethod def normalize_file_name_component(file_name: str) -> str: """ https://peps.python.org/pep-0427/#escaping-and-unicode """ return re.sub(r"[^\w\d.]+", "_", file_name, flags=re.UNICODE) ================================================ FILE: backend/src/hatchling/builders/sdist.py ================================================ from __future__ import annotations import gzip import os import tarfile import tempfile from contextlib import closing from copy import copy from io import BytesIO from time import time as get_current_timestamp from typing import TYPE_CHECKING, Any from hatchling.builders.config import BuilderConfig from hatchling.builders.plugin.interface import BuilderInterface from hatchling.builders.utils import ( get_reproducible_timestamp, normalize_archive_path, normalize_artifact_permissions, normalize_file_permissions, normalize_relative_path, replace_file, ) from hatchling.metadata.spec import DEFAULT_METADATA_VERSION, get_core_metadata_constructors from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT, DEFAULT_CONFIG_FILE if TYPE_CHECKING: from collections.abc import Callable from types import TracebackType class SdistArchive: def __init__(self, name: str, *, reproducible: bool) -> None: """ https://peps.python.org/pep-0517/#source-distributions """ self.name = name self.reproducible = reproducible self.timestamp: int | None = get_reproducible_timestamp() if reproducible else None raw_fd, self.path = tempfile.mkstemp(suffix=".tar.gz") self.fd = os.fdopen(raw_fd, "w+b") self.gz = gzip.GzipFile(fileobj=self.fd, mode="wb", mtime=self.timestamp) self.tf = tarfile.TarFile(fileobj=self.gz, mode="w", format=tarfile.PAX_FORMAT) self.gettarinfo = lambda *args, **kwargs: self.normalize_tar_metadata(self.tf.gettarinfo(*args, **kwargs)) def create_file(self, contents: str | bytes, *relative_paths: str) -> None: if not isinstance(contents, bytes): contents = contents.encode("utf-8") tar_info = tarfile.TarInfo(normalize_archive_path(os.path.join(self.name, *relative_paths))) tar_info.size = len(contents) if self.reproducible and self.timestamp is not None: tar_info.mtime = self.timestamp else: tar_info.mtime = int(get_current_timestamp()) with closing(BytesIO(contents)) as buffer: self.tf.addfile(tar_info, buffer) def normalize_tar_metadata(self, tar_info: tarfile.TarInfo | None) -> tarfile.TarInfo | None: if not self.reproducible or tar_info is None: return tar_info tar_info = copy(tar_info) tar_info.uid = 0 tar_info.gid = 0 tar_info.uname = "" tar_info.gname = "" tar_info.mode = normalize_file_permissions(tar_info.mode) if self.timestamp is not None: tar_info.mtime = self.timestamp return tar_info def __getattr__(self, name: str) -> Any: attr = getattr(self.tf, name) setattr(self, name, attr) return attr def __enter__(self) -> SdistArchive: # noqa: PYI034 return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None ) -> None: self.tf.close() self.gz.close() self.fd.close() class SdistBuilderConfig(BuilderConfig): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.__core_metadata_constructor: Callable[..., str] | None = None self.__strict_naming: bool | None = None self.__support_legacy: bool | None = None @property def core_metadata_constructor(self) -> Callable[..., str]: if self.__core_metadata_constructor is None: core_metadata_version = self.target_config.get("core-metadata-version", DEFAULT_METADATA_VERSION) if not isinstance(core_metadata_version, str): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.core-metadata-version` must be a string" raise TypeError(message) constructors = get_core_metadata_constructors() if core_metadata_version not in constructors: message = ( f"Unknown metadata version `{core_metadata_version}` for field " f"`tool.hatch.build.targets.{self.plugin_name}.core-metadata-version`. " f"Available: {', '.join(sorted(constructors))}" ) raise ValueError(message) self.__core_metadata_constructor = constructors[core_metadata_version] return self.__core_metadata_constructor @property def strict_naming(self) -> bool: if self.__strict_naming is None: if "strict-naming" in self.target_config: strict_naming = self.target_config["strict-naming"] if not isinstance(strict_naming, bool): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.strict-naming` must be a boolean" raise TypeError(message) else: strict_naming = self.build_config.get("strict-naming", True) if not isinstance(strict_naming, bool): message = "Field `tool.hatch.build.strict-naming` must be a boolean" raise TypeError(message) self.__strict_naming = strict_naming return self.__strict_naming @property def support_legacy(self) -> bool: if self.__support_legacy is None: self.__support_legacy = bool(self.target_config.get("support-legacy", False)) return self.__support_legacy class SdistBuilder(BuilderInterface): """ Build an archive of the source files """ PLUGIN_NAME = "sdist" def get_version_api(self) -> dict[str, Callable]: return {"standard": self.build_standard} def get_default_versions(self) -> list[str]: # noqa: PLR6301 return ["standard"] def clean( # noqa: PLR6301 self, directory: str, versions: list[str], # noqa: ARG002 ) -> None: for filename in os.listdir(directory): if filename.endswith(".tar.gz"): os.remove(os.path.join(directory, filename)) def build_standard(self, directory: str, **build_data: Any) -> str: found_packages = set() with SdistArchive(self.artifact_project_id, reproducible=self.config.reproducible) as archive: for included_file in self.recurse_included_files(): if self.config.support_legacy: possible_package, file_name = os.path.split(included_file.relative_path) if file_name == "__init__.py": found_packages.add(possible_package) tar_info = archive.gettarinfo( included_file.path, arcname=normalize_archive_path( os.path.join(self.artifact_project_id, included_file.distribution_path) ), ) if tar_info is None: # no cov continue if tar_info.isfile(): with open(included_file.path, "rb") as f: archive.addfile(tar_info, f) else: # no cov # TODO: Investigate if this is necessary (for symlinks, etc.) archive.addfile(tar_info) archive.create_file( self.config.core_metadata_constructor(self.metadata, extra_dependencies=build_data["dependencies"]), "PKG-INFO", ) if self.config.support_legacy: archive.create_file( self.construct_setup_py_file(sorted(found_packages), extra_dependencies=build_data["dependencies"]), "setup.py", ) target = os.path.join(directory, f"{self.artifact_project_id}.tar.gz") replace_file(archive.path, target) normalize_artifact_permissions(target) return target @property def artifact_project_id(self) -> str: return ( self.project_id if self.config.strict_naming else f"{self.normalize_file_name_component(self.metadata.core.raw_name)}-{self.metadata.version}" ) def construct_setup_py_file(self, packages: list[str], extra_dependencies: tuple[()] = ()) -> str: contents = "from setuptools import setup\n\n" contents += "setup(\n" contents += f" name={self.metadata.core.name!r},\n" contents += f" version={self.metadata.version!r},\n" if self.metadata.core.description: contents += f" description={self.metadata.core.description!r},\n" if self.metadata.core.readme: contents += f" long_description={self.metadata.core.readme!r},\n" authors_data = self.metadata.core.authors_data if authors_data["name"]: contents += f" author={', '.join(authors_data['name'])!r},\n" if authors_data["email"]: contents += f" author_email={', '.join(authors_data['email'])!r},\n" maintainers_data = self.metadata.core.maintainers_data if maintainers_data["name"]: contents += f" maintainer={', '.join(maintainers_data['name'])!r},\n" if maintainers_data["email"]: contents += f" maintainer_email={', '.join(maintainers_data['email'])!r},\n" if self.metadata.core.classifiers: contents += " classifiers=[\n" for classifier in self.metadata.core.classifiers: contents += f" {classifier!r},\n" contents += " ],\n" dependencies = list(self.metadata.core.dependencies) dependencies.extend(extra_dependencies) if dependencies: contents += " install_requires=[\n" for raw_specifier in dependencies: specifier = raw_specifier.replace("'", '"') contents += f" {specifier!r},\n" contents += " ],\n" if self.metadata.core.optional_dependencies: contents += " extras_require={\n" for option, specifiers in self.metadata.core.optional_dependencies.items(): if not specifiers: continue contents += f" {option!r}: [\n" for raw_specifier in specifiers: specifier = raw_specifier.replace("'", '"') contents += f" {specifier!r},\n" contents += " ],\n" contents += " },\n" if self.metadata.core.scripts or self.metadata.core.gui_scripts or self.metadata.core.entry_points: contents += " entry_points={\n" if self.metadata.core.scripts: contents += " 'console_scripts': [\n" for name, object_ref in self.metadata.core.scripts.items(): contents += f" '{name} = {object_ref}',\n" contents += " ],\n" if self.metadata.core.gui_scripts: contents += " 'gui_scripts': [\n" for name, object_ref in self.metadata.core.gui_scripts.items(): contents += f" '{name} = {object_ref}',\n" contents += " ],\n" if self.metadata.core.entry_points: for group, entry_points in self.metadata.core.entry_points.items(): contents += f" {group!r}: [\n" for name, object_ref in entry_points.items(): contents += f" '{name} = {object_ref}',\n" contents += " ],\n" contents += " },\n" if packages: src_layout = False contents += " packages=[\n" for package in packages: if package.startswith(f"src{os.sep}"): src_layout = True contents += f" {package.replace(os.sep, '.')[4:]!r},\n" else: contents += f" {package.replace(os.sep, '.')!r},\n" contents += " ],\n" if src_layout: contents += " package_dir={'': 'src'},\n" contents += ")\n" return contents def get_default_build_data(self) -> dict[str, Any]: force_include = {} for filename in ["pyproject.toml", DEFAULT_CONFIG_FILE, DEFAULT_BUILD_SCRIPT]: path = os.path.join(self.root, filename) if os.path.exists(path): force_include[path] = filename build_data = {"force_include": force_include, "dependencies": []} for exclusion_files in self.config.vcs_exclusion_files.values(): for exclusion_file in exclusion_files: force_include[exclusion_file] = os.path.basename(exclusion_file) readme_path = self.metadata.core.readme_path if readme_path: readme_path = normalize_relative_path(readme_path) force_include[os.path.join(self.root, readme_path)] = readme_path license_files = self.metadata.core.license_files if license_files: for license_file in license_files: relative_path = normalize_relative_path(license_file) force_include[os.path.join(self.root, relative_path)] = relative_path return build_data @classmethod def get_config_class(cls) -> type[SdistBuilderConfig]: return SdistBuilderConfig ================================================ FILE: backend/src/hatchling/builders/utils.py ================================================ from __future__ import annotations import os import shutil from base64 import urlsafe_b64encode from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Iterable from zipfile import ZipInfo def replace_file(src: str, dst: str) -> None: try: os.replace(src, dst) # Happens when on different filesystems like /tmp or caused by layering in containers except OSError: shutil.copy2(src, dst) os.remove(src) def safe_walk(path: str) -> Iterable[tuple[str, list[str], list[str]]]: seen = set() for root, dirs, files in os.walk(path, followlinks=True): stat = os.stat(root) identifier = stat.st_dev, stat.st_ino if identifier in seen: del dirs[:] continue seen.add(identifier) yield root, dirs, files def get_known_python_major_versions() -> map: return map(str, sorted((2, 3))) def get_relative_path(path: str, start: str) -> str: relative_path = os.path.relpath(path, start) # First iteration of `os.walk` if relative_path == ".": return "" return relative_path def normalize_relative_path(path: str) -> str: return os.path.normpath(path).strip(os.sep) def normalize_relative_directory(path: str) -> str: return normalize_relative_path(path) + os.sep def normalize_inclusion_map(inclusion_map: dict[str, str], root: str) -> dict[str, str]: normalized_inclusion_map = {} for raw_source, relative_path in inclusion_map.items(): source = os.path.expanduser(os.path.normpath(raw_source)) if not os.path.isabs(source): source = os.path.abspath(os.path.join(root, source)) normalized_inclusion_map[source] = normalize_relative_path(relative_path) return dict( sorted( normalized_inclusion_map.items(), key=lambda item: (item[1].count(os.sep), item[1], item[0]), ) ) def normalize_archive_path(path: str) -> str: if os.sep != "/": return path.replace(os.sep, "/") return path def format_file_hash(digest: bytes) -> str: # https://peps.python.org/pep-0427/#signed-wheel-files return urlsafe_b64encode(digest).decode("ascii").rstrip("=") def get_reproducible_timestamp() -> int: """ Returns an `int` derived from the `SOURCE_DATE_EPOCH` environment variable; see https://reproducible-builds.org/specs/source-date-epoch/. The default value will always be: `1580601600` """ return int(os.environ.get("SOURCE_DATE_EPOCH", "1580601600")) def normalize_file_permissions(st_mode: int) -> int: """ https://github.com/takluyver/flit/blob/6a2a8c6462e49f584941c667b70a6f48a7b3f9ab/flit_core/flit_core/common.py#L257 Normalize the permission bits in the st_mode field from stat to 644/755. Popular VCSs only track whether a file is executable or not. The exact permissions can vary on systems with different umasks. Normalizing to 644 (non executable) or 755 (executable) makes builds more reproducible. """ # Set 644 permissions, leaving higher bits of st_mode unchanged new_mode = (st_mode | 0o644) & ~0o133 if st_mode & 0o100: # no cov new_mode |= 0o111 # Executable: 644 -> 755 return new_mode def normalize_artifact_permissions(path: str) -> None: """ Normalize the permission bits for artifacts """ file_stat = os.stat(path) new_mode = normalize_file_permissions(file_stat.st_mode) os.chmod(path, new_mode) def set_zip_info_mode(zip_info: ZipInfo, mode: int = 0o644) -> None: """ https://github.com/python/cpython/blob/v3.12.3/Lib/zipfile/__init__.py#L574 https://github.com/takluyver/flit/commit/3889583719888aef9f28baaa010e698cb7884904 """ zip_info.external_attr = (mode & 0xFFFF) << 16 ================================================ FILE: backend/src/hatchling/builders/wheel.py ================================================ from __future__ import annotations import csv import hashlib import os import stat import sys import tempfile import zipfile from functools import cached_property from io import StringIO from typing import TYPE_CHECKING, Any, NamedTuple, cast from hatchling.__about__ import __version__ from hatchling.builders.config import BuilderConfig from hatchling.builders.constants import EDITABLES_REQUIREMENT from hatchling.builders.plugin.interface import BuilderInterface from hatchling.builders.utils import ( format_file_hash, get_known_python_major_versions, get_reproducible_timestamp, normalize_archive_path, normalize_artifact_permissions, normalize_file_permissions, normalize_inclusion_map, replace_file, set_zip_info_mode, ) from hatchling.metadata.spec import DEFAULT_METADATA_VERSION, get_core_metadata_constructors if TYPE_CHECKING: from collections.abc import Callable, Iterable, Sequence from types import TracebackType from hatchling.builders.plugin.interface import IncludedFile TIME_TUPLE = tuple[int, int, int, int, int, int] class FileSelectionOptions(NamedTuple): include: list[str] exclude: list[str] packages: list[str] only_include: list[str] class RecordFile: def __init__(self) -> None: self.__file_obj = StringIO() self.__writer = csv.writer(self.__file_obj, delimiter=",", quotechar='"', lineterminator="\n") def write(self, record: Iterable[Any]) -> None: self.__writer.writerow(record) def construct(self) -> str: return self.__file_obj.getvalue() def __enter__(self) -> RecordFile: # noqa: PYI034 return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None ) -> None: self.__file_obj.close() class WheelArchive: def __init__(self, project_id: str, *, reproducible: bool) -> None: """ https://peps.python.org/pep-0427/#abstract """ self.metadata_directory = f"{project_id}.dist-info" self.shared_data_directory = f"{project_id}.data" self.time_tuple: TIME_TUPLE | None = None self.reproducible = reproducible if self.reproducible: self.time_tuple = self.get_reproducible_time_tuple() else: self.time_tuple = None raw_fd, self.path = tempfile.mkstemp(suffix=".whl") self.fd = os.fdopen(raw_fd, "w+b") self.zf = zipfile.ZipFile(self.fd, "w", compression=zipfile.ZIP_DEFLATED) @staticmethod def get_reproducible_time_tuple() -> TIME_TUPLE: from datetime import datetime, timezone # `zipfile.ZipInfo` does not support timestamps before 1980 min_ts = 315532800 # 1980-01-01T00:00:00Z d = datetime.fromtimestamp(max(get_reproducible_timestamp(), min_ts), timezone.utc) return d.year, d.month, d.day, d.hour, d.minute, d.second def add_file(self, included_file: IncludedFile) -> tuple[str, str, str]: relative_path = normalize_archive_path(included_file.distribution_path) file_stat = os.stat(included_file.path) if self.reproducible: zip_info = zipfile.ZipInfo(relative_path, cast(TIME_TUPLE, self.time_tuple)) # https://github.com/takluyver/flit/pull/66 new_mode = normalize_file_permissions(file_stat.st_mode) set_zip_info_mode(zip_info, new_mode) if stat.S_ISDIR(file_stat.st_mode): # no cov zip_info.external_attr |= 0x10 else: zip_info.file_size = file_stat.st_size else: zip_info = zipfile.ZipInfo.from_file(included_file.path, relative_path) zip_info.compress_type = zipfile.ZIP_DEFLATED hash_obj = hashlib.sha256() with open(included_file.path, "rb") as in_file, self.zf.open(zip_info, "w") as out_file: while True: chunk = in_file.read(16384) if not chunk: break hash_obj.update(chunk) out_file.write(chunk) hash_digest = format_file_hash(hash_obj.digest()) return relative_path, f"sha256={hash_digest}", str(file_stat.st_size) def write_metadata(self, relative_path: str, contents: str | bytes) -> tuple[str, str, str]: relative_path = f"{self.metadata_directory}/{normalize_archive_path(relative_path)}" return self.write_file(relative_path, contents) def write_shared_script(self, included_file: IncludedFile, contents: str | bytes) -> tuple[str, str, str]: relative_path = ( f"{self.shared_data_directory}/scripts/{normalize_archive_path(included_file.distribution_path)}" ) if sys.platform == "win32": return self.write_file(relative_path, contents) file_stat = os.stat(included_file.path) return self.write_file( relative_path, contents, mode=normalize_file_permissions(file_stat.st_mode) if self.reproducible else file_stat.st_mode, ) def add_shared_file(self, shared_file: IncludedFile) -> tuple[str, str, str]: shared_file.distribution_path = f"{self.shared_data_directory}/data/{shared_file.distribution_path}" return self.add_file(shared_file) def add_extra_metadata_file(self, extra_metadata_file: IncludedFile) -> tuple[str, str, str]: extra_metadata_file.distribution_path = ( f"{self.metadata_directory}/extra_metadata/{extra_metadata_file.distribution_path}" ) return self.add_file(extra_metadata_file) def add_sbom_file(self, sbom_file: IncludedFile) -> tuple[str, str, str]: """Add SBOM file to .dist-info/sboms/ directory.""" sbom_file.distribution_path = f"{self.metadata_directory}/sboms/{sbom_file.distribution_path}" return self.add_file(sbom_file) def write_file( self, relative_path: str, contents: str | bytes, *, mode: int | None = None, ) -> tuple[str, str, str]: if not isinstance(contents, bytes): contents = contents.encode("utf-8") time_tuple = self.time_tuple or (2020, 2, 2, 0, 0, 0) zip_info = zipfile.ZipInfo(relative_path, time_tuple) if mode is None: set_zip_info_mode(zip_info) else: set_zip_info_mode(zip_info, mode) hash_obj = hashlib.sha256(contents) hash_digest = format_file_hash(hash_obj.digest()) self.zf.writestr(zip_info, contents, compress_type=zipfile.ZIP_DEFLATED) return relative_path, f"sha256={hash_digest}", str(len(contents)) def __enter__(self) -> WheelArchive: # noqa: PYI034 return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None ) -> None: self.zf.close() self.fd.close() class WheelBuilderConfig(BuilderConfig): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.__core_metadata_constructor: Callable[..., str] | None = None self.__shared_data: dict[str, str] | None = None self.__shared_scripts: dict[str, str] | None = None self.__extra_metadata: dict[str, str] | None = None self.__strict_naming: bool | None = None self.__macos_max_compat: bool | None = None @cached_property def default_file_selection_options(self) -> FileSelectionOptions: include = self.target_config.get("include", self.build_config.get("include", [])) exclude = self.target_config.get("exclude", self.build_config.get("exclude", [])) packages = self.target_config.get("packages", self.build_config.get("packages", [])) only_include = self.target_config.get("only-include", self.build_config.get("only-include", [])) if include or packages or only_include: return FileSelectionOptions(include, exclude, packages, only_include) project_names: set[str] = set() for project_name in ( self.builder.normalize_file_name_component(self.builder.metadata.core.raw_name), self.builder.normalize_file_name_component(self.builder.metadata.core.name), ): if os.path.isfile(os.path.join(self.root, project_name, "__init__.py")): normalized_project_name = self.get_raw_fs_path_name(self.root, project_name) return FileSelectionOptions([], exclude, [normalized_project_name], []) if os.path.isfile(os.path.join(self.root, "src", project_name, "__init__.py")): normalized_project_name = self.get_raw_fs_path_name(os.path.join(self.root, "src"), project_name) return FileSelectionOptions([], exclude, [f"src/{normalized_project_name}"], []) module_file = f"{project_name}.py" if os.path.isfile(os.path.join(self.root, module_file)): return FileSelectionOptions([], exclude, [], [module_file]) from glob import glob possible_namespace_packages = glob(os.path.join(self.root, "*", project_name, "__init__.py")) if len(possible_namespace_packages) == 1: relative_path = os.path.relpath(possible_namespace_packages[0], self.root) namespace = relative_path.split(os.sep)[0] return FileSelectionOptions([], exclude, [namespace], []) project_names.add(project_name) if self.bypass_selection or self.build_artifact_spec is not None or self.get_force_include(): self.set_exclude_all() return FileSelectionOptions([], exclude, [], []) project_names_text = " or ".join(sorted(project_names)) message = ( f"Unable to determine which files to ship inside the wheel using the following heuristics: " f"https://hatch.pypa.io/latest/plugins/builder/wheel/#default-file-selection\n\n" f"The most likely cause of this is that there is no directory that matches the name of your " f"project ({project_names_text}).\n\n" f"At least one file selection option must be defined in the `tool.hatch.build.targets.wheel` " f"table, see: https://hatch.pypa.io/latest/config/build/\n\n" f"As an example, if you intend to ship a directory named `foo` that resides within a `src` " f"directory located at the root of your project, you can define the following:\n\n" f"[tool.hatch.build.targets.wheel]\n" f'packages = ["src/foo"]' ) raise ValueError(message) def default_include(self) -> list[str]: return self.default_file_selection_options.include def default_exclude(self) -> list[str]: return self.default_file_selection_options.exclude def default_packages(self) -> list[str]: return self.default_file_selection_options.packages def default_only_include(self) -> list[str]: return self.default_file_selection_options.only_include @property def core_metadata_constructor(self) -> Callable[..., str]: if self.__core_metadata_constructor is None: core_metadata_version = self.target_config.get("core-metadata-version", DEFAULT_METADATA_VERSION) if not isinstance(core_metadata_version, str): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.core-metadata-version` must be a string" raise TypeError(message) constructors = get_core_metadata_constructors() if core_metadata_version not in constructors: message = ( f"Unknown metadata version `{core_metadata_version}` for field " f"`tool.hatch.build.targets.{self.plugin_name}.core-metadata-version`. " f"Available: {', '.join(sorted(constructors))}" ) raise ValueError(message) self.__core_metadata_constructor = constructors[core_metadata_version] return self.__core_metadata_constructor @property def shared_data(self) -> dict[str, str]: if self.__shared_data is None: shared_data = self.target_config.get("shared-data", {}) if not isinstance(shared_data, dict): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.shared-data` must be a mapping" raise TypeError(message) for i, (source, relative_path) in enumerate(shared_data.items(), 1): if not source: message = ( f"Source #{i} in field `tool.hatch.build.targets.{self.plugin_name}.shared-data` " f"cannot be an empty string" ) raise ValueError(message) if not isinstance(relative_path, str): message = ( f"Path for source `{source}` in field " f"`tool.hatch.build.targets.{self.plugin_name}.shared-data` must be a string" ) raise TypeError(message) if not relative_path: message = ( f"Path for source `{source}` in field " f"`tool.hatch.build.targets.{self.plugin_name}.shared-data` cannot be an empty string" ) raise ValueError(message) self.__shared_data = normalize_inclusion_map(shared_data, self.root) return self.__shared_data @property def shared_scripts(self) -> dict[str, str]: if self.__shared_scripts is None: shared_scripts = self.target_config.get("shared-scripts", {}) if not isinstance(shared_scripts, dict): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.shared-scripts` must be a mapping" raise TypeError(message) for i, (source, relative_path) in enumerate(shared_scripts.items(), 1): if not source: message = ( f"Source #{i} in field `tool.hatch.build.targets.{self.plugin_name}.shared-scripts` " f"cannot be an empty string" ) raise ValueError(message) if not isinstance(relative_path, str): message = ( f"Path for source `{source}` in field " f"`tool.hatch.build.targets.{self.plugin_name}.shared-scripts` must be a string" ) raise TypeError(message) if not relative_path: message = ( f"Path for source `{source}` in field " f"`tool.hatch.build.targets.{self.plugin_name}.shared-scripts` cannot be an empty string" ) raise ValueError(message) self.__shared_scripts = normalize_inclusion_map(shared_scripts, self.root) return self.__shared_scripts @property def extra_metadata(self) -> dict[str, str]: if self.__extra_metadata is None: extra_metadata = self.target_config.get("extra-metadata", {}) if not isinstance(extra_metadata, dict): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.extra-metadata` must be a mapping" raise TypeError(message) for i, (source, relative_path) in enumerate(extra_metadata.items(), 1): if not source: message = ( f"Source #{i} in field `tool.hatch.build.targets.{self.plugin_name}.extra-metadata` " f"cannot be an empty string" ) raise ValueError(message) if not isinstance(relative_path, str): message = ( f"Path for source `{source}` in field " f"`tool.hatch.build.targets.{self.plugin_name}.extra-metadata` must be a string" ) raise TypeError(message) if not relative_path: message = ( f"Path for source `{source}` in field " f"`tool.hatch.build.targets.{self.plugin_name}.extra-metadata` cannot be an empty string" ) raise ValueError(message) self.__extra_metadata = normalize_inclusion_map(extra_metadata, self.root) return self.__extra_metadata @property def sbom_files(self) -> list[str]: """ https://peps.python.org/pep-0770/ """ sbom_files = self.target_config.get("sbom-files", []) if not isinstance(sbom_files, list): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.sbom-files` must be an array" raise TypeError(message) for i, sbom_file in enumerate(sbom_files, 1): if not isinstance(sbom_file, str): message = ( f"SBOM file #{i} in field `tool.hatch.build.targets.{self.plugin_name}.sbom-files` must be a string" ) raise TypeError(message) return sbom_files @property def strict_naming(self) -> bool: if self.__strict_naming is None: if "strict-naming" in self.target_config: strict_naming = self.target_config["strict-naming"] if not isinstance(strict_naming, bool): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.strict-naming` must be a boolean" raise TypeError(message) else: strict_naming = self.build_config.get("strict-naming", True) if not isinstance(strict_naming, bool): message = "Field `tool.hatch.build.strict-naming` must be a boolean" raise TypeError(message) self.__strict_naming = strict_naming return self.__strict_naming @property def macos_max_compat(self) -> bool: if self.__macos_max_compat is None: macos_max_compat = self.target_config.get("macos-max-compat", False) if not isinstance(macos_max_compat, bool): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.macos-max-compat` must be a boolean" raise TypeError(message) self.__macos_max_compat = macos_max_compat return self.__macos_max_compat @cached_property def bypass_selection(self) -> bool: bypass_selection = self.target_config.get("bypass-selection", False) if not isinstance(bypass_selection, bool): message = f"Field `tool.hatch.build.targets.{self.plugin_name}.bypass-selection` must be a boolean" raise TypeError(message) return bypass_selection if sys.platform in {"darwin", "win32"}: @staticmethod def get_raw_fs_path_name(directory: str, name: str) -> str: normalized = name.casefold() entries = os.listdir(directory) for entry in entries: if entry.casefold() == normalized: return entry return name # no cov else: @staticmethod def get_raw_fs_path_name(directory: str, name: str) -> str: # noqa: ARG004 return name class WheelBuilder(BuilderInterface): """ Build a binary distribution (.whl file) """ PLUGIN_NAME = "wheel" def get_version_api(self) -> dict[str, Callable]: return {"standard": self.build_standard, "editable": self.build_editable} def get_default_versions(self) -> list[str]: # noqa: PLR6301 return ["standard"] def clean( # noqa: PLR6301 self, directory: str, versions: list[str], # noqa: ARG002 ) -> None: for filename in os.listdir(directory): if filename.endswith(".whl"): os.remove(os.path.join(directory, filename)) def build_standard(self, directory: str, **build_data: Any) -> str: if "tag" not in build_data: if build_data["infer_tag"]: build_data["tag"] = self.get_best_matching_tag() else: build_data["tag"] = self.get_default_tag() with ( WheelArchive(self.artifact_project_id, reproducible=self.config.reproducible) as archive, RecordFile() as records, ): for included_file in self.recurse_included_files(): record = archive.add_file(included_file) records.write(record) self.write_data(archive, records, build_data, build_data["dependencies"]) records.write((f"{archive.metadata_directory}/RECORD", "", "")) archive.write_metadata("RECORD", records.construct()) target = os.path.join(directory, f"{self.artifact_project_id}-{build_data['tag']}.whl") replace_file(archive.path, target) normalize_artifact_permissions(target) return target def build_editable(self, directory: str, **build_data: Any) -> str: if self.config.dev_mode_dirs: return self.build_editable_explicit(directory, **build_data) return self.build_editable_detection(directory, **build_data) def build_editable_detection(self, directory: str, **build_data: Any) -> str: from editables import EditableProject build_data["tag"] = self.get_default_tag() with ( WheelArchive(self.artifact_project_id, reproducible=self.config.reproducible) as archive, RecordFile() as records, ): exposed_packages = {} for included_file in self.recurse_selected_project_files(): if not included_file.path.endswith(".py"): continue relative_path = included_file.relative_path distribution_path = included_file.distribution_path path_parts = relative_path.split(os.sep) # Root file if len(path_parts) == 1: # no cov exposed_packages[os.path.splitext(relative_path)[0]] = os.path.join(self.root, relative_path) continue # Root package root_module = path_parts[0] if distribution_path == relative_path: exposed_packages[root_module] = os.path.join(self.root, root_module) else: distribution_module = distribution_path.split(os.sep)[0] try: exposed_packages[distribution_module] = os.path.join( self.root, f"{relative_path[: relative_path.index(distribution_path)]}{distribution_module}", ) except ValueError: message = ( "Dev mode installations are unsupported when any path rewrite in the `sources` option " "changes a prefix rather than removes it, see: " "https://github.com/pfmoore/editables/issues/20" ) raise ValueError(message) from None editable_project = EditableProject(self.metadata.core.name, self.root) if self.config.dev_mode_exact: for module, relative_path in exposed_packages.items(): editable_project.map(module, relative_path) else: for relative_path in exposed_packages.values(): editable_project.add_to_path(os.path.dirname(relative_path)) for raw_filename, content in sorted(editable_project.files()): filename = raw_filename if filename.endswith(".pth") and not filename.startswith("_"): filename = f"_{filename}" record = archive.write_file(filename, content) records.write(record) for included_file in self.recurse_forced_files(self.get_forced_inclusion_map(build_data)): record = archive.add_file(included_file) records.write(record) extra_dependencies = list(build_data["dependencies"]) for raw_dependency in editable_project.dependencies(): dependency = raw_dependency if dependency == "editables": dependency = EDITABLES_REQUIREMENT else: # no cov pass extra_dependencies.append(dependency) self.write_data(archive, records, build_data, extra_dependencies) records.write((f"{archive.metadata_directory}/RECORD", "", "")) archive.write_metadata("RECORD", records.construct()) target = os.path.join(directory, f"{self.artifact_project_id}-{build_data['tag']}.whl") replace_file(archive.path, target) normalize_artifact_permissions(target) return target def build_editable_explicit(self, directory: str, **build_data: Any) -> str: build_data["tag"] = self.get_default_tag() with ( WheelArchive(self.artifact_project_id, reproducible=self.config.reproducible) as archive, RecordFile() as records, ): directories = sorted( os.path.normpath(os.path.join(self.root, relative_directory)) for relative_directory in self.config.dev_mode_dirs ) record = archive.write_file(f"_{self.metadata.core.name.replace('-', '_')}.pth", "\n".join(directories)) records.write(record) for included_file in self.recurse_forced_files(self.get_forced_inclusion_map(build_data)): record = archive.add_file(included_file) records.write(record) self.write_data(archive, records, build_data, build_data["dependencies"]) records.write((f"{archive.metadata_directory}/RECORD", "", "")) archive.write_metadata("RECORD", records.construct()) target = os.path.join(directory, f"{self.artifact_project_id}-{build_data['tag']}.whl") replace_file(archive.path, target) normalize_artifact_permissions(target) return target def write_data( self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any], extra_dependencies: Sequence[str] ) -> None: self.add_shared_data(archive, records, build_data) self.add_shared_scripts(archive, records, build_data) # Ensure metadata is written last, see https://peps.python.org/pep-0427/#recommended-archiver-features self.write_metadata(archive, records, build_data, extra_dependencies=extra_dependencies) def add_shared_data(self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any]) -> None: shared_data = dict(self.config.shared_data) shared_data.update(normalize_inclusion_map(build_data["shared_data"], self.root)) for shared_file in self.recurse_explicit_files(shared_data): record = archive.add_shared_file(shared_file) records.write(record) def add_shared_scripts(self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any]) -> None: import re from io import BytesIO # https://packaging.python.org/en/latest/specifications/binary-distribution-format/#recommended-installer-features shebang = re.compile(rb"^#!.*(?:pythonw?|pypyw?)[0-9.]*(.*)", flags=re.DOTALL) shared_scripts = dict(self.config.shared_scripts) shared_scripts.update(normalize_inclusion_map(build_data["shared_scripts"], self.root)) for shared_script in self.recurse_explicit_files(shared_scripts): with open(shared_script.path, "rb") as f: content = BytesIO() for line in f: # Ignore leading blank lines if not line.strip(): continue match = shebang.match(line) if match is None: content.write(line) else: content.write(b"#!python") if remaining := match.group(1): content.write(remaining) content.write(f.read()) break record = archive.write_shared_script(shared_script, content.getvalue()) records.write(record) def add_sboms(self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any]) -> None: sbom_files = self.config.sbom_files sbom_files.extend(build_data["sbom_files"]) if not sbom_files: return for sbom_file in sbom_files: sbom_path = os.path.join(self.root, sbom_file) if not os.path.isfile(sbom_path): message = f"SBOM file not found: {sbom_file}" raise FileNotFoundError(message) sbom_map = {os.path.join(self.root, sbom_file): os.path.basename(sbom_file) for sbom_file in sbom_files} for included_file in self.recurse_explicit_files(sbom_map): record = archive.add_sbom_file(included_file) records.write(record) def write_metadata( self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any], extra_dependencies: Sequence[str] = (), ) -> None: # <<< IMPORTANT >>> # Ensure calls are ordered by the number of path components followed by the name of the components # METADATA self.write_project_metadata(archive, records, extra_dependencies=extra_dependencies) # WHEEL self.write_archive_metadata(archive, records, build_data) # entry_points.txt self.write_entry_points_file(archive, records) # licenses/ self.add_licenses(archive, records) # sboms/ self.add_sboms(archive, records, build_data) # extra_metadata/ - write last self.add_extra_metadata(archive, records, build_data) @staticmethod def write_archive_metadata(archive: WheelArchive, records: RecordFile, build_data: dict[str, Any]) -> None: from packaging.tags import parse_tag metadata = f"""\ Wheel-Version: 1.0 Generator: hatchling {__version__} Root-Is-Purelib: {"true" if build_data["pure_python"] else "false"} """ for tag in sorted(map(str, parse_tag(build_data["tag"]))): metadata += f"Tag: {tag}\n" record = archive.write_metadata("WHEEL", metadata) records.write(record) def write_entry_points_file(self, archive: WheelArchive, records: RecordFile) -> None: entry_points_file = self.construct_entry_points_file() if entry_points_file: record = archive.write_metadata("entry_points.txt", entry_points_file) records.write(record) def write_project_metadata( self, archive: WheelArchive, records: RecordFile, extra_dependencies: Sequence[str] = () ) -> None: record = archive.write_metadata( "METADATA", self.config.core_metadata_constructor(self.metadata, extra_dependencies=extra_dependencies) ) records.write(record) def add_licenses(self, archive: WheelArchive, records: RecordFile) -> None: for relative_path in self.metadata.core.license_files: license_file = os.path.normpath(os.path.join(self.root, relative_path)) with open(license_file, "rb") as f: record = archive.write_metadata(f"licenses/{relative_path}", f.read()) records.write(record) def add_extra_metadata(self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any]) -> None: extra_metadata = dict(self.config.extra_metadata) extra_metadata.update(normalize_inclusion_map(build_data["extra_metadata"], self.root)) for extra_metadata_file in self.recurse_explicit_files(extra_metadata): record = archive.add_extra_metadata_file(extra_metadata_file) records.write(record) def construct_entry_points_file(self) -> str: core_metadata = self.metadata.core metadata_file = "" if core_metadata.scripts: metadata_file += "\n[console_scripts]\n" for name, object_ref in core_metadata.scripts.items(): metadata_file += f"{name} = {object_ref}\n" if core_metadata.gui_scripts: metadata_file += "\n[gui_scripts]\n" for name, object_ref in core_metadata.gui_scripts.items(): metadata_file += f"{name} = {object_ref}\n" if core_metadata.entry_points: for group, entry_points in core_metadata.entry_points.items(): metadata_file += f"\n[{group}]\n" for name, object_ref in entry_points.items(): metadata_file += f"{name} = {object_ref}\n" return metadata_file.lstrip() def get_default_tag(self) -> str: known_major_versions = list(get_known_python_major_versions()) max_version_part = 100 supported_python_versions = [] for major_version in known_major_versions: for minor_version in range(max_version_part): # Try an artificially high patch version to account for common cases like `>=3.11.4` or `>=3.10,<3.11` if self.metadata.core.python_constraint.contains(f"{major_version}.{minor_version}.{max_version_part}"): supported_python_versions.append(f"py{major_version}") break # Slow path, try all permutations to account for narrow support ranges like `<=3.11.4` if not supported_python_versions: for major_version in known_major_versions: for minor_version in range(max_version_part): for patch_version in range(max_version_part): if self.metadata.core.python_constraint.contains( f"{major_version}.{minor_version}.{patch_version}" ): supported_python_versions.append(f"py{major_version}") break else: continue break return f"{'.'.join(supported_python_versions)}-none-any" def get_best_matching_tag(self) -> str: import sys from packaging.tags import sys_tags # Linux tag is after many/musl; packaging tools are required to skip # many/musl, see https://github.com/pypa/packaging/issues/160 tag = next(iter(t for t in sys_tags() if "manylinux" not in t.platform and "musllinux" not in t.platform)) tag_parts = [tag.interpreter, tag.abi, tag.platform] if sys.platform == "darwin": from hatchling.builders.macos import process_macos_plat_tag tag_parts[2] = process_macos_plat_tag(tag_parts[2], compat=self.config.macos_max_compat) return "-".join(tag_parts) def get_default_build_data(self) -> dict[str, Any]: # noqa: PLR6301 return { "infer_tag": False, "pure_python": True, "dependencies": [], "force_include_editable": {}, "extra_metadata": {}, "shared_data": {}, "shared_scripts": {}, "sbom_files": [], } def get_forced_inclusion_map(self, build_data: dict[str, Any]) -> dict[str, str]: if not build_data["force_include_editable"]: return self.config.get_force_include() return normalize_inclusion_map(build_data["force_include_editable"], self.root) @property def artifact_project_id(self) -> str: return ( self.project_id if self.config.strict_naming else f"{self.normalize_file_name_component(self.metadata.core.raw_name)}-{self.metadata.version}" ) @classmethod def get_config_class(cls) -> type[WheelBuilderConfig]: return WheelBuilderConfig ================================================ FILE: backend/src/hatchling/cli/__init__.py ================================================ import argparse from hatchling.cli.build import build_command from hatchling.cli.dep import dep_command from hatchling.cli.metadata import metadata_command from hatchling.cli.version import version_command def hatchling() -> int: parser = argparse.ArgumentParser(prog="hatchling", allow_abbrev=False) subparsers = parser.add_subparsers() defaults = {"metavar": ""} build_command(subparsers, defaults) dep_command(subparsers, defaults) metadata_command(subparsers, defaults) version_command(subparsers, defaults) kwargs = vars(parser.parse_args()) try: command = kwargs.pop("func") except KeyError: parser.print_help() else: command(**kwargs) return 0 ================================================ FILE: backend/src/hatchling/cli/build/__init__.py ================================================ from __future__ import annotations import argparse from typing import Any def build_impl( *, called_by_app: bool, # noqa: ARG001 directory: str, targets: list[str], hooks_only: bool, no_hooks: bool, clean: bool, clean_hooks_after: bool, clean_only: bool, show_dynamic_deps: bool, ) -> None: import os from hatchling.bridge.app import Application from hatchling.builders.constants import BuildEnvVars from hatchling.metadata.core import ProjectMetadata from hatchling.plugin.manager import PluginManager app = Application() if hooks_only and no_hooks: app.abort("Cannot use both --hooks-only and --no-hooks together") root = os.getcwd() plugin_manager = PluginManager() metadata = ProjectMetadata(root, plugin_manager) target_data: dict[str, Any] = {} if targets: for data in targets: target_name, _, version_data = data.partition(":") versions = version_data.split(",") if version_data else [] target_data.setdefault(target_name, []).extend(versions) else: # no cov target_data["sdist"] = [] target_data["wheel"] = [] builders = {} unknown_targets = [] for target_name in target_data: builder_class = plugin_manager.builder.get(target_name) if builder_class is None: unknown_targets.append(target_name) else: builders[target_name] = builder_class if unknown_targets: app.abort(f"Unknown build targets: {', '.join(sorted(unknown_targets))}") # We guarantee that builds occur within the project directory root = os.getcwd() if no_hooks: os.environ[BuildEnvVars.NO_HOOKS] = "true" dynamic_dependencies: dict[str, None] = {} for i, (target_name, versions) in enumerate(target_data.items()): # Separate targets with a blank line if not (clean_only or show_dynamic_deps) and i != 0: # no cov app.display_info() builder_class = builders[target_name] # Display name before instantiation in case of errors if not (clean_only or show_dynamic_deps) and len(target_data) > 1: app.display_mini_header(target_name) builder = builder_class(root, plugin_manager=plugin_manager, metadata=metadata, app=app.get_safe_application()) if show_dynamic_deps: for dependency in builder.config.dynamic_dependencies: dynamic_dependencies[dependency] = None continue for artifact in builder.build( directory=directory, versions=versions, hooks_only=hooks_only, clean=clean, clean_hooks_after=clean_hooks_after, clean_only=clean_only, ): if os.path.isfile(artifact) and artifact.startswith(root): app.display_info(os.path.relpath(artifact, root)) else: # no cov app.display_info(artifact) if show_dynamic_deps: app.display(str(list(dynamic_dependencies))) def build_command(subparsers: argparse._SubParsersAction, defaults: Any) -> None: parser = subparsers.add_parser("build") parser.add_argument( "-d", "--directory", dest="directory", help="The directory in which to build artifacts", **defaults ) parser.add_argument( "-t", "--target", dest="targets", action="append", help="Comma-separated list of targets to build, overriding project defaults", **defaults, ) parser.add_argument("--hooks-only", dest="hooks_only", action="store_true", default=None) parser.add_argument("--no-hooks", dest="no_hooks", action="store_true", default=None) parser.add_argument("-c", "--clean", dest="clean", action="store_true", default=None) parser.add_argument("--clean-hooks-after", dest="clean_hooks_after", action="store_true", default=None) parser.add_argument("--clean-only", dest="clean_only", action="store_true") parser.add_argument("--show-dynamic-deps", dest="show_dynamic_deps", action="store_true") parser.add_argument("--app", dest="called_by_app", action="store_true", help=argparse.SUPPRESS) parser.set_defaults(func=build_impl) ================================================ FILE: backend/src/hatchling/cli/dep/__init__.py ================================================ from __future__ import annotations import sys from typing import TYPE_CHECKING, Any if TYPE_CHECKING: import argparse def synced_impl(*, dependencies: list[str], python: str) -> None: import subprocess from ast import literal_eval from packaging.requirements import Requirement from hatchling.cli.dep.core import dependencies_in_sync sys_path = None if python: output = subprocess.check_output([python, "-c", "import sys;print([path for path in sys.path if path])"]) sys_path = literal_eval(output.strip().decode("utf-8")) sys.exit(0 if dependencies_in_sync(list(map(Requirement, dependencies)), sys_path) else 1) def synced_command(subparsers: argparse._SubParsersAction, defaults: Any) -> None: parser = subparsers.add_parser("synced") parser.add_argument("dependencies", nargs="+") parser.add_argument("-p", "--python", dest="python", **defaults) parser.set_defaults(func=synced_impl) def dep_command(subparsers: argparse._SubParsersAction, defaults: Any) -> None: parser = subparsers.add_parser("dep") subparsers = parser.add_subparsers() synced_command(subparsers, defaults) ================================================ FILE: backend/src/hatchling/cli/dep/core.py ================================================ from __future__ import annotations import re import sys from importlib.metadata import Distribution, DistributionFinder from packaging.markers import default_environment from packaging.requirements import Requirement class DistributionCache: def __init__(self, sys_path: list[str]) -> None: self._resolver = Distribution.discover(context=DistributionFinder.Context(path=sys_path)) self._distributions: dict[str, Distribution] = {} self._search_exhausted = False self._canonical_regex = re.compile(r"[-_.]+") def __getitem__(self, item: str) -> Distribution | None: item = self._canonical_regex.sub("-", item).lower() possible_distribution = self._distributions.get(item) if possible_distribution is not None: return possible_distribution # Be safe even though the code as-is will never reach this since # the first unknown distribution will fail fast if self._search_exhausted: # no cov return None for distribution in self._resolver: name = distribution.metadata["Name"] if name is None: continue name = self._canonical_regex.sub("-", name).lower() self._distributions[name] = distribution if name == item: return distribution self._search_exhausted = True return None def dependency_in_sync( requirement: Requirement, environment: dict[str, str], installed_distributions: DistributionCache ) -> bool: if requirement.marker and not requirement.marker.evaluate(environment): return True distribution = installed_distributions[requirement.name] if distribution is None: return False extras = requirement.extras if extras: transitive_requirements: list[str] = distribution.metadata.get_all("Requires-Dist", []) if not transitive_requirements: return False available_extras: list[str] = distribution.metadata.get_all("Provides-Extra", []) for requirement_string in transitive_requirements: transitive_requirement = Requirement(requirement_string) if not transitive_requirement.marker: continue for extra in extras: # FIXME: This may cause a build to never be ready if newer versions do not provide the desired # extra and it's just a user error/typo. See: https://github.com/pypa/pip/issues/7122 if extra not in available_extras: return False extra_environment = dict(environment) extra_environment["extra"] = extra if not dependency_in_sync(transitive_requirement, extra_environment, installed_distributions): return False if requirement.specifier and not requirement.specifier.contains(distribution.version): return False # TODO: handle https://discuss.python.org/t/11938 if requirement.url: direct_url_file = distribution.read_text("direct_url.json") if direct_url_file is not None: import json # https://packaging.python.org/specifications/direct-url/ direct_url_data = json.loads(direct_url_file) if "vcs_info" in direct_url_data: url = direct_url_data["url"] vcs_info = direct_url_data["vcs_info"] vcs = vcs_info["vcs"] commit_id = vcs_info["commit_id"] requested_revision = vcs_info.get("requested_revision") # Try a few variations, see https://peps.python.org/pep-0440/#direct-references if ( requested_revision and requirement.url == f"{vcs}+{url}@{requested_revision}#{commit_id}" ) or requirement.url == f"{vcs}+{url}@{commit_id}": return True if requirement.url in {f"{vcs}+{url}", f"{vcs}+{url}@{requested_revision}"}: import subprocess if vcs == "git": vcs_cmd = [vcs, "ls-remote", url] if requested_revision: vcs_cmd.append(requested_revision) # TODO: add elifs for hg, svn, and bzr https://github.com/pypa/hatch/issues/760 else: return False result = subprocess.run(vcs_cmd, capture_output=True, text=True) # noqa: PLW1510 if result.returncode or not result.stdout.strip(): return False latest_commit_id, *_ = result.stdout.split() return commit_id == latest_commit_id return False return True def dependencies_in_sync( requirements: list[Requirement], sys_path: list[str] | None = None, environment: dict[str, str] | None = None ) -> bool: if sys_path is None: sys_path = sys.path if environment is None: environment = default_environment() # type: ignore[assignment] installed_distributions = DistributionCache(sys_path) return all(dependency_in_sync(requirement, environment, installed_distributions) for requirement in requirements) # type: ignore[arg-type] ================================================ FILE: backend/src/hatchling/cli/metadata/__init__.py ================================================ from __future__ import annotations import argparse from typing import Any def metadata_impl( *, called_by_app: bool, # noqa: ARG001 field: str, compact: bool, ) -> None: import json import os from hatchling.bridge.app import Application from hatchling.metadata.core import ProjectMetadata from hatchling.metadata.utils import resolve_metadata_fields from hatchling.plugin.manager import PluginManager app = Application() root = os.getcwd() plugin_manager = PluginManager() project_metadata = ProjectMetadata(root, plugin_manager) metadata = resolve_metadata_fields(project_metadata) if field: # no cov if field not in metadata: app.abort(f"Unknown metadata field: {field}") elif field == "readme": app.display(metadata[field]["text"]) elif isinstance(metadata[field], str): app.display(metadata[field]) else: app.display(json.dumps(metadata[field], indent=4)) return for key, value in list(metadata.items()): if not value: metadata.pop(key) if compact: app.display(json.dumps(metadata, separators=(",", ":"))) else: # no cov app.display(json.dumps(metadata, indent=4)) def metadata_command( subparsers: argparse._SubParsersAction, defaults: Any, # noqa: ARG001 ) -> None: parser = subparsers.add_parser("metadata") parser.add_argument("field", nargs="?") parser.add_argument("-c", "--compact", action="store_true") parser.add_argument("--app", dest="called_by_app", action="store_true", help=argparse.SUPPRESS) parser.set_defaults(func=metadata_impl) ================================================ FILE: backend/src/hatchling/cli/version/__init__.py ================================================ from __future__ import annotations import argparse from typing import Any def version_impl( *, called_by_app: bool, # noqa: ARG001 desired_version: str, ) -> None: import os from hatchling.bridge.app import Application from hatchling.metadata.core import ProjectMetadata from hatchling.plugin.manager import PluginManager app = Application() root = os.getcwd() plugin_manager = PluginManager() metadata = ProjectMetadata(root, plugin_manager) if "version" in metadata.config.get("project", {}): if desired_version: app.abort("Cannot set version when it is statically defined by the `project.version` field") else: app.display(metadata.core.version) return source = metadata.hatch.version.source version_data = source.get_version_data() original_version = version_data["version"] if not desired_version: app.display(original_version) return updated_version = metadata.hatch.version.scheme.update(desired_version, original_version, version_data) source.set_version(updated_version, version_data) app.display_info(f"Old: {original_version}") app.display_info(f"New: {updated_version}") def version_command(subparsers: argparse._SubParsersAction, defaults: Any) -> None: parser = subparsers.add_parser("version") parser.add_argument("desired_version", default="", nargs="?", **defaults) parser.add_argument("--app", dest="called_by_app", action="store_true", help=argparse.SUPPRESS) parser.set_defaults(func=version_impl) ================================================ FILE: backend/src/hatchling/dep/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/dep/core.py ================================================ from hatchling.cli.dep.core import dependencies_in_sync # noqa: F401 ================================================ FILE: backend/src/hatchling/licenses/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/licenses/supported.py ================================================ from packaging.licenses._spdx import VERSION # noqa: F401, PLC2701 ================================================ FILE: backend/src/hatchling/metadata/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/metadata/core.py ================================================ from __future__ import annotations import os import sys from contextlib import suppress from copy import deepcopy from typing import TYPE_CHECKING, Any, Generic, cast from hatchling.metadata.utils import ( format_dependency, is_valid_project_name, normalize_project_name, normalize_requirement, ) from hatchling.plugin.manager import PluginManagerBound from hatchling.utils.constants import DEFAULT_CONFIG_FILE from hatchling.utils.fs import locate_file if TYPE_CHECKING: from packaging.requirements import Requirement from packaging.specifiers import SpecifierSet from hatchling.metadata.plugin.interface import MetadataHookInterface from hatchling.utils.context import Context from hatchling.version.scheme.plugin.interface import VersionSchemeInterface from hatchling.version.source.plugin.interface import VersionSourceInterface if sys.version_info >= (3, 11): import tomllib else: import tomli as tomllib def load_toml(path: str) -> dict[str, Any]: with open(path, encoding="utf-8") as f: return tomllib.loads(f.read()) class ProjectMetadata(Generic[PluginManagerBound]): def __init__( self, root: str, plugin_manager: PluginManagerBound | None, config: dict[str, Any] | None = None, ) -> None: self.root = root self.plugin_manager = plugin_manager self._config = config self._context: Context | None = None self._build: BuildMetadata | None = None self._core: CoreMetadata | None = None self._hatch: HatchMetadata | None = None self._core_raw_metadata: dict[str, Any] | None = None self._dynamic: list[str] | None = None self._name: str | None = None self._version: str | None = None self._project_file: str | None = None # App already loaded config if config is not None and root is not None: self._project_file = os.path.join(root, "pyproject.toml") def has_project_file(self) -> bool: _ = self.config if self._project_file is None: return False return os.path.isfile(self._project_file) @property def context(self) -> Context: if self._context is None: from hatchling.utils.context import Context self._context = Context(self.root) return self._context @property def core_raw_metadata(self) -> dict[str, Any]: if self._core_raw_metadata is None: if "project" not in self.config: message = "Missing `project` metadata table in configuration" raise ValueError(message) core_raw_metadata = self.config["project"] if not isinstance(core_raw_metadata, dict): message = "The `project` configuration must be a table" raise TypeError(message) core_raw_metadata = deepcopy(core_raw_metadata) pkg_info = os.path.join(self.root, "PKG-INFO") if os.path.isfile(pkg_info): from hatchling.metadata.spec import PROJECT_CORE_METADATA_FIELDS, project_metadata_from_core_metadata with open(pkg_info, encoding="utf-8") as f: pkg_info_contents = f.read() base_metadata = project_metadata_from_core_metadata(pkg_info_contents) defined_dynamic = core_raw_metadata.get("dynamic", []) for field in list(defined_dynamic): if field in PROJECT_CORE_METADATA_FIELDS and field in base_metadata: core_raw_metadata[field] = base_metadata[field] defined_dynamic.remove(field) self._core_raw_metadata = core_raw_metadata return self._core_raw_metadata @property def dynamic(self) -> list[str]: # Keep track of the original dynamic fields before depopulation if self._dynamic is None: dynamic = self.core_raw_metadata.get("dynamic", []) if not isinstance(dynamic, list): message = "Field `project.dynamic` must be an array" raise TypeError(message) for i, field in enumerate(dynamic, 1): if not isinstance(field, str): message = f"Field #{i} of field `project.dynamic` must be a string" raise TypeError(message) self._dynamic = list(dynamic) return self._dynamic @property def name(self) -> str: # Duplicate the name parsing here for situations where it's # needed but metadata plugins might not be available if self._name is None: name = self.core_raw_metadata.get("name", "") if not name: message = "Missing required field `project.name`" raise ValueError(message) self._name = normalize_project_name(name) return self._name @property def version(self) -> str: """ https://peps.python.org/pep-0621/#version """ if self._version is None: self._version = self._get_version() with suppress(ValueError): self.core.dynamic.remove("version") return self._version @property def config(self) -> dict[str, Any]: if self._config is None: project_file = locate_file(self.root, "pyproject.toml") if project_file is None: self._config = {} else: self._project_file = project_file self._config = load_toml(project_file) return self._config @property def build(self) -> BuildMetadata: if self._build is None: build_metadata = self.config.get("build-system", {}) if not isinstance(build_metadata, dict): message = "The `build-system` configuration must be a table" raise TypeError(message) self._build = BuildMetadata(self.root, build_metadata) return self._build @property def core(self) -> CoreMetadata: if self._core is None: metadata = CoreMetadata(self.root, self.core_raw_metadata, self.hatch.metadata, self.context) # Save the fields _ = self.dynamic metadata_hooks = self.hatch.metadata.hooks if metadata_hooks: static_fields = set(self.core_raw_metadata) if "version" in self.hatch.config: self._version = self._get_version(metadata) self.core_raw_metadata["version"] = self.version if metadata.dynamic: for metadata_hook in metadata_hooks.values(): metadata_hook.update(self.core_raw_metadata) metadata.add_known_classifiers(metadata_hook.get_known_classifiers()) new_fields = set(self.core_raw_metadata) - static_fields for new_field in new_fields: if new_field in metadata.dynamic: metadata.dynamic.remove(new_field) else: message = ( f"The field `{new_field}` was set dynamically and therefore must be " f"listed in `project.dynamic`" ) raise ValueError(message) self._core = metadata return self._core @property def hatch(self) -> HatchMetadata: if self._hatch is None: tool_config = self.config.get("tool", {}) if not isinstance(tool_config, dict): message = "The `tool` configuration must be a table" raise TypeError(message) hatch_config = tool_config.get("hatch", {}) if not isinstance(hatch_config, dict): message = "The `tool.hatch` configuration must be a table" raise TypeError(message) hatch_file = ( os.path.join(os.path.dirname(self._project_file), DEFAULT_CONFIG_FILE) if self._project_file is not None else locate_file(self.root, DEFAULT_CONFIG_FILE) or "" ) if hatch_file and os.path.isfile(hatch_file): config = load_toml(hatch_file) hatch_config = hatch_config.copy() hatch_config.update(config) self._hatch = HatchMetadata(self.root, hatch_config, self.plugin_manager) return self._hatch def _get_version(self, core_metadata: CoreMetadata | None = None) -> str: if core_metadata is None: core_metadata = self.core version = core_metadata.version if version is None: version = self.hatch.version.cached source = f"source `{self.hatch.version.source_name}`" core_metadata._version_set = True # noqa: SLF001 else: source = "field `project.version`" from packaging.version import InvalidVersion, Version try: normalized_version = str(Version(version)) except InvalidVersion: message = f"Invalid version `{version}` from {source}, see https://peps.python.org/pep-0440/" raise ValueError(message) from None else: return normalized_version def validate_fields(self) -> None: _ = self.version self.core.validate_fields() class BuildMetadata: """ https://peps.python.org/pep-0517/ """ def __init__(self, root: str, config: dict[str, str | list[str]]) -> None: self.root = root self.config = config self._requires: list[str] | None = None self._requires_complex: list[Requirement] | None = None self._build_backend: str | None = None self._backend_path: list[str] | None = None @property def requires_complex(self) -> list[Requirement]: if self._requires_complex is None: from packaging.requirements import InvalidRequirement, Requirement requires = self.config.get("requires", []) if not isinstance(requires, list): message = "Field `build-system.requires` must be an array" raise TypeError(message) requires_complex = [] for i, entry in enumerate(requires, 1): if not isinstance(entry, str): message = f"Dependency #{i} of field `build-system.requires` must be a string" raise TypeError(message) try: requires_complex.append(Requirement(entry)) except InvalidRequirement as e: message = f"Dependency #{i} of field `build-system.requires` is invalid: {e}" raise ValueError(message) from None self._requires_complex = requires_complex return self._requires_complex @property def requires(self) -> list[str]: if self._requires is None: self._requires = [str(r) for r in self.requires_complex] return self._requires @property def build_backend(self) -> str: if self._build_backend is None: build_backend = self.config.get("build-backend", "") if not isinstance(build_backend, str): message = "Field `build-system.build-backend` must be a string" raise TypeError(message) self._build_backend = build_backend return self._build_backend @property def backend_path(self) -> list[str]: if self._backend_path is None: backend_path = self.config.get("backend-path", []) if not isinstance(backend_path, list): message = "Field `build-system.backend-path` must be an array" raise TypeError(message) for i, entry in enumerate(backend_path, 1): if not isinstance(entry, str): message = f"Entry #{i} of field `build-system.backend-path` must be a string" raise TypeError(message) self._backend_path = backend_path return self._backend_path class CoreMetadata: """ https://peps.python.org/pep-0621/ """ def __init__( self, root: str, config: dict[str, Any], hatch_metadata: HatchMetadataSettings, context: Context, ) -> None: self.root = root self.config = config self.hatch_metadata = hatch_metadata self.context = context self._raw_name: str | None = None self._name: str | None = None self._version: str | None = None self._description: str | None = None self._readme: str | None = None self._readme_content_type: str | None = None self._readme_path: str | None = None self._requires_python: str | None = None self._python_constraint: SpecifierSet | None = None self._license: str | None = None self._license_expression: str | None = None self._license_files: list[str] | None = None self._authors: list[str] | None = None self._authors_data: dict[str, list[str]] | None = None self._maintainers: list[str] | None = None self._maintainers_data: dict[str, list[str]] | None = None self._keywords: list[str] | None = None self._classifiers: list[str] | None = None self._extra_classifiers: set[str] = set() self._urls: dict[str, str] | None = None self._scripts: dict[str, str] | None = None self._gui_scripts: dict[str, str] | None = None self._entry_points: dict[str, dict[str, str]] | None = None self._dependencies_complex: dict[str, Requirement] | None = None self._dependencies: list[str] | None = None self._optional_dependencies_complex: dict[str, dict[str, Requirement]] | None = None self._optional_dependencies: dict[str, list[str]] | None = None self._dynamic: list[str] | None = None # Indicates that the version has been successfully set dynamically self._version_set: bool = False @property def raw_name(self) -> str: """ https://peps.python.org/pep-0621/#name """ if self._raw_name is None: if "name" in self.dynamic: message = "Static metadata field `name` cannot be present in field `project.dynamic`" raise ValueError(message) raw_name = self.config.get("name", "") if not raw_name: message = "Missing required field `project.name`" raise ValueError(message) if not isinstance(raw_name, str): message = "Field `project.name` must be a string" raise TypeError(message) if not is_valid_project_name(raw_name): message = ( "Required field `project.name` must only contain ASCII letters/digits, underscores, " "hyphens, and periods, and must begin and end with ASCII letters/digits." ) raise ValueError(message) self._raw_name = raw_name return self._raw_name @property def name(self) -> str: """ https://peps.python.org/pep-0621/#name """ if self._name is None: self._name = normalize_project_name(self.raw_name) return self._name @property def version(self) -> str: """ https://peps.python.org/pep-0621/#version """ version: str if self._version is None: if "version" not in self.config: if not self._version_set and "version" not in self.dynamic: message = ( "Field `project.version` can only be resolved dynamically " "if `version` is in field `project.dynamic`" ) raise ValueError(message) else: if "version" in self.dynamic: message = ( "Metadata field `version` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) version = self.config["version"] if not isinstance(version, str): message = "Field `project.version` must be a string" raise TypeError(message) self._version = version return cast(str, self._version) @property def description(self) -> str: """ https://peps.python.org/pep-0621/#description """ if self._description is None: if "description" in self.config: description = self.config["description"] if "description" in self.dynamic: message = ( "Metadata field `description` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: description = "" if not isinstance(description, str): message = "Field `project.description` must be a string" raise TypeError(message) self._description = " ".join(description.splitlines()) return self._description @property def readme(self) -> str: """ https://peps.python.org/pep-0621/#readme """ readme: str | dict[str, str] | None content_type: str | None if self._readme is None: if "readme" in self.config: readme = self.config["readme"] if "readme" in self.dynamic: message = ( "Metadata field `readme` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: readme = None if readme is None: self._readme = "" self._readme_content_type = "text/markdown" self._readme_path = "" elif isinstance(readme, str): normalized_path = readme.lower() if normalized_path.endswith(".md"): content_type = "text/markdown" elif normalized_path.endswith(".rst"): content_type = "text/x-rst" elif normalized_path.endswith(".txt"): content_type = "text/plain" else: message = f"Unable to determine the content-type based on the extension of readme file: {readme}" raise TypeError(message) readme_path = os.path.normpath(os.path.join(self.root, readme)) if not os.path.isfile(readme_path): message = f"Readme file does not exist: {readme}" raise OSError(message) with open(readme_path, encoding="utf-8") as f: self._readme = f.read() self._readme_content_type = content_type self._readme_path = readme elif isinstance(readme, dict): content_type = readme.get("content-type") if content_type is None: message = "Field `content-type` is required in the `project.readme` table" raise ValueError(message) if not isinstance(content_type, str): message = "Field `content-type` in the `project.readme` table must be a string" raise TypeError(message) if content_type not in {"text/markdown", "text/x-rst", "text/plain"}: message = ( "Field `content-type` in the `project.readme` table must be one of the following: " "text/markdown, text/x-rst, text/plain" ) raise ValueError(message) if "file" in readme and "text" in readme: message = "Cannot specify both `file` and `text` in the `project.readme` table" raise ValueError(message) if "file" in readme: relative_path = readme["file"] if not isinstance(relative_path, str): message = "Field `file` in the `project.readme` table must be a string" raise TypeError(message) path = os.path.normpath(os.path.join(self.root, relative_path)) if not os.path.isfile(path): message = f"Readme file does not exist: {relative_path}" raise OSError(message) with open(path, encoding=readme.get("charset", "utf-8")) as f: contents = f.read() readme_path = relative_path elif "text" in readme: contents = readme["text"] if not isinstance(contents, str): message = "Field `text` in the `project.readme` table must be a string" raise TypeError(message) readme_path = "" else: message = "Must specify either `file` or `text` in the `project.readme` table" raise ValueError(message) self._readme = contents self._readme_content_type = content_type self._readme_path = readme_path else: message = "Field `project.readme` must be a string or a table" raise TypeError(message) return self._readme @property def readme_content_type(self) -> str: """ https://peps.python.org/pep-0621/#readme """ if self._readme_content_type is None: _ = self.readme return cast(str, self._readme_content_type) @property def readme_path(self) -> str: """ https://peps.python.org/pep-0621/#readme """ if self._readme_path is None: _ = self.readme return cast(str, self._readme_path) @property def requires_python(self) -> str: """ https://peps.python.org/pep-0621/#requires-python """ if self._requires_python is None: from packaging.specifiers import InvalidSpecifier, SpecifierSet if "requires-python" in self.config: requires_python = self.config["requires-python"] if "requires-python" in self.dynamic: message = ( "Metadata field `requires-python` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: requires_python = "" if not isinstance(requires_python, str): message = "Field `project.requires-python` must be a string" raise TypeError(message) try: self._python_constraint = SpecifierSet(requires_python) except InvalidSpecifier as e: message = f"Field `project.requires-python` is invalid: {e}" raise ValueError(message) from None self._requires_python = str(self._python_constraint) return self._requires_python @property def python_constraint(self) -> SpecifierSet: from packaging.specifiers import SpecifierSet if self._python_constraint is None: _ = self.requires_python return cast(SpecifierSet, self._python_constraint) @property def license(self) -> str: """ https://peps.python.org/pep-0621/#license """ if self._license is None: if "license" in self.config: data = self.config["license"] if "license" in self.dynamic: message = ( "Metadata field `license` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: data = None if data is None: self._license = "" self._license_expression = "" elif isinstance(data, str): from packaging.licenses import canonicalize_license_expression try: self._license_expression = str(canonicalize_license_expression(data)) except ValueError as e: message = f"Error parsing field `project.license` - {e}" raise ValueError(message) from None self._license = "" elif isinstance(data, dict): if "file" in data and "text" in data: message = "Cannot specify both `file` and `text` in the `project.license` table" raise ValueError(message) if "file" in data: relative_path = data["file"] if not isinstance(relative_path, str): message = "Field `file` in the `project.license` table must be a string" raise TypeError(message) path = os.path.normpath(os.path.join(self.root, relative_path)) if not os.path.isfile(path): message = f"License file does not exist: {relative_path}" raise OSError(message) with open(path, encoding="utf-8") as f: contents = f.read() elif "text" in data: contents = data["text"] if not isinstance(contents, str): message = "Field `text` in the `project.license` table must be a string" raise TypeError(message) else: message = "Must specify either `file` or `text` in the `project.license` table" raise ValueError(message) self._license = contents self._license_expression = "" else: message = "Field `project.license` must be a string or a table" raise TypeError(message) return self._license @property def license_expression(self) -> str: """ https://peps.python.org/pep-0639/ """ if self._license_expression is None: _ = self.license return cast(str, self._license_expression) @property def license_files(self) -> list[str]: """ https://peps.python.org/pep-0639/ """ if self._license_files is None: if "license-files" in self.config: globs = self.config["license-files"] if "license-files" in self.dynamic: message = ( "Metadata field `license-files` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) if isinstance(globs, dict): globs = globs.get("globs", globs.get("paths", [])) else: globs = ["LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*"] from glob import glob license_files: list[str] = [] if not isinstance(globs, list): message = "Field `project.license-files` must be an array" raise TypeError(message) for i, pattern in enumerate(globs, 1): if not isinstance(pattern, str): message = f"Entry #{i} of field `project.license-files` must be a string" raise TypeError(message) full_pattern = os.path.normpath(os.path.join(self.root, pattern)) license_files.extend( os.path.relpath(path, self.root).replace("\\", "/") for path in glob(full_pattern) if os.path.isfile(path) ) self._license_files = sorted(license_files) return self._license_files @property def authors(self) -> list[str]: """ https://peps.python.org/pep-0621/#authors-maintainers """ authors: list[str] authors_data: dict[str, list[str]] if self._authors is None: if "authors" in self.config: authors = self.config["authors"] if "authors" in self.dynamic: message = ( "Metadata field `authors` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: authors = [] if not isinstance(authors, list): message = "Field `project.authors` must be an array" raise TypeError(message) from email.headerregistry import Address authors = deepcopy(authors) authors_data = {"name": [], "email": []} for i, data in enumerate(authors, 1): if not isinstance(data, dict): message = f"Author #{i} of field `project.authors` must be an inline table" raise TypeError(message) name = data.get("name", "") if not isinstance(name, str): message = f"Name of author #{i} of field `project.authors` must be a string" raise TypeError(message) email = data.get("email", "") if not isinstance(email, str): message = f"Email of author #{i} of field `project.authors` must be a string" raise TypeError(message) if name and email: authors_data["email"].append(str(Address(display_name=name, addr_spec=email))) elif email: authors_data["email"].append(str(Address(addr_spec=email))) elif name: authors_data["name"].append(name) else: message = f"Author #{i} of field `project.authors` must specify either `name` or `email`" raise ValueError(message) self._authors = authors self._authors_data = authors_data return self._authors @property def authors_data(self) -> dict[str, list[str]]: """ https://peps.python.org/pep-0621/#authors-maintainers """ if self._authors_data is None: _ = self.authors return cast(dict, self._authors_data) @property def maintainers(self) -> list[str]: """ https://peps.python.org/pep-0621/#authors-maintainers """ maintainers: list[str] if self._maintainers is None: if "maintainers" in self.config: maintainers = self.config["maintainers"] if "maintainers" in self.dynamic: message = ( "Metadata field `maintainers` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: maintainers = [] if not isinstance(maintainers, list): message = "Field `project.maintainers` must be an array" raise TypeError(message) from email.headerregistry import Address maintainers = deepcopy(maintainers) maintainers_data: dict[str, list[str]] = {"name": [], "email": []} for i, data in enumerate(maintainers, 1): if not isinstance(data, dict): message = f"Maintainer #{i} of field `project.maintainers` must be an inline table" raise TypeError(message) name = data.get("name", "") if not isinstance(name, str): message = f"Name of maintainer #{i} of field `project.maintainers` must be a string" raise TypeError(message) email = data.get("email", "") if not isinstance(email, str): message = f"Email of maintainer #{i} of field `project.maintainers` must be a string" raise TypeError(message) if name and email: maintainers_data["email"].append(str(Address(display_name=name, addr_spec=email))) elif email: maintainers_data["email"].append(str(Address(addr_spec=email))) elif name: maintainers_data["name"].append(name) else: message = f"Maintainer #{i} of field `project.maintainers` must specify either `name` or `email`" raise ValueError(message) self._maintainers = maintainers self._maintainers_data = maintainers_data return self._maintainers @property def maintainers_data(self) -> dict[str, list[str]]: """ https://peps.python.org/pep-0621/#authors-maintainers """ if self._maintainers_data is None: _ = self.maintainers return cast(dict, self._maintainers_data) @property def keywords(self) -> list[str]: """ https://peps.python.org/pep-0621/#keywords """ if self._keywords is None: if "keywords" in self.config: keywords = self.config["keywords"] if "keywords" in self.dynamic: message = ( "Metadata field `keywords` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: keywords = [] if not isinstance(keywords, list): message = "Field `project.keywords` must be an array" raise TypeError(message) unique_keywords = set() for i, keyword in enumerate(keywords, 1): if not isinstance(keyword, str): message = f"Keyword #{i} of field `project.keywords` must be a string" raise TypeError(message) unique_keywords.add(keyword) self._keywords = sorted(unique_keywords) return self._keywords @property def classifiers(self) -> list[str]: """ https://peps.python.org/pep-0621/#classifiers """ if self._classifiers is None: import bisect if "classifiers" in self.config: classifiers = self.config["classifiers"] if "classifiers" in self.dynamic: message = ( "Metadata field `classifiers` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: classifiers = [] if not isinstance(classifiers, list): message = "Field `project.classifiers` must be an array" raise TypeError(message) verify_classifiers = not os.environ.get("HATCH_METADATA_CLASSIFIERS_NO_VERIFY") if verify_classifiers: import trove_classifiers known_classifiers = trove_classifiers.classifiers | self._extra_classifiers sorted_classifiers = list(trove_classifiers.sorted_classifiers) for classifier in sorted(self._extra_classifiers - trove_classifiers.classifiers): bisect.insort(sorted_classifiers, classifier) unique_classifiers = set() for i, classifier in enumerate(classifiers, 1): if not isinstance(classifier, str): message = f"Classifier #{i} of field `project.classifiers` must be a string" raise TypeError(message) if ( not self.__classifier_is_private(classifier) and verify_classifiers and classifier not in known_classifiers ): message = f"Unknown classifier in field `project.classifiers`: {classifier}" raise ValueError(message) unique_classifiers.add(classifier) if not verify_classifiers: import re # combined text-numeric sort that ensures that Python versions sort correctly split_re = re.compile(r"(\D*)(\d*)") sorted_classifiers = sorted( classifiers, key=lambda value: ([(a, int(b) if b else None) for a, b in split_re.findall(value)]), ) self._classifiers = sorted( unique_classifiers, key=lambda c: -1 if self.__classifier_is_private(c) else sorted_classifiers.index(c) ) return self._classifiers @property def urls(self) -> dict[str, str]: """ https://peps.python.org/pep-0621/#urls """ if self._urls is None: if "urls" in self.config: urls = self.config["urls"] if "urls" in self.dynamic: message = ( "Metadata field `urls` cannot be both statically defined and listed in field `project.dynamic`" ) raise ValueError(message) else: urls = {} if not isinstance(urls, dict): message = "Field `project.urls` must be a table" raise TypeError(message) sorted_urls = {} for label, url in urls.items(): if not isinstance(url, str): message = f"URL `{label}` of field `project.urls` must be a string" raise TypeError(message) sorted_urls[label] = url self._urls = sorted_urls return self._urls @property def scripts(self) -> dict[str, str]: """ https://peps.python.org/pep-0621/#entry-points """ if self._scripts is None: if "scripts" in self.config: scripts = self.config["scripts"] if "scripts" in self.dynamic: message = ( "Metadata field `scripts` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: scripts = {} if not isinstance(scripts, dict): message = "Field `project.scripts` must be a table" raise TypeError(message) sorted_scripts = {} for name, object_ref in sorted(scripts.items()): if not isinstance(object_ref, str): message = f"Object reference `{name}` of field `project.scripts` must be a string" raise TypeError(message) sorted_scripts[name] = object_ref self._scripts = sorted_scripts return self._scripts @property def gui_scripts(self) -> dict[str, str]: """ https://peps.python.org/pep-0621/#entry-points """ if self._gui_scripts is None: if "gui-scripts" in self.config: gui_scripts = self.config["gui-scripts"] if "gui-scripts" in self.dynamic: message = ( "Metadata field `gui-scripts` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: gui_scripts = {} if not isinstance(gui_scripts, dict): message = "Field `project.gui-scripts` must be a table" raise TypeError(message) sorted_gui_scripts = {} for name, object_ref in sorted(gui_scripts.items()): if not isinstance(object_ref, str): message = f"Object reference `{name}` of field `project.gui-scripts` must be a string" raise TypeError(message) sorted_gui_scripts[name] = object_ref self._gui_scripts = sorted_gui_scripts return self._gui_scripts @property def entry_points(self) -> dict[str, dict[str, str]]: """ https://peps.python.org/pep-0621/#entry-points """ if self._entry_points is None: if "entry-points" in self.config: defined_entry_point_groups = self.config["entry-points"] if "entry-points" in self.dynamic: message = ( "Metadata field `entry-points` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: defined_entry_point_groups = {} if not isinstance(defined_entry_point_groups, dict): message = "Field `project.entry-points` must be a table" raise TypeError(message) for forbidden_field, expected_field in (("console_scripts", "scripts"), ("gui-scripts", "gui-scripts")): if forbidden_field in defined_entry_point_groups: message = ( f"Field `{forbidden_field}` must be defined as `project.{expected_field}` " f"instead of in the `project.entry-points` table" ) raise ValueError(message) entry_point_groups = {} for group, entry_point_data in sorted(defined_entry_point_groups.items()): if not isinstance(entry_point_data, dict): message = f"Field `project.entry-points.{group}` must be a table" raise TypeError(message) entry_points = {} for name, object_ref in sorted(entry_point_data.items()): if not isinstance(object_ref, str): message = f"Object reference `{name}` of field `project.entry-points.{group}` must be a string" raise TypeError(message) entry_points[name] = object_ref if entry_points: entry_point_groups[group] = entry_points self._entry_points = entry_point_groups return self._entry_points @property def dependencies_complex(self) -> dict[str, Requirement]: """ https://peps.python.org/pep-0621/#dependencies-optional-dependencies """ if self._dependencies_complex is None: from packaging.requirements import InvalidRequirement, Requirement if "dependencies" in self.config: dependencies = self.config["dependencies"] if "dependencies" in self.dynamic: message = ( "Metadata field `dependencies` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: dependencies = [] if not isinstance(dependencies, list): message = "Field `project.dependencies` must be an array" raise TypeError(message) dependencies_complex = {} for i, entry in enumerate(dependencies, 1): if not isinstance(entry, str): message = f"Dependency #{i} of field `project.dependencies` must be a string" raise TypeError(message) try: requirement = Requirement(self.context.format(entry)) except InvalidRequirement as e: message = f"Dependency #{i} of field `project.dependencies` is invalid: {e}" raise ValueError(message) from None else: if requirement.url and not self.hatch_metadata.allow_direct_references: message = ( f"Dependency #{i} of field `project.dependencies` cannot be a direct reference unless " f"field `tool.hatch.metadata.allow-direct-references` is set to `true`" ) raise ValueError(message) normalize_requirement(requirement) dependencies_complex[format_dependency(requirement)] = requirement self._dependencies_complex = dict(sorted(dependencies_complex.items())) return self._dependencies_complex @property def dependencies(self) -> list[str]: """ https://peps.python.org/pep-0621/#dependencies-optional-dependencies """ if self._dependencies is None: self._dependencies = list(self.dependencies_complex) return self._dependencies @property def optional_dependencies_complex(self) -> dict[str, dict[str, Requirement]]: """ https://peps.python.org/pep-0621/#dependencies-optional-dependencies """ if self._optional_dependencies_complex is None: from packaging.requirements import InvalidRequirement, Requirement if "optional-dependencies" in self.config: optional_dependencies = self.config["optional-dependencies"] if "optional-dependencies" in self.dynamic: message = ( "Metadata field `optional-dependencies` cannot be both statically defined and " "listed in field `project.dynamic`" ) raise ValueError(message) else: optional_dependencies = {} if not isinstance(optional_dependencies, dict): message = "Field `project.optional-dependencies` must be a table" raise TypeError(message) normalized_options: dict[str, str] = {} optional_dependency_entries = {} inherited_options: dict[str, set[str]] = {} for option, dependencies in optional_dependencies.items(): if not is_valid_project_name(option): message = ( f"Optional dependency group `{option}` of field `project.optional-dependencies` must only " f"contain ASCII letters/digits, underscores, hyphens, and periods, and must begin and end with " f"ASCII letters/digits." ) raise ValueError(message) normalized_option = ( option if self.hatch_metadata.allow_ambiguous_features else normalize_project_name(option) ) if normalized_option in normalized_options: message = ( f"Optional dependency groups `{normalized_options[normalized_option]}` and `{option}` of " f"field `project.optional-dependencies` both evaluate to `{normalized_option}`." ) raise ValueError(message) if not isinstance(dependencies, list): message = ( f"Dependencies for option `{option}` of field `project.optional-dependencies` must be an array" ) raise TypeError(message) entries = {} for i, entry in enumerate(dependencies, 1): if not isinstance(entry, str): message = ( f"Dependency #{i} of option `{option}` of field `project.optional-dependencies` " f"must be a string" ) raise TypeError(message) try: requirement = Requirement(self.context.format(entry)) except InvalidRequirement as e: message = ( f"Dependency #{i} of option `{option}` of field `project.optional-dependencies` " f"is invalid: {e}" ) raise ValueError(message) from None else: if requirement.url and not self.hatch_metadata.allow_direct_references: message = ( f"Dependency #{i} of option `{option}` of field `project.optional-dependencies` " f"cannot be a direct reference unless field " f"`tool.hatch.metadata.allow-direct-references` is set to `true`" ) raise ValueError(message) normalize_requirement(requirement) if requirement.name == self.name: if normalized_option in inherited_options: inherited_options[normalized_option].update(requirement.extras) else: inherited_options[normalized_option] = set(requirement.extras) else: entries[format_dependency(requirement)] = requirement normalized_options[normalized_option] = option optional_dependency_entries[normalized_option] = entries visited: set[str] = set() resolved: set[str] = set() for dependent_option in inherited_options: _resolve_optional_dependencies( optional_dependency_entries, dependent_option, inherited_options, visited, resolved ) self._optional_dependencies_complex = { option: dict(sorted(entries.items())) for option, entries in sorted(optional_dependency_entries.items()) } return self._optional_dependencies_complex @property def optional_dependencies(self) -> dict[str, list[str]]: """ https://peps.python.org/pep-0621/#dependencies-optional-dependencies """ if self._optional_dependencies is None: self._optional_dependencies = { option: list(entries) for option, entries in self.optional_dependencies_complex.items() } return self._optional_dependencies @property def dynamic(self) -> list[str]: """ https://peps.python.org/pep-0621/#dynamic """ if self._dynamic is None: dynamic = self.config.get("dynamic", []) if not isinstance(dynamic, list): message = "Field `project.dynamic` must be an array" raise TypeError(message) if not all(isinstance(entry, str) for entry in dynamic): message = "Field `project.dynamic` must only contain strings" raise TypeError(message) self._dynamic = sorted(dynamic) return self._dynamic def add_known_classifiers(self, classifiers: list[str]) -> None: self._extra_classifiers.update(classifiers) def validate_fields(self) -> None: # Trigger validation for everything for attribute in dir(self): getattr(self, attribute) @staticmethod def __classifier_is_private(classifier: str) -> bool: return classifier.lower().startswith("private ::") class HatchMetadata(Generic[PluginManagerBound]): def __init__(self, root: str, config: dict[str, dict[str, Any]], plugin_manager: PluginManagerBound) -> None: self.root = root self.config = config self.plugin_manager = plugin_manager self._metadata: HatchMetadataSettings | None = None self._build_config: dict[str, Any] | None = None self._build_targets: dict[str, Any] | None = None self._version: HatchVersionConfig | None = None @property def metadata(self) -> HatchMetadataSettings: if self._metadata is None: metadata_config = self.config.get("metadata", {}) if not isinstance(metadata_config, dict): message = "Field `tool.hatch.metadata` must be a table" raise TypeError(message) self._metadata = HatchMetadataSettings(self.root, metadata_config, self.plugin_manager) return self._metadata @property def build_config(self) -> dict[str, Any]: if self._build_config is None: build_config = self.config.get("build", {}) if not isinstance(build_config, dict): message = "Field `tool.hatch.build` must be a table" raise TypeError(message) self._build_config = build_config return self._build_config @property def build_targets(self) -> dict[str, Any]: if self._build_targets is None: build_targets: dict = self.build_config.get("targets", {}) if not isinstance(build_targets, dict): message = "Field `tool.hatch.build.targets` must be a table" raise TypeError(message) self._build_targets = build_targets return self._build_targets @property def version(self) -> HatchVersionConfig: if self._version is None: if "version" not in self.config: message = "Missing `tool.hatch.version` configuration" raise ValueError(message) options = self.config["version"] if not isinstance(options, dict): message = "Field `tool.hatch.version` must be a table" raise TypeError(message) self._version = HatchVersionConfig(self.root, deepcopy(options), self.plugin_manager) return self._version class HatchVersionConfig(Generic[PluginManagerBound]): def __init__(self, root: str, config: dict[str, Any], plugin_manager: PluginManagerBound) -> None: self.root = root self.config = config self.plugin_manager = plugin_manager self._cached: str | None = None self._source_name: str | None = None self._scheme_name: str | None = None self._source: VersionSourceInterface | None = None self._scheme: VersionSchemeInterface | None = None @property def cached(self) -> str: if self._cached is None: try: self._cached = self.source.get_version_data()["version"] except Exception as e: # noqa: BLE001 message = f"Error getting the version from source `{self.source.PLUGIN_NAME}`: {e}" raise type(e)(message) from None return self._cached @property def source_name(self) -> str: if self._source_name is None: source: str = self.config.get("source", "regex") if not source: message = "The `source` option under the `tool.hatch.version` table must not be empty if defined" raise ValueError(message) if not isinstance(source, str): message = "Field `tool.hatch.version.source` must be a string" raise TypeError(message) self._source_name = source return self._source_name @property def scheme_name(self) -> str: if self._scheme_name is None: scheme: str = self.config.get("scheme", "standard") if not scheme: message = "The `scheme` option under the `tool.hatch.version` table must not be empty if defined" raise ValueError(message) if not isinstance(scheme, str): message = "Field `tool.hatch.version.scheme` must be a string" raise TypeError(message) self._scheme_name = scheme return self._scheme_name @property def source(self) -> VersionSourceInterface: if self._source is None: from copy import deepcopy source_name = self.source_name version_source = self.plugin_manager.version_source.get(source_name) if version_source is None: from hatchling.plugin.exceptions import UnknownPluginError message = f"Unknown version source: {source_name}" raise UnknownPluginError(message) self._source = version_source(self.root, deepcopy(self.config)) return self._source @property def scheme(self) -> VersionSchemeInterface: if self._scheme is None: from copy import deepcopy scheme_name = self.scheme_name version_scheme = self.plugin_manager.version_scheme.get(scheme_name) if version_scheme is None: from hatchling.plugin.exceptions import UnknownPluginError message = f"Unknown version scheme: {scheme_name}" raise UnknownPluginError(message) self._scheme = version_scheme(self.root, deepcopy(self.config)) return self._scheme class HatchMetadataSettings(Generic[PluginManagerBound]): def __init__(self, root: str, config: dict[str, Any], plugin_manager: PluginManagerBound) -> None: self.root = root self.config = config self.plugin_manager = plugin_manager self._allow_direct_references: bool | None = None self._allow_ambiguous_features: bool | None = None self._hook_config: dict[str, Any] | None = None self._hooks: dict[str, MetadataHookInterface] | None = None @property def allow_direct_references(self) -> bool: if self._allow_direct_references is None: allow_direct_references: bool = self.config.get("allow-direct-references", False) if not isinstance(allow_direct_references, bool): message = "Field `tool.hatch.metadata.allow-direct-references` must be a boolean" raise TypeError(message) self._allow_direct_references = allow_direct_references return self._allow_direct_references @property def allow_ambiguous_features(self) -> bool: # TODO: remove in the first minor release after Jan 1, 2024 if self._allow_ambiguous_features is None: allow_ambiguous_features: bool = self.config.get("allow-ambiguous-features", False) if not isinstance(allow_ambiguous_features, bool): message = "Field `tool.hatch.metadata.allow-ambiguous-features` must be a boolean" raise TypeError(message) self._allow_ambiguous_features = allow_ambiguous_features return self._allow_ambiguous_features @property def hook_config(self) -> dict[str, Any]: if self._hook_config is None: hook_config: dict[str, Any] = self.config.get("hooks", {}) if not isinstance(hook_config, dict): message = "Field `tool.hatch.metadata.hooks` must be a table" raise TypeError(message) self._hook_config = hook_config return self._hook_config @property def hooks(self) -> dict[str, MetadataHookInterface]: if self._hooks is None: hook_config = self.hook_config configured_hooks = {} for hook_name, config in hook_config.items(): metadata_hook = self.plugin_manager.metadata_hook.get(hook_name) if metadata_hook is None: from hatchling.plugin.exceptions import UnknownPluginError message = f"Unknown metadata hook: {hook_name}" raise UnknownPluginError(message) configured_hooks[hook_name] = metadata_hook(self.root, config) self._hooks = configured_hooks return self._hooks def _resolve_optional_dependencies( optional_dependencies_complex, dependent_option, inherited_options, visited, resolved ): if dependent_option in resolved: return if dependent_option in visited: message = f"Field `project.optional-dependencies` defines a circular dependency group: {dependent_option}" raise ValueError(message) visited.add(dependent_option) if dependent_option in inherited_options: for selected_option in inherited_options[dependent_option]: _resolve_optional_dependencies( optional_dependencies_complex, selected_option, inherited_options, visited, resolved ) if selected_option not in optional_dependencies_complex: message = ( f"Unknown recursive dependency group in field `project.optional-dependencies`: {selected_option}" ) raise ValueError(message) optional_dependencies_complex[dependent_option].update(optional_dependencies_complex[selected_option]) resolved.add(dependent_option) visited.remove(dependent_option) ================================================ FILE: backend/src/hatchling/metadata/custom.py ================================================ from __future__ import annotations import os from typing import Any from hatchling.metadata.plugin.interface import MetadataHookInterface from hatchling.plugin.utils import load_plugin_from_script from hatchling.utils.constants import DEFAULT_BUILD_SCRIPT class CustomMetadataHook: PLUGIN_NAME = "custom" def __new__( # type: ignore[misc] cls, root: str, config: dict[str, Any], *args: Any, **kwargs: Any, ) -> MetadataHookInterface: build_script = config.get("path", DEFAULT_BUILD_SCRIPT) if not isinstance(build_script, str): message = f"Option `path` for metadata hook `{cls.PLUGIN_NAME}` must be a string" raise TypeError(message) if not build_script: message = f"Option `path` for metadata hook `{cls.PLUGIN_NAME}` must not be empty if defined" raise ValueError(message) path = os.path.normpath(os.path.join(root, build_script)) if not os.path.isfile(path): message = f"Build script does not exist: {build_script}" raise OSError(message) hook_class = load_plugin_from_script(path, build_script, MetadataHookInterface, "metadata_hook") # type: ignore[type-abstract] hook = hook_class(root, config, *args, **kwargs) # Always keep the name to avoid confusion hook.PLUGIN_NAME = cls.PLUGIN_NAME return hook ================================================ FILE: backend/src/hatchling/metadata/plugin/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/metadata/plugin/hooks.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from hatchling.metadata.custom import CustomMetadataHook from hatchling.plugin import hookimpl if TYPE_CHECKING: from hatchling.metadata.plugin.interface import MetadataHookInterface @hookimpl def hatch_register_metadata_hook() -> type[MetadataHookInterface]: return CustomMetadataHook # type: ignore[return-value] ================================================ FILE: backend/src/hatchling/metadata/plugin/interface.py ================================================ from __future__ import annotations from abc import ABC, abstractmethod class MetadataHookInterface(ABC): # no cov """ Example usage: ```python tab="plugin.py" from hatchling.metadata.plugin.interface import MetadataHookInterface class SpecialMetadataHook(MetadataHookInterface): PLUGIN_NAME = "special" ... ``` ```python tab="hooks.py" from hatchling.plugin import hookimpl from .plugin import SpecialMetadataHook @hookimpl def hatch_register_metadata_hook(): return SpecialMetadataHook ``` """ PLUGIN_NAME = "" """The name used for selection.""" def __init__(self, root: str, config: dict) -> None: self.__root = root self.__config = config @property def root(self) -> str: """ The root of the project tree. """ return self.__root @property def config(self) -> dict: """ The hook configuration. ```toml config-example [tool.hatch.metadata.hooks.] ``` """ return self.__config @abstractmethod def update(self, metadata: dict) -> None: """ This updates the metadata mapping of the `project` table in-place. """ def get_known_classifiers(self) -> list[str]: # noqa: PLR6301 """ This returns extra classifiers that should be considered valid in addition to the ones known to PyPI. """ return [] ================================================ FILE: backend/src/hatchling/metadata/spec.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from collections.abc import Callable from hatchling.metadata.core import ProjectMetadata DEFAULT_METADATA_VERSION = "2.4" LATEST_METADATA_VERSION = "2.4" CORE_METADATA_PROJECT_FIELDS = { "Author": ("authors",), "Author-email": ("authors",), "Classifier": ("classifiers",), "Description": ("readme",), "Description-Content-Type": ("readme",), "Dynamic": ("dynamic",), "Keywords": ("keywords",), "License": ("license",), "License-Expression": ("license",), "License-Files": ("license-files",), "Maintainer": ("maintainers",), "Maintainer-email": ("maintainers",), "Name": ("name",), "Provides-Extra": ("dependencies", "optional-dependencies"), "Requires-Dist": ("dependencies",), "Requires-Python": ("requires-python",), "Summary": ("description",), "Project-URL": ("urls",), "Version": ("version",), } PROJECT_CORE_METADATA_FIELDS = { "authors": ("Author", "Author-email"), "classifiers": ("Classifier",), "dependencies": ("Requires-Dist",), "dynamic": ("Dynamic",), "keywords": ("Keywords",), "license": ("License", "License-Expression"), "license-files": ("License-Files",), "maintainers": ("Maintainer", "Maintainer-email"), "name": ("Name",), "optional-dependencies": ("Requires-Dist", "Provides-Extra"), "readme": ("Description", "Description-Content-Type"), "requires-python": ("Requires-Python",), "description": ("Summary",), "urls": ("Project-URL",), "version": ("Version",), } def get_core_metadata_constructors() -> dict[str, Callable]: """ https://packaging.python.org/specifications/core-metadata/ """ return { "1.2": construct_metadata_file_1_2, "2.1": construct_metadata_file_2_1, "2.2": construct_metadata_file_2_2, "2.3": construct_metadata_file_2_3, "2.4": construct_metadata_file_2_4, } def project_metadata_from_core_metadata(core_metadata: str) -> dict[str, Any]: # https://packaging.python.org/en/latest/specifications/core-metadata/ import email from email.headerregistry import HeaderRegistry header_registry = HeaderRegistry() message = email.message_from_string(core_metadata) metadata: dict[str, Any] = {} if name := message.get("Name"): metadata["name"] = name else: error_message = "Missing required core metadata: Name" raise ValueError(error_message) if version := message.get("Version"): metadata["version"] = version else: error_message = "Missing required core metadata: Version" raise ValueError(error_message) if (dynamic_fields := message.get_all("Dynamic")) is not None: # Use as an ordered set to retain bidirectional formatting. # This likely doesn't matter but we try hard around here. metadata["dynamic"] = list({ project_field: None for core_metadata_field in dynamic_fields for project_field in CORE_METADATA_PROJECT_FIELDS.get(core_metadata_field, ()) }) if description := message.get_payload(): metadata["readme"] = { "content-type": message.get("Description-Content-Type", "text/plain"), "text": description, } if (license_expression := message.get("License-Expression")) is not None: metadata["license"] = license_expression elif (license_text := message.get("License")) is not None: metadata["license"] = {"text": license_text} if (license_files := message.get_all("License-File")) is not None: metadata["license-files"] = license_files if (summary := message.get("Summary")) is not None: metadata["description"] = summary if (keywords := message.get("Keywords")) is not None: metadata["keywords"] = keywords.split(",") if (classifiers := message.get_all("Classifier")) is not None: metadata["classifiers"] = classifiers if (project_urls := message.get_all("Project-URL")) is not None: urls = {} for project_url in project_urls: label, url = project_url.split(",", maxsplit=1) urls[label.strip()] = url.strip() metadata["urls"] = urls authors = [] if (author := message.get("Author")) is not None: authors.append({"name": author}) if (author_email := message.get("Author-email")) is not None: address_header = header_registry("resent-from", author_email) for address in address_header.addresses: # type: ignore[attr-defined] data = {"email": address.addr_spec} if name := address.display_name: data["name"] = name authors.append(data) if authors: metadata["authors"] = authors maintainers = [] if (maintainer := message.get("Maintainer")) is not None: maintainers.append({"name": maintainer}) if (maintainer_email := message.get("Maintainer-email")) is not None: address_header = header_registry("resent-from", maintainer_email) for address in address_header.addresses: # type: ignore[attr-defined] data = {"email": address.addr_spec} if name := address.display_name: data["name"] = name maintainers.append(data) if maintainers: metadata["maintainers"] = maintainers if (requires_python := message.get("Requires-Python")) is not None: metadata["requires-python"] = requires_python optional_dependencies: dict[str, list[str]] = {} if (extras := message.get_all("Provides-Extra")) is not None: for extra in extras: optional_dependencies[extra] = [] if (requirements := message.get_all("Requires-Dist")) is not None: from packaging.requirements import Requirement dependencies = [] for requirement in requirements: req = Requirement(requirement) if req.marker is None: dependencies.append(str(req)) continue markers = req.marker._markers # noqa: SLF001 for i, marker in enumerate(markers): if isinstance(marker, tuple): left, _, right = marker if left.value == "extra": extra = right.value del markers[i] # noqa: B909 # If there was only one marker then there will be an unnecessary # trailing semicolon in the string representation if not markers: req.marker = None # Otherwise we need to remove the preceding `and` operation else: del markers[i - 1] optional_dependencies.setdefault(extra, []).append(str(req)) break else: dependencies.append(str(req)) metadata["dependencies"] = dependencies if optional_dependencies: metadata["optional-dependencies"] = optional_dependencies return metadata def construct_metadata_file_1_2(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str: """ https://peps.python.org/pep-0345/ """ metadata_file = "Metadata-Version: 1.2\n" metadata_file += f"Name: {metadata.core.raw_name}\n" metadata_file += f"Version: {metadata.version}\n" if metadata.core.description: metadata_file += f"Summary: {metadata.core.description}\n" if metadata.core.urls: for label, url in metadata.core.urls.items(): metadata_file += f"Project-URL: {label}, {url}\n" authors_data = metadata.core.authors_data if authors_data["name"]: metadata_file += f"Author: {', '.join(authors_data['name'])}\n" if authors_data["email"]: metadata_file += f"Author-email: {', '.join(authors_data['email'])}\n" maintainers_data = metadata.core.maintainers_data if maintainers_data["name"]: metadata_file += f"Maintainer: {', '.join(maintainers_data['name'])}\n" if maintainers_data["email"]: metadata_file += f"Maintainer-email: {', '.join(maintainers_data['email'])}\n" if metadata.core.license: license_start = "License: " indent = " " * (len(license_start) - 1) metadata_file += license_start for i, line in enumerate(metadata.core.license.splitlines()): if i == 0: metadata_file += f"{line}\n" else: metadata_file += f"{indent}{line}\n" elif metadata.core.license_expression: metadata_file += f"License: {metadata.core.license_expression}\n" if metadata.core.keywords: metadata_file += f"Keywords: {','.join(metadata.core.keywords)}\n" if metadata.core.classifiers: for classifier in metadata.core.classifiers: metadata_file += f"Classifier: {classifier}\n" if metadata.core.requires_python: metadata_file += f"Requires-Python: {metadata.core.requires_python}\n" if metadata.core.dependencies: for dependency in metadata.core.dependencies: metadata_file += f"Requires-Dist: {dependency}\n" if extra_dependencies: for dependency in extra_dependencies: metadata_file += f"Requires-Dist: {dependency}\n" return metadata_file def construct_metadata_file_2_1(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str: """ https://peps.python.org/pep-0566/ """ metadata_file = "Metadata-Version: 2.1\n" metadata_file += f"Name: {metadata.core.raw_name}\n" metadata_file += f"Version: {metadata.version}\n" if metadata.core.description: metadata_file += f"Summary: {metadata.core.description}\n" if metadata.core.urls: for label, url in metadata.core.urls.items(): metadata_file += f"Project-URL: {label}, {url}\n" authors_data = metadata.core.authors_data if authors_data["name"]: metadata_file += f"Author: {', '.join(authors_data['name'])}\n" if authors_data["email"]: metadata_file += f"Author-email: {', '.join(authors_data['email'])}\n" maintainers_data = metadata.core.maintainers_data if maintainers_data["name"]: metadata_file += f"Maintainer: {', '.join(maintainers_data['name'])}\n" if maintainers_data["email"]: metadata_file += f"Maintainer-email: {', '.join(maintainers_data['email'])}\n" if metadata.core.license: license_start = "License: " indent = " " * (len(license_start) - 1) metadata_file += license_start for i, line in enumerate(metadata.core.license.splitlines()): if i == 0: metadata_file += f"{line}\n" else: metadata_file += f"{indent}{line}\n" elif metadata.core.license_expression: metadata_file += f"License: {metadata.core.license_expression}\n" if metadata.core.keywords: metadata_file += f"Keywords: {','.join(metadata.core.keywords)}\n" if metadata.core.classifiers: for classifier in metadata.core.classifiers: metadata_file += f"Classifier: {classifier}\n" if metadata.core.requires_python: metadata_file += f"Requires-Python: {metadata.core.requires_python}\n" if metadata.core.dependencies: for dependency in metadata.core.dependencies: metadata_file += f"Requires-Dist: {dependency}\n" if extra_dependencies: for dependency in extra_dependencies: metadata_file += f"Requires-Dist: {dependency}\n" if metadata.core.optional_dependencies: for option, dependencies in metadata.core.optional_dependencies.items(): metadata_file += f"Provides-Extra: {option}\n" for dependency in dependencies: if ";" in dependency: dep_name, dep_env_marker = dependency.split(";", maxsplit=1) metadata_file += f"Requires-Dist: {dep_name}; ({dep_env_marker.strip()}) and extra == {option!r}\n" elif "@ " in dependency: metadata_file += f"Requires-Dist: {dependency} ; extra == {option!r}\n" else: metadata_file += f"Requires-Dist: {dependency}; extra == {option!r}\n" if metadata.core.readme: metadata_file += f"Description-Content-Type: {metadata.core.readme_content_type}\n" metadata_file += f"\n{metadata.core.readme}" return metadata_file def construct_metadata_file_2_2(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str: """ https://peps.python.org/pep-0643/ """ metadata_file = "Metadata-Version: 2.2\n" metadata_file += f"Name: {metadata.core.raw_name}\n" metadata_file += f"Version: {metadata.version}\n" if metadata.core.dynamic: # Ordered set for field in { core_metadata_field: None for project_field in metadata.core.dynamic for core_metadata_field in PROJECT_CORE_METADATA_FIELDS.get(project_field, ()) }: metadata_file += f"Dynamic: {field}\n" if metadata.core.description: metadata_file += f"Summary: {metadata.core.description}\n" if metadata.core.urls: for label, url in metadata.core.urls.items(): metadata_file += f"Project-URL: {label}, {url}\n" authors_data = metadata.core.authors_data if authors_data["name"]: metadata_file += f"Author: {', '.join(authors_data['name'])}\n" if authors_data["email"]: metadata_file += f"Author-email: {', '.join(authors_data['email'])}\n" maintainers_data = metadata.core.maintainers_data if maintainers_data["name"]: metadata_file += f"Maintainer: {', '.join(maintainers_data['name'])}\n" if maintainers_data["email"]: metadata_file += f"Maintainer-email: {', '.join(maintainers_data['email'])}\n" if metadata.core.license: license_start = "License: " indent = " " * (len(license_start) - 1) metadata_file += license_start for i, line in enumerate(metadata.core.license.splitlines()): if i == 0: metadata_file += f"{line}\n" else: metadata_file += f"{indent}{line}\n" elif metadata.core.license_expression: metadata_file += f"License: {metadata.core.license_expression}\n" if metadata.core.keywords: metadata_file += f"Keywords: {','.join(metadata.core.keywords)}\n" if metadata.core.classifiers: for classifier in metadata.core.classifiers: metadata_file += f"Classifier: {classifier}\n" if metadata.core.requires_python: metadata_file += f"Requires-Python: {metadata.core.requires_python}\n" if metadata.core.dependencies: for dependency in metadata.core.dependencies: metadata_file += f"Requires-Dist: {dependency}\n" if extra_dependencies: for dependency in extra_dependencies: metadata_file += f"Requires-Dist: {dependency}\n" if metadata.core.optional_dependencies: for option, dependencies in metadata.core.optional_dependencies.items(): metadata_file += f"Provides-Extra: {option}\n" for dependency in dependencies: if ";" in dependency: dep_name, dep_env_marker = dependency.split(";", maxsplit=1) metadata_file += f"Requires-Dist: {dep_name}; ({dep_env_marker.strip()}) and extra == {option!r}\n" elif "@ " in dependency: metadata_file += f"Requires-Dist: {dependency} ; extra == {option!r}\n" else: metadata_file += f"Requires-Dist: {dependency}; extra == {option!r}\n" if metadata.core.readme: metadata_file += f"Description-Content-Type: {metadata.core.readme_content_type}\n" metadata_file += f"\n{metadata.core.readme}" return metadata_file def construct_metadata_file_2_3(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str: """ https://peps.python.org/pep-0685/ """ metadata_file = "Metadata-Version: 2.3\n" metadata_file += f"Name: {metadata.core.raw_name}\n" metadata_file += f"Version: {metadata.version}\n" if metadata.core.dynamic: # Ordered set for field in { core_metadata_field: None for project_field in metadata.core.dynamic for core_metadata_field in PROJECT_CORE_METADATA_FIELDS.get(project_field, ()) }: metadata_file += f"Dynamic: {field}\n" if metadata.core.description: metadata_file += f"Summary: {metadata.core.description}\n" if metadata.core.urls: for label, url in metadata.core.urls.items(): metadata_file += f"Project-URL: {label}, {url}\n" authors_data = metadata.core.authors_data if authors_data["name"]: metadata_file += f"Author: {', '.join(authors_data['name'])}\n" if authors_data["email"]: metadata_file += f"Author-email: {', '.join(authors_data['email'])}\n" maintainers_data = metadata.core.maintainers_data if maintainers_data["name"]: metadata_file += f"Maintainer: {', '.join(maintainers_data['name'])}\n" if maintainers_data["email"]: metadata_file += f"Maintainer-email: {', '.join(maintainers_data['email'])}\n" if metadata.core.license: license_start = "License: " indent = " " * (len(license_start) - 1) metadata_file += license_start for i, line in enumerate(metadata.core.license.splitlines()): if i == 0: metadata_file += f"{line}\n" else: metadata_file += f"{indent}{line}\n" elif metadata.core.license_expression: metadata_file += f"License: {metadata.core.license_expression}\n" if metadata.core.keywords: metadata_file += f"Keywords: {','.join(metadata.core.keywords)}\n" if metadata.core.classifiers: for classifier in metadata.core.classifiers: metadata_file += f"Classifier: {classifier}\n" if metadata.core.requires_python: metadata_file += f"Requires-Python: {metadata.core.requires_python}\n" if metadata.core.dependencies: for dependency in metadata.core.dependencies: metadata_file += f"Requires-Dist: {dependency}\n" if extra_dependencies: for dependency in extra_dependencies: metadata_file += f"Requires-Dist: {dependency}\n" if metadata.core.optional_dependencies: for option, dependencies in metadata.core.optional_dependencies.items(): metadata_file += f"Provides-Extra: {option}\n" for dependency in dependencies: if ";" in dependency: dep_name, dep_env_marker = dependency.split(";", maxsplit=1) metadata_file += f"Requires-Dist: {dep_name}; ({dep_env_marker.strip()}) and extra == {option!r}\n" elif "@ " in dependency: metadata_file += f"Requires-Dist: {dependency} ; extra == {option!r}\n" else: metadata_file += f"Requires-Dist: {dependency}; extra == {option!r}\n" if metadata.core.readme: metadata_file += f"Description-Content-Type: {metadata.core.readme_content_type}\n" metadata_file += f"\n{metadata.core.readme}" return metadata_file def construct_metadata_file_2_4(metadata: ProjectMetadata, extra_dependencies: tuple[str] | None = None) -> str: """ https://peps.python.org/pep-0639/ """ metadata_file = "Metadata-Version: 2.4\n" metadata_file += f"Name: {metadata.core.raw_name}\n" metadata_file += f"Version: {metadata.version}\n" if metadata.core.dynamic: # Ordered set for field in { core_metadata_field: None for project_field in metadata.core.dynamic for core_metadata_field in PROJECT_CORE_METADATA_FIELDS.get(project_field, ()) }: metadata_file += f"Dynamic: {field}\n" if metadata.core.description: metadata_file += f"Summary: {metadata.core.description}\n" if metadata.core.urls: for label, url in metadata.core.urls.items(): metadata_file += f"Project-URL: {label}, {url}\n" authors_data = metadata.core.authors_data if authors_data["name"]: metadata_file += f"Author: {', '.join(authors_data['name'])}\n" if authors_data["email"]: metadata_file += f"Author-email: {', '.join(authors_data['email'])}\n" maintainers_data = metadata.core.maintainers_data if maintainers_data["name"]: metadata_file += f"Maintainer: {', '.join(maintainers_data['name'])}\n" if maintainers_data["email"]: metadata_file += f"Maintainer-email: {', '.join(maintainers_data['email'])}\n" if metadata.core.license: license_start = "License: " indent = " " * (len(license_start) - 1) metadata_file += license_start for i, line in enumerate(metadata.core.license.splitlines()): if i == 0: metadata_file += f"{line}\n" else: metadata_file += f"{indent}{line}\n" if metadata.core.license_expression: metadata_file += f"License-Expression: {metadata.core.license_expression}\n" if metadata.core.license_files: for license_file in metadata.core.license_files: metadata_file += f"License-File: {license_file}\n" if metadata.core.keywords: metadata_file += f"Keywords: {','.join(metadata.core.keywords)}\n" if metadata.core.classifiers: for classifier in metadata.core.classifiers: metadata_file += f"Classifier: {classifier}\n" if metadata.core.requires_python: metadata_file += f"Requires-Python: {metadata.core.requires_python}\n" if metadata.core.dependencies: for dependency in metadata.core.dependencies: metadata_file += f"Requires-Dist: {dependency}\n" if extra_dependencies: for dependency in extra_dependencies: metadata_file += f"Requires-Dist: {dependency}\n" if metadata.core.optional_dependencies: for option, dependencies in metadata.core.optional_dependencies.items(): metadata_file += f"Provides-Extra: {option}\n" for dependency in dependencies: if ";" in dependency: dep_name, dep_env_marker = dependency.split(";", maxsplit=1) metadata_file += f"Requires-Dist: {dep_name}; ({dep_env_marker.strip()}) and extra == {option!r}\n" elif "@ " in dependency: metadata_file += f"Requires-Dist: {dependency} ; extra == {option!r}\n" else: metadata_file += f"Requires-Dist: {dependency}; extra == {option!r}\n" if metadata.core.readme: metadata_file += f"Description-Content-Type: {metadata.core.readme_content_type}\n" metadata_file += f"\n{metadata.core.readme}" return metadata_file ================================================ FILE: backend/src/hatchling/metadata/utils.py ================================================ from __future__ import annotations import re from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from packaging.requirements import Requirement from hatchling.metadata.core import ProjectMetadata # NOTE: this module should rarely be changed because it is likely to be used by other packages like Hatch def is_valid_project_name(project_name: str) -> bool: # https://peps.python.org/pep-0508/#names return re.search("^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", project_name, re.IGNORECASE) is not None def normalize_project_name(project_name: str) -> str: # https://peps.python.org/pep-0503/#normalized-names return re.sub(r"[-_.]+", "-", project_name).lower() def normalize_requirement(requirement: Requirement) -> None: # Changes to this function affect reproducibility between versions from packaging.specifiers import SpecifierSet requirement.name = normalize_project_name(requirement.name) if requirement.specifier: requirement.specifier = SpecifierSet(str(requirement.specifier).lower()) if requirement.extras: requirement.extras = {normalize_project_name(extra) for extra in requirement.extras} def format_dependency(requirement: Requirement) -> str: # All TOML writers use double quotes, so allow direct writing or copy/pasting to avoid escaping return str(requirement).replace('"', "'") def get_normalized_dependency(requirement: Requirement) -> str: normalize_requirement(requirement) return format_dependency(requirement) def resolve_metadata_fields(metadata: ProjectMetadata) -> dict[str, Any]: # https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ return { "name": metadata.core.name, "version": metadata.version, "description": metadata.core.description, "readme": {"content-type": metadata.core.readme_content_type, "text": metadata.core.readme}, "requires-python": metadata.core.requires_python, "license": metadata.core.license_expression or metadata.core.license, "authors": metadata.core.authors, "maintainers": metadata.core.maintainers, "keywords": metadata.core.keywords, "classifiers": metadata.core.classifiers, "urls": metadata.core.urls, "scripts": metadata.core.scripts, "gui-scripts": metadata.core.gui_scripts, "entry-points": metadata.core.entry_points, "dependencies": metadata.core.dependencies, "optional-dependencies": metadata.core.optional_dependencies, } ================================================ FILE: backend/src/hatchling/ouroboros.py ================================================ from __future__ import annotations import os import re from ast import literal_eval from typing import Any from hatchling.build import * # noqa: F403 def read_dependencies() -> list[str]: pattern = r"^dependencies = (\[.*?\])$" with open(os.path.join(os.getcwd(), "pyproject.toml"), encoding="utf-8") as f: # Windows \r\n prevents match contents = "\n".join(line.rstrip() for line in f) match = re.search(pattern, contents, flags=re.MULTILINE | re.DOTALL) if match is None: message = "No dependencies found" raise ValueError(message) return literal_eval(match.group(1)) def get_requires_for_build_sdist( # type: ignore[no-redef] config_settings: dict[str, Any] | None = None, # noqa: ARG001 ) -> list[str]: """ https://peps.python.org/pep-0517/#get-requires-for-build-sdist """ return read_dependencies() def get_requires_for_build_wheel( # type: ignore[no-redef] config_settings: dict[str, Any] | None = None, # noqa: ARG001 ) -> list[str]: """ https://peps.python.org/pep-0517/#get-requires-for-build-wheel """ return read_dependencies() def get_requires_for_build_editable( # type: ignore[no-redef] config_settings: dict[str, Any] | None = None, # noqa: ARG001 ) -> list[str]: """ https://peps.python.org/pep-0660/#get-requires-for-build-editable """ from hatchling.builders.constants import EDITABLES_REQUIREMENT return [*read_dependencies(), EDITABLES_REQUIREMENT] ================================================ FILE: backend/src/hatchling/plugin/__init__.py ================================================ import pluggy hookimpl = pluggy.HookimplMarker("hatch") ================================================ FILE: backend/src/hatchling/plugin/exceptions.py ================================================ class UnknownPluginError(ValueError): pass ================================================ FILE: backend/src/hatchling/plugin/manager.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING, TypeVar import pluggy if TYPE_CHECKING: from collections.abc import Callable class PluginManager: def __init__(self) -> None: self.manager = pluggy.PluginManager("hatch") self.third_party_plugins = ThirdPartyPlugins(self.manager) self.initialized = False def initialize(self) -> None: from hatchling.plugin import specs self.manager.add_hookspecs(specs) def __getattr__(self, name: str) -> ClassRegister: if not self.initialized: self.initialize() self.initialized = True hook_name = f"hatch_register_{name}" hook = getattr(self, hook_name, None) if hook: hook() register = ClassRegister(getattr(self.manager.hook, hook_name), "PLUGIN_NAME", self.third_party_plugins) setattr(self, name, register) return register def hatch_register_version_source(self) -> None: from hatchling.version.source.plugin import hooks self.manager.register(hooks) def hatch_register_version_scheme(self) -> None: from hatchling.version.scheme.plugin import hooks self.manager.register(hooks) def hatch_register_builder(self) -> None: from hatchling.builders.plugin import hooks self.manager.register(hooks) def hatch_register_build_hook(self) -> None: from hatchling.builders.hooks.plugin import hooks self.manager.register(hooks) def hatch_register_metadata_hook(self) -> None: from hatchling.metadata.plugin import hooks self.manager.register(hooks) class ClassRegister: def __init__(self, registration_method: Callable, identifier: str, third_party_plugins: ThirdPartyPlugins) -> None: self.registration_method = registration_method self.identifier = identifier self.third_party_plugins = third_party_plugins def collect(self, *, include_third_party: bool = True) -> dict: if include_third_party and not self.third_party_plugins.loaded: self.third_party_plugins.load() classes: dict[str, type] = {} for raw_registered_classes in self.registration_method(): registered_classes = ( raw_registered_classes if isinstance(raw_registered_classes, list) else [raw_registered_classes] ) for registered_class in registered_classes: name = getattr(registered_class, self.identifier, None) if not name: # no cov message = f"Class `{registered_class.__name__}` does not have a {name} attribute." raise ValueError(message) if name in classes: # no cov message = ( f"Class `{registered_class.__name__}` defines its name as `{name}` but " f"that name is already used by `{classes[name].__name__}`." ) raise ValueError(message) classes[name] = registered_class return classes def get(self, name: str) -> type | None: if not self.third_party_plugins.loaded: classes = self.collect(include_third_party=False) if name in classes: return classes[name] return self.collect().get(name) class ThirdPartyPlugins: def __init__(self, manager: pluggy.PluginManager) -> None: self.manager = manager self.loaded = False def load(self) -> None: self.manager.load_setuptools_entrypoints("hatch") self.loaded = True PluginManagerBound = TypeVar("PluginManagerBound", bound=PluginManager) ================================================ FILE: backend/src/hatchling/plugin/specs.py ================================================ import pluggy hookspec = pluggy.HookspecMarker("hatch") @hookspec def hatch_register_version_source() -> None: """Register new classes that adhere to the version source interface.""" @hookspec def hatch_register_builder() -> None: """Register new classes that adhere to the builder interface.""" @hookspec def hatch_register_build_hook() -> None: """Register new classes that adhere to the build hook interface.""" @hookspec def hatch_register_metadata_hook() -> None: """Register new classes that adhere to the metadata hook interface.""" ================================================ FILE: backend/src/hatchling/plugin/utils.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING, TypeVar if TYPE_CHECKING: from hatchling.builders.hooks.plugin.interface import BuildHookInterface from hatchling.builders.plugin.interface import BuilderInterface from hatchling.metadata.plugin.interface import MetadataHookInterface T = TypeVar("T", BuilderInterface, BuildHookInterface, MetadataHookInterface) def load_plugin_from_script(path: str, script_name: str, plugin_class: type[T], plugin_id: str) -> type[T]: from importlib.util import module_from_spec, spec_from_file_location spec = spec_from_file_location(script_name, path) module = module_from_spec(spec) # type: ignore[arg-type] spec.loader.exec_module(module) # type: ignore[union-attr] plugin_finder = f"get_{plugin_id}" names = dir(module) if plugin_finder in names: return getattr(module, plugin_finder)() subclasses = [] for name in names: obj = getattr(module, name) if obj is plugin_class: continue try: if issubclass(obj, plugin_class): subclasses.append(obj) except TypeError: continue if not subclasses: message = f"Unable to find a subclass of `{plugin_class.__name__}` in `{script_name}`: {path}" raise ValueError(message) if len(subclasses) > 1: message = ( f"Multiple subclasses of `{plugin_class.__name__}` found in `{script_name}`, " f"select one by defining a function named `{plugin_finder}`: {path}" ) raise ValueError(message) return subclasses[0] ================================================ FILE: backend/src/hatchling/py.typed ================================================ ================================================ FILE: backend/src/hatchling/utils/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/utils/constants.py ================================================ DEFAULT_BUILD_SCRIPT = "hatch_build.py" DEFAULT_CONFIG_FILE = "hatch.toml" class VersionEnvVars: VALIDATE_BUMP = "HATCH_VERSION_VALIDATE_BUMP" ================================================ FILE: backend/src/hatchling/utils/context.py ================================================ from __future__ import annotations import os import string from abc import ABC, abstractmethod from collections import ChainMap from contextlib import contextmanager from typing import TYPE_CHECKING, Any from hatchling.utils.fs import path_to_uri if TYPE_CHECKING: from collections.abc import Iterable, Iterator, Mapping, MutableMapping, Sequence class ContextFormatter(ABC): @abstractmethod def get_formatters(self) -> MutableMapping: """ This returns a mapping of supported field names to their respective formatting functions. Each function accepts 2 arguments: - the `value` that was passed to the format call, defaulting to `None` - the modifier `data`, defaulting to an empty string """ @classmethod def format_path(cls, path: str, modifier: str) -> str: if not modifier: return os.path.normpath(path) modifiers = modifier.split(":")[::-1] while modifiers and modifiers[-1] == "parent": path = os.path.dirname(path) modifiers.pop() if not modifiers: return path if len(modifiers) > 1: message = f"Expected a single path modifier and instead got: {', '.join(reversed(modifiers))}" raise ValueError(message) modifier = modifiers[0] if modifier == "uri": return path_to_uri(path) if modifier == "real": return os.path.realpath(path) message = f"Unknown path modifier: {modifier}" raise ValueError(message) class DefaultContextFormatter(ContextFormatter): CONTEXT_NAME = "default" def __init__(self, root: str) -> None: self.__root = root def get_formatters(self) -> MutableMapping: return { "/": self.__format_directory_separator, ";": self.__format_path_separator, "env": self.__format_env, "home": self.__format_home, "root": self.__format_root, } def __format_directory_separator(self, value: str, data: str) -> str: # noqa: ARG002, PLR6301 return os.sep def __format_path_separator(self, value: str, data: str) -> str: # noqa: ARG002, PLR6301 return os.pathsep def __format_root(self, value: str, data: str) -> str: # noqa: ARG002 return self.format_path(self.__root, data) def __format_home(self, value: str, data: str) -> str: # noqa: ARG002 return self.format_path(os.path.expanduser("~"), data) def __format_env(self, value: str, data: str) -> str: # noqa: ARG002, PLR6301 if not data: message = "The `env` context formatting field requires a modifier" raise ValueError(message) env_var, separator, default = data.partition(":") if env_var in os.environ: return os.environ[env_var] if not separator: message = f"Nonexistent environment variable must set a default: {env_var}" raise ValueError(message) return default class Context: def __init__(self, root: str) -> None: self.__root = str(root) # Allow callers to define their own formatters with precedence self.__formatters: ChainMap = ChainMap() self.__configured_contexts: set[str] = set() self.__formatter = ContextStringFormatter(self.__formatters) self.add_context(DefaultContextFormatter(self.__root)) def format(self, *args: Any, **kwargs: Any) -> str: return self.__formatter.format(*args, **kwargs) def add_context(self, context: DefaultContextFormatter) -> None: if context.CONTEXT_NAME in self.__configured_contexts: return self.__add_formatters(context.get_formatters()) self.__configured_contexts.add(context.CONTEXT_NAME) @contextmanager def apply_context(self, context: DefaultContextFormatter) -> Iterator: self.__add_formatters(context.get_formatters()) try: yield finally: self.__remove_formatters() def __add_formatters(self, formatters: MutableMapping) -> None: return self.__formatters.maps.insert(0, formatters) def __remove_formatters(self) -> None: if len(self.__formatters.maps) > 1: self.__formatters.maps.pop(0) class ContextStringFormatter(string.Formatter): def __init__(self, formatters: ChainMap) -> None: super().__init__() self.__formatters = formatters def vformat(self, format_string: str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> str: # We override to increase the recursion limit from 2 to 10 # # TODO: remove type ignore after https://github.com/python/typeshed/pull/9228 used_args = set() # type: ignore[var-annotated] result, _ = self._vformat(format_string, args, kwargs, used_args, 10) self.check_unused_args(used_args, args, kwargs) return result def get_value(self, key: int | str, args: Sequence[Any], kwargs: Mapping[str, Any]) -> Any: if key in self.__formatters: # Avoid hard look-up and rely on `None` to indicate that the field is undefined return kwargs.get(str(key)) try: return super().get_value(key, args, kwargs) except KeyError: message = f"Unknown context field `{key}`" raise ValueError(message) from None def format_field(self, value: Any, format_spec: str) -> Any: formatter, _, data = format_spec.partition(":") if formatter in self.__formatters: return self.__formatters[formatter](value, data) return super().format_field(value, format_spec) def parse(self, format_string: str) -> Iterable: for literal_text, field_name, format_spec, conversion in super().parse(format_string): if field_name in self.__formatters: yield literal_text, field_name, f"{field_name}:{format_spec}", conversion else: yield literal_text, field_name, format_spec, conversion ================================================ FILE: backend/src/hatchling/utils/fs.py ================================================ from __future__ import annotations import os def locate_file(root: str, file_name: str, *, boundary: str | None = None) -> str | None: while True: file_path = os.path.join(root, file_name) if os.path.isfile(file_path): return file_path if boundary is not None and os.path.exists(os.path.join(root, boundary)): return None new_root = os.path.dirname(root) if new_root == root: return None root = new_root def path_to_uri(path: str) -> str: if os.sep == "/": return f"file://{os.path.abspath(path).replace(' ', '%20')}" return f"file:///{os.path.abspath(path).replace(' ', '%20').replace(os.sep, '/')}" ================================================ FILE: backend/src/hatchling/version/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/version/core.py ================================================ from __future__ import annotations import os import re DEFAULT_PATTERN = r'(?i)^(__version__|VERSION) *= *([\'"])v?(?P.+?)\2' DEFAULT_TEMPLATE = """\ # This file is auto-generated by Hatchling. As such, do not: # - modify # - track in version control e.g. be sure to add to .gitignore __version__ = VERSION = {version!r} """ class VersionFile: def __init__(self, root: str, relative_path: str) -> None: self.__relative_path = relative_path self.__path = os.path.normpath(os.path.join(root, relative_path)) self.__cached_read_data: tuple | None = None def read(self, *, pattern: str | bool) -> str: if not os.path.isfile(self.__path): message = f"file does not exist: {self.__relative_path}" raise OSError(message) with open(self.__path, encoding="utf-8") as f: contents = f.read() if not pattern or pattern is True: pattern = DEFAULT_PATTERN match = re.search(pattern, contents, flags=re.MULTILINE) if not match: message = f"unable to parse the version from the file: {self.__relative_path}" raise ValueError(message) groups = match.groupdict() if "version" not in groups: message = "no group named `version` was defined in the pattern" raise ValueError(message) self.__cached_read_data = groups["version"], contents, match.span("version") return self.__cached_read_data[0] def set_version(self, version: str) -> None: _old_version, file_contents, (start, end) = self.__cached_read_data # type: ignore[misc] with open(self.__path, "w", encoding="utf-8") as f: f.write(f"{file_contents[:start]}{version}{file_contents[end:]}") def write(self, version: str, template: str = DEFAULT_TEMPLATE) -> None: template = template or DEFAULT_TEMPLATE parent_dir = os.path.dirname(self.__path) if not os.path.isdir(parent_dir): os.makedirs(parent_dir) with open(self.__path, "w", encoding="utf-8") as f: f.write(template.format(version=version)) ================================================ FILE: backend/src/hatchling/version/scheme/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/version/scheme/plugin/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/version/scheme/plugin/hooks.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from hatchling.plugin import hookimpl from hatchling.version.scheme.standard import StandardScheme if TYPE_CHECKING: from hatchling.version.scheme.plugin.interface import VersionSchemeInterface @hookimpl def hatch_register_version_scheme() -> type[VersionSchemeInterface]: return StandardScheme ================================================ FILE: backend/src/hatchling/version/scheme/plugin/interface.py ================================================ from __future__ import annotations import os from abc import ABC, abstractmethod from functools import cached_property class VersionSchemeInterface(ABC): # no cov """ Example usage: ```python tab="plugin.py" from hatchling.version.scheme.plugin.interface import VersionSchemeInterface class SpecialVersionScheme(VersionSchemeInterface): PLUGIN_NAME = "special" ... ``` ```python tab="hooks.py" from hatchling.plugin import hookimpl from .plugin import SpecialVersionScheme @hookimpl def hatch_register_version_scheme(): return SpecialVersionScheme ``` """ PLUGIN_NAME = "" """The name used for selection.""" def __init__(self, root: str, config: dict) -> None: self.__root = root self.__config = config @property def root(self) -> str: """ The root of the project tree as a string. """ return self.__root @property def config(self) -> dict: """ ```toml config-example [tool.hatch.version] ``` """ return self.__config @cached_property def validate_bump(self) -> bool: """ This is the value of the `validate-bump` option, with the `HATCH_VERSION_VALIDATE_BUMP` environment variable taking precedence. Validation is enabled by default. ```toml config-example [tool.hatch.version] validate-bump = true ``` """ from hatchling.utils.constants import VersionEnvVars if VersionEnvVars.VALIDATE_BUMP in os.environ: return os.environ[VersionEnvVars.VALIDATE_BUMP] not in {"false", "0"} validate_bump = self.config.get("validate-bump", True) if not isinstance(validate_bump, bool): message = "option `validate-bump` must be a boolean" raise TypeError(message) return validate_bump @abstractmethod def update(self, desired_version: str, original_version: str, version_data: dict) -> str: """ This should return a normalized form of the desired version. If the [validate_bump](reference.md#hatchling.version.scheme.plugin.interface.VersionSchemeInterface.validate_bump) property is `True`, this method should also verify that the version is higher than the original version. """ ================================================ FILE: backend/src/hatchling/version/scheme/standard.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING, Any, Literal, cast from hatchling.version.scheme.plugin.interface import VersionSchemeInterface if TYPE_CHECKING: from packaging.version import Version class StandardScheme(VersionSchemeInterface): """ See https://peps.python.org/pep-0440/ """ PLUGIN_NAME = "standard" def update( self, desired_version: str, original_version: str, version_data: dict, # noqa: ARG002 ) -> str: from packaging.version import Version original = Version(original_version) versions = desired_version.split(",") for version in versions: if version == "release": original = reset_version_parts(original, release=original.release) elif version == "major": original = reset_version_parts(original, release=update_release(original, [original.major + 1])) elif version == "minor": original = reset_version_parts( original, release=update_release(original, [original.major, original.minor + 1]) ) elif version in {"micro", "patch", "fix"}: original = reset_version_parts( original, release=update_release(original, [original.major, original.minor, original.micro + 1]) ) elif version in {"a", "b", "c", "rc", "alpha", "beta", "pre", "preview"}: phase, number = parse_letter_version(version, 0) if original.pre: current_phase, current_number = parse_letter_version(*original.pre) if phase == current_phase: number = current_number + 1 original = reset_version_parts(original, pre=(phase, number)) elif version in {"post", "rev", "r"}: number = 0 if original.post is None else original.post + 1 original = reset_version_parts(original, post=number) elif version == "dev": number = 0 if original.dev is None else original.dev + 1 original = reset_version_parts(original, dev=number) else: if len(versions) > 1: message = "Cannot specify multiple update operations with an explicit version" raise ValueError(message) next_version = Version(version) if self.validate_bump and next_version <= original: message = f"Version `{version}` is not higher than the original version `{original_version}`" raise ValueError(message) return str(next_version) return str(original) def reset_version_parts(version: Version, **kwargs: Any) -> Version: """ Update version parts and clear all subsequent parts in the sequence. When __replace__ is available (packaging 26.0+), returns a new Version instance. Otherwise mutates version via private ._version and returns the same instance. """ parts: dict[str, Any] = {} ordered_part_names = ("epoch", "release", "pre", "post", "dev", "local") reset = False for part_name in ordered_part_names: if reset: parts[part_name] = kwargs.get(part_name) elif part_name in kwargs: parts[part_name] = kwargs[part_name] reset = True else: parts[part_name] = getattr(version, part_name) # Use __replace__ if available for efficiency if hasattr(version, "__replace__"): return version.__replace__(**parts) # Reference: https://github.com/pypa/packaging/blob/20.9/packaging/version.py#L301-L310 internal_version = version._version # noqa: SLF001 version._version = type(internal_version)(**parts) # noqa: SLF001 return version def update_release(original_version: Version, new_release_parts: list[int]) -> tuple[int, ...]: # Retain release length new_release_parts.extend(0 for _ in range(len(original_version.release) - len(new_release_parts))) return tuple(new_release_parts) def parse_letter_version(*args: Any, **kwargs: Any) -> tuple[Literal["a", "b", "rc"], int]: from packaging.version import _parse_letter_version # noqa: PLC2701 return cast(tuple[Literal["a", "b", "rc"], int], _parse_letter_version(*args, **kwargs)) ================================================ FILE: backend/src/hatchling/version/source/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/version/source/code.py ================================================ from __future__ import annotations import os from hatchling.version.source.plugin.interface import VersionSourceInterface class CodeSource(VersionSourceInterface): PLUGIN_NAME = "code" def get_version_data(self) -> dict: import sys from importlib.util import module_from_spec, spec_from_file_location relative_path = self.config.get("path") if not relative_path: message = "option `path` must be specified" raise ValueError(message) if not isinstance(relative_path, str): message = "option `path` must be a string" raise TypeError(message) path = os.path.normpath(os.path.join(self.root, relative_path)) if not os.path.isfile(path): message = f"file does not exist: {relative_path}" raise OSError(message) expression = self.config.get("expression") or "__version__" if not isinstance(expression, str): message = "option `expression` must be a string" raise TypeError(message) search_paths = self.config.get("search-paths", []) if not isinstance(search_paths, list): message = "option `search-paths` must be an array" raise TypeError(message) absolute_search_paths = [] for i, search_path in enumerate(search_paths, 1): if not isinstance(search_path, str): message = f"entry #{i} of option `search-paths` must be a string" raise TypeError(message) absolute_search_paths.append(os.path.normpath(os.path.join(self.root, search_path))) spec = spec_from_file_location(os.path.splitext(path)[0], path) module = module_from_spec(spec) # type: ignore[arg-type] old_search_paths = list(sys.path) try: sys.path[:] = [*absolute_search_paths, *old_search_paths] spec.loader.exec_module(module) # type: ignore[union-attr] finally: sys.path[:] = old_search_paths # Execute the expression to determine the version version = eval(expression, vars(module)) # noqa: S307 return {"version": version} def set_version(self, version: str, version_data: dict) -> None: message = "Cannot rewrite loaded code" raise NotImplementedError(message) ================================================ FILE: backend/src/hatchling/version/source/env.py ================================================ from __future__ import annotations import os from hatchling.version.source.plugin.interface import VersionSourceInterface class EnvSource(VersionSourceInterface): PLUGIN_NAME = "env" def get_version_data(self) -> dict: variable = self.config.get("variable", "") if not variable: message = "option `variable` must be specified" raise ValueError(message) if not isinstance(variable, str): message = "option `variable` must be a string" raise TypeError(message) if variable not in os.environ: message = f"environment variable `{variable}` is not set" raise RuntimeError(message) return {"version": os.environ[variable]} def set_version(self, version: str, version_data: dict) -> None: message = "Cannot set environment variables" raise NotImplementedError(message) ================================================ FILE: backend/src/hatchling/version/source/plugin/__init__.py ================================================ ================================================ FILE: backend/src/hatchling/version/source/plugin/hooks.py ================================================ from __future__ import annotations from typing import TYPE_CHECKING from hatchling.plugin import hookimpl from hatchling.version.source.code import CodeSource from hatchling.version.source.env import EnvSource from hatchling.version.source.regex import RegexSource if TYPE_CHECKING: from hatchling.version.source.plugin.interface import VersionSourceInterface @hookimpl def hatch_register_version_source() -> list[type[VersionSourceInterface]]: return [CodeSource, EnvSource, RegexSource] ================================================ FILE: backend/src/hatchling/version/source/plugin/interface.py ================================================ from __future__ import annotations from abc import ABC, abstractmethod class VersionSourceInterface(ABC): # no cov """ Example usage: ```python tab="plugin.py" from hatchling.version.source.plugin.interface import VersionSourceInterface class SpecialVersionSource(VersionSourceInterface): PLUGIN_NAME = "special" ... ``` ```python tab="hooks.py" from hatchling.plugin import hookimpl from .plugin import SpecialVersionSource @hookimpl def hatch_register_version_source(): return SpecialVersionSource ``` """ PLUGIN_NAME = "" """The name used for selection.""" def __init__(self, root: str, config: dict) -> None: self.__root = root self.__config = config @property def root(self) -> str: """ The root of the project tree as a string. """ return self.__root @property def config(self) -> dict: """ ```toml config-example [tool.hatch.version] ``` """ return self.__config @abstractmethod def get_version_data(self) -> dict: """ This should return a mapping with a `version` key representing the current version of the project and will be displayed when invoking the [`version`](../../cli/reference.md#hatch-version) command without any arguments. The mapping can contain anything else and will be passed to [set_version](reference.md#hatchling.version.source.plugin.interface.VersionSourceInterface.set_version) when updating the version. """ def set_version(self, version: str, version_data: dict) -> None: """ This should update the version to the first argument with the data provided during retrieval. """ raise NotImplementedError ================================================ FILE: backend/src/hatchling/version/source/regex.py ================================================ from hatchling.version.core import VersionFile from hatchling.version.source.plugin.interface import VersionSourceInterface class RegexSource(VersionSourceInterface): PLUGIN_NAME = "regex" def get_version_data(self) -> dict: relative_path = self.config.get("path", "") if not relative_path: message = "option `path` must be specified" raise ValueError(message) if not isinstance(relative_path, str): message = "option `path` must be a string" raise TypeError(message) pattern = self.config.get("pattern", "") if not isinstance(pattern, str): message = "option `pattern` must be a string" raise TypeError(message) version_file = VersionFile(self.root, relative_path) version = version_file.read(pattern=pattern) return {"version": version, "version_file": version_file} def set_version(self, version: str, version_data: dict) -> None: # noqa: PLR6301 version_data["version_file"].set_version(version) ================================================ FILE: backend/tests/__init__.py ================================================ ================================================ FILE: backend/tests/downstream/datadogpy/data.json ================================================ { "repo_url": "https://github.com/DataDog/datadogpy", "statements": [ "from datadog import initialize, api" ] } ================================================ FILE: backend/tests/downstream/datadogpy/pyproject.toml ================================================ [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "datadog" description = "The Datadog Python library" readme = "README.md" license = "BSD-3-Clause" keywords = [ "datadog", ] authors = [ { name = "Datadog, Inc.", email = "dev@datadoghq.com" }, ] classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", 'Programming Language :: Python :: Implementation :: CPython', "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ "requests>=2.6.0", "typing; python_version<'3.5'", "configparser<5; python_version<'3.0'", ] dynamic = ["version"] [project.urls] "Bug Tracker" = "https://github.com/DataDog/datadogpy/issues" Documentation = "https://datadogpy.readthedocs.io/en/latest/" "Source Code" = "https://github.com/DataDog/datadogpy" [project.scripts] dog = "datadog.dogshell:main" dogwrap = "datadog.dogshell.wrap:main" dogshell = "datadog.dogshell:main" dogshellwrap = "datadog.dogshell.wrap:main" [tool.hatch.version] path = "datadog/version.py" [tool.hatch.build] packages = ["datadog"] [tool.hatch.build.targets.sdist] include = [ "/LICENSE", "/tests", ] [tool.hatch.build.targets.wheel] ================================================ FILE: backend/tests/downstream/hatch-showcase/data.json ================================================ { "repo_url": "https://github.com/ofek/hatch-showcase", "statements": [ "from hatch_showcase.fib import fibonacci; assert fibonacci(32) == 2178309" ], "env_vars": { "HATCH_BUILD_HOOKS_ENABLE": "true" } } ================================================ FILE: backend/tests/downstream/integrate.py ================================================ import errno import json import os import platform import shutil import stat import subprocess import sys import tempfile from contextlib import contextmanager from zipfile import ZipFile import requests import tomli from packaging.requirements import Requirement from packaging.specifiers import SpecifierSet from virtualenv import cli_run HERE = os.path.dirname(os.path.abspath(__file__)) ON_WINDOWS = platform.system() == "Windows" def handle_remove_readonly(func, path, exc): # no cov # PermissionError: [WinError 5] Access is denied: '...\\.git\\...' if func in {os.rmdir, os.remove, os.unlink} and exc[1].errno == errno.EACCES: os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) func(path) else: raise exc class EnvVars(dict): def __init__(self, env_vars=None, ignore=None): super().__init__(os.environ) self.old_env = dict(self) if env_vars is not None: self.update(env_vars) if ignore is not None: for env_var in ignore: self.pop(env_var, None) def __enter__(self): os.environ.clear() os.environ.update(self) def __exit__(self, exc_type, exc_value, traceback): os.environ.clear() os.environ.update(self.old_env) def python_version_supported(project_config): requires_python = project_config["project"].get("requires-python", "") if requires_python: python_constraint = SpecifierSet(requires_python) if not python_constraint.contains(str(".".join(map(str, sys.version_info[:2])))): return False return True def download_file(url, file_name): response = requests.get(url, stream=True, timeout=20) with open(file_name, "wb") as f: for chunk in response.iter_content(16384): f.write(chunk) @contextmanager def temp_dir(): d = tempfile.mkdtemp() try: d = os.path.realpath(d) yield d finally: shutil.rmtree(d, ignore_errors=False, onerror=handle_remove_readonly) def get_venv_exe_dir(venv_dir): exe_dir = os.path.join(venv_dir, "Scripts" if ON_WINDOWS else "bin") if os.path.isdir(exe_dir): return exe_dir # PyPy if ON_WINDOWS: exe_dir = os.path.join(venv_dir, "bin") if os.path.isdir(exe_dir): return exe_dir message = f"Unable to locate executables directory within: {venv_dir}" raise OSError(message) # Debian if os.path.isdir(os.path.join(venv_dir, "local")): exe_dir = os.path.join(venv_dir, "local", "bin") if os.path.isdir(exe_dir): return exe_dir message = f"Unable to locate executables directory within: {venv_dir}" raise OSError(message) message = f"Unable to locate executables directory within: {venv_dir}" raise OSError(message) def main(): original_backend_path = os.path.dirname(os.path.dirname(HERE)) with temp_dir() as links_dir, temp_dir() as build_dir: print("<<<<< Copying backend >>>>>") backend_path = os.path.join(build_dir, "backend") shutil.copytree(original_backend_path, backend_path) # Increment the minor version version_file = os.path.join(backend_path, "src", "hatchling", "__about__.py") with open(version_file, encoding="utf-8") as f: lines = f.readlines() for i, line in enumerate(lines): if line.startswith("__version__"): version = line.strip().split(" = ")[1].strip("'\"") version_parts = version.split(".") version_parts[1] = str(int(version_parts[1]) + 1) lines[i] = line.replace(version, ".".join(version_parts)) break else: message = "No version found" raise ValueError(message) with open(version_file, "w", encoding="utf-8") as f: f.writelines(lines) print("<<<<< Building backend >>>>>") subprocess.check_call([sys.executable, "-m", "build", "--wheel", "-o", links_dir, backend_path]) subprocess.check_call([ sys.executable, "-m", "pip", "download", "-q", "--disable-pip-version-check", "-d", links_dir, os.path.join(links_dir, os.listdir(links_dir)[0]), ]) constraints = [] constraints_file = os.path.join(build_dir, "constraints.txt") with open(constraints_file, "w", encoding="utf-8") as f: f.write("\n".join(constraints)) for project in os.listdir(HERE): project_dir = os.path.join(HERE, project) if not os.path.isdir(project_dir): continue print(f"<<<<< Project: {project} >>>>>") project_config = {} potential_project_file = os.path.join(project_dir, "pyproject.toml") # Not yet ported if os.path.isfile(potential_project_file): with open(potential_project_file, encoding="utf-8") as f: project_config.update(tomli.loads(f.read())) if not python_version_supported(project_config): print("--> Unsupported version of Python, skipping") continue with open(os.path.join(project_dir, "data.json"), encoding="utf-8") as f: test_data = json.loads(f.read()) with temp_dir() as d: if "repo_url" in test_data: print("--> Cloning repository") repo_dir = os.path.join(d, "repo") subprocess.check_call(["git", "clone", "-q", "--depth", "1", test_data["repo_url"], repo_dir]) else: archive_name = f"{project}.zip" archive_path = os.path.join(d, archive_name) print("--> Downloading archive") download_file(test_data["archive_url"], archive_path) with ZipFile(archive_path) as zip_file: zip_file.extractall(d) entries = os.listdir(d) entries.remove(archive_name) repo_dir = os.path.join(d, entries[0]) project_file = os.path.join(repo_dir, "pyproject.toml") if project_config: shutil.copyfile(potential_project_file, project_file) else: if not os.path.isfile(project_file): sys.exit("--> Missing file: pyproject.toml") with open(project_file, encoding="utf-8") as f: project_config.update(tomli.loads(f.read())) for requirement in project_config.get("build-system", {}).get("requires", []): if Requirement(requirement).name == "hatchling": break else: sys.exit("--> Field `build-system.requires` must specify `hatchling` as a requirement") if not python_version_supported(project_config): print("--> Unsupported version of Python, skipping") continue for file_name in ("MANIFEST.in", "setup.cfg", "setup.py"): possible_path = os.path.join(repo_dir, file_name) if os.path.isfile(possible_path): os.remove(possible_path) venv_dir = os.path.join(d, ".venv") print("--> Creating virtual environment") cli_run([venv_dir, "--no-download", "--no-periodic-update"]) env_vars = dict(test_data.get("env_vars", {})) env_vars["VIRTUAL_ENV"] = venv_dir env_vars["PATH"] = f"{get_venv_exe_dir(venv_dir)}{os.pathsep}{os.environ['PATH']}" env_vars["PIP_CONSTRAINT"] = constraints_file with EnvVars(env_vars, ignore=("__PYVENV_LAUNCHER__", "PYTHONHOME")): print("--> Installing project") subprocess.check_call([ shutil.which("pip"), "install", "-q", "--disable-pip-version-check", "--find-links", links_dir, "--no-deps", repo_dir, ]) print("--> Installing dependencies") subprocess.check_call([ shutil.which("pip"), "install", "-q", "--disable-pip-version-check", repo_dir, ]) print("--> Testing package") for statement in test_data["statements"]: subprocess.check_call([shutil.which("python"), "-c", statement]) scripts = project_config["project"].get("scripts", {}) if scripts: print("--> Testing scripts") for script in scripts: if not shutil.which(script): sys.exit(f"--> Could not locate script: {script}") print("--> Success!") if __name__ == "__main__": main() ================================================ FILE: backend/tests/downstream/requirements.txt ================================================ build packaging requests tomli virtualenv>=21 ================================================ FILE: docs/.hooks/expand_blocks.py ================================================ import re import textwrap from markdown.preprocessors import Preprocessor _code_tab_regex = re.compile( r'^( *)((`{3,})[^ ].*) tab="(.+)"\n([\s\S]+?)\n\1\3$', re.MULTILINE, ) _config_example_regex = re.compile( r"^( *)((`{3,})toml\b.*) config-example\n([\s\S]+?)\n\1\3$", re.MULTILINE, ) def _code_tab_replace(m): indent, fence_start, fence_end, title, content = m.groups() return f"""\ {indent}=== ":octicons-file-code-16: {title}" {indent} {fence_start} {textwrap.indent(content, " ")} {indent} {fence_end} """ def _config_example_replace(m): indent, fence_start, fence_end, content = m.groups() content_without = re.sub(r" *\[tool.hatch\]\n", "", content.replace("[tool.hatch.", "[")) return f"""\ {indent}=== ":octicons-file-code-16: pyproject.toml" {indent} {fence_start} {textwrap.indent(content, " ")} {indent} {fence_end} {indent}=== ":octicons-file-code-16: hatch.toml" {indent} {fence_start} {textwrap.indent(content_without, " ")} {indent} {fence_end} """ class ExpandedBlocksPreprocessor(Preprocessor): def run(self, lines): # noqa: PLR6301 markdown = "\n".join(lines) markdown = _config_example_regex.sub(_config_example_replace, markdown) markdown = _code_tab_regex.sub(_code_tab_replace, markdown) return markdown.splitlines() ================================================ FILE: docs/.hooks/inject_version.py ================================================ import os import subprocess from functools import cache from markdown.preprocessors import Preprocessor MARKER = "" SEMVER_PARTS = 3 @cache def get_latest_version(): env = dict(os.environ) # Ignore the current documentation environment so that the version # command can execute as usual in the default build environment env.pop("HATCH_ENV_ACTIVE", None) output = subprocess.check_output(["hatch", "--no-color", "version"], env=env).decode("utf-8").strip() # noqa: S607 version = output.replace("dev", "") parts = list(map(int, version.split("."))) major, minor, patch = parts[:SEMVER_PARTS] if len(parts) > SEMVER_PARTS: patch -= 1 return f"{major}.{minor}.{patch}" class VersionInjectionPreprocessor(Preprocessor): def run(self, lines): # noqa: PLR6301 for i, line in enumerate(lines): lines[i] = line.replace(MARKER, get_latest_version()) return lines ================================================ FILE: docs/.hooks/plugin_register.py ================================================ import os import sys from markdown.extensions import Extension HERE = os.path.dirname(__file__) def on_config( config, **kwargs, # noqa: ARG001 ): config.markdown_extensions.append(GlobalExtension()) class GlobalExtension(Extension): def extendMarkdown(self, md): # noqa: N802, PLR6301 sys.path.insert(0, HERE) from expand_blocks import ExpandedBlocksPreprocessor from inject_version import VersionInjectionPreprocessor from render_default_test_env import TestEnvDefaultsPreprocessor from render_ruff_defaults import RuffDefaultsPreprocessor md.preprocessors.register(ExpandedBlocksPreprocessor(), ExpandedBlocksPreprocessor.__name__, 100) md.preprocessors.register(VersionInjectionPreprocessor(), VersionInjectionPreprocessor.__name__, 101) md.preprocessors.register(RuffDefaultsPreprocessor(), RuffDefaultsPreprocessor.__name__, 102) md.preprocessors.register(TestEnvDefaultsPreprocessor(), TestEnvDefaultsPreprocessor.__name__, 103) sys.path.pop(0) ================================================ FILE: docs/.hooks/render_default_test_env.py ================================================ from __future__ import annotations import os from ast import literal_eval from functools import cache import tomlkit from markdown.preprocessors import Preprocessor MARKER_DEPENDENCIES = "" MARKER_MATRIX = "" MARKER_SCRIPTS = "" @cache def test_env_config(): path = os.path.join(os.getcwd(), "src", "hatch", "env", "internal", "test.py") with open(path, encoding="utf-8") as f: contents = f.read() value = "".join(contents.split(" return ")[1].strip().splitlines()) return literal_eval(value) @cache def get_dependencies_toml(): env_config = {"dependencies": test_env_config()["dependencies"]} content = tomlkit.dumps({"tool": {"hatch": {"envs": {"hatch-test": env_config}}}}).strip() # Reload to fix the long array config = tomlkit.loads(content) config["tool"]["hatch"]["envs"]["hatch-test"]["dependencies"].multiline(True) # Reduce indentation content = tomlkit.dumps(config).strip() return content.replace(' "', ' "') @cache def get_matrix_toml(): env_config = {"matrix": test_env_config()["matrix"]} return tomlkit.dumps({"tool": {"hatch": {"envs": {"hatch-test": env_config}}}}).strip() @cache def get_scripts_toml(): env_config = {"scripts": test_env_config()["scripts"]} return tomlkit.dumps({"tool": {"hatch": {"envs": {"hatch-test": env_config}}}}).strip() class TestEnvDefaultsPreprocessor(Preprocessor): def run(self, lines): # noqa: PLR6301 return ( "\n".join(lines) .replace(MARKER_DEPENDENCIES, get_dependencies_toml()) .replace(MARKER_MATRIX, get_matrix_toml()) .replace(MARKER_SCRIPTS, get_scripts_toml()) .splitlines() ) ================================================ FILE: docs/.hooks/render_ruff_defaults.py ================================================ from __future__ import annotations import os import re from collections import defaultdict from functools import cache from typing import Any from markdown.preprocessors import Preprocessor MARKER_VERSION = "" MARKER_SELECTED_RULES = "" MARKER_UNSELECTED_RULES = "" MARKER_STABLE_RULES_COUNT = "" MARKER_PREVIEW_RULES_COUNT = "" MARKER_UNSELECTED_RULES_COUNT = "" MARKER_PER_FILE_IGNORED_RULES = "" RULE_URLS = {"S": "https://docs.astral.sh/ruff/rules/#flake8-bandit-s"} def read_constants(path: str, start: str) -> dict[str, Any]: with open(path, encoding="utf-8") as f: lines = f.read().splitlines() for i, line in enumerate(lines): if line.startswith(start): block_start = i break else: message = f"Could not find {start} in {path}" raise RuntimeError(message) data = {} exec("\n".join(lines[block_start:]), None, data) # noqa: S102 return data def parse_rules(rules: tuple[str, ...]) -> defaultdict[str, list[str]]: selected_rules: defaultdict[str, list[str]] = defaultdict(list) separator = re.compile(r"^(\D+)(\d+)$") for rule in rules: match = separator.search(rule) if match is None: message = f"Could not parse rule {rule}" raise RuntimeError(message) group, number = match.groups() selected_rules[group].append(number) return selected_rules def construct_collapsed_markdown_rule_list(text: str, rules: defaultdict[str, list[str]]) -> str: preview_rule_set = set(ruff_data()["PREVIEW_RULES"]) lines = [f'??? "{text}"'] for group, numbers in sorted(rules.items()): numbers.sort(key=lambda x: int(x[0])) parts = [] for number in numbers: rule = f"{group}{number}" part = f"[{rule}](https://docs.astral.sh/ruff/rules/{rule})" if f"{group}{number}" in preview_rule_set: part += "^P^" parts.append(part) lines.append(f" - {', '.join(parts)}") return "\n".join(lines) @cache def ruff_data(): root = os.getcwd() data = {} for path, start in ( (os.path.join(root, "src", "hatch", "cli", "fmt", "core.py"), "STABLE_RULES"), (os.path.join(root, "src", "hatch", "env", "internal", "static_analysis.py"), "RUFF_DEFAULT_VERSION"), ): data.update(read_constants(path, start)) return data @cache def get_ruff_version(): return ruff_data()["RUFF_DEFAULT_VERSION"] @cache def get_stable_rules_count(): return str(len(ruff_data()["STABLE_RULES"])) @cache def get_preview_rules_count(): return str(len(ruff_data()["PREVIEW_RULES"])) @cache def get_unselected_rules_count(): return str(len(UNSELECTED_RULES)) @cache def get_selected_rules(): data = ruff_data() rules = parse_rules(data["STABLE_RULES"]) for group, numbers in parse_rules(data["PREVIEW_RULES"]).items(): rules[group].extend(numbers) return construct_collapsed_markdown_rule_list("Selected rules", rules) @cache def get_unselected_rules(): return construct_collapsed_markdown_rule_list("Unselected rules", parse_rules(UNSELECTED_RULES)) @cache def get_per_file_ignored_rules(): lines = [] for glob, rules in sorted(ruff_data()["PER_FILE_IGNORED_RULES"].items()): parts = [] for rule in rules: url = RULE_URLS.get(rule) or f"https://docs.astral.sh/ruff/rules/{rule}" parts.append(f"[{rule}]({url})") lines.append(f"- `{glob}`: {', '.join(parts)}") return "\n".join(lines) class RuffDefaultsPreprocessor(Preprocessor): def run(self, lines): # noqa: PLR6301 return ( "\n".join(lines) .replace(MARKER_VERSION, get_ruff_version()) .replace(MARKER_STABLE_RULES_COUNT, get_stable_rules_count()) .replace(MARKER_PREVIEW_RULES_COUNT, get_preview_rules_count()) .replace(MARKER_UNSELECTED_RULES_COUNT, get_unselected_rules_count()) .replace(MARKER_SELECTED_RULES, get_selected_rules()) .replace(MARKER_UNSELECTED_RULES, get_unselected_rules()) .replace(MARKER_PER_FILE_IGNORED_RULES, get_per_file_ignored_rules()) .splitlines() ) UNSELECTED_RULES: tuple[str, ...] = ( "AIR001", "AIR002", "AIR301", "AIR302", "AIR311", "AIR312", "ANN001", "ANN002", "ANN003", "ANN101", "ANN102", "ANN201", "ANN202", "ANN204", "ANN205", "ANN206", "ANN401", "B027", "C901", "COM812", "COM819", "CPY001", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D200", "D201", "D202", "D203", "D204", "D205", "D206", "D207", "D208", "D209", "D210", "D211", "D212", "D213", "D214", "D215", "D300", "D301", "D400", "D401", "D402", "D403", "D404", "D405", "D406", "D407", "D408", "D409", "D410", "D411", "D412", "D413", "D414", "D415", "D416", "D417", "D418", "D419", "DJ001", "DJ003", "DJ006", "DJ007", "DJ008", "DJ012", "DJ013", "E111", "E114", "E117", "E301", "E302", "E303", "E304", "E305", "E306", "E501", "E999", "ERA001", "FBT003", "FIX001", "FIX002", "FIX003", "FIX004", "FURB101", "FURB103", "FURB140", "ISC001", "ISC002", "NPY001", "NPY002", "NPY003", "NPY201", "PD002", "PD003", "PD004", "PD007", "PD008", "PD009", "PD010", "PD011", "PD012", "PD013", "PD015", "PD101", "PD901", "PERF203", "PGH001", "PGH002", "PGH003", "PGH004", "PLR0904", "PLR0911", "PLR0912", "PLR0913", "PLR0914", "PLR0915", "PLR0916", "PLR0917", "PLR1701", "PLR1702", "PLR1706", "PT004", "PT005", "PTH100", "PTH101", "PTH102", "PTH103", "PTH104", "PTH105", "PTH106", "PTH107", "PTH108", "PTH109", "PTH110", "PTH111", "PTH112", "PTH113", "PTH114", "PTH115", "PTH116", "PTH117", "PTH118", "PTH119", "PTH120", "PTH121", "PTH122", "PTH123", "PTH124", "PTH201", "PTH202", "PTH203", "PTH204", "PTH205", "PTH206", "PTH207", "PTH208", "PTH210", "PTH211", "Q000", "Q001", "Q002", "Q003", "Q004", "RET501", "RET502", "RUF011", "RUF035", "RUF200", "S320", "S404", "S410", "S603", "SIM401", "TD001", "TD002", "TD003", "TRY200", "UP027", "UP038", "W191", ) ================================================ FILE: docs/.hooks/title_from_content.py ================================================ def on_page_markdown( markdown, page, **kwargs, # noqa: ARG001 ): if "title" in page.meta: return first_line = markdown.strip().splitlines()[0] if first_line.startswith("# "): title = first_line[2:].split(" # {:", maxsplit=1)[0].strip() page.meta["title"] = title page.meta["social"] = {"cards_layout_options": {"title": title}} ================================================ FILE: docs/.overrides/partials/copyright.html ================================================ ================================================ FILE: docs/.snippets/abbrs.txt ================================================ *[PyPI]: Python Package Index ================================================ FILE: docs/.snippets/links.txt ================================================ [PEP 440 version specifiers]: https://peps.python.org/pep-0440/#version-specifiers [PEP 508]: https://peps.python.org/pep-0508/ [PEP 517]: https://peps.python.org/pep-0517/ [PEP 639]: https://peps.python.org/pep-0639/ [PEP 660]: https://peps.python.org/pep-0660/ [PEP 665]: https://peps.python.org/pep-0665/ [project metadata standard]: https://packaging.python.org/en/latest/specifications/pyproject-toml/#declaring-project-metadata-the-project-table ================================================ FILE: docs/assets/badge/v0.json ================================================ {"schemaVersion":1,"label":"","message":"Hatch","labelColor":"grey","color":"#4051b5","logoSvg":""} ================================================ FILE: docs/assets/css/custom.css ================================================ :root > * { /* Use font but disable ligatures, see https://github.com/pypa/hatch/issues/104 */ font-variant-ligatures: none; } /* Brighter links for dark mode */ [data-md-color-scheme=slate] { /* https://github.com/squidfunk/mkdocs-material/blob/9.1.2/src/assets/stylesheets/main/_colors.scss#L91-L92 */ --md-typeset-a-color: var(--md-primary-fg-color--light); } /* FiraCode https://github.com/tonsky/FiraCode */ code { font-family: 'Fira Code', monospace; } @supports (font-variation-settings: normal) { code { font-family: 'Fira Code VF', monospace; } } /* https://github.com/squidfunk/mkdocs-material/issues/1522 */ .md-typeset h5 { color: var(--md-default-fg-color); text-transform: none; } ================================================ FILE: docs/blog/.authors.yml ================================================ authors: ofek: name: Ofek Lev description: Creator avatar: https://avatars.githubusercontent.com/u/9677399 flying-sheep: name: Phil A. description: Contributor avatar: https://avatars.githubusercontent.com/u/291575 cjames23: name: Cary Hawkins description: Co-maintainer avatar: https://avatars.githubusercontent.com/u/8369962?v=4 ================================================ FILE: docs/blog/index.md ================================================ # Blog ================================================ FILE: docs/blog/posts/release-hatch-1100.md ================================================ --- date: 2024-05-02 authors: [ofek,flying-sheep] description: >- Hatch v1.10.0 brings a test command, support for UV, and a Python script runner. categories: - Release --- # Hatch v1.10.0 Hatch [v1.10.0](https://github.com/pypa/hatch/releases/tag/hatch-v1.10.0) brings a test command, support for UV, and a Python script runner. ## Test command The new [`test`](../../cli/reference.md#hatch-test) command allows you to easily run tests for your project on multiple versions of Python. The default behavior follows best practices, using [pytest](https://github.com/pytest-dev/pytest) with select plugins for test execution and [coverage.py](https://github.com/nedbat/coveragepy) for code coverage measurement. The command is designed to be both simple to use while also satisfying the needs of most projects. For example, the following shows Hatch running tests for [Jinja](https://github.com/pallets/jinja) in all environments in the [default matrix](../../config/internal/testing.md#matrix):
![Testing Jinja example](release-hatch-1100/testing-jinja.gif){ loading=lazy role="img" }
Here is us testing [Rich](https://github.com/Textualize/rich), with a bit of configuration:
![Testing Rich example](release-hatch-1100/testing-rich.gif){ loading=lazy role="img" }
See the [tutorial](../../tutorials/testing/overview.md) for a detailed walk-through and the [config reference](../../config/internal/testing.md) for options. ## UV The package installer [UV](https://github.com/astral-sh/uv), brought to you by the same folks behind [Ruff](https://github.com/astral-sh/ruff), is now supported. In any environment, you can set the `installer` option to `uv` to use UV in place of [virtualenv](https://github.com/pypa/virtualenv) & [pip](https://github.com/pypa/pip) for virtual environment creation and dependency management, respectively. This often results in a significant performance benefit. For example, if you wanted to enable this functionality for the [default](../../config/environment/overview.md#inheritance) environment, you could set the following: ```toml config-example [tool.hatch.envs.default] installer = "uv" ``` Semi-internal environments like those used for [testing](../../config/internal/testing.md) and [static analysis](../../config/internal/static-analysis.md) have this enabled by default. See the [how-to guide](../../how-to/environment/select-installer.md) for more information about switching the installer. ## Python script runner The [`run`](../../cli/reference.md#hatch-run) command now supports executing Python scripts with [inline metadata](https://packaging.python.org/en/latest/specifications/inline-script-metadata/) as standardized by [PEP 723](https://peps.python.org/pep-0723/). As an example, consider the following script: ```python tab="script.py" # /// script # requires-python = ">=3.11" # dependencies = [ # "httpx", # "rich", # ] # /// import httpx from rich.pretty import pprint resp = httpx.get("https://peps.python.org/api/peps.json") data = resp.json() pprint([(k, v["title"]) for k, v in data.items()][:10]) ``` If you run the script for the first time as follows: ``` hatch run script.py ``` Hatch will create a dedicated environment for that script using a version of Python greater than or equal to 3.11 with dependencies `httpx` and `rich`.
![Script running example](release-hatch-1100/run-script.gif){ loading=lazy role="img" }
See the [how-to guide](../../how-to/run/python-scripts.md) for more information. ## Static analysis The environment used for static analysis is now [completely configurable](../../config/internal/static-analysis.md#customize-behavior) such that you can fully alter the underlying behavior of the [`fmt`](../../cli/reference.md#hatch-fmt) command (see the [how-to](../../how-to/static-analysis/behavior.md)). Additionally, Ruff has been updated to version 1.4.0 and the rules selected by default have been updated accordingly. Check out their [blog post](https://astral.sh/blog/ruff-v0.4.0) about how the new hand-written parser has made it twice as fast! ## Community highlights ### Visual Studio Code Visual Studio Code [announced support](https://code.visualstudio.com/updates/v1_88#_hatch-environment-discovery) for Hatch environments in their latest release. This means that you can now easily discover and select Hatch environments for your projects directly from the editor. See the [how-to guide](../../how-to/integrate/vscode.md) for detailed instructions. ### CMake build plugin A [new release](https://github.com/scikit-build/scikit-build-core/releases/tag/v0.9.0) of the extension module builder [scikit-build-core](https://github.com/scikit-build/scikit-build-core) has introduced a [build plugin](https://scikit-build-core.readthedocs.io/en/stable/plugins/hatchling.html) for Hatchling. This means that you can use Hatchling as your build backend while also shipping extension modules built with CMake. To get started, add the dependency to your [build requirements](../../config/build.md#build-system): ```toml tab="pyproject.toml" [build-system] requires = ["hatchling>=1.24.2", "scikit-build-core~=0.9.3"] build-backend = "hatchling.build" ``` Then explicitly enable the `experimental` option (acknowledging that the plugin will move to a dedicated package in the future): ```toml config-example [tool.hatch.build.targets.wheel.hooks.scikit-build] experimental = true ``` At this point, you can create your `CMakeLists.txt` file as usual and start building your extension modules with CMake! Check out the dedicated [example project](https://github.com/scikit-build/scikit-build-sample-projects/tree/main/projects/hatchling-pybind11-hello) for a complete demonstration. ## Meta ### Docs The efforts toward documentation improvements have increased substantially and the priorities have [shifted](https://github.com/pypa/hatch/issues/1245). From now on expect to see far more tutorials and how-to guides rather than just reference material. ### Future Upcoming features include: - workspaces functionality [similar to Cargo](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) - commands to manage dependencies - Windows release artifacts signed by the PSF (just like macOS) - performance improvements for both the CLI and the Hatchling build system ### Support If you or your organization finds value in what Hatch provides, consider a [sponsorship](https://github.com/sponsors/ofek) to assist with maintenance and more rapid development! ================================================ FILE: docs/blog/posts/release-hatch-1160.md ================================================ --- date: 2025-11-24 authors: [cjames23] description: >- Hatch v1.16.0 brings workspace support, dependency-groups, and sbom support. categories: - Release --- # Hatch v1.16.0 Hatch [v1.16.0](https://github.com/pypa/hatch/releases/tag/hatch-v1.16.0) brings support for workspaces, dependency groups, and SBOMs. ## Workspaces Workspaces allow repositories with several related packages (e.g. monorepos) to be installed and tested in lockstep. Our design is inspired by [Cargo workspaces](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html). Workspace members are defined by filters of relative or absolute paths, with support for glob patterns. Each member may also select which of their [features](../../config/environment/overview.md/#features) to enable. One design choice that users will find different from a tool like uv is that workspaces are configured per environment. Adhering to Hatch's environment-first [philosophy](../../why.md/#environment-management) allows full compatibility with the environment feature set, such as matrices of workspaces with different configuration. ### Example ```toml config-example [tool.hatch.envs.default] workspace.members = [ "packages/core", "packages/utils", "packages/cli" ] ``` For more information on usage, see the [workspace docs](../../how-to/environment/workspace.md). ## Dependency Groups Environments now support [dependency groups](../../config/environment/overview.md#dependency-groups) as defined by [PEP-735](https://peps.python.org/pep-0735/). You can think of them as [features](../../config/environment/overview.md#features), but for non-runtime dependencies, never being included in user-facing package metadata. ## Software Bill of Materials (SBOM) Support for [PEP-770](https://peps.python.org/pep-0770/) has been added. This enables adding sbom files to wheels with hatchling. This support does not add sbom generation, only the ability to have already created sbom files added to a wheel during wheel builds. For more information on usage, see [Wheel Options](../../plugins/builder/wheel.md#options). ## Meta ### Changes with maintainership Some may have noticed already during PR interactions, but I wanted to take some time to introduce myself as the new co-maintainer of hatch along with Ofek. I was browsing through the PyPA Discord, about to ask a question about workspace support for hatch, as I had created one version of it for the needs of my organization. That led to some discussions with Ofek and me taking on the contributions of finishing workspace support from where development had stopped. It made sense for me to join the efforts of maintainership with hatch and take some stress off of Ofek. I am excited to be here and to see what amazing things we can make hatch do in the future. A little about me as a developer and person. I have been writing Python code for 12 years now, and work at AWS as a Python Ecologist. My role is to provide tools to builders to be able to be more productive. In the past, I have made contributions to Airflow and a Poetry plugin for proxy setups. In my spare time, I enjoy hanging out with my dog Jacques, hiking, and rock climbing. And of course, I also enjoy giving back to the community in Python. ### Future Upcoming features: - Typing default command like `hatch fmt` for linting. - Documentation improvements including contributor guidelines. - Performance improvements for both the CLI and the Hatchling build system. ### Support If you or your organization finds value in what Hatch provides, consider a [sponsorship](https://github.com/sponsors/ofek) to assist with maintenance and more rapid development! ================================================ FILE: docs/blog/posts/release-hatch-160.md ================================================ --- date: 2022-10-08 authors: [ofek] description: >- Hatch v1.6.0 brings improvements to build environments, better handling of dynamic metadata, and support for tools like Visual Studio Code. categories: - Release --- # Hatch v1.6.0 Hatch [v1.6.0](https://github.com/pypa/hatch/releases/tag/hatch-v1.6.0) brings improvements to build environments, better handling of dynamic metadata, and support for tools like Visual Studio Code. ## Build environments Originally, the environment interface method for providing builder sub-environments was intended to be used in conjunction with some cleanup logic in order to provide a fresh setup every time. However, this is unnecessary in practice because build dependencies rarely change. Without caching, repeat build environment use is slow which affects the following scenarios: - the [`build`](../../cli/reference.md#hatch-build) command - commands that read project metadata, like [`dep hash`](../../cli/reference.md#hatch-dep-hash), if any fields are [set dynamically](../../config/metadata.md#dynamic) Now a new environment interface method `build_environment_exists` is used by Hatch to determine whether or not it has already been created, for implementations that have a caching mechanism. The [`virtual`](../../plugins/environment/virtual.md) environment type now uses this method to cache build environments. ## Project metadata Dynamically defined metadata is now supported everywhere, thanks to the new caching of `virtual` build environments. A [`project metadata`](../../cli/reference.md#hatch-project-metadata) command is introduced that displays the fully resolved [metadata](../../config/metadata.md). The output format is JSON unless a field is specified as an argument. For example, if you checkout a project that is built by Hatch, like [FastAPI](https://github.com/tiangolo/fastapi), and run: ``` hatch project metadata readme ``` only the `readme` text will be displayed. If the content is in Markdown, then [Rich](https://github.com/Textualize/rich) will render it directly in your terminal: ![FastAPI readme](release-hatch-160/rich-readme.png) ## Virtual environment location The [`virtual`](../../plugins/environment/virtual.md) environment type now uses a flat layout for storage in the configured `virtual` [environment directory](../../config/hatch.md#environments) if the directory resides somewhere within the project root or if it is set to a `.virtualenvs` directory within the user's home directory. For example, if you define the following Hatch configuration: ```toml tab="config.toml" [dirs.env] virtual = ".hatch" ``` and the following [matrix](../../config/environment/advanced.md#matrix): ```toml config-example [[tool.hatch.envs.test.matrix]] python = ["3.7", "3.8", "3.9", "3.10", "3.11"] ``` then [locating](../../cli/reference.md#hatch-env-find) environments with the following command: ``` hatch env find test ``` will show that the general directory structure is: ``` .hatch ├── test.py3.7 ├── test.py3.8 ├── test.py3.9 ├── test.py3.10 └── test.py3.11 ``` This flat structure is required for detection of virtual environments by tools like Visual Studio Code and PyCharm. Additionally, the `virtual` environment type now supports a `path` option to specify an explicit path that all [inherited](../../config/environment/overview.md#inheritance) environments will share, such as the common `.venv`. ## Migration script improvements The [script](https://github.com/pypa/hatch/blob/hatch-v1.6.0/src/hatch/cli/new/migrate.py) used to migrate [existing projects](../../intro.md#existing-project) from `setuptools` has been improved to handle more edge cases that were encountered in the wild and now no longer modifies the formatting of existing `pyproject.toml` configuration. ## Hatchling Hatch now depends on Hatchling [v1.11.0](https://github.com/pypa/hatch/releases/tag/hatchling-v1.11.0), which was also just released. ### Environment version source A new [`env` version source](../../plugins/version-source/env.md) is available that allows for the project version to be defined by an environment variable. ### Relaxed version bumping The [`standard` version scheme](../../plugins/version-scheme/standard.md) now supports a `validate-bump` option that when set to `false` will forego the check when [updating the version](../../version.md#updating) that the desired version is higher than the current version. This use case comes from [Project Jupyter](https://jupyter.org): > A common pattern we use in Jupyter is to bump to a `.dev0` minor version bump after making a release. If we have a bug fix that needs to go out in the interim, we'd rather not be forced to create a branch every time. ================================================ FILE: docs/blog/posts/release-hatch-180.md ================================================ --- date: 2023-12-11 authors: [ofek] description: >- Hatch v1.8.0 brings Python distribution management, static analysis and formatting backed by Ruff, and binaries for every platform. categories: - Release --- # Hatch v1.8.0 Hatch [v1.8.0](https://github.com/pypa/hatch/releases/tag/hatch-v1.8.0) brings Python distribution management, static analysis and formatting backed by Ruff, and binaries for every platform. ## Installation made easy One thing that has been a perpetual problem for Hatch and other Python applications is that Python itself is a dependency. You, and more importantly your users, need to in some way get Python before your software can even be used. The recommended way to go about that is platform-dependent and even differs based on your target audience. I viewed this as a central UX problem for Hatch and so severe that I took a bit of a hiatus to solve it. Luckily, I have to my satisfaction solved this problem in the form of [PyApp](https://github.com/ofek/pyapp). It is a runtime installer for Python projects written in Rust. Apps are distributed as standalone executables as users have come to expect and bootstrapping occurs upon the first invocation. Here is an example of what you would see the first time you run a binary from this release:
![Installation example](release-hatch-180/install-demo.gif){ loading=lazy role="img" }
Now that we have binaries, creating installers for different platforms becomes trivial. Starting with this release not only are binaries available for every platform but also we have installers for Windows and macOS. The installer for macOS is signed using a certificate from the same account used to sign the official distributions from https://www.python.org, so users will not get any security pop-ups. Shout out to @ewdurbin for their extreme generosity in setting up multiple certificates in their free time! These installers and binaries are now the [recommended way](../../install.md) to install and update Hatch. These binaries have built-in management so you can update to the latest version by running `hatch self update`. !!! note "Windows signing" In future we will sign the installers for Windows but I did not have time to look into how that works. macOS signing took way longer than I anticipated :sweat_smile: ## Python management For a long time I and other users have desired that Hatch gain the ability to manage Python distributions. In my mind this was always blocked on a better installation experience because there was sort of a chicken-or-egg problem where you want a Python manager but you first need Python. No longer is that the case! The new [`python`](../../cli/reference.md#hatch-python) command group allows for easy installation of various distributions to arbitrary locations which are then added to your PATH by default. Hatch supports CPython and PyPy distributions:
![Available Python distributions](release-hatch-180/available-pythons.png){ loading=lazy width="200" role="img" }
## Virtual environment Python resolution The `virtual` environment type is now far more intelligent when resolving the parent distribution to use and guarantees that, when no specific version is requested, the resolved distribution will always be [compatible](../../config/metadata.md#python-support) with the project. Additionally, when a requested version cannot be found on PATH it will [automatically](../../plugins/environment/virtual.md#python-resolution) be downloaded and managed internally. ## Static analysis There is a new [`fmt`](../../cli/reference.md#hatch-fmt) command, backed entirely by [Ruff](https://github.com/astral-sh/ruff), that checks and fixes your code for formatting and linting issues. Starting with this release, Hatch maintains [default settings](../../config/internal/static-analysis.md#default-settings) that are guaranteed to be up-to-date and represent best practices for programming in modern Python. The idea is to provide defaults that are so broadly applicable that the majority of users will maintain little if any of their own [overrides](../../config/internal/static-analysis.md#extending-config). The default behavior is internal management of settings to provide an OOTB experience that works. It is recommended however that you [persist](../../config/internal/static-analysis.md#persistent-config) the default config file in version control so that other tools like IDEs can utilize your full configuration. Since Ruff is now provided as a built-in feature, new project templates no longer have such configuration and are much less verbose. ## Build improvements [Building](../../cli/reference.md#hatch-build) projects that do not use Hatchling as a backend is now supported and such builds are managed with the standard [build](https://github.com/pypa/build) tool. The bridge between Hatch and the Hatchling CLI has been removed. Previously, the builder would send serialized messages to Hatch that would contain the desired content and style for each line of output. This was done in an effort to allow builder and build hook plugins to output pretty messages without actually requiring a dependency like [Rich](https://github.com/Textualize/rich). A problem that arises with this is that builders that invoke subprocesses will not display ANSI codes as one might expect and will lose out on the interactive experience of such invocations, like the built-in [binary builder plugin](../../plugins/builder/binary.md) calling `cargo build`. So now everything is simpler at the expense of no colored output without manual logic, or adding a dependency if you're a third-party plugin. ## Faster environment usage [Spawning a shell](../../environment.md#entering-environments) or [running commands](../../environment.md#command-execution) within environments always first checks that your project's dependencies are satisfied and if not synchronizes the environment with what is defined. Previously, this had the potential to be quite slow for projects that have many dependencies. Now the set of dependency definitions is [hashed](../../plugins/environment/reference.md#hatch.env.plugin.interface.EnvironmentInterface.dependency_hash) and no check is performed if the hash is the same as before, significantly speeding up environment usage in most cases. ## Hatchling Hatch now depends on Hatchling [v1.19.0](https://github.com/pypa/hatch/releases/tag/hatchling-v1.19.0), which was also just released. ### Better defaults Hatchling is all about providing the best possible defaults, even at the expense of backward compatibility. In this release, there are two breaking changes that provide a much better user experience and were in fact requested by users. - Both the [`force-include`](../../config/build.md#forced-inclusion) option and the [`force_include_editable`](../../plugins/builder/wheel.md#build-data) wheel build data setting now raise errors if source paths do not exist. - The `wheel` build target now raises an error when no file inclusion options have been defined and none of its [heuristics](../../plugins/builder/wheel.md#default-file-selection) to determine what to ship are satisfied. ### Binary build target A new [`binary`](../../plugins/builder/binary.md) build target is now stable that allows for the building of standalone binaries for projects. This is what Hatch itself uses for its binaries. ## Meta ### Why Hatch? A [new page](../../why.md) has been introduced that discusses the value proposition of Hatch and Hatchling in comparison to alternatives. Currently, it only addresses a few features but in future this page will become more comprehensive. ### Future Upcoming features include a `test` command, commands to manage dependencies, and workspaces functionality [similar to Cargo](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) that will make managing monorepos far easier. Next year there will be two large efforts that you should expect to see: 1. A significant amount of my free time (and some at work) will be devoted to introducing lock file functionality in Hatch and trying to get whatever that happens to be standardized. I met with @brettcannon about his thoughts post-[PEP 665](https://peps.python.org/pep-0665/) and about [mousebender](https://github.com/brettcannon/mousebender). I also met with the [prefix.dev](https://github.com/prefix-dev) team about [rip](https://github.com/prefix-dev/rip) and was fortunate enough to be shown a demo before its official announcement. At the moment, the two options I see are to either go all in and contribute to mousebender or rely on the Prefix folks and use rip. The latter has the benefit of _potentially_ supporting Conda as a side effect with the downside of being quite new with the spec firmly out of our control. The former has the benefit of being able to easily gain institutional support from the Python packaging team and each of our employers with the downside being a significant amount of work needing to be done. 1. When @henryiii is able to get some free time away from teaching I plan to work with him once again and push very hard for the Python build ecosystem to adopt the [extensionlib](https://github.com/ofek/extensionlib) approach. I am of the opinion that the Python community has not fully completed the expressed outcome of [PEP 517][] in that build backends are still (for the most part) reliant on setuptools for building non-Python code bases. Basically, there are components that interact with compilers to produce extension modules and components that pack files into an archive which we call a build backend. These are two distinct pieces of functionality and my view is that there should be an API that allows backends to consume extension module builders to find out where things got created and where they should be shipped inside archives. In this hypothetical future any build backend would be able to trigger the building of extension modules based on user configuration. ### Support If you or your organization finds value in what Hatch provides, consider a [sponsorship](https://github.com/sponsors/ofek) to assist with maintenance and more rapid development! ================================================ FILE: docs/blog/posts/release-hatch-190.md ================================================ --- date: 2023-12-18 authors: [ofek] description: >- Hatch v1.9.0 brings improvements to static analysis and important bug fixes. categories: - Release --- # Hatch v1.9.0 Hatch [v1.9.0](https://github.com/pypa/hatch/releases/tag/hatch-v1.9.0) brings improvements to static analysis and important bug fixes. ## Static analysis The default version of Ruff has been increased to [v0.1.8](https://astral.sh/blog/ruff-v0.1.8). This release brings formatting capabilities to docstrings and Hatch enables this by default with line length set to 80. This length was chosen as the default because it plays nicely with the rendering of the most popular themes for Python documentation, such as [Material for MkDocs](https://github.com/squidfunk/mkdocs-material) and [Furo](https://github.com/pradyunsg/furo). Additionally, it is now possible for projects to [pin](../../config/internal/static-analysis.md#dependencies) to specific versions of Ruff for upgrading at a later time: ```toml config-example [tool.hatch.envs.hatch-static-analysis] dependencies = ["ruff==X.Y.Z"] ``` ## Notable fixes - Python resolution for environments that do not install the project is no longer bound by the project's [Python requirement](../../config/metadata.md#python-support). - Fixed an edge case for out-of-the-box static analysis when there was existing configuration. - Compatibility checks for environments no longer occur if the environment is already created. This significantly increases the responsiveness of environment usage. ================================================ FILE: docs/build.md ================================================ # Builds ----- ## Configuration Builds are [configured](config/build.md) using the `tool.hatch.build` table. Every [target](config/build.md#build-targets) is defined by a section within `tool.hatch.build.targets`, for example: ```toml config-example [tool.hatch.build.targets.sdist] exclude = [ "/.github", "/docs", ] [tool.hatch.build.targets.wheel] packages = ["src/foo"] ``` ## Building Invoking the [`build`](cli/reference.md#hatch-build) command without any arguments will build the [sdist](plugins/builder/sdist.md) and [wheel](plugins/builder/wheel.md) targets: ```console $ hatch build [sdist] dist/hatch_demo-1rc0.tar.gz [wheel] dist/hatch_demo-1rc0-py3-none-any.whl ``` To only build specific targets, use the `-t`/`--target` option: ```console $ hatch build -t wheel [wheel] dist/hatch_demo-1rc0-py3-none-any.whl ``` If the target supports multiple [versions](config/build.md#versions), you can specify the exact versions to build by appending a colon followed by the desired versions separated by commas: ```console $ hatch -v build -t wheel:standard [wheel] Building `wheel` version `standard` dist/hatch_demo-1rc0-py3-none-any.whl ``` ## Packaging ecosystem Hatch [complies](config/build.md#build-system) with modern Python packaging specs and therefore your projects can be used by other tools with Hatch serving as just the build backend. So you could use [tox](https://github.com/tox-dev/tox) as an alternative to Hatch's [environment management](environment.md), or [cibuildwheel](https://github.com/pypa/cibuildwheel) to distribute packages for every platform, and they both will transparently use Hatch without any extra modification. ================================================ FILE: docs/cli/about.md ================================================ # CLI usage ----- ## Verbosity The amount of displayed output is controlled solely by the `-v`/`--verbose` (environment variable `HATCH_VERBOSE`) and `-q`/`--quiet` (environment variable `HATCH_QUIET`) [root options](reference.md#hatch). The levels are documented [here](../config/hatch.md#terminal). ## Project awareness No matter the [mode](../config/hatch.md#mode), Hatch will always change to the project's root directory for [entering](../environment.md#entering-environments) or [running commands](../environment.md#command-execution) in environments. ## Tab completion Completion is achieved by saving a script and then executing it as a part of your shell's startup sequence. Afterward, you'll need to start a new shell in order for the changes to take effect. === "Bash" Save the script somewhere: ```console _HATCH_COMPLETE=bash_source hatch > ~/.hatch-complete.bash ``` Source the file in `~/.bashrc` (or `~/.bash_profile` if on macOS): ```bash . ~/.hatch-complete.bash ``` === "Z shell" Save the script somewhere: ```console _HATCH_COMPLETE=zsh_source hatch > ~/.hatch-complete.zsh ``` Source the file in `~/.zshrc`: ```zsh . ~/.hatch-complete.zsh ``` === "fish" Save the script in `~/.config/fish/completions`: ```console _HATCH_COMPLETE=fish_source hatch > ~/.config/fish/completions/hatch.fish ``` ================================================ FILE: docs/cli/reference.md ================================================ ::: mkdocs-click :module: hatch.cli :command: hatch :depth: 0 :style: table :remove_ascii_art: true ================================================ FILE: docs/community/contributing.md ================================================ # Contributing The usual process to make a contribution is to: 1. Check for existing related issues 2. Fork the repository and create a new branch 3. Make your changes 4. Make sure formatting, linting and tests passes. 5. Add tests if possible to cover the lines you added. 6. Commit, and send a Pull Request. ## Clone the repository Clone the `hatch` repository, `cd` into it, and create a new branch for your contribution: ```bash cd hatch git switch -c add-my-contribution ``` ## Run the tests Run the test suite while developing: ```bash hatch test ``` Run the test suite with coverage report: ```bash hatch test --cover ``` Run the extended test suite with coverage: ```bash hatch test --cover --all ``` ## Lint Run automated formatting: ```bash hatch fmt ``` Run full linting and type checking: ```bash hatch fmt --check hatch run types:check ``` ## Docs Start the documentation in development: ```bash hatch run docs:serve ``` Build and validate the documentation website: ```bash hatch run docs:build-check ``` ================================================ FILE: docs/community/highlights.md ================================================ # Community highlights ----- ## Integration - Project Jupyter - https://blog.jupyter.org/packaging-for-jupyter-in-2022-c7be64c38926 - Visual Studio Code - https://code.visualstudio.com/updates/v1_88#_hatch-environment-discovery ## Adoption - Black - https://ichard26.github.io/blog/2022/10/black-22.10.0/#goodbye-python-36-and-hello-hatchling - "Switching to Hatch" - https://andrich.me/2023/08/switching-to-hatch/ ================================================ FILE: docs/community/users.md ================================================ # Users ----- The following is not intended to be a complete enumeration. Be sure to view the [development version](/dev/community/users/) of this page for an up-to-date listing. ## Projects [aiogram](https://github.com/aiogram/aiogram/blob/a2e5f9a8b8c994ad65bce05cde9c744760f47c4c/pyproject.toml#L1-L3) | [Apache Airflow](https://github.com/apache/airflow/blob/ba2ba7f49395b528ea67611c423ddd71b64b8ede/pyproject.toml#L18-L39) | [argon2-cffi](https://github.com/hynek/argon2-cffi/blob/59c7470af1a65b3b71e18fbf9abeca2cca3d707a/pyproject.toml#L3-L5) | [attrs](https://github.com/python-attrs/attrs/blob/01413df3db8e64437547f7fa6439a646fa116a98/pyproject.toml#L3-L5) | [Black](https://github.com/psf/black/blob/f22273a72b3f1c15085f2d4a43e8d785bf48c822/pyproject.toml#L28-L30) | [coffea](https://github.com/CoffeaTeam/coffea/blob/bab41f66869293f8ba630556f21ac093828788b7/pyproject.toml#L1-L3) | [Colorama](https://github.com/tartley/colorama/blob/cd653d75be52f4d8c3953eb6942fe597375f8b97/pyproject.toml#L1-L5) | [Django Anymail](https://github.com/anymail/django-anymail/blob/63e355084c057d60bcce41afa1de315b163b6235/pyproject.toml#L1-L3) | [Django Debug Toolbar](https://github.com/jazzband/django-debug-toolbar/blob/d04b9d1a666fd6427604c92f86f91380597eae14/pyproject.toml#L1-L5) | [Django NYT](https://github.com/django-wiki/django-nyt/blob/b87107f5fadc2a77941bb15e7dfb95dba3d7f40d/pyproject.toml#L1-L3) | [Django OTP](https://github.com/django-otp/django-otp/blob/1cb288fceaab66e7921f80c27f40df475c056811/pyproject.toml#L135-L137) | [Django OTP Agents](https://github.com/django-otp/django-otp-agents/blob/b9cd473bef9153c05c8768f72208229f2a25951d/pyproject.toml#L118-L120) | [Django OTP Twilio](https://github.com/django-otp/django-otp-twilio/blob/a0c68a829cbffe373605df03f62e093b3f9d4170/pyproject.toml#L118-L120) | [Django OTP YubiKey](https://github.com/django-otp/django-otp-yubikey/blob/fbd121dfb0f4890745df10ce2fb129e2b588da24/pyproject.toml#L118-L120) | [Django Places](https://github.com/oscarmcm/django-places/blob/76630ccc1a45380d40cca1262fa4f9a269cf5112/pyproject.toml#L1-L3) | [Django Wiki](https://github.com/django-wiki/django-wiki/blob/1b03661c3fe7260b0eb82565cc3812b96de6b674/pyproject.toml#L1-L3) | [FastAPI](https://github.com/tiangolo/fastapi/blob/1073062c7f2c48bcc28bcedbdc009c18c171f6fb/pyproject.toml#L1-L3) | [filelock](https://github.com/tox-dev/filelock/blob/c06aa983616804c349007c7a536c361d0e1a8cff/pyproject.toml#L1-L6) | [Fluentd](https://github.com/fluent/fluent-logger-python/blob/1e58a7e8b62b435d42f80f7b8ca264012925edce/pyproject.toml#L1-L3) | [github3.py](https://github.com/sigmavirus24/github3.py/blob/94541f8adee67e39f3061c6b29db3e39cef5ce05/pyproject.toml#L1-L3) | [Gradio](https://github.com/gradio-app/gradio/blob/f43481c18ac6468fbf30bf9a80981b7eab453961/pyproject.toml#L1-L3) | [HTTPX](https://github.com/encode/httpx/blob/45b7cfaad3a8987ea35fa5bf092bbdda485444fd/pyproject.toml#L1-L3) | [iCalendar for Humans](https://github.com/ics-py/ics-py/blob/133a0955f6efbb83ff0eae45ad0bbe6902a8f2f1/pyproject.toml#L61-L63) | [LinkChecker](https://github.com/linkchecker/linkchecker/blob/de40321b57a2271e90e696b5320c0409faaa895d/pyproject.toml#L29-L34) | [Litestar](https://github.com/litestar-org/litestar/blob/f9e3f727e8ae71e4b58a518240fb6c66e83c10de/pyproject.toml#L181-L183) | [Material for MkDocs](https://github.com/squidfunk/mkdocs-material/blob/7ca1c1d623b4750d4aaa0cfd673b0ed2c6050c2b/pyproject.toml#L21-L23) | [MicroPython](https://github.com/micropython/micropython/blob/30a9ccf4caa72c62cb8656a1572518fef34b08a4/tools/mpremote/pyproject.toml#L1-L7) | [MkDocs](https://github.com/mkdocs/mkdocs/blob/65c24c21f0057ec4717d20d14d5fb7af22fe8caf/pyproject.toml#L1-L3) | [openSUSE](https://github.com/openSUSE/py2pack/blob/25be8cdb53ee6966213474e3399fe451f33993f6/pyproject.toml#L1-L3) | [Nox](https://github.com/wntrblm/nox/blob/cc710bde9d6a8781833144bac02a5f4581d9eca7/pyproject.toml#L1-L5) | [Packit](https://github.com/packit/packit/blob/6e286a7b4d0f79cd2a8213a8ae978788be5219c5/pyproject.toml#L1-L3) | [pipx](https://github.com/pypa/pipx/blob/bc7dd03c4d872c443257685109a650ec3d524814/pyproject.toml#L1-L3) | [platformdirs](https://github.com/platformdirs/platformdirs/blob/382e961c436f9974e56dc69ce105b6fd8945c343/pyproject.toml#L1-L3) | [Pydantic](https://github.com/pydantic/pydantic/blob/f341049b9e5538a125751d75b4e44c1609b53df6/pyproject.toml#L1-L3) | [Pygments](https://github.com/pygments/pygments/blob/0f3ddb3a6e3ed99957fe20aab695446f85835387/pyproject.toml#L1-L3) | [PyHamcrest](https://github.com/hamcrest/PyHamcrest/blob/07a787207619a7f7d51088d36051a632432a0144/pyproject.toml#L1-L3) | [PyMdown Extensions](https://github.com/facelessuser/pymdown-extensions/blob/72390ce2d0b40df638e31b75f1f02f45659724de/pyproject.toml#L1-L5) | [Python JSON Schema](https://github.com/python-jsonschema/jsonschema/blob/afc22f09e74d696ab00be8a711bbc5c2a15327b7/pyproject.toml#L1-L3) | [Rye](https://github.com/mitsuhiko/rye/blob/92b571bfd42e5748d2e535174d78fc7311a889a3/pyproject.toml#L20-L22) | [SALib](https://github.com/SALib/SALib/blob/7490a686e959b436f7db9bc9cf6fa4b2e7bfa3fc/pyproject.toml#L1-L3) | [Spack](https://github.com/spack/spack/blob/7a5e527cab5980cb4732bb3504fab77d75286a19/pyproject.toml#L36-L38) | [Starlette](https://github.com/encode/starlette/blob/31164e346b9bd1ce17d968e1301c3bb2c23bb418/pyproject.toml#L1-L3) | [structlog](https://github.com/hynek/structlog/blob/6e2e8c6025fb90484c5e6c5ff2fd3e96a61854cf/pyproject.toml#L3-L5) | [tox](https://github.com/tox-dev/tox/blob/f2b4a4a6f5e8bbc8f9f0cff3dd5d17c50e874172/pyproject.toml#L1-L3) | [Twisted](https://github.com/twisted/twisted/blob/960e26bb1f4c67b3f7819553d0c45b25e6db4aae/pyproject.toml#L1-L7) | [urllib3](https://github.com/urllib3/urllib3/blob/8dda1974ae51839304f8517ab7993006c0d9db2e/pyproject.toml#L3-L5) | [Uvicorn](https://github.com/encode/uvicorn/blob/ccd1aae48e49dd8c9365600fd79e886efe88be1d/pyproject.toml#L1-L3) | [virtualenv](https://github.com/pypa/virtualenv/blob/69664d522d98899c21dcf0e88a0af3efcb0c71e7/pyproject.toml#L1-L6) | [Voilà](https://github.com/voila-dashboards/voila/blob/71292e4124b1f4a6f91c8b4e16ea9ad6b5ef500b/pyproject.toml#L1-L7) | [XGBoost](https://github.com/dmlc/xgboost/blob/62571b79eb08398a031873c3704da4e9cfd2c301/python-package/pyproject.toml#L1-L6) | [Ypy](https://github.com/y-crdt/ypy/tree/b9241a9e7ca248b6c44b62707d719b1ef20eef74#using-hatch) | [yt-dlp](https://github.com/yt-dlp/yt-dlp/blob/111b61ddef305584d45a48e7b7c73ffcedf062a2/pyproject.toml#L1-L3) ## Industry - [Anaconda](https://www.anaconda.com) \[[1](https://github.com/ContinuumIO/dask-awkward/blob/105275b1937cce9a80a352af0b200d4e264f27f7/pyproject.toml#L1-L3)|[2](https://github.com/conda-incubator/ensureconda/blob/b20dbcf7166009ff4e9270f35ed75da7afc3db60/pyproject.toml#L1-L3)|[3](https://github.com/conda-incubator/conda-lock/blob/9187487698f9afbb08e131cd585a17bba82ce9f2/pyproject.toml#L1-L3)|[4](https://github.com/conda-incubator/conda-auth/blob/437ca609ea8bf4b8bd91d32dd427abe8294f6a3b/pyproject.toml#L1-L3)|[5](https://github.com/conda/conda-content-trust/blob/f72a50b04126177f37b965c25d02564223b7acf8/pyproject.toml#L1-L6)|[6](https://github.com/conda/conda-build/blob/37ab8d3de084d32b907b726ba2ad4570e91d326b/pyproject.toml#L1-L6)|[7](https://github.com/conda/conda/blob/0c38f5660f7eca66434827af910beddf9f7e462d/pyproject.toml#L1-L6)\] - [Airbnb](https://www.airbnb.com) \[[1](https://github.com/airbnb/omniduct/blob/98c66e10b493c83d42f69bc6b97fab7a8c91eab1/pyproject.toml#L1-L3)\] - [Astronomer](https://www.astronomer.io) \[[1](https://github.com/astronomer/astronomer-cosmos/blob/29886492a46cf1dccd4c17a1643010975cb8094a/pyproject.toml#L1-L3)|[2](https://github.com/astronomer/astro-provider-databricks/blob/3e1ca039a024a98f9079d178478aa24702e15453/pyproject.toml#L1-L3)|[3](https://github.com/astronomer/astro-providers-template/blob/5be542eb5763f3d9accc7d6d7bc35c9214d15904/pyproject.toml#L1-L3)\] - [Bitwarden](https://bitwarden.com) \[[1](https://github.com/bitwarden/gh-actions/blob/c3bc6a192283618c6ae92f33bde7c2f28e198539/lint-workflow-v2/pyproject.toml#L1-L3)\] - [Bloomberg](https://www.bloomberg.com) \[[1](https://github.com/bloomberg/ipydatagrid/blob/04b73fe67bf33d054e69036fe2794ac72057b105/pyproject.toml#L1-L6)|[2](https://github.com/bloomberg/pytest-memray/blob/4ea6a7608adb0de4572d35768fbd370aee016627/pyproject.toml#L1-L3)\] - [Blue Robotics](https://bluerobotics.com) \[[1](https://github.com/bluerobotics/navigator-lib/blob/1d8afadb0804ffbbf32147232b1c627e92786c07/pyproject.toml#L26-L38)\] - [Cars.com](https://www.cars.com) \[[1](https://github.com/carsdotcom/cars-forge/blob/ba14db991a5c7cb3c5adc3a4a364121e43f6aa0e/pyproject.toml#L63-L65)\] - [Cisco](https://www.cisco.com) \[[1](https://github.com/CiscoDevNet/sastre/blob/76da836c9df01f1d3d40df5475c0d2caff4db566/pyproject.toml#L1-L3)|[2](https://github.com/CiscoDevNet/sdwan-devops/blob/bb6dde778af881be257fab722b12196599f63ddf/sdwan_config_builder/pyproject.toml#L1-L3)\] - [Databricks](https://www.databricks.com) \[[1](https://github.com/databrickslabs/ucx/blob/80145a4f2b6dccf65c1ad048fdb4d1e2622afa09/pyproject.toml#L1-L3)|[2](https://github.com/databricks-industry-solutions/many-model-forecasting/blob/a9e347b0444354bf836a8f528e4deb547e7bdd05/pyproject.toml#L35-L37)|[3](https://github.com/databrickslabs/pylint-plugin/blob/3b33c79dea74bdaac011488e16ad0121db4150b1/pyproject.toml#L34-L36)\] - [Datadog](https://www.datadoghq.com) \[[1](https://github.com/DataDog/datadogpy/blob/63d0c01b5bbcb8158cf3ddab153639951ab44945/pyproject.toml#L1-L3)|[2](https://github.com/DataDog/integrations-core/pulls?q=is%3Apr+author%3Aofek+in%3Atitle+Add+pyproject.toml+file)|[3](https://github.com/DataDog/integrations-extras/pulls?q=is%3Apr+author%3Aofek+in%3Atitle+Add+pyproject.toml+file)|[4](https://github.com/DataDog/mkdocs-click/blob/434925323f3bb187595d4c7f6a2c80b790015109/pyproject.toml#L1-L3)\] - [deepset](https://www.deepset.ai) \[[1](https://github.com/deepset-ai/haystack/blob/728383a14968111b0a032480ac276d6e3313332b/pyproject.toml#L1-L5)|[2](https://github.com/deepset-ai/deepset-cloud-sdk/blob/18c76d4b7a3863040fac0d9e6f47c765f266d7fa/pyproject.toml#L1-L3)\] - [Elastic](https://www.elastic.co) \[[1](https://github.com/elastic/rally/blob/8ba7980bb25b85f25fe20f3fd5dd8e12b9b1214b/pyproject.toml#L1-L3)|[2](https://github.com/elastic/rally-tracks/blob/33840005cd3e2a6191d73a567e5c2c0858169270/pyproject.toml#L1-L3)|[3](https://github.com/elastic/curator/blob/b41743a061ad790820affe7acee5f71abe819357/pyproject.toml#L1-L3)\] - [Google](https://about.google) \[[1](https://github.com/google/latexify_py/blob/9307e6e70df0d0a5f7d524833a85e2c25ffe66ef/pyproject.toml#L1-L5)|[2](https://github.com/google/gcp_scanner/blob/93dc594a6d920d1aff9bc8fef780a32056c12e27/pyproject.toml#L1-L3)|[3](https://github.com/GoogleCloudPlatform/cloud-build-samples/blob/a66407bc412a2726781f30063923a49bb6789064/python-example-noncontainer-artifacts/pyproject.toml#L1-L3)|[4](https://github.com/google/visualblocks/blob/3809f598253cdad2d93ed82b1e2623c10b4a5a0b/python/pyproject.toml#L1-L3)|[5](https://github.com/google/jaxtyping/blob/1acc0d7153f3881870b0376496d8efa27689cb3b/pyproject.toml#L29-L31)|[6](https://github.com/GoogleCloudPlatform/database-assessment/blob/d14d587cb2cab55cc0b1b92d79d0b30f12807b42/pyproject.toml#L126-L128)\] - [IBM](https://www.ibm.com) \[[1](https://github.com/IBM/python-log-router/blob/b0fc624cde262c6faadd5cb2e780e1ed7847f6c2/pyproject.toml#L1-L3)\] - [JPMorgan Chase](https://www.jpmorganchase.com) \[[1](https://github.com/jpmorganchase/jupyter-fs/blob/e7ea3ced16e8f7f1297ac8bed3f028b641558256/pyproject.toml#L1-L7)\] - [Intel Corporation](https://www.intel.com) \[[1](https://github.com/intel/neural-compressor/blob/5f6f38b96d45d0253b8de239df51c09b2471a8fb/neural_coder/extensions/neurl_compressor_ext_lab_alibaba/pyproject.toml#L1-L3)|[2](https://github.com/intel/tdx-tools/blob/ba4ba1796f21388d15cb14ecf673747c303ea0ae/utils/ovmfkeyenroll/pyproject.toml#L1-L3)|[3](https://github.com/intel/open-domain-question-and-answer/blob/6d8e90acb738ea3fe33d400c549c45ee05461afc/pyproject.toml#L1-L5)\] - [McKinsey](https://www.mckinsey.com) \[[1](https://github.com/mckinsey/vizro/blob/a7e88f19b7f50df19f9e0981ae19b36ccd83bc52/vizro-core/pyproject.toml#L1-L3)|[2](https://github.com/mckinsey/vizro/blob/a7e88f19b7f50df19f9e0981ae19b36ccd83bc52/vizro-ai/pyproject.toml#L1-L3)\] - [Meta](https://about.facebook.com) \[[1](https://github.com/facebook/usort/blob/b3d1dc49abac0c06ac29f1ceb332d2b86a50e850/pyproject.toml#L1-L3)|[2](https://github.com/Instagram/Fixit/blob/c95b0ef9f8c02adfd6a541b55f22f0bd6a922706/pyproject.toml#L1-L3)|[3](https://github.com/meta-llama/llama-recipes/blob/44b66374bec23ad77c00af4348197e6641a8d2e3/pyproject.toml#L1-L3)\] - [Microsoft](https://www.microsoft.com) \[[1](https://github.com/microsoft/qsharp/blob/2ef271eea86f6cc4dff3c79526aaa79422489fcd/jupyterlab/pyproject.toml#L1-L3)|[2](https://github.com/microsoft/responsible-ai-toolbox-tracker/blob/4e37f81726ba7ccf76d0539a5edc3ba6a988c3a5/pyproject.toml#L1-L7)|[3](https://github.com/microsoft/CoML/blob/9a4d670c3f7ff7710556b8d75e502824f74664ce/pyproject.toml#L1-L3)|[4](https://github.com/microsoft/microxcaling/blob/142efb98622df68e4a4c01ca77d2fc02dfdec261/pyproject.toml#L18-L20)|[5](https://github.com/microsoft/sca-fuzzer/blob/c0d42786e06115daf8281e40e5475e8e69f6b10e/pyproject.toml#L1-L3)|[6](https://github.com/microsoft/TypeChat/blob/f53b971179d0136424a75d67287903a2421af98b/python/pyproject.toml#L1-L3)\] - [OpenAI](https://openai.com) \[[1](https://github.com/openai/openai-python/blob/e36956673d9049713c91bca6ce7aebe58638f483/pyproject.toml#L88-L90)\] - [Oracle](https://www.oracle.com) \[[1](https://github.com/oracle/graalpython/blob/9b41424fd80727614878b5903f9d8ae0447bfd4e/graalpy_virtualenv/pyproject.toml#L40-L42)\] - [Palo Alto Networks](https://www.paloaltonetworks.com) \[[1](https://github.com/PaloAltoNetworks/pc-python-integration/blob/a3e29d71c6704dfb07cf85d592dec15a9ea575b7/pyproject.toml#L1-L3)\] - [Quansight](https://quansight.com) \[[1](https://github.com/Quansight-Labs/jupyter-a11y-testing/blob/f36bf5b2e8cb87613c637fc5aa03401c92ec58d0/pyproject.toml#L3-L6)\] - [Red Hat](https://www.redhat.com) \[[1](https://github.com/RedHatQE/wrapanapi/blob/036f85a7fa97b86eee732804f61cfe574c571a6e/pyproject.toml#L1-L3)|[2](https://github.com/RedHatQE/widgetastic.core/blob/c40d7f50f3e55c9ac9f0da1b91a56f89949bbe0c/pyproject.toml#L52-L54)|[3](https://github.com/RedHatQE/widgetastic.patternfly4/blob/5b19fcdc123732639edc8cf715dbe5fc64f3bd28/pyproject.toml#L38-L40)|[4](https://github.com/redhat-developer/devspaces-images/blob/db8de2f54466e37986ce64d96436b566c75b0677/devspaces-udi/build/python/requirements-build.in#L12)|[5](https://github.com/RedHatQE/Sentaku/blob/19dc91c00b70cb2054e0c28d69906e894fa8c104/pyproject.toml#L1-L6)\] - [Salesforce](https://www.salesforce.com) \[[1](https://github.com/SalesforceAIResearch/uni2ts/blob/ce27c2f9a0c6ee9119997e8ef0026388f143dcd6/pyproject.toml#L1-L3)\] - [Snowflake](https://www.snowflake.com) \[[1](https://github.com/Snowflake-Labs/snowcli/blob/a8cafe80ef81969655a4391425b0f45c2874d1a4/pyproject.toml#L1-L3)\] - [Splunk](https://www.splunk.com) \[[1](https://github.com/splunk/splunk-mltk-container-docker/blob/e13ae55a4a16ea459092ee9c1e9ba9772cbe6bf2/package-dsdlsupport/pyproject.toml#L1-L3)\] - [The Westervelt Company](https://westervelt.com) \[[1](https://github.com/westerveltco/django-twc-project/blob/f20768d4d42761ec0ce44f3f2283b66e47f2c8f8/pyproject.toml#L1-L3)|[2](https://github.com/westerveltco/django-email-relay/blob/e576c0561408f3c27babc9035b7284fd580a69c2/pyproject.toml#L1-L3)|[3](https://github.com/westerveltco/django-simple-nav/blob/4c0dfd5ee4bfa28fd6696e1394e6bbe2e119bfcc/pyproject.toml#L1-L3)|[4](https://github.com/westerveltco/django-q-registry/blob/59ae52978a8d900b05a50465f40e2834a16f4303/pyproject.toml#L1-L3)|[5](https://github.com/westerveltco/wagtail-heroicons/blob/a8b8985ec3994156b85c07a440a30c8ad2f21263/pyproject.toml#L1-L3)|[6](https://github.com/westerveltco/django-opfield/blob/7818ce3cdc56d25807cd5bc8f613eb12de2c6177/pyproject.toml#L1-L3)\] - [Virtru](https://www.virtru.com) \[[1](https://github.com/virtru/access-pdp/blob/46089e8a2ef691b80f92bbd6777bdfbcff1c1671/clients/python/accesspdp/pyproject.toml#L24-L26)|[2](https://github.com/virtru/access-pdp/blob/46089e8a2ef691b80f92bbd6777bdfbcff1c1671/clients/python/attributes/pyproject.toml#L21-L23)\] - [VMware](https://www.vmware.com) \[[1](https://github.com/vmware/versatile-data-kit/blob/f77faec3e9ccd840b6dc6fdc95af8a434e822e71/projects/vdk-plugins/vdk-jupyter/vdk-jupyterlab-extension/pyproject.toml#L1-L3)|[2](https://github.com/vmware/repository-service-tuf-cli/blob/374f1ac0c2a4ada6d7a7c26fba55e811f2998be8/pyproject.toml#L1-L4)|[3](https://github.com/vmware/vhpc-toolkit/blob/b8429bc4753caa302a4fc8bb160cca89e84cfd45/pyproject.toml#L20-L22)\] - [Volvo Group](https://www.volvogroup.com) \[[1](https://github.com/VolvoGroup/dymoval/blob/75261b85635dce594719b01c5fc33ad951ce55b0/pyproject.toml#L1-L3)\] ## Organizations - [Free Ebook Foundation](https://ebookfoundation.org) \[[1](https://github.com/EbookFoundation/alt-text/blob/00433b1a971309a441ef4822322cc6ea6347d9b2/pyproject.toml#L1-L3)\] - [Greater Paris University Hospitals (AP-HP)](https://www.aphp.fr) \[[1](https://github.com/aphp/edspdf/blob/ec083ed7fedddbdbb398c6feee530e05273f7dbb/pyproject.toml#L195-L197)\] - [Massachusetts General Hospital](https://www.massgeneral.org) \[[1](https://github.com/pinellolab/DNA-Diffusion/blob/6530de4ae4e0ff95f6e0852cd0d77ee763fb8833/pyproject.toml#L1-L3)\] - [Let's Encrypt](https://letsencrypt.org) \[[1](https://github.com/letsencrypt/mariadb-sequential-partition-manager-py/blob/666de864bcd3e17001513cd14f8919b01be7dd58/pyproject.toml#L1-L3)\] - [Max Planck Society](https://www.mpg.de/en) \[[1](https://github.com/center-for-humans-and-machines/transformer-heads/blob/0a362a6654a9a0e357d759700c08991017b39fec/pyproject.toml#L1-L3)\] - [OpenTelemetry](https://opentelemetry.io) \[[1](https://github.com/open-telemetry/opentelemetry-python/issues/2884#issuecomment-1229539511)|[2](https://github.com/open-telemetry/opentelemetry-python-contrib/issues/1259#issuecomment-1235028860)\] - [Smithsonian Institution](https://www.si.edu) \[[1](https://github.com/Smithsonian/ngehtutil/blob/02921f3a2ce11eb3f1555a0b9d3b177592d2be37/pyproject.toml#L1-L3)\] - [The New York Public Library](https://www.nypl.org) \[[1](https://github.com/NYPL/python-utils/blob/79b6d1b98d35b318af23c2af2f4f25e2c8162b15/pyproject.toml#L1-L3)\] ## Government - [European Molecular Biology Laboratory](https://www.embl.org) - [European Bioinformatics Institute](https://www.ebi.ac.uk) \[[1](https://github.com/MarioniLab/oor_benchmark/blob/9117c354bb780b3cb5a73a30e68aa26fc68efdb5/pyproject.toml#L1-L3)\] - [Germany](https://en.wikipedia.org/wiki/Germany) - [Berlin Institute of Health](https://www.bihealth.org/en/) \[[1](https://github.com/BIH-CEI/napkon-string-matching/blob/48d0d0ade9f1f173df9a2881a71412bbe73a006b/pyproject.toml#L25-L27)\] - [Helmholtz Munich](https://www.helmholtz-munich.de/en) \[[1](https://github.com/theislab/moscot/blob/545d8ac7c6a648931699cddaa757ea47b63d9b5e/pyproject.toml#L1-L3)|[2](https://github.com/theislab/multigrate/blob/1974d5901d2894573acd823c3d4d3c4ba23aba7a/pyproject.toml#L1-L3)\] - [Norway](https://en.wikipedia.org/wiki/Norway) - [Statistics Norway](https://www.ssb.no/en/) \[[1](https://github.com/statisticsnorway/dapla-hurtigstart-jupyter-extension/blob/96ac7441c46ed92684a8850df5cc72be15446289/pyproject.toml#L1-L3)\] - [United Kingdom](https://en.wikipedia.org/wiki/United_Kingdom) - [The Alan Turing Institute](https://www.turing.ac.uk) \[[1](https://github.com/alan-turing-institute/bureau/blob/6ed1882eaeb2410814549c4ffc2c1860c1acf7ca/build/pyproject.toml#L1-L3)\] - [Department for Business and Trade](https://www.gov.uk/government/organisations/department-for-business-and-trade) \[[1](https://github.com/uktrade/mirror-git-to-s3/blob/ce38c7c689f5dba1f3c9de4e10b8889afc8e44b7/pyproject.toml#L1-L3)\] - [The National Archives](https://www.nationalarchives.gov.uk) \[[1](https://github.com/nationalarchives/da-ayr-webapp/blob/1e62d38c0fe14f7d391835c704ba715241affcdb/pyproject.toml#L1-L3)\] - [United States](https://en.wikipedia.org/wiki/United_States) - [NASA](https://www.nasa.gov) \[[1](https://github.com/spacetelescope/hstaxe/blob/c6a73c8211c3eac71f0aa6eb4125f5be227ae7c4/pyproject.toml#L1-L3)\] - [National Institute of Standards and Technology](https://www.nist.gov) \[[1](https://github.com/usnistgov/thermoextrap/blob/536bb94b5c08814171dccfe9569d16854a5404bc/pyproject.toml#L1-L7)|[2](https://github.com/usnistgov/labbench/blob/fc5762fc155b8eb30ba32b487b5244ed6ff78739/pyproject.toml#L99-L101)|[3](https://github.com/NERSC/sfapi_client/blob/685c4988501cd10ec3cb495368e2839d3b648124/pyproject.toml#L1-L3)|[4](https://github.com/usnistgov/cmomy/blob/1689a97c65d00fc6d48221e128b789839c56b034/pyproject.toml#L1-L7)|[5](https://github.com/usnistgov/tmmc-lnpy/blob/ee6e16e21aef5824352f214042ed52ba252bd588/pyproject.toml#L1-L7)\] - [National Security Agency](https://www.nsa.gov) \[[1](https://github.com/NationalSecurityAgency/ghidra/blob/6242fda158fed6c7dbbd6928a4a74371a212c373/Ghidra/Debug/Debugger-agent-lldb/src/main/py/pyproject.toml#L1-L3)|[2](https://github.com/NationalSecurityAgency/ghidra/blob/6242fda158fed6c7dbbd6928a4a74371a212c373/Ghidra/Debug/Debugger-agent-gdb/src/main/py/pyproject.toml#L1-L3)\] - [National Telecommunications and Information Administration](https://www.ntia.gov) \[[1](https://github.com/NTIA/scos-tekrsa/blob/73090a737fdc0bd3a6c7c08deb170e00018d9ceb/pyproject.toml#L1-L3)|[2](https://github.com/NTIA/scos-actions/blob/a388aa46d414c7b5e67f76f8982bff2f534014f7/pyproject.toml#L1-L3)|[3](https://github.com/NTIA/tekrsa-api-wrap/blob/edce621075f053809c1640c6197c46bbc6456a10/pyproject.toml#L1-L3)|[4](https://github.com/NTIA/Preselector/pull/10)\] ## Academia - [Brown University](https://www.brown.edu) - [Carney Institute for Brain Science](https://www.brown.edu/carney/) \[[1](https://github.com/AutoResearch/sourpea/blob/f3007a58d3e5a647ccfb37fee24e44468d5ec707/pyproject.toml#L1-L3)\] - [Carnegie Mellon University](https://www.cmu.edu) - [Department of Chemical Engineering](https://www.cheme.engineering.cmu.edu/) \[[1](https://github.com/FAIR-Chem/fairchem/blob/e344dc83f9e295c4be3830118302daf96e8a9b78/packages/fairchem-core/pyproject.toml#L1-L3)|[2](https://github.com/FAIR-Chem/fairchem/blob/e344dc83f9e295c4be3830118302daf96e8a9b78/packages/fairchem-data-oc/pyproject.toml#L1-L3)|[3](https://github.com/FAIR-Chem/fairchem/blob/e344dc83f9e295c4be3830118302daf96e8a9b78/packages/fairchem-data-om/pyproject.toml#L1-L3)|[4](https://github.com/FAIR-Chem/fairchem/blob/e344dc83f9e295c4be3830118302daf96e8a9b78/packages/fairchem-demo-ocpapi/pyproject.toml#L1-L3)|[5](https://github.com/FAIR-Chem/fairchem/blob/e344dc83f9e295c4be3830118302daf96e8a9b78/packages/fairchem-applications-AdsorbML/pyproject.toml#L1-L3)|[6](https://github.com/FAIR-Chem/fairchem/blob/e344dc83f9e295c4be3830118302daf96e8a9b78/packages/fairchem-applications-cattsunami/pyproject.toml#L1-L3)\] - [Chinese Academy of Sciences](https://english.cas.cn) - [Academy of Mathematics and Systems Science](http://english.amss.cas.cn) \[[1](https://github.com/zhanglabtools/ConsTADs/blob/db732cf820569564f933cd290736ad83b9c99dea/pyproject.toml#L1-L3)\] - [Georgia Institute of Technology](https://www.gatech.edu) - [Georgia Tech Database Group](https://db.cc.gatech.edu) \[[1](https://github.com/georgia-tech-db/sqlfuzz/blob/e85895dae1c92a223cbc13b12d4a19f297c410ab/pyproject.toml#L1-L3)\] - [Harvard University](https://www.harvard.edu) - [Department of Molecular and Cellular Biology](https://www.mcb.harvard.edu) \[[1](https://github.com/Hekstra-Lab/raman-analysis/blob/4b548b5ea935e52a7bd1f0ec8f4a00c822b81ede/pyproject.toml#L2-L4)\] - [Heidelberg University](https://www.uni-heidelberg.de) - [Center for Molecular Biology](https://www.zmbh.uni-heidelberg.de) \[[1](https://github.com/anders-biostat/pymetdense/blob/a1d210f2c03d2919b549f2fed1e4db986d01c8d5/pyproject.toml#L1-L3)\] - [Leiden University](https://www.universiteitleiden.nl/en) - [Leiden University Libraries](https://www.library.universiteitleiden.nl) \[[1](https://github.com/LeidenUniversityLibrary/maps-tools/blob/d7a9fc683be919d4f5538f6a6c80319558064968/pyproject.toml#L3-L5)|[2](https://github.com/LeidenUniversityLibrary/archminer/blob/61465dc36924ffe593653aa5888a27617c93860e/pyproject.toml#L1-L3)\] - [Maastricht University](https://www.maastrichtuniversity.nl) - [Institute of Data Science](https://www.maastrichtuniversity.nl/research/institute-data-science) \[[1](https://github.com/MaastrichtU-IDS/fair-test/blob/9c88c18cb1b0fa8d37336cdd2b7b132cb979a83a/pyproject.toml#L95-L97)|[2](https://github.com/MaastrichtU-IDS/fair-enough-metrics/blob/dad29ef1f99f5e01a76799d909e538565ae2ed4e/pyproject.toml#L50-L52)|[3](https://github.com/MaastrichtU-IDS/cookiecutter-python-package/blob/1eda79b6ca64c27b4b12407464b3c2dc2511af94/%7B%7Bcookiecutter.package_name%7D%7D/pyproject.toml#L70-L72)|[4](https://github.com/MaastrichtU-IDS/translator-openpredict/blob/b6e0f5f5100129d3038618f86e4c2c05d62d51f4/pyproject.toml#L1-L3)|[5](https://github.com/MaastrichtU-IDS/cookiecutter-trapi-predict-kit/blob/a329c6d66c1b96b53e9fd02501c762aee32a69fb/%7B%7Bcookiecutter.package_name%7D%7D/pyproject.toml#L1-L3)|[6](https://github.com/MaastrichtU-IDS/sparql-profiler/blob/ac70a9e8575f9c9769eb1caf140e2f81b136835c/pyproject.toml#L1-L3)|[7](https://github.com/MaastrichtU-IDS/knowledge-collaboratory/blob/8263d69e7b8e485b0aff7e88a3a7aed3cceaa253/backend/pyproject.toml#L1-L3)|[8](https://github.com/MaastrichtU-IDS/LUCE/blob/94c9a0dda840a3d81828a89aefcfb19fee51cd60/pyproject.toml#L1-L3)\] - [Massachusetts Institute of Technology](https://www.mit.edu) - [Computer Science and Artificial Intelligence Laboratory](https://www.csail.mit.edu) \[[1](https://github.com/Learning-and-Intelligent-Systems/lisdf/blob/d49a85a3924909f1d10fef40463757b141f47f90/pyproject.toml#L1-L3)\] - [Digital Humanities](https://digitalhumanities.mit.edu) \[[1](https://github.com/cuthbertLab/music21/blob/5417b3ce6415ab016a39564e21e29799387263e9/pyproject.toml#L1-L5)\] - [Medical University of Innsbruck](https://www.i-med.ac.at/mypoint/index.xml.en) - [Institute of Bioinformatics](https://icbi.i-med.ac.at) \[[1](https://github.com/icbi-lab/infercnvpy/blob/12c103f4062860d5d91152222163eb7d22340146/pyproject.toml#L1-L3)\] - [Polytechnique Montréal](https://www.polymtl.ca/en/) - [Department of Computer Engineering and Software Engineering](https://www.polymtl.ca/gigl/) \[[1](https://github.com/corail-research/seahorse/blob/e876042f92c704180c16055a6720ef828c21e0ae/pyproject.toml#L1-L3)\] - [Siberian Branch of the Russian Academy of Sciences](https://www.sbras.ru/en/) - [Institute of Cytology and Genetics](https://www.icgbio.ru/en/) \[[1](https://github.com/genomech/FastContext/blob/f8ff7f4bbea9d6d3cdf2e3a361f72e9283b04f67/pyproject.toml#L1-L3)|[2](https://github.com/genomech/exoclasma-index/blob/2e0555c3e86d731f3aa8c978b23b586d3a0c492e/pyproject.toml#L1-L3)|[3](https://github.com/genomech/exoclasma-fastq/blob/80ea3eddf603d2b54bb02b5ada6d275a9436f287/pyproject.toml#L1-L3)|[4](https://github.com/genomech/exoclasma-pipe/blob/fbe365dd9301eec51879ef53b1704be66813bb8b/pyproject.toml#L1-L3)\] - [Stanford University](https://www.stanford.edu) - [Empirical Security Research Group](https://esrg.stanford.edu/) \[[1](https://github.com/stanford-esrg/gps/blob/66f803bfd4726cd9d1b3e1724abfd34a36079530/pyproject.toml#L1-L3)\] - [University of British Columbia](https://www.ubc.ca) - [Department of Earth, Ocean and Atmospheric Sciences](https://www.eoas.ubc.ca) \[[1](https://github.com/UBC-MOAD/cookiecutter-MOAD-pypkg/blob/75441f962a6e7b87c09bcae031fdfaec3cf75f74/%7B%7Bcookiecutter.package_name%7D%7D/pyproject.toml#L18-L20)|[2](https://github.com/SalishSeaCast/NEMO-Cmd/blob/be5425d49eaf845eaba8f1611455f2de75aa194b/pyproject.toml#L19-L21)|[3](https://github.com/SalishSeaCast/SalishSeaNowcast/blob/1a850c1368b7f3504e5804101647ab481fbe7048/pyproject.toml#L19-L21)\] - [University of California, Berkeley](https://www.berkeley.edu) - [Center for Computational Biology](https://ccb.berkeley.edu) \[[1](https://github.com/YosefLab/scib-metrics/blob/4dcbf55d80e21cf141332ba718fc5c0eb012eac1/pyproject.toml#L1-L3)\] - [University of California, Santa Barbara](https://www.ucsb.edu) - [Department of Computer Science](https://www.cs.ucsb.edu) \[[1](https://github.com/UCSBarchlab/PyRTL/blob/46b0f3d2ff0e334d9cf3a04ef5b090bd55fcc177/pyproject.toml#L10-L12)\] - [University of Freiburg](https://uni-freiburg.de) - [Freiburg Center for Data Analysis and Modeling](https://www.fdm.uni-freiburg.de) \[[1](https://github.com/Spatial-Systems-Biology-Freiburg/FisInMa/blob/b9c5a980ae03d6f577e17242e6bce7822f665f94/pyproject.toml#L1-L3)\] - [University of Illinois Urbana-Champaign](https://illinois.edu) - [Grainger College of Engineering](https://grainger.illinois.edu) \[[1](https://github.com/SPI2Py/SPI2Py/blob/feefd7bb003b42f4790982d68e7e4e5fdb6ca8ad/pyproject.toml#L1-L3)\] - [University of Lausanne](https://www.unil.ch/central/en/home.html) - [Department of Computational Biology](https://www.unil.ch/dbc/en/home.html) \[[1](https://github.com/CSOgroup/cellcharter/blob/00b4cd44f13702bd8832ed6705614efda048b7b7/pyproject.toml#L1-L3)\] - [University of Ljubljana](https://www.uni-lj.si/eng/) - [Faculty of Mechanical Engineering](https://www.uni-lj.si/academies_and_faculties/faculties/2013071111460582/) \[[1](https://github.com/ladisk/speckle_pattern/blob/055f45b66c7985564a9fa400d8d2f41ddd181d31/pyproject.toml#L1-L3)\] - [University of Massachusetts Amherst](https://www.umass.edu) - [College of Information and Computer Sciences](https://www.cics.umass.edu) \[[1](https://github.com/plasma-umass/ChatDBG/blob/1bc32332464afe2f3932b0a9f586a88c8fb7a357/pyproject.toml#L1-L3)\] - [University of Oxford](https://www.ox.ac.uk) - [Oxford Research Software Engineering](https://www.rse.ox.ac.uk) \[[1](https://github.com/OxfordRSE/oxrse_unit_conv/blob/e4cb7d15bbc8ba4ab7ff816d3bbdfb65fbda3f76/pyproject.toml#L21-L23)\] - [University of Pennsylvania](https://www.upenn.edu) - [Lifespan Informatics and Neuroimaging Center](https://www.pennlinc.io) \[[1](https://github.com/PennLINC/qsiprep/blob/f0d661589cc2efd9a787b2c1b3db397a897daa98/pyproject.toml#L1-L3)|[2](https://github.com/PennLINC/xcp_d/blob/e68c802604ac9ca2c179ca2f164ceb4db7c1fe66/pyproject.toml#L1-L3)|[3](https://github.com/PennLINC/aslprep/blob/aeee1a22fce8f8f1bd922de6d822124fb7b3343f/pyproject.toml#L1-L3)|[4](https://github.com/PennLINC/CuBIDS/blob/fac73803b7c6d6ab938af142783c8159a6df6c60/pyproject.toml#L1-L3)\] - [University of Regensburg](https://www.uni-regensburg.de/en) - [Spang Lab](https://www.spang-lab.de) \[[1](https://github.com/spang-lab/adadmire/blob/14f169a4d493952433224e518c4ed2484d6cc2bd/pyproject.toml#L1-L3)\] - [University of Sussex](https://www.sussex.ac.uk) - [Predictive Analytics Lab](https://wearepal.ai) \[[1](https://github.com/wearepal/teext/blob/9253c9412b4ca340c42c0b9de0e8ac8f5ccdd0e3/pyproject.toml#L1-L3)\] - [University of Toronto Scarborough](https://www.utsc.utoronto.ca/home/) - [utsc-networking](https://github.com/utsc-networking) \[[1](https://github.com/utsc-networking/utsc-tools/blob/02a79d48d133470a4394fced138b40c660cf111c/projects/core/pyproject.toml#L1-L3)|[2](https://github.com/utsc-networking/utsc-tools/blob/02a79d48d133470a4394fced138b40c660cf111c/projects/nautobot/pyproject.toml#L1-L3)|[3](https://github.com/utsc-networking/utsc-tools/blob/02a79d48d133470a4394fced138b40c660cf111c/projects/switchconfig/pyproject.toml#L1-L3)|[4](https://github.com/utsc-networking/utsc-tools/blob/02a79d48d133470a4394fced138b40c660cf111c/projects/scripts/pyproject.toml#L1-L3)\] - [University of Washington](https://www.washington.edu) - [Interactive Data Lab](https://idl.cs.washington.edu) \[[1](https://github.com/uwdata/mosaic/blob/a3b78fef28fcc3e711bb922c97c3113aa6cf9122/packages/widget/pyproject.toml#L1-L3)\] - [Virtual Brain Lab](https://github.com/VirtualBrainLab) \[[1](https://github.com/VirtualBrainLab/ephys-link/blob/ebdf3a1488f1010faa19f22397f10d6be4d29d6f/pyproject.toml#L1-L3)\] - [University of Wisconsin-Madison](https://www.wisc.edu) - [Data Science Institute](https://datascience.wisc.edu/institute/) \[[1](https://github.com/UW-Madison-DSI/ask-xDD/blob/ae62d038303927b69dba9dadfef94b55b55731b3/pyproject.toml#L20-L22)\] - [Waseda University](https://www.waseda.jp/top/en/) - [Tackeuchi Laboratory](https://www.f.waseda.jp/atacke/) \[[1](https://github.com/wasedatakeuchilab/python-project-template-hatch/blob/58949ab351d81b67f14aa45abf7c70b87394e2dc/pyproject.toml#L1-L3)|[2](https://github.com/wasedatakeuchilab/webapp-photo-luminescence/blob/864d1019650a2b057f761aa91ed9a6cbe6c1b455/pyproject.toml#L1-L3)|[3](https://github.com/wasedatakeuchilab/tlab-analysis/blob/72f0a710e35613e8996f473a80e5cb6c3f8c523e/pyproject.toml#L1-L3)|[4](https://github.com/wasedatakeuchilab/tlab-pptx/blob/ef331176906447dbbcf33e46f060b60ac3c007c5/pyproject.toml#L1-L3)|[5](https://github.com/wasedatakeuchilab/tlab-google/blob/53ae597611a146c90116b3b9277430832e1d04c9/pyproject.toml#L1-L3)\] - [Wellcome Sanger Institute](https://www.sanger.ac.uk) \[[1](https://github.com/sanger/lab-share-lib/blob/b3290b1922aabc29ac256dc034b8cfcc7b30f143/pyproject.toml#L25-L27)\] ## Research - [Clariah](https://www.clariah.nl) \[[1](https://github.com/CLARIAH/pure3d/blob/3f93d62cb1f5223836c9ebf4c058e6f491de71b9/pyproject.toml#L1-L3)\] - [CloudDrift](https://cloud-drift.github.io/clouddrift/) \[[1](https://github.com/Cloud-Drift/clouddrift/blob/5e654569c869a027fe0a486f06917b358837d41e/pyproject.toml#L1-L3)\] - [Dask](https://www.dask.org) \[[1](https://github.com/dask/dask-ml/blob/b95ba909c6dcd37c566f5193ba0b918396edaaee/pyproject.toml#L1-L3)|[2](https://github.com/dask/dask-labextension/blob/39b69ac5b8bfdb726347aabe3da86a15cb201b77/pyproject.toml#L1-L3)\] - [GAMA](https://gama-platform.org) \[[1](https://github.com/gama-platform/Gama-client-python/blob/d9fecae0dff9050f39a011c4f4bdb02f5137b241/pyproject.toml#L1-L3)\] - [IPython](https://ipython.org) \[[1](https://github.com/ipython/ipykernel/blob/dd0a9863e07c1d49f5aaf72c0c62670acee71b55/pyproject.toml#L1-L3)|[2](https://github.com/ipython/ipyparallel/blob/06f5d3df1f6e858a83c3af29438ae6d5af801267/pyproject.toml#L1-L6)|[3](https://github.com/ipython/traitlets/blob/ac13bbb885c275fd446f85a9d2e74d8058c2b3c1/pyproject.toml#L1-L3)\] - [MNE](https://mne.tools) \[[1](https://github.com/mne-tools/mne-python/blob/8af33df490f94c3dd628cfc23beafed1a6cc6361/pyproject.toml#L1-L3)|[2](https://github.com/mne-tools/mne-bids-pipeline/blob/a6995abc39fab333ab957baa45b0026bdb12a3f9/pyproject.toml#L1-L3)|[3](https://github.com/mne-tools/mne-bids/blob/8321aef66e1c920bd4df748e326e06b0bf696e4c/pyproject.toml#L1-L3)\] - [NIPY](https://nipy.org) \[[1](https://github.com/nipy/nibabel/blob/298788070a36e8d8616df36ebed0d4339f00e43b/pyproject.toml#L1-L3)|[2](https://github.com/nipy/quickshear/blob/83b362b794d52183ff40ec5dcc98239b94c5633a/pyproject.toml#L1-L3)\] - [Project Jupyter](https://jupyter.org) - [Jupyter](https://github.com/jupyter) \[[1](https://github.com/jupyter/notebook/blob/b9bab689c9a2f33eb3b2cca1383c2d99baa7a2e8/pyproject.toml#L1-L3)|[2](https://github.com/jupyter/jupyter_core/blob/2a6fb6d2b28ca712268eee15d7b907a3a73271d8/pyproject.toml#L1-L3)|[3](https://github.com/jupyter/jupyter_client/blob/e526895a29e0331a167167070b1603f20a4b2840/pyproject.toml#L1-L3)|[4](https://github.com/jupyter/nbconvert/blob/af70c9fa83bee4d0c92e06b4ede4ef5ea7c920b0/pyproject.toml#L1-L3)\] - [JupyterLab](https://github.com/jupyterlab) \[[1](https://github.com/jupyterlab/hatch_jupyter_builder)|[2](https://github.com/jupyterlab/jupyterlab/pull/12606)|[3](https://github.com/jupyterlab/maintainer-tools/blob/0e95a837469f5325e5a840bd194fe8273087d2f6/pyproject.toml#L1-L3)|[4](https://github.com/jupyterlab/pytest-check-links/blob/b07e705d590e9fce22dc21191018f4f72ec7215b/pyproject.toml#L1-L3)|[5](https://github.com/jupyterlab/extension-cookiecutter-js/pull/41)\] - [Jupyter Server](https://github.com/jupyter-server) \[[1](https://github.com/jupyter-server/jupyter_server/blob/061d846fbd0cf2f0be50d12c4a15feffd3214774/pyproject.toml#L1-L3)|[2](https://github.com/jupyter-server/enterprise_gateway/blob/b45a81ae70680be7f8e0d1e3daed1df3063667fa/pyproject.toml#L1-L3)|[3](https://github.com/jupyter-server/jupyter_server_terminals/blob/4b32ceb34b9b6ae9c677424cc65c9c3bfe243719/pyproject.toml#L1-L3)|[4](https://github.com/jupyter-server/synchronizer/blob/5809e9ffd188beff743874a434884662867bb573/pyproject.toml#L1-L3)\] - [RAPIDS](https://rapids.ai) \[[1](https://github.com/rapidsai/jupyterlab-nvdashboard/blob/578b58b4fd0ec31a7cc02ac6d2795622c00ef478/pyproject.toml#L3-L9)\] - [Scikit-HEP](https://scikit-hep.org) \[[1](https://github.com/scikit-hep/uproot-browser/blob/f41ce3f3887057f5ec9a6cd164c3c41d1ec3d633/pyproject.toml#L1-L3)|[2](https://github.com/scikit-hep/uhi/blob/95ad870218a6fd7f2ab02f3d2b5c421e93a1f03f/pyproject.toml#L1-L3)|[3](https://github.com/scikit-hep/repo-review/blob/007026a62c6c61914ec49e111be587104f59b8ae/pyproject.toml#L1-L3)|[4](https://github.com/scikit-hep/hist/blob/768ea7de75f20c06caa6ded72d70bd132e4c9467/pyproject.toml#L1-L3)|[5](https://github.com/scikit-hep/vector/blob/cac88a2e0f1c4bf7bceaafbea6e234b3147e3ca3/pyproject.toml#L1-L6)|[6](https://github.com/scikit-hep/uproot5/blob/f9213e0f8c29435890e5aa72e336330bb7a785fe/pyproject.toml#L1-L5)|[7](https://github.com/scikit-hep/particle/blob/723c1618c7058feb0a914a6738d8b8018a5df1bd/pyproject.toml#L1-L3)|[8](https://github.com/scikit-hep/hepunits/blob/bd1302cbb85ed486c057f8b078ad4e026d65bb1c/pyproject.toml#L1-L3)|[9](https://github.com/scikit-hep/decaylanguage/blob/eae09aee69acef2d1c19f55665c5ca8b28588e01/pyproject.toml#L1-L6)|[10](https://github.com/scikit-hep/pyhf/blob/efbf201b57345063afec66c254aace3148f1f055/pyproject.toml#L1-L3)\] - [scverse](https://scverse.org) \[[1](https://github.com/scverse/spatialdata-io/blob/15c395de859d6d06e5032016c9406acae5cac454/pyproject.toml#L1-L3)|[2](https://github.com/scverse/spatialdata-notebooks/blob/2b539a1d23b06b509a46a6bf3cb6594f1952f830/pyproject.toml#L1-L3)|[3](https://github.com/scverse/cookiecutter-scverse/blob/2892e1ddf0dd558cb9b547b47a7c2d0a156c9ef1/%7B%7Bcookiecutter.project_name%7D%7D/pyproject.toml#L1-L3)\] - [Spyder](https://www.spyder-ide.org) \[[1](https://github.com/spyder-ide/envs-manager/blob/9c487532cbb4804c94d7cf23dcec9404b2a1c7ec/pyproject.toml#L1-L3)\] ## Security - [Armory](https://github.com/twosixlabs/armory/blob/330caa23d54ce82886606810f103ce1a0eec98ce/pyproject.toml#L129-L134) - [in-toto](https://github.com/in-toto/in-toto/blob/2768904b8a3892529aba8f8a605461fd178d9a58/pyproject.toml#L1-L3) - [The Update Framework](https://github.com/theupdateframework/python-tuf/blob/72424a958b60817155fcacfed1216163790b26f7/pyproject.toml#L2-L4) ## Crypto - [Ocean Protocol](https://oceanprotocol.com) \[[1](https://github.com/oceanprotocol/pybundlr/blob/484c755d96be2da35cda83f01861745867cdb2d4/pyproject.toml#L1-L6)\] ================================================ FILE: docs/config/build.md ================================================ # Build configuration ----- [Build targets](#build-targets) are defined as sections within `tool.hatch.build.targets`: ```toml config-example [tool.hatch.build.targets.] ``` !!! tip Although not recommended, you may define global configuration in the `tool.hatch.build` table. Keys may then be overridden by target config. ## Build system To be compatible with the broader [Python packaging ecosystem](../build.md#packaging-ecosystem), you must define the [build system](https://peps.python.org/pep-0517/#source-trees) as follows: ```toml tab="pyproject.toml" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" ``` The version of `hatchling` defined here will be used to build all targets. Hatchling is a standards-compliant[^1] build backend and is a dependency of Hatch itself. ## File selection ### VCS By default, Hatch will respect the first `.gitignore` or `.hgignore` file found in your project's root directory or parent directories. Set `ignore-vcs` to `true` to disable this behavior: ```toml config-example [tool.hatch.build.targets.sdist] ignore-vcs = true ``` !!! note For `.hgignore` files only glob syntax is supported. ### Patterns You can set the `include` and `exclude` options to select exactly which files will be shipped in each build, with `exclude` taking precedence. Every entry represents a [Git-style glob pattern](https://git-scm.com/docs/gitignore#_pattern_format). For example, the following configuration: ```toml config-example [tool.hatch.build.targets.sdist] include = [ "pkg/*.py", "/tests", ] exclude = [ "*.json", "pkg/_compat.py", ] ``` will exclude every file with a `.json` extension, and will include everything under a `tests` directory located at the root and every file with a `.py` extension that is directly under a `pkg` directory located at the root except for `_compat.py`. ### Artifacts If you want to include files that are [ignored by your VCS](#vcs), such as those that might be created by [build hooks](#build-hooks), you can use the `artifacts` option. This option is semantically equivalent to `include`. Note that artifacts are not affected by the `exclude` option. Artifacts can be excluded by using more explicit paths or by using the `!` negation operator. When using the `!` operator, the negated pattern(s) must come after the more generic ones. ```toml config-example [tool.hatch.build.targets.wheel] artifacts = [ "*.so", "*.dll", "!/foo/*.so", ] ``` ### Explicit selection #### Generic You can use the `only-include` option to prevent directory traversal starting at the project root and only select specific relative paths to directories or files. Using this option ignores any defined [`include` patterns](#patterns). ```toml config-example [tool.hatch.build.targets.sdist] only-include = ["pkg", "tests/unit"] ``` #### Packages The `packages` option is semantically equivalent to `only-include` (which takes precedence) except that the shipped path will be collapsed to only include the final component. So for example, if you want to ship a package `foo` that is stored in a directory `src` you would do: ```toml config-example [tool.hatch.build.targets.wheel] packages = ["src/foo"] ``` ### Forced inclusion The `force-include` option allows you to select specific files or directories from anywhere on the file system that should be included and map them to the desired relative distribution path. For example, if there was a directory alongside the project root named `artifacts` containing a file named `lib.so` and a file named `lib.h` in your home directory, you could ship both files in a `pkg` directory with the following configuration: ```toml config-example [tool.hatch.build.targets.wheel.force-include] "../artifacts" = "pkg" "~/lib.h" = "pkg/lib.h" ``` !!! note - Files must be mapped exactly to their desired paths, not to directories. - The contents of directory sources are recursively included. - To map directory contents directly to the root use `/` (a forward slash). - Sources that do not exist will raise an error. !!! warning Files included using this option will overwrite any file path that was already included by other file selection options. ### Default file selection If no file selection options are provided, then what gets included is determined by each [build target](#build-targets). ### Excluding files outside packages If you want to exclude non-[artifact](#artifacts) files that do not reside within a Python package, set `only-packages` to `true`: ```toml config-example [tool.hatch.build.targets.wheel] only-packages = true ``` ### Rewriting paths You can rewrite relative paths to directories with the `sources` option. For example, the following configuration: ```toml config-example [tool.hatch.build.targets.wheel.sources] "src/foo" = "bar" ``` would distribute the file `src/foo/file.ext` as `bar/file.ext`. If you want to remove path prefixes entirely, rather than setting each to an empty string, you can define `sources` as an array: ```toml config-example [tool.hatch.build.targets.wheel] sources = ["src"] ``` If you want to add a prefix to paths, you can use an empty string. For example, the following configuration: ```toml config-example [tool.hatch.build.targets.wheel.sources] "" = "foo" ``` would distribute the file `bar/file.ext` as `foo/bar/file.ext`. The [packages](#packages) option itself relies on sources. Defining `#!toml packages = ["src/foo"]` for the `wheel` target is equivalent to the following: ```toml config-example [tool.hatch.build.targets.wheel] only-include = ["src/foo"] sources = ["src"] ``` ### Performance All encountered directories are traversed by default. To skip non-[artifact](#artifacts) directories that are excluded, set `skip-excluded-dirs` to `true`: ```toml config-example [tool.hatch.build] skip-excluded-dirs = true ``` !!! warning This may result in not shipping desired files. For example, if you want to include the file `a/b/c.txt` but your [VCS ignores](#vcs) `a/b`, the file `c.txt` will not be seen because its parent directory will not be entered. In such cases you can use the [`force-include`](#forced-inclusion) option. ## Reproducible builds By default, [build targets](#build-targets) will build in a reproducible manner provided that they support that behavior. To disable this, set `reproducible` to `false`: ```toml config-example [tool.hatch.build] reproducible = false ``` When enabled, the [SOURCE_DATE_EPOCH](https://reproducible-builds.org/specs/source-date-epoch/) environment variable will be used for all build timestamps. If not set, then Hatch will use an [unchanging default value](../plugins/utilities.md#hatchling.builders.utils.get_reproducible_timestamp). ## Output directory When the output directory is not provided to the [`build`](../cli/reference.md#hatch-build) command, the `dist` directory will be used by default. You can change the default to a different directory using a relative or absolute path like so: ```toml config-example [tool.hatch.build] directory = "" ``` ## Dev mode By default for [dev mode](environment/overview.md#dev-mode) environment installations or [editable installs](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs), the `wheel` target will determine which directories should be added to Python's search path based on the [selected files](#file-selection). If you want to override this detection or perhaps instruct other build targets as well, you can use the `dev-mode-dirs` option: ```toml config-example [tool.hatch.build] dev-mode-dirs = ["."] ``` If you don't want to add entire directories to Python's search path, you can enable a more targeted mechanism with the mutually exclusive `dev-mode-exact` option: ```toml config-example [tool.hatch.build] dev-mode-exact = true ``` !!! warning The `dev-mode-exact` mechanism is [not supported](https://github.com/microsoft/pylance-release/issues/2114) by static analysis tools & IDEs, therefore functionality such as autocompletion is unlikely to work. ## Build targets A build target can be provided by any [builder plugin](../plugins/builder/reference.md). There are three built-in build targets: [wheel](../plugins/builder/wheel.md), [sdist](../plugins/builder/sdist.md), and [custom](../plugins/builder/custom.md). ### Dependencies ### {: #target-dependencies } You can specify additional dependencies that will be installed in each build environment, such as for third party builders: ```toml config-example [tool.hatch.build.targets.your-target-name] dependencies = [ "your-builder-plugin" ] ``` You can also declare dependence on the project's [runtime dependencies](metadata.md#required) with the `require-runtime-dependencies` option: ```toml config-example [tool.hatch.build.targets.your-target-name] require-runtime-dependencies = true ``` Additionally, you may declare dependence on specific [runtime features](metadata.md#optional) of the project with the `require-runtime-features` option: ```toml config-example [tool.hatch.build.targets.your-target-name] require-runtime-features = [ "feature1", "feature2", ] ``` ### Versions If a build target supports multiple build strategies or if there are major changes over time, you can specify exactly which versions you want to build using the `versions` option: ```toml config-example [tool.hatch.build.targets.] versions = [ "v1", "beta-feature", ] ``` See the [wheel](../plugins/builder/wheel.md#versions) target for a real world example. ## Build hooks A build hook defines code that will be executed at various stages of the build process and can be provided by any [build hook plugin](../plugins/build-hook/reference.md). There is one built-in build hook: [custom](../plugins/build-hook/custom.md). Build hooks can be applied either globally: ```toml config-example [tool.hatch.build.hooks.] ``` or to specific build targets: ```toml config-example [tool.hatch.build.targets..hooks.] ``` ### Dependencies ### {: #hook-dependencies } You can specify additional dependencies that will be installed in each build environment, such as for third party build hooks: ```toml config-example [tool.hatch.build.hooks.your-hook-name] dependencies = [ "your-build-hook-plugin" ] ``` You can also declare dependence on the project's [runtime dependencies](metadata.md#required) with the `require-runtime-dependencies` option: ```toml config-example [tool.hatch.build.hooks.your-hook-name] require-runtime-dependencies = true ``` Additionally, you may declare dependence on specific [runtime features](metadata.md#optional) of the project with the `require-runtime-features` option: ```toml config-example [tool.hatch.build.hooks.your-hook-name] require-runtime-features = [ "feature1", "feature2", ] ``` ### Order of execution For each build target, build hooks execute in the order in which they are defined, starting with global hooks. As an example, for the following configuration: ```toml config-example [tool.hatch.build.targets.foo.hooks.hook2] [tool.hatch.build.hooks.hook3] [tool.hatch.build.hooks.hook1] ``` When target `foo` is built, build hook `hook3` will be executed first, followed by `hook1`, and then finally `hook2`. ### Conditional execution If you want to disable a build hook by default and control its use by [environment variables](#environment-variables), you can do so by setting the `enable-by-default` option to `false`: ```toml config-example [tool.hatch.build.hooks.] enable-by-default = false ``` ## Environment variables | Variable | Default | Description | | --- | --- | --- | | `HATCH_BUILD_CLEAN` | `false` | Whether or not existing artifacts should first be removed | | `HATCH_BUILD_CLEAN_HOOKS_AFTER` | `false` | Whether or not build hook artifacts should be removed after each build | | `HATCH_BUILD_HOOKS_ONLY` | `false` | Whether or not to only execute build hooks | | `HATCH_BUILD_NO_HOOKS` | `false` | Whether or not to disable all build hooks; this takes precedence over other options | | `HATCH_BUILD_HOOKS_ENABLE` | `false` | Whether or not to enable all build hooks | | `HATCH_BUILD_HOOK_ENABLE_` | `false` | Whether or not to enable the build hook named `` | | `HATCH_BUILD_LOCATION` | `dist` | The location with which to build the targets; only used by the [`build`](../cli/reference.md#hatch-build) command | [^1]: Support for [PEP 517][] and [PEP 660][] guarantees interoperability with other build tools. ================================================ FILE: docs/config/context.md ================================================ # Context formatting ----- You can populate configuration with the values of certain supported fields using the syntax of Python's [format strings](https://docs.python.org/3/library/string.html#formatstrings). Each field interprets the modifier part after the colon differently, if at all. ## Global fields Any configuration that declares support for context formatting will always support these fields. ### Paths | Field | Description | | --- | --- | | `root` | The root project directory | | `home` | The user's home directory | All paths support the following modifiers: | Modifier | Description | | --- | --- | | `uri` | The normalized absolute URI path prefixed by `file:` | | `real` | The path with all symbolic links resolved | | `parent` | The parent of the preceding path | !!! tip The `parent` modifier can be chained and may be combined with either the `uri` or `real` modifier, with the latter placed at the end. For example: ```toml config-example [tool.hatch.envs.test] dependencies = [ "example-project @ {root:parent:parent:uri}/example-project", ] ``` ### System separators | Field | Description | | --- | --- | | `/` | `\` on Windows, `/` otherwise | | `;` | `;` on Windows, `:` otherwise | ### Environment variables The `env` field and its modifier allow you to select the value of an environment variable. If the environment variable is not set, you must specify a default value as an additional modifier e.g. `{env:PATH:DEFAULT}`. ## Field nesting You can insert fields within others. For example, if you wanted a [script](environment/overview.md#scripts) that displays the value of the environment variable `FOO`, with a fallback to the environment variable `BAR`, with its own fallback to the user's home directory, you could do the following: ```toml config-example [tool.hatch.envs.test.scripts] display = "echo {env:FOO:{env:BAR:{home}}}" ``` ================================================ FILE: docs/config/dependency.md ================================================ # Dependency configuration ----- [Project dependencies](metadata.md#dependencies) are defined with [PEP 508][] strings using optional [PEP 440 version specifiers][]. ## Version specifiers A version specifier consists of a series of version clauses, separated by commas. For example: ```toml tab="pyproject.toml" [project] ... dependencies = [ "cryptography", "click>=7, <9, != 8.0.0", "python-dateutil==2.8.*", "numpy~=1.21.4", ] ``` The comma is equivalent to a logical `AND` operator: a candidate version must match all given version clauses in order to match the specifier as a whole. ### Operators | Operators | Function | | :---: | --- | | `~=` | [Compatible release](#compatible-release) | | `==` | [Version matching](#version-matching) | | `!=` | [Version exclusion](#version-exclusion) | | `<=`, `>=` | [Inclusive ordered comparison](#ordered-comparison) | | `<`, `>` | [Exclusive ordered comparison](#ordered-comparison) | | `===` | [Arbitrary equality](#arbitrary-equality) | ### Version matching A version matching clause includes the version matching operator `==` and a version identifier. By default, the version matching operator is based on a strict equality comparison: the specified version must be exactly the same as the requested version. | Clause | Allowed versions | | --- | --- | | `==1` | `1.0.0` | | `==1.2` | `1.2.0` | Prefix matching may be requested instead of strict comparison, by appending a trailing `.*` to the version identifier in the version matching clause. This means that additional trailing segments will be ignored when determining whether or not a version identifier matches the clause. | Clause | Allowed versions | | --- | --- | | `==1.*` | `>=1.0.0, <2.0.0` | | `==1.2.*` | `>=1.2.0, <1.3.0` | ### Compatible release A compatible release clause consists of the compatible release operator `~=` and a version identifier. It matches any candidate version that is expected to be compatible with the specified version. For a given release identifier `V.N`, the compatible release clause is approximately equivalent to the following pair of comparison clauses: ``` >= V.N, == V.* ``` This operator cannot be used with a single segment version number such as `~=1`. | Clause | Allowed versions | | --- | --- | | `~=1.2` | `>=1.2.0, <2.0.0` | | `~=1.2.3` | `>=1.2.3, <1.3.0` | ### Version exclusion A version exclusion clause includes the version exclusion operator `!=` and a version identifier. The allowed version identifiers and comparison semantics are the same as those of the [Version matching](#version-matching) operator, except that the sense of any match is inverted. ### Ordered comparison Inclusive comparisons allow for the version identifier part of clauses whereas exclusive comparisons do not. For example, `>=1.2` allows for version `1.2.0` while `>1.2` does not. Unlike the inclusive ordered comparisons `<=` and `>=`, the exclusive ordered comparisons `<` and `>` specifically exclude pre-releases, post-releases, and local versions of the specified version. ### Arbitrary equality Though heavily discouraged, arbitrary equality comparisons allow for simple string matching without any version semantics, for example `===foobar`. ## Environment markers [Environment markers](https://peps.python.org/pep-0508/#environment-markers) allow for dependencies to only be installed when certain conditions are met. For example, if you need to install the latest version of `cryptography` that is available for a given Python major version you could define the following: ``` cryptography==3.3.2; python_version < "3" cryptography>=35.0; python_version > "3" ``` Alternatively, if you only need it on Python 3 when running on Windows you could do: ``` cryptography; python_version ~= "3.0" and platform_system == "Windows" ``` The available environment markers are as follows. | Marker | Python equivalent | Examples | | --- | --- | --- | | `os_name` | `#!python import os`
`os.name` |
  • posix
  • java
| | `sys_platform` | `#!python import sys`
`sys.platform` |
  • linux
  • win32
  • darwin
| | `platform_machine` | `#!python import platform`
`platform.machine()` |
  • x86_64
| | `platform_python_implementation` | `#!python import platform`
`platform.python_implementation()` |
  • CPython
  • Jython
| | `platform_release` | `#!python import platform`
`platform.release()` |
  • 1.8.0_51
  • 3.14.1-x86_64-linode39
| | `platform_system` | `#!python import platform`
`platform.system()` |
  • Linux
  • Windows
  • Darwin
| | `platform_version` | `#!python import platform`
`platform.version()` |
  • 10.0.19041
  • \#1 SMP Fri Apr 2 22:23:49 UTC 2021
| | `python_version` | `#!python import platform`
`'.'.join(platform.python_version_tuple()[:2])` |
  • 2.7
  • 3.10
| | `python_full_version` | `#!python import platform`
`platform.python_version()` |
  • 2.7.18
  • 3.11.0b1
| | `implementation_name` | `#!python import sys`
`sys.implementation.name` |
  • cpython
| | `implementation_version` | See [here](https://peps.python.org/pep-0508/#environment-markers) |
  • 2.7.18
  • 3.11.0b1
| ## Features You can select groups of [optional dependencies](metadata.md#optional) to install using the [extras](https://peps.python.org/pep-0508/#extras) syntax. For example, if a dependency named `foo` defined the following: ```toml tab="pyproject.toml" [project.optional-dependencies] crypto = [ "PyJWT", "cryptography", ] fastjson = [ "orjson", ] cli = [ "prompt-toolkit", "colorama; platform_system == 'Windows'", ] ``` You can select the `cli` and `crypto` features like so: ``` foo[cli,crypto]==1.* ``` Note that the features come immediately after the package name, before any [version specifiers](#version-specifiers). ### Self-referential Feature groups can self-referentially extend others. For example, for a project called `awesome-project`, the `dev` feature group in the following `pyproject.toml` file would select everything in the `crypto` feature group, plus `black`: ```toml tab="pyproject.toml" [project] name = "awesome-project" [project.optional-dependencies] crypto = [ "PyJWT", "cryptography", ] dev = [ "awesome-project[crypto]", "black", ] ``` ## Direct references Instead of using normal [version specifiers](#version-specifiers) and fetching packages from an index like PyPI, you can define exact sources using [direct references](https://peps.python.org/pep-0440/#direct-references) with an explicit [URI](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax). Direct references are usually not meant to be used for dependencies of a published project but rather are used for defining [dependencies for an environment](environment/overview.md#dependencies). All direct reference types are prefixed by the package name like: ``` @ ``` ### Version control systems Various version control systems (VCS) are [supported](#supported-vcs) as long as the associated executable is available along your `PATH`. VCS direct references are defined using one of the following formats: ``` @ :// @ ://@ ``` You may also append a `#subdirectory=` component for specifying the relative path to the Python package when it is not located at the root e.g. `#subdirectory=lib/foo`. For more information, refer to [this](https://pip.pypa.io/en/stable/topics/vcs-support/). #### Supported VCS === "Git" | Executable | Schemes | Revisions | Example | | --- | --- | --- | --- | | `git` |
  • git+file
  • git+https
  • git+ssh
  • git+http :warning:
  • git+git :warning:
  • git :warning:
|
  • Commit hash
  • Tag name
  • Branch name
| `proj @ git+https://github.com/org/proj.git@v1` | === "Mercurial" | Executable | Schemes | Revisions | Example | | --- | --- | --- | --- | | `hg` |
  • hg+file
  • hg+https
  • hg+ssh
  • hg+http :warning:
  • hg+static-http :warning:
|
  • Revision hash
  • Revision number
  • Tag name
  • Branch name
| `proj @ hg+file:///path/to/proj@v1` | === "Subversion" | Executable | Schemes | Revisions | Example | | --- | --- | --- | --- | | `svn` |
  • svn+https
  • svn+ssh
  • svn+http :warning:
  • svn+svn :warning:
  • svn :warning:
|
  • Revision number
| `proj @ svn+file:///path/to/proj` | === "Bazaar" | Executable | Schemes | Revisions | Example | | --- | --- | --- | --- | | `bzr` |
  • bzr+https
  • bzr+ssh
  • bzr+sftp
  • bzr+lp
  • bzr+http :warning:
  • bzr+ftp :warning:
|
  • Revision number
  • Tag name
| `proj @ bzr+lp:proj@v1` | ### Local You can install local packages with the `file` scheme in the following format: ``` @ file:/// ``` The `` is only used on Windows systems, where it can refer to a network share. If omitted it is assumed to be `localhost` and the third slash must still be present. The `` can refer to a source archive, a wheel, or a directory containing a Python package. | Type | Unix | Windows | | --- | --- | --- | | Source archive | `proj @ file:///path/to/pkg.tar.gz` | `proj @ file:///c:/path/to/pkg.tar.gz` | | Wheel | `proj @ file:///path/to/pkg.whl` | `proj @ file:///c:/path/to/pkg.whl` | | Directory | `proj @ file:///path/to/pkg` | `proj @ file:///c:/path/to/pkg` | !!! tip You may also specify paths relative to your project's root directory on all platforms by using [context formatting](context.md#paths): ``` @ {root:uri}/pkg_inside_project @ {root:parent:uri}/pkg_alongside_project ``` ### Remote You can install source archives and wheels by simply referring to a URL: ``` black @ https://github.com/psf/black/archive/refs/tags/21.10b0.zip pytorch @ https://download.pytorch.org/whl/cu102/torch-1.10.0%2Bcu102-cp39-cp39-linux_x86_64.whl ``` An expected hash value may be specified by appending a `#=` component: ``` requests @ https://github.com/psf/requests/archive/refs/tags/v2.26.0.zip#sha256=eb729a757f01c10546ebd179ae2aec852dd0d7f8ada2328ccf4558909d859985 ``` If the hash differs from the expected hash, the installation will fail. It is recommended that only hashes which are unconditionally provided by the latest version of the standard library's [hashlib module](https://docs.python.org/dev/library/hashlib.html) be used for hashes. As of Python 3.10, that list consists of: - `md5` - `sha1` - `sha224` - `sha256` - `sha384` - `sha512` - `blake2b` - `blake2s` ### Complex syntax The following is an example that uses [features](#features) and [environment markers](#environment-markers): ``` pkg[feature1,feature2] @ ; python_version < "3.7" ``` Note that the space before the semicolon is required. ================================================ FILE: docs/config/environment/advanced.md ================================================ # Advanced environment configuration ----- ## Context formatting All environments support the following extra [context formatting](../context.md) fields: | Field | Description | | --- | --- | | `env_name` | The name of the environment | | `env_type` | The [type](overview.md#type) of environment | | `matrix` | Its modifier selects the value of that matrix variable. If the environment is not part of a matrix or was not generated with the variable, you must specify a default value as an additional modifier e.g. `{matrix:version:v1.0.0}`. | | `verbosity` | The integer verbosity value of Hatch. A `flag` modifier is supported that will render the value as a CLI flag e.g. `-2` becomes `-qq`, `1` becomes `-v`, and `0` becomes an empty string. An additional flag integer modifier may be used to adjust the verbosity level. For example, if you wanted to make a command quiet by default, you could use `{verbosity:flag:-1}` within the command. | | `args` | For [executed commands](../../environment.md#command-execution) only, any extra command line arguments with an optional default modifier if none were provided | ## Matrix Environments can define a series of matrices with the `matrix` option: ```toml config-example [tool.hatch.envs.test] dependencies = [ "pytest" ] [[tool.hatch.envs.test.matrix]] python = ["3.10", "3.11"] version = ["42", "3.14"] [[tool.hatch.envs.test.matrix]] python = ["3.11", "3.12"] version = ["9000"] feature = ["foo", "bar"] ``` Doing so will result in the product of each variable combination being its own environment. ### Naming The name of the generated environments will be the variable values of each combination separated by hyphens, altogether prefixed by `.`. For example, the following configuration: ```toml config-example [[tool.hatch.envs.test.matrix]] version = ["42"] feature = ["foo", "bar"] ``` would indicate the following unique environments: ``` test.42-foo test.42-bar ``` The exceptions to this format are described below. #### Python variables If the variables `py` or `python` are specified, then they will rank first in the product result and will be prefixed by `py` if the value is not. For example, the following configuration: ```toml config-example [[tool.hatch.envs.test.matrix]] version = ["42"] python = ["3.9", "pypy3"] ``` would generate the following environments: ``` test.py3.9-42 test.pypy3-42 ``` !!! note The value of this variable sets the [Python version](overview.md#python-version). #### Name formatting You can set the `matrix-name-format` option to modify how each variable part is formatted which recognizes the placeholders `{variable}` and `{value}`. For example, the following configuration: ```toml config-example [tool.hatch.envs.test] matrix-name-format = "{variable}_{value}" [[tool.hatch.envs.test.matrix]] version = ["42"] feature = ["foo", "bar"] ``` would produce the following environments: ``` test.version_42-feature_foo test.version_42-feature_bar ``` By default this option is set to `{value}`. #### Default environment If the `default` environment defines matrices, then the generated names will not be prefixed by the environment name. This can be useful for projects that only need a single series of matrices without any standalone environments. ### Selection Rather than [selecting](../../environment.md#selection) a single generated environment, you can select the root environment to target all of them. For example, if you have the following configuration: ```toml config-example [tool.hatch.envs.test] dependencies = [ "coverage[toml]", "pytest", "pytest-cov", ] [tool.hatch.envs.test.scripts] cov = 'pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=pkg --cov=tests' [[tool.hatch.envs.test.matrix]] python = ["3.11", "3.12"] version = ["42", "3.14"] ``` you could then run your tests consecutively in all 4 environments with: ``` hatch run test:cov ``` ## Dependency Groups Environments can use dependency groups[^1] using the environment `dependency-groups` array: ```toml config-example [dependency-groups] test = [ "pytest>=7.0.0", "pytest-cov>=4.1.0", ] lint = [ "black", "ruff", "mypy", ] # Groups can include other groups dev = [ {include-group = "test"}, {include-group = "lint"}, "pre-commit", ] [tool.hatch.envs.test] dependency-groups = ["test"] [tool.hatch.envs.lint] dependency-groups = ["lint"] [tool.hatch.envs.dev] dependency-groups = ["dev"] ``` The `dependency-groups` array specifies which dependency groups to include in the environment's dependencies. This is particularly useful for organizing related dependencies and including them in appropriate environments. ### Combining with Other Dependencies Dependency groups can be combined with other dependency mechanisms: ```toml config-example [project] name = "my-app" version = "0.1.0" dependencies = [ "requests>=2.28.0", ] [dependency-groups] test = ["pytest>=7.0.0"] docs = ["sphinx>=7.0.0"] [tool.hatch.envs.test] # Include the test dependency group dependency-groups = ["test"] # Add environment-specific dependencies dependencies = [ "coverage[toml]>=7.0.0", ] ``` In this example, the test environment would include: 1. Project dependencies (`requests>=2.28.0`) 2. The test dependency group (`pytest>=7.0.0`) 3. Environment-specific dependencies (`coverage[toml]>=7.0.0`) ## Option overrides You can modify options based on the conditions of different sources like [matrix variables](#matrix-variable-overrides) with the `overrides` table, using [dotted key](https://toml.io/en/v1.0.0#table) syntax for each declaration: ```toml config-example [tool.hatch.envs..overrides] ..