-
Notifications
You must be signed in to change notification settings - Fork 55
LCORE-298: @connection decorator and no-op conversion cache #568
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
LCORE-298: @connection decorator and no-op conversion cache #568
Conversation
WalkthroughAdds a NoopCache backend, updates the cache factory to always return a Cache (including for “noop”), expands allowed cache types and messages, introduces a connection decorator for auto-connect behavior, and adds/updates unit tests across cache, config, and utils. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant App
participant CacheFactory
participant NoopCache
App->>CacheFactory: conversation_cache(config.type = "noop")
Note over CacheFactory: Validate cache type
CacheFactory-->>App: instance of NoopCache
App->>NoopCache: get/insert/delete/list
Note over NoopCache: No-op behavior, input validation only
NoopCache-->>App: Empty results / True as applicable
sequenceDiagram
autonumber
participant Caller
participant Obj as DecoratedObject
participant Backend as Connectable
Caller->>Obj: some_action(...)
alt Not connected
Obj->>Backend: connected()
Backend-->>Obj: False
Obj->>Backend: connect()
else Already connected
Obj->>Backend: connected()
Backend-->>Obj: True
end
Obj->>Obj: execute original method
Obj-->>Caller: result or exception
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests
Tip 👮 Agentic pre-merge checks are now available in preview!Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.
Please see the documentation for more information. Example: reviews:
pre_merge_checks:
custom_checks:
- name: "Undocumented Breaking Changes"
mode: "warning"
instructions: |
Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).Please share your feedback with us on this Discord post. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/models/config.py (1)
492-527: Add NOOP validation to forbid backend configs when type='noop'.Currently, NOOP bypasses checks and allows conflicting backend configs.
Apply this diff:
match self.type: + case constants.CACHE_TYPE_NOOP: + # NOOP must not specify any backend-specific configuration + if any([self.memory, self.sqlite, self.postgres]): + raise ValueError("No backend configuration must be provided for NOOP cache") case constants.CACHE_TYPE_MEMORY: if self.memory is None: raise ValueError("Memory cache is selected, but not configured") # no other DBs configuration allowed if any([self.sqlite, self.postgres]): raise ValueError("Only memory cache config must be provided")
🧹 Nitpick comments (19)
tests/unit/models/config/test_conversation_cache.py (2)
47-51: Fix mismatched subtest label.Second “SQLite cache” label should be “PostgreSQL cache” for clarity.
Apply this diff:
- with subtests.test(msg="SQLite cache"): + with subtests.test(msg="PostgreSQL cache"): with pytest.raises( ValidationError, match="PostgreSQL cache is selected, but not configured" ): _ = ConversationCacheConfiguration(type=constants.CACHE_TYPE_POSTGRES)
24-31: Add a NOOP‑type happy‑path test.Please add a test asserting that type="noop" with no backend config validates, and that type="noop" with any backend config is rejected (after implementing the model validation below).
tests/unit/utils/test_connection_decorator.py (2)
15-23: Tighten test fixture: default False and clearer naming.Initialize as False (not None) and rename the flag to reflect
some_action.Apply this diff:
- def __init__(self, raise_exception_from_foo: bool): + def __init__(self, raise_exception: bool): """Initialize class used to test connection decorator.""" - self._raise_exception_from_foo = raise_exception_from_foo - self._connected = None + self._raise_exception = raise_exception + self._connected = False @@ - if self._raise_exception_from_foo: + if self._raise_exception: raise SomeActionException("some_action error!")And update call sites:
- c = Connectable(raise_exception_from_foo=False) + c = Connectable(raise_exception=False) @@ - c = Connectable(raise_exception_from_foo=True) + c = Connectable(raise_exception=True)
56-59: Assert auto-connect occurs even on exceptions.Strengthens the decorator guarantee.
Apply this diff:
with pytest.raises(SomeActionException, match="some_action error!"): # this method should autoconnect c.some_action() +assert c.connected() is Truesrc/cache/cache_factory.py (1)
21-22: Update Returns doc to include NOOP.Docstring still mentions only SQLite/Postgres/InMemory.
Apply this diff:
- Returns: - An instance of `Cache` (either `SQLiteCache`, `PostgresCache` or `InMemoryCache`). + Returns: + An instance of `Cache` (`NoopCache`, `InMemoryCache`, `SQLiteCache`, or `PostgresCache`).src/cache/noop_cache.py (3)
5-9: Align logging with coding guidelines.Use logger = logging.getLogger(name) instead of a custom getter.
Apply this diff:
-from log import get_logger +import logging @@ -logger = get_logger("cache.noop_cache") +logger = logging.getLogger(__name__)
14-16: Remove empty initializer.Not needed; the base class requires none.
Apply this diff:
- def __init__(self) -> None: - """Create a new instance of no-op cache.""" - + # No initialization required for NoopCache
28-45: Minor: call static validators via class/instance, not super().These are static methods;
self.construct_key(...)is clearer.Apply this diff:
- super().construct_key(user_id, conversation_id, skip_user_id_check) + self.construct_key(user_id, conversation_id, skip_user_id_check)Repeat similarly in delete().
tests/unit/cache/test_cache_factory.py (3)
11-15: Simplify fixture naming (remove alias indirection).The alias via name= creates unnecessary indirection. Prefer naming the function the same as the fixture.
Apply:
-@pytest.fixture(scope="module", name="noop_cache_config_fixture") -def noop_cache_config(): +@pytest.fixture(scope="module") +def noop_cache_config_fixture(): """Fixture containing initialized instance of ConversationCacheConfiguration.""" return ConversationCacheConfiguration(type=CACHE_TYPE_NOOP)
17-23: Construct invalid config without post-init mutation.Mutating a Pydantic model after creation can be brittle if validate_assignment is enabled. Build an invalid instance explicitly without validation.
Apply:
-@pytest.fixture(scope="module", name="invalid_cache_type_config_fixture") -def invalid_cache_type_config(): +@pytest.fixture(scope="module") +def invalid_cache_type_config_fixture(): """Fixture containing instance of ConversationCacheConfiguration with improper settings.""" - c = ConversationCacheConfiguration() - c.type = "foo bar baz" - return c + # Bypass validation intentionally to feed invalid type to the factory + return ConversationCacheConfiguration.model_construct(type="foo bar baz")
25-31: Consider adding a None-type test.Factory behavior when config.type is None isn’t covered.
Example:
+def test_conversation_cache_none_type(): + cfg = ConversationCacheConfiguration() + with pytest.raises(ValueError, match="Invalid cache type"): + CacheFactory.conversation_cache(cfg)tests/unit/cache/test_noop_cache.py (8)
21-26: Fix typo in fixture docstring.“constucted” → “constructed”.
Apply:
- """Fixture with constucted and initialized in memory cache object.""" + """Fixture with constructed and initialized no-op cache object."""
28-31: Optionally assert connectivity contract.Since connect() is a no-op with logging, also assert connected() is True to document the contract.
Apply:
def test_connect(cache_fixture): """Test the behavior of connect method.""" cache_fixture.connect() + assert cache_fixture.connected() is True
64-70: Remove stale comment referencing DEFAULT_USER_UID.The test doesn’t rely on any DEFAULT_USER_UID; the comment can mislead future readers.
Apply:
- # this UUID is different from DEFAULT_USER_UID assert ( cache_fixture.get("ffffffff-ffff-ffff-ffff-ffffffffffff", CONVERSATION_ID) == [] )
146-149: Clarify docstring (this is NoopCache, not in-memory cache).Tighten wording.
Apply:
- """Test if in memory cache always report ready.""" + """Test that NoopCache always reports ready."""
151-161: Fix typos in comments and prefer precise wording.“chararacter” → “character”.
Apply:
- "ffffffff-ffff-ffff-ffff-fffffffffff", # UUID-like string with missing chararacter + "ffffffff-ffff-ffff-ffff-fffffffffff", # UUID-like string with one missing character
33-48: Reduce repetition via parametrization for skip_user_id_check.The two insert_or_append tests differ only by skip_user_id_check. Parametrize instead.
Example:
-def test_insert_or_append(cache_fixture): - """Test the behavior of insert_or_append method.""" - cache_fixture.insert_or_append( - USER_ID, - CONVERSATION_ID, - cache_entry_1, - ) - - -def test_insert_or_append_skip_user_id_check(cache_fixture): - """Test the behavior of insert_or_append method.""" - skip_user_id_check = True - cache_fixture.insert_or_append( - USER_PROVIDED_USER_ID, CONVERSATION_ID, cache_entry_1, skip_user_id_check - ) +@pytest.mark.parametrize( + "user_id,skip", + [(USER_ID, False), (USER_PROVIDED_USER_ID, True)], +) +def test_insert_or_append(cache_fixture, user_id, skip): + """insert_or_append should accept both validated and user-provided IDs.""" + cache_fixture.insert_or_append(user_id, CONVERSATION_ID, cache_entry_1, skip)
185-189: Add missing negative test for insert_or_append with invalid conversation ID.We cover get/delete invalid IDs; add insert_or_append parity.
Append:
+def test_insert_or_append_improper_conversation_id(cache_fixture): + """insert_or_append should validate conversation ID.""" + with pytest.raises(ValueError, match="Invalid conversation ID"): + cache_fixture.insert_or_append(USER_ID, "not-a-uuid", cache_entry_1)
20-26: Consider function-scope fixture explicitly.Default is function scope; make it explicit for readability since state isolation matters for cache tests.
Apply:
-@pytest.fixture(name="cache_fixture") +@pytest.fixture(scope="function", name="cache_fixture") def cache():
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
src/cache/cache_factory.py(2 hunks)src/cache/noop_cache.py(1 hunks)src/constants.py(1 hunks)src/models/config.py(1 hunks)src/utils/connection_decorator.py(1 hunks)tests/unit/cache/__init__.py(1 hunks)tests/unit/cache/test_cache_factory.py(1 hunks)tests/unit/cache/test_noop_cache.py(1 hunks)tests/unit/models/config/test_conversation_cache.py(1 hunks)tests/unit/utils/test_connection_decorator.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
src/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
Use absolute imports for internal modules (e.g., from auth import get_auth_dependency)
Files:
src/utils/connection_decorator.pysrc/cache/cache_factory.pysrc/constants.pysrc/cache/noop_cache.pysrc/models/config.py
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: All modules start with descriptive module-level docstrings explaining purpose
Use logger = logging.getLogger(name) for module logging after import logging
Define type aliases at module level for clarity
All functions require docstrings with brief descriptions
Provide complete type annotations for all function parameters and return types
Use typing_extensions.Self in model validators where appropriate
Use modern union syntax (str | int) and Optional[T] or T | None consistently
Function names use snake_case with descriptive, action-oriented prefixes (get_, validate_, check_)
Avoid in-place parameter modification; return new data structures instead of mutating arguments
Use appropriate logging levels: debug, info, warning, error with clear messages
All classes require descriptive docstrings explaining purpose
Class names use PascalCase with conventional suffixes (Configuration, Error/Exception, Resolver, Interface)
Abstract base classes should use abc.ABC and @AbstractMethod for interfaces
Provide complete type annotations for all class attributes
Follow Google Python docstring style for modules, classes, and functions, including Args, Returns, Raises, Attributes sections as needed
Files:
src/utils/connection_decorator.pysrc/cache/cache_factory.pysrc/constants.pysrc/cache/noop_cache.pytests/unit/cache/test_cache_factory.pytests/unit/models/config/test_conversation_cache.pytests/unit/utils/test_connection_decorator.pysrc/models/config.pytests/unit/cache/__init__.pytests/unit/cache/test_noop_cache.py
src/constants.py
📄 CodeRabbit inference engine (CLAUDE.md)
Keep shared constants in a central src/constants.py with descriptive comments
Files:
src/constants.py
tests/{unit,integration}/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
tests/{unit,integration}/**/*.py: Use pytest for all unit and integration tests
Do not use unittest in tests; pytest is the standard
Files:
tests/unit/cache/test_cache_factory.pytests/unit/models/config/test_conversation_cache.pytests/unit/utils/test_connection_decorator.pytests/unit/cache/__init__.pytests/unit/cache/test_noop_cache.py
tests/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
tests/**/*.py: Use pytest-mock to create AsyncMock objects for async interactions in tests
Use the shared auth mock constant: MOCK_AUTH = ("mock_user_id", "mock_username", False, "mock_token") in tests
Files:
tests/unit/cache/test_cache_factory.pytests/unit/models/config/test_conversation_cache.pytests/unit/utils/test_connection_decorator.pytests/unit/cache/__init__.pytests/unit/cache/test_noop_cache.py
src/{models/config.py,configuration.py}
📄 CodeRabbit inference engine (CLAUDE.md)
src/{models/config.py,configuration.py}: All configuration uses Pydantic models extending ConfigurationBase
Configuration base models must set model_config with extra="forbid" to reject unknown fields
Files:
src/models/config.py
src/{models/**/*.py,configuration.py}
📄 CodeRabbit inference engine (CLAUDE.md)
src/{models/**/*.py,configuration.py}: Use @field_validator and @model_validator for custom validation in Pydantic models
Use precise type hints in configuration (e.g., Optional[FilePath], PositiveInt, SecretStr)
Files:
src/models/config.py
src/models/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
src/models/**/*.py: Pydantic models: use BaseModel for data models and extend ConfigurationBase for configuration
Use @model_validator and @field_validator for Pydantic model validation
Files:
src/models/config.py
**/__init__.py
📄 CodeRabbit inference engine (CLAUDE.md)
Package init.py files contain brief package descriptions
Files:
tests/unit/cache/__init__.py
🧬 Code graph analysis (6)
src/utils/connection_decorator.py (1)
src/cache/noop_cache.py (2)
connected(21-23)connect(17-19)
src/cache/cache_factory.py (4)
src/cache/noop_cache.py (1)
NoopCache(11-106)src/configuration.py (1)
conversation_cache(147-153)src/models/config.py (2)
config(138-144)ConversationCacheConfiguration(489-527)src/cache/cache.py (1)
Cache(9-110)
src/cache/noop_cache.py (5)
tests/unit/cache/test_noop_cache.py (1)
cache(21-25)src/cache/cache.py (3)
Cache(9-110)construct_key(37-43)_check_user_id(23-28)src/models/cache_entry.py (1)
CacheEntry(6-19)src/log.py (1)
get_logger(7-13)src/utils/connection_decorator.py (1)
connection(6-22)
tests/unit/cache/test_cache_factory.py (3)
src/models/config.py (2)
config(138-144)ConversationCacheConfiguration(489-527)src/cache/cache_factory.py (1)
CacheFactory(13-38)src/cache/noop_cache.py (1)
NoopCache(11-106)
tests/unit/utils/test_connection_decorator.py (1)
src/utils/connection_decorator.py (1)
connection(6-22)
tests/unit/cache/test_noop_cache.py (3)
src/models/cache_entry.py (1)
CacheEntry(6-19)src/cache/noop_cache.py (2)
NoopCache(11-106)initialize_cache(25-26)src/utils/suid.py (1)
get_suid(6-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: build-pr
- GitHub Check: e2e_tests
🔇 Additional comments (7)
tests/unit/cache/__init__.py (1)
1-1: LGTM — module header only.Docstring is fine; no side effects in test package init.
tests/unit/models/config/test_conversation_cache.py (1)
27-29: Good: updated allowed Literal values to include 'noop'.Expectation matches the Pydantic v2 error message.
src/constants.py (1)
66-66: LGTM — NOOP cache type constant added.Consistent with config/model changes.
src/models/config.py (1)
497-507: Consider defaulting type=None to NOOP, or ensure factory handles None.To align with “factory always returns a Cache,” either:
- set
self.type = constants.CACHE_TYPE_NOOPwhen no backend config is provided; or- keep
Noneand rely on factory case None → NoopCache (see factory comment).src/cache/cache_factory.py (1)
24-38: Critical: Factory returns NoopCache for all configured types and doesn't handle None — wire real caches and treat None as NOOP
- Issue: src/cache/cache_factory.py (lines 24–38) currently returns NoopCache() for MEMORY/SQLITE/POSTGRES and will raise if config.type is None; this silently drops conversation history or crashes default configs.
- Action: Replace NoopCache() returns with the concrete implementations (InMemoryCache, SQLiteCache, PostgresCache) and add a None case that returns NoopCache(); confirm actual class names and import paths in the repo and update imports. If implementations are absent, keep unspecified/None as NOOP and update config defaults/docs accordingly.
tests/unit/cache/test_cache_factory.py (2)
25-31: Good positive-path test for NOOP.Covers the new backend and asserts correct type returned.
33-36: Strengthen error assertions and align help text with supported types (include 'noop').
- Update tests/unit/cache/test_cache_factory.py (lines 33–36) to capture the ValueError and assert the message contains "Invalid cache type" and that it lists the supported options: "noop", "memory", "sqlite", "postgres".
- Update src/cache/cache_factory.py to raise ValueError with help text listing those same options (use constants: CACHE_TYPE_NOOP, CACHE_TYPE_MEMORY, CACHE_TYPE_SQLITE, CACHE_TYPE_POSTGRES).
- Repo search returned no match for the factory message; confirm src/cache/cache_factory.py exists and run: rg -nP "Invalid cache type:.*Use" -C2 src/cache/cache_factory.py to verify.
| """Decocator that makes sure the object is 'connected' according to it's connected predicate.""" | ||
|
|
||
| from typing import Any, Callable | ||
|
|
||
|
|
||
| def connection(f: Callable) -> Callable: | ||
| """Decocator that makes sure the object is 'connected' according to it's connected predicate. | ||
|
|
||
| Example: | ||
| ```python | ||
| @connection | ||
| def list_history(self) -> list[str]: | ||
| pass | ||
| ``` | ||
| """ | ||
|
|
||
| def wrapper(connectable: Any, *args: Any, **kwargs: Any) -> Callable: | ||
| if not connectable.connected(): | ||
| connectable.connect() | ||
| return f(connectable, *args, **kwargs) | ||
|
|
||
| return wrapper |
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.
Decorator needs typing, wraps, and typos fixed.
Preserve metadata with wraps, provide precise typing via Protocol/ParamSpec, and fix docstring typos (“Decorator”, “its”).
Apply this diff:
-"""Decocator that makes sure the object is 'connected' according to it's connected predicate."""
-
-from typing import Any, Callable
-
-
-def connection(f: Callable) -> Callable:
- """Decocator that makes sure the object is 'connected' according to it's connected predicate.
-
- Example:
- ```python
- @connection
- def list_history(self) -> list[str]:
- pass
- ```
- """
-
- def wrapper(connectable: Any, *args: Any, **kwargs: Any) -> Callable:
- if not connectable.connected():
- connectable.connect()
- return f(connectable, *args, **kwargs)
-
- return wrapper
+"""Decorator that ensures the object is connected according to its `connected()` predicate."""
+
+from functools import wraps
+from typing import Callable, Protocol, TypeVar
+from typing_extensions import Concatenate, ParamSpec
+
+P = ParamSpec("P")
+R = TypeVar("R")
+
+
+class Connectable(Protocol):
+ """Protocol for objects supporting connection lifecycle."""
+
+ def connected(self) -> bool: ...
+ def connect(self) -> None: ...
+
+
+def connection(
+ f: Callable[Concatenate[Connectable, P], R]
+) -> Callable[Concatenate[Connectable, P], R]:
+ """Ensure the object is connected before calling the wrapped method.
+
+ Args:
+ f: Bound instance method to wrap.
+
+ Returns:
+ A wrapper that auto-connects on demand, then calls the original method.
+ """
+
+ @wraps(f)
+ def wrapper(connectable: Connectable, *args: P.args, **kwargs: P.kwargs) -> R:
+ if not connectable.connected():
+ connectable.connect()
+ return f(connectable, *args, **kwargs)
+
+ return wrapper
Description
LCORE-298: @connection decorator and no-op conversion cache
Type of change
Related Tickets & Documents
Summary by CodeRabbit