33import importlib
44import os
55from dataclasses import dataclass , field
6- from typing import Callable , Dict , Iterable , List , Tuple , TypedDict , TypeVar
6+ from typing import (
7+ Awaitable ,
8+ Callable ,
9+ Dict ,
10+ Iterable ,
11+ List ,
12+ NewType ,
13+ Tuple ,
14+ TypedDict ,
15+ TypeVar ,
16+ )
717
818from sanic .log import logger
919
@@ -83,11 +93,13 @@ class NodeData:
8393 side_effects : bool
8494 deprecated : bool
8595 default_nodes : List [DefaultNode ] | None # For iterators only
96+ features : List [FeatureId ]
8697
8798 run : RunFn
8899
89100
90101T = TypeVar ("T" , bound = RunFn )
102+ S = TypeVar ("S" )
91103
92104
93105@dataclass
@@ -114,14 +126,20 @@ def register(
114126 default_nodes : List [DefaultNode ] | None = None ,
115127 decorators : List [Callable ] | None = None ,
116128 see_also : List [str ] | str | None = None ,
129+ features : List [FeatureId ] | FeatureId | None = None ,
117130 ):
118131 if not isinstance (description , str ):
119132 description = "\n \n " .join (description )
120133
121- if see_also is None :
122- see_also = []
123- if isinstance (see_also , str ):
124- see_also = [see_also ]
134+ def to_list (x : List [S ] | S | None ) -> List [S ]:
135+ if x is None :
136+ return []
137+ if isinstance (x , list ):
138+ return x
139+ return [x ]
140+
141+ see_also = to_list (see_also )
142+ features = to_list (features )
125143
126144 def run_check (level : CheckLevel , run : Callable [[bool ], None ]):
127145 if level == CheckLevel .NONE :
@@ -170,6 +188,7 @@ def inner_wrapper(wrapped_func: T) -> T:
170188 side_effects = side_effects ,
171189 deprecated = deprecated ,
172190 default_nodes = default_nodes ,
191+ features = features ,
173192 run = wrapped_func ,
174193 )
175194
@@ -226,13 +245,59 @@ def toDict(self):
226245 }
227246
228247
248+ FeatureId = NewType ("FeatureId" , str )
249+
250+
251+ @dataclass
252+ class Feature :
253+ id : str
254+ name : str
255+ description : str
256+ behavior : FeatureBehavior | None = None
257+
258+ def add_behavior (self , check : Callable [[], Awaitable [FeatureState ]]) -> FeatureId :
259+ if self .behavior is not None :
260+ raise ValueError ("Behavior already set" )
261+
262+ self .behavior = FeatureBehavior (check = check )
263+ return FeatureId (self .id )
264+
265+ def toDict (self ):
266+ return {
267+ "id" : self .id ,
268+ "name" : self .name ,
269+ "description" : self .description ,
270+ }
271+
272+
273+ @dataclass
274+ class FeatureBehavior :
275+ check : Callable [[], Awaitable [FeatureState ]]
276+
277+
278+ @dataclass (frozen = True )
279+ class FeatureState :
280+ is_enabled : bool
281+ details : str | None = None
282+
283+ @staticmethod
284+ def enabled (details : str | None = None ) -> "FeatureState" :
285+ return FeatureState (is_enabled = True , details = details )
286+
287+ @staticmethod
288+ def disabled (details : str | None = None ) -> "FeatureState" :
289+ return FeatureState (is_enabled = False , details = details )
290+
291+
229292@dataclass
230293class Package :
231294 where : str
295+ id : str
232296 name : str
233297 description : str
234298 dependencies : List [Dependency ] = field (default_factory = list )
235299 categories : List [Category ] = field (default_factory = list )
300+ features : List [Feature ] = field (default_factory = list )
236301
237302 def add_category (
238303 self ,
@@ -259,6 +324,19 @@ def add_dependency(
259324 ):
260325 self .dependencies .append (dependency )
261326
327+ def add_feature (
328+ self ,
329+ id : str , # pylint: disable=redefined-builtin
330+ name : str ,
331+ description : str ,
332+ ) -> Feature :
333+ if any (f .id == id for f in self .features ):
334+ raise ValueError (f"Duplicate feature id: { id } " )
335+
336+ feature = Feature (id = id , name = name , description = description )
337+ self .features .append (feature )
338+ return feature
339+
262340
263341def _iter_py_files (directory : str ):
264342 for root , _ , files in os .walk (directory ):
@@ -331,6 +409,18 @@ def _refresh_nodes(self):
331409
332410
333411def add_package (
334- where : str , name : str , description : str , dependencies : List [Dependency ]
412+ where : str ,
413+ id : str , # pylint: disable=redefined-builtin
414+ name : str ,
415+ description : str ,
416+ dependencies : List [Dependency ] | None = None ,
335417) -> Package :
336- return registry .add (Package (where , name , description , dependencies ))
418+ return registry .add (
419+ Package (
420+ where = where ,
421+ id = id ,
422+ name = name ,
423+ description = description ,
424+ dependencies = dependencies or [],
425+ )
426+ )
0 commit comments