@@ -244,3 +244,125 @@ def test_rm_file_without_implementation():
244244 fs = fsspec .asyn .AsyncFileSystem ()
245245 with pytest .raises (NotImplementedError ):
246246 fs .rm_file ("test/file.txt" )
247+
248+
249+ # ---------------------------------------------------------------------------
250+ # Tests for the prefix= hint that _glob passes to _find
251+ # ---------------------------------------------------------------------------
252+
253+ _GLOB_PREFIX_FILES = [
254+ "data/2024/results.csv" ,
255+ "data/2024/report.txt" ,
256+ "data/2023/results.csv" ,
257+ "top_results.csv" ,
258+ "top_other.txt" ,
259+ "other/results.csv" ,
260+ ]
261+
262+
263+ class _PrefixCapturingFS (fsspec .asyn .AsyncFileSystem ):
264+ """Minimal AsyncFileSystem that records every _find call's kwargs.
265+
266+ _find ignores the prefix hint and returns all files under *root* so that
267+ the client-side glob pattern-matching in _glob still works correctly.
268+ This simulates a "naive" backend that silently absorbs unknown kwargs.
269+ """
270+
271+ protocol = "prefixmock"
272+
273+ def __init__ (self , ** kwargs ):
274+ super ().__init__ (** kwargs )
275+ self .find_calls = []
276+
277+ async def _find (self , path , maxdepth = None , withdirs = False , detail = False , ** kwargs ):
278+ self .find_calls .append ({"path" : path , "kwargs" : dict (kwargs )})
279+ root = (path .rstrip ("/" ) + "/" ) if path else ""
280+ results = {
281+ f : {"name" : f , "type" : "file" , "size" : 0 }
282+ for f in _GLOB_PREFIX_FILES
283+ if f .startswith (root )
284+ }
285+ return results if detail else list (results )
286+
287+ async def _info (self , path , ** kwargs ):
288+ for f in _GLOB_PREFIX_FILES :
289+ if f == path :
290+ return {"name" : f , "type" : "file" , "size" : 0 }
291+ raise FileNotFoundError (path )
292+
293+
294+ @pytest .fixture
295+ def prefix_fs ():
296+ return _PrefixCapturingFS (skip_instance_cache = True )
297+
298+
299+ @pytest .mark .parametrize (
300+ "pattern, expected_results, expected_prefix" ,
301+ [
302+ # root/stem* -> prefix="stem"
303+ (
304+ "data/2024/res*" ,
305+ ["data/2024/results.csv" ],
306+ "res" ,
307+ ),
308+ # root/stem*.ext -> prefix="stem"
309+ (
310+ "data/2024/re*.txt" ,
311+ ["data/2024/report.txt" ],
312+ "re" ,
313+ ),
314+ # stem* (no slash) -> prefix="stem"
315+ (
316+ "top_*" ,
317+ ["top_other.txt" , "top_results.csv" ],
318+ "top_" ,
319+ ),
320+ # ? wildcard: prefix is everything before the first ?
321+ (
322+ "top_r?sults.csv" ,
323+ ["top_results.csv" ],
324+ "top_r" ,
325+ ),
326+ # [ wildcard: prefix is everything before the first [
327+ # re[rp]* translates to re[rp][^/]* so only report.txt matches (not results.csv)
328+ (
329+ "data/2024/re[rp]*" ,
330+ ["data/2024/report.txt" ],
331+ "re" ,
332+ ),
333+ # root/* (wildcard immediately after /) -> empty prefix, NOT forwarded
334+ (
335+ "data/2024/*" ,
336+ ["data/2024/report.txt" , "data/2024/results.csv" ],
337+ None ,
338+ ),
339+ # bare * (no prefix at all) -> NOT forwarded
340+ # * translates to [^/]+ so paths containing / are excluded
341+ (
342+ "*" ,
343+ ["top_other.txt" , "top_results.csv" ],
344+ None ,
345+ ),
346+ ],
347+ )
348+ def test_glob_prefix_hint (prefix_fs , pattern , expected_results , expected_prefix ):
349+ """_glob should extract the literal stem before the first wildcard and
350+ forward it as ``prefix=`` to ``_find``. When the stem is empty the kwarg
351+ must not be forwarded at all so that backends that reject unknown kwargs
352+ are not broken. The glob results must be correct regardless."""
353+ results = prefix_fs .glob (pattern )
354+ assert sorted (results ) == sorted (expected_results )
355+
356+ assert len (prefix_fs .find_calls ) == 1
357+ forwarded = prefix_fs .find_calls [0 ]["kwargs" ]
358+
359+ if expected_prefix is None :
360+ assert "prefix" not in forwarded , (
361+ f"prefix= should not be forwarded for pattern { pattern !r} , "
362+ f"but got prefix={ forwarded .get ('prefix' )!r} "
363+ )
364+ else :
365+ assert forwarded .get ("prefix" ) == expected_prefix , (
366+ f"expected prefix={ expected_prefix !r} for pattern { pattern !r} , "
367+ f"got { forwarded .get ('prefix' )!r} "
368+ )
0 commit comments