Repository: pydantic/pydantic
Pull request: Introducing classproperty decorator for model_computed_fields
## Change Summary

A `classproperty` decorator has been added in order to access `model_computed_fields` as a class attribute and an instance property.

## Related issue number
fix #7986

## Checklist

* [x] The pull request title is a good summary of the changes - it will be used in the changelog
* [x] Unit tests for the changes exist
* [x] Tests pass on CI
* [x] Documentation reflects the changes where applicable
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


Selected Reviewer: @samuelcolvin

Problem statement:
should 'model_computed_fields' be a class attribute not an instance property?
### Initial Checks

- [X] I confirm that I'm using Pydantic V2

### Description

Hopefully title is quite self explanatory, but here is a bit of extra detail....

if I have a Model subclass "Schema", then I expected to be able to get to computed_fields via "model_computed_fields" attribute e.g. Schema.model_computed_fields, but this returns a "property" instance

looking under the hood, model_computed_fields returns data from `self.__pydantic_decorators__.computed_fields` - but `__pydantic_decorators__` is a class attribute and could presumably be accessed from the class without having a model instance

So in summary, this works:
`Schema.__pydantic_decorators__.computed_fields`

I expected this to work, but it doesn't:
`Schema.model_computed_fields`

Thanks!

### Example Code

_No response_

### Python, Pydantic & OS Version

```Text
pydantic version: 2.2.0
        pydantic-core version: 2.6.0
          pydantic-core build: profile=release pgo=true mimalloc=true
               python version: 3.10.2 (main, Jan 28 2022, 16:19:08) [GCC 10.3.0]
                     platform: Linux-5.15.0-87-generic-x86_64-with-glibc2.31
     optional deps. installed: ['typing-extensions']
```

Hints from issue discussion:
@hottwaj,

Thanks for reporting this! This seems like a reasonable change to make - I agree with your logic. Would you like to open a PR to make this change?
Great thanks!  I'm keen to submit a PR, but it might take a little time.  Will try to get to it soon :)
@sydney-runkle  

I could pick this up!
@geospackle Hi, I don't know if you had the chance to have a look at this issue yet ?


The current implementation, defined in `main.py` is as follows:
```
@property
    def model_computed_fields(self) -> dict[str, ComputedFieldInfo]:
        """Get the computed fields of this model instance.

        Returns:
            A dictionary of computed field names and their corresponding `ComputedFieldInfo` objects.
        """
        return {k: v.info for k, v in self.__pydantic_decorators__.computed_fields.items()}
   ```
   
   In order to make this property to be both accessible as an instance property and a class property, I think it should be rewritten as such:
   ```
   @classmethod
   @property
    def model_computed_fields(cls) -> dict[str, ComputedFieldInfo]:
        """Get the computed fields of this model.

        Returns:
            A dictionary of computed field names and their corresponding `ComputedFieldInfo` objects.
        """
        return {k: v.info for k, v in cls.__pydantic_decorators__.computed_fields.items()}
```

Before opening a PR, I'd like to gather your thoughts @hottwaj @sydney-runkle @geospackle 🙏🏼 

   
In principle that looks fine to me except that chaining classmethod and property together in that way is deprecated from python 3.11
https://docs.python.org/3.11/library/functions.html?highlight=classmethod#classmethod

So I think this needs to be set as a class level attribute during class creation instead 
Sounds good! I'll have a look after the holidays.

On Fri, Dec 22, 2023, 11:26 AM hottwaj ***@***.***> wrote:

> In principle that looks fine to me except that chaining classmethod and
> property together in that way is deprecated from python 3.11
>
> https://docs.python.org/3.11/library/functions.html?highlight=classmethod#classmethod
>
> So I think this needs to be set as a class level attribute during class
> creation instead
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/pydantic/pydantic/issues/7986#issuecomment-1867869163>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AFSOXV65PCOJJ4QNMY72TZTYKWYEFAVCNFSM6AAAAAA6ZTITUOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNRXHA3DSMJWGM>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

Commit message: feat: introduce classproperty decorator for model_computed_fields

Patch to review:
diff --git a/pydantic/main.py b/pydantic/main.py
index 59afce4e7c0..eff94bce974 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -58,6 +58,14 @@
 _object_setattr = _model_construction.object_setattr
 
 
+class classproperty:
+    def __init__(self, function) -> None:
+        self.fget = function
+
+    def __get__(self, instance, owner) -> Any:
+        return self.fget(owner)
+
+
 class BaseModel(metaclass=_model_construction.ModelMetaclass):
     """Usage docs: https://docs.pydantic.dev/2.6/concepts/models/
 
@@ -167,14 +175,14 @@ def __init__(self, /, **data: Any) -> None:  # type: ignore
     # The following line sets a flag that we use to determine when `__init__` gets overridden by the user
     __init__.__pydantic_base_init__ = True
 
-    @property
-    def model_computed_fields(self) -> dict[str, ComputedFieldInfo]:
+    @classproperty
+    def model_computed_fields(cls) -> dict[str, ComputedFieldInfo]:
         """Get the computed fields of this model instance.
 
         Returns:
             A dictionary of computed field names and their corresponding `ComputedFieldInfo` objects.
         """
-        return {k: v.info for k, v in self.__pydantic_decorators__.computed_fields.items()}
+        return {k: v.info for k, v in cls.__pydantic_decorators__.computed_fields.items()}
 
     @property
     def model_extra(self) -> dict[str, Any] | None:
diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py
index 9a4377a21c6..0bc0d0a7e20 100644
--- a/tests/test_computed_fields.py
+++ b/tests/test_computed_fields.py
@@ -62,6 +62,13 @@ def double_width(self) -> int:
     assert rect.model_dump() == {'width': 10, 'length': 5, 'area': 50, 'area2': 50}
     assert rect.model_dump_json() == '{"width":10,"length":5,"area":50,"area2":50}'
 
+    assert set(Rectangle.model_fields) == {'width', 'length'}
+    assert set(Rectangle.model_computed_fields) == {'area', 'area2'}
+
+    assert Rectangle.model_computed_fields['area'].description == 'An awesome area'
+    assert Rectangle.model_computed_fields['area2'].title == 'Pikarea'
+    assert Rectangle.model_computed_fields['area2'].description == 'Another area'
+
 
 def test_computed_fields_json_schema():
     class Rectangle(BaseModel):


Code review comments:
diff_hunk: @@ -58,6 +58,14 @@
 _object_setattr = _model_construction.object_setattr
 
 
+class classproperty:
+    def __init__(self, function) -> None:
+        self.fget = function
+
+    def __get__(self, instance, owner) -> Any:
+        return self.fget(owner)
path: pydantic/main.py
text: @user1:
This could benefit from proper type checking on the arguments. What happens currently when `model_computed_fields` is accessed with `mypy` and `pyright`?

@author:
I think it should be solved since I removed this class. Thanks