@@ -257,6 +257,139 @@ EOF
257257) "
258258```
259259
260+ ## Asyncio Development
261+
262+ ### Architecture
263+
264+ libvcs async support is organized in ` _async/ ` subpackages:
265+
266+ ```
267+ libvcs/
268+ ├── _internal/
269+ │ ├── subprocess.py # Sync subprocess wrapper
270+ │ └── async_subprocess.py # Async subprocess wrapper
271+ ├── cmd/
272+ │ ├── git.py # Git (sync)
273+ │ └── _async/git.py # AsyncGit
274+ ├── sync/
275+ │ ├── git.py # GitSync (sync)
276+ │ └── _async/git.py # AsyncGitSync
277+ ```
278+
279+ ### Async Subprocess Patterns
280+
281+ ** Always use ` communicate() ` for subprocess I/O:**
282+ ``` python
283+ proc = await asyncio.create_subprocess_shell(... )
284+ stdout, stderr = await proc.communicate() # Prevents deadlocks
285+ ```
286+
287+ ** Use ` asyncio.timeout() ` for timeouts:**
288+ ``` python
289+ async with asyncio.timeout(300 ):
290+ stdout, stderr = await proc.communicate()
291+ ```
292+
293+ ** Handle BrokenPipeError gracefully:**
294+ ``` python
295+ try :
296+ proc.stdin.write(data)
297+ await proc.stdin.drain()
298+ except BrokenPipeError :
299+ pass # Process already exited - expected behavior
300+ ```
301+
302+ ### Async API Conventions
303+
304+ - ** Class naming** : Use ` Async ` prefix: ` AsyncGit ` , ` AsyncGitSync `
305+ - ** Callbacks** : Async APIs accept only async callbacks (no union types)
306+ - ** Shared logic** : Extract argument-building to sync functions, share with async
307+
308+ ``` python
309+ # Shared argument building (sync)
310+ def build_clone_args (url : str , depth : int | None = None ) -> list[str ]:
311+ args = [" clone" , url]
312+ if depth:
313+ args.extend([" --depth" , str (depth)])
314+ return args
315+
316+ # Async method uses shared logic
317+ async def clone (self , url : str , depth : int | None = None ) -> str :
318+ args = build_clone_args(url, depth)
319+ return await self .run(args)
320+ ```
321+
322+ ### Async Testing
323+
324+ ** pytest configuration:**
325+ ``` toml
326+ [tool .pytest .ini_options ]
327+ asyncio_mode = " strict"
328+ asyncio_default_fixture_loop_scope = " function"
329+ ```
330+
331+ ** Async fixture pattern:**
332+ ``` python
333+ @pytest_asyncio.fixture (loop_scope = " function" )
334+ async def async_git_repo (tmp_path : Path) -> t.AsyncGenerator[AsyncGitSync, None ]:
335+ repo = AsyncGitSync(url = " ..." , path = tmp_path / " repo" )
336+ await repo.obtain()
337+ yield repo
338+ ```
339+
340+ ** Parametrized async tests:**
341+ ``` python
342+ class CloneFixture (t .NamedTuple ):
343+ test_id: str
344+ clone_kwargs: dict[str , t.Any]
345+ expected: list[str ]
346+
347+ CLONE_FIXTURES = [
348+ CloneFixture(" basic" , {}, [" .git" ]),
349+ CloneFixture(" shallow" , {" depth" : 1 }, [" .git" ]),
350+ ]
351+
352+ @pytest.mark.parametrize (
353+ list (CloneFixture._fields),
354+ CLONE_FIXTURES ,
355+ ids = [f.test_id for f in CLONE_FIXTURES ],
356+ )
357+ @pytest.mark.asyncio
358+ async def test_clone (test_id : str , clone_kwargs : dict , expected : list ) -> None :
359+ ...
360+ ```
361+
362+ ### Async Anti-Patterns
363+
364+ ** DON'T poll returncode:**
365+ ``` python
366+ # WRONG
367+ while proc.returncode is None :
368+ await asyncio.sleep(0.1 )
369+
370+ # RIGHT
371+ await proc.wait()
372+ ```
373+
374+ ** DON'T mix blocking calls in async code:**
375+ ``` python
376+ # WRONG
377+ async def bad ():
378+ subprocess.run([" git" , " clone" , url]) # Blocks event loop!
379+
380+ # RIGHT
381+ async def good ():
382+ proc = await asyncio.create_subprocess_shell(... )
383+ await proc.wait()
384+ ```
385+
386+ ** DON'T close the event loop in tests:**
387+ ``` python
388+ # WRONG - breaks pytest-asyncio cleanup
389+ loop = asyncio.get_running_loop()
390+ loop.close()
391+ ```
392+
260393## Debugging Tips
261394
262395When stuck in debugging loops:
0 commit comments