|
| 1 | +--- |
| 2 | +name: sync-async-pattern |
| 3 | +description: > |
| 4 | + Guide for implementing sync/async mirrored code in this project. |
| 5 | + Use when adding new classes, methods, or feature filters that need both sync and async versions, |
| 6 | + or when modifying existing sync code that has an async counterpart in featuremanagement/aio/. |
| 7 | +--- |
| 8 | + |
| 9 | +# Sync/Async Mirroring Pattern |
| 10 | + |
| 11 | +This project maintains parallel sync and async implementations. Every change to sync code in `featuremanagement/` must be mirrored in `featuremanagement/aio/`, and vice versa. |
| 12 | + |
| 13 | +## Directory mapping |
| 14 | + |
| 15 | +| Sync | Async | |
| 16 | +|------|-------| |
| 17 | +| `featuremanagement/_featuremanager.py` | `featuremanagement/aio/_featuremanager.py` | |
| 18 | +| `featuremanagement/_featurefilters.py` | `featuremanagement/aio/_featurefilters.py` | |
| 19 | +| `featuremanagement/_defaultfilters.py` | `featuremanagement/aio/_defaultfilters.py` | |
| 20 | +| `featuremanagement/__init__.py` | `featuremanagement/aio/__init__.py` | |
| 21 | + |
| 22 | +Shared code that does NOT have an async counterpart: |
| 23 | +- `featuremanagement/_featuremanagerbase.py` — base class used by both sync and async `FeatureManager` |
| 24 | +- `featuremanagement/_models/` — data models imported by both |
| 25 | +- `featuremanagement/_time_window_filter/` — time window logic (no I/O, used as-is) |
| 26 | +- `featuremanagement/azuremonitor/` — telemetry (no async version) |
| 27 | + |
| 28 | +## Copyright header |
| 29 | + |
| 30 | +Every source file MUST start with: |
| 31 | + |
| 32 | +```python |
| 33 | +# ------------------------------------------------------------------------ |
| 34 | +# Copyright (c) Microsoft Corporation. All rights reserved. |
| 35 | +# Licensed under the MIT License. See License.txt in the project root for |
| 36 | +# license information. |
| 37 | +# ------------------------------------------------------------------------- |
| 38 | +``` |
| 39 | + |
| 40 | +Followed by a module-level docstring. |
| 41 | + |
| 42 | +## How to convert sync to async |
| 43 | + |
| 44 | +### Classes |
| 45 | + |
| 46 | +- Keep the **same class name** (e.g., both are `FeatureManager`). Users disambiguate by import path. |
| 47 | +- Both sync and async `FeatureManager` inherit from `FeatureManagerBase`. |
| 48 | + |
| 49 | +### Methods |
| 50 | + |
| 51 | +- Add `async` to method definitions: `def evaluate(...)` → `async def evaluate(...)` |
| 52 | +- Add `await` to calls that invoke filters, callbacks, or accessors. |
| 53 | + |
| 54 | +### Default filters (composition pattern) |
| 55 | + |
| 56 | +Async default filters do NOT duplicate logic. They wrap the sync implementation: |
| 57 | + |
| 58 | +```python |
| 59 | +from .._defaultfilters import TimeWindowFilter as SyncTimeWindowFilter |
| 60 | + |
| 61 | +class TimeWindowFilter(FeatureFilter): |
| 62 | + def __init__(self): |
| 63 | + self._filter = SyncTimeWindowFilter() |
| 64 | + |
| 65 | + @FeatureFilter.alias("Microsoft.TimeWindow") |
| 66 | + async def evaluate(self, context, **kwargs): |
| 67 | + return self._filter.evaluate(context, **kwargs) |
| 68 | +``` |
| 69 | + |
| 70 | +Use this pattern for any new filter whose `evaluate` does not perform I/O. |
| 71 | + |
| 72 | +### Callbacks and accessors |
| 73 | + |
| 74 | +The async `FeatureManager` supports BOTH sync and async callbacks. Use `inspect.iscoroutinefunction` to detect and handle both: |
| 75 | + |
| 76 | +```python |
| 77 | +import inspect |
| 78 | + |
| 79 | +if inspect.iscoroutinefunction(self._on_feature_evaluated): |
| 80 | + await self._on_feature_evaluated(result) |
| 81 | +else: |
| 82 | + self._on_feature_evaluated(result) |
| 83 | +``` |
| 84 | + |
| 85 | +### Imports |
| 86 | + |
| 87 | +- Sync files import from `._models`, `._featurefilters`, etc. |
| 88 | +- Async files import from `.._models`, `.._featurefilters`, etc. (one level up from `aio/`). |
| 89 | + |
| 90 | +## `__init__.py` exports |
| 91 | + |
| 92 | +### Sync (`featuremanagement/__init__.py`) |
| 93 | + |
| 94 | +Exports everything: `FeatureManager`, filters, all models, `__version__`, and defines `__all__`. |
| 95 | + |
| 96 | +### Async (`featuremanagement/aio/__init__.py`) |
| 97 | + |
| 98 | +Exports ONLY async-specific classes: `FeatureManager`, `FeatureFilter`, `TimeWindowFilter`, `TargetingFilter`. Does NOT re-export models or `__version__` — users import those from the sync package. |
| 99 | + |
| 100 | +When adding a new public class: |
| 101 | +1. Add to sync `__init__.py` with `__all__` entry. |
| 102 | +2. If it has an async version, add to async `__init__.py` with `__all__` entry. |
| 103 | + |
| 104 | +## Test file naming |
| 105 | + |
| 106 | +| Sync test | Async counterpart | |
| 107 | +|-----------|-------------------| |
| 108 | +| `tests/test_feature_manager.py` | `tests/test_feature_manager_async.py` | |
| 109 | +| `tests/test_feature_variants.py` | `tests/test_feature_variants_async.py` | |
| 110 | +| `tests/test_default_feature_flags.py` | `tests/test_default_feature_flags_async.py` | |
| 111 | + |
| 112 | +- Async test files append `_async` to the sync filename. |
| 113 | +- Async tests use `pytest-asyncio` with `@pytest.mark.asyncio` on test functions. |
| 114 | +- Not every sync test needs an async counterpart (e.g., refresh and telemetry tests are sync-only). |
| 115 | + |
| 116 | +## Checklist for adding new code |
| 117 | + |
| 118 | +1. [ ] Write the sync implementation in `featuremanagement/`. |
| 119 | +2. [ ] Write the async mirror in `featuremanagement/aio/` following the patterns above. |
| 120 | +3. [ ] Export from both `__init__.py` files if public. |
| 121 | +4. [ ] Write sync tests in `tests/test_*.py`. |
| 122 | +5. [ ] Write async tests in `tests/test_*_async.py`. |
| 123 | +6. [ ] Run all validation: `pylint featuremanagement`, `black featuremanagement`, `mypy featuremanagement`, `pytest tests`. |
0 commit comments