Skip to content

Commit 2d90011

Browse files
committed
feat(python): class method attribute
1 parent 52d9915 commit 2d90011

File tree

4 files changed

+118
-9
lines changed

4 files changed

+118
-9
lines changed

src/Fable.Core/Fable.Core.Py.fs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ module Py =
2828
abstract Decorate: fn: Callable * info: Reflection.MethodInfo -> Callable
2929

3030
/// <summary>
31-
/// Adds Python decorators to generated classes, enabling integration with Python
32-
/// frameworks like dataclasses, attrs, functools, and any other decorator-based
31+
/// Adds Python decorators to generated classes or methods, enabling integration with Python
32+
/// frameworks like dataclasses, attrs, functools, Pydantic, and any other decorator-based
3333
/// libraries.
3434
/// </summary>
3535
/// <remarks>
@@ -38,15 +38,33 @@ module Py =
3838
/// <para>Multiple [&lt;Decorate&gt;] attributes are applied in reverse order
3939
/// (bottom to top), following Python's standard decorator stacking behavior.</para>
4040
/// <para>Examples:</para>
41-
/// <para>[&lt;Decorate("dataclasses.dataclass")&gt;] - Simple decorator</para>
41+
/// <para>[&lt;Decorate("dataclasses.dataclass")&gt;] - Simple class decorator</para>
4242
/// <para>[&lt;Decorate("functools.lru_cache", "maxsize=128")&gt;] - Decorator with
4343
/// parameters</para>
44+
/// <para>[&lt;Decorate("pydantic.field_validator", "'Name'")&gt;] - Method decorator for
45+
/// Pydantic validators</para>
4446
/// </remarks>
45-
[<AttributeUsage(AttributeTargets.Class, AllowMultiple = true)>]
47+
[<AttributeUsage(AttributeTargets.Class ||| AttributeTargets.Method, AllowMultiple = true)>]
4648
type DecorateAttribute(decorator: string) =
4749
inherit Attribute()
4850
new(decorator: string, parameters: string) = DecorateAttribute(decorator)
4951

52+
/// <summary>
53+
/// Marks a static member to be emitted as a Python @classmethod instead of @staticmethod.
54+
/// </summary>
55+
/// <remarks>
56+
/// <para>Use this attribute on static members when you need the first parameter to receive
57+
/// the class (cls) instead of being a regular static method.</para>
58+
/// <para>This is commonly needed for Pydantic validators and other Python frameworks that
59+
/// require @classmethod decorators.</para>
60+
/// <para>Example:</para>
61+
/// <para>[&lt;Py.ClassMethod&gt;]</para>
62+
/// <para>static member validate_name(cls, v: string) = ...</para>
63+
/// </remarks>
64+
[<AttributeUsage(AttributeTargets.Method)>]
65+
type ClassMethodAttribute() =
66+
inherit Attribute()
67+
5068
[<RequireQualifiedAccess>]
5169
type ClassAttributeStyle =
5270
// Translates to properties with instance attributes backing

