@@ -1416,6 +1416,33 @@ class Settings(BaseSettings):
1416
1416
assert Settings ().model_dump () == {'foo' : 'foo_secret_value_str' }
1417
1417
1418
1418
1419
+ def test_secrets_path_multiple (tmp_path ):
1420
+ d1 = tmp_path / 'dir1'
1421
+ d2 = tmp_path / 'dir2'
1422
+ d1 .mkdir ()
1423
+ d2 .mkdir ()
1424
+ (d1 / 'foo1' ).write_text ('foo1_dir1_secret_value_str' )
1425
+ (d1 / 'foo2' ).write_text ('foo2_dir1_secret_value_str' )
1426
+ (d2 / 'foo2' ).write_text ('foo2_dir2_secret_value_str' )
1427
+ (d2 / 'foo3' ).write_text ('foo3_dir2_secret_value_str' )
1428
+
1429
+ class Settings (BaseSettings ):
1430
+ foo1 : str
1431
+ foo2 : str
1432
+ foo3 : str
1433
+
1434
+ assert Settings (_secrets_dir = (d1 , d2 )).model_dump () == {
1435
+ 'foo1' : 'foo1_dir1_secret_value_str' ,
1436
+ 'foo2' : 'foo2_dir2_secret_value_str' , # dir2 takes priority
1437
+ 'foo3' : 'foo3_dir2_secret_value_str' ,
1438
+ }
1439
+ assert Settings (_secrets_dir = (d2 , d1 )).model_dump () == {
1440
+ 'foo1' : 'foo1_dir1_secret_value_str' ,
1441
+ 'foo2' : 'foo2_dir1_secret_value_str' , # dir1 takes priority
1442
+ 'foo3' : 'foo3_dir2_secret_value_str' ,
1443
+ }
1444
+
1445
+
1419
1446
def test_secrets_path_with_validation_alias (tmp_path ):
1420
1447
p = tmp_path / 'foo'
1421
1448
p .write_text ('{"bar": ["test"]}' )
@@ -1535,6 +1562,28 @@ class Settings(BaseSettings):
1535
1562
Settings ()
1536
1563
1537
1564
1565
+ def test_secrets_invalid_secrets_dir_multiple_all (tmp_path ):
1566
+ class Settings (BaseSettings ):
1567
+ foo : str
1568
+
1569
+ (d1 := tmp_path / 'dir1' ).write_text ('' )
1570
+ (d2 := tmp_path / 'dir2' ).write_text ('' )
1571
+
1572
+ with pytest .raises (SettingsError , match = 'secrets_dir must reference a directory, not a file' ):
1573
+ Settings (_secrets_dir = [d1 , d2 ])
1574
+
1575
+
1576
+ def test_secrets_invalid_secrets_dir_multiple_one (tmp_path ):
1577
+ class Settings (BaseSettings ):
1578
+ foo : str
1579
+
1580
+ (d1 := tmp_path / 'dir1' ).mkdir ()
1581
+ (d2 := tmp_path / 'dir2' ).write_text ('' )
1582
+
1583
+ with pytest .raises (SettingsError , match = 'secrets_dir must reference a directory, not a file' ):
1584
+ Settings (_secrets_dir = [d1 , d2 ])
1585
+
1586
+
1538
1587
@pytest .mark .skipif (sys .platform .startswith ('win' ), reason = 'windows paths break regex' )
1539
1588
def test_secrets_missing_location (tmp_path ):
1540
1589
class Settings (BaseSettings ):
@@ -1544,6 +1593,34 @@ class Settings(BaseSettings):
1544
1593
Settings ()
1545
1594
1546
1595
1596
+ @pytest .mark .skipif (sys .platform .startswith ('win' ), reason = 'windows paths break regex' )
1597
+ def test_secrets_missing_location_multiple_all (tmp_path ):
1598
+ class Settings (BaseSettings ):
1599
+ foo : Optional [str ] = None
1600
+
1601
+ with pytest .warns () as record :
1602
+ Settings (_secrets_dir = [tmp_path / 'dir1' , tmp_path / 'dir2' ])
1603
+
1604
+ assert len (record ) == 2
1605
+ assert record [0 ].category is UserWarning and record [1 ].category is UserWarning
1606
+ assert str (record [0 ].message ) == f'directory "{ tmp_path } /dir1" does not exist'
1607
+ assert str (record [1 ].message ) == f'directory "{ tmp_path } /dir2" does not exist'
1608
+
1609
+
1610
+ @pytest .mark .skipif (sys .platform .startswith ('win' ), reason = 'windows paths break regex' )
1611
+ def test_secrets_missing_location_multiple_one (tmp_path ):
1612
+ class Settings (BaseSettings ):
1613
+ foo : Optional [str ] = None
1614
+
1615
+ (d1 := tmp_path / 'dir1' ).mkdir ()
1616
+ (d1 / 'foo' ).write_text ('secret_value' )
1617
+
1618
+ with pytest .warns (UserWarning , match = f'directory "{ tmp_path } /dir2" does not exist' ):
1619
+ conf = Settings (_secrets_dir = [d1 , tmp_path / 'dir2' ])
1620
+
1621
+ assert conf .foo == 'secret_value' # value obtained from first directory
1622
+
1623
+
1547
1624
@pytest .mark .skipif (sys .platform .startswith ('win' ), reason = 'windows paths break regex' )
1548
1625
def test_secrets_file_is_a_directory (tmp_path ):
1549
1626
p1 = tmp_path / 'foo'
@@ -1558,6 +1635,42 @@ class Settings(BaseSettings):
1558
1635
Settings ()
1559
1636
1560
1637
1638
+ @pytest .mark .skipif (sys .platform .startswith ('win' ), reason = 'windows paths break regex' )
1639
+ def test_secrets_file_is_a_directory_multiple_all (tmp_path ):
1640
+ class Settings (BaseSettings ):
1641
+ foo : Optional [str ] = None
1642
+
1643
+ (d1 := tmp_path / 'dir1' ).mkdir ()
1644
+ (d2 := tmp_path / 'dir2' ).mkdir ()
1645
+ (d1 / 'foo' ).mkdir ()
1646
+ (d2 / 'foo' ).mkdir ()
1647
+
1648
+ with pytest .warns () as record :
1649
+ Settings (_secrets_dir = [d1 , d2 ])
1650
+
1651
+ assert len (record ) == 2
1652
+ assert record [0 ].category is UserWarning and record [1 ].category is UserWarning
1653
+ # warnings are emitted in reverse order
1654
+ assert str (record [0 ].message ) == f'attempted to load secret file "{ d2 } /foo" but found a directory instead.'
1655
+ assert str (record [1 ].message ) == f'attempted to load secret file "{ d1 } /foo" but found a directory instead.'
1656
+
1657
+
1658
+ @pytest .mark .skipif (sys .platform .startswith ('win' ), reason = 'windows paths break regex' )
1659
+ def test_secrets_file_is_a_directory_multiple_one (tmp_path ):
1660
+ class Settings (BaseSettings ):
1661
+ foo : Optional [str ] = None
1662
+
1663
+ (d1 := tmp_path / 'dir1' ).mkdir ()
1664
+ (d2 := tmp_path / 'dir2' ).mkdir ()
1665
+ (d1 / 'foo' ).write_text ('secret_value' )
1666
+ (d2 / 'foo' ).mkdir ()
1667
+
1668
+ with pytest .warns (UserWarning , match = f'attempted to load secret file "{ d2 } /foo" but found a directory instead.' ):
1669
+ conf = Settings (_secrets_dir = [d1 , d2 ])
1670
+
1671
+ assert conf .foo == 'secret_value' # value obtained from first directory
1672
+
1673
+
1561
1674
def test_secrets_dotenv_precedence (tmp_path ):
1562
1675
s = tmp_path / 'foo'
1563
1676
s .write_text ('foo_secret_value_str' )
0 commit comments