diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e45a4b331da..9b7a846fbda 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -17,8 +17,8 @@ current development version. To confirm this, you have three options: - create a new virtualenv (make sure it's the same Python version); - clone this repository; - run `pip install -e .[d]`; - - run `pip install -r test_requirements.txt` - - make sure it's sane by running `python -m pytest`; and + - run `pip install --group tests` + - make sure it's sane by running `python -m pytest -n auto`; and - run `black` like you did last time. --> diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3f6641c91a0..f4703ffe62b 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,7 +6,6 @@ blank_issues_enabled: true contact_links: - name: Chat on Python Discord url: https://discord.gg/RtVdv86PrH - about: | - User support, questions, and other lightweight requests can be - handled via the #black-formatter text channel we have on Python - Discord. + about: >- + User support, questions, and other lightweight requests can be handled via the + #black-formatter text channel we have on Python Discord. diff --git a/.github/ISSUE_TEMPLATE/style_issue.md b/.github/ISSUE_TEMPLATE/style_issue.md index a9ce85fd977..ff9283f9ac5 100644 --- a/.github/ISSUE_TEMPLATE/style_issue.md +++ b/.github/ISSUE_TEMPLATE/style_issue.md @@ -18,7 +18,7 @@ how the current _Black_ style is not great: --> ```python def f(): - "Make sure this code is blackened""" + """This code should be formatted as per the current Black style""" pass ``` @@ -29,6 +29,7 @@ def f(): ```python def f( ): + """This code can be formatted however you suggest!""" pass ``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 1500a84a493..82b7d718ef5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,8 +23,7 @@ - Documentation changes are necessary for most formatting changes and other enhancements. --> -- [ ] Implement any code style changes under the `--preview` style, following the - stability policy? +- [ ] Implement any code style changes under the `--preview` style, following the stability policy? - [ ] Add an entry in `CHANGES.md` if necessary? - [ ] Add / update tests if necessary? - [ ] Add new / update outdated documentation? diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 26083c8dea5..7865d4eda3c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,9 +12,9 @@ updates: default-days: 7 - package-ecosystem: "pip" - directory: "docs/" + directory: "/" schedule: interval: "weekly" - labels: ["skip news", "C: dependencies", "T: documentation"] + labels: ["skip news", "C: dependencies"] cooldown: default-days: 7 diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 113643b4bde..dfde3c355f0 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -8,9 +8,7 @@ permissions: contents: read jobs: - build: - name: Changelog Entry Check - + check: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/diff_shades.yml b/.github/workflows/diff_shades.yml index 0683dda8711..a7ab851e6f1 100644 --- a/.github/workflows/diff_shades.yml +++ b/.github/workflows/diff_shades.yml @@ -3,10 +3,20 @@ name: diff-shades on: push: branches: [main] - paths: ["src/**", "pyproject.toml", ".github/workflows/*"] + paths: + - src/** + - pyproject.toml + - scripts/diff_shades_gha_helper.py + - .github/workflows/diff_shades.yml + - .github/workflows/diff_shades_comment.yml pull_request: - paths: ["src/**", "pyproject.toml", ".github/workflows/*"] + paths: + - src/** + - pyproject.toml + - scripts/diff_shades_gha_helper.py + - .github/workflows/diff_shades.yml + - .github/workflows/diff_shades_comment.yml concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} @@ -34,17 +44,14 @@ jobs: - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - - - name: Install diff-shades and support dependencies - run: | - python -m pip install 'click>=8.1.7' packaging urllib3 - python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip + pip-version: "25.3" + pip-install: --group diff-shades --group diff-shades-comment - name: Calculate run configuration & metadata id: set-config env: GITHUB_TOKEN: ${{ github.token }} - run: python scripts/diff_shades_gha_helper.py config ${{ github.event_name }} + run: python scripts/diff_shades_gha_helper.py config analysis-base: name: analysis / base / ${{ matrix.mode }} @@ -66,10 +73,11 @@ jobs: - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" + pip-version: "25.3" + pip-install: --group diff-shades - - name: Install diff-shades + - name: Configure git run: | - python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip git config user.name "diff-shades-gha" git config user.email "diff-shades-gha@example.com" @@ -84,15 +92,15 @@ jobs: if: steps.baseline-cache.outputs.cache-hit != 'true' env: GITHUB_TOKEN: ${{ github.token }} - run: > + run: | ${{ matrix.baseline-setup-cmd }} - && python -m pip install . + python -m pip install . - name: Analyze baseline revision if: steps.baseline-cache.outputs.cache-hit != 'true' - run: > - diff-shades analyze ${{ matrix.baseline-analysis }} - -v --work-dir projects-cache/ ${{ matrix.force-flag }} + run: + diff-shades analyze ${{ matrix.baseline-analysis }} --work-dir projects-cache/ + --force-${{ matrix.style }}-style -v - name: Upload baseline analysis uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 @@ -120,19 +128,20 @@ jobs: - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" + pip-version: "25.3" + pip-install: --group diff-shades - - name: Install diff-shades + - name: Configure git run: | - python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip git config user.name "diff-shades-gha" git config user.email "diff-shades-gha@example.com" - name: Build and install target revision env: GITHUB_TOKEN: ${{ github.token }} - run: > + run: | ${{ matrix.target-setup-cmd }} - && python -m pip install . + python -m pip install . # Pull it from previous runs - we're NOT trying to get it from this run # (but it wouldn't cause problems if we theoretically did) @@ -145,16 +154,16 @@ jobs: - name: Analyze target revision (with repeated projects) if: steps.baseline-cache.outputs.cache-hit == 'true' - run: > - diff-shades analyze ${{ matrix.target-analysis }} - -v --work-dir projects-cache/ ${{ matrix.force-flag }} + run: | + diff-shades analyze ${{ matrix.target-analysis }} --work-dir projects-cache/ \ + --force-${{ matrix.style }}-style -v \ --repeat-projects-from ${{ matrix.baseline-analysis }} - name: Analyze target revision (without repeated projects) if: steps.baseline-cache.outputs.cache-hit != 'true' - run: > - diff-shades analyze ${{ matrix.target-analysis }} - -v --work-dir projects-cache/ ${{ matrix.force-flag }} + run: + diff-shades analyze ${{ matrix.target-analysis }} --work-dir projects-cache/ + --force-${{ matrix.style }}-style -v - name: Upload target analysis uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 @@ -163,8 +172,7 @@ jobs: path: ${{ matrix.target-analysis }} - name: Check for failed files for target revision - run: > - diff-shades show-failed --check --show-log ${{ matrix.target-analysis }} + run: diff-shades show-failed --check --show-log ${{ matrix.target-analysis }} compare: name: compare / ${{ matrix.mode }} @@ -188,41 +196,41 @@ jobs: - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - - - name: Install diff-shades and support dependencies - run: | - python -m pip install 'click>=8.1.7' packaging urllib3 - python -m pip install https://github.com/ichard26/diff-shades/archive/stable.zip + pip-version: "25.3" + pip-install: --group diff-shades --group diff-shades-comment - name: Generate HTML diff report - run: > - diff-shades --dump-html diff.html compare --diff - ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} + run: | + diff-shades --dump-html diff.html \ + compare --diff ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} - name: Upload diff report uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: - name: ${{ matrix.mode }}-diff.html + name: ${{ matrix.style }}-diff.html path: diff.html - name: Generate summary file (PR only) - if: github.event_name == 'pull_request' && matrix.mode == 'preview-new-changes' - run: > - python scripts/diff_shades_gha_helper.py comment-body - ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} - ${{ matrix.baseline-sha }} ${{ matrix.target-sha }} - ${{ github.event.pull_request.number }} + if: github.event_name == 'pull_request' + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + python scripts/diff_shades_gha_helper.py comment-body \ + ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} \ + ${{ matrix.style }} ${{ matrix.mode }} - name: Upload summary file (PR only) - if: github.event_name == 'pull_request' && matrix.mode == 'preview-new-changes' + if: github.event_name == 'pull_request' uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: - name: .pr-comment.json - path: .pr-comment.json + name: .${{ matrix.style }}.pr-comment.md + path: .${{ matrix.style }}.pr-comment.md include-hidden-files: true - name: Verify zero changes (PR only) if: matrix.mode == 'assert-no-changes' - run: > - diff-shades compare --check ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} - || (echo "Please verify you didn't change the stable code style unintentionally!" && exit 1) + run: | + diff-shades compare --check \ + ${{ matrix.baseline-analysis }} ${{ matrix.target-analysis }} || \ + (echo "Please verify you didn't change the stable code style unintentionally!" \ + && exit 1) diff --git a/.github/workflows/diff_shades_comment.yml b/.github/workflows/diff_shades_comment.yml index 30de91112bd..21ada8dddd9 100644 --- a/.github/workflows/diff_shades_comment.yml +++ b/.github/workflows/diff_shades_comment.yml @@ -1,4 +1,4 @@ -name: diff-shades-comment +name: diff-shades comment on: workflow_run: @@ -10,43 +10,63 @@ permissions: {} jobs: comment: runs-on: ubuntu-latest + # We want to comment even if there were failed files or the stable style changed + # That would cause the main workflow to "fail" + if: + github.event.workflow_run.event == 'pull_request' && + contains(fromJSON('["success", "failure"]'), github.event.workflow_run.conclusion) permissions: pull-requests: write steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false + + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + id: artifacts + with: + merge-multiple: true + pattern: ".*.pr-comment.md" + github-token: ${{ github.token }} + run-id: ${{ github.event.workflow_run.id }} + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" + pip-version: "25.3" + pip-install: --group diff-shades-comment - - name: Install support dependencies - run: | - python -m pip install pip --upgrade - python -m pip install click packaging urllib3 + - name: Get PR number + id: pr + run: + echo pr=$(gh pr list --search $sha --json number --jq ".[0].number") >> + "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ github.token }} + sha: ${{ github.event.workflow_run.head_sha }} - name: Get details from initial workflow run id: metadata + run: | + python scripts/diff_shades_gha_helper.py comment-details \ + $pr $run_id $(echo .*.pr-comment.md) env: GITHUB_TOKEN: ${{ github.token }} - run: > - python scripts/diff_shades_gha_helper.py comment-details - ${{github.event.workflow_run.id }} + pr: ${{ steps.pr.outputs.pr }} + run_id: ${{ github.event.workflow_run.id }} - name: Try to find pre-existing PR comment - if: steps.metadata.outputs.needs-comment == 'true' id: find-comment uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad with: - issue-number: ${{ steps.metadata.outputs.pr-number }} + issue-number: ${{ steps.pr.outputs.pr }} comment-author: "github-actions[bot]" body-includes: "diff-shades" - name: Create or update PR comment - if: steps.metadata.outputs.needs-comment == 'true' uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 with: comment-id: ${{ steps.find-comment.outputs.comment-id }} - issue-number: ${{ steps.metadata.outputs.pr-number }} + issue-number: ${{ steps.pr.outputs.pr }} body: ${{ steps.metadata.outputs.comment-body }} edit-mode: replace diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 1279cba8a7e..c35b33a2e31 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -10,19 +10,27 @@ on: permissions: contents: read +env: + REGISTRY: pyfound/black + jobs: - docker: + build: if: github.repository == 'psf/black' - runs-on: ubuntu-latest + runs-on: ${{ matrix.runner }} + name: build (${{ matrix.platform }}) + strategy: + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm steps: - name: Checkout uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - name: Set up QEMU - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 @@ -32,42 +40,84 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Check + set version tag - run: - echo "GIT_TAG=$(git describe --candidates=0 --tags 2> /dev/null || echo - latest_non_release)" >> $GITHUB_ENV + - name: Prepare + id: prepare + run: echo "platform=${platform//\//-}" >> $GITHUB_OUTPUT + env: + platform: ${{ matrix.platform }} - name: Build and push + id: build uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 with: context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: pyfound/black:latest,pyfound/black:${{ env.GIT_TAG }} - - - name: Build and push latest_release tag - if: - ${{ github.event_name == 'release' && github.event.action == 'published' && - !github.event.release.prerelease }} - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + platforms: ${{ matrix.platform }} + outputs: type=image,push-by-digest=true,name-canonical=true,push=true + tags: ${{ env.REGISTRY }} + cache-from: type=gha,scope=${{ steps.prepare.outputs.platform }} + cache-to: type=gha,scope=${{ steps.prepare.outputs.platform }},mode=max + + - name: Export digest + run: | + mkdir -p digests + touch "digests/${digest#sha256:}" + env: + digest: ${{ steps.build.outputs.digest }} + + - name: Upload digest + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: pyfound/black:latest_release - - - name: Build and push latest_prerelease tag - if: - ${{ github.event_name == 'release' && github.event.action == 'published' && - github.event.release.prerelease }} - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + name: digests-${{ steps.prepare.outputs.platform }} + path: digests/* + if-no-files-found: error + + push: + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: pyfound/black:latest_prerelease + persist-credentials: false - - name: Image digest - run: echo ${STEPS_DOCKER_BUILD_OUTPUTS_DIGEST} + - name: Download digests + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + path: digests + pattern: digests-* + merge-multiple: true + + - name: Login to DockerHub + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + + - name: Create manifest list and push + run: | + TAGS="-t $REGISTRY:latest" + + if [[ "$EVENT_NAME" == "release" ]]; then + TAGS="$TAGS -t $REGISTRY:$(git describe --candidates=0 --tags)" + + if [[ "$PRERELEASE" == "true" ]]; then + TAGS="$TAGS -t $REGISTRY:latest_prerelease" + else + TAGS="$TAGS -t $REGISTRY:latest_release" + fi + else + TAGS="$TAGS -t $REGISTRY:latest_non_release" + fi + + cd digests + docker buildx imagetools create $TAGS $(printf "$REGISTRY@sha256:%s " *) env: - STEPS_DOCKER_BUILD_OUTPUTS_DIGEST: ${{ steps.docker_build.outputs.digest }} + EVENT_NAME: ${{ github.event_name }} + PRERELEASE: ${{ github.event.release.prerelease }} + + - name: Inspect image + run: docker buildx imagetools inspect $REGISTRY:latest diff --git a/.github/workflows/doc.yml b/.github/workflows/docs.yml similarity index 70% rename from .github/workflows/doc.yml rename to .github/workflows/docs.yml index 1aa154fdbc5..0c7b46d380c 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/docs.yml @@ -1,17 +1,17 @@ -name: Documentation +name: docs on: push: - paths: ["docs/**", "pyproject.toml", ".github/workflows/*"] + paths: ["docs/**", "src/**", "pyproject.toml", ".github/workflows/docs.yml"] pull_request: - paths: ["docs/**", "pyproject.toml", ".github/workflows/*"] + paths: ["docs/**", "src/**", "pyproject.toml", ".github/workflows/docs.yml"] permissions: contents: read jobs: - build: + docs: # We want to run on external PRs, but not on our own internal PRs as they'll be run # by the push to the branch. Without this if check, checks are duplicated since # internal PRs match both the push and pull_request events. @@ -34,14 +34,8 @@ jobs: uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - allow-prereleases: true - - - name: Install dependencies - run: | - python -m pip install uv - python -m uv venv - python -m uv pip install -e ".[d]" - python -m uv pip install -r "docs/requirements.txt" + pip-version: "25.3" + pip-install: -e .[d] --group docs - name: Build documentation run: sphinx-build -a -b html -W --keep-going docs/ docs/_build diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 941507175f2..da0325d7b9f 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -1,15 +1,23 @@ -name: Fuzz +name: fuzz on: push: - paths-ignore: - - "docs/**" - - "*.md" + paths: + - tox.ini + - .github/workflows/fuzz.yml + - scripts/fuzz.py + - src/** + - tests/** + - pyproject.toml pull_request: - paths-ignore: - - "docs/**" - - "*.md" + paths: + - tox.ini + - .github/workflows/fuzz.yml + - scripts/fuzz.py + - src/** + - tests/** + - pyproject.toml concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} @@ -19,7 +27,7 @@ permissions: contents: read jobs: - build: + fuzz: # We want to run on external PRs, but not on our own internal PRs as they'll be run # by the push to the branch. Without this if check, checks are duplicated since # internal PRs match both the push and pull_request events. @@ -43,12 +51,8 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade tox + pip-version: "25.3" + pip-install: --group tox - name: Run fuzz tests - run: | - tox -e fuzz + run: tox -e fuzz diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d5a9e99f6be..085905bbd6d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,11 +1,11 @@ -name: Lint + format ourselves +name: lint and format on: [push, pull_request] permissions: {} jobs: - build: + lint: # We want to run on external PRs, but not on our own internal PRs as they'll be run # by the push to the branch. Without this if check, checks are duplicated since # internal PRs match both the push and pull_request events. @@ -31,20 +31,14 @@ jobs: uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - allow-prereleases: true - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e '.' - python -m pip install tox + pip-version: "25.3" + pip-install: -e . --group tox - name: Run pre-commit hooks uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - name: Format ourselves - run: | - tox -e run_self + run: tox -e run_self - name: Regenerate schema run: | diff --git a/.github/workflows/upload_binary.yml b/.github/workflows/publish_binaries.yml similarity index 80% rename from .github/workflows/upload_binary.yml rename to .github/workflows/publish_binaries.yml index 7c6a6d3d958..adb07473436 100644 --- a/.github/workflows/upload_binary.yml +++ b/.github/workflows/publish_binaries.yml @@ -1,4 +1,4 @@ -name: Publish executables +name: publish binaries on: release: @@ -8,14 +8,14 @@ permissions: contents: write # actions/upload-release-asset needs this. jobs: - build: + publish: + name: publish (${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [windows-2025, windows-11-arm, ubuntu-22.04, ubuntu-22.04-arm, macos-latest] include: - - os: windows-2025 + - os: windows-latest pathsep: ";" asset_name: black_windows.exe executable_mime: "application/vnd.microsoft.portable-executable" @@ -23,11 +23,11 @@ jobs: pathsep: ";" asset_name: black_windows-arm.exe executable_mime: "application/vnd.microsoft.portable-executable" - - os: ubuntu-22.04 + - os: ubuntu-latest pathsep: ":" asset_name: black_linux executable_mime: "application/x-executable" - - os: ubuntu-22.04-arm + - os: ubuntu-24.04-arm pathsep: ":" asset_name: black_linux-arm executable_mime: "application/x-executable" @@ -45,15 +45,11 @@ jobs: uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - - - name: Install Black and PyInstaller - run: | - python -m pip install --upgrade pip wheel - python -m pip install .[colorama] - python -m pip install pyinstaller + pip-version: "25.3" + pip-install: .[colorama] --group binary - name: Build executable with PyInstaller - run: > + run: python -m PyInstaller -F --name ${{ matrix.asset_name }} --add-data 'src/blib2to3${{ matrix.pathsep }}blib2to3' src/black/__main__.py diff --git a/.github/workflows/pypi_upload.yml b/.github/workflows/pypi_upload.yml index 94f4ad2b709..93ce811bfa0 100644 --- a/.github/workflows/pypi_upload.yml +++ b/.github/workflows/pypi_upload.yml @@ -1,4 +1,4 @@ -name: Build and publish +name: build and publish on: release: @@ -10,18 +10,15 @@ on: permissions: {} +env: + PROJECT: https://pypi.org/p/black + jobs: - main: - name: sdist + pure wheel + configure: + name: generate wheels matrix runs-on: ubuntu-latest - if: github.event_name == 'release' - environment: - name: release - url: https://pypi.org/p/black - - permissions: - id-token: write # Required for PyPI trusted publishing - + outputs: + include: ${{ steps.set-matrix.outputs.include }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: @@ -31,36 +28,9 @@ jobs: uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - allow-prereleases: true - - - name: Install latest pip, build - run: | - python -m pip install --upgrade --disable-pip-version-check pip - python -m pip install --upgrade build - - - name: Build wheel and source distributions - run: python -m build - - - if: github.event_name == 'release' - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 - with: - verbose: true + pip-version: "25.3" + pip-install: --group wheels - generate_wheels_matrix: - name: generate wheels matrix - runs-on: ubuntu-latest - outputs: - include: ${{ steps.set-matrix.outputs.include }} - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - # Keep cibuildwheel version in sync with below - - name: Install cibuildwheel and pypyp - run: | - pipx install cibuildwheel==3.2.1 - pipx install pypyp==1.3.0 - name: generate matrix if: | github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci: build all wheels') @@ -79,6 +49,7 @@ jobs: CIBW_ARCHS_LINUX: x86_64 CIBW_ARCHS_MACOS: x86_64 arm64 CIBW_ARCHS_WINDOWS: AMD64 + - name: generate matrix (PR) if: | github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'ci: build all wheels') @@ -89,29 +60,24 @@ jobs: } | pyp 'json.dumps(list(map(json.loads, lines)))' > /tmp/matrix env: CIBW_ARCHS_LINUX: x86_64 + - id: set-matrix run: echo "include=$(cat /tmp/matrix)" | tee -a $GITHUB_OUTPUT mypyc: name: mypyc wheels ${{ matrix.only }} - needs: generate_wheels_matrix + needs: configure runs-on: ${{ matrix.os }} - if: github.event_name == 'release' - environment: - name: release - url: https://pypi.org/p/black - permissions: - id-token: write # Required for PyPI trusted publishing strategy: - fail-fast: false matrix: - include: ${{ fromJson(needs.generate_wheels_matrix.outputs.include) }} + include: ${{ fromJson(needs.configure.outputs.include) }} steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - # Keep cibuildwheel version in sync with above + + # Keep cibuildwheel version in sync with pyproject.toml - uses: pypa/cibuildwheel@63fd63b352a9a8bdcc24791c9dbee952ee9a8abc # v3.3.0 with: only: ${{ matrix.only }} @@ -120,20 +86,80 @@ jobs: uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: ${{ matrix.only }}-mypyc-wheels - path: ./wheelhouse/*.whl + path: wheelhouse/ + + hatch: + name: sdist + pure wheel + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Set up latest Python + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: "3.13" + pip-version: "25.3" + pip-install: --group build + + - name: Build wheel and source distributions + run: python -m hatch build + + - name: Store the distribution packages + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: sdist-and-pure-wheel + path: dist/ - - if: github.event_name == 'release' - name: Publish package distributions to PyPI + publish-mypyc: + if: github.event_name == 'release' + needs: mypyc + runs-on: ubuntu-latest + environment: + name: release + url: ${{ env.PROJECT }} + permissions: + id-token: write # Required for PyPI trusted publishing + steps: + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + pattern: "*-mypyc-wheels" + path: wheelhouse/ + merge-multiple: true + + - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: packages-dir: wheelhouse/ verbose: true - update-stable-branch: - name: Update stable branch - needs: [main, mypyc] - runs-on: ubuntu-latest + publish-hatch: if: github.event_name == 'release' + needs: hatch + runs-on: ubuntu-latest + environment: + name: release + url: ${{ env.PROJECT }} + permissions: + id-token: write # Required for PyPI trusted publishing + steps: + - uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + name: sdist-and-pure-wheel + path: dist/ + + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + with: + verbose: true + + update-stable: + name: update stable branch + needs: [publish-mypyc, publish-hatch] + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.release.prerelease != 'true' permissions: contents: write @@ -145,8 +171,7 @@ jobs: fetch-depth: 0 persist-credentials: true # needed for `git push` below - - if: github.event_name == 'release' - name: Update stable branch to release tag & push + - name: Update stable branch to release tag & push run: | git reset --hard "${TAG_NAME}" git push diff --git a/.github/workflows/release_tests.yml b/.github/workflows/release_tests.yml index cf095804529..84c890c0871 100644 --- a/.github/workflows/release_tests.yml +++ b/.github/workflows/release_tests.yml @@ -1,21 +1,21 @@ -name: Release tool CI +name: test release tool on: push: paths: - .github/workflows/release_tests.yml - - release.py - - release_tests.py + - scripts/release.py + - scripts/release_tests.py pull_request: paths: - .github/workflows/release_tests.yml - - release.py - - release_tests.py + - scripts/release.py + - scripts/release_tests.py permissions: {} jobs: - build: + test-release-tool: # We want to run on external PRs, but not on our own internal PRs as they'll be run # by the push to the branch. Without this if check, checks are duplicated since # internal PRs match both the push and pull_request events. @@ -23,7 +23,6 @@ jobs: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository - name: Running python ${{ matrix.python-version }} on ${{matrix.os}} runs-on: ${{ matrix.os }} strategy: matrix: @@ -36,24 +35,25 @@ jobs: # Give us all history, branches and tags fetch-depth: 0 persist-credentials: false + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true + pip-version: "25.3" + pip-install: --group coverage - name: Print Python Version run: python --version --version && which python + - name: Print Pip Version + run: pip --version && which pip + - name: Print Git Version run: git --version && which git - - name: Update pip, setuptools + wheels - run: | - python -m pip install --upgrade pip setuptools wheel - - name: Run unit tests via coverage + print report run: | - python -m pip install coverage coverage run scripts/release_tests.py coverage report --show-missing diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 129cee7f034..153544f57c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,15 +1,21 @@ -name: Test +name: test on: push: - paths-ignore: - - "docs/**" - - "*.md" + paths: + - .github/workflows/test.yml + - src/** + - tests/** + - tox.ini + - pyproject.toml pull_request: - paths-ignore: - - "docs/**" - - "*.md" + paths: + - .github/workflows/test.yml + - src/** + - tests/** + - tox.ini + - pyproject.toml permissions: contents: read @@ -19,7 +25,7 @@ concurrency: cancel-in-progress: true jobs: - main: + test: # We want to run on external PRs, but not on our own internal PRs as they'll be run # by the push to the branch. Without this if check, checks are duplicated since # internal PRs match both the push and pull_request events. @@ -50,11 +56,8 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: true - - - name: Install tox - run: | - python -m pip install --upgrade pip - python -m pip install --upgrade tox + pip-version: "25.3" + pip-install: --group tox - name: Unit tests if: "!startsWith(matrix.python-version, 'pypy')" @@ -79,7 +82,7 @@ jobs: debug: true coveralls-finish: - needs: main + needs: test if: github.repository == 'psf/black' runs-on: ubuntu-latest @@ -87,6 +90,7 @@ jobs: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false + - name: Send finished signal to Coveralls uses: AndreMiras/coveralls-python-action@ac868b9540fad490f7ca82b8ca00480fd751ed19 with: @@ -112,11 +116,8 @@ jobs: uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.13" - - - name: Install black with uvloop - run: | - python -m pip install pip --upgrade --disable-pip-version-check - python -m pip install -e ".[uvloop]" + pip-version: "25.3" + pip-install: -e .[uvloop] - name: Format ourselves - run: python -m black --check src/ tests/ + run: python -m black --check . diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 510e956d21c..1aeca15f6a9 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -1,16 +1,17 @@ -name: GitHub Actions Security Analysis with zizmor 🌈 +name: zizmor on: push: branches: ["main"] + paths: ["**.yml"] pull_request: - branches: ["**"] + paths: ["**.yml"] permissions: {} jobs: zizmor: - name: Run zizmor 🌈 + name: zizmor runs-on: ubuntu-latest permissions: security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files. @@ -20,5 +21,5 @@ jobs: with: persist-credentials: false - - name: Run zizmor 🌈 + - name: Run zizmor uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ea73459992..83a5e01a703 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,18 +10,18 @@ repos: entry: python -m scripts.check_pre_commit_rev_in_example files: '(CHANGES\.md|source_version_control\.md)$' additional_dependencies: - &version_check_dependencies [ - commonmark==0.9.1, - pyyaml==6.0.1, - beautifulsoup4==4.9.3, - ] + - beautifulsoup4>=4.14.2 + - commonmark==0.9.1 + - pyyaml==6.0.1 - id: check-version-in-the-basics-example name: Check black version in the basics example language: python entry: python -m scripts.check_version_in_basics_example files: '(CHANGES\.md|the_basics\.md)$' - additional_dependencies: *version_check_dependencies + additional_dependencies: + - beautifulsoup4>=4.14.2 + - commonmark==0.9.1 - repo: https://github.com/pycqa/isort rev: 6.1.0 @@ -42,37 +42,41 @@ repos: rev: v1.19.0 hooks: - id: mypy - exclude: ^(docs/conf.py|scripts/generate_schema.py)$ + exclude: ^docs/conf.py$ args: [] - additional_dependencies: &mypy_deps - - types-PyYAML - - types-atheris - - tomli >= 0.2.6, < 2.0.0 - - click >= 8.2.0 + additional_dependencies: # Click is intentionally out-of-sync with pyproject.toml # v8.2 has breaking changes. We work around them at runtime, but we need the newer stubs. - - packaging >= 22.0 - - platformdirs >= 2.1.0 - - pytokens >= 0.3.0 - - pytest + - click>=8.2.0 + - packaging>=22.0 + - platformdirs>=2 + - pytokens>=0.3.0 + - tomli>=1.1.0,<2.0.0 + + # blackd + - aiohttp>=3.10 + + # tests + - pytest>=7 + + # fuzz - hypothesis - - aiohttp >= 3.7.4 - - types-commonmark - - beautifulsoup4 - - urllib3 - hypothesmith - - id: mypy - name: mypy (Python 3.10) - files: scripts/generate_schema.py - args: ["--python-version=3.10"] - additional_dependencies: *mypy_deps + - types-atheris + + # diff-shades + - urllib3 + + # version check + - beautifulsoup4>=4.14.2 + - types-commonmark + - types-pyyaml - repo: https://github.com/rbubley/mirrors-prettier rev: v3.6.2 hooks: - id: prettier types_or: [markdown, yaml, json] - exclude: \.github/workflows/diff_shades\.yml - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 @@ -81,4 +85,4 @@ repos: - id: trailing-whitespace ci: - autoupdate_schedule: quarterly + autoupdate_schedule: weekly diff --git a/.prettierrc.yaml b/.prettierrc.yaml index beda5ba4da8..fe1f7e09292 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -1,3 +1,9 @@ proseWrap: always printWidth: 88 endOfLine: auto +overrides: + - files: + - ".github/ISSUE_TEMPLATE/*.md" + - ".github/PULL_REQUEST_TEMPLATE.md" + options: + proseWrap: never diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 5e25991c4a7..231cc84ccfa 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,18 +4,14 @@ formats: - htmlzip build: - os: ubuntu-22.04 + os: ubuntu-lts-latest tools: - python: "3.11" - -python: - install: - - requirements: docs/requirements.txt - - - method: pip - path: . - extra_requirements: - - d + python: "3.13" + jobs: + install: + - pip install --upgrade pip + - pip install .[d] + - pip install --group docs sphinx: configuration: docs/conf.py diff --git a/AUTHORS.md b/AUTHORS.md index fa1ce8f7112..c18e305fc5c 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -142,6 +142,7 @@ Multiple contributions by: - [Pablo Galindo](mailto:Pablogsal@gmail.com) - [Paul Ganssle](mailto:p.ganssle@gmail.com) - [Paul Meinhardt](mailto:mnhrdt@gmail.com) +- [Paul S. Reid](mailto:paul@reid-family.org) - [Peter Bengtsson](mailto:mail@peterbe.com) - [Peter Grayson](mailto:pete@jpgrayson.net) - [Peter Stensmyr](mailto:peter.stensmyr@gmail.com) diff --git a/Dockerfile b/Dockerfile index e804c5b6237..18e6321ec2b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-slim AS builder +FROM python:3.13-slim AS builder RUN mkdir /src COPY . /src/ @@ -6,14 +6,17 @@ ENV VIRTUAL_ENV=/opt/venv ENV HATCH_BUILD_HOOKS_ENABLE=1 # Install build tools to compile black + dependencies RUN apt update && apt install -y build-essential git python3-dev + +ENV PATH="$VIRTUAL_ENV/bin:$PATH" RUN python -m venv $VIRTUAL_ENV -RUN python -m pip install --no-cache-dir hatch==1.15.1 hatch-fancy-pypi-readme hatch-vcs -RUN . /opt/venv/bin/activate && pip install --no-cache-dir --upgrade pip setuptools \ - && cd /src && hatch build -t wheel \ +RUN cd /src \ + && pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir --group build \ + && hatch build -t wheel \ && pip install --no-cache-dir dist/*-cp* \ && pip install black[colorama,d,uvloop] -FROM python:3.12-slim +FROM python:3.13-slim # copy only Python packages to limit the image size COPY --from=builder /opt/venv /opt/venv diff --git a/autoload/black.vim b/autoload/black.vim index 76f2bf30f48..ad94c7e65c0 100644 --- a/autoload/black.vim +++ b/autoload/black.vim @@ -4,233 +4,280 @@ import os import sys import vim + def strtobool(text): - if text.lower() in ['y', 'yes', 't', 'true', 'on', '1']: - return True - if text.lower() in ['n', 'no', 'f', 'false', 'off', '0']: - return False - raise ValueError(f"{text} is not convertible to boolean") + if text.lower() in ["y", "yes", "t", "true", "on", "1"]: + return True + if text.lower() in ["n", "no", "f", "false", "off", "0"]: + return False + raise ValueError(f"{text} is not convertible to boolean") + class Flag(collections.namedtuple("FlagBase", "name, cast")): - @property - def var_name(self): - return self.name.replace("-", "_") + @property + def var_name(self): + return self.name.replace("-", "_") - @property - def vim_rc_name(self): - name = self.var_name - if name == "line_length": - name = name.replace("_", "") - return "g:black_" + name + @property + def vim_rc_name(self): + name = self.var_name + if name == "line_length": + name = name.replace("_", "") + return "g:black_" + name FLAGS = [ - Flag(name="line_length", cast=int), - Flag(name="fast", cast=strtobool), - Flag(name="skip_string_normalization", cast=strtobool), - Flag(name="quiet", cast=strtobool), - Flag(name="skip_magic_trailing_comma", cast=strtobool), - Flag(name="preview", cast=strtobool), + Flag(name="line_length", cast=int), + Flag(name="fast", cast=strtobool), + Flag(name="skip_string_normalization", cast=strtobool), + Flag(name="quiet", cast=strtobool), + Flag(name="skip_magic_trailing_comma", cast=strtobool), + Flag(name="preview", cast=strtobool), ] def _get_python_binary(exec_prefix, pyver): - try: - default = vim.eval("g:pymode_python").strip() - except vim.error: - default = "" - if default and os.path.exists(default): - return default - if sys.platform[:3] == "win": - return exec_prefix / 'python.exe' - bin_path = exec_prefix / "bin" - exec_path = (bin_path / f"python{pyver[0]}.{pyver[1]}").resolve() - if exec_path.exists(): - return exec_path - # It is possible that some environments may only have python3 - exec_path = (bin_path / f"python3").resolve() - if exec_path.exists(): - return exec_path - raise ValueError("python executable not found") + try: + default = vim.eval("g:pymode_python").strip() + except vim.error: + default = "" + if default and os.path.exists(default): + return default + if sys.platform[:3] == "win": + return exec_prefix / "python.exe" + bin_path = exec_prefix / "bin" + exec_path = (bin_path / f"python{pyver[0]}.{pyver[1]}").resolve() + if exec_path.exists(): + return exec_path + # It is possible that some environments may only have python3 + exec_path = (bin_path / "python3").resolve() + if exec_path.exists(): + return exec_path + raise ValueError("python executable not found") + def _get_pip(venv_path): - if sys.platform[:3] == "win": - return venv_path / 'Scripts' / 'pip.exe' - return venv_path / 'bin' / 'pip' + if sys.platform[:3] == "win": + return venv_path / "Scripts" / "pip.exe" + return venv_path / "bin" / "pip" + def _get_virtualenv_site_packages(venv_path, pyver): - if sys.platform[:3] == "win": - return venv_path / 'Lib' / 'site-packages' - if venv_path.exists() and not (venv_path / 'lib' / f'python{pyver[0]}.{pyver[1]}').exists(): - # The virtualenv already exists but it doesn't seem to have the expected - # Python version, so we disregard the requested `pyver` and - # discover the real Python interpreter from this virtualenv - import subprocess - result = subprocess.run([_get_python_binary(venv_path, pyver), "--version"], stdout=subprocess.PIPE, text=True) - venv_version = result.stdout.split(" ")[1].strip().split(".") - return venv_path / 'lib' / f'python{venv_version[0]}.{venv_version[1]}' / 'site-packages' - else: - return venv_path / 'lib' / f'python{pyver[0]}.{pyver[1]}' / 'site-packages' + if sys.platform[:3] == "win": + return venv_path / "Lib" / "site-packages" + if ( + venv_path.exists() + and not (venv_path / "lib" / f"python{pyver[0]}.{pyver[1]}").exists() + ): + # The virtualenv already exists but it doesn't seem to have the expected + # Python version, so we disregard the requested `pyver` and + # discover the real Python interpreter from this virtualenv + import subprocess + + result = subprocess.run( + [_get_python_binary(venv_path, pyver), "--version"], + stdout=subprocess.PIPE, + text=True, + ) + venv_version = result.stdout.split(" ")[1].strip().split(".") + return ( + venv_path + / "lib" + / f"python{venv_version[0]}.{venv_version[1]}" + / "site-packages" + ) + else: + return venv_path / "lib" / f"python{pyver[0]}.{pyver[1]}" / "site-packages" + def _initialize_black_env(upgrade=False): - if vim.eval("g:black_use_virtualenv ? 'true' : 'false'") == "false": + if vim.eval("g:black_use_virtualenv ? 'true' : 'false'") == "false": + if upgrade: + print("Upgrade disabled due to g:black_use_virtualenv being disabled.") + print( + "Either use your system package manager (or pip) to upgrade black" + " separately," + ) + print("or modify your vimrc to have 'let g:black_use_virtualenv = 1'.") + return False + else: + # Nothing needed to be done. + return True + + pyver = sys.version_info[:3] + if pyver < (3, 10): + print("Sorry, Black requires Python 3.10+ to run.") + return False + + from pathlib import Path + import subprocess + import venv + + virtualenv_path = Path(vim.eval("g:black_virtualenv")).expanduser() + virtualenv_site_packages = str( + _get_virtualenv_site_packages(virtualenv_path, pyver) + ) + first_install = False + if not virtualenv_path.is_dir(): + print("Please wait, one time setup for Black.") + _executable = sys.executable + _base_executable = getattr(sys, "_base_executable", _executable) + try: + executable = str(_get_python_binary(Path(sys.exec_prefix), pyver)) + sys.executable = executable + sys._base_executable = executable + print(f"Creating a virtualenv in {virtualenv_path}...") + print( + "(this path can be customized in .vimrc by setting g:black_virtualenv)" + ) + venv.create(virtualenv_path, with_pip=True) + except Exception: + print( + "Encountered exception while creating virtualenv (see traceback below)." + ) + print(f"Removing {virtualenv_path}...") + import shutil + + shutil.rmtree(virtualenv_path) + raise + finally: + sys.executable = _executable + sys._base_executable = _base_executable + first_install = True + if first_install: + print("Installing Black with pip...") if upgrade: - print("Upgrade disabled due to g:black_use_virtualenv being disabled.") - print("Either use your system package manager (or pip) to upgrade black separately,") - print("or modify your vimrc to have 'let g:black_use_virtualenv = 1'.") - return False - else: - # Nothing needed to be done. - return True - - pyver = sys.version_info[:3] - if pyver < (3, 10): - print("Sorry, Black requires Python 3.10+ to run.") - return False - - from pathlib import Path - import subprocess - import venv - virtualenv_path = Path(vim.eval("g:black_virtualenv")).expanduser() - virtualenv_site_packages = str(_get_virtualenv_site_packages(virtualenv_path, pyver)) - first_install = False - if not virtualenv_path.is_dir(): - print('Please wait, one time setup for Black.') - _executable = sys.executable - _base_executable = getattr(sys, "_base_executable", _executable) - try: - executable = str(_get_python_binary(Path(sys.exec_prefix), pyver)) - sys.executable = executable - sys._base_executable = executable - print(f'Creating a virtualenv in {virtualenv_path}...') - print('(this path can be customized in .vimrc by setting g:black_virtualenv)') - venv.create(virtualenv_path, with_pip=True) - except Exception: - print('Encountered exception while creating virtualenv (see traceback below).') - print(f'Removing {virtualenv_path}...') - import shutil - shutil.rmtree(virtualenv_path) - raise - finally: - sys.executable = _executable - sys._base_executable = _base_executable - first_install = True - if first_install: - print('Installing Black with pip...') - if upgrade: - print('Upgrading Black with pip...') - if first_install or upgrade: - subprocess.run([str(_get_pip(virtualenv_path)), 'install', '-U', 'black'], stdout=subprocess.PIPE) - print('DONE! You are all set, thanks for waiting ✨ 🍰 ✨') - if first_install: - print('Pro-tip: to upgrade Black in the future, use the :BlackUpgrade command and restart Vim.\n') - if virtualenv_site_packages not in sys.path: - sys.path.insert(0, virtualenv_site_packages) - return True + print("Upgrading Black with pip...") + if first_install or upgrade: + subprocess.run( + [str(_get_pip(virtualenv_path)), "install", "-U", "black"], + stdout=subprocess.PIPE, + ) + print("DONE! You are all set, thanks for waiting ✨ 🍰 ✨") + if first_install: + print( + "Pro-tip: to upgrade Black in the future, use the :BlackUpgrade command and" + " restart Vim.\n" + ) + if virtualenv_site_packages not in sys.path: + sys.path.insert(0, virtualenv_site_packages) + return True + if _initialize_black_env(): - try: - import black - except ImportError: - print(f"Could not import black from any of: {', '.join(sys.path)}.") - import time + try: + import black + except ImportError: + print(f"Could not import black from any of: {', '.join(sys.path)}.") + import time + def get_target_version(tv): - if isinstance(tv, black.TargetVersion): - return tv - ret = None - try: - ret = black.TargetVersion[tv.upper()] - except KeyError: - print(f"WARNING: Target version {tv!r} not recognized by Black, using default target") - return ret + if isinstance(tv, black.TargetVersion): + return tv + ret = None + try: + ret = black.TargetVersion[tv.upper()] + except KeyError: + print( + f"WARNING: Target version {tv!r} not recognized by Black, using default" + " target" + ) + return ret + def Black(**kwargs): - """ - kwargs allows you to override ``target_versions`` argument of - ``black.FileMode``. - - ``target_version`` needs to be cleaned because ``black.FileMode`` - expects the ``target_versions`` argument to be a set of TargetVersion enums. - - Allow kwargs["target_version"] to be a string to allow - to type it more quickly. - - Using also target_version instead of target_versions to remain - consistent to Black's documentation of the structure of pyproject.toml. - """ - start = time.time() - configs = get_configs() - - black_kwargs = {} - if "target_version" in kwargs: - target_version = kwargs["target_version"] - - if not isinstance(target_version, (list, set)): - target_version = [target_version] - target_version = set(filter(lambda x: x, map(lambda tv: get_target_version(tv), target_version))) - black_kwargs["target_versions"] = target_version - - mode = black.FileMode( - line_length=configs["line_length"], - string_normalization=not configs["skip_string_normalization"], - is_pyi=vim.current.buffer.name.endswith('.pyi'), - magic_trailing_comma=not configs["skip_magic_trailing_comma"], - preview=configs["preview"], - **black_kwargs, - ) - quiet = configs["quiet"] - - buffer_str = '\n'.join(vim.current.buffer) + '\n' - try: - new_buffer_str = black.format_file_contents( - buffer_str, - fast=configs["fast"], - mode=mode, + """ + kwargs allows you to override ``target_versions`` argument of + ``black.FileMode``. + + ``target_version`` needs to be cleaned because ``black.FileMode`` + expects the ``target_versions`` argument to be a set of TargetVersion enums. + + Allow kwargs["target_version"] to be a string to allow + to type it more quickly. + + Using also target_version instead of target_versions to remain + consistent to Black's documentation of the structure of pyproject.toml. + """ + start = time.time() + configs = get_configs() + + black_kwargs = {} + if "target_version" in kwargs: + target_version = kwargs["target_version"] + + if not isinstance(target_version, (list, set)): + target_version = [target_version] + target_version = set( + filter(lambda x: x, map(lambda tv: get_target_version(tv), target_version)) + ) + black_kwargs["target_versions"] = target_version + + mode = black.FileMode( + line_length=configs["line_length"], + string_normalization=not configs["skip_string_normalization"], + is_pyi=vim.current.buffer.name.endswith(".pyi"), + magic_trailing_comma=not configs["skip_magic_trailing_comma"], + preview=configs["preview"], + **black_kwargs, ) - except black.NothingChanged: - if not quiet: - print(f'Black: already well formatted, good job. (took {time.time() - start:.4f}s)') - except Exception as exc: - print(f'Black: {exc}') - else: - current_buffer = vim.current.window.buffer - cursors = [] - for i, tabpage in enumerate(vim.tabpages): - if tabpage.valid: - for j, window in enumerate(tabpage.windows): - if window.valid and window.buffer == current_buffer: - cursors.append((i, j, window.cursor)) - vim.current.buffer[:] = new_buffer_str.split('\n')[:-1] - for i, j, cursor in cursors: - window = vim.tabpages[i].windows[j] - try: - window.cursor = cursor - except vim.error: - window.cursor = (len(window.buffer), 0) - if not quiet: - print(f'Black: reformatted in {time.time() - start:.4f}s.') + quiet = configs["quiet"] + + buffer_str = "\n".join(vim.current.buffer) + "\n" + try: + new_buffer_str = black.format_file_contents( + buffer_str, + fast=configs["fast"], + mode=mode, + ) + except black.NothingChanged: + if not quiet: + print( + "Black: already well formatted, good job. (took" + f" {time.time() - start:.4f}s)" + ) + except Exception as exc: + print(f"Black: {exc}") + else: + current_buffer = vim.current.window.buffer + cursors = [] + for i, tabpage in enumerate(vim.tabpages): + if tabpage.valid: + for j, window in enumerate(tabpage.windows): + if window.valid and window.buffer == current_buffer: + cursors.append((i, j, window.cursor)) + vim.current.buffer[:] = new_buffer_str.split("\n")[:-1] + for i, j, cursor in cursors: + window = vim.tabpages[i].windows[j] + try: + window.cursor = cursor + except vim.error: + window.cursor = (len(window.buffer), 0) + if not quiet: + print(f"Black: reformatted in {time.time() - start:.4f}s.") + def get_configs(): - filename = vim.eval("@%") - path_pyproject_toml = black.find_pyproject_toml((filename,)) - if path_pyproject_toml: - toml_config = black.parse_pyproject_toml(path_pyproject_toml) - else: - toml_config = {} + filename = vim.eval("@%") + path_pyproject_toml = black.find_pyproject_toml((filename,)) + if path_pyproject_toml: + toml_config = black.parse_pyproject_toml(path_pyproject_toml) + else: + toml_config = {} - return { - flag.var_name: toml_config.get(flag.name, flag.cast(vim.eval(flag.vim_rc_name))) - for flag in FLAGS - } + return { + flag.var_name: toml_config.get(flag.name, flag.cast(vim.eval(flag.vim_rc_name))) + for flag in FLAGS + } def BlackUpgrade(): - _initialize_black_env(upgrade=True) + _initialize_black_env(upgrade=True) -def BlackVersion(): - print(f'Black, version {black.__version__} on Python {sys.version}.') +def BlackVersion(): + print(f"Black, version {black.__version__} on Python {sys.version}.") EndPython3 function black#Black(...) @@ -241,6 +288,7 @@ function black#Black(...) endfor python3 << EOF import vim + kwargs = vim.eval("kwargs") EOF :py3 Black(**kwargs) diff --git a/docs/Makefile b/docs/Makefile index cb0463c842b..6e98756b0a3 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -10,11 +10,11 @@ BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py index 8f2a216fdd5..16047255562 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -76,9 +76,6 @@ def setup(app: Sphinx) -> None: # -- General configuration --------------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = "4.4" - # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. @@ -91,9 +88,6 @@ def setup(app: Sphinx) -> None: "sphinx_copybutton", ] -# If you need extensions of a certain version or higher, list them here. -needs_extensions = {"myst_parser": "0.13.7"} - # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/contributing/gauging_changes.md b/docs/contributing/gauging_changes.md index d1c28a92c50..0af0c29bbe9 100644 --- a/docs/contributing/gauging_changes.md +++ b/docs/contributing/gauging_changes.md @@ -5,7 +5,7 @@ these changes is hard, so we have tooling to help make it easier. It's recommended you evaluate the quantifiable changes your _Black_ formatting modification causes before submitting a PR. Think about if the change seems disruptive -enough to cause frustration to projects that are already "black formatted". +enough to cause frustration to projects that are already "Black-formatted". ## diff-shades @@ -18,14 +18,14 @@ For more information, please see the [diff-shades documentation][diff-shades]. ### CI integration -diff-shades is also the tool behind the "diff-shades results comparing ..." / -"diff-shades reports zero changes ..." comments on PRs. The project has a GitHub Actions -workflow that analyzes and compares two revisions of _Black_ according to these rules: +diff-shades is also the tool behind the "diff-shades results comparing ..." comments on +PRs. The project has a GitHub Actions workflow that analyzes and compares two revisions +of _Black_ according to these rules: -| | Baseline revision | Target revision | -| --------------------- | ----------------------- | ---------------------------- | -| On PRs | latest commit on `main` | PR commit with `main` merged | -| On pushes (main only) | latest PyPI version | the pushed commit | +| | Baseline revision | Target revision | +| --------------------- | ------------------------------- | ---------------------------- | +| On PRs | latest commit on PR base branch | PR commit with `main` merged | +| On pushes (main only) | latest PyPI version | the pushed commit | For pushes to main, there's only one analysis job named `preview-new-changes` where the preview style is used for all projects. @@ -35,24 +35,23 @@ For PRs they get one more analysis job: `assert-no-changes`. It's similar to made. This makes sure code won't be reformatted again and again within the same year in accordance to Black's stability policy. -Additionally for PRs, a PR comment will be posted embedding a summary previewing the -changes in the preview style and links to further information. The next time the -workflow is triggered on the same PR, it'll update the pre-existing diff-shades comment. +Additionally for PRs, a PR comment will be posted embedding a summary previewing any +changes in both styles and links to further information. The next time the workflow is +triggered on the same PR, it'll update the pre-existing diff-shades comment. ```{note} -The `preview-new-changes` job will only fail intentionally if while analyzing a file failed to -format. Otherwise a failure indicates a bug in the workflow. +Jobs will only fail intentionally if a file failed to format while analyzing, or if +changes were made to the stable style. Otherwise a failure indicates a bug in the +workflow. ``` The workflow uploads several artifacts upon completion: -- The raw analyses (.json) - HTML diffs (.html) -- `.pr-comment.json` (if triggered by a PR) - -The last one is downloaded by the `diff-shades-comment` workflow and shouldn't be -downloaded locally. The HTML diffs come in handy for push-based where there's no PR to -post a comment. And the analyses exist just in case you want to do further analysis -using the collected data locally. + - handy for pushes where there's no PR to post a comment +- The raw analyses (.json) + - in case you want to do further analysis using the collected data locally +- `.preview.pr-comment.md` and `.stable.pr-comment.md` (if triggered by a PR) + - used to generate the PR comment and shouldn't be downloaded [diff-shades]: https://github.com/ichard26/diff-shades#readme diff --git a/docs/contributing/issue_triage.md b/docs/contributing/issue_triage.md index 3378824b284..fad25cc71e5 100644 --- a/docs/contributing/issue_triage.md +++ b/docs/contributing/issue_triage.md @@ -6,16 +6,16 @@ can be eventually be resolved somehow. This document outlines the triaging proce also the current guidelines and recommendations. ```{tip} -If you're looking for a way to contribute without submitting patches, this might be -the area for you. Since _Black_ is a popular project, its issue tracker is quite busy -and always needs more attention than is available. While triage isn't the most -glamorous or technically challenging form of contribution, it's still important. -For example, we would love to know whether that old bug report is still reproducible! +If you're looking for a way to contribute without submitting patches, this might be the +area for you. Since _Black_ is a popular project, its issue tracker is quite busy and +always needs more attention than is available. While triage isn't the most glamorous or +technically challenging form of contribution, it's still important. For example, we +would love to know whether that old bug report is still reproducible! You can get easily started by reading over this document and then responding to issues. -If you contribute enough and have stayed for a long enough time, you may even be -given Triage permissions! +If you contribute enough and have stayed for long enough, you may even be given +Triage permissions! ``` ## The basics @@ -97,8 +97,8 @@ We also have a few standalone labels: ```{note} We do use labels for PRs, in particular the `skip news` label, but we aren't that -rigorous about it. Just follow your judgement on what labels make sense for the -specific PR (if any even make sense). +rigorous about it. Just follow your judgement on what labels make sense for the specific +PR (if any even make sense). ``` ## Projects @@ -108,7 +108,8 @@ projects with no true end (e.g. the "Amazing documentation" project) while other more focused and have a definite end (like the "Getting to beta" project). ```{note} -To modify GitHub Projects you need the [Write repository permission level or higher](https://docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/repository-permission-levels-for-an-organization#repository-access-for-each-permission-level). +To modify GitHub Projects you need the +[Write repository permission level or higher](https://docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/repository-permission-levels-for-an-organization#repository-access-for-each-permission-level). ``` ## Closing issues @@ -160,8 +161,7 @@ So no, this is not a bug, but an intended feature. Anyway, [here's the documenta ```text Hi, -This is expected behaviour, please see the documentation regarding this case (emphasis -mine): +This is expected behaviour, please see the documentation regarding this case (emphasis mine): > PEP 8 recommends to treat : in slices as a binary operator with the lowest priority, and to leave an equal amount of space on either side, **except if a parameter is omitted (e.g. ham[1 + 1 :])**. It recommends no spaces around : operators for “simple expressions” (ham[lower:upper]), and **extra space for “complex expressions” (ham[lower : upper + offset])**. **Black treats anything more than variable names as “complex” (ham[lower : upper + 1]).** It also states that for extended slices, both : operators have to have the same amount of spacing, except if a parameter is omitted (ham[1 + 1 ::]). Black enforces these rules consistently. diff --git a/docs/contributing/release_process.md b/docs/contributing/release_process.md index cfa15f2a32a..22ae781f6f6 100644 --- a/docs/contributing/release_process.md +++ b/docs/contributing/release_process.md @@ -35,7 +35,7 @@ builds all release artifacts and publishes them to the various platforms we publ We now have a `scripts/release.py` script to help with cutting the release PRs. - `python3 scripts/release.py --help` is your friend. - - `release.py` has only been tested in Python 3.12 (so get with the times :D) + - `release.py` has only been tested in Python 3.12+ (so get with the times :D) To cut a release: @@ -43,7 +43,7 @@ To cut a release: - **_Black_ follows the [CalVer] versioning standard using the `YY.M.N` format** - So unless there already has been a release during this month, `N` should be `0` - Example: the first release in January, 2022 → `22.1.0` - - `release.py` will calculate this and log to stderr for you copy paste pleasure + - `release.py` will calculate this and log it to stdout for your copy-paste pleasure 1. Double-check that no changelog entries since the last release were put in the wrong section (e.g., run `git diff origin/stable CHANGES.md`) 1. File a PR editing `CHANGES.md` and the docs to version the latest changes @@ -102,7 +102,7 @@ They are triggered by the publication of a [GitHub Release]. Below are descriptions of our release workflows. -### Publish to PyPI +### build and publish This is our main workflow. It builds an [sdist] and [wheels] to upload to PyPI where the vast majority of users will download Black from. It's divided into three job groups: @@ -110,10 +110,10 @@ vast majority of users will download Black from. It's divided into three job gro #### sdist + pure wheel This single job builds the sdist and pure Python wheel (i.e., a wheel that only contains -Python code) using [build] and then uploads them to PyPI using [twine]. These artifacts -are general-purpose and can be used on basically any platform supported by Python. +Python code) using [Hatch]. These artifacts are general-purpose and can be used on +basically any platform supported by Python. -#### mypyc wheels (…) +#### generate wheels matrix / mypyc wheels (…) We use [mypyc] to compile _Black_ into a CPython C extension for significantly improved performance. Wheels built with mypyc are platform and Python version specific. @@ -124,9 +124,12 @@ extensions for many environments for us. Since building these wheels is slow, th multiple mypyc wheels jobs (hence the term "matrix") that build for a specific platform (as noted in the job name in parentheses). -Like the previous job group, the built wheels are uploaded to PyPI using [twine]. +#### publish-hatch / publish-mypyc -#### Update stable branch +These jobs upload the built sdist and all wheels to PyPI using [Trusted +publishing][trusted-publishing]. + +#### update stable branch So this job doesn't _really_ belong here, but updating the `stable` branch after the other PyPI jobs pass (they must pass for this job to start) makes the most sense. This @@ -134,7 +137,7 @@ saves us from remembering to update the branch sometime after cutting the releas - _Currently this workflow uses an API token associated with @ambv's PyPI account_ -### Publish executables +### publish binaries This workflow builds native executables for multiple platforms using [PyInstaller]. This allows people to download the executable for their platform and run _Black_ without a @@ -155,18 +158,16 @@ This also runs on each push to `main`. ``` [black-actions]: https://github.com/psf/black/actions -[build]: https://pypa-build.readthedocs.io/ [calver]: https://calver.org [cibuildwheel]: https://cibuildwheel.readthedocs.io/ [gh-4563]: https://github.com/psf/black/pull/4563 [github actions]: https://github.com/features/actions [github release]: https://github.com/psf/black/releases -[new-release]: https://github.com/psf/black/releases/new +[hatch]: https://hatch.pypa.io/latest/ [mypyc]: https://mypyc.readthedocs.io/ -[mypyc-platform-support]: - /faq.html#what-is-compiled-yes-no-all-about-in-the-version-output +[new-release]: https://github.com/psf/black/releases/new [pyinstaller]: https://www.pyinstaller.org/ [sdist]: - https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist -[twine]: https://github.com/features/actions -[wheels]: https://packaging.python.org/en/latest/glossary/#term-Wheel + https://packaging.python.org/en/latest/glossary/#term-source-distribution-or-sdist +[trusted-publishing]: https://docs.pypi.org/trusted-publishers/ +[wheels]: https://packaging.python.org/en/latest/glossary/#term-wheel diff --git a/docs/contributing/the_basics.md b/docs/contributing/the_basics.md index f69867eb08d..4537ca127ce 100644 --- a/docs/contributing/the_basics.md +++ b/docs/contributing/the_basics.md @@ -22,7 +22,7 @@ $ python3 -m venv .venv $ source .venv/bin/activate # activation for linux and mac $ .venv\Scripts\activate # activation for windows -(.venv)$ pip install -r test_requirements.txt +(.venv)$ pip install --group dev (.venv)$ pip install -e ".[d]" (.venv)$ pre-commit install ``` @@ -31,17 +31,10 @@ Before submitting pull requests, run lints and tests with the following commands the root of the black repo: ```console -# Linting -(.venv)$ pre-commit run -a - -# Unit tests -(.venv)$ tox -e py - -# Optional Fuzz testing -(.venv)$ tox -e fuzz - -# Format Black itself -(.venv)$ tox -e run_self +(.venv)$ pre-commit run -a # Linting +(.venv)$ tox -e py # Unit tests +(.venv)$ tox -e fuzz # Optional Fuzz testing +(.venv)$ tox -e run_self # Format Black itself ``` ### Development @@ -49,23 +42,10 @@ the root of the black repo: Further examples of invoking the tests ```console -# Run all of the above mentioned, in parallel -(.venv)$ tox --parallel=auto - -# Run tests on a specific python version -(.venv)$ tox -e py314 - -# Run an individual test -(.venv)$ pytest -k - -# Pass arguments to pytest -(.venv)$ tox -e py -- --no-cov - -# Print full tree diff, see documentation below -(.venv)$ tox -e py -- --print-full-tree - -# Disable diff printing, see documentation below -(.venv)$ tox -e py -- --print-tree-diff=False +(.venv)$ tox --parallel=auto # Run all the above in parallel +(.venv)$ tox -e py314 # Run tests on a specific python version +(.venv)$ pytest -k # Run an individual test +(.venv)$ tox -e py -- --no-cov # Pass arguments to pytest ``` ### Testing @@ -88,7 +68,7 @@ files in the `tests/data/cases` directory. These files consist of up to three pa If this is omitted, the test asserts that _Black_ will leave the input code unchanged. _Black_ has two pytest command-line options affecting test files in `tests/data/` that -are split into an input part, and an output part, separated by a line with`# output`. +are split into an input part, and an output part, separated by a line with `# output`. These can be passed to `pytest` through `tox`, or directly into pytest if not using `tox`. @@ -99,11 +79,19 @@ the input ("actual"), and the tree that's yielded after parsing the output ("exp Note that a test can fail with different output with the same CST. This used to be the default, but now defaults to `False`. +```console +(.venv)$ tox -e py -- --print-full-tree +``` + #### `--print-tree-diff` Upon a failing test, print the diff of the trees as described above. This is the default. To turn it off pass `--print-tree-diff=False`. +```console +(.venv)$ tox -e py -- --print-tree-diff=False +``` + ### News / Changelog Requirement `Black` has CI that will check for an entry corresponding to your PR in `CHANGES.md`. If @@ -128,18 +116,24 @@ does not need to go back and workout what to add to the `CHANGES.md` for each re ### Style Changes +Please familiarize yourself with our [stability policy](labels/stability-policy). +Therefore, most style changes must be added to the `--preview` style. Exceptions are +fixing crashes or changes that would not affect an already-formatted file. + If a change would affect the advertised code style, please modify the documentation (The _Black_ code style) to reflect that change. Patches that fix unintended bugs in -formatting don't need to be mentioned separately though. If the change is implemented -with the `--preview` flag, please include the change in the future style document -instead and write the changelog entry under the dedicated "Preview style" heading. +formatting don't need to be mentioned separately. + +If the change is implemented with the `--preview` flag, please include the change in the +Future Style document instead and write the changelog entry under the dedicated "Preview +style" heading. ### Docs Testing If you make changes to docs, you can test they still build locally too. ```console -(.venv)$ pip install -r docs/requirements.txt +(.venv)$ pip install --group docs (.venv)$ pip install -e ".[d]" (.venv)$ sphinx-build -a -b html -W docs/ docs/_build/ ``` diff --git a/docs/faq.md b/docs/faq.md index d2b49ae35ed..e418b015f71 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -123,12 +123,12 @@ CPython doesn't accept. While _Black_ is indeed a pure Python project, we use [mypyc] to compile _Black_ into a C Python extension, usually doubling performance. These compiled wheels are available -for 64-bit versions of Windows, Linux (via the manylinux standard), and macOS across all -supported CPython versions. +for 64-bit versions of Windows (both AMD and ARM), Linux (via the manylinux standard), +and macOS across all supported CPython versions. -Platforms including musl-based and/or ARM Linux distributions, and ARM Windows are -currently **not** supported. These platforms will fall back to the slower pure Python -wheel available on PyPI. +Platforms including musl-based and/or ARM Linux distributions are currently **not** +supported. These platforms will fall back to the slower pure Python wheel available on +PyPI. If you are experiencing exceptionally weird issues or even segfaults, you can try passing `--no-binary black` to your pip install invocation. This flag excludes all diff --git a/docs/guides/introducing_black_to_your_project.md b/docs/guides/introducing_black_to_your_project.md index 5ba1963c2d0..5c9b47cf980 100644 --- a/docs/guides/introducing_black_to_your_project.md +++ b/docs/guides/introducing_black_to_your_project.md @@ -1,8 +1,7 @@ # Introducing _Black_ to your project ```{note} -This guide is incomplete. Contributions are welcomed and would be deeply -appreciated! +This guide is incomplete. Contributions are welcomed and would be deeply appreciated! ``` ## Avoiding ruining git blame @@ -46,7 +45,8 @@ $ git config blame.ignoreRevsFile .git-blame-ignore-revs **The one caveat is that some online Git-repositories do not yet support ignoring revisions using their native blame UI.** So blame information will be cluttered with a -reformatting commit on those platforms. -[GitHub supports `.git-blame-ignore-revs`](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view) -by default in blame views however. -[GitLab supports this since version 17.10](https://about.gitlab.com/releases/2025/03/20/gitlab-17-10-released/#ignore-specific-revisions-in-git-blame) +reformatting commit on those platforms. However, +[GitHub](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view) +and +[GitLab (since version 17.10)](https://about.gitlab.com/releases/2025/03/20/gitlab-17-10-released/#ignore-specific-revisions-in-git-blame) +both support `.git-blame-ignore-revs` in blame views by default. diff --git a/docs/guides/using_black_with_other_tools.md b/docs/guides/using_black_with_other_tools.md index 187e3a3e6f5..6c398827115 100644 --- a/docs/guides/using_black_with_other_tools.md +++ b/docs/guides/using_black_with_other_tools.md @@ -40,7 +40,7 @@ If you're using an isort version that is older than 5.0.0 or you have some custo configuration for _Black_, you can tweak your isort configuration to make it compatible with _Black_. Below, an example for `.isort.cfg`: -``` +```ini multi_line_output = 3 include_trailing_comma = True force_grid_wrap = 0 @@ -59,7 +59,7 @@ behaviour can be isort's default mode of wrapping imports that extend past the `line_length` limit is "Grid". -```py3 +```python from third_party import (lib1, lib2, lib3, lib4, lib5, ...) ``` @@ -67,7 +67,7 @@ from third_party import (lib1, lib2, lib3, This style is incompatible with _Black_, but isort can be configured to use a different wrapping mode called "Vertical Hanging Indent" which looks like this: -```py3 +```python from third_party import ( lib1, lib2, @@ -143,7 +143,7 @@ There are a few deviations that cause incompatibilities with _Black_. #### Configuration -``` +```ini max-line-length = 88 ignore = E203,E701 ``` @@ -214,7 +214,7 @@ instead of using Flake8's E501, because it aligns with Install Bugbear and use the following config: -``` +```ini [flake8] max-line-length = 80 extend-select = B950 @@ -226,7 +226,7 @@ extend-ignore = E203,E501,E701 In cases where you can't or don't want to install Bugbear, you can use this minimally compatible config: -``` +```ini [flake8] max-line-length = 88 extend-ignore = E203,E701 @@ -257,7 +257,7 @@ style conventions like variable naming. #### Configuration -``` +```ini max-line-length = 88 ``` @@ -284,7 +284,7 @@ max-line-length = 88
setup.cfg -```cfg +```ini [pylint] max-line-length = 88 ``` diff --git a/docs/index.md b/docs/index.md index 49a44ecca5a..a1c61bc62fb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,17 +18,16 @@ transparent after a while and you can focus on the content instead. Try it out now using the [Black Playground](https://black.vercel.app). ```{admonition} Note - Black is now stable! -*Black* is [successfully used](https://github.com/psf/black#used-by) by -many projects, small and big. *Black* has a comprehensive test suite, with efficient -parallel tests, our own auto formatting and parallel Continuous Integration runner. -Now that we have become stable, you should not expect large changes to formatting in -the future. Stylistic changes will mostly be responses to bug reports and support for new Python +_Black_ is [successfully used](https://github.com/psf/black#used-by) by many projects, +small and big. _Black_ has a comprehensive test suite, with efficient parallel tests, +our own auto formatting and parallel Continuous Integration runner. Now that we have +become stable, you should not expect large changes to formatting in the future. +Stylistic changes will mostly be responses to bug reports and support for new Python syntax. -Also, as a safety measure which slows down processing, *Black* will check that the +Also, as a safety measure which slows down processing, _Black_ will check that the reformatted code still produces a valid AST that is effectively equivalent to the -original (see the -[Pragmatism](./the_black_code_style/current_style.md#pragmatism) +original (see the [Pragmatism](./the_black_code_style/current_style.md#pragmatism) section for details). If you're feeling confident, use `--fast`. ``` diff --git a/docs/integrations/editors.md b/docs/integrations/editors.md index 257112bc174..d906f38b753 100644 --- a/docs/integrations/editors.md +++ b/docs/integrations/editors.md @@ -88,7 +88,7 @@ There are several different ways you can use _Black_ from PyCharm: ```console $ where black - %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location + C:\Program Files\Python313\Scripts\black.exe # possible location ``` Note that if you are using a virtual environment detected by PyCharm, this is an @@ -135,7 +135,7 @@ There are several different ways you can use _Black_ from PyCharm: ```console $ where black - %LocalAppData%\Programs\Python\Python36-32\Scripts\black.exe # possible location + C:\Program Files\Python313\Scripts\black.exe # possible location ``` Note that if you are using a virtual environment detected by PyCharm, this is an @@ -243,7 +243,7 @@ To install with [vim-plug](https://github.com/junegunn/vim-plug): _Black_'s `stable` branch tracks official version updates, and can be used to simply follow the most recent stable version. -``` +```vim Plug 'psf/black', { 'branch': 'stable' } ``` @@ -255,14 +255,14 @@ The following matches all stable versions (see the [Release Process](../contributing/release_process.md) section for documentation of version scheme used by Black): -``` +```vim Plug 'psf/black', { 'tag': '*.*.*' } ``` and the following demonstrates pinning to a specific year's stable style (2022 in this case): -``` +```vim Plug 'psf/black', { 'tag': '22.*.*' } ``` @@ -270,7 +270,7 @@ Plug 'psf/black', { 'tag': '22.*.*' } or with [Vundle](https://github.com/VundleVim/Vundle.vim): -``` +```vim Plugin 'psf/black' ``` @@ -293,7 +293,7 @@ or you can copy the plugin files from [plugin/black.vim](https://github.com/psf/black/blob/stable/plugin/black.vim) and [autoload/black.vim](https://github.com/psf/black/blob/stable/autoload/black.vim). -``` +```sh mkdir -p ~/.vim/pack/python/start/black/plugin mkdir -p ~/.vim/pack/python/start/black/autoload curl https://raw.githubusercontent.com/psf/black/stable/plugin/black.vim -o ~/.vim/pack/python/start/black/plugin/black.vim @@ -316,7 +316,7 @@ example you want to run a version from main), create a virtualenv manually and p If you would prefer to use the system installation of _Black_ rather than a virtualenv, then add this to your vimrc: -``` +```vim let g:black_use_virtualenv = 0 ``` @@ -327,7 +327,7 @@ whatever tool you used to install _Black_ originally. To run _Black_ on save, add the following lines to `.vimrc` or `init.vim`: -``` +```vim augroup black_on_save autocmd! autocmd BufWritePre *.py Black @@ -336,7 +336,7 @@ augroup end To run _Black_ on a key press (e.g. F9 below), add this: -``` +```vim nnoremap :Black ``` @@ -368,7 +368,7 @@ $ gedit 1. Add a new tool using `+` button. 1. Copy the below content to the code window. -```console +```sh #!/bin/bash Name=$GEDIT_CURRENT_DOCUMENT_NAME black $Name diff --git a/docs/make.bat b/docs/make.bat index eecd650cf03..79372c6c008 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -5,7 +5,7 @@ pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build + set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build @@ -15,15 +15,15 @@ if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 3ee47a91eaa..00000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -# Used by ReadTheDocs; pinned requirements for stability. - -myst-parser==4.0.1 -Sphinx==8.2.3 -# Older versions break Sphinx even though they're declared to be supported. -docutils==0.21.2 -sphinxcontrib-programoutput==0.18 -sphinx_copybutton==0.5.2 -furo==2025.12.19 diff --git a/docs/the_black_code_style/current_style.md b/docs/the_black_code_style/current_style.md index e43a93451d5..722de26fff5 100644 --- a/docs/the_black_code_style/current_style.md +++ b/docs/the_black_code_style/current_style.md @@ -21,7 +21,7 @@ whatever makes `pycodestyle` happy. As for vertical whitespace, _Black_ tries to render one full expression or simple statement per line. If this fits the allotted line length, great. -```py3 +```python # in: j = [1, @@ -37,7 +37,7 @@ j = [1, 2, 3] If not, _Black_ will look at the contents of the first outer matching brackets and put that in a separate indented line. -```py3 +```python # in: ImportantClass.important_method(exc, limit, lookup_lines, capture_locals, extra_argument) @@ -55,7 +55,7 @@ matching brackets pair are comma-separated (like an argument list, or a dict lit and so on) then _Black_ will first try to keep them on the same line with the matching brackets. If that doesn't work, it will put all of them in separate lines. -```py3 +```python # in: def very_important_function(template: str, *variables, file: os.PathLike, engine: str, header: bool = True, debug: bool = False): @@ -95,7 +95,7 @@ indentation level (like the arguments list and the docstring in the example abov _Black_ prefers parentheses over backslashes, and will remove backslashes if found. -```py3 +```python # in: if some_short_rule1 \ @@ -360,7 +360,7 @@ Please note that _Black_ does not add or remove any additional nested parenthese you might want to have for clarity or further code organization. For example those parentheses are not going to be removed: -```py3 +```python return not (this or that) decision = (maybe.this() and values > 0) or (maybe.that() and values < 0) ``` @@ -373,7 +373,7 @@ those by treating dots that follow a call or an indexing operation like a very l priority delimiter. It's easier to show the behavior than to explain it. Look at the example: -```py3 +```python def example(session): result = ( session.query(models.Customer.id) @@ -445,7 +445,7 @@ but you anticipate it will grow in the future. For example: -```py3 +```python TRANSLATIONS = { "en_us": "English (US)", "pl_pl": "polski", diff --git a/gallery/Dockerfile b/gallery/Dockerfile index ced85e58e6e..6d671112c67 100644 --- a/gallery/Dockerfile +++ b/gallery/Dockerfile @@ -1,12 +1,12 @@ -FROM python:3-slim +FROM python:3.13-slim # note: a single RUN to avoid too many image layers being produced RUN apt-get update \ - && apt-get upgrade -y \ - && apt-get install git apt-utils -y --no-install-recommends\ - && git config --global user.email "black@psf.github.com" \ - && git config --global user.name "Gallery/Black" \ - && rm -rf /var/lib/apt/lists/* + && apt-get upgrade -y \ + && apt-get install git apt-utils -y --no-install-recommends\ + && git config --global user.email "black@psf.github.com" \ + && git config --global user.name "Gallery/Black" \ + && rm -rf /var/lib/apt/lists/* COPY gallery.py / ENTRYPOINT ["python", "/gallery.py"] diff --git a/plugin/black.vim b/plugin/black.vim index 7180e80939f..db6b56646c5 100644 --- a/plugin/black.vim +++ b/plugin/black.vim @@ -2,10 +2,10 @@ " Author: Łukasz Langa " Created: Mon Mar 26 23:27:53 2018 -0700 " Requires: Vim Ver7.0+ -" Version: 1.2 +" Version: 1.2 " " Documentation: -" This plugin formats Python files. +" This plugin formats Python files. " " History: " 1.0: @@ -16,7 +16,7 @@ " - use autoload script if exists("g:load_black") - finish + finish endif if v:version < 700 || !has('python3') @@ -24,10 +24,10 @@ if v:version < 700 || !has('python3') let messages = [] if v:version < 700 - call add(messages, "vim7.0+") + call add(messages, "vim7.0+") endif if !has('python3') - call add(messages, "Python 3.10 support") + call add(messages, "Python 3.10 support") endif echo "The black.vim plugin requires" join(messages, " and ") @@ -40,53 +40,53 @@ endif let g:load_black = "py1.0" if !exists("g:black_virtualenv") - if has("nvim") - let g:black_virtualenv = "~/.local/share/nvim/black" - else - let g:black_virtualenv = "~/.vim/black" - endif + if has("nvim") + let g:black_virtualenv = "~/.local/share/nvim/black" + else + let g:black_virtualenv = "~/.vim/black" + endif endif if !exists("g:black_fast") - let g:black_fast = 0 + let g:black_fast = 0 endif if !exists("g:black_linelength") - let g:black_linelength = 88 + let g:black_linelength = 88 endif if !exists("g:black_skip_string_normalization") - if exists("g:black_string_normalization") - let g:black_skip_string_normalization = !g:black_string_normalization - else - let g:black_skip_string_normalization = 0 - endif + if exists("g:black_string_normalization") + let g:black_skip_string_normalization = !g:black_string_normalization + else + let g:black_skip_string_normalization = 0 + endif endif if !exists("g:black_skip_magic_trailing_comma") - if exists("g:black_magic_trailing_comma") - let g:black_skip_magic_trailing_comma = !g:black_magic_trailing_comma - else - let g:black_skip_magic_trailing_comma = 0 - endif + if exists("g:black_magic_trailing_comma") + let g:black_skip_magic_trailing_comma = !g:black_magic_trailing_comma + else + let g:black_skip_magic_trailing_comma = 0 + endif endif if !exists("g:black_quiet") - let g:black_quiet = 0 + let g:black_quiet = 0 endif if !exists("g:black_target_version") - let g:black_target_version = "" + let g:black_target_version = "" endif if !exists("g:black_use_virtualenv") - let g:black_use_virtualenv = 1 + let g:black_use_virtualenv = 1 endif if !exists("g:black_preview") - let g:black_preview = 0 + let g:black_preview = 0 endif function BlackComplete(ArgLead, CmdLine, CursorPos) - return [ -\ 'target_version=py310', -\ 'target_version=py311', -\ 'target_version=py312', -\ 'target_version=py313', -\ 'target_version=py314', -\ ] + return [ +\ 'target_version=py310', +\ 'target_version=py311', +\ 'target_version=py312', +\ 'target_version=py313', +\ 'target_version=py314', +\ ] endfunction command! -nargs=* -complete=customlist,BlackComplete Black :call black#Black() diff --git a/pyproject.toml b/pyproject.toml index 01ed067f022..05b9dcdb0ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,19 @@ # Example configuration for Black. # NOTE: you have to use single-quoted strings in TOML for regular expressions. -# It's the equivalent of r-strings in Python. Multiline strings are treated as -# verbose regular expressions by Black. Use [ ] to denote a significant space -# character. +# It's the equivalent of r-strings in Python. +# Multiline strings are treated as verbose regular expressions by Black. +# Use [ ] to denote a significant space character. [tool.black] line-length = 88 -target-version = ['py310'] +target-version = ["py310"] include = '\.pyi?$' extend-exclude = ''' /( - # The following are specific to Black, you probably don't want those. - tests/data/ - | profiling/ + # The following are specific to Black, you probably don't want those. + tests/data/ + | profiling/ ) ''' # We use the unstable style for formatting Black itself. If you @@ -26,7 +26,7 @@ unstable = true # NOTE: You don't need this in your own Black configuration. [build-system] -requires = ["hatchling>=1.27.0", "hatch-vcs", "hatch-fancy-pypi-readme"] +requires = ["hatch-fancy-pypi-readme", "hatch-vcs", "hatchling>=1.27.0"] build-backend = "hatchling.build" [project] @@ -35,42 +35,32 @@ description = "The uncompromising code formatter." license = "MIT" license-files = ["LICENSE"] requires-python = ">=3.10" -authors = [ - { name = "Łukasz Langa", email = "lukasz@langa.pl" }, -] -keywords = [ - "automation", - "autopep8", - "formatter", - "gofmt", - "pyfmt", - "rustfmt", - "yapf", -] +authors = [{ name = "Łukasz Langa", email = "lukasz@langa.pl" }] +keywords = ["automation", "autopep8", "formatter", "gofmt", "pyfmt", "rustfmt", "yapf"] classifiers = [ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3 :: Only", - "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", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Quality Assurance", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "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", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Quality Assurance", ] dependencies = [ - "click>=8.0.0", - "mypy_extensions>=0.4.3", - "packaging>=22.0", - "pathspec>=1.0.0", - "platformdirs>=2", - "pytokens>=0.3.0", - "tomli>=1.1.0; python_version < '3.11'", - "typing_extensions>=4.0.1; python_version < '3.11'", + "click>=8.0.0", + "mypy_extensions>=0.4.3", + "packaging>=22.0", + "pathspec>=1.0.0", + "platformdirs>=2", + "pytokens>=0.3.0", + "tomli>=1.1.0; python_version<'3.11'", + "typing_extensions>=4.0.1; python_version<'3.11'", ] dynamic = ["readme", "version"] @@ -78,10 +68,39 @@ dynamic = ["readme", "version"] colorama = ["colorama>=0.4.3"] uvloop = ["uvloop>=0.15.2"] d = ["aiohttp>=3.10"] -jupyter = [ - "ipython>=7.8.0", - "tokenize-rt>=3.2.0", +jupyter = ["ipython>=7.8.0", "tokenize-rt>=3.2.0"] + +[dependency-groups] +build = ["hatch==1.15.1", "hatch-fancy-pypi-readme", "hatch-vcs"] +wheels = ["cibuildwheel==3.3.0", "pypyp==1.3.0"] +binary = ["pyinstaller", "wheel>=0.45.1"] + +dev = [{ include-group = "cov-tests" }, { include-group = "tox" }, "pre-commit"] +cov-tests = [ + { include-group = "coverage" }, + { include-group = "tests" }, + "pytest-cov>=4.1.0", +] +docs = [ + "docutils==0.21.2", + "furo==2025.12.19", + "myst-parser==4.0.1", + "sphinx_copybutton==0.5.2", + "sphinx==8.2.3", + "sphinxcontrib-programoutput==0.18", +] + +tox = ["tox>=4.22"] +tests = ["pytest>=7", "pytest-xdist>=3.0.2"] +coverage = ["coverage>=5.3"] + +fuzz = [{ include-group = "coverage" }, "hypothesis", "hypothesmith"] +diff-shades = [ + "diff-shades @ https://github.com/ichard26/diff-shades/archive/stable.zip", ] +diff-shades-comment = ["click>=8.1.7", "packaging>=22.0", "urllib3"] + +width-table = ["wcwidth>=0.2.6"] [project.scripts] black = "black:patched_main" @@ -98,19 +117,16 @@ Issues = "https://github.com/psf/black/issues" [tool.hatch.metadata.hooks.fancy-pypi-readme] content-type = "text/markdown" -fragments = [ - { path = "README.md" }, - { path = "CHANGES.md" }, -] +fragments = [{ path = "README.md" }, { path = "CHANGES.md" }] [tool.hatch.version] source = "vcs" [tool.hatch.build.hooks.vcs] version-file = "src/_black_version.py" -template = ''' +template = """ version = "{version}" -''' +""" [tool.hatch.build.targets.sdist] exclude = ["/profiling"] @@ -123,26 +139,22 @@ macos-max-compat = true [tool.hatch.build.targets.wheel.hooks.mypyc] enable-by-default = false -dependencies = [ - "hatch-mypyc>=0.16.0", - "mypy==1.19.0", - "click>=8.1.7", -] +dependencies = ["hatch-mypyc>=0.16.0", "mypy==1.19.0"] require-runtime-dependencies = true exclude = [ - # There's no good reason for blackd to be compiled. - "/src/blackd", - # Not performance sensitive, so save bytes + compilation time: - "/src/blib2to3/__init__.py", - "/src/blib2to3/pgen2/__init__.py", - "/src/black/output.py", - "/src/black/concurrency.py", - "/src/black/files.py", - "/src/black/report.py", - # Breaks the test suite when compiled (and is also useless): - "/src/black/debug.py", - # Compiled modules can't be run directly and that's a problem here: - "/src/black/__main__.py", + # There's no good reason for blackd to be compiled. + "/src/blackd", + # Not performance sensitive, so save bytes + compilation time: + "/src/blib2to3/__init__.py", + "/src/blib2to3/pgen2/__init__.py", + "/src/black/output.py", + "/src/black/concurrency.py", + "/src/black/files.py", + "/src/black/report.py", + # Breaks the test suite when compiled (and is also useless): + "/src/black/debug.py", + # Compiled modules can't be run directly and that's a problem here: + "/src/black/__main__.py", ] mypy-args = ["--ignore-missing-imports"] options = { debug_level = "0" } @@ -160,15 +172,12 @@ skip = [ "*-musllinux_*", "*-win32", "pp*", - "cp31?t-*", # mypyc doesn't have great support for free threaded builds + "cp31?t-*", # mypyc doesn't have great support for free threaded builds ] -# This is the bare minimum needed to run the test suite. Pulling in the full -# test_requirements.txt would download a bunch of other packages not necessary -# here and would slow down the testing step a fair bit. -test-requires = ["pytest>=7"] -test-command = 'pytest {project} -k "not incompatible_with_mypyc"' -test-extras = ["d"," jupyter"] +test-groups = ["tests"] +test-command = 'pytest {project} -k "not incompatible_with_mypyc" -n auto' +test-extras = ["d", " jupyter"] # Skip trying to test arm64 builds on Intel Macs. (so cross-compilation doesn't # straight up crash) test-skip = ["*-macosx_arm64", "*-macosx_universal2:arm64"] @@ -177,20 +186,11 @@ test-skip = ["*-macosx_arm64", "*-macosx_universal2:arm64"] HATCH_BUILD_HOOKS_ENABLE = "1" MYPYC_OPT_LEVEL = "3" MYPYC_DEBUG_LEVEL = "0" +CC = "clang" [tool.cibuildwheel.linux] manylinux-x86_64-image = "manylinux_2_28" -before-build = [ - "yum install -y clang gcc", -] - -[tool.cibuildwheel.linux.environment] -HATCH_BUILD_HOOKS_ENABLE = "1" -MYPYC_OPT_LEVEL = "3" -MYPYC_DEBUG_LEVEL = "0" - -# Black needs Clang to compile successfully on Linux. -CC = "clang" +before-build = ["yum install -y clang"] [tool.isort] atomic = true @@ -204,21 +204,14 @@ known_first_party = ["black", "blib2to3", "blackd", "_black_version"] # Option below requires `tests/optional.py` addopts = "--strict-config --strict-markers" optional-tests = [ - "no_blackd: run when `d` extra NOT installed", - "no_jupyter: run when `jupyter` extra NOT installed", -] -markers = [ - "incompatible_with_mypyc: run when testing mypyc compiled black" + "no_blackd: run when `d` extra NOT installed", + "no_jupyter: run when `jupyter` extra NOT installed", ] +markers = ["incompatible_with_mypyc: run when testing mypyc compiled black"] xfail_strict = true filterwarnings = ["error"] [tool.coverage.report] -omit = [ - "src/blib2to3/*", - "tests/data/*", - "*/site-packages/*", - ".tox/*" -] +omit = ["src/blib2to3/*", "tests/data/*", "*/site-packages/*", ".tox/*"] [tool.coverage.run] relative_files = true branch = true diff --git a/scripts/diff_shades_gha_helper.py b/scripts/diff_shades_gha_helper.py index 971ed7f73d5..3641b62b1e9 100644 --- a/scripts/diff_shades_gha_helper.py +++ b/scripts/diff_shades_gha_helper.py @@ -20,26 +20,24 @@ import pprint import subprocess import sys -import zipfile from base64 import b64encode -from io import BytesIO +from os.path import dirname, join from pathlib import Path -from typing import Any, Final, Literal +from typing import Any, Final import click import urllib3 from packaging.version import Version -COMMENT_FILE: Final = ".pr-comment.json" +COMMENT_FILE: Final = ".pr-comment.md" DIFF_STEP_NAME: Final = "Generate HTML diff report" DOCS_URL: Final = ( - "https://black.readthedocs.io/en/latest/" - "contributing/gauging_changes.html#diff-shades" + "https://black.readthedocs.io/en/latest/contributing/gauging_changes.html#diff-shades" ) -USER_AGENT: Final = f"psf/black diff-shades workflow via urllib3/{urllib3.__version__}" SHA_LENGTH: Final = 10 GH_API_TOKEN: Final = os.getenv("GITHUB_TOKEN") REPO: Final = os.getenv("GITHUB_REPOSITORY", default="psf/black") +USER_AGENT: Final = f"{REPO} diff-shades workflow via urllib3/{urllib3.__version__}" http = urllib3.PoolManager() @@ -84,19 +82,25 @@ def http_get(url: str, *, is_json: bool = True, **kwargs: Any) -> Any: return data -def get_main_revision() -> str: +def get_latest_revision(ref: str) -> str: data = http_get( f"https://api.github.com/repos/{REPO}/commits", - fields={"per_page": "1", "sha": "main"}, + fields={"per_page": "1", "sha": ref}, ) assert isinstance(data[0]["sha"], str) return data[0]["sha"] -def get_pr_revision(pr: int) -> str: +def get_pr_branches(pr: int | None = None) -> tuple[Any, Any, int]: + if not pr: + pr_ref = os.getenv("GITHUB_REF") + assert pr_ref is not None + pr = int(pr_ref[10:-6]) + data = http_get(f"https://api.github.com/repos/{REPO}/pulls/{pr}") + assert isinstance(data["base"]["sha"], str) assert isinstance(data["head"]["sha"], str) - return data["head"]["sha"] + return data["base"], data["head"], pr def get_pypi_version() -> Version: @@ -112,47 +116,44 @@ def main() -> None: @main.command("config", help="Acquire run configuration and metadata.") -@click.argument("event", type=click.Choice(["push", "pull_request"])) -def config(event: Literal["push", "pull_request"]) -> None: +def config() -> None: import diff_shades # type: ignore[import-not-found] + jobs = [{"mode": "preview-new-changes", "style": "preview"}] + + event = os.getenv("GITHUB_EVENT_NAME") if event == "push": - jobs = [{"mode": "preview-new-changes", "force-flag": "--force-preview-style"}] # Push on main, let's use PyPI Black as the baseline. baseline_name = str(get_pypi_version()) baseline_cmd = f"git checkout {baseline_name}" + target_rev = os.getenv("GITHUB_SHA") assert target_rev is not None target_name = "main-" + target_rev[:SHA_LENGTH] target_cmd = f"git checkout {target_rev}" elif event == "pull_request": - jobs = [ - {"mode": "preview-new-changes", "force-flag": "--force-preview-style"}, - {"mode": "assert-no-changes", "force-flag": "--force-stable-style"}, - ] - # PR, let's use main as the baseline. - baseline_rev = get_main_revision() - baseline_name = "main-" + baseline_rev[:SHA_LENGTH] + jobs.insert(0, {"mode": "assert-no-changes", "style": "stable"}) + # PR, let's use the PR base as the baseline. + base, head, pr_num = get_pr_branches() + + baseline_rev = get_latest_revision(base["ref"]) + baseline_name = f"{base['ref']}-{baseline_rev[:SHA_LENGTH]}" baseline_cmd = f"git checkout {baseline_rev}" - pr_ref = os.getenv("GITHUB_REF") - assert pr_ref is not None - pr_num = int(pr_ref[10:-6]) - pr_rev = get_pr_revision(pr_num) - target_name = f"pr-{pr_num}-{pr_rev[:SHA_LENGTH]}" - target_cmd = f"gh pr checkout {pr_num} && git merge origin/main" + + target_name = f"pr-{pr_num}-{head['sha'][:SHA_LENGTH]}" + target_cmd = f"gh pr checkout {pr_num}\ngit merge origin/{base['ref']}" + else: + raise ValueError(f"Unknown event {event}") env = f"{platform.system()}-{platform.python_version()}-{diff_shades.__version__}" for entry in jobs: - entry["baseline-analysis"] = f"{entry['mode']}-{baseline_name}.json" + entry["baseline-analysis"] = f"{entry['style']}-{baseline_name}.json" entry["baseline-setup-cmd"] = baseline_cmd - entry["target-analysis"] = f"{entry['mode']}-{target_name}.json" + entry["baseline-cache-key"] = f"{env}-{baseline_name}-{entry['style']}" + + entry["target-analysis"] = f"{entry['style']}-{target_name}.json" entry["target-setup-cmd"] = target_cmd - entry["baseline-cache-key"] = f"{env}-{baseline_name}-{entry['mode']}" - if event == "pull_request": - # These are only needed for the PR comment. - entry["baseline-sha"] = baseline_rev - entry["target-sha"] = pr_rev set_output("matrix", json.dumps(jobs, indent=None)) pprint.pprint(jobs) @@ -161,69 +162,68 @@ def config(event: Literal["push", "pull_request"]) -> None: @main.command("comment-body", help="Generate the body for a summary PR comment.") @click.argument("baseline", type=click.Path(exists=True, path_type=Path)) @click.argument("target", type=click.Path(exists=True, path_type=Path)) -@click.argument("baseline-sha") -@click.argument("target-sha") -@click.argument("pr-num", type=int) -def comment_body( - baseline: Path, target: Path, baseline_sha: str, target_sha: str, pr_num: int -) -> None: - # fmt: off - cmd = [ - sys.executable, "-m", "diff_shades", "--no-color", - "compare", str(baseline), str(target), "--quiet", "--check" - ] - # fmt: on +@click.argument("style") +@click.argument("mode") +def comment_body(baseline: Path, target: Path, style: str, mode: str) -> None: + cmd = ( + f"{sys.executable} -m diff_shades --no-color " + f"compare {baseline} {target} --quiet --check" + ).split(" ") proc = subprocess.run(cmd, stdout=subprocess.PIPE, encoding="utf-8") - if not proc.returncode: + if proc.returncode: + run_id = os.getenv("GITHUB_RUN_ID") + jobs = http_get( + f"https://api.github.com/repos/{REPO}/actions/runs/{run_id}/jobs", + )["jobs"] + job = next(j for j in jobs if j["name"] == f"compare / {mode}") + diff_step = next(s for s in job["steps"] if s["name"] == DIFF_STEP_NAME) + diff_url = f"{job['html_url']}#step:{diff_step['number']}:1" + body = ( - f"**diff-shades** reports zero changes comparing this PR ({target_sha}) to" - f" main ({baseline_sha}).\n\n---\n\n" + "
" + f"--{style} style " + f'(View full diff):' + f"
{proc.stdout.strip()}
" + "
" ) else: - body = ( - f"**diff-shades** results comparing this PR ({target_sha}) to main" - f" ({baseline_sha}). The full diff is [available in the logs]" - f'($job-diff-url) under the "{DIFF_STEP_NAME}" step.' - ) - body += "\n```text\n" + proc.stdout.strip() + "\n```\n" - body += ( - f"[**What is this?**]({DOCS_URL}) | [Workflow run]($workflow-run-url) |" - " [diff-shades documentation](https://github.com/ichard26/diff-shades#readme)" - ) - print(f"[INFO]: writing comment details to {COMMENT_FILE}") - with open(COMMENT_FILE, "w", encoding="utf-8") as f: - json.dump({"body": body, "pr-number": pr_num}, f) + body = f"--{style} style: no changes" + + filename = f".{style}{COMMENT_FILE}" + print(f"[INFO]: writing comment details to {filename}") + with open(filename, "w", encoding="utf-8") as f: + f.write(body) @main.command("comment-details", help="Get PR comment resources from a workflow run.") +@click.argument("pr") @click.argument("run-id") -def comment_details(run_id: str) -> None: - data = http_get(f"https://api.github.com/repos/{REPO}/actions/runs/{run_id}") - if data["event"] != "pull_request" or data["conclusion"] == "cancelled": - set_output("needs-comment", "false") - return - - set_output("needs-comment", "true") - jobs = http_get(data["jobs_url"])["jobs"] - job = next(j for j in jobs if j["name"] == "compare / preview-new-changes") - diff_step = next(s for s in job["steps"] if s["name"] == DIFF_STEP_NAME) - diff_url = job["html_url"] + f"#step:{diff_step['number']}:1" - - artifacts = http_get(data["artifacts_url"])["artifacts"] - comment_artifact = next(a for a in artifacts if a["name"] == COMMENT_FILE) - comment_url = comment_artifact["archive_download_url"] - comment_zip = BytesIO(http_get(comment_url, is_json=False)) - with zipfile.ZipFile(comment_zip) as zfile: - with zfile.open(COMMENT_FILE) as rf: - comment_data = json.loads(rf.read().decode("utf-8")) - - set_output("pr-number", str(comment_data["pr-number"])) - body = comment_data["body"] - # It's more convenient to fill in these fields after the first workflow is done - # since this command can access the workflows API (doing it in the main workflow - # while it's still in progress seems impossible). - body = body.replace("$workflow-run-url", data["html_url"]) - body = body.replace("$job-diff-url", diff_url) +@click.argument("styles", nargs=-1) +def comment_details(pr: int, run_id: str, styles: tuple[str, ...]) -> None: + base, head, _ = get_pr_branches(pr) + + lines = [ + f"**diff-shades** results comparing this PR ({head['sha']}) to {base['ref']}" + f" ({base['sha']}):" + ] + for style_file in styles: + with open( + join(dirname(__file__), "..", style_file), + "r", + encoding="utf-8", + ) as f: + content = f.read() + lines.append(content) + + lines.append("---") + + lines.append( + f"[**What is this?**]({DOCS_URL}) | " + f"[Workflow run](https://github.com/psf/black/actions/runs/{run_id}) | " + "[diff-shades documentation](https://github.com/ichard26/diff-shades#readme)" + ) + + body = "\n\n".join(lines) set_output("comment-body", body) diff --git a/scripts/make_width_table.py b/scripts/make_width_table.py index 2f55a3dfeb5..bf4dbd04791 100644 --- a/scripts/make_width_table.py +++ b/scripts/make_width_table.py @@ -12,7 +12,7 @@ In order to run this script, you need to install the latest version of wcwidth. You can do this by running: - pip install -U wcwidth + pip install --group width-table """ diff --git a/src/blib2to3/Grammar.txt b/src/blib2to3/Grammar.txt index 286b762ef05..a2161dab86e 100644 --- a/src/blib2to3/Grammar.txt +++ b/src/blib2to3/Grammar.txt @@ -4,9 +4,9 @@ # https://devguide.python.org/grammar/ # Start symbols for the grammar: -# file_input is a module or sequence of commands read from an input file; -# single_input is a single interactive statement; -# eval_input is the input for the eval() and input() functions. +# file_input is a module or sequence of commands read from an input file; +# single_input is a single interactive statement; +# eval_input is the input for the eval() and input() functions. # NB: compound_stmt in single_input is followed by extra NEWLINE! file_input: (NEWLINE | stmt)* ENDMARKER single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE @@ -27,15 +27,15 @@ parameters: '(' [typedargslist] ')' # The following definition for typedarglist is equivalent to this set of rules: # -# arguments = argument (',' argument)* -# argument = tfpdef ['=' test] -# kwargs = '**' tname [','] -# args = '*' [tname_star] -# kwonly_kwargs = (',' argument)* [',' [kwargs]] -# args_kwonly_kwargs = args kwonly_kwargs | kwargs -# poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]] -# typedargslist_no_posonly = poskeyword_args_kwonly_kwargs | args_kwonly_kwargs -# typedarglist = arguments ',' '/' [',' [typedargslist_no_posonly]])|(typedargslist_no_posonly)" +# arguments = argument (',' argument)* +# argument = tfpdef ['=' test] +# kwargs = '**' tname [','] +# args = '*' [tname_star] +# kwonly_kwargs = (',' argument)* [',' [kwargs]] +# args_kwonly_kwargs = args kwonly_kwargs | kwargs +# poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]] +# typedargslist_no_posonly = poskeyword_args_kwonly_kwargs | args_kwonly_kwargs +# typedarglist = arguments ',' '/' [',' [typedargslist_no_posonly]])|(typedargslist_no_posonly)" # # It needs to be fully expanded to allow our LL(1) parser to work on it. @@ -54,15 +54,15 @@ tfplist: tfpdef (',' tfpdef)* [','] # The following definition for varargslist is equivalent to this set of rules: # -# arguments = argument (',' argument )* -# argument = vfpdef ['=' test] -# kwargs = '**' vname [','] -# args = '*' [vname] -# kwonly_kwargs = (',' argument )* [',' [kwargs]] -# args_kwonly_kwargs = args kwonly_kwargs | kwargs -# poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]] -# vararglist_no_posonly = poskeyword_args_kwonly_kwargs | args_kwonly_kwargs -# varargslist = arguments ',' '/' [','[(vararglist_no_posonly)]] | (vararglist_no_posonly) +# arguments = argument (',' argument )* +# argument = vfpdef ['=' test] +# kwargs = '**' vname [','] +# args = '*' [vname] +# kwonly_kwargs = (',' argument )* [',' [kwargs]] +# args_kwonly_kwargs = args kwonly_kwargs | kwargs +# poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]] +# vararglist_no_posonly = poskeyword_args_kwonly_kwargs | args_kwonly_kwargs +# varargslist = arguments ',' '/' [','[(vararglist_no_posonly)]] | (vararglist_no_posonly) # # It needs to be fully expanded to allow our LL(1) parser to work on it. @@ -117,9 +117,9 @@ while_stmt: 'while' namedexpr_test ':' suite ['else' ':' suite] for_stmt: 'for' exprlist 'in' testlist_star_expr ':' suite ['else' ':' suite] try_stmt: ('try' ':' suite ((except_clause ':' suite)+ - ['else' ':' suite] - ['finally' ':' suite] | - 'finally' ':' suite)) + ['else' ':' suite] + ['finally' ':' suite] | + 'finally' ':' suite)) with_stmt: 'with' asexpr_test (',' asexpr_test)* ':' suite # NB compile.c makes sure that the default except clause is last @@ -176,7 +176,7 @@ testlist: test (',' test)* [','] dictsetmaker: ( ((test ':' asexpr_test | '**' expr) (comp_for | (',' (test ':' asexpr_test | '**' expr))* [','])) | ((test [':=' test] | star_expr) - (comp_for | (',' (test [':=' test] | star_expr))* [','])) ) + (comp_for | (',' (test [':=' test] | star_expr))* [','])) ) classdef: 'class' NAME [typeparams] ['(' [arglist] ')'] ':' suite @@ -193,7 +193,7 @@ argument: ( test [comp_for] | test ':=' test [comp_for] | test 'as' test | test '=' asexpr_test | - '**' test | + '**' test | '*' test ) comp_iter: comp_for | comp_if diff --git a/src/blib2to3/PatternGrammar.txt b/src/blib2to3/PatternGrammar.txt index 36bf8148273..e4b798182d5 100644 --- a/src/blib2to3/PatternGrammar.txt +++ b/src/blib2to3/PatternGrammar.txt @@ -19,7 +19,7 @@ Unit: [NAME '='] ( STRING [Repeater] | NAME [Details] [Repeater] | '(' Alternatives ')' [Repeater] | '[' Alternatives ']' - ) + ) NegatedUnit: 'not' (STRING | NAME [Details] | '(' Alternatives ')') diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index c69fb0a1848..00000000000 --- a/test_requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -coverage >= 5.3 -pre-commit -pytest >= 7 -pytest-xdist >= 3.0.2 -pytest-cov >= 4.1.0 -tox diff --git a/tox.ini b/tox.ini index 3bb08983c28..bf4cfd399be 100644 --- a/tox.ini +++ b/tox.ini @@ -11,8 +11,7 @@ skip_install = True # the `no_jupyter` tests would run with the jupyter extra dependencies installed. # See https://github.com/psf/black/issues/2367. recreate = True -deps = - -r{toxinidir}/test_requirements.txt +dependency_groups = cov-tests commands = pip install -e .[d] coverage erase @@ -27,51 +26,23 @@ commands = coverage report [testenv:{,ci-}pypy3] -setenv = PYTHONPATH = {toxinidir}/src +setenv = + PYTHONPATH = {toxinidir}/src skip_install = True recreate = True -deps = - -r{toxinidir}/test_requirements.txt +dependency_groups = tests commands = pip install -e .[d] - pytest tests \ - --run-optional no_jupyter \ + pytest tests --run-optional no_jupyter \ --numprocesses auto pip install -e .[jupyter] pytest tests --run-optional jupyter \ -m jupyter \ --numprocesses auto -[testenv:{,ci-}311] -setenv = - PYTHONPATH = {toxinidir}/src - AIOHTTP_NO_EXTENSIONS = 1 -skip_install = True -recreate = True -deps = -; We currently need > aiohttp 3.8.1 that is on PyPI for 3.11 - git+https://github.com/aio-libs/aiohttp - -r{toxinidir}/test_requirements.txt -commands = - pip install -e .[d] - coverage erase - pytest tests \ - --run-optional no_jupyter \ - --numprocesses auto \ - --cov {posargs} - pip install -e .[jupyter] - pytest tests --run-optional jupyter \ - -m jupyter \ - --numprocesses auto \ - --cov --cov-append {posargs} - coverage report - [testenv:fuzz] skip_install = True -deps = - -r{toxinidir}/test_requirements.txt - hypothesmith - lark-parser +dependency_groups = fuzz commands = pip install -e .[d] coverage erase @@ -79,16 +50,17 @@ commands = coverage report [testenv:run_self] -setenv = PYTHONPATH = {toxinidir}/src +setenv = + PYTHONPATH = {toxinidir}/src skip_install = True commands = pip install -e . - black --check {toxinidir}/src {toxinidir}/tests {toxinidir}/docs {toxinidir}/scripts + black --check {toxinidir} [testenv:generate_schema] -setenv = PYTHONWARNDEFAULTENCODING = +setenv = + PYTHONWARNDEFAULTENCODING = skip_install = True -deps = commands = pip install -e . python {toxinidir}/scripts/generate_schema.py --outfile {toxinidir}/src/black/resources/black.schema.json