From 9069bad2da2d6f9e6153d4e69dca194fee2d9635 Mon Sep 17 00:00:00 2001 From: Jac Fitzgerald Date: Wed, 24 Jun 2026 12:02:57 -0700 Subject: [PATCH] fix: use exclude instead of include in packages.find to fix broken wheel The include=["tableauserverclient*"] glob caused setuptools to strip the tableauserverclient/ prefix from all subpackages, producing a wheel where top_level.txt listed bin/helpers/models/server instead of tableauserverclient. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/publish-pypi.yml | 67 +++++++++++++++++++++----- .github/workflows/pypi-smoke-tests.yml | 2 + pyproject.toml | 7 ++- test/test_packaging.py | 19 ++++++++ 4 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 test/test_packaging.py diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index d6d36f7ba..fe3728b9d 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,9 +1,7 @@ name: Publish to PyPi -# This will build a package with a version set by versioneer from the most recent tag matching v____ -# It will publish to TestPyPi, and to real Pypi *if* run on master where head has a release tag -# For a live run, this should only need to be triggered by a newly published repo release. -# This can also be run manually for testing +# Builds the package, checks wheel structure, publishes to TestPyPI, runs smoke +# tests against TestPyPI, then publishes to real PyPI only if all prior steps pass. on: release: types: [published] @@ -13,8 +11,8 @@ on: - 'v*.*.*' jobs: - build-n-publish: - name: Build dist files for PyPi + build: + name: Build and check wheel runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -22,22 +20,67 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v5 with: - python-version: 3.13 + python-version: '3.13' - name: Build dist files run: | python -m pip install --upgrade pip python -m pip install -e .[test] build python -m build git describe --tag --dirty --always - - - name: Publish distribution 📦 to Test PyPI # always run + - name: Check wheel structure + run: python -m pytest test/test_packaging.py -m packaging -v + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish-test-pypi: + name: Publish to Test PyPI + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to Test PyPI uses: pypa/gh-action-pypi-publish@release/v1 # license BSD-2 with: password: ${{ secrets.TEST_PYPI_API_TOKEN }} repository_url: https://test.pypi.org/legacy/ - - - name: Publish distribution 📦 to PyPI - if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') }} + skip_existing: true + + smoke-test-pypi: + name: Smoke test from Test PyPI + needs: publish-test-pypi + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.x'] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install from Test PyPI + run: | + pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ tableauserverclient + - name: Smoke test + run: | + python -c "import tableauserverclient as TSC; server = TSC.Server('http://example.com', use_server_version=False)" + + publish-pypi: + name: Publish to PyPI + needs: smoke-test-pypi + if: ${{ github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') }} + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 # license BSD-2 with: password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/pypi-smoke-tests.yml b/.github/workflows/pypi-smoke-tests.yml index 45ea94400..a125b4bc5 100644 --- a/.github/workflows/pypi-smoke-tests.yml +++ b/.github/workflows/pypi-smoke-tests.yml @@ -5,6 +5,8 @@ name: Pypi smoke tests on: workflow_dispatch: + release: + types: [published] schedule: - cron: 0 11 * * * # Every day at 11AM UTC (7AM EST) diff --git a/pyproject.toml b/pyproject.toml index 72f0625e4..8134f73f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ tableauserverclient = ["*"] [tool.setuptools.packages.find] where = ["."] -include = ["tableauserverclient*"] +exclude = ["test*", "samples*", "test_e2e*", "docs*", "dist*", "build*"] [tool.setuptools.dynamic] version = {attr = "versioneer.get_version"} @@ -68,7 +68,10 @@ exclude = ['/bin/'] [tool.pytest.ini_options] testpaths = ["test"] addopts = "--junitxml=./test.junit.xml -n auto" -markers = ["e2e: mark test as end-to-end (requires a real Tableau server)"] +markers = [ + "e2e: mark test as end-to-end (requires a real Tableau server)", + "packaging: mark test as requiring a built wheel in dist/", +] [tool.versioneer] VCS = "git" diff --git a/test/test_packaging.py b/test/test_packaging.py new file mode 100644 index 000000000..3674a938b --- /dev/null +++ b/test/test_packaging.py @@ -0,0 +1,19 @@ +import zipfile +from pathlib import Path +import pytest + +pytestmark = pytest.mark.packaging + + +def _find_wheel(): + wheels = list(Path("dist").glob("tableauserverclient-*.whl")) + if not wheels: + pytest.skip("No wheel in dist/ -- run 'python -m build --wheel' first") + return max(wheels, key=lambda p: p.stat().st_mtime) + + +def test_wheel_only_tableauserverclient_at_root(): + with zipfile.ZipFile(_find_wheel()) as whl: + top_dirs = {n.split("/")[0] for n in whl.namelist() if "/" in n} + non_dist_info = {d for d in top_dirs if not d.endswith(".dist-info") and not d.endswith(".data")} + assert non_dist_info == {"tableauserverclient"}, f"Unexpected top-level entries: {non_dist_info}"