44 "encoding/json"
55 "os"
66 "path/filepath"
7+ "strings"
78 "testing"
89)
910
@@ -21,7 +22,13 @@ const baselineInvocation = "types ./types"
2122func loadCollections (t * testing.T ) []Collection {
2223 t .Helper ()
2324
24- payload , err := os .ReadFile (filepath .Join ("testdata" , "collections.json" ))
25+ return loadFixture (t , "collections.json" )
26+ }
27+
28+ func loadFixture (t * testing.T , name string ) []Collection {
29+ t .Helper ()
30+
31+ payload , err := os .ReadFile (filepath .Join ("testdata" , name ))
2532 if err != nil {
2633 t .Fatalf ("reading fixture: %v" , err )
2734 }
@@ -52,7 +59,13 @@ func loadBaseline(t *testing.T, name string) string {
5259func assertRendersBaseline (t * testing.T , language Language , current * Collection , strict bool , baseline string ) {
5360 t .Helper ()
5461
55- rendered , err := language .Render (loadCollections (t ), current , strict , baselineInvocation )
62+ assertRendersFixture (t , language , loadCollections (t ), current , strict , baseline )
63+ }
64+
65+ func assertRendersFixture (t * testing.T , language Language , collections []Collection , current * Collection , strict bool , baseline string ) {
66+ t .Helper ()
67+
68+ rendered , err := language .Render (collections , current , strict , baselineInvocation )
5669 if err != nil {
5770 t .Fatalf ("render: %v" , err )
5871 }
@@ -183,6 +196,105 @@ func TestCSharpMatchesBaseline(t *testing.T) {
183196 assertMultiFileBaselines (t , CSharp {}, "cs" )
184197}
185198
199+ func TestPythonMatchesBaseline (t * testing.T ) {
200+ for _ , fixture := range []struct { prefix , file string }{
201+ {"python" , "collections.json" },
202+ {"python.coverage" , "python.collections.json" },
203+ } {
204+ for _ , mode := range []string {"loose" , "strict" } {
205+ t .Run (fixture .prefix + "." + mode , func (t * testing.T ) {
206+ baseline := fixture .prefix + "." + mode + "." + (Python {}).FileName (nil )
207+ assertRendersFixture (t , Python {}, loadFixture (t , fixture .file ), nil , mode == "strict" , baseline )
208+ })
209+ }
210+ }
211+ }
212+
213+ func TestPythonFieldNamesAreUsableIdentifiers (t * testing.T ) {
214+ attributes := []Attribute {
215+ {Key : "fullName" }, {Key : "full_name" }, {Key : "class" }, {Key : "_hidden" },
216+ {Key : "9lives" }, {Key : "!!!" }, {Key : "model_config" }, {Key : "Items" }, {Key : "published.at" },
217+ }
218+ classes := pythonClasses {order : []string {"Items" }}
219+
220+ for strict , want := range map [bool ][]string {
221+ false : {"fullName" , "full_name" , "class_" , "hidden" , "field_9lives" , "field" , "model_config_" , "Items_" , "published_at" },
222+ true : {"full_name_1" , "full_name" , "class_" , "hidden" , "field_9lives" , "field" , "model_config_" , "items" , "published_at" },
223+ } {
224+ got := pythonFieldNames (attributes , strict , classes )
225+ if strings .Join (got , " " ) != strings .Join (want , " " ) {
226+ t .Errorf ("strict=%v:\n want %v\n got %v" , strict , want , got )
227+ }
228+ }
229+ }
230+
231+ func TestPythonClassNamesNeverCollide (t * testing.T ) {
232+ classes := newPythonClasses ([]Collection {
233+ {ID : "a" , Name : "Orders Status" },
234+ {ID : "b" , Name : "Orders" , Attributes : []Attribute {{Key : "status" , Format : AttributeTypeEnum }}},
235+ {ID : "c" , Name : "orders" },
236+ {ID : "d" , Name : "2024 sales" },
237+ })
238+
239+ want := map [string ]string {"a" : "OrdersStatus" , "b" : "Orders" , "c" : "Orders1" , "d" : "Model2024Sales" }
240+ for id , name := range want {
241+ if model := classes .models [pythonModelKey (Collection {ID : id })]; model != name {
242+ t .Errorf ("model %s = %q, want %q" , id , model , name )
243+ }
244+ }
245+ if enum := classes .enums [pythonEnumKey (Collection {ID : "b" }, "status" )]; enum != "OrdersStatus1" {
246+ t .Errorf ("enum = %q, want OrdersStatus1" , enum )
247+ }
248+ }
249+
250+ func TestPythonModelsAreKeyedByDatabase (t * testing.T ) {
251+ collections := []Collection {
252+ {ID : "items" , DatabaseID : "a" , Name : "First" , Attributes : []Attribute {
253+ {Key : "status" , Type : AttributeTypeString , Format : AttributeTypeEnum , Required : true , Elements : []string {"open" }},
254+ {Key : "owner" , Type : AttributeTypeRelationship , Required : true , RelatedTable : "owners" , RelationType : "oneToOne" , Side : "parent" },
255+ }},
256+ {ID : "items" , DatabaseID : "b" , Name : "Second" , Attributes : []Attribute {
257+ {Key : "status" , Type : AttributeTypeString , Format : AttributeTypeEnum , Required : true , Elements : []string {"closed" }},
258+ {Key : "owner" , Type : AttributeTypeRelationship , Required : true , RelatedTable : "owners" , RelationType : "oneToOne" , Side : "parent" },
259+ }},
260+ {ID : "owners" , DatabaseID : "a" , Name : "First Owners" },
261+ {ID : "owners" , DatabaseID : "b" , Name : "Second Owners" },
262+ }
263+
264+ rendered , err := (Python {}).Render (collections , nil , false , baselineInvocation )
265+ if err != nil {
266+ t .Fatalf ("render: %v" , err )
267+ }
268+
269+ for _ , want := range []string {
270+ "class FirstStatus(str, Enum):\n OPEN = \" open\" \n " ,
271+ "class SecondStatus(str, Enum):\n CLOSED = \" closed\" \n " ,
272+ "class First(BaseModel):\n model_config = ConfigDict(extra=\" allow\" , populate_by_name=True)\n \n status: FirstStatus\n owner: Union[FirstOwners, str]\n " ,
273+ "class Second(BaseModel):\n model_config = ConfigDict(extra=\" allow\" , populate_by_name=True)\n \n status: SecondStatus\n owner: Union[SecondOwners, str]\n " ,
274+ "class FirstOwners(BaseModel):" ,
275+ "class SecondOwners(BaseModel):" ,
276+ } {
277+ if strings .Count (rendered , want ) != 1 {
278+ t .Errorf ("want exactly one %q in:\n %s" , want , rendered )
279+ }
280+ }
281+ }
282+
283+ func TestPythonStringMatchesBlackQuoting (t * testing.T ) {
284+ for value , want := range map [string ]string {
285+ "plain" : `"plain"` ,
286+ `say "hi"` : `'say "hi"'` ,
287+ "it's" : `"it's"` ,
288+ `it's "x"` : `'it\'s "x"'` ,
289+ `it's "x` : `"it's \"x"` ,
290+ "a\\ b\n c\x01 d" : `"a\\b\nc\x01d"` ,
291+ } {
292+ if got := pythonString (value ); got != want {
293+ t .Errorf ("pythonString(%q) = %s, want %s" , value , got , want )
294+ }
295+ }
296+ }
297+
186298// TestPHPArrayNeverGainsNullBranch pins the early return in PHP.Type: an
187299// optional array attribute is `array`, not `array|null`, because the array
188300// check precedes the nullable suffix.
0 commit comments