mypy-pylint-fixed #21

Merged
aditya merged 15 commits from mypy-pylint-fixed into master 2025-11-04 13:48:10 +00:00
Member

fixed mypy --strict and pylint errors and warnings of feat/multi-agent , only merge after merging pr #5

fixed mypy --strict and pylint errors and warnings of feat/multi-agent , only merge after merging pr #5
ISSUES FIXED: #4
brent.edwards requested changes 2025-10-27 18:18:40 +00:00
Dismissed
brent.edwards left a comment

Five left for mypy --strict and pylint.

You can do it!

Five left for `mypy --strict` and `pylint`. You can do it!
@ -286,3 +283,3 @@
# Extract result
if hasattr(result, "messages") and result.messages:
return result.messages[-1].get("content", "")
content = result.messages[-1].get("content", "")
Member

In line 279 (I cannot leave a comment on code you didn't write) is

      result = await graph.execute(
         {"messages": [{"role": "user", "content": message}], "metadata": context}
     )

mypy --strict src reports:

src/cleveragents/agents/composite.py:279: error: Item "None" of "LangGraph | None" has no attribute "execute"  [union-attr]
In line 279 (I cannot leave a comment on code you didn't write) is > result = await graph.execute( > {"messages": [{"role": "user", "content": message}], "metadata": context} > ) `mypy --strict src` reports: ``` src/cleveragents/agents/composite.py:279: error: Item "None" of "LangGraph | None" has no attribute "execute" [union-attr] ```
brent.edwards marked this conversation as resolved
@ -450,3 +482,1 @@
f"Registered {len(templates.get('agents', {}))} agent templates, "
f"{len(templates.get('graphs', {}))} graph templates, "
f"{len(templates.get('streams', {}))} stream templates"
"Registered %d agent templates, %d graph templates, %d stream templates",
Member

On line 478 (I cannot leave comments on lines you did not change) is the line

              self.template_registry.register_all_templates(self.config.templates)

mypy --strict src reports:

src/cleveragents/core/application.py:478: error: "object" has no attribute "register_all_templates"  [attr-defined]
On line 478 (I cannot leave comments on lines you did not change) is the line > self.template_registry.register_all_templates(self.config.templates) `mypy --strict src` reports: ``` src/cleveragents/core/application.py:478: error: "object" has no attribute "register_all_templates" [attr-defined] ```
Member

Still missing this problem from mypy --strict src. Here's what I get:

src/cleveragents/core/application.py:481: error: "object" has no attribute "register_all_templates"  [attr-defined]
Still missing this problem from `mypy --strict src`. Here's what I get: ``` src/cleveragents/core/application.py:481: error: "object" has no attribute "register_all_templates" [attr-defined] ```
Member

It's still here this morning:

src/cleveragents/core/application.py:481: error: "object" has no attribute "register_all_templates"  [attr-defined]
It's still here this morning: ``` src/cleveragents/core/application.py:481: error: "object" has no attribute "register_all_templates" [attr-defined] ```
Author
Member

@brent.edwards
Fixed the mypy --strict issues.

@brent.edwards Fixed the mypy --strict issues.
@ -56,3 +57,3 @@
graph_def["name"] = self.name
return graph_def
return cast(Dict[str, Any], graph_def)
Member
    return cast(Dict[str, Any], graph_def)

According to mypy --strict src:

src/cleveragents/templates/graph_templates.py:59: error: Redundant cast to "dict[str, Any]"  [redundant-cast]
> return cast(Dict[str, Any], graph_def) According to `mypy --strict src`: ``` src/cleveragents/templates/graph_templates.py:59: error: Redundant cast to "dict[str, Any]" [redundant-cast] ```
brent.edwards marked this conversation as resolved
@ -283,3 +280,3 @@
result = self._apply_templates_recursive(result, templates, context)
return result
return cast(Dict[str, Any], result)
Member
      return cast(Dict[str, Any], result)

According to mypy --strict src:

src/cleveragents/templates/inline_jinja_handler.py:282: error: Redundant cast to "dict[str, Any]"  [redundant-cast]
> return cast(Dict[str, Any], result) According to `mypy --strict src`: ``` src/cleveragents/templates/inline_jinja_handler.py:282: error: Redundant cast to "dict[str, Any]" [redundant-cast] ```
brent.edwards marked this conversation as resolved
@ -52,3 +53,3 @@
stream_def["name"] = self.name
return stream_def
return cast(Dict[str, Any], stream_def)
Member
    return cast(Dict[str, Any], stream_def)

According to mypy --strict src:

src/cleveragents/templates/stream_templates.py:55: error: Redundant cast to "dict[str, Any]"  [redundant-cast]
> return cast(Dict[str, Any], stream_def) According to `mypy --strict src`: ``` src/cleveragents/templates/stream_templates.py:55: error: Redundant cast to "dict[str, Any]" [redundant-cast] ```
brent.edwards marked this conversation as resolved
khird left a comment

To achieve meaningful mypy compliance, the use of cast to subvert its warnings should be reduced, or at least there should be comments justifying why each usage is safe despite mypy's warning. The other issues are optional and won't block my approval of this MR.

To achieve meaningful mypy compliance, the use of cast to subvert its warnings should be reduced, or at least there should be comments justifying why each usage is safe despite mypy's warning. The other issues are optional and won't block my approval of this MR.
@ -100,3 +99,3 @@
kwargs["api_key"] = api_key
return ChatAnthropic(**kwargs)
elif self.provider.lower() == "google":
if self.provider.lower() == "google":
Member

These if/elif blocks would probably be more clearly expressed as one block of code that operates on a data structure like

{   "openai": {
        "renames": {},
        "class": ChatOpenAI},
    "anthropic": {
        "renames": {},
        "class": ChatAnthropic},
    "google": {
        "renames": {
            "max_tokens": "max_output_tokens",
            "api_key": "google_api_key"},
        "class": ChatGoogleGenerativeAPI}}

and decides what members of common_kwargs (if any) to mangle based on the renames field.

These if/elif blocks would probably be more clearly expressed as one block of code that operates on a data structure like ``` { "openai": { "renames": {}, "class": ChatOpenAI}, "anthropic": { "renames": {}, "class": ChatAnthropic}, "google": { "renames": { "max_tokens": "max_output_tokens", "api_key": "google_api_key"}, "class": ChatGoogleGenerativeAPI}} ``` and decides what members of `common_kwargs` (if any) to mangle based on the `renames` field.
Author
Member

@khird
Replaced if/elif blocks with a data-driven provider_config dictionary that maps providers to their parameter renames and classes, using a unified flow for all providers.

@khird Replaced if/elif blocks with a data-driven provider_config dictionary that maps providers to their parameter renames and classes, using a unified flow for all providers.
khird marked this conversation as resolved
@ -205,2 +228,3 @@
return {}
async def _execute_tool(self, state: GraphState) -> Dict[str, Any]:
async def _execute_tool(self, _state: GraphState) -> Dict[str, Any]:
Member

I assume this makes some automated tool shut up about you not using the argument state anywhere in the function body. Is there any reason not to simply remove it from the function definition? If there are places that rely on being able to call a function that might be self._execute_tool() or might be self._execute_conditional() and construct their argument list to include a GraphState, this change would cause it to break if they set it by keyword. If you absolutely can't remove it, using *args and **kwargs might be more forgiving.

I assume this makes some automated tool shut up about you not using the argument `state` anywhere in the function body. Is there any reason not to simply remove it from the function definition? If there are places that rely on being able to call a function that might be `self._execute_tool()` or might be `self._execute_conditional()` and construct their argument list to include a GraphState, this change would cause it to break if they set it by keyword. If you absolutely can't remove it, using `*args` and `**kwargs` might be more forgiving.
Author
Member

@khird
Fixed by removing the unused state parameter entirely from _execute_tool() since it wasn't being used in the function body, while keeping it in _execute_conditional() where it's actually needed.

@khird Fixed by removing the unused state parameter entirely from _execute_tool() since it wasn't being used in the function body, while keeping it in _execute_conditional() where it's actually needed.
Member

same issue in _execute_state(). Not a blocker, so I'm approving

same issue in _execute_state(). Not a blocker, so I'm approving
@ -156,2 +156,3 @@
if default_value.isdigit():
return default_value
elif default_value.replace(".", "").isdigit():
if default_value.replace(".", "").isdigit():
Member

What is the point of this test? If to match decimal numbers, should probably be replace(x, y, 1). If to match IP addresses, this doesn't work on IPv6. And in any case, if the condition isn't satisfied, we do the same thing (i.e. returning the default value) anyway when we fall through.

What is the point of this test? If to match decimal numbers, should probably be replace(x, y, 1). If to match IP addresses, this doesn't work on IPv6. And in any case, if the condition isn't satisfied, we do the same thing (i.e. returning the default value) anyway when we fall through.
Author
Member

Fixed by removing the redundant decimal check since it was doing the same thing as the fall-through case - returning the default value as a string. @khird

Fixed by removing the redundant decimal check since it was doing the same thing as the fall-through case - returning the default value as a string. @khird
khird marked this conversation as resolved
@ -173,2 +171,3 @@
if config.isdigit():
return int(config)
elif config.replace(".", "").isdigit() and config.count(".") == 1:
if config.replace(".", "").isdigit() and config.count(".") == 1:
Member

So I guess the answer here is that it's a decimal number. I note that negatives aren't handled; may or may not be an issue.

So I guess the answer [here](https://git.cleverthis.com/cleveragents/cleveragents-core/pulls/21/files#issuecomment-30424) is that it's a decimal number. I note that negatives aren't handled; may or may not be an issue.
Author
Member

Fixed by using lstrip('-') to handle negative numbers and replace(".", "", 1) to properly handle decimal numbers with only one decimal point, addressing the negative number issue you mentioned. @khird

Fixed by using lstrip('-') to handle negative numbers and replace(".", "", 1) to properly handle decimal numbers with only one decimal point, addressing the negative number issue you mentioned. @khird
khird marked this conversation as resolved
@ -332,2 +336,2 @@
import threading
import asyncio as async_io # pylint: disable=reimported,import-outside-toplevel
import threading # pylint: disable=import-outside-toplevel
Member

It's not clear why reimporting modules is necessary, or why they wouldn't be at the top level. Maybe comment to justify why do it this way that requires circumventing pylint + mypy?

It's not clear why reimporting modules is necessary, or why they wouldn't be at the top level. Maybe comment to justify why do it this way that requires circumventing pylint + mypy?
Author
Member

@khird
fixed the re importing modules, only keeping it on places where there is circular import issue.

@khird fixed the re importing modules, only keeping it on places where there is circular import issue.
khird marked this conversation as resolved
@ -6,3 +6,4 @@
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from typing import Dict
Member

typing.Dict is deprecated in favour of dict in recent versions of python, including all the versions CleverAgents claims to support.

typing.Dict is deprecated in favour of dict in recent versions of python, including all the versions CleverAgents claims to support.
Author
Member

@khird
Replaced the Dict[..] with dict[..] and removed the import statement.

@khird Replaced the Dict[..] with dict[..] and removed the import statement.
khird marked this conversation as resolved
@ -174,1 +173,3 @@
return template.instantiate(params, self, context or InstantiationContext())
return cast(
Dict[str, Any],
template.instantiate(params, self, context or InstantiationContext()),
Member

Please add a comment here explaining why fooling mypy with a cast() is necessary - if you know that template.instantiate() is going to return a dict[str, Any] then why not just annotate it as such in the base submodule? And if not, please explain why it's safe to proceed as if this were true, despite mypy's warning.

Please add a comment here explaining why fooling mypy with a `cast()` is necessary - if you know that `template.instantiate()` is going to return a `dict[str, Any]` then why not just annotate it as such in the base submodule? And if not, please explain why it's safe to proceed as if this were true, despite mypy's warning.
Author
Member

@khird
Replaced cast() with runtime validation that checks isinstance(result, dict) and raises ValueError with clear error message if YAML parses as anything other than a dict. This handles the case where YAML could be a list, string, or other type, making the code robust and type-safe without hiding potential issues.

@khird Replaced cast() with runtime validation that checks isinstance(result, dict) and raises ValueError with clear error message if YAML parses as anything other than a dict. This handles the case where YAML could be a list, string, or other type, making the code robust and type-safe without hiding potential issues.
khird marked this conversation as resolved
@ -84,3 +83,3 @@
# If no templates, just parse
if "{%" not in yaml_content and "{{" not in yaml_content:
return yaml.safe_load(yaml_content)
return cast(Dict[str, Any], yaml.safe_load(yaml_content))
Member

It's possible for yaml to represent a list rather than a dict. If there's a reason we know that can't be the case for the particular specimen in yaml_content, it would be good to document that reason. If there isn't such a reason, then mypy's correct here and you ought to handle or check that case. I'm not going to spam a bunch of identical comments about cast everywhere, but please give it a general look.

It's possible for yaml to represent a list rather than a dict. If there's a reason we know that can't be the case for the particular specimen in `yaml_content`, it would be good to document that reason. If there isn't such a reason, then mypy's correct here and you ought to handle or check that case. I'm not going to spam a bunch of identical comments about `cast` everywhere, but please give it a general look.
Author
Member

@khird
Replaced cast() with runtime validation that checks isinstance(result, dict) and raises ValueError with clear error message if YAML parses as anything other than a dict. This handles the case where YAML could be a list, string, or other type.

@khird Replaced cast() with runtime validation that checks isinstance(result, dict) and raises ValueError with clear error message if YAML parses as anything other than a dict. This handles the case where YAML could be a list, string, or other type.
khird marked this conversation as resolved
@ -53,2 +53,3 @@
if template_type == TemplateType.STREAM:
template = StreamTemplate(name, template_type, definition)
else:
if template_type not in [TemplateType.AGENT, TemplateType.GRAPH, TemplateType.STREAM]:
Member

I think the elif/else construction worked better here, because now anyone adding a new template type has to add an if block and an element to this list. If you'd just left it as elif blocks, there wouldn't be the possibility to forget the second part.

I think the elif/else construction worked better here, because now anyone adding a new template type has to add an if block *and* an element to this list. If you'd just left it as elif blocks, there wouldn't be the possibility to forget the second part.
Author
Member

@khird
You're right elif/else is better. Adding a new template type only requires one elif block, avoiding the risk of forgetting to update both a validation list and the handling logic.

@khird You're right elif/else is better. Adding a new template type only requires one elif block, avoiding the risk of forgetting to update both a validation list and the handling logic.
khird marked this conversation as resolved
@ -226,4 +230,3 @@
parsed = {"_content": parsed}
parsed["__templates__"] = templates
return parsed
Member

mypy --strict src reports:

src/cleveragents/templates/inline_jinja_handler.py:232: error: Returning Any from function declared to return "dict[str, Any]"  [no-any-return]
`mypy --strict src` reports: ``` src/cleveragents/templates/inline_jinja_handler.py:232: error: Returning Any from function declared to return "dict[str, Any]" [no-any-return] ```
Author
Member

@brent.edwards
Added runtime validation checks and raise ValueError with clear messages.

@brent.edwards Added runtime validation checks and raise ValueError with clear messages.
brent.edwards requested changes 2025-10-29 18:44:36 +00:00
Dismissed
brent.edwards left a comment

I feel for you. You are so close to done!

I feel for you. You are so close to done!
brent.edwards left a comment

Here is the exact result from mypy --strict src:

❯ mypy --strict src
src/cleveragents/templates/inline_jinja_handler.py:232: error: Returning Any from function declared to return "dict[str, Any]"  [no-any-return]
src/cleveragents/core/application.py:481: error: "object" has no attribute "register_all_templates"  [attr-defined]
Found 2 errors in 2 files (checked 48 source files)
Here is the exact result from `mypy --strict src`: ``` ❯ mypy --strict src src/cleveragents/templates/inline_jinja_handler.py:232: error: Returning Any from function declared to return "dict[str, Any]" [no-any-return] src/cleveragents/core/application.py:481: error: "object" has no attribute "register_all_templates" [attr-defined] Found 2 errors in 2 files (checked 48 source files) ```
khird approved these changes 2025-10-31 14:40:09 +00:00
aditya force-pushed mypy-pylint-fixed from 271d3a76d3 to 6fff3968a6 2025-11-04 13:47:34 +00:00 Compare
aditya merged commit 6fff3968a6 into master 2025-11-04 13:48:10 +00:00
Owner

Relates to #23

Relates to #23
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core!21
No description provided.