Skip to content

Upgrade to Pydantic v2#512

Open
JamieMagee wants to merge 1 commit intorobusta-dev:mainfrom
JamieMagee:upgrade-pydantic-v2
Open

Upgrade to Pydantic v2#512
JamieMagee wants to merge 1 commit intorobusta-dev:mainfrom
JamieMagee:upgrade-pydantic-v2

Conversation

@JamieMagee
Copy link

@JamieMagee JamieMagee commented Mar 23, 2026

Distributions that only ship Pydantic v2 can't run KRR because it pins pydantic v1. This brings the main app in line with the enforcer module, which was already on v2.

What changed:

  • pydantic bumped to >=2.0,<3.0, added pydantic-settings for BaseSettings
  • @validator-> @field_validator with @classmethod (7 in config.py, 1 in allocations.py)
  • .dict() / .json() -> .model_dump() / .model_dump_json() (3 call sites)
  • __fields__ -> model_fields in the CLI generation code in main.py
  • Result.__init__ -> model_post_init
  • Mutable defaults (set(), {}) wrapped in Field(default_factory=...)
  • _Settings proxy no longer inherits BaseSettings — it just proxies via __getattr__, so the inheritance was unnecessary and breaks under v2's stricter init

Fixes #244

Replace pydantic v1 APIs with their v2 equivalents:
- BaseSettings now comes from pydantic-settings
- @validator becomes @field_validator
- .dict()/.json() become .model_dump()/.model_dump_json()
- __fields__ becomes model_fields
- Result.__init__ becomes model_post_init
- Mutable defaults wrapped in Field(default_factory=...)
- _Settings proxy no longer inherits BaseSettings

Fixes robusta-dev#244
@CLAassistant
Copy link

CLAassistant commented Mar 23, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link

coderabbitai bot commented Mar 23, 2026

Walkthrough

This pull request migrates the codebase from Pydantic v1 to v2 by updating dependency constraints, replacing deprecated APIs with v2 equivalents (validators, serialization methods, schema introspection), moving BaseSettings to the pydantic-settings package, and fixing mutable default values in model fields.

Changes

Cohort / File(s) Summary
Dependency Version Constraints
pyproject.toml, requirements.txt
Updated Pydantic from ^1.10.7 / 1.10.15 to >=2.0,<3.0, added pydantic-settings >=2.0,<3.0, and relaxed typing-extensions constraint to >=4.6.1.
Mutable Default Fixes
robusta_krr/core/models/allocations.py, robusta_krr/core/models/objects.py
Replaced mutable set() and {} defaults with pd.Field(default_factory=...) to prevent shared state across instances.
Validator Migration
robusta_krr/core/models/allocations.py
Migrated @pd.validator(..., pre=True) decorators to @field_validator(..., mode="before") with @classmethod.
Settings Base Class & Validators
robusta_krr/core/models/config.py
Changed Config base class from pd.BaseSettings to pydantic_settings.BaseSettings; updated 7 validator decorators to Pydantic v2 @field_validator(...) syntax with @classmethod; modified _Settings proxy class to remove base class inheritance and add delegation logic.
Lifecycle Hook Migration
robusta_krr/core/models/result.py
Replaced custom __init__ with Pydantic's model_post_init hook for score computation.
Serialization API Updates
robusta_krr/core/runner.py, robusta_krr/formatters/pprint.py
Changed dict() to model_dump() for Pydantic v2 compatibility.
JSON Serialization Update
robusta_krr/formatters/yaml.py
Replaced json() with model_dump_json() followed by JSON parsing before YAML output.
Schema Introspection Migration
robusta_krr/main.py
Updated CLI option generation to use Pydantic v2's model_fields and field_info.annotation instead of v1's __fields__ and field_meta.type_.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Upgrade to Pydantic v2' clearly and concisely describes the main change throughout the entire changeset, which comprehensively migrates from Pydantic v1 to v2 APIs.
Linked Issues check ✅ Passed The PR successfully addresses issue #244 by migrating to Pydantic v2, adding pydantic-settings for BaseSettings, and updating all v1 APIs to v2 equivalents, enabling compatibility with distributions like openSUSE Tumbleweed that ship Pydantic v2 only.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the Pydantic v1 to v2 migration objectives; no unrelated modifications or scope creep are present in the changeset.
Description check ✅ Passed The pull request description clearly explains the migration from Pydantic v1 to v2, including specific changes made (dependency updates, API migrations, mutable defaults handling) and references the issue being fixed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
robusta_krr/core/models/config.py (1)

124-136: ⚠️ Potential issue | 🟡 Minor

Missing return for the "*" case in validate_namespaces.

When v equals "*", the function falls through to line 135 which calls .lower() on a list, but the "*" string literal should be returned early. The condition on line 127 only handles empty list, not "*".

