1212from .parser import Binding , parse_stream
1313from .variables import parse_variables
1414
15+ _DUPLICATE_VALUES = ("warn" , "raise" , "ignore" )
16+
1517# A type alias for a string path to be used for the paths in this file.
1618# These paths may flow to `open()` and `os.replace()`.
1719StrPath = Union [str , "os.PathLike[str]" ]
@@ -48,14 +50,21 @@ def __init__(
4850 encoding : Optional [str ] = None ,
4951 interpolate : bool = True ,
5052 override : bool = True ,
53+ on_duplicate : str = "warn" ,
5154 ) -> None :
55+ if on_duplicate not in _DUPLICATE_VALUES :
56+ raise ValueError (
57+ f"Invalid value for on_duplicate: { on_duplicate !r} . "
58+ f"Expected one of: { ', ' .join (_DUPLICATE_VALUES )} "
59+ )
5260 self .dotenv_path : Optional [StrPath ] = dotenv_path
5361 self .stream : Optional [IO [str ]] = stream
5462 self ._dict : Optional [Dict [str , Optional [str ]]] = None
5563 self .verbose : bool = verbose
5664 self .encoding : Optional [str ] = encoding
5765 self .interpolate : bool = interpolate
5866 self .override : bool = override
67+ self .on_duplicate : str = on_duplicate
5968
6069 @contextmanager
6170 def _get_stream (self ) -> Iterator [IO [str ]]:
@@ -90,8 +99,26 @@ def dict(self) -> Dict[str, Optional[str]]:
9099
91100 def parse (self ) -> Iterator [Tuple [str , Optional [str ]]]:
92101 with self ._get_stream () as stream :
102+ seen_keys : Dict [str , int ] = {}
93103 for mapping in with_warn_for_invalid_lines (parse_stream (stream )):
94104 if mapping .key is not None :
105+ if mapping .key in seen_keys :
106+ msg = (
107+ "Duplicate key %r found in %s "
108+ "(first defined on line %d, redefined on line %d)."
109+ )
110+ args = (
111+ mapping .key ,
112+ self .dotenv_path or "<stream>" ,
113+ seen_keys [mapping .key ],
114+ mapping .original .line ,
115+ )
116+ if self .on_duplicate == "raise" :
117+ raise ValueError (msg % args )
118+ elif self .on_duplicate == "warn" :
119+ logger .warning (msg , * args )
120+ else :
121+ seen_keys [mapping .key ] = mapping .original .line
95122 yield mapping .key , mapping .value
96123
97124 def set_as_environment_variables (self ) -> bool :
@@ -387,6 +414,7 @@ def load_dotenv(
387414 override : bool = False ,
388415 interpolate : bool = True ,
389416 encoding : Optional [str ] = "utf-8" ,
417+ on_duplicate : str = "warn" ,
390418) -> bool :
391419 """Parse a .env file and then load all the variables found as environment variables.
392420
@@ -426,6 +454,7 @@ def load_dotenv(
426454 interpolate = interpolate ,
427455 override = override ,
428456 encoding = encoding ,
457+ on_duplicate = on_duplicate ,
429458 )
430459 return dotenv .set_as_environment_variables ()
431460
@@ -436,6 +465,7 @@ def dotenv_values(
436465 verbose : bool = False ,
437466 interpolate : bool = True ,
438467 encoding : Optional [str ] = "utf-8" ,
468+ on_duplicate : str = "warn" ,
439469) -> Dict [str , Optional [str ]]:
440470 """
441471 Parse a .env file and return its content as a dict.
@@ -464,6 +494,7 @@ def dotenv_values(
464494 interpolate = interpolate ,
465495 override = True ,
466496 encoding = encoding ,
497+ on_duplicate = on_duplicate ,
467498 ).dict ()
468499
469500
0 commit comments