@@ -295,6 +295,139 @@ EOF
295295) "
296296```
297297
298+ ## Asyncio Development
299+
300+ ### Architecture
301+
302+ libvcs async support is organized in ` _async/ ` subpackages:
303+
304+ ```
305+ libvcs/
306+ ├── _internal/
307+ │ ├── subprocess.py # Sync subprocess wrapper
308+ │ └── async_subprocess.py # Async subprocess wrapper
309+ ├── cmd/
310+ │ ├── git.py # Git (sync)
311+ │ └── _async/git.py # AsyncGit
312+ ├── sync/
313+ │ ├── git.py # GitSync (sync)
314+ │ └── _async/git.py # AsyncGitSync
315+ ```
316+
317+ ### Async Subprocess Patterns
318+
319+ ** Always use ` communicate() ` for subprocess I/O:**
320+ ``` python
321+ proc = await asyncio.create_subprocess_shell(... )
322+ stdout, stderr = await proc.communicate() # Prevents deadlocks
323+ ```
324+
325+ ** Use ` asyncio.timeout() ` for timeouts:**
326+ ``` python
327+ async with asyncio.timeout(300 ):
328+ stdout, stderr = await proc.communicate()
329+ ```
330+
331+ ** Handle BrokenPipeError gracefully:**
332+ ``` python
333+ try :
334+ proc.stdin.write(data)
335+ await proc.stdin.drain()
336+ except BrokenPipeError :
337+ pass # Process already exited - expected behavior
338+ ```
339+
340+ ### Async API Conventions
341+
342+ - ** Class naming** : Use ` Async ` prefix: ` AsyncGit ` , ` AsyncGitSync `
343+ - ** Callbacks** : Async APIs accept only async callbacks (no union types)
344+ - ** Shared logic** : Extract argument-building to sync functions, share with async
345+
346+ ``` python
347+ # Shared argument building (sync)
348+ def build_clone_args (url : str , depth : int | None = None ) -> list[str ]:
349+ args = [" clone" , url]
350+ if depth:
351+ args.extend([" --depth" , str (depth)])
352+ return args
353+
354+ # Async method uses shared logic
355+ async def clone (self , url : str , depth : int | None = None ) -> str :
356+ args = build_clone_args(url, depth)
357+ return await self .run(args)
358+ ```
359+
360+ ### Async Testing
361+
362+ ** pytest configuration:**
363+ ``` toml
364+ [tool .pytest .ini_options ]
365+ asyncio_mode = " strict"
366+ asyncio_default_fixture_loop_scope = " function"
367+ ```
368+
369+ ** Async fixture pattern:**
370+ ``` python
371+ @pytest_asyncio.fixture (loop_scope = " function" )
372+ async def async_git_repo (tmp_path : Path) -> t.AsyncGenerator[AsyncGitSync, None ]:
373+ repo = AsyncGitSync(url = " ..." , path = tmp_path / " repo" )
374+ await repo.obtain()
375+ yield repo
376+ ```
377+
378+ ** Parametrized async tests:**
379+ ``` python
380+ class CloneFixture (t .NamedTuple ):
381+ test_id: str
382+ clone_kwargs: dict[str , t.Any]
383+ expected: list[str ]
384+
385+ CLONE_FIXTURES = [
386+ CloneFixture(" basic" , {}, [" .git" ]),
387+ CloneFixture(" shallow" , {" depth" : 1 }, [" .git" ]),
388+ ]
389+
390+ @pytest.mark.parametrize (
391+ list (CloneFixture._fields),
392+ CLONE_FIXTURES ,
393+ ids = [f.test_id for f in CLONE_FIXTURES ],
394+ )
395+ @pytest.mark.asyncio
396+ async def test_clone (test_id : str , clone_kwargs : dict , expected : list ) -> None :
397+ ...
398+ ```
399+
400+ ### Async Anti-Patterns
401+
402+ ** DON'T poll returncode:**
403+ ``` python
404+ # WRONG
405+ while proc.returncode is None :
406+ await asyncio.sleep(0.1 )
407+
408+ # RIGHT
409+ await proc.wait()
410+ ```
411+
412+ ** DON'T mix blocking calls in async code:**
413+ ``` python
414+ # WRONG
415+ async def bad ():
416+ subprocess.run([" git" , " clone" , url]) # Blocks event loop!
417+
418+ # RIGHT
419+ async def good ():
420+ proc = await asyncio.create_subprocess_shell(... )
421+ await proc.wait()
422+ ```
423+
424+ ** DON'T close the event loop in tests:**
425+ ``` python
426+ # WRONG - breaks pytest-asyncio cleanup
427+ loop = asyncio.get_running_loop()
428+ loop.close()
429+ ```
430+
298431## Debugging Tips
299432
300433When stuck in debugging loops:
0 commit comments