pydantic-ai-kernel 1.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydantic_ai_kernel/__init__.py +6 -0
- pydantic_ai_kernel/__main__.py +4 -0
- pydantic_ai_kernel/agent_config.py +330 -0
- pydantic_ai_kernel/boosted_magic.py +256 -0
- pydantic_ai_kernel/config_scheme.json +190 -0
- pydantic_ai_kernel/jupydantic_agent.py +339 -0
- pydantic_ai_kernel/kernel.py +410 -0
- pydantic_ai_kernel/magics/__init__.py +0 -0
- pydantic_ai_kernel/magics/agent_history_magic.py +174 -0
- pydantic_ai_kernel/magics/config_magic.py +172 -0
- pydantic_ai_kernel/magics/forget_magic.py +16 -0
- pydantic_ai_kernel/magics/formatter_magic.py +25 -0
- pydantic_ai_kernel/magics/mcp_magic.py +118 -0
- pydantic_ai_kernel/magics/think_magic.py +26 -0
- pydantic_ai_kernel/magics/tools_magic.py +186 -0
- pydantic_ai_kernel/magics/usage_magic.py +63 -0
- pydantic_ai_kernel/utils.py +78 -0
- pydantic_ai_kernel-1.5.2.data/data/share/jupyter/kernels/pydantic_ai/kernel.json +1 -0
- pydantic_ai_kernel-1.5.2.dist-info/METADATA +292 -0
- pydantic_ai_kernel-1.5.2.dist-info/RECORD +22 -0
- pydantic_ai_kernel-1.5.2.dist-info/WHEEL +4 -0
- pydantic_ai_kernel-1.5.2.dist-info/licenses/LICENSE-2.0.txt +202 -0
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""A wrapper of pydantic-ai in jupyter kernel"""
|
|
2
|
+
|
|
3
|
+
from .kernel import PydanticAIBaseKernel # noqa: F401
|
|
4
|
+
from .jupydantic_agent import JuPydanticAgent
|
|
5
|
+
from .agent_config import AgentConfig, ModelProviderConfig, ModelConfig # noqa: F401
|
|
6
|
+
from .boosted_magic import BoostedMagic, boosted_option, complete_from_list
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
from pydantic_ai import UserError
|
|
2
|
+
from pydantic_ai.models import infer_model, Model
|
|
3
|
+
from pydantic_ai.providers import infer_provider_class, Provider
|
|
4
|
+
from typing import Annotated, Any, Literal, Optional, Dict
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
AllModelKind = Literal[
|
|
10
|
+
"test",
|
|
11
|
+
"openai",
|
|
12
|
+
"azure",
|
|
13
|
+
"deepseek",
|
|
14
|
+
"fireworks",
|
|
15
|
+
"github",
|
|
16
|
+
"grok",
|
|
17
|
+
"heroku",
|
|
18
|
+
"moonshotai",
|
|
19
|
+
"ollama",
|
|
20
|
+
"together",
|
|
21
|
+
"vercel",
|
|
22
|
+
"litellm",
|
|
23
|
+
"nebius",
|
|
24
|
+
"ovhcloud",
|
|
25
|
+
"alibaba",
|
|
26
|
+
"openai-chat",
|
|
27
|
+
"openai-responses",
|
|
28
|
+
"google-gla",
|
|
29
|
+
"google-vertex",
|
|
30
|
+
"google",
|
|
31
|
+
"groq",
|
|
32
|
+
"cohere",
|
|
33
|
+
"mistral",
|
|
34
|
+
"openrouter",
|
|
35
|
+
"anthropic",
|
|
36
|
+
"bedrock",
|
|
37
|
+
"huggingface",
|
|
38
|
+
"cerebras",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def infer_model_type( # noqa: C901
|
|
43
|
+
model_kind: str,
|
|
44
|
+
) -> type[Model]:
|
|
45
|
+
"""Infer the model type from the name.
|
|
46
|
+
Function extracted and modified from pydantic-ai `infer_model`.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
model_kind:
|
|
50
|
+
Kind of model to instantiate. Use the string "test" to instantiate TestModel.
|
|
51
|
+
"""
|
|
52
|
+
if model_kind == "test":
|
|
53
|
+
from pydantic_ai.models.test import TestModel
|
|
54
|
+
|
|
55
|
+
return TestModel
|
|
56
|
+
|
|
57
|
+
if model_kind in (
|
|
58
|
+
"openai",
|
|
59
|
+
"azure",
|
|
60
|
+
"deepseek",
|
|
61
|
+
"fireworks",
|
|
62
|
+
"github",
|
|
63
|
+
"grok",
|
|
64
|
+
"heroku",
|
|
65
|
+
"moonshotai",
|
|
66
|
+
"ollama",
|
|
67
|
+
"together",
|
|
68
|
+
"vercel",
|
|
69
|
+
"litellm",
|
|
70
|
+
"nebius",
|
|
71
|
+
"ovhcloud",
|
|
72
|
+
"alibaba",
|
|
73
|
+
):
|
|
74
|
+
model_kind = "openai-chat"
|
|
75
|
+
elif model_kind in ("google-gla", "google-vertex"):
|
|
76
|
+
model_kind = "google"
|
|
77
|
+
|
|
78
|
+
if model_kind == "openai-chat":
|
|
79
|
+
from pydantic_ai.models.openai import OpenAIChatModel
|
|
80
|
+
|
|
81
|
+
return OpenAIChatModel
|
|
82
|
+
elif model_kind == "openai-responses":
|
|
83
|
+
from pydantic_ai.models.openai import OpenAIResponsesModel
|
|
84
|
+
|
|
85
|
+
return OpenAIResponsesModel
|
|
86
|
+
elif model_kind == "google":
|
|
87
|
+
from pydantic_ai.models.google import GoogleModel
|
|
88
|
+
|
|
89
|
+
return GoogleModel
|
|
90
|
+
elif model_kind == "groq":
|
|
91
|
+
from pydantic_ai.models.groq import GroqModel
|
|
92
|
+
|
|
93
|
+
return GroqModel
|
|
94
|
+
elif model_kind == "cohere":
|
|
95
|
+
from pydantic_ai.models.cohere import CohereModel
|
|
96
|
+
|
|
97
|
+
return CohereModel
|
|
98
|
+
elif model_kind == "mistral":
|
|
99
|
+
from pydantic_ai.models.mistral import MistralModel
|
|
100
|
+
|
|
101
|
+
return MistralModel
|
|
102
|
+
elif model_kind == "openrouter":
|
|
103
|
+
from pydantic_ai.models.openrouter import OpenRouterModel
|
|
104
|
+
|
|
105
|
+
return OpenRouterModel
|
|
106
|
+
elif model_kind == "anthropic":
|
|
107
|
+
from pydantic_ai.models.anthropic import AnthropicModel
|
|
108
|
+
|
|
109
|
+
return AnthropicModel
|
|
110
|
+
elif model_kind == "bedrock":
|
|
111
|
+
from pydantic_ai.models.bedrock import BedrockConverseModel
|
|
112
|
+
|
|
113
|
+
return BedrockConverseModel
|
|
114
|
+
elif model_kind == "huggingface":
|
|
115
|
+
from pydantic_ai.models.huggingface import HuggingFaceModel
|
|
116
|
+
|
|
117
|
+
return HuggingFaceModel
|
|
118
|
+
elif model_kind == "cerebras":
|
|
119
|
+
from pydantic_ai.models.cerebras import CerebrasModel
|
|
120
|
+
|
|
121
|
+
return CerebrasModel
|
|
122
|
+
else:
|
|
123
|
+
raise UserError(f"Unknown model: {model_kind}") # pragma: no cover
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class ModelProviderConfig(BaseModel):
|
|
127
|
+
name: Annotated[
|
|
128
|
+
str,
|
|
129
|
+
Field(
|
|
130
|
+
description="Name of the inference provider. The complete list is the one from pydantic-ai. https://ai.pydantic.dev/api/providers/",
|
|
131
|
+
examples=[
|
|
132
|
+
"ollama",
|
|
133
|
+
"litellm",
|
|
134
|
+
"anthropic",
|
|
135
|
+
"nebius",
|
|
136
|
+
"alibaba",
|
|
137
|
+
"ovhcloud",
|
|
138
|
+
],
|
|
139
|
+
),
|
|
140
|
+
]
|
|
141
|
+
params: Annotated[
|
|
142
|
+
dict,
|
|
143
|
+
Field(
|
|
144
|
+
description="The parameters that will be given to pydantic-ai Provider class. Depends on provider name. Go to : https://ai.pydantic.dev/models/overview/",
|
|
145
|
+
examples=[{"base_url": "http://localhost:11434/v1"}],
|
|
146
|
+
),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def get_provider(self) -> Provider[Any]:
|
|
151
|
+
ProviderConstructor = infer_provider_class(self.name)
|
|
152
|
+
return ProviderConstructor(**self.params)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ModelConfig(BaseModel):
|
|
156
|
+
"""
|
|
157
|
+
Describe a model and its inference provider. Is given to pydantic-ai
|
|
158
|
+
agent.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
model_type: AllModelKind | None = None
|
|
162
|
+
model_name: Annotated[
|
|
163
|
+
str,
|
|
164
|
+
Field(
|
|
165
|
+
description="""Following pydantic-ai implementation, there are two ways
|
|
166
|
+
to write model name :
|
|
167
|
+
- <provider_name>:<model_name>, example 'ollama:qwen3:1.7b
|
|
168
|
+
- <model_name>
|
|
169
|
+
|
|
170
|
+
In the first one, you CAN provide additionnal informations on the provider within
|
|
171
|
+
the field 'provider' below. They will **erase** the ones infered by pydantic-ai.
|
|
172
|
+
A wide range of values is possible here, and you can refer to pydantic-ai documentation :
|
|
173
|
+
https://ai.pydantic.dev/models/overview/
|
|
174
|
+
|
|
175
|
+
In the second one, you MUST provide additionnal informations on the provider within
|
|
176
|
+
the field 'provider' below.""",
|
|
177
|
+
examples=["ollama:qwen3:1.7b", "qwen3.1:1.7b"],
|
|
178
|
+
),
|
|
179
|
+
]
|
|
180
|
+
model_settings: Annotated[
|
|
181
|
+
dict | None,
|
|
182
|
+
Field(
|
|
183
|
+
description="Settings for the model. Depends on model and inference provider. See https://ai.pydantic.dev/api/settings/#pydantic_ai.settings.ModelSettings",
|
|
184
|
+
default=None,
|
|
185
|
+
),
|
|
186
|
+
]
|
|
187
|
+
model_provider: Annotated[
|
|
188
|
+
ModelProviderConfig | None,
|
|
189
|
+
Field(
|
|
190
|
+
description="Description of the inference provider. When specified, inferred information about the provider from model_name will be erased.",
|
|
191
|
+
default=None,
|
|
192
|
+
),
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def get_model(self) -> Model | None:
|
|
197
|
+
if self.model_type is None:
|
|
198
|
+
try:
|
|
199
|
+
return infer_model(self.model_name)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
raise Exception(
|
|
202
|
+
f"Could not access model. See error above or consider adding at least information about the model type in the configuration file. Model types are : {AllModelKind}."
|
|
203
|
+
) from e
|
|
204
|
+
if self.model_provider is None:
|
|
205
|
+
try:
|
|
206
|
+
return infer_model(self.model_name)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
raise Exception(
|
|
209
|
+
"Could not access model. See error above or consider adding at least a model provider in configuration file"
|
|
210
|
+
) from e
|
|
211
|
+
|
|
212
|
+
provider = self.model_provider.get_provider
|
|
213
|
+
ModelConstructor = infer_model_type(self.model_type)
|
|
214
|
+
return ModelConstructor(self.model_name, provider=provider) # pyright: ignore
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class AgentConfig(BaseModel):
|
|
218
|
+
agent_name: Annotated[
|
|
219
|
+
str,
|
|
220
|
+
Field(
|
|
221
|
+
description="The name of the agent within the application - different from model name.",
|
|
222
|
+
examples=["my-agent", "summarizer", "image-generator"],
|
|
223
|
+
),
|
|
224
|
+
]
|
|
225
|
+
system_prompt: Annotated[
|
|
226
|
+
str,
|
|
227
|
+
Field(
|
|
228
|
+
description="The system prompt for this agent, or a path towards a text file containing system prompt.",
|
|
229
|
+
examples=[
|
|
230
|
+
"You are a nice agent who answers nicely to requests of the user",
|
|
231
|
+
"You are a specialist in cooking and are always willing to provide informations on new cooking recipees.",
|
|
232
|
+
"/Users/mgg/Documents/system_prompt.txt",
|
|
233
|
+
"/Users/mgg/Documents/system_prompt.md",
|
|
234
|
+
],
|
|
235
|
+
),
|
|
236
|
+
]
|
|
237
|
+
model: ModelConfig
|
|
238
|
+
mcp_servers: Annotated[
|
|
239
|
+
Optional[Dict[str, Any]],
|
|
240
|
+
Field(
|
|
241
|
+
description="A dictionnary of MCP server configuration to connect to. Keys are MCP server names.",
|
|
242
|
+
examples=[
|
|
243
|
+
{
|
|
244
|
+
"time_mcp_server": {
|
|
245
|
+
"command": "uvx",
|
|
246
|
+
"args": ["mcp-run-python", "stdio"],
|
|
247
|
+
},
|
|
248
|
+
"weather_server": {"command": "python", "args": ["mcp_server.py"]},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"python-runner": {
|
|
252
|
+
"command": "uv",
|
|
253
|
+
"args": ["run", "mcp-run-python", "stdio"],
|
|
254
|
+
},
|
|
255
|
+
"weather": {"command": "python", "args": ["mcp_server.py"]},
|
|
256
|
+
"weather-api": {"url": "http://localhost:3001/sse"},
|
|
257
|
+
"calculator": {"url": "http://localhost:8000/mcp"},
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
),
|
|
261
|
+
] = None
|
|
262
|
+
mcp_servers_user_approval: Annotated[
|
|
263
|
+
Optional[Dict[str, bool | dict]],
|
|
264
|
+
Field(
|
|
265
|
+
description="For each MCP server, information about whether the tool requires user approval or not. Can be either a boolean : True = All tools in the MCP server requires user validation (False = all tools **don't** requires validation). Or can be a dict where keys are tool names, and values boolean. When not specified, a tool needs user validation by default.",
|
|
266
|
+
examples=[
|
|
267
|
+
{
|
|
268
|
+
"python-runner": True,
|
|
269
|
+
"weather-api": {"get_weather": True, "authenticate_to_api": False},
|
|
270
|
+
"calculator": False,
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
),
|
|
274
|
+
] = None
|
|
275
|
+
display_thinking: Annotated[
|
|
276
|
+
bool,
|
|
277
|
+
Field(
|
|
278
|
+
description="Whether or not to display thinking text of the model. Default to true."
|
|
279
|
+
),
|
|
280
|
+
] = True
|
|
281
|
+
formatter: Annotated[
|
|
282
|
+
str,
|
|
283
|
+
Field(
|
|
284
|
+
description="Output formatter for magics (text, terminal, md, json).",
|
|
285
|
+
examples=["text", "terminal", "md", "json"],
|
|
286
|
+
),
|
|
287
|
+
] = "terminal"
|
|
288
|
+
use_widget: Annotated[
|
|
289
|
+
bool,
|
|
290
|
+
Field(
|
|
291
|
+
description="Whether to use ipywidget in frontend for enhanced displaying"
|
|
292
|
+
),
|
|
293
|
+
] = False
|
|
294
|
+
|
|
295
|
+
@field_validator("system_prompt", mode="after")
|
|
296
|
+
@classmethod
|
|
297
|
+
def load_system_prompt(cls, value: str):
|
|
298
|
+
"""
|
|
299
|
+
If system prompt argument is a path, loads it.
|
|
300
|
+
Else, does nothing.
|
|
301
|
+
"""
|
|
302
|
+
if os.path.isfile(value):
|
|
303
|
+
with open(value, "rt") as f:
|
|
304
|
+
o = f.read()
|
|
305
|
+
return o
|
|
306
|
+
return value
|
|
307
|
+
|
|
308
|
+
@field_validator("display_thinking", mode="after")
|
|
309
|
+
@classmethod
|
|
310
|
+
def validate_display_thinking(cls, value):
|
|
311
|
+
# env variable overrides config value
|
|
312
|
+
if os.getenv("PYDANTIC_AI_KERNEL_DISPLAY_THINKING", False):
|
|
313
|
+
return True
|
|
314
|
+
return value
|
|
315
|
+
|
|
316
|
+
@field_validator("formatter", mode="after")
|
|
317
|
+
@classmethod
|
|
318
|
+
def validate_formatter(cls, value):
|
|
319
|
+
# env variable overrides config value; fallback to default if not set
|
|
320
|
+
env = os.getenv("PYDANTIC_AI_KERNEL_FORMATTER", False)
|
|
321
|
+
if env:
|
|
322
|
+
return env
|
|
323
|
+
return value
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
import json
|
|
328
|
+
|
|
329
|
+
with open("config_scheme.json", "wt") as f:
|
|
330
|
+
json.dump(AgentConfig.model_json_schema(), f)
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from metakernel import Magic
|
|
2
|
+
from metakernel.magic import (
|
|
3
|
+
_indent,
|
|
4
|
+
_split_args,
|
|
5
|
+
)
|
|
6
|
+
from typing import Any, TypeVar, Callable
|
|
7
|
+
import inspect
|
|
8
|
+
import traceback
|
|
9
|
+
import argparse
|
|
10
|
+
from statikomand import KomandParser
|
|
11
|
+
|
|
12
|
+
_F = TypeVar("_F", bound=Callable[..., Any])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BoostedMagic(Magic):
|
|
16
|
+
"""
|
|
17
|
+
Subclass of metakernel Magics, that provides additional
|
|
18
|
+
features :
|
|
19
|
+
- completion for options names and values, based
|
|
20
|
+
on custom completer objects
|
|
21
|
+
- parsing based on argparse
|
|
22
|
+
|
|
23
|
+
To implement custom parser for magic line, the get_args
|
|
24
|
+
and call_magic had to be reimplemented.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
28
|
+
super().__init__(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
def get_args(self, mtype: str, name: str, code: str, args: Any) -> Any:
|
|
31
|
+
self.code = code
|
|
32
|
+
old_args = args
|
|
33
|
+
mtype = mtype.replace("sticky", "cell")
|
|
34
|
+
|
|
35
|
+
func = getattr(self, mtype + "_" + name)
|
|
36
|
+
|
|
37
|
+
if "-h" in args or "--help" in args:
|
|
38
|
+
# --help flag of argparse writes to stdout,
|
|
39
|
+
# which break kernels (do_is_complete message is
|
|
40
|
+
# unanswered, which makes the kernel to crash).
|
|
41
|
+
return ["help"], dict(), list()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
args, kwargs = _boosted_parse_args(
|
|
45
|
+
func, args, usage=self.get_help(mtype, name)
|
|
46
|
+
)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
self.kernel.Error(str(e))
|
|
49
|
+
return self
|
|
50
|
+
arg_spec = inspect.getfullargspec(func)
|
|
51
|
+
fargs = arg_spec.args
|
|
52
|
+
if fargs[0] == "self":
|
|
53
|
+
fargs = fargs[1:]
|
|
54
|
+
|
|
55
|
+
fargs = [f for f in fargs if f not in kwargs.keys()]
|
|
56
|
+
if len(args) > len(fargs) and not arg_spec.varargs:
|
|
57
|
+
extra = " ".join(str(s) for s in (args[len(fargs) - 1 :]))
|
|
58
|
+
args = args[: len(fargs) - 1] + [extra]
|
|
59
|
+
|
|
60
|
+
return (args, kwargs, old_args)
|
|
61
|
+
|
|
62
|
+
def call_magic(self, mtype: str, name: str, code: str, args: Any) -> Magic:
|
|
63
|
+
self.code = code
|
|
64
|
+
old_args = args
|
|
65
|
+
mtype = mtype.replace("sticky", "cell")
|
|
66
|
+
|
|
67
|
+
func = getattr(self, mtype + "_" + name)
|
|
68
|
+
try:
|
|
69
|
+
args, kwargs = _boosted_parse_args(
|
|
70
|
+
func, args, usage=self.get_help(mtype, name)
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
self.kernel.Error(str(e))
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
arg_spec = inspect.getfullargspec(func)
|
|
77
|
+
fargs = arg_spec.args
|
|
78
|
+
if fargs[0] == "self":
|
|
79
|
+
fargs = fargs[1:]
|
|
80
|
+
|
|
81
|
+
fargs = [f for f in fargs if f not in kwargs.keys()]
|
|
82
|
+
if len(args) > len(fargs) and not arg_spec.varargs:
|
|
83
|
+
extra = " ".join(str(s) for s in (args[len(fargs) - 1 :]))
|
|
84
|
+
args = args[: len(fargs) - 1] + [extra]
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
try:
|
|
88
|
+
func(*args, **kwargs)
|
|
89
|
+
except TypeError:
|
|
90
|
+
func(old_args)
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
msg = f"Error in calling magic '{name}' on {mtype}:\n {exc!s}\n args: {args}\n kwargs: {kwargs}"
|
|
93
|
+
self.kernel.Error(msg)
|
|
94
|
+
self.kernel.Error(traceback.format_exc())
|
|
95
|
+
self.kernel.Error(self.get_help(mtype, name))
|
|
96
|
+
# return dummy magic to end processing:
|
|
97
|
+
return Magic(self.kernel)
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def get_completions(self, info: dict[str, Any]) -> list[str]:
|
|
101
|
+
"""
|
|
102
|
+
Uses statikomand argparse subclass to complete the current magic.
|
|
103
|
+
"""
|
|
104
|
+
self.kernel.log.info(f"Info boosted magic : {info}")
|
|
105
|
+
func = None
|
|
106
|
+
for each_attr in dir(self):
|
|
107
|
+
if each_attr.startswith("line"):
|
|
108
|
+
func = getattr(self, each_attr)
|
|
109
|
+
break
|
|
110
|
+
# for now, info does not contains magic type
|
|
111
|
+
# if each_attr.startswith("cell"):
|
|
112
|
+
# func = getattr(self, each_attr)
|
|
113
|
+
# break
|
|
114
|
+
if func is None:
|
|
115
|
+
self.kernel.log.warning("Could not find any magic to complete.")
|
|
116
|
+
return []
|
|
117
|
+
if not getattr(func, "has_boosted_options", False):
|
|
118
|
+
self.kernel.log.debug(
|
|
119
|
+
"No boosted options found for magic, hence not completing."
|
|
120
|
+
)
|
|
121
|
+
return []
|
|
122
|
+
boosted_parser: KomandParser | bool = getattr(func, "boosted_parser", False)
|
|
123
|
+
if not boosted_parser:
|
|
124
|
+
self.kernel.log.warning(
|
|
125
|
+
"Could not find boosted parser for magic, but has_boosted_options is True. Not completing."
|
|
126
|
+
)
|
|
127
|
+
return []
|
|
128
|
+
args = info["code"]
|
|
129
|
+
if isinstance(args, list):
|
|
130
|
+
args = " ".join(args)
|
|
131
|
+
|
|
132
|
+
args = _split_args(args)
|
|
133
|
+
if not isinstance(boosted_parser, KomandParser):
|
|
134
|
+
raise TypeError(
|
|
135
|
+
f"boosted_parser parameter of magic method must be a KomandParser, not {type(boosted_parser)}."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# finally calls complete method of boosted parser
|
|
139
|
+
try:
|
|
140
|
+
completed = boosted_parser.complete(args)
|
|
141
|
+
except Exception:
|
|
142
|
+
self.kernel.log.debug(
|
|
143
|
+
f"Error when completing option : {traceback.format_exc()}"
|
|
144
|
+
)
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
if len(args) > 0:
|
|
148
|
+
self.kernel.log.info(f"Modifying -- and - tokens : {completed}")
|
|
149
|
+
result = []
|
|
150
|
+
last_token = args[-1]
|
|
151
|
+
self.kernel.log.info(f"last token : {last_token}")
|
|
152
|
+
for each_match in completed:
|
|
153
|
+
if each_match == last_token:
|
|
154
|
+
continue
|
|
155
|
+
# for option tokens (starting with '-'), metakernel does
|
|
156
|
+
# not replace the full token, but adds the completion.
|
|
157
|
+
# so we need to truncate it. Somehow, tokens starting
|
|
158
|
+
# with '-' are not treated as others.
|
|
159
|
+
# For other tokens, it works as statikomand.
|
|
160
|
+
if last_token.startswith("--") and each_match.startswith("--"):
|
|
161
|
+
each_match = each_match[2:]
|
|
162
|
+
elif last_token.startswith("-") and each_match.startswith("-"):
|
|
163
|
+
each_match = each_match[1:]
|
|
164
|
+
|
|
165
|
+
result.append(each_match)
|
|
166
|
+
else:
|
|
167
|
+
result = completed
|
|
168
|
+
self.kernel.log.debug(f"Boosted magic completion : {result}")
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def boosted_option(*args: Any, **kwargs: Any) -> Callable[[_F], _F]:
|
|
173
|
+
"""
|
|
174
|
+
Return decorator that adds a boosted magic option to a function.
|
|
175
|
+
The boosted magic uses argparse instead of optparse, and
|
|
176
|
+
allows for tab completion among options names and values.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def decorator(func: _F) -> _F:
|
|
180
|
+
help_text = ""
|
|
181
|
+
if not getattr(func, "has_boosted_options", False):
|
|
182
|
+
func.has_boosted_options = True # type:ignore[attr-defined]
|
|
183
|
+
func.boosted_parser = KomandParser(prog=func.__name__, add_help=False) # type:ignore[attr-defined]
|
|
184
|
+
# boosted_parser is an instance of argparse.ArgumentParser
|
|
185
|
+
# add_help is set to False because argparse help writes to stdout, which
|
|
186
|
+
# breaks kernel machinery
|
|
187
|
+
help_text += "Options:\n-------\n"
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
action = func.boosted_parser.add_argument(*args, **kwargs) # type:ignore[attr-defined]
|
|
191
|
+
except AttributeError:
|
|
192
|
+
pass
|
|
193
|
+
except argparse.ArgumentError:
|
|
194
|
+
help_text += args[0] + "\n"
|
|
195
|
+
else:
|
|
196
|
+
help_text += _format_action(action) + "\n"
|
|
197
|
+
if func.__doc__:
|
|
198
|
+
func.__doc__ += _indent(func.__doc__, help_text)
|
|
199
|
+
else:
|
|
200
|
+
func.__doc__ = help_text
|
|
201
|
+
return func
|
|
202
|
+
|
|
203
|
+
return decorator
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _boosted_parse_args(
|
|
207
|
+
func, args: str | list[str], usage
|
|
208
|
+
) -> tuple[list[Any], dict[str, Any]]:
|
|
209
|
+
|
|
210
|
+
if isinstance(args, list):
|
|
211
|
+
args = " ".join(args)
|
|
212
|
+
args = _split_args(args)
|
|
213
|
+
|
|
214
|
+
# raise ValueError(f"dajkbz {args}")
|
|
215
|
+
if getattr(func, "has_boosted_options", False) and getattr(
|
|
216
|
+
func, "boosted_parser", False
|
|
217
|
+
):
|
|
218
|
+
if not isinstance(func.boosted_parser, argparse.ArgumentParser):
|
|
219
|
+
return list(), dict()
|
|
220
|
+
parsed_args = func.boosted_parser.parse_args(args)
|
|
221
|
+
return parsed_args._get_args(), parsed_args.__dict__
|
|
222
|
+
return list(), dict()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _format_action(action: argparse.Action):
|
|
226
|
+
out = ""
|
|
227
|
+
out = ", ".join(action.option_strings) + f" : {action.help}"
|
|
228
|
+
return out
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def complete_from_list(
|
|
232
|
+
list_of_reference: list[str],
|
|
233
|
+
word: str,
|
|
234
|
+
rank: int = 0,
|
|
235
|
+
):
|
|
236
|
+
"""
|
|
237
|
+
Completes a word from a given list_of_reference.
|
|
238
|
+
Return all occurences of list_of_reference whose
|
|
239
|
+
beginning matches word.
|
|
240
|
+
Can be used as within a lambda function with a custom list:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
my_list = ["bobo", "baba"]
|
|
244
|
+
completer = lambda w,r : complete_from_list(my_list, w, r)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
all_matches = []
|
|
249
|
+
|
|
250
|
+
for each_word in list_of_reference:
|
|
251
|
+
if len(each_word) < len(word):
|
|
252
|
+
continue
|
|
253
|
+
potential_match = each_word[: len(word)]
|
|
254
|
+
if potential_match == word:
|
|
255
|
+
all_matches.append(each_word)
|
|
256
|
+
return all_matches
|