src/Fable.Transforms/Python/Fable2Python.Transforms.fs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2906,8 +2906,14 @@ let transformAttachedProperty
29062906
[]
29072907
else
29082908
// Instance properties use the traditional approach (properties style)
2909+
// Get custom decorators from Py.Decorate attributes
2910+
let customDecorators =
2911+
let decoratorInfos = Util.getDecoratorInfo info.Attributes
2912+
Util.generateDecorators com ctx decoratorInfos
2913+
29092914
let decorators =
2910-
[
2915+
customDecorators
2916+
@ [
29112917
if isGetter then
29122918
Expression.name "property"
29132919
else
@@ -2947,11 +2953,26 @@ let transformAttachedMethod (com: IPythonCompiler) ctx (info: Fable.MemberFuncti
29472953

29482954
let isStatic = not info.IsInstance
29492955

2956+
// Check if this method has Py.ClassMethod attribute
2957+
let hasClassMethodAttribute =
2958+
info.Attributes
2959+
|> Seq.exists (fun att -> att.Entity.FullName = Atts.pyClassMethod)
2960+
2961+
// Get custom decorators from Py.Decorate attributes
2962+
let customDecorators =
2963+
let decoratorInfos = Util.getDecoratorInfo info.Attributes
2964+
Util.generateDecorators com ctx decoratorInfos
2965+
29502966
let decorators =
2951-
if isStatic then
2952-
[ Expression.name "staticmethod" ]
2953-
else
2954-
[]
2967+
// Custom decorators come first (outermost), then static/classmethod decorator
2968+
customDecorators
2969+
@ if isStatic then
2970+
if hasClassMethodAttribute then
2971+
[ Expression.name "classmethod" ]
2972+
else
2973+
[ Expression.name "staticmethod" ]
2974+
else
2975+
[]
29552976

29562977
let makeMethod name args body decorators returnType =
29572978
let key = memberFromName com ctx name |> nameFromKey com ctx

src/Fable.Transforms/Transforms.Util.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ module Atts =
127127
[<Literal>]
128128
let pyClassAttributes = "Fable.Core.Py.ClassAttributes" // typeof<Fable.Core.Py.ClassAttributes>.FullName
129129

130+
[<Literal>]
131+
let pyClassMethod = "Fable.Core.Py.ClassMethodAttribute" // typeof<Fable.Core.Py.ClassMethodAttribute>.FullName
132+
130133
[<Literal>]
131134
let dartIsConst = "Fable.Core.Dart.IsConstAttribute" // typeof<Fable.Core.Dart.IsConstAttribute>.FullName
132135

tests/Python/TestPyInterop.fs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,73 @@ let ``test PropertiesUserWithInit`` () =
590590
user.Email |> equal (Some "[email protected]")
591591
user.Enabled |> equal true
592592

593+
// Test Py.Decorate on static methods
594+
595+
[<AttachMembers>]
596+
type ClassWithDecoratedStaticMethod() =
597+
member val Value: int = 0 with get, set
598+
599+
[<Py.Decorate("functools.lru_cache")>]
600+
static member cached_function(x: int) : int =
601+
x * 2
602+
603+
[<Fact>]
604+
let ``test Py.Decorate on static method`` () =
605+
// Test that @lru_cache decorator is applied to static method
606+
let result1 = ClassWithDecoratedStaticMethod.cached_function(5)
607+
let result2 = ClassWithDecoratedStaticMethod.cached_function(5)
608+
result1 |> equal 10
609+
result2 |> equal 10
610+
611+
[<AttachMembers>]
612+
type ClassWithDecoratedStaticMethodWithParams() =
613+
[<Py.Decorate("functools.lru_cache", "maxsize=32")>]
614+
static member cached_with_params(x: int) : int =
615+
x * 3
616+
617+
[<Fact>]
618+
let ``test Py.Decorate on static method with parameters`` () =
619+
// Test that decorator with parameters is applied to static method
620+
let result = ClassWithDecoratedStaticMethodWithParams.cached_with_params(4)
621+
result |> equal 12
622+
623+
[<AttachMembers>]
624+
type ClassWithClassMethod() =
625+
member val Name: string = "" with get, set
626+
627+
// Note: With @classmethod, Python automatically passes `cls` as first argument
628+
// So from F# we only pass the remaining arguments
629+
[<Py.ClassMethod>]
630+
static member create_instance(cls: obj, name: string) : ClassWithClassMethod =
631+
let instance = ClassWithClassMethod()
632+
instance.Name <- name
633+
instance
634+
635+
[<Fact>]
636+
let ``test Py.ClassMethod attribute`` () =
637+
// Test that @classmethod decorator is applied instead of @staticmethod
638+
// Note: cls is automatically passed by Python, so we only pass the name argument
639+
// We use Emit to call the method without the cls argument from F#
640+
let instance = emitPyExpr<ClassWithClassMethod> [] "ClassWithClassMethod.create_instance('TestName')"
641+
instance.Name |> equal "TestName"
642+
643+
644+
[<AttachMembers>]
645+
type ClassWithMultipleDecoratedMethods() =
646+
[<Py.Decorate("functools.lru_cache", "maxsize=16")>]
647+
static member method_a(x: int) : int = x * 2
648+
649+
[<Py.Decorate("functools.lru_cache", "maxsize=32")>]
650+
static member method_b(x: int) : int = x * 3
651+
652+
[<Fact>]
653+
let ``test multiple static methods with decorators`` () =
654+
// Test that decorators work on multiple static methods in same class
655+
let resultA = ClassWithMultipleDecoratedMethods.method_a(5)
656+
let resultB = ClassWithMultipleDecoratedMethods.method_b(5)
657+
resultA |> equal 10
658+
resultB |> equal 15
659+
593660
// Import fable_library version to verify we're using local build, not PyPI
594661
let fableLibraryVersion: string = import "__version__" "fable_library._version"
595662

0 commit comments

Comments
 (0)