Skip to content

feat(pywrangler): honor [tool.uv.sources] and [tool.uv.workspace] via uv export#107

Open
LuisDuarte1 wants to merge 1 commit into
cloudflare:mainfrom
LuisDuarte1:honor-uv-workspace-and-sources
Open

feat(pywrangler): honor [tool.uv.sources] and [tool.uv.workspace] via uv export#107
LuisDuarte1 wants to merge 1 commit into
cloudflare:mainfrom
LuisDuarte1:honor-uv-workspace-and-sources

Conversation

@LuisDuarte1
Copy link
Copy Markdown

When the project's pyproject.toml has either [tool.uv.sources] or [tool.uv.workspace], parse_requirements now resolves dependencies via uv export --no-dev --no-editable --no-hashes --no-emit-project instead of reading [project.dependencies] directly. This honors local-path sources and workspace members, so consumers can depend on in-repo Python packages without having to publish them to PyPI or pre-build wheels into a find-links directory.

Workspace-relative paths emitted by uv export (e.g. ./packages/foo) are rewritten to absolute paths in the requirements file so the inner uv pip install -r resolves them regardless of cwd.

_install_requirements_to_vendor drops --no-build when any requirement is a local path, so pure-Python workspace members can be built on the fly into a wheel and vendored into python_modules/. Non-workspace deps continue to be resolved from the Pyodide wheel index and --no-build is preserved in their case.

Behavior is unchanged for projects that don't use sources or workspaces.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 18, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@LuisDuarte1
Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

… uv export

When the project's pyproject.toml has either `[tool.uv.sources]` or
`[tool.uv.workspace]`, `parse_requirements` now resolves dependencies via
`uv export --no-dev --no-editable --no-hashes --no-emit-project` instead of
reading `[project.dependencies]` directly. This honors local-path sources
and workspace members, so consumers can depend on in-repo Python packages
without having to publish them to PyPI or pre-build wheels into a
`find-links` directory.

Workspace-relative paths emitted by `uv export` (e.g. `./packages/foo`) are
rewritten to absolute paths in the requirements file so the inner
`uv pip install -r` resolves them regardless of cwd.

`_install_requirements_to_vendor` drops `--no-build` when any requirement
is a local path, so pure-Python workspace members can be built on the fly
into a wheel and vendored into `python_modules/`. Non-workspace deps
continue to be resolved from the Pyodide wheel index and `--no-build` is
preserved in their case.

Behavior is unchanged for projects that don't use sources or workspaces.
Comment on lines +284 to +290
# `--no-build-package` for each *remote* dep instead.
local_paths = {
Path(r).resolve()
for r in requirements
if Path(r).is_absolute() and Path(r).exists()
}
if local_paths:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is somewhat tricky part. We don't want to allow users from building non-pure python wheels as they will end up having a native (linux, macos) wheel which cannot run in the worker environment.

But I do agree that we need a way to support local packages for development/workspace purpose. I think we should find a way to combine features such as --no-build, --no-source, --no-build-package to make it work properly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a draft PR for this but didn't make it work (#81)

LuisDuarte1 added a commit to LuisDuarte1/dynamic-workflows-fork that referenced this pull request May 19, 2026
…mple

Adds a Python port of @cloudflare/dynamic-workflows alongside the existing
JS package, and an interactive browser playground example mirroring
examples/basic.

`packages/dynamic-workflows-py/`:

  * Pure-Python `_core` module exporting the envelope helpers
    (`_wrap_params`, `_unwrap_params`, `MissingDispatcherMetadataError`),
    `dispatcher_binding_impl`, and `dispatch_workflow_core`. No `js` /
    `workers` / `pyodide.ffi` imports so the helpers are host-testable.
  * `_workerd` module with the workerd/Pyodide-bound layer:
    `DynamicWorkflowBinding` (a single `WorkerEntrypoint` subclass minted
    by `wrap_workflow_binding`), `create_dynamic_workflow_entrypoint`,
    `dispatch_workflow`, and `WrappedWorkflow` / `WrappedInstance` tenant
    facades. `create()` returns a plain JS object literal
    `{id, status, pause, resume, terminate, restart, sendEvent}` with
    methods bound to the underlying JS WorkflowInstance via
    `Function.prototype.bind` — sync `.id`, RPC-callable method
    handles, no second WorkerEntrypoint class, no factory ceremony for
    instance handles.
  * `__init__.py` falls back to the `_core`-only surface when imported
    outside the Pyodide runtime, so host pytest doesn't need import shims.
  * 40 host pytest tests covering envelope wrap/unwrap, the binding-impl
    contract (mirrors JS `binding.test.ts`), and the dispatch-core flow
    (mirrors JS `entrypoint.test.ts`).

`examples/python/`:

  * Interactive playground dispatcher with the same UX as
    `examples/basic`: editor + payload + step timeline + status polling.
    SSE log streaming is omitted (no Python streaming-tail story yet);
    progress is driven by polling `/api/status/:runId`.
  * Demonstrates **both** trigger shapes, switchable in the dashboard UI:
      - **tenant**: dispatcher RPCs into the tenant's
        `Default.start_workflow()`, which calls `env.WORKFLOWS.create()`
        from the tenant's own context. Models the multi-tenant SaaS case
        where the tenant code is what triggers workflows.
      - **direct**: dispatcher calls `wrap_workflow_binding(metadata).create()`
        directly. The tenant only needs `TenantWorkflow(WorkflowEntrypoint)`
        — no `Default(WorkerEntrypoint)` required.
  * Per-run tenant source ships with the dispatcher metadata so workflow
    replays survive isolate recycles without a Durable Object.
  * Verified end-to-end against `pywrangler dev`: both modes complete the
    full Dashboard → dispatcher → tenant → @step.do chain.

Workspace plumbing:

  * Repo-root `pyproject.toml` declares a uv workspace with both Python
    packages as members.
  * `examples/python/pyproject.toml` resolves `dynamic-workflows` via
    `[tool.uv.sources] dynamic-workflows = { workspace = true }`. The
    `workers-py` and `workers-runtime-sdk` deps are pinned to a fork
    commit that patches `pywrangler sync` to honor uv workspaces /
    sources (upstream PR cloudflare/workers-py#107). No pre-built
    wheels or `find-links` config needed.
  * `.gitignore` updated with the standard Python / uv / pywrangler /
    pytest / linter artifacts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants