diff --git a/.changeset/young-vans-reply.md b/.changeset/young-vans-reply.md new file mode 100644 index 0000000000..11f67514bc --- /dev/null +++ b/.changeset/young-vans-reply.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Fix Python Workers deployment failing on Windows due to path separator handling + +Previously, deploying Python Workers on Windows would fail because the backslash path separator (`\`) was not properly handled, causing the entire full path to be treated as a single filename. The deployment process now correctly normalizes paths to use forward slashes on all platforms. diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index 03900743ea..41362c4771 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -13951,9 +13951,8 @@ export default{ const expectedModules = { "index.py": mainPython, "helper.py": "# Helper module\ndef helper(): pass", - [`python_modules${path.sep}module1.so`]: "binary content for module 1", - [`python_modules${path.sep}module2.py`]: - "# Python vendor module 2\nprint('hello')", + "python_modules/module1.so": "binary content for module 1", + "python_modules/module2.py": "# Python vendor module 2\nprint('hello')", }; mockSubDomainRequest(); @@ -13961,8 +13960,8 @@ export default{ expectedMainModule: "index.py", expectedModules, excludedModules: [ - `python_modules${path.sep}test.pyc`, - `python_modules${path.sep}other${path.sep}test.pyc`, + "python_modules/test.pyc", + "python_modules/other/test.pyc", ], }); diff --git a/packages/wrangler/src/__tests__/find-additional-modules.test.ts b/packages/wrangler/src/__tests__/find-additional-modules.test.ts index 6b882e6cb9..8c92b9b72e 100644 --- a/packages/wrangler/src/__tests__/find-additional-modules.test.ts +++ b/packages/wrangler/src/__tests__/find-additional-modules.test.ts @@ -322,3 +322,68 @@ describe("traverse module graph", () => { ); }); }); + +describe("Python modules", () => { + runInTempDir(); + mockConsoleMethods(); + + it("should find python_modules with forward slashes (for cross-platform deploy)", async () => { + await mkdir("./python_modules/pkg/subpkg", { recursive: true }); + await writeFile("./index.py", "def fetch(request): pass"); + await writeFile("./python_modules/pkg/__init__.py", ""); + await writeFile("./python_modules/pkg/subpkg/mod.py", "x = 1"); + + const modules = await findAdditionalModules( + { + file: path.join(process.cwd(), "./index.py"), + projectRoot: process.cwd(), + configPath: undefined, + format: "modules", + moduleRoot: process.cwd(), + exports: [], + }, + [] + ); + + const pythonModules = modules.filter((m) => + m.name.startsWith("python_modules/") + ); + expect(pythonModules.map((m) => m.name)).toEqual( + expect.arrayContaining([ + "python_modules/pkg/__init__.py", + "python_modules/pkg/subpkg/mod.py", + ]) + ); + // This assertion catches Windows path.join() regression + pythonModules.forEach((m) => { + expect(m.name).not.toContain("\\"); + }); + }); + + it("should exclude files matching pythonModulesExcludes patterns", async () => { + await mkdir("./python_modules", { recursive: true }); + await writeFile("./index.py", "def fetch(request): pass"); + await writeFile("./python_modules/module.py", "x = 1"); + await writeFile("./python_modules/module.pyc", "compiled"); + await writeFile("./python_modules/test_module.py", "def test(): pass"); + + const modules = await findAdditionalModules( + { + file: path.join(process.cwd(), "./index.py"), + projectRoot: process.cwd(), + configPath: undefined, + format: "modules", + moduleRoot: process.cwd(), + exports: [], + }, + [], + false, + ["**/*.pyc", "**/test_*.py"] + ); + + const moduleNames = modules.map((m) => m.name); + expect(moduleNames).toContain("python_modules/module.py"); + expect(moduleNames).not.toContain("python_modules/module.pyc"); + expect(moduleNames).not.toContain("python_modules/test_module.py"); + }); +}); diff --git a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts index a050bee7f0..c58b2f89ed 100644 --- a/packages/wrangler/src/deployment-bundle/find-additional-modules.ts +++ b/packages/wrangler/src/deployment-bundle/find-additional-modules.ts @@ -49,6 +49,16 @@ function isValidPythonPackageName(name: string): boolean { return regex.test(name); } +/** + * Checks if a given module name is a Python vendor module. + * @param moduleName The module name to check + */ +function isPythonVendorModule(moduleName: string): boolean { + // separator should be forward slash, as we always use forward slash for module names + // see `getFiles()` for more details + return moduleName.startsWith("python_modules/"); +} + function removePythonVendorModules( isPythonEntrypoint: boolean, modules: CfModule[] @@ -56,13 +66,11 @@ function removePythonVendorModules( if (!isPythonEntrypoint) { return modules; } - return modules.filter((m) => !m.name.startsWith("python_modules" + path.sep)); + return modules.filter((m) => !isPythonVendorModule(m.name)); } function getPythonVendorModulesSize(modules: CfModule[]): number { - const vendorModules = modules.filter((m) => - m.name.startsWith("python_modules" + path.sep) - ); + const vendorModules = modules.filter((m) => isPythonVendorModule(m.name)); return vendorModules.reduce((total, m) => total + m.content.length, 0); } @@ -187,7 +195,10 @@ export async function findAdditionalModules( return true; // Include this file }) .map((m) => { - const prefixedPath = path.join("python_modules", m.name); + // Always use forward slashes for module names, regardless of platform. + // path.join() uses backslashes on Windows, but module names must use + // forward slashes for proper directory structure when deployed. + const prefixedPath = `python_modules/${m.name}`; return { ...m, name: prefixedPath,