Skip to content

Commit 5fd802f

Browse files
added missing docstrings; fixed type hints; fixed issues detected by pylint; run pre-commit auto refactor
1 parent 9ba89e5 commit 5fd802f

File tree

13 files changed

+158
-72
lines changed

13 files changed

+158
-72
lines changed

src/dbally/collection/collection.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33
import textwrap
44
import time
55
from collections import defaultdict
6-
from typing import Callable, Dict, List, Optional, Type, TypeVar
6+
from typing import Callable, Dict, Iterable, List, Optional, Type, TypeVar
77

88
from dbally.audit.event_handlers.base import EventHandler
99
from dbally.audit.event_tracker import EventTracker
1010
from dbally.audit.events import RequestEnd, RequestStart
1111
from dbally.collection.exceptions import IndexUpdateError, NoViewFoundError
1212
from dbally.collection.results import ExecutionResult
13+
from dbally.context.context import CustomContext
1314
from dbally.llms.base import LLM
1415
from dbally.llms.clients.base import LLMOptions
1516
from dbally.nl_responder.nl_responder import NLResponder
1617
from dbally.similarity.index import AbstractSimilarityIndex
1718
from dbally.view_selection.base import ViewSelector
1819
from dbally.views.base import BaseView, IndexLocation
19-
from dbally.context.context import BaseCallerContext, CustomContextsList
2020

2121