🐛 Proposed fix
 `@field_validator`("namespaces")
 `@classmethod`
 def validate_namespaces(cls, v: Union[list[str], Literal["*"]]) -> Union[list[str], Literal["*"]]:
     if v == []:
         return "*"
+    if v == "*":
+        return v

     if isinstance(v, list):
         for val in v:
             if val.startswith("*"):
                 raise ValueError("Namespace's values cannot start with an asterisk (*)")

     return [val.lower() for val in v]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@robusta_krr/core/models/config.py` around lines 124 - 136, In
validate_namespaces (the `@field_validator` on "namespaces") add an explicit early
return when v == "*" so the literal asterisk is returned unchanged; keep the
existing empty-list-to-"*" behavior, and for the list branch (isinstance(v,
list)) validate that no element startswith("*") and then return [val.lower() for
val in v]; this ensures the function never falls through and attempts .lower()
on a non-list value and preserves the Union[list[str], Literal["*"]] return
type.
robusta_krr/core/runner.py (1)

524-524: ⚠️ Potential issue | 🟠 Major

Missed Pydantic v2 migration: .json() should be .model_dump_json().

This line still uses the deprecated Pydantic v1 .json() method while the rest of the PR migrates to v2 APIs. This will cause a deprecation warning or failure.

🐛 Proposed fix
-        result_dict = json.loads(result.json(indent=2))
+        result_dict = json.loads(result.model_dump_json(indent=2))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@robusta_krr/core/runner.py` at line 524, Replace the deprecated Pydantic v1
call result.json() with the v2 API result.model_dump_json() where result is the
Pydantic model being serialized (the expression currently assigned to
result_dict); keep the surrounding json.loads(...) call if you need a dict, or
better yet call result.model_dump() to get a dict directly—update the expression
that creates result_dict accordingly (e.g., use
json.loads(result.model_dump_json(...)) or result.model_dump()) so it uses
Pydantic v2.
🧹 Nitpick comments (1)
robusta_krr/formatters/yaml.py (1)

11-11: Consider using model_dump() directly to avoid unnecessary JSON round-trip.

The current approach serializes to JSON string then parses it back to a dict. Since yaml_module.dump() accepts a dict, you can use model_dump() directly for better efficiency.

♻️ Suggested optimization
-    return yaml_module.dump(json.loads(result.model_dump_json()), sort_keys=False)
+    return yaml_module.dump(result.model_dump(), sort_keys=False)

This also allows removing the unused json import at line 1.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@robusta_krr/formatters/yaml.py` at line 11, Replace the JSON round-trip in
the YAML formatter by passing the model dict directly to yaml_module.dump: use
result.model_dump() instead of json.loads(result.model_dump_json()) inside the
return statement (update the call site that currently references
model_dump_json), and remove the now-unused json import; ensure you keep
sort_keys=False on yaml_module.dump.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@robusta_krr/core/models/config.py`:
- Around line 124-136: In validate_namespaces (the `@field_validator` on
"namespaces") add an explicit early return when v == "*" so the literal asterisk
is returned unchanged; keep the existing empty-list-to-"*" behavior, and for the
list branch (isinstance(v, list)) validate that no element startswith("*") and
then return [val.lower() for val in v]; this ensures the function never falls
through and attempts .lower() on a non-list value and preserves the
Union[list[str], Literal["*"]] return type.

In `@robusta_krr/core/runner.py`:
- Line 524: Replace the deprecated Pydantic v1 call result.json() with the v2
API result.model_dump_json() where result is the Pydantic model being serialized
(the expression currently assigned to result_dict); keep the surrounding
json.loads(...) call if you need a dict, or better yet call result.model_dump()
to get a dict directly—update the expression that creates result_dict
accordingly (e.g., use json.loads(result.model_dump_json(...)) or
result.model_dump()) so it uses Pydantic v2.

---

Nitpick comments:
In `@robusta_krr/formatters/yaml.py`:
- Line 11: Replace the JSON round-trip in the YAML formatter by passing the
model dict directly to yaml_module.dump: use result.model_dump() instead of
json.loads(result.model_dump_json()) inside the return statement (update the
call site that currently references model_dump_json), and remove the now-unused
json import; ensure you keep sort_keys=False on yaml_module.dump.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5436bea6-317b-4b21-9c53-1d8091ba3b29

📥 Commits

Reviewing files that changed from the base of the PR and between c40fe37 and 64f8242.

📒 Files selected for processing (10)
  • pyproject.toml
  • requirements.txt
  • robusta_krr/core/models/allocations.py
  • robusta_krr/core/models/config.py
  • robusta_krr/core/models/objects.py
  • robusta_krr/core/models/result.py
  • robusta_krr/core/runner.py
  • robusta_krr/formatters/pprint.py
  • robusta_krr/formatters/yaml.py
  • robusta_krr/main.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Building krr for openSUSE Tumbleweed fails: pydantic.errors.PydanticImportError: BaseSettings has been moved to the pydantic-settings package.

2 participants