2626import aiohttp
2727import packaging .specifiers
2828import packaging .version
29- import tomli
3029import tomlkit
3130from termcolor import colored
3231
32+ from ts_utils .metadata import StubMetadata , metadata_path , read_metadata , stubs_path
33+
3334TYPESHED_OWNER = "python"
3435TYPESHED_API_URL = f"https://api.github.com/repos/{ TYPESHED_OWNER } /typeshed"
3536
@@ -56,27 +57,6 @@ def from_cmd_arg(cls, cmd_arg: str) -> ActionLevel:
5657 everything = 3 , "do everything, e.g. open PRs"
5758
5859
59- @dataclass
60- class StubInfo :
61- distribution : str
62- version_spec : str
63- upstream_repository : str | None
64- obsolete : bool
65- no_longer_updated : bool
66-
67-
68- def read_typeshed_stub_metadata (stub_path : Path ) -> StubInfo :
69- with (stub_path / "METADATA.toml" ).open ("rb" ) as f :
70- meta = tomli .load (f )
71- return StubInfo (
72- distribution = stub_path .name ,
73- version_spec = meta ["version" ],
74- upstream_repository = meta .get ("upstream_repository" ),
75- obsolete = "obsolete_since" in meta ,
76- no_longer_updated = meta .get ("no_longer_updated" , False ),
77- )
78-
79-
8060@dataclass
8161class PypiReleaseDownload :
8262 distribution : str
@@ -141,7 +121,6 @@ async def fetch_pypi_info(distribution: str, session: aiohttp.ClientSession) ->
141121@dataclass
142122class Update :
143123 distribution : str
144- stub_path : Path
145124 old_version_spec : str
146125 new_version_spec : str
147126 links : dict [str , str ]
@@ -154,7 +133,6 @@ def __str__(self) -> str:
154133@dataclass
155134class Obsolete :
156135 distribution : str
157- stub_path : Path
158136 obsolete_since_version : str
159137 obsolete_since_date : datetime .datetime
160138 links : dict [str , str ]
@@ -304,7 +282,7 @@ class GitHubInfo:
304282 tags : list [dict [str , Any ]] = field (repr = False )
305283
306284
307- async def get_github_repo_info (session : aiohttp .ClientSession , stub_info : StubInfo ) -> GitHubInfo | None :
285+ async def get_github_repo_info (session : aiohttp .ClientSession , stub_info : StubMetadata ) -> GitHubInfo | None :
308286 """
309287 If the project represented by `stub_info` is hosted on GitHub,
310288 return information regarding the project as it exists on GitHub.
@@ -335,7 +313,7 @@ class GitHubDiffInfo(NamedTuple):
335313
336314
337315async def get_diff_info (
338- session : aiohttp .ClientSession , stub_info : StubInfo , pypi_version : packaging .version .Version
316+ session : aiohttp .ClientSession , stub_info : StubMetadata , pypi_version : packaging .version .Version
339317) -> GitHubDiffInfo | None :
340318 """Return a tuple giving info about the diff between two releases, if possible.
341319
@@ -355,7 +333,7 @@ async def get_diff_info(
355333 with contextlib .suppress (packaging .version .InvalidVersion ):
356334 versions_to_tags [packaging .version .Version (tag_name )] = tag_name
357335
358- curr_specifier = packaging .specifiers .SpecifierSet (f"=={ stub_info .version_spec } " )
336+ curr_specifier = packaging .specifiers .SpecifierSet (f"=={ stub_info .version } " )
359337
360338 try :
361339 new_tag = versions_to_tags [pypi_version ]
@@ -469,7 +447,7 @@ def __str__(self) -> str:
469447
470448
471449async def analyze_diff (
472- github_repo_path : str , stub_path : Path , old_tag : str , new_tag : str , * , session : aiohttp .ClientSession
450+ github_repo_path : str , distribution : str , old_tag : str , new_tag : str , * , session : aiohttp .ClientSession
473451) -> DiffAnalysis | None :
474452 url = f"https://api.github.com/repos/{ github_repo_path } /compare/{ old_tag } ...{ new_tag } "
475453 async with session .get (url , headers = get_github_api_headers ()) as response :
@@ -478,22 +456,23 @@ async def analyze_diff(
478456 assert isinstance (json_resp , dict )
479457 # https://docs.github.com/en/rest/commits/commits#compare-two-commits
480458 py_files : list [FileInfo ] = [file for file in json_resp ["files" ] if Path (file ["filename" ]).suffix == ".py" ]
459+ stub_path = stubs_path (distribution )
481460 files_in_typeshed = set (stub_path .rglob ("*.pyi" ))
482461 py_files_stubbed_in_typeshed = [file for file in py_files if (stub_path / f"{ file ['filename' ]} i" ) in files_in_typeshed ]
483462 return DiffAnalysis (py_files = py_files , py_files_stubbed_in_typeshed = py_files_stubbed_in_typeshed )
484463
485464
486- async def determine_action (stub_path : Path , session : aiohttp .ClientSession ) -> Update | NoUpdate | Obsolete :
487- stub_info = read_typeshed_stub_metadata ( stub_path )
488- if stub_info .obsolete :
465+ async def determine_action (distribution : str , session : aiohttp .ClientSession ) -> Update | NoUpdate | Obsolete :
466+ stub_info = read_metadata ( distribution )
467+ if stub_info .is_obsolete :
489468 return NoUpdate (stub_info .distribution , "obsolete" )
490469 if stub_info .no_longer_updated :
491470 return NoUpdate (stub_info .distribution , "no longer updated" )
492471
493472 pypi_info = await fetch_pypi_info (stub_info .distribution , session )
494473 latest_release = pypi_info .get_latest_release ()
495474 latest_version = latest_release .version
496- spec = packaging .specifiers .SpecifierSet (f"=={ stub_info .version_spec } " )
475+ spec = packaging .specifiers .SpecifierSet (f"=={ stub_info .version } " )
497476 obsolete_since = await find_first_release_with_py_typed (pypi_info , session = session )
498477 if obsolete_since is None and latest_version in spec :
499478 return NoUpdate (stub_info .distribution , "up to date" )
@@ -517,7 +496,6 @@ async def determine_action(stub_path: Path, session: aiohttp.ClientSession) -> U
517496 if obsolete_since :
518497 return Obsolete (
519498 stub_info .distribution ,
520- stub_path ,
521499 obsolete_since_version = str (obsolete_since .version ),
522500 obsolete_since_date = obsolete_since .upload_date ,
523501 links = links ,
@@ -528,17 +506,16 @@ async def determine_action(stub_path: Path, session: aiohttp.ClientSession) -> U
528506 else :
529507 diff_analysis = await analyze_diff (
530508 github_repo_path = diff_info .repo_path ,
531- stub_path = stub_path ,
509+ distribution = distribution ,
532510 old_tag = diff_info .old_tag ,
533511 new_tag = diff_info .new_tag ,
534512 session = session ,
535513 )
536514
537515 return Update (
538516 distribution = stub_info .distribution ,
539- stub_path = stub_path ,
540- old_version_spec = stub_info .version_spec ,
541- new_version_spec = get_updated_version_spec (stub_info .version_spec , latest_version ),
517+ old_version_spec = stub_info .version ,
518+ new_version_spec = get_updated_version_spec (stub_info .version , latest_version ),
542519 links = links ,
543520 diff_analysis = diff_analysis ,
544521 )
@@ -705,10 +682,10 @@ async def suggest_typeshed_update(update: Update, session: aiohttp.ClientSession
705682 async with _repo_lock :
706683 branch_name = f"{ BRANCH_PREFIX } /{ normalize (update .distribution )} "
707684 subprocess .check_call (["git" , "checkout" , "-B" , branch_name , "origin/main" ])
708- with open (update .stub_path / "METADATA.toml" , "rb" ) as f :
685+ with metadata_path (update .distribution ). open ( "rb" ) as f :
709686 meta = tomlkit .load (f )
710687 meta ["version" ] = update .new_version_spec
711- with open (update .stub_path / "METADATA.toml" , "w" , encoding = "UTF-8" ) as f :
688+ with metadata_path (update .distribution ). open ( "w" , encoding = "UTF-8" ) as f :
712689 # tomlkit.dump has partially unknown IO type
713690 tomlkit .dump (meta , f ) # pyright: ignore[reportUnknownMemberType]
714691 body = get_update_pr_body (update , meta )
@@ -732,12 +709,12 @@ async def suggest_typeshed_obsolete(obsolete: Obsolete, session: aiohttp.ClientS
732709 async with _repo_lock :
733710 branch_name = f"{ BRANCH_PREFIX } /{ normalize (obsolete .distribution )} "
734711 subprocess .check_call (["git" , "checkout" , "-B" , branch_name , "origin/main" ])
735- with open (obsolete .stub_path / "METADATA.toml" , "rb" ) as f :
712+ with metadata_path (obsolete .distribution ). open ( "rb" ) as f :
736713 meta = tomlkit .load (f )
737714 obs_string = tomlkit .string (obsolete .obsolete_since_version )
738715 obs_string .comment (f"Released on { obsolete .obsolete_since_date .date ().isoformat ()} " )
739716 meta ["obsolete_since" ] = obs_string
740- with open (obsolete .stub_path / "METADATA.toml" , "w" , encoding = "UTF-8" ) as f :
717+ with metadata_path (obsolete .distribution ). open ( "w" , encoding = "UTF-8" ) as f :
741718 # tomlkit.dump has partially unknown Mapping type
742719 tomlkit .dump (meta , f ) # pyright: ignore[reportUnknownMemberType]
743720 body = "\n " .join (f"{ k } : { v } " for k , v in obsolete .links .items ())
@@ -774,9 +751,9 @@ async def main() -> None:
774751 args = parser .parse_args ()
775752
776753 if args .distributions :
777- paths_to_update = [ Path ( "stubs" ) / distribution for distribution in args .distributions ]
754+ dists_to_update = args .distributions
778755 else :
779- paths_to_update = list ( Path ("stubs" ).iterdir ())
756+ dists_to_update = [ path . name for path in Path ("stubs" ).iterdir ()]
780757
781758 if args .action_level > ActionLevel .nothing :
782759 subprocess .run (["git" , "update-index" , "--refresh" ], capture_output = True )
@@ -808,9 +785,9 @@ async def main() -> None:
808785 conn = aiohttp .TCPConnector (limit_per_host = 10 )
809786 async with aiohttp .ClientSession (connector = conn ) as session :
810787 tasks = [
811- asyncio .create_task (determine_action (stubs_path , session ))
812- for stubs_path in paths_to_update
813- if stubs_path . name not in denylist
788+ asyncio .create_task (determine_action (distribution , session ))
789+ for distribution in dists_to_update
790+ if distribution not in denylist
814791 ]
815792
816793 action_count = 0
0 commit comments