55from pathlib import Path
66from importlib .machinery import SourceFileLoader
77from pkgutil import iter_modules
8- from typing import (
9- List ,
10- Dict ,
11- Optional ,
12- Any ,
13- NamedTuple ,
14- Iterable ,
15- TypeVar ,
16- Callable ,
17- Iterator ,
18- )
19-
20- from .sh import echo
21- from .utils import split_package_name_and_version
8+ from typing import List , Dict , Optional , Any , NamedTuple , Iterable , Iterator , TypeVar
229
10+ from .utils import split_package_name_and_version
2311
24- _F = TypeVar ( "_F" , bound = Callable [..., Any ])
12+ from idom . cli import console
2513
2614
27- class BuildState :
15+ class BuildConfigFile :
2816
29- __slots__ = "save_location" , "configs"
17+ __slots__ = "configs" , "_path"
18+ _filename = ".idom-build-configs.json"
3019
3120 def __init__ (self , path : Path ) -> None :
32- self .save_location = path
21+ self ._path = path / self . _filename
3322 self .configs = self ._load_configs ()
3423
3524 @contextmanager
@@ -45,15 +34,15 @@ def transaction(self) -> Iterator[None]:
4534 self .save ()
4635
4736 def add (self , build_configs : Iterable [Any ], ignore_existing : bool = False ) -> None :
48- for config in map (to_build_config , build_configs ):
37+ for config in map (BuildConfigItem . cast , build_configs ):
4938 source_name = config .source_name
5039 if not ignore_existing and source_name in self .configs :
5140 raise ValueError (f"A build config for { source_name !r} already exists" )
5241 self .configs [source_name ] = config
5342 return None
5443
5544 def save (self ) -> None :
56- with ( self .save_location / ".idom-build-state.json" ) .open ("w" ) as f :
45+ with self ._path .open ("w" ) as f :
5746 json .dump ({name : conf ._asdict () for name , conf in self .configs .items ()}, f )
5847
5948 def show (self , indent : int = 2 ) -> str :
@@ -65,22 +54,39 @@ def show(self, indent: int = 2) -> str:
6554 def clear (self ) -> None :
6655 self .configs = {}
6756
68- def _load_configs (self ) -> Dict [str , "BuildConfig" ]:
69- fname = self .save_location / ".idom-build-state.json"
70- if not fname .exists ():
57+ def _load_configs (self ) -> Dict [str , "BuildConfigItem" ]:
58+ if not self ._path .exists ():
7159 return {}
72- with fname .open () as f :
60+ with self . _path .open () as f :
7361 content = f .read ()
7462 if not content :
7563 return {}
7664 else :
77- return {n : BuildConfig (** c ) for n , c in json .loads (content ).items ()}
65+ return {n : BuildConfigItem (** c ) for n , c in json .loads (content ).items ()}
7866
7967 def __repr__ (self ) -> str :
8068 return f"{ type (self ).__name__ } ({ self .show (indent = 0 )} )"
8169
8270
83- class BuildConfig (NamedTuple ):
71+ _Class = TypeVar ("_Class" )
72+ _Self = TypeVar ("_Self" )
73+
74+
75+ class BuildConfigItem (NamedTuple ):
76+ """Describes build requirements for a Python package or application"""
77+
78+ @classmethod
79+ def cast (cls : _Class , value : Any , source_name : Optional [str ] = None ) -> _Class :
80+ if isinstance (value , cls ):
81+ return value
82+ elif isinstance (value , tuple ):
83+ return cls (* value )._validate ()
84+ elif isinstance (value , dict ):
85+ if source_name is not None :
86+ value = {"source_name" : source_name , ** value }
87+ return cls (** value )._validate ()
88+ else :
89+ raise ValueError (f"Expected a dict or tuple, not { value !r} " )
8490
8591 source_name : str
8692 js_dependencies : List [str ]
@@ -100,42 +106,33 @@ def aliased_js_dependencies(self) -> List[str]:
100106 aliased_dependencies .append (f"{ name } -{ idf } @npm:{ dep } " )
101107 return aliased_dependencies
102108
103-
104- def to_build_config (config : Any , source_name : Optional [str ] = None ) -> BuildConfig :
105- if isinstance (config , BuildConfig ):
106- return config
107-
108- if not isinstance (config , dict ):
109- raise ValueError (
110- f"build config must be a dictionary, but found { config !r} " ,
111- )
112-
113- source_name = config .setdefault ("source_name" , source_name )
114- if not isinstance (source_name , str ):
115- raise ValueError (f"'source_name' must be a string, not { source_name !r} " )
116-
117- js_dependencies = config .get ("js_dependencies" )
118- if not isinstance (js_dependencies , list ):
119- raise ValueError (f"'js_dependencies' must be a list, not { js_dependencies !r} " )
120- for item in js_dependencies :
121- if not isinstance (item , str ):
109+ def _validate (self : _Self ) -> _Self :
110+ if not isinstance (self .source_name , str ):
122111 raise ValueError (
123- f"items of 'js_dependencies ' must be strings , not { item !r} "
112+ f"'source_name ' must be a string , not { self . source_name !r} "
124113 )
125-
126- return BuildConfig (** config )
114+ if not isinstance (self .js_dependencies , list ):
115+ raise ValueError (
116+ f"'js_dependencies' must be a list, not { self .js_dependencies !r} "
117+ )
118+ for item in self .js_dependencies :
119+ if not isinstance (item , str ):
120+ raise ValueError (
121+ f"items of 'js_dependencies' must be strings, not { item !r} "
122+ )
123+ return self
127124
128125
129- def find_build_config_in_python_source (
126+ def find_build_config_item_in_python_source (
130127 module_name : str , path : Path
131- ) -> Optional [BuildConfig ]:
128+ ) -> Optional [BuildConfigItem ]:
132129 with path .open () as f :
133- return _parse_build_config_from_python_source (module_name , f .read ())
130+ return _parse_build_config_item_from_python_source (module_name , f .read ())
134131
135132
136- def find_python_packages_build_configs (
133+ def find_python_packages_build_config_items (
137134 path : Optional [str ] = None ,
138- ) -> List [BuildConfig ]:
135+ ) -> List [BuildConfigItem ]:
139136 """Find javascript dependencies declared by Python modules
140137
141138 Parameters:
@@ -146,20 +143,22 @@ def find_python_packages_build_configs(
146143 Returns:
147144 Mapping of module names to their corresponding list of discovered dependencies.
148145 """
149- build_configs : List [BuildConfig ] = []
146+ build_configs : List [BuildConfigItem ] = []
150147 for module_info in iter_modules (path ):
151148 module_loader = module_info .module_finder .find_module (module_info .name )
152149 if isinstance (module_loader , SourceFileLoader ):
153150 module_src = module_loader .get_source (module_info .name )
154- conf = _parse_build_config_from_python_source (module_info .name , module_src )
151+ conf = _parse_build_config_item_from_python_source (
152+ module_info .name , module_src
153+ )
155154 if conf is not None :
156155 build_configs .append (conf )
157156 return build_configs
158157
159158
160- def _parse_build_config_from_python_source (
159+ def _parse_build_config_item_from_python_source (
161160 module_name : str , module_src : str
162- ) -> Optional [BuildConfig ]:
161+ ) -> Optional [BuildConfigItem ]:
163162 for node in ast .parse (module_src ).body :
164163 if isinstance (node , ast .Assign ) and (
165164 len (node .targets ) == 1
@@ -175,14 +174,14 @@ def _parse_build_config_from_python_source(
175174 return None
176175
177176 try :
178- return to_build_config (raw_config , module_name )
177+ return BuildConfigItem . cast (raw_config , module_name )
179178 except ValueError as error :
180179 _echo_error (
181- f"Failed to load build config for { module_name !r} - { error } "
180+ f"Failed to load build config for { module_name !r} because { error } "
182181 )
183182 return None
184183 return None
185184
186185
187186def _echo_error (msg : str ) -> None :
188- echo (msg , color = "red" )
187+ console . echo (msg , color = "red" )
0 commit comments