3636import subprocess
3737import tempfile
3838
39+ # pyproject.toml `py_modules` values that are incorrect. These should all have PRs filed!
40+ # and should be removed when the fixed version is incorporated in its respective bundle.
41+
42+ pyproject_py_modules_blacklist = set ((
43+ # adafruit bundle
44+ "adafruit_colorsys" ,
45+
46+ # community bundle
47+ "at24mac_eeprom" ,
48+ "circuitpython_Candlesticks" ,
49+ "CircuitPython_Color_Picker" ,
50+ "CircuitPython_Equalizer" ,
51+ "CircuitPython_Scales" ,
52+ "circuitPython_Slider" ,
53+ "circuitpython_uboxplot" ,
54+ "P1AM" ,
55+ "p1am_200_helpers" ,
56+ ))
57+
58+ if sys .version_info >= (3 , 11 ):
59+ from tomllib import loads as load_toml
60+ else :
61+ from tomli import loads as load_toml
62+
63+ def load_pyproject_toml (lib_path : pathlib .Path ):
64+ try :
65+ return load_toml ((lib_path / "pyproject.toml" ) .read_text (encoding = "utf-8" ))
66+ except FileNotFoundError :
67+ print (f"No pyproject.toml in { lib_path } " )
68+ return {}
69+
70+ def get_nested (doc , * args , default = None ):
71+ for a in args :
72+ if doc is None : return default
73+ try :
74+ doc = doc [a ]
75+ except (KeyError , IndexError ) as e :
76+ return default
77+ return doc
78+
3979IGNORE_PY = ["setup.py" , "conf.py" , "__init__.py" ]
40- GLOB_PATTERNS = ["*.py" , "font5x8 .bin" ]
80+ GLOB_PATTERNS = ["*.py" , "* .bin" ]
4181S3_MPY_PREFIX = "https://adafruit-circuit-python.s3.amazonaws.com/bin/mpy-cross"
4282
4383def version_string (path = None , * , valid_semver = False ):
@@ -131,17 +171,13 @@ def mpy_cross(mpy_cross_filename, circuitpython_tag, quiet=False):
131171 shutil .copy ("build_deps/circuitpython/mpy-cross/mpy-cross" , mpy_cross_filename )
132172
133173def _munge_to_temp (original_path , temp_file , library_version ):
134- with open (original_path , "rb " ) as original_file :
174+ with open (original_path , "r" , encoding = "utf-8 " ) as original_file :
135175 for line in original_file :
136- if original_path .endswith (".bin" ):
137- # this is solely for adafruit_framebuf/examples/font5x8.bin
138- temp_file .write (line )
139- else :
140- line = line .decode ("utf-8" ).strip ("\n " )
141- if line .startswith ("__version__" ):
142- line = line .replace ("0.0.0-auto.0" , library_version )
143- line = line .replace ("0.0.0+auto.0" , library_version )
144- temp_file .write (line .encode ("utf-8" ) + b"\r \n " )
176+ line = line .strip ("\n " )
177+ if line .startswith ("__version__" ):
178+ line = line .replace ("0.0.0-auto.0" , library_version )
179+ line = line .replace ("0.0.0+auto.0" , library_version )
180+ print (line , file = temp_file )
145181 temp_file .flush ()
146182
147183def get_package_info (library_path , package_folder_prefix ):
@@ -154,61 +190,52 @@ def get_package_info(library_path, package_folder_prefix):
154190 for pattern in GLOB_PATTERNS :
155191 glob_search .extend (list (lib_path .rglob (pattern )))
156192
157- package_info ["is_package" ] = False
158- for file in glob_search :
159- if file .parts [parent_idx ] != "examples" :
160- if len (file .parts ) > parent_idx + 1 :
161- for prefix in package_folder_prefix :
162- if file .parts [parent_idx ].startswith (prefix ):
163- package_info ["is_package" ] = True
164- if package_info ["is_package" ]:
165- package_files .append (file )
166- else :
167- if file .name in IGNORE_PY :
168- #print("Ignoring:", file.resolve())
169- continue
170- if file .parent == lib_path :
171- py_files .append (file )
172-
173- if package_files :
174- package_info ["module_name" ] = package_files [0 ].relative_to (library_path ).parent .name
175- elif py_files :
176- package_info ["module_name" ] = py_files [0 ].relative_to (library_path ).name [:- 3 ]
177- else :
178- package_info ["module_name" ] = None
179-
180- try :
181- package_info ["version" ] = version_string (library_path , valid_semver = True )
182- except ValueError as e :
183- package_info ["version" ] = version_string (library_path )
184-
185- return package_info
186-
187- def library (library_path , output_directory , package_folder_prefix ,
188- mpy_cross = None , example_bundle = False ):
189- py_files = []
190- package_files = []
191- example_files = []
192- total_size = 512
193-
194- lib_path = pathlib .Path (library_path )
195- parent_idx = len (lib_path .parts )
196- glob_search = []
197- for pattern in GLOB_PATTERNS :
198- glob_search .extend (list (lib_path .rglob (pattern )))
193+ pyproject_toml = load_pyproject_toml (lib_path )
194+ py_modules = get_nested (pyproject_toml , "tool" , "setuptools" , "py-modules" , default = [])
195+ packages = get_nested (pyproject_toml , "tool" , "setuptools" , "packages" , default = [])
196+
197+ blacklisted = [name for name in py_modules if name in pyproject_py_modules_blacklist ]
198+
199+ if blacklisted :
200+ print (f"{ lib_path } /settings.toml:1: { blacklisted [0 ]} blacklisted: not using metadata from pyproject.toml" )
201+ py_modules = packages = ()
202+
203+ example_files = [sub_path for sub_path in (lib_path / "examples" ).rglob ("*" )
204+ if sub_path .is_file ()]
205+
206+ if packages and py_modules :
207+ raise ValueError ("Cannot specify both tool.setuptools.py-modules and .packages" )
208+
209+ elif packages :
210+ if len (packages ) > 1 :
211+ raise ValueError ("Only a single package is supported" )
212+ package_name = packages [0 ]
213+ #print(f"Using package name from pyproject.toml: {package_name}")
214+ package_info ["is_package" ] = True
215+ package_info ["module_name" ] = package_name
216+ package_files = [sub_path for sub_path in (lib_path / package_name ).rglob ("*" )
217+ if sub_path .is_file ()]
218+
219+ elif py_modules :
220+ if len (py_modules ) > 1 :
221+ raise ValueError ("Only a single module is supported" )
222+ py_module = py_modules [0 ]
223+ #print(f"Using module name from pyproject.toml: {py_module}")
224+ package_name = py_module
225+ package_info ["is_package" ] = False
226+ package_info ["module_name" ] = py_module
227+ py_files = [lib_path / f"{ py_module } .py" ]
199228
200- for file in glob_search :
201- if file .parts [parent_idx ] == "examples" :
202- example_files .append (file )
203- else :
204- if not example_bundle :
205- is_package = False
229+ else :
230+ print (f"{ lib_path } : Using legacy autodetection" )
231+ package_info ["is_package" ] = False
232+ for file in glob_search :
233+ if file .parts [parent_idx ] != "examples" :
206234 if len (file .parts ) > parent_idx + 1 :
207235 for prefix in package_folder_prefix :
208236 if file .parts [parent_idx ].startswith (prefix ):
209- is_package = True
210-
211- if is_package :
237+ package_info ["is_package" ] = True
238+ if package_info ["is_package" ]:
212239 package_files .append (file )
213240 else :
214241 if file .name in IGNORE_PY :
@@ -217,91 +244,78 @@ def library(library_path, output_directory, package_folder_prefix,
217244 if file .parent == lib_path :
218245 py_files .append (file )
219246
247+ if package_files :
248+ package_info ["module_name" ] = package_files [0 ].relative_to (library_path ).parent .name
249+ elif py_files :
250+ package_info ["module_name" ] = py_files [0 ].relative_to (library_path ).name [:- 3 ]
251+ else :
252+ package_info ["module_name" ] = None
253+
220254 if len (py_files ) > 1 :
221255 raise ValueError ("Multiple top level py files not allowed. Please put "
222256 "them in a package or combine them into a single file." )
223257
224- if package_files :
225- module_name = package_files [0 ].relative_to (library_path ).parent .name
226- elif py_files :
227- module_name = py_files [0 ].relative_to (library_path ).name [:- 3 ]
228- else :
229- module_name = None
258+ package_info ["package_files" ] = package_files
259+ package_info ["py_files" ] = py_files
260+ package_info ["example_files" ] = example_files
261+
262+ try :
263+ package_info ["version" ] = version_string (library_path , valid_semver = True )
264+ except ValueError as e :
265+ print (library_path + " has version that doesn't follow SemVer (semver.org)" )
266+ print (e )
267+ package_info ["version" ] = version_string (library_path )
268+
269+ return package_info
270+
271+ def library (library_path , output_directory , package_folder_prefix ,
272+ mpy_cross = None , example_bundle = False ):
273+ lib_path = pathlib .Path (library_path )
274+ package_info = get_package_info (library_path , package_folder_prefix )
275+ py_package_files = package_info ["package_files" ] + package_info ["py_files" ]
276+ example_files = package_info ["example_files" ]
277+ module_name = package_info ["module_name" ]
230278
231279 for fn in example_files :
232280 base_dir = os .path .join (output_directory .replace ("/lib" , "/" ),
233281 fn .relative_to (library_path ).parent )
234282 if not os .path .isdir (base_dir ):
235283 os .makedirs (base_dir )
236- total_size += 512
237284
238- for fn in package_files :
285+ for fn in py_package_files :
239286 base_dir = os .path .join (output_directory ,
240287 fn .relative_to (library_path ).parent )
241288 if not os .path .isdir (base_dir ):
242289 os .makedirs (base_dir )
243- total_size += 512
244290
245- new_extension = ".py"
246- if mpy_cross :
247- new_extension = ".mpy"
291+ library_version = package_info ['version' ]
248292
249- try :
250- library_version = version_string (library_path , valid_semver = True )
251- except ValueError as e :
252- print (library_path + " has version that doesn't follow SemVer (semver.org)" )
253- print (e )
254- library_version = version_string (library_path )
255-
256- for filename in py_files :
257- full_path = os .path .join (library_path , filename )
258- output_file = os .path .join (
259- output_directory ,
260- filename .relative_to (library_path ).with_suffix (new_extension )
261- )
262- with tempfile .NamedTemporaryFile (delete = False ) as temp_file :
263- _munge_to_temp (full_path , temp_file , library_version )
264- temp_filename = temp_file .name
265- # Windows: close the temp file before it can be read or copied by name
266- if mpy_cross :
267- mpy_success = subprocess .call ([
268- mpy_cross ,
269- "-o" , output_file ,
270- "-s" , str (filename .relative_to (library_path )),
271- temp_filename
272- ])
273- if mpy_success != 0 :
274- raise RuntimeError ("mpy-cross failed on" , full_path )
275- else :
276- shutil .copyfile (temp_filename , output_file )
277- os .remove (temp_filename )
278-
279- for filename in package_files :
280- full_path = os .path .join (library_path , filename )
281- output_file = ""
282- with tempfile .NamedTemporaryFile (delete = False ) as temp_file :
283- _munge_to_temp (full_path , temp_file , library_version )
284- temp_filename = temp_file .name
285- # Windows: close the temp file before it can be read or copied by name
286- if not mpy_cross or os .stat (full_path ).st_size == 0 :
287- output_file = os .path .join (output_directory ,
288- filename .relative_to (library_path ))
289- shutil .copyfile (temp_filename , output_file )
290- else :
291- output_file = os .path .join (
292- output_directory ,
293- filename .relative_to (library_path ).with_suffix (new_extension )
294- )
295-
296- mpy_success = subprocess .call ([
297- mpy_cross ,
298- "-o" , output_file ,
299- "-s" , str (filename .relative_to (library_path )),
300- temp_filename
301- ])
302- if mpy_success != 0 :
303- raise RuntimeError ("mpy-cross failed on" , full_path )
304- os .remove (temp_filename )
293+ if not example_bundle :
294+ for filename in py_package_files :
295+ full_path = os .path .join (library_path , filename )
296+ output_file = output_directory / filename .relative_to (library_path )
297+ if filename .suffix == ".py" :
298+ with tempfile .NamedTemporaryFile (delete = False , mode = "w+" ) as temp_file :
299+ temp_file_name = temp_file .name
300+ try :
301+ _munge_to_temp (full_path , temp_file , library_version )
302+ temp_file .close ()
303+ if mpy_cross and os .stat (temp_file .name ).st_size != 0 :
304+ output_file = output_file .with_suffix (".mpy" )
305+ mpy_success = subprocess .call ([
306+ mpy_cross ,
307+ "-o" , output_file ,
308+ "-s" , str (filename .relative_to (library_path )),
309+ temp_file .name
310+ ])
311+ if mpy_success != 0 :
312+ raise RuntimeError ("mpy-cross failed on" , full_path )
313+ else :
314+ shutil .copyfile (full_path , output_file )
315+ finally :
316+ os .remove (temp_file_name )
317+ else :
318+ shutil .copyfile (full_path , output_file )
305319
306320 requirements_files = lib_path .glob ("requirements.txt*" )
307321 requirements_files = [f for f in requirements_files if f .stat ().st_size > 0 ]
@@ -314,11 +328,9 @@ def library(library_path, output_directory, package_folder_prefix,
314328 requirements_dir = pathlib .Path (output_directory ).parent / "requirements"
315329 if not os .path .isdir (requirements_dir ):
316330 os .makedirs (requirements_dir , exist_ok = True )
317- total_size += 512
318331 requirements_subdir = f"{ requirements_dir } /{ module_name } "
319332 if not os .path .isdir (requirements_subdir ):
320333 os .makedirs (requirements_subdir , exist_ok = True )
321- total_size += 512
322334 for filename in requirements_files :
323335 full_path = os .path .join (library_path , filename )
324336 output_file = os .path .join (requirements_subdir , filename .name )
@@ -328,9 +340,4 @@ def library(library_path, output_directory, package_folder_prefix,
328340 full_path = os .path .join (library_path , filename )
329341 output_file = os .path .join (output_directory .replace ("/lib" , "/" ),
330342 filename .relative_to (library_path ))
331- temp_filename = ""
332- with tempfile .NamedTemporaryFile (delete = False ) as temp_file :
333- _munge_to_temp (full_path , temp_file , library_version )
334- temp_filename = temp_file .name
335- shutil .copyfile (temp_filename , output_file )
336- os .remove (temp_filename )
343+ shutil .copyfile (full_path , output_file )
0 commit comments