1212from noxopt import Annotated , NoxOpt , Option , Session
1313
1414
15+ # --- Typing Preamble ------------------------------------------------------------------
16+
17+
1518if TYPE_CHECKING :
1619 # not available in typing module until Python 3.8
17- from typing import Literal
20+ # not available in typing module until Python 3.10
21+ from typing import Literal , Protocol , TypeAlias
22+
23+ class ReleasePrepFunc (Protocol ):
24+ def __call__ (self , session : Session ) -> Callable [[bool ], None ]:
25+ ...
26+
27+
28+ LanguageName : TypeAlias = "Literal['py', 'js']"
29+
30+
31+ # --- Constants ------------------------------------------------------------------------
32+
1833
1934ROOT_DIR = Path (__file__ ).parent .resolve ()
2035SRC_DIR = ROOT_DIR / "src"
2136CLIENT_DIR = SRC_DIR / "client"
2237REACTPY_DIR = SRC_DIR / "reactpy"
23-
38+ LANGUAGE_TYPES : list [ LanguageName ] = [ "py" , "js" ]
2439TAG_PATTERN = re .compile (
25- r"^(?P<name>[0-9a-zA-Z-@/]+)-v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)$"
40+ # start
41+ r"^"
42+ # package name
43+ r"(?P<name>[0-9a-zA-Z-@/]+)-"
44+ # package language
45+ rf"(?P<language>{ '|' .join (LANGUAGE_TYPES )} )-"
46+ # package version
47+ r"v(?P<version>[0-9][0-9a-zA-Z-\.\+]*)"
48+ # end
49+ r"$"
2650)
27-
51+ print ( TAG_PATTERN . pattern )
2852REMAINING_ARGS = Option (nargs = REMAINDER , type = str )
2953
54+
55+ # --- Session Setup --------------------------------------------------------------------
56+
57+
3058group = NoxOpt (auto_tag = True )
3159
3260
@@ -42,6 +70,9 @@ def setup_javascript_checks(session: Session) -> None:
4270 session .run ("npm" , "ci" , external = True )
4371
4472
73+ # --- Session Definitions --------------------------------------------------------------
74+
75+
4576@group .session
4677def format (session : Session ) -> None :
4778 """Auto format Python and Javascript code"""
@@ -236,31 +267,43 @@ def build_python(session: Session) -> None:
236267
237268
238269@group .session
239- def publish (session : Session ) -> None :
270+ def publish (session : Session , dry_run : bool = False ) -> None :
240271 packages = get_packages (session )
241272
242- release_prep = {"js" : prepare_javascript_release , "py" : prepare_python_release }
273+ release_prep : dict [LanguageName , ReleasePrepFunc ] = {
274+ "js" : prepare_javascript_release ,
275+ "py" : prepare_python_release ,
276+ }
277+
278+ publishers : list [Callable [[bool ], None ]] = []
279+ for tag , tag_lang , tag_pkg , tag_ver in get_current_tags (session ):
280+ if tag_pkg not in packages :
281+ session .error (f"Tag { tag } references package { tag_pkg } that does not exist" )
243282
244- publishers : list [Callable [[], None ]] = []
245- for tag , tag_package , tag_version in get_current_tags (session ):
246- if tag_package not in packages :
247- session .error (f"Tag { tag } references missing package { tag_package } " )
283+ pkg_path , pkg_lang , pkg_ver = packages [tag_pkg ]
284+ if pkg_ver != tag_ver :
285+ session .error (
286+ f"Tag { tag } references version { tag_ver } of package { tag_pkg } , "
287+ f"but the current version is { pkg_ver } "
288+ )
248289
249- pkg_kind , pkg_path , pkg_version = packages [tag_package ]
250- if pkg_version != tag_version :
290+ if pkg_lang != tag_lang :
251291 session .error (
252- f"Tag { tag } references version { tag_version } of package { tag_package } , "
253- f"but the current version is { pkg_version } "
292+ f"Tag { tag } references language { tag_lang } of package { tag_pkg } , "
293+ f"but the current language is { pkg_lang } "
254294 )
255295
256296 session .chdir (pkg_path )
257- session .log (f"Preparing { tag_package } for release..." )
258- publishers .append ((pkg_path , release_prep [pkg_kind ](session , tag_package )))
297+ session .log (f"Preparing { tag_pkg } for release..." )
298+ publishers .append ((pkg_path , release_prep [tag_lang ](session )))
259299
260300 for pkg_path , publish in publishers :
261- session .log (f"Publishing { tag_package } ..." )
301+ session .log (f"Publishing { tag_pkg } ..." )
262302 session .chdir (pkg_path )
263- publish ()
303+ publish (dry_run )
304+
305+
306+ # --- Utilities ------------------------------------------------------------------------
264307
265308
266309def install_requirements_file (session : Session , name : str ) -> None :
@@ -287,14 +330,17 @@ def get_reactpy_script_env() -> dict[str, str]:
287330 }
288331
289332
290- def prepare_javascript_release (session : Session , name : str ) -> Callable [[], None ]:
333+ def prepare_javascript_release (session : Session ) -> Callable [[bool ], None ]:
291334 node_auth_token = session .env .get ("NODE_AUTH_TOKEN" )
292335 if node_auth_token is None :
293336 session .error ("NODE_AUTH_TOKEN environment variable must be set" )
294337
295338 session .run ("npm" , "ci" , external = True )
296339
297- def publish () -> None :
340+ def publish (dry_run : bool ) -> None :
341+ if dry_run :
342+ session .run ("npm" , "pack" , "--dry-run" , external = True )
343+ return
298344 session .run (
299345 "npm" ,
300346 "publish" ,
@@ -307,7 +353,7 @@ def publish() -> None:
307353 return publish
308354
309355
310- def prepare_python_release (session : Session , name : str ) -> Callable [[], None ]:
356+ def prepare_python_release (session : Session ) -> Callable [[bool ], None ]:
311357 twine_username = session .env .get ("PYPI_USERNAME" )
312358 twine_password = session .env .get ("PYPI_PASSWORD" )
313359
@@ -322,7 +368,11 @@ def prepare_python_release(session: Session, name: str) -> Callable[[], None]:
322368 install_requirements_file (session , "build-pkg" )
323369 session .run ("python" , "-m" , "build" , "--sdist" , "--wheel" , "--outdir" , "dist" , "." )
324370
325- def publish ():
371+ def publish (dry_run : bool ):
372+ if dry_run :
373+ session .run ("twine" , "check" , "dist/*" )
374+ return
375+
326376 session .run (
327377 "twine" ,
328378 "upload" ,
@@ -335,7 +385,7 @@ def publish():
335385
336386def get_packages (session : Session ) -> dict [str , PackageInfo ]:
337387 packages : dict [str , PackageInfo ] = {
338- "reactpy" : PackageInfo ("py" , ROOT_DIR , get_reactpy_package_version (session ))
388+ "reactpy" : PackageInfo (ROOT_DIR , "py" , get_reactpy_package_version (session ))
339389 }
340390
341391 # collect javascript packages
@@ -359,14 +409,14 @@ def get_packages(session: Session) -> dict[str, PackageInfo]:
359409 if pkg_name in packages :
360410 session .error (f"Duplicate package name { pkg_name } " )
361411
362- packages [pkg_name ] = PackageInfo ("js" , pkg , pkg_version )
412+ packages [pkg_name ] = PackageInfo (pkg , "js" , pkg_version )
363413
364414 return packages
365415
366416
367417class PackageInfo (NamedTuple ):
368- kind : Literal ["js" , "py" ]
369418 path : Path
419+ language : LanguageName
370420 version : str
371421
372422
@@ -420,9 +470,16 @@ def get_current_tags(session: Session) -> list[TagInfo]:
420470 match = TAG_PATTERN .match (tag )
421471 if not match :
422472 session .error (
423- f"Invalid tag { tag } - must be of the form <package>-<version>"
473+ f"Invalid tag { tag } - must be of the form <package>-<language>-<version>"
474+ )
475+ parsed_tags .append (
476+ TagInfo (
477+ tag ,
478+ match ["language" ],
479+ match ["name" ],
480+ match ["version" ],
424481 )
425- parsed_tags . append ( TagInfo ( tag , match [ "name" ], match [ "version" ]) )
482+ )
426483
427484 session .log (f"Found tags: { [info .tag for info in parsed_tags ]} " )
428485
@@ -431,6 +488,7 @@ def get_current_tags(session: Session) -> list[TagInfo]:
431488
432489class TagInfo (NamedTuple ):
433490 tag : str
491+ language : LanguageName
434492 package : str
435493 version : str
436494
0 commit comments