2222
class Collection:
@@ -157,7 +157,7 @@ async def ask(
157157
dry_run: bool = False,
158158
return_natural_response: bool = False,
159159
llm_options: Optional[LLMOptions] = None,
160-
contexts: Optional[CustomContextsList] = None
160+
contexts: Optional[Iterable[CustomContext]] = None,
161161
) -> ExecutionResult:
162162
"""
163163
Ask question in a text form and retrieve the answer based on the available views.
@@ -177,6 +177,8 @@ async def ask(
177177
the natural response will be included in the answer
178178
llm_options: options to use for the LLM client. If provided, these options will be merged with the default
179179
options provided to the LLM client, prioritizing option values other than NOT_GIVEN
180+
contexts: An iterable (typically a list) of context objects, each being an instance of
181+
a subclass of BaseCallerContext. May contain contexts irrelevant for the currently processed query.
180182
181183
Returns:
182184
ExecutionResult object representing the result of the query execution.
@@ -217,7 +219,7 @@ async def ask(
217219
n_retries=self.n_retries,
218220
dry_run=dry_run,
219221
llm_options=llm_options,
220-
contexts=contexts
222+
contexts=contexts,
221223
)
222224
end_time_view = time.monotonic()
223225

src/dbally/context/_utils.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,28 @@
1-
import typing_extensions as type_ext
2-
3-
from typing import Sequence, Tuple, Optional, Type, Any, Union
41
from inspect import isclass
2+
from typing import Any, Optional, Sequence, Tuple, Type, Union
3+
4+
import typing_extensions as type_ext
55

66
from dbally.context.context import BaseCallerContext
77
from dbally.views.exposed_functions import MethodParamWithTyping
88

9+
ContextClass: type_ext.TypeAlias = Optional[Type[BaseCallerContext]]
10+
911

1012
def _extract_params_and_context(
1113
filter_method_: type_ext.Callable, hidden_args: Sequence[str]
12-
) -> Tuple[Sequence[MethodParamWithTyping], Optional[Type[BaseCallerContext]]]:
14+
) -> Tuple[Sequence[MethodParamWithTyping], ContextClass]:
1315
"""
1416
Processes the MethodsBaseView filter method signauture to extract the args and type hints in the desired format.
1517
Context claases are getting excluded the returned MethodParamWithTyping list. Only the first BaseCallerContext
1618
class is returned.
1719
1820
Args:
1921
filter_method_: MethodsBaseView filter method (annotated with @decorators.view_filter() decorator)
22+
hidden_args: method arguments that should not be extracted
2023
2124
Returns:
22-
A tuple. The first field contains the list of arguments, each encapsulated as MethodParamWithTyping.
25+
The first field contains the list of arguments, each encapsulated as MethodParamWithTyping.
2326
The 2nd is the BaseCallerContext subclass provided for this filter, or None if no context specified.
2427
"""
2528

@@ -52,6 +55,16 @@ class is returned.
5255

5356

5457
def _does_arg_allow_context(arg: MethodParamWithTyping) -> bool:
58+
"""
59+
Verifies whether a method argument allows contextualization based on the type hints attached to a method signature.
60+
61+
Args:
62+
arg: MethodParamWithTyping container preserving information about the method argument
63+
64+
Returns:
65+
Verification result.
66+
"""
67+
5568
if type_ext.get_origin(arg.type) is not Union and not issubclass(arg.type, BaseCallerContext):
5669
return False
5770

src/dbally/context/context.py

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,59 @@
11
import ast
2+
from typing import Iterable
23

3-
from typing import Sequence, TypeVar
4-
from typing_extensions import Self
54
from pydantic import BaseModel
5+
from typing_extensions import Self, TypeAlias
66

77
from dbally.context.exceptions import ContextNotAvailableError
88

9-
10-
CustomContext = TypeVar('CustomContext', bound='BaseCallerContext', covariant=True)
11-
CustomContextsList = Sequence[CustomContext] # TODO confirm the naming
9+
# CustomContext = TypeVar('CustomContext', bound='BaseCallerContext', covariant=True)
10+
CustomContext: TypeAlias = "BaseCallerContext"
1211

1312

1413
class BaseCallerContext(BaseModel):
1514
"""
16-
Base class for contexts that are used to pass additional knowledge about the caller environment to the filters. It is not made abstract for the convinience of IQL parsing.
17-
LLM will always return `BaseCallerContext()` when the context is required and this call will be later substitue by a proper subclass instance selected based on the filter method signature (type hints).
15+
Pydantic-based record class. Base class for contexts that are used to pass additional knowledge about
16+
the caller environment to the filters. It is not made abstract for the convinience of IQL parsing.
17+
LLM will always return `BaseCallerContext()` when the context is required and this call will be
18+
later substituted by a proper subclass instance selected based on the filter method signature (type hints).
1819
"""
1920

2021
@classmethod
21-
def select_context(cls, contexts: CustomContextsList) -> Self:
22+
def select_context(cls, contexts: Iterable[CustomContext]) -> Self:
23+
"""
24+
Typically called from a subclass of BaseCallerContext, selects a member object from `contexts` being
25+
an instance of the same class. Effectively provides a type dispatch mechanism, substituting the context
26+
class by its right instance.
27+
28+
Args:
29+
contexts: A sequence of objects, each being an instance of a different BaseCallerContext subclass.
30+
31+
Returns:
32+
An instance of the same BaseCallerContext subclass this method is caller from.
33+
34+
Raises:
35+
ContextNotAvailableError: If the sequence of context objects passed as argument is empty.
36+
"""
37+
2238
if not contexts:
23-
raise ContextNotAvailableError("The LLM detected that the context is required to execute the query and the filter signature allows contextualization while the context was not provided.")
39+
raise ContextNotAvailableError(
40+
"The LLM detected that the context is required to execute the query +\
41+
and the filter signature allows contextualization while the context was not provided."
42+
)
2443

25-
# this method is called from the subclass of BaseCallerContext pointing the right type of custom context
26-
return next(filter(lambda obj: isinstance(obj, cls), contexts))
44+
# TODO confirm whether it is possible to design a correct type hints here and skipping `type: ignore`
45+
return next(filter(lambda obj: isinstance(obj, cls), contexts)) # type: ignore
2746

2847
@classmethod
2948
def is_context_call(cls, node: ast.expr) -> bool:
49+
"""
50+
Verifies whether an AST node indicates context substitution.
51+
52+
Args:
53+
node: An AST node (expression) to verify:
54+
55+
Returns:
56+
Verification result.
57+
"""
58+
3059
return isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == cls.__name__

src/dbally/context/exceptions.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,22 @@
33

44
class BaseContextException(Exception, ABC):
55
"""
6-
A base exception for all specification context-related exception.
6+
A base (abstract) exception for all specification context-related exception.
77
"""
8-
pass
98

109

1110
class ContextNotAvailableError(Exception):
12-
pass
11+
"""
12+
An exception inheriting from BaseContextException pointining that no sufficient context information
13+
was provided by the user while calling view.ask().
14+
"""
1315

1416

1517
class ContextualisationNotAllowed(Exception):
16-
pass
18+
"""
19+
An exception inheriting from BaseContextException pointining that the filter method signature
20+
does not allow to provide an additional context.
21+
"""
1722

1823

1924
# WORKAROUND - traditional inhertiance syntax is not working in context of abstract Exceptions

src/dbally/iql/_processor.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import ast
2-
3-
from typing import TYPE_CHECKING, Any, List, Optional, Union, Mapping, Type
2+
from typing import Any, Iterable, List, Mapping, Optional, Union
43

54
from dbally.audit.event_tracker import EventTracker
5+
from dbally.context._utils import _does_arg_allow_context
6+
from dbally.context.context import BaseCallerContext, CustomContext
7+
from dbally.context.exceptions import ContextualisationNotAllowed
68
from dbally.iql import syntax
79
from dbally.iql._exceptions import (
810
IQLArgumentParsingError,
@@ -12,35 +14,48 @@
1214
IQLUnsupportedSyntaxError,
1315
)
1416
from dbally.iql._type_validators import validate_arg_type
15-
from dbally.context.context import BaseCallerContext, CustomContextsList
16-
from dbally.context.exceptions import ContextNotAvailableError, ContextualisationNotAllowed
17-
from dbally.context._utils import _extract_params_and_context, _does_arg_allow_context
18-
from dbally.views.exposed_functions import MethodParamWithTyping, ExposedFunction
17+
from dbally.views.exposed_functions import ExposedFunction, MethodParamWithTyping
1918

2019

2120
class IQLProcessor:
2221
"""
2322
Parses IQL string to tree structure.
23+
24+
Attributes:
25+
source: Raw LLM response containing IQL filter calls.
26+
allowed_functions: A mapping (typically a dict) of all filters implemented for a certain View.
27+
contexts: A sequence (typically a list) of context objects, each being an instance of
28+
a subclass of BaseCallerContext. May contain contexts irrelevant for the currently processed query.
2429
"""
30+
2531
source: str
2632
allowed_functions: Mapping[str, "ExposedFunction"]
27-
contexts: CustomContextsList
33+
contexts: Iterable[CustomContext]
2834
_event_tracker: EventTracker
2935

30-
3136
def __init__(
3237
self,
3338
source: str,
34-
allowed_functions: List["ExposedFunction"],
35-
contexts: Optional[CustomContextsList] = None,
36-
event_tracker: Optional[EventTracker] = None
39+
allowed_functions: Iterable[ExposedFunction],
40+
contexts: Optional[Iterable[CustomContext]] = None,
41+
event_tracker: Optional[EventTracker] = None,
3742
) -> None:
43+
"""
44+
IQLProcessor class constructor.
45+
46+
Args:
47+
source: Raw LLM response containing IQL filter calls.
48+
allowed_functions: An interable (typically a list) of all filters implemented for a certain View.
49+
contexts: An iterable (typically a list) of context objects, each being an instance of
50+
a subclass of BaseCallerContext.
51+
even_tracker: An EvenTracker instance.
52+
"""
53+
3854
self.source = source
3955
self.allowed_functions = {func.name: func for func in allowed_functions}
4056
self.contexts = contexts or []
4157
self._event_tracker = event_tracker or EventTracker()
4258

43-
4459
async def process(self) -> syntax.Node:
4560
"""
4661
Process IQL string to root IQL.Node.
@@ -89,7 +104,7 @@ async def _parse_call(self, node: ast.Call) -> syntax.FunctionCall:
89104
if not isinstance(func, ast.Name):
90105
raise IQLUnsupportedSyntaxError(node, self.source, context="FunctionCall")
91106

