-
Notifications
You must be signed in to change notification settings - Fork 128
try claude - add a basic config and play around with some quality of life changes i was offputting #584
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
try claude - add a basic config and play around with some quality of life changes i was offputting #584
Changes from all commits
5a292b3
d203f70
82aee10
a85f465
d9c4e2d
6a505ec
a6c8a9c
3d717ed
ac8e4dd
9533c42
ccd761d
f46834b
b69db1f
9096292
3721d8d
6c75844
ad71541
fc3270e
e754127
0c956fd
44f68a0
cbe879e
59b5453
a4772bb
cb748f6
6570d43
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"permissions": { | ||
"allow": [ | ||
"Bash(uv run pytest:*)", | ||
"Bash(git add:*)", | ||
"Bash(uv run mypy:*)", | ||
"Bash(uv run pre-commit:*)" | ||
|
||
], | ||
"deny": [] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,3 +67,6 @@ pip-wheel-metadata/ | |
|
||
# pytest-benchmark | ||
.benchmarks/ | ||
|
||
# Claude Code local settings | ||
.claude/settings.local.json |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# CLAUDE.md | ||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
||
## Project Overview | ||
|
||
Pluggy is a minimalist production-ready plugin system that serves as the core framework for pytest, datasette and devpi. | ||
It provides hook specification and implementation mechanisms through a plugin manager system. | ||
|
||
## Development Commands | ||
|
||
### Testing | ||
- `uv run pytest` - Run all tests, prefer runnign all tests to quickly get feedback | ||
- `uv run pytest testing/benchmark.py` runs the benchmark tests | ||
|
||
### Code Quality | ||
- `uv run pre-commit run -a` - Run all pre-commit hooks - gives linting and typing errors + corrects files | ||
- reread files that get fixed by pre-commit | ||
|
||
|
||
## Development process | ||
|
||
- always read `src/pluggy/*.py` to get a full picture | ||
- consider backward compatibility | ||
- always run all tests | ||
- always run pre-commit before try to commit | ||
- prefer running full pre-commit over ruff/mypy alone | ||
|
||
|
||
|
||
## Core Architecture | ||
|
||
### Main Components | ||
|
||
- **PluginManager** (`src/pluggy/_manager.py`): Central registry that manages plugins and coordinates hook calls | ||
- **HookCaller** (`src/pluggy/_hooks.py`): Executes hook implementations with proper argument binding | ||
- **HookImpl/HookSpec** (`src/pluggy/_hooks.py`): Represent hook implementations and specifications | ||
- **Result** (`src/pluggy/_result.py`): Handles hook call results and exception propagation | ||
- **Multicall** (`src/pluggy/_callers.py`): Core execution engine for calling multiple hook implementations | ||
|
||
### Package Structure | ||
- `src/pluggy/` - Main package source | ||
- `testing/` - Test suite using pytest | ||
- `docs/` - Sphinx documentation and examples | ||
- `changelog/` - Towncrier fragments for changelog generation | ||
|
||
## Configuration Files | ||
- `pyproject.toml` - Project metadata, build system, tool configuration (ruff, mypy, setuptools-scm) | ||
- `tox.ini` - Multi-environment testing configuration | ||
- `.pre-commit-config.yaml` - Code quality automation (ruff, mypy, flake8, etc.) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
""" | ||
Async support for pluggy using greenlets. | ||
|
||
This module provides async functionality for pluggy, allowing hook implementations | ||
to return awaitable objects that are automatically awaited when running in an | ||
async context. | ||
""" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @simonw i'd appreciate early feedback on this wrt utility for datasette its not the deep integration like I wanted/hoped to implement at first however its a directly working integration that doesn’t break on color mixing - but it does have the caveat of breaking never officieally supported threading patterns some people use i currently intend to clean up history and then have this land and release |
||
from __future__ import annotations | ||
|
||
from collections.abc import AsyncGenerator | ||
from collections.abc import Awaitable | ||
from collections.abc import Callable | ||
from collections.abc import Generator | ||
from typing import Any | ||
from typing import TYPE_CHECKING | ||
from typing import TypeVar | ||
|
||
|
||
_T = TypeVar("_T") | ||
_Y = TypeVar("_Y") | ||
_S = TypeVar("_S") | ||
_R = TypeVar("_R") | ||
|
||
if TYPE_CHECKING: | ||
import greenlet | ||
|
||
|
||
def make_greenlet(func: Callable[..., Any]) -> greenlet.greenlet: | ||
"""indirection to defer import""" | ||
import greenlet | ||
|
||
return greenlet.greenlet(func) | ||
|
||
|
||
class Submitter: | ||
# practice we expect te root greenlet to be the key submitter | ||
_active_submitter: greenlet.greenlet | None | ||
|
||
def __init__(self) -> None: | ||
self._active_submitter = None | ||
|
||
def __repr__(self) -> str: | ||
return f"<Submitter active={self._active_submitter is not None}>" | ||
|
||
def maybe_submit(self, coro: Awaitable[_T]) -> _T | Awaitable[_T]: | ||
"""await an awaitable if active, else return it | ||
|
||
this enables backward compatibility for datasette | ||
and https://simonwillison.net/2020/Sep/2/await-me-maybe/ | ||
""" | ||
active = self._active_submitter | ||
if active is not None: | ||
# We're in a greenlet context, switch with the awaitable | ||
# The parent will await it and switch back with the result | ||
res: _T = active.switch(coro) | ||
return res | ||
else: | ||
return coro | ||
|
||
def require_await(self, coro: Awaitable[_T]) -> _T: | ||
"""await an awaitable, raising an error if not in async context | ||
|
||
this is for cases where async context is required | ||
""" | ||
active = self._active_submitter | ||
if active is not None: | ||
# Switch to the active submitter greenlet with the awaitable | ||
# The active submitter will await it and switch back with the result | ||
res: _T = active.switch(coro) | ||
return res | ||
else: | ||
raise RuntimeError("require_await called outside of async context") | ||
|
||
async def run(self, sync_func: Callable[[], _T]) -> _T: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry if this question is obvious—I don’t have much experience with greenlets. Isn’t the purpose of using If that’s correct, what does From my (admittedly limited) understanding, to support async hooks in async def run_async2(self, func: Callable[[], _T]) -> _T:
results = func()
for index, result in enumerate(results):
if inspect.isawaitable(result):
result = await result
results[index] = result
return results If we replace There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Greenlet allows to switch stacks Which allows to switch from 10 sync calls in back to a async function that can await a awaitable There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The run function will pass into asyncio trio or anyio. Then it will switch back into the sync function And now any sync call below can send a sync call to the outer async function There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, @RonnyPfannschmidt, but it’s still not clear to me how In other words, what does If not, we should create a test case that only works with |
||
"""Run a synchronous function with async support.""" | ||
try: | ||
import greenlet | ||
except ImportError: | ||
raise RuntimeError("greenlet is required for async support") | ||
|
||
if self._active_submitter is not None: | ||
raise RuntimeError("Submitter is already active") | ||
|
||
# Set the current greenlet as the main async context | ||
main_greenlet = greenlet.getcurrent() | ||
result: _T | None = None | ||
exception: BaseException | None = None | ||
|
||
def greenlet_func() -> None: | ||
nonlocal result, exception | ||
try: | ||
result = sync_func() | ||
except BaseException as e: | ||
exception = e | ||
|
||
# Create the worker greenlet | ||
worker_greenlet = greenlet.greenlet(greenlet_func) | ||
# Set the active submitter to the main greenlet so maybe_submit can switch back | ||
self._active_submitter = main_greenlet | ||
|
||
try: | ||
# Switch to the worker greenlet and handle any awaitables it passes back | ||
awaitable = worker_greenlet.switch() | ||
while awaitable is not None: | ||
# Await the awaitable and send the result back to the greenlet | ||
awaited_result = await awaitable | ||
awaitable = worker_greenlet.switch(awaited_result) | ||
except Exception as e: | ||
# If something goes wrong, try to send the exception to the greenlet | ||
try: | ||
worker_greenlet.throw(e) | ||
except BaseException as inner_e: | ||
exception = inner_e | ||
finally: | ||
self._active_submitter = None | ||
|
||
if exception is not None: | ||
raise exception | ||
if result is None: | ||
raise RuntimeError("Function completed without setting result") | ||
return result | ||
|
||
|
||
def async_generator_to_sync( | ||
async_gen: AsyncGenerator[_Y, _S], submitter: Submitter | ||
) -> Generator[_Y, _S, None]: | ||
"""Convert an async generator to a sync generator using a Submitter. | ||
|
||
This helper allows wrapper implementations to use async generators while | ||
maintaining compatibility with the sync generator interface expected by | ||
the hook system. | ||
|
||
Args: | ||
async_gen: The async generator to convert | ||
submitter: The Submitter to use for awaiting async operations | ||
|
||
Yields: | ||
Values from the async generator | ||
|
||
Returns: | ||
None (async generators don't return values) | ||
|
||
Example: | ||
async def my_async_wrapper(): | ||
yield # Setup phase | ||
result = await some_async_operation() | ||
|
||
# In a wrapper hook implementation: | ||
def my_wrapper_hook(): | ||
async_gen = my_async_wrapper() | ||
gen = async_generator_to_sync(async_gen, submitter) | ||
try: | ||
while True: | ||
value = next(gen) | ||
yield value | ||
except StopIteration: | ||
return | ||
""" | ||
try: | ||
# Start the async generator | ||
value = submitter.require_await(async_gen.__anext__()) | ||
|
||
while True: | ||
try: | ||
# Yield the value and get the sent value | ||
sent_value = yield value | ||
|
||
# Send the value to the async generator and get the next value | ||
try: | ||
value = submitter.require_await(async_gen.asend(sent_value)) | ||
except StopAsyncIteration: | ||
# Async generator completed | ||
return | ||
|
||
except GeneratorExit: | ||
# Generator is being closed, close the async generator | ||
try: | ||
submitter.require_await(async_gen.aclose()) | ||
except StopAsyncIteration: | ||
pass | ||
raise | ||
|
||
except BaseException as exc: | ||
# Exception was thrown into the generator, | ||
# throw it into the async generator | ||
try: | ||
value = submitter.require_await(async_gen.athrow(exc)) | ||
except StopAsyncIteration: | ||
# Async generator completed | ||
return | ||
except StopIteration as sync_stop_exc: | ||
# Re-raise StopIteration as it was passed through | ||
raise sync_stop_exc | ||
|
||
except StopAsyncIteration: | ||
# Async generator completed normally | ||
return |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please update
docs/index.rst
,Development
section, about usinguv
.