diff --git a/.github/workflows/pyrefly-diff-comment.yml b/.github/workflows/pyrefly-diff-comment.yml new file mode 100644 index 0000000000..b9790945f9 --- /dev/null +++ b/.github/workflows/pyrefly-diff-comment.yml @@ -0,0 +1,88 @@ +name: Comment with Pyrefly Diff + +on: + workflow_run: + workflows: + - Pyrefly Diff Check + types: + - completed + +permissions: {} + +jobs: + comment: + name: Comment PR with pyrefly diff + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + issues: write + pull-requests: write + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.pull_requests[0].head.repo.full_name != github.repository }} + steps: + - name: Download pyrefly diff artifact + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }}, + }); + const match = artifacts.data.artifacts.find((artifact) => + artifact.name === 'pyrefly_diff' + ); + if (!match) { + throw new Error('pyrefly_diff artifact not found'); + } + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: match.id, + archive_format: 'zip', + }); + fs.writeFileSync('pyrefly_diff.zip', Buffer.from(download.data)); + + - name: Unzip artifact + run: unzip -o pyrefly_diff.zip + + - name: Post comment + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + let diff = fs.readFileSync('pyrefly_diff.txt', { encoding: 'utf8' }); + let prNumber = null; + try { + prNumber = parseInt(fs.readFileSync('pr_number.txt', { encoding: 'utf8' }), 10); + } catch (err) { + // Fallback to workflow_run payload if artifact is missing or incomplete. + const prs = context.payload.workflow_run.pull_requests || []; + if (prs.length > 0 && prs[0].number) { + prNumber = prs[0].number; + } + } + if (!prNumber) { + throw new Error('PR number not found in artifact or workflow_run payload'); + } + + const MAX_CHARS = 65000; + if (diff.length > MAX_CHARS) { + diff = diff.slice(0, MAX_CHARS); + diff = diff.slice(0, diff.lastIndexOf('\\n')); + diff += '\\n\\n... (truncated) ...'; + } + + const body = diff.trim() + ? `### Pyrefly Diff (base → PR)\\n\\`\\`\\`diff\\n${diff}\\n\\`\\`\\`` + : '### Pyrefly Diff\\nNo changes detected.'; + + await github.rest.issues.createComment({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); diff --git a/.github/workflows/pyrefly-diff.yml b/.github/workflows/pyrefly-diff.yml new file mode 100644 index 0000000000..7dc407c11c --- /dev/null +++ b/.github/workflows/pyrefly-diff.yml @@ -0,0 +1,85 @@ +name: Pyrefly Diff Check + +on: + pull_request: + paths: + - 'api/**/*.py' + +permissions: + contents: read + +jobs: + pyrefly-diff: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Checkout PR branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Python & UV + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --project api --dev + + - name: Run pyrefly on PR branch + run: | + uv run --directory api pyrefly check > /tmp/pyrefly_pr.txt 2>&1 || true + + - name: Checkout base branch + run: git checkout ${{ github.base_ref }} + + - name: Run pyrefly on base branch + run: | + uv run --directory api pyrefly check > /tmp/pyrefly_base.txt 2>&1 || true + + - name: Compute diff + run: | + diff /tmp/pyrefly_base.txt /tmp/pyrefly_pr.txt > pyrefly_diff.txt || true + + - name: Save PR number + run: | + echo ${{ github.event.pull_request.number }} > pr_number.txt + + - name: Upload pyrefly diff + uses: actions/upload-artifact@v4 + with: + name: pyrefly_diff + path: | + pyrefly_diff.txt + pr_number.txt + + - name: Comment PR with pyrefly diff + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + let diff = fs.readFileSync('pyrefly_diff.txt', { encoding: 'utf8' }); + const prNumber = context.payload.pull_request.number; + + const MAX_CHARS = 65000; + if (diff.length > MAX_CHARS) { + diff = diff.slice(0, MAX_CHARS); + diff = diff.slice(0, diff.lastIndexOf('\n')); + diff += '\n\n... (truncated) ...'; + } + + const body = diff.trim() + ? `### Pyrefly Diff (base → PR)\n\`\`\`diff\n${diff}\n\`\`\`` + : '### Pyrefly Diff\nNo changes detected.'; + + await github.rest.issues.createComment({ + issue_number: prNumber, + owner: context.repo.owner, + repo: context.repo.repo, + body, + }); diff --git a/api/extensions/storage/aws_s3_storage.py b/api/extensions/storage/aws_s3_storage.py index 6ab2a95e3c..978f60c9b0 100644 --- a/api/extensions/storage/aws_s3_storage.py +++ b/api/extensions/storage/aws_s3_storage.py @@ -83,5 +83,5 @@ class AwsS3Storage(BaseStorage): except: return False - def delete(self, filename): + def delete(self, filename: str): self.client.delete_object(Bucket=self.bucket_name, Key=filename) diff --git a/api/extensions/storage/azure_blob_storage.py b/api/extensions/storage/azure_blob_storage.py index 4bccaf13c8..f270267ce9 100644 --- a/api/extensions/storage/azure_blob_storage.py +++ b/api/extensions/storage/azure_blob_storage.py @@ -75,7 +75,7 @@ class AzureBlobStorage(BaseStorage): blob = client.get_blob_client(container=self.bucket_name, blob=filename) return blob.exists() - def delete(self, filename): + def delete(self, filename: str): if not self.bucket_name: return diff --git a/api/extensions/storage/baidu_obs_storage.py b/api/extensions/storage/baidu_obs_storage.py index 0bb4648c0a..65345b0e4b 100644 --- a/api/extensions/storage/baidu_obs_storage.py +++ b/api/extensions/storage/baidu_obs_storage.py @@ -53,5 +53,5 @@ class BaiduObsStorage(BaseStorage): return False return True - def delete(self, filename): + def delete(self, filename: str): self.client.delete_object(bucket_name=self.bucket_name, key=filename) diff --git a/api/extensions/storage/base_storage.py b/api/extensions/storage/base_storage.py index 8ddedb24ae..b987c7d253 100644 --- a/api/extensions/storage/base_storage.py +++ b/api/extensions/storage/base_storage.py @@ -28,7 +28,7 @@ class BaseStorage(ABC): raise NotImplementedError @abstractmethod - def delete(self, filename): + def delete(self, filename: str): raise NotImplementedError def scan(self, path, files=True, directories=False) -> list[str]: diff --git a/api/extensions/storage/google_cloud_storage.py b/api/extensions/storage/google_cloud_storage.py index 7f59252f2f..4ad7e2d159 100644 --- a/api/extensions/storage/google_cloud_storage.py +++ b/api/extensions/storage/google_cloud_storage.py @@ -61,6 +61,6 @@ class GoogleCloudStorage(BaseStorage): blob = bucket.blob(filename) return blob.exists() - def delete(self, filename): + def delete(self, filename: str): bucket = self.client.get_bucket(self.bucket_name) bucket.delete_blob(filename) diff --git a/api/extensions/storage/huawei_obs_storage.py b/api/extensions/storage/huawei_obs_storage.py index 72cb59abbe..2e4961bcd5 100644 --- a/api/extensions/storage/huawei_obs_storage.py +++ b/api/extensions/storage/huawei_obs_storage.py @@ -41,7 +41,7 @@ class HuaweiObsStorage(BaseStorage): return False return True - def delete(self, filename): + def delete(self, filename: str): self.client.deleteObject(bucketName=self.bucket_name, objectKey=filename) def _get_meta(self, filename): diff --git a/api/extensions/storage/oracle_oci_storage.py b/api/extensions/storage/oracle_oci_storage.py index c032803045..c7217874e6 100644 --- a/api/extensions/storage/oracle_oci_storage.py +++ b/api/extensions/storage/oracle_oci_storage.py @@ -55,5 +55,5 @@ class OracleOCIStorage(BaseStorage): except: return False - def delete(self, filename): + def delete(self, filename: str): self.client.delete_object(Bucket=self.bucket_name, Key=filename) diff --git a/api/extensions/storage/supabase_storage.py b/api/extensions/storage/supabase_storage.py index 2ca84d4c15..76066e12f5 100644 --- a/api/extensions/storage/supabase_storage.py +++ b/api/extensions/storage/supabase_storage.py @@ -51,7 +51,7 @@ class SupabaseStorage(BaseStorage): return True return False - def delete(self, filename): + def delete(self, filename: str): self.client.storage.from_(self.bucket_name).remove([filename]) def bucket_exists(self): diff --git a/api/extensions/storage/tencent_cos_storage.py b/api/extensions/storage/tencent_cos_storage.py index cf092c6973..c886c82038 100644 --- a/api/extensions/storage/tencent_cos_storage.py +++ b/api/extensions/storage/tencent_cos_storage.py @@ -47,5 +47,5 @@ class TencentCosStorage(BaseStorage): def exists(self, filename): return self.client.object_exists(Bucket=self.bucket_name, Key=filename) - def delete(self, filename): + def delete(self, filename: str): self.client.delete_object(Bucket=self.bucket_name, Key=filename) diff --git a/api/extensions/storage/volcengine_tos_storage.py b/api/extensions/storage/volcengine_tos_storage.py index a44959221f..d19d6b3032 100644 --- a/api/extensions/storage/volcengine_tos_storage.py +++ b/api/extensions/storage/volcengine_tos_storage.py @@ -60,7 +60,7 @@ class VolcengineTosStorage(BaseStorage): return False return True - def delete(self, filename): + def delete(self, filename: str): if not self.bucket_name: return self.client.delete_object(bucket=self.bucket_name, key=filename) diff --git a/api/pyproject.toml b/api/pyproject.toml index 904c4c9ca9..24569504cc 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -176,6 +176,7 @@ dev = [ "sseclient-py>=1.8.0", "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", + "pyrefly>=0.54.0", ] ############################################################ diff --git a/api/uv.lock b/api/uv.lock index 605c62552f..3adfbecaa0 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1471,6 +1471,7 @@ dev = [ { name = "lxml-stubs" }, { name = "mypy" }, { name = "pandas-stubs" }, + { name = "pyrefly" }, { name = "pytest" }, { name = "pytest-benchmark" }, { name = "pytest-cov" }, @@ -1671,6 +1672,7 @@ dev = [ { name = "lxml-stubs", specifier = "~=0.5.1" }, { name = "mypy", specifier = "~=1.17.1" }, { name = "pandas-stubs", specifier = "~=2.2.3" }, + { name = "pyrefly", specifier = ">=0.54.0" }, { name = "pytest", specifier = "~=8.3.2" }, { name = "pytest-benchmark", specifier = "~=4.0.0" }, { name = "pytest-cov", specifier = "~=4.1.0" }, @@ -5107,6 +5109,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, ] +[[package]] +name = "pyrefly" +version = "0.54.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/44/c10b16a302fda90d0af1328f880b232761b510eab546616a7be2fdf35a57/pyrefly-0.54.0.tar.gz", hash = "sha256:c6663be64d492f0d2f2a411ada9f28a6792163d34133639378b7f3dd9a8dca94", size = 5098893, upload-time = "2026-02-23T15:44:35.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/99/8fdcdb4e55f0227fdd9f6abce36b619bab1ecb0662b83b66adc8cba3c788/pyrefly-0.54.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:58a3f092b6dc25ef79b2dc6c69a40f36784ca157c312bfc0baea463926a9db6d", size = 12223973, upload-time = "2026-02-23T15:44:14.278Z" }, + { url = "https://files.pythonhosted.org/packages/90/35/c2aaf87a76003ad27b286594d2e5178f811eaa15bfe3d98dba2b47d56dd1/pyrefly-0.54.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:615081414106dd95873bc39c3a4bed68754c6cc24a8177ac51d22f88f88d3eb3", size = 11785585, upload-time = "2026-02-23T15:44:17.468Z" }, + { url = "https://files.pythonhosted.org/packages/c4/4a/ced02691ed67e5a897714979196f08ad279ec7ec7f63c45e00a75a7f3c0e/pyrefly-0.54.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbcaf20f5fe585079079a95205c1f3cd4542d17228cdf1df560288880623b70", size = 33381977, upload-time = "2026-02-23T15:44:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/72a117ed437c8f6950862181014b41e36f3c3997580e29b772b71e78d587/pyrefly-0.54.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d5da116c0d34acfbd66663addd3ca8aa78a636f6692a66e078126d3620a883", size = 35962821, upload-time = "2026-02-23T15:44:22.357Z" }, + { url = "https://files.pythonhosted.org/packages/85/de/89013f5ae0a35d2b6b01274a92a35ee91431ea001050edf0a16748d39875/pyrefly-0.54.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef3ac27f1a4baaf67aead64287d3163350844794aca6315ad1a9650b16ec26a", size = 38496689, upload-time = "2026-02-23T15:44:25.236Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9a/33b097c7bf498b924742dca32dd5d9c6a3fa6c2b52b63a58eb9e1980ca89/pyrefly-0.54.0-py3-none-win32.whl", hash = "sha256:7d607d72200a8afbd2db10bfefb40160a7a5d709d207161c21649cedd5cfc09a", size = 11295268, upload-time = "2026-02-23T15:44:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/d4/21/9263fd1144d2a3d7342b474f183f7785b3358a1565c864089b780110b933/pyrefly-0.54.0-py3-none-win_amd64.whl", hash = "sha256:fd416f04f89309385696f685bd5c9141011f18c8072f84d31ca20c748546e791", size = 12081810, upload-time = "2026-02-23T15:44:29.461Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5b/fad062a196c064cbc8564de5b2f4d3cb6315f852e3b31e8a1ce74c69a1ea/pyrefly-0.54.0-py3-none-win_arm64.whl", hash = "sha256:f06ab371356c7b1925e0bffe193b738797e71e5dbbff7fb5a13f90ee7521211d", size = 11564930, upload-time = "2026-02-23T15:44:33.053Z" }, +] + [[package]] name = "pytest" version = "8.3.5"