66import argparse
77import asyncio
88import builtins
9+ import io
910import logging
11+ from collections .abc import Iterable
1012from functools import partial
1113from io import StringIO
1214
15+ import aiofiles
1316import reorder_python_imports
1417from autoflake import _main as autoflake_main
1518from isort .main import main as isort_main
1619from pyupgrade ._main import main as pyupgrade_main
1720
18- from .const import FileStatus
21+ from .const import FileAttributes , FileStatus
1922from .utils import (
2023 async_check_uncommitted_changes , async_restore_files ,
21- check_comment_between_imports , check_files_exist )
24+ check_comment_between_imports , check_files_exist , extract_imports )
2225
2326logger = logging .getLogger ("typing-update" )
2427
@@ -107,6 +110,32 @@ async def typing_update(
107110 return 0 , filename
108111
109112
113+ async def async_load_files (
114+ args : argparse .Namespace ,
115+ filenames : Iterable [str ], * ,
116+ check_comments : bool ,
117+ ) -> dict [str , FileAttributes ]:
118+ """Process files from file list."""
119+ active_tasks : int = 0
120+
121+ async def async_load_file (filename : str ) -> tuple [str , FileAttributes ]:
122+ """Load file into memory and perform token analysis."""
123+ nonlocal active_tasks
124+ while active_tasks > args .concurrent_files :
125+ await asyncio .sleep (0 )
126+ active_tasks += 1
127+ async with aiofiles .open (filename , encoding = "utf-8" ) as fp :
128+ data = await fp .read ()
129+ file_status = check_comment_between_imports (io .StringIO (data )) \
130+ if check_comments is True else FileStatus .CLEAR
131+ imports_set = extract_imports (io .StringIO (data ))
132+ active_tasks -= 1
133+ return filename , FileAttributes (file_status , imports_set )
134+
135+ results = await asyncio .gather (* [async_load_file (file_ ) for file_ in filenames ])
136+ return dict (results )
137+
138+
110139async def async_run (args : argparse .Namespace ) -> int :
111140 """Update Python typing syntax.
112141
@@ -129,15 +158,11 @@ async def async_run(args: argparse.Namespace) -> int:
129158 print ("Abort! Commit all changes to '.py' files before running again." )
130159 return 11
131160
132- filenames : dict [str , FileStatus ] = {}
133- for filename in args .filenames :
134- with open (filename ) as fp :
135- result = check_comment_between_imports (fp )
136- filenames [filename ] = result
161+ filenames : dict [str , FileAttributes ] = await async_load_files (args , args .filenames , check_comments = True )
137162
138163 if args .only_force :
139- filenames = {filename : file_status for filename , file_status in filenames .items ()
140- if file_status != FileStatus .CLEAR }
164+ filenames = {filename : attrs for filename , attrs in filenames .items ()
165+ if attrs . status != FileStatus .CLEAR }
141166
142167 loop = asyncio .get_running_loop ()
143168 files_updated : list [str ] = []
@@ -148,7 +173,7 @@ async def async_run(args: argparse.Namespace) -> int:
148173 builtins .print = lambda * args , ** kwargs : None
149174
150175 return_values = await asyncio .gather (
151- * [typing_update (loop , filename , args , file_status ) for filename , file_status in filenames .items ()])
176+ * [typing_update (loop , filename , args , attrs . status ) for filename , attrs in filenames .items ()])
152177 for status , filename in return_values :
153178 if status == 0 :
154179 files_updated .append (filename )
@@ -176,35 +201,53 @@ async def async_run(args: argparse.Namespace) -> int:
176201
177202 files_updated_set : set [str ] = set (files_updated )
178203 files_with_comments = sorted ([
179- filename for filename , file_status in filenames .items ()
180- if FileStatus .COMMENT in file_status and filename in files_updated_set
204+ filename for filename , attrs in filenames .items ()
205+ if FileStatus .COMMENT in attrs . status and filename in files_updated_set
181206 ])
182- if files_with_comments :
207+ files_imports_changed : list [str ] = []
208+ for file_ , attrs in (await async_load_files (args , files_updated_set , check_comments = False )).items ():
209+ import_diff = filenames [file_ ].imports .difference (attrs .imports )
210+ for import_ in import_diff :
211+ if not import_ .startswith ('typing' ):
212+ files_imports_changed .append (file_ )
213+ break
214+ files_imports_changed = sorted (files_imports_changed )
215+ files_no_automatic_update = set (files_with_comments + files_imports_changed )
216+
217+ if files_no_automatic_update :
183218 if args .force or args .only_force :
184219 print ("Force mode selected!" )
185220 print ("Make sure to double check:" )
186221 for file_ in files_with_comments :
187222 print (f" - { file_ } " )
223+ if files_with_comments and files_imports_changed :
224+ print (" --" )
225+ for file_ in files_imports_changed :
226+ print (f" - { file_ } " )
188227 else :
189228 print ("Could not update all files, check:" )
190229 for file_ in files_with_comments :
191230 print (f" - { file_ } " )
192- await async_restore_files (files_with_comments )
231+ if files_with_comments and files_imports_changed :
232+ print (" --" )
233+ for file_ in files_imports_changed :
234+ print (f" - { file_ } " )
235+ await async_restore_files (files_no_automatic_update )
193236
194237 print ("---" )
195238 print (f"All files: { len (filenames )} " )
196239 print (f"No changes: { len (files_no_changes )} " )
197- print (f"Files updated: { len (files_updated ) - len (files_with_comments )} " )
198- print (f"Files (no automatic update): { len (files_with_comments )} " )
240+ print (f"Files updated: { len (files_updated ) - len (files_no_automatic_update )} " )
241+ print (f"Files (no automatic update): { len (files_no_automatic_update )} " )
199242
200243 if (
201- not files_with_comments
244+ not files_no_automatic_update
202245 and not args .force
203246 and not args .only_force
204247 and args .verbose == 0
205248 ):
206249 return 0
207- if files_with_comments :
250+ if files_no_automatic_update :
208251 return 2
209252 if args .verbose > 0 :
210253 return 12
0 commit comments