@@ -34,70 +34,90 @@ def _make_feature_decorator(
3434 default_message : str ,
3535 block_usage : bool = False ,
3636 bypass_env_var : Optional [str ] = None ,
37- ) -> Callable [[str ], Callable [[T ], T ]]:
38- def decorator_factory (message : str = default_message ) -> Callable [[T ], T ]:
39- def decorator (obj : T ) -> T :
40- obj_name = getattr (obj , "__name__" , type (obj ).__name__ )
41- msg = f"[{ label .upper ()} ] { obj_name } : { message } "
42-
43- if isinstance (obj , type ): # decorating a class
44- orig_init = obj .__init__
45-
46- @functools .wraps (orig_init )
47- def new_init (self , * args , ** kwargs ):
48- # Load .env file if dotenv is available
49- load_dotenv ()
50-
51- # Check if usage should be bypassed via environment variable at call time
52- should_bypass = (
53- bypass_env_var is not None
54- and os .environ .get (bypass_env_var , "" ).lower () == "true"
55- )
56-
57- if should_bypass :
58- # Bypass completely - no warning, no error
59- pass
60- elif block_usage :
61- raise RuntimeError (msg )
62- else :
63- warnings .warn (msg , category = UserWarning , stacklevel = 2 )
64- return orig_init (self , * args , ** kwargs )
65-
66- obj .__init__ = new_init # type: ignore[attr-defined]
67- return cast (T , obj )
68-
69- elif callable (obj ): # decorating a function or method
70-
71- @functools .wraps (obj )
72- def wrapper (* args , ** kwargs ):
73- # Load .env file if dotenv is available
74- load_dotenv ()
75-
76- # Check if usage should be bypassed via environment variable at call time
77- should_bypass = (
78- bypass_env_var is not None
79- and os .environ .get (bypass_env_var , "" ).lower () == "true"
80- )
81-
82- if should_bypass :
83- # Bypass completely - no warning, no error
84- pass
85- elif block_usage :
86- raise RuntimeError (msg )
87- else :
88- warnings .warn (msg , category = UserWarning , stacklevel = 2 )
89- return obj (* args , ** kwargs )
90-
91- return cast (T , wrapper )
92-
93- else :
94- raise TypeError (
95- f"@{ label } can only be applied to classes or callable objects"
37+ ) -> Callable :
38+ def decorator_factory (message_or_obj = None ):
39+ # Case 1: Used as @decorator without parentheses
40+ # message_or_obj is the decorated class/function
41+ if message_or_obj is not None and (
42+ isinstance (message_or_obj , type ) or callable (message_or_obj )
43+ ):
44+ return _create_decorator (
45+ default_message , label , block_usage , bypass_env_var
46+ )(message_or_obj )
47+
48+ # Case 2: Used as @decorator() with or without message
49+ # message_or_obj is either None or a string message
50+ message = (
51+ message_or_obj if isinstance (message_or_obj , str ) else default_message
52+ )
53+ return _create_decorator (message , label , block_usage , bypass_env_var )
54+
55+ return decorator_factory
56+
57+
58+ def _create_decorator (
59+ message : str , label : str , block_usage : bool , bypass_env_var : Optional [str ]
60+ ) -> Callable [[T ], T ]:
61+ def decorator (obj : T ) -> T :
62+ obj_name = getattr (obj , "__name__" , type (obj ).__name__ )
63+ msg = f"[{ label .upper ()} ] { obj_name } : { message } "
64+
65+ if isinstance (obj , type ): # decorating a class
66+ orig_init = obj .__init__
67+
68+ @functools .wraps (orig_init )
69+ def new_init (self , * args , ** kwargs ):
70+ # Load .env file if dotenv is available
71+ load_dotenv ()
72+
73+ # Check if usage should be bypassed via environment variable at call time
74+ should_bypass = (
75+ bypass_env_var is not None
76+ and os .environ .get (bypass_env_var , "" ).lower () == "true"
9677 )
9778
98- return decorator
79+ if should_bypass :
80+ # Bypass completely - no warning, no error
81+ pass
82+ elif block_usage :
83+ raise RuntimeError (msg )
84+ else :
85+ warnings .warn (msg , category = UserWarning , stacklevel = 2 )
86+ return orig_init (self , * args , ** kwargs )
87+
88+ obj .__init__ = new_init # type: ignore[attr-defined]
89+ return cast (T , obj )
90+
91+ elif callable (obj ): # decorating a function or method
92+
93+ @functools .wraps (obj )
94+ def wrapper (* args , ** kwargs ):
95+ # Load .env file if dotenv is available
96+ load_dotenv ()
97+
98+ # Check if usage should be bypassed via environment variable at call time
99+ should_bypass = (
100+ bypass_env_var is not None
101+ and os .environ .get (bypass_env_var , "" ).lower () == "true"
102+ )
99103
100- return decorator_factory
104+ if should_bypass :
105+ # Bypass completely - no warning, no error
106+ pass
107+ elif block_usage :
108+ raise RuntimeError (msg )
109+ else :
110+ warnings .warn (msg , category = UserWarning , stacklevel = 2 )
111+ return obj (* args , ** kwargs )
112+
113+ return cast (T , wrapper )
114+
115+ else :
116+ raise TypeError (
117+ f"@{ label } can only be applied to classes or callable objects"
118+ )
119+
120+ return decorator
101121
102122
103123working_in_progress = _make_feature_decorator (
@@ -137,8 +157,19 @@ def my_wip_function():
137157Sample usage:
138158
139159```
140- @experimental("This API may have breaking change in the future.")
160+ # Use with default message
161+ @experimental
141162class ExperimentalClass:
142163 pass
164+
165+ # Use with custom message
166+ @experimental("This API may have breaking change in the future.")
167+ class CustomExperimentalClass:
168+ pass
169+
170+ # Use with empty parentheses (same as default message)
171+ @experimental()
172+ def experimental_function():
173+ pass
143174```
144175"""
0 commit comments