92-
if func.id not in self.allowed_functions: # TODO add context class constructors to self.allowed_functions
107+
if func.id not in self.allowed_functions:
93108
raise IQLFunctionNotExists(func, self.source)
94109

95110
func_def = self.allowed_functions[func.id]
@@ -117,9 +132,8 @@ def _parse_arg(
117132
self,
118133
arg: ast.expr,
119134
arg_spec: Optional[MethodParamWithTyping] = None,
120-
parent_func_def: Optional[ExposedFunction] = None
135+
parent_func_def: Optional[ExposedFunction] = None,
121136
) -> Any:
122-
123137
if isinstance(arg, ast.List):
124138
return [self._parse_arg(x) for x in arg.elts]
125139

@@ -129,10 +143,16 @@ def _parse_arg(
129143
raise IQLArgumentParsingError(arg, self.source)
130144

131145
if parent_func_def.context_class is None:
132-
raise ContextualisationNotAllowed("The LLM detected that the context is required to execute the query while the filter signature does not allow it at all.")
146+
raise ContextualisationNotAllowed(
147+
"The LLM detected that the context is required +\
148+
to execute the query while the filter signature does not allow it at all."
149+
)
133150

134151
if not _does_arg_allow_context(arg_spec):
135-
raise ContextualisationNotAllowed(f"The LLM detected that the context is required to execute the query while the filter signature does allow it for `{arg_spec.name}` argument.")
152+
raise ContextualisationNotAllowed(
153+
f"The LLM detected that the context is required +\
154+
to execute the query while the filter signature does allow it for `{arg_spec.name}` argument."
155+
)
136156

137157
return parent_func_def.context_class.select_context(self.contexts)
138158

src/dbally/iql/_query.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
from typing import TYPE_CHECKING, List, Optional, Type
1+
from typing import TYPE_CHECKING, Iterable, List, Optional
2+
23
from typing_extensions import Self
34

5+
from dbally.context.context import CustomContext
6+
47
from ..audit.event_tracker import EventTracker
58
from . import syntax
69
from ._processor import IQLProcessor
7-
from dbally.context.context import BaseCallerContext, CustomContextsList
810

911
if TYPE_CHECKING:
1012
from dbally.views.structured import ExposedFunction
@@ -30,7 +32,7 @@ async def parse(
3032
source: str,
3133
allowed_functions: List["ExposedFunction"],
3234
event_tracker: Optional[EventTracker] = None,
33-
contexts: Optional[CustomContextsList] = None
35+
contexts: Optional[Iterable[CustomContext]] = None,
3436
) -> Self:
3537
"""
3638
Parse IQL string to IQLQuery object.
@@ -39,6 +41,8 @@ async def parse(
3941
source: IQL string that needs to be parsed
4042
allowed_functions: list of IQL functions that are allowed for this query
4143
event_tracker: EventTracker object to track events
44+
contexts: An iterable (typically a list) of context objects, each being
45+
an instance of a subclass of BaseCallerContext.
4246
Returns:
4347
IQLQuery object
4448
"""

src/dbally/iql/_type_validators.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import typing_extensions as type_ext
2-
31
from dataclasses import dataclass
42
from typing import _GenericAlias # type: ignore
53
from typing import Any, Callable, Dict, Literal, Optional, Type, Union
64

5+
import typing_extensions as type_ext
6+
77

88
@dataclass
99
class _ValidationResult:
@@ -67,8 +67,10 @@ def validate_arg_type(required_type: Union[Type, _GenericAlias], value: Any) ->
6767
Returns:
6868
_ValidationResult instance
6969
"""
70-
actual_type = type_ext.get_origin(required_type) if isinstance(required_type, _GenericAlias) else required_type # typing.Union is an instance of _GenericAlias
71-
if actual_type is None: # workaround to prevent type warning in line `if isisntanc(value, actual_type):`, TODO check whether necessary
70+
actual_type = type_ext.get_origin(required_type) if isinstance(required_type, _GenericAlias) else required_type
71+
# typing.Union is an instance of _GenericAlias
72+
if actual_type is None:
73+
# workaround to prevent type warning in line `if isisntanc(value, actual_type):`, TODO check whether necessary
7274
actual_type = required_type.__origin__
7375

7476
if actual_type is Union:
@@ -77,7 +79,8 @@ def validate_arg_type(required_type: Union[Type, _GenericAlias], value: Any) ->
7779
if res.valid:
7880
return _ValidationResult(True)
7981

80-
return _ValidationResult(False, f"{repr(value)} is not of type {repr(required_type)}") # typing.Union does not have __name__ property
82+
# typing.Union does not have __name__ property, thus using repr() is necessary
83+
return _ValidationResult(False, f"{repr(value)} is not of type {repr(required_type)}")
8184

8285
custom_type_checker = TYPE_VALIDATOR.get(actual_type)
8386

0 commit comments

Comments
 (0)