Skip to content

Commit 27cf959

Browse files
authored
[Python] Add DecorateTemplate and ClassAttributesTemplate for library authors (#4291)
* [Python] Add DecorateTemplateAttribute * [Python] Add ClassAttributesTemplate
1 parent 33c9095 commit 27cf959

File tree

12 files changed

+420
-65
lines changed

12 files changed

+420
-65
lines changed

src/Fable.Cli/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
* [Python] Add `[<Py.DecorateTemplate>]` attribute for creating custom decorator attributes (by @dbrattli)
13+
* [Python] Add `[<Py.ClassAttributesTemplate>]` attribute for creating custom class attribute shortcuts (by @dbrattli)
14+
* [Python] Add `[<Py.DataClass>]` as a built-in shorthand for `[<Py.ClassAttributes(style = Attributes, init = false)>]` (by @dbrattli)
15+
1016
### Fixed
1117

18+
* [Python] Fix regression `[<Erase>]` on class types not preventing them from being emitted to Python (by @dbrattli)
1219
* [Python] Fix regression `%A` format specifier to output booleans as lowercase `true`/`false` (by @dbrattli)
1320

1421
### Changed

src/Fable.Compiler/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
* [Python] Add `[<Py.DecorateTemplate>]` attribute for creating custom decorator attributes (by @dbrattli)
13+
* [Python] Add `[<Py.ClassAttributesTemplate>]` attribute for creating custom class attribute shortcuts (by @dbrattli)
14+
* [Python] Add `[<Py.DataClass>]` as a built-in shorthand for `[<Py.ClassAttributes(style = Attributes, init = false)>]` (by @dbrattli)
15+
1016
### Fixed
1117

18+
* [Python] Fix regression `[<Erase>]` on class types not preventing them from being emitted to Python (by @dbrattli)
19+
1220
* [Python] Fix regression `%A` format specifier to output booleans as lowercase `true`/`false` (by @dbrattli)
1321

1422
### Changed

src/Fable.Core/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
* [Python] Add `[<Py.DecorateTemplate>]` attribute for creating custom decorator attributes (by @dbrattli)
13+
* [Python] Add `[<Py.ClassAttributesTemplate>]` attribute for creating custom class attribute shortcuts (by @dbrattli)
14+
* [Python] Add `[<Py.DataClass>]` as a built-in shorthand for `[<Py.ClassAttributes(style = Attributes, init = false)>]` (by @dbrattli)
15+
1016
### Changed
1117

1218
* [Python] `[<Py.Decorate>]` now emits decorator strings verbatim and adds `importFrom` parameter for explicit import control (by @dbrattli)

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

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,48 @@ module Py =
1616
[<Emit "$0">]
1717
abstract Instance: obj
1818

19+
/// <summary>
20+
/// Base class for creating F#-side decorator attributes that transform functions at compile time.
21+
/// This is similar to Python decorators but operates during Fable compilation, not at Python runtime.
22+
/// </summary>
23+
/// <remarks>
24+
/// <para>Inherit from this class and implement the Decorate method to wrap or transform functions.</para>
25+
/// <para>The decorated function is passed to Decorate, and the returned Callable replaces it.</para>
26+
/// <para>Example:</para>
27+
/// <code>
28+
/// type LogAttribute() =
29+
/// inherit Py.DecoratorAttribute()
30+
/// override _.Decorate(fn) =
31+
/// Py.argsFunc (fun args ->
32+
/// printfn "Calling function"
33+
/// fn.Invoke(args))
34+
/// </code>
35+
/// <para>Note: This does NOT emit Python @decorator syntax. For emitting Python decorators,
36+
/// use DecorateAttribute or DecorateTemplateAttribute instead.</para>
37+
/// </remarks>
1938
[<AbstractClass>]
2039
type DecoratorAttribute() =
2140
inherit Attribute()
2241
abstract Decorate: fn: Callable -> Callable
2342

43+
/// <summary>
44+
/// Base class for creating F#-side decorator attributes with access to reflection metadata.
45+
/// Like DecoratorAttribute, but the Decorate method also receives MethodInfo for the decorated member.
46+
/// </summary>
47+
/// <remarks>
48+
/// <para>Use this when your decorator needs information about the decorated method (name, parameters, etc.).</para>
49+
/// <para>Example:</para>
50+
/// <code>
51+
/// type LogWithNameAttribute() =
52+
/// inherit Py.ReflectedDecoratorAttribute()
53+
/// override _.Decorate(fn, info) =
54+
/// Py.argsFunc (fun args ->
55+
/// printfn "Calling %s" info.Name
56+
/// fn.Invoke(args))
57+
/// </code>
58+
/// <para>Note: This does NOT emit Python @decorator syntax. For emitting Python decorators,
59+
/// use DecorateAttribute or DecorateTemplateAttribute instead.</para>
60+
/// </remarks>
2461
[<AbstractClass>]
2562
type ReflectedDecoratorAttribute() =
2663
inherit Attribute()
@@ -49,7 +86,32 @@ module Py =
4986
/// Decorator with import but no parameters
5087
new(decorator: string, importFrom: string) = DecorateAttribute(decorator, importFrom, "")
5188

52-
/// Decorator with all parameters
89+
/// <summary>
90+
/// Marks a custom attribute class as a decorator template, enabling library authors to create
91+
/// ergonomic decorator attributes that users can apply without knowing the underlying Python syntax.
92+
/// </summary>
93+
/// <remarks>
94+
/// <para>Place this attribute on a custom attribute class. The template string uses {0}, {1}, etc.
95+
/// as placeholders for the custom attribute's constructor arguments.</para>
96+
/// <para>Example - defining a custom decorator attribute:</para>
97+
/// <code>
98+
/// [&lt;Erase; Py.DecorateTemplate("app.get('{0}')")&gt;]
99+
/// type GetAttribute(path: string) = inherit Attribute()
100+
/// </code>
101+
/// <para>Example - using the custom decorator:</para>
102+
/// <code>
103+
/// [&lt;Get("/users")&gt;]
104+
/// static member get_users() = ...
105+
/// // Generates: @app.get('/users')
106+
/// </code>
107+
/// <para>Use [&lt;Erase&gt;] to prevent the attribute type from being emitted to Python.</para>
108+
/// </remarks>
109+
[<AttributeUsage(AttributeTargets.Class)>]
110+
type DecorateTemplateAttribute(template: string) =
111+
inherit Attribute()
112+
/// Template with import specification
113+
new(template: string, importFrom: string) = DecorateTemplateAttribute(template)
114+
53115
/// <summary>
54116
/// Marks a static member to be emitted as a Python @classmethod instead of @staticmethod.
55117
/// </summary>
@@ -72,6 +134,7 @@ module Py =
72134
// Translates to class attributes
73135
| Attributes = 1
74136

137+
/// <summary>
75138
/// Used on a class to provide Python-specific control over how F# types are transpiled to Python classes.
76139
/// This attribute implies member attachment (similar to AttachMembers) while offering Python-specific parameters.
77140
/// </summary>
@@ -80,12 +143,58 @@ module Py =
80143
/// <para>Additional Python-specific parameters control the generated Python class style and features.</para>
81144
/// </remarks>
82145
[<AttributeUsage(AttributeTargets.Class)>]
83-
type ClassAttributes() =
146+
type ClassAttributesAttribute() =
84147
inherit Attribute()
85148

86-
new(style: ClassAttributeStyle) = ClassAttributes()
149+
new(style: ClassAttributeStyle) = ClassAttributesAttribute()
150+
151+
new(style: ClassAttributeStyle, init: bool) = ClassAttributesAttribute()
152+
153+
/// <summary>
154+
/// Marks a custom attribute class as a class attributes template, enabling library authors
155+
/// to create ergonomic class attributes that users can apply without knowing the underlying parameters.
156+
/// </summary>
157+
/// <remarks>
158+
/// <para>Place this attribute on a custom attribute class to define the class generation style.</para>
159+
/// <para>Example - defining a custom class attribute:</para>
160+
/// <code>
161+
/// [&lt;Erase; Py.ClassAttributesTemplate(Py.ClassAttributeStyle.Attributes, init = false)&gt;]
162+
/// type BaseModelAttribute() = inherit Attribute()
163+
/// </code>
164+
/// <para>Example - using the custom attribute:</para>
165+
/// <code>
166+
/// [&lt;BaseModel&gt;]
167+
/// type User(name: string, age: int) = ...
168+
/// // Generates class with class-level attributes, no __init__
169+
/// </code>
170+
/// <para>Use [&lt;Erase&gt;] to prevent the attribute type from being emitted to Python.</para>
171+
/// </remarks>
172+
[<AttributeUsage(AttributeTargets.Class)>]
173+
type ClassAttributesTemplateAttribute(style: ClassAttributeStyle, init: bool) =
174+
inherit Attribute()
175+
/// Template with Attributes style and init = false (common for Pydantic/dataclasses)
176+
new(style: ClassAttributeStyle) = ClassAttributesTemplateAttribute(style, false)
87177

88-
new(style: ClassAttributeStyle, init: bool) = ClassAttributes()
178+
/// <summary>
179+
/// Shorthand for [&lt;Py.ClassAttributes(style = Attributes, init = false)&gt;].
180+
/// Use this for Python dataclasses, Pydantic models, attrs classes, or any class
181+
/// that needs class-level type annotations without a generated __init__.
182+
/// </summary>
183+
/// <remarks>
184+
/// <para>Example:</para>
185+
/// <code>
186+
/// [&lt;Py.DataClass&gt;]
187+
/// type User(name: string, age: int) = ...
188+
/// // Generates:
189+
/// // class User:
190+
/// // name: str
191+
/// // age: int
192+
/// </code>
193+
/// </remarks>
194+
[<Erase>]
195+
[<ClassAttributesTemplate(ClassAttributeStyle.Attributes, false)>]
196+
type DataClassAttribute() =
197+
inherit Attribute()
89198

90199
// Hack because currently Fable doesn't keep information about spread for anonymous functions
91200
[<Emit("lambda *args: $0(args)")>]

src/Fable.Transforms/FSharp2Fable.Util.fs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2111,6 +2111,20 @@ module Util =
21112111
| _ -> false
21122112
)
21132113

2114+
/// Checks if an attribute is a Python class attribute, either directly via [<Py.ClassAttributes>]
2115+
/// or indirectly via a custom attribute marked with [<Py.ClassAttributesTemplate>].
2116+
let private isPyClassAttribute (att: FSharpAttribute) =
2117+
match att.AttributeType.TryFullName with
2118+
| Some Atts.pyClassAttributes -> true
2119+
| _ ->
2120+
// Check if the attribute type itself has ClassAttributesTemplate
2121+
att.AttributeType.Attributes
2122+
|> Seq.exists (fun a ->
2123+
match a.AttributeType.TryFullName with
2124+
| Some Atts.pyClassAttributesTemplate -> true
2125+
| _ -> false
2126+
)
2127+
21142128
let isAttachMembersEntity (com: Compiler) (ent: FSharpEntity) =
21152129
not (ent.IsFSharpModule || ent.IsInterface)
21162130
&& (
@@ -2119,11 +2133,9 @@ module Util =
21192133
|| // attach all members for Rust
21202134
ent.Attributes
21212135
|> Seq.exists (fun att ->
2122-
// Should we make sure the attribute is not an alias?
21232136
match att.AttributeType.TryFullName with
21242137
| Some Atts.attachMembers -> true
2125-
| Some Atts.pyClassAttributes -> true
2126-
| _ -> false
2138+
| _ -> isPyClassAttribute att
21272139
))
21282140

21292141
let isPojoDefinedByConsArgsEntity (entity: Fable.Entity) =

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2674,7 +2674,7 @@ let declareClassType
26742674

26752675
// Generate custom decorators from [<Decorate>] attributes
26762676
let customDecorators =
2677-
let decoratorInfos = Util.getDecoratorInfo ent.Attributes
2677+
let decoratorInfos = Util.getDecoratorInfo com ent.Attributes
26782678
Util.generateDecorators com ctx decoratorInfos
26792679

26802680
stmts
@@ -2908,7 +2908,7 @@ let transformAttachedProperty
29082908
// Instance properties use the traditional approach (properties style)
29092909
// Get custom decorators from Py.Decorate attributes
29102910
let customDecorators =
2911-
let decoratorInfos = Util.getDecoratorInfo info.Attributes
2911+
let decoratorInfos = Util.getDecoratorInfo com info.Attributes
29122912
Util.generateDecorators com ctx decoratorInfos
29132913

29142914
let decorators =
@@ -2960,7 +2960,7 @@ let transformAttachedMethod (com: IPythonCompiler) ctx (info: Fable.MemberFuncti
29602960

29612961
// Get custom decorators from Py.Decorate attributes
29622962
let customDecorators =
2963-
let decoratorInfos = Util.getDecoratorInfo info.Attributes
2963+
let decoratorInfos = Util.getDecoratorInfo com info.Attributes
29642964
Util.generateDecorators com ctx decoratorInfos
29652965

29662966
let decorators =
@@ -3723,11 +3723,14 @@ let rec transformDeclaration (com: IPythonCompiler) ctx (decl: Fable.Declaration
37233723
elif hasEraseAttribute && ent.IsInterface then
37243724
// Erased interfaces should not generate any code
37253725
[]
3726+
elif hasEraseAttribute then
3727+
// Erased classes (e.g., custom attribute types) should not generate any code
3728+
[]
37263729
else
37273730
// Check for PythonClass attribute and extract parameters
37283731
let classAttributes =
3729-
if hasPythonClassAttribute ent.Attributes then
3730-
getPythonClassParameters ent.Attributes
3732+
if hasPythonClassAttribute com ent.Attributes then
3733+
getPythonClassParameters com ent.Attributes
37313734
else
37323735
ClassAttributes.Default
37333736

0 commit comments

Comments
 (0)