11from pathlib import Path
2- from typing import Any , Callable , Collection , Dict , Generator , List , Optional , Union
2+ from typing import (
3+ Any ,
4+ Callable ,
5+ Collection ,
6+ Dict ,
7+ Generator ,
8+ List ,
9+ Optional ,
10+ Type ,
11+ Union ,
12+ )
313
414import pathspec
5- from cantok import AbstractToken , DefaultToken
15+ from cantok import AbstractToken , CancellationError , DefaultToken
616from printo import describe_data_object , not_none
717from sigmatch import PossibleCallMatcher
18+ from sigmatch .errors import SignatureMismatchError , SignatureNotFoundError
819
920from dirstree .crawlers .abstract import AbstractCrawler
1021from dirstree .errors import IncompatibleCrawlerOptionsError
1122
1223
24+ def _exception_class_accepts_single_positional (cls : type ) -> bool :
25+ try :
26+ PossibleCallMatcher ('.' ).match (cls , raise_exception = True )
27+ except SignatureNotFoundError :
28+ return True
29+ except SignatureMismatchError :
30+ return False
31+ return True
32+
33+
1334# TODO: add typing tests
1435class Crawler (AbstractCrawler ):
1536 """
@@ -40,6 +61,7 @@ def __init__( # noqa: PLR0913
4061 token : AbstractToken = DefaultToken (), # noqa: B008
4162 only_files : bool = True ,
4263 freeze : bool = False ,
64+ raise_on_cancel : Union [bool , BaseException , Type [BaseException ]] = False ,
4365 ) -> None :
4466 if extensions is not None and not only_files :
4567 raise IncompatibleCrawlerOptionsError (
@@ -56,6 +78,19 @@ def __init__( # noqa: PLR0913
5678 if filter is not None :
5779 PossibleCallMatcher ('.' ).match (filter , raise_exception = True )
5880
81+ if not (
82+ isinstance (raise_on_cancel , (bool , BaseException ))
83+ or (
84+ isinstance (raise_on_cancel , type )
85+ and issubclass (raise_on_cancel , BaseException )
86+ and _exception_class_accepts_single_positional (raise_on_cancel )
87+ )
88+ ):
89+ raise TypeError (
90+ 'raise_on_cancel must be a bool, a BaseException instance, '
91+ 'or a BaseException subclass whose constructor accepts a single positional argument.' ,
92+ )
93+
5994 self .paths = paths
6095 self .extensions = extensions
6196 self .exclude = exclude if exclude is not None else []
@@ -64,6 +99,13 @@ def __init__( # noqa: PLR0913
6499 self .only_files = only_files
65100 self .frozen = freeze
66101
102+ if isinstance (raise_on_cancel , bool ):
103+ self .raise_on_cancel : bool = raise_on_cancel
104+ self .cancellation_exception : Optional [Union [BaseException , Type [BaseException ]]] = None
105+ else :
106+ self .raise_on_cancel = True
107+ self .cancellation_exception = raise_on_cancel
108+
67109 self .addictional_repr_filters : Dict [str , Callable [[Any ], bool ]] = {}
68110
69111 def __repr__ (self ) -> str :
@@ -74,9 +116,14 @@ def __repr__(self) -> str:
74116 'token' : lambda x : not isinstance (x , DefaultToken ),
75117 'only_files' : lambda x : x is False ,
76118 'freeze' : lambda x : x is True ,
119+ 'raise_on_cancel' : lambda x : x is not False ,
77120 }
78121 filters .update (self .addictional_repr_filters )
79122
123+ displayed_raise_on_cancel : Union [bool , BaseException , Type [BaseException ]] = (
124+ self .cancellation_exception if self .cancellation_exception is not None else self .raise_on_cancel
125+ )
126+
80127 return describe_data_object (
81128 self .__class__ .__name__ ,
82129 self .paths ,
@@ -87,41 +134,57 @@ def __repr__(self) -> str:
87134 'token' : self .token ,
88135 'only_files' : self .only_files ,
89136 'freeze' : self .frozen ,
137+ 'raise_on_cancel' : displayed_raise_on_cancel ,
90138 },
91139 filters = filters , # type: ignore[arg-type]
92140 )
93141
142+ def _check_token (self , token : AbstractToken ) -> bool :
143+ if token :
144+ return True
145+ if self .raise_on_cancel :
146+ try :
147+ token .check ()
148+ except CancellationError as original_exception :
149+ if self .cancellation_exception is None :
150+ raise
151+ if isinstance (self .cancellation_exception , type ):
152+ raise self .cancellation_exception (str (original_exception )) from original_exception
153+ raise self .cancellation_exception from original_exception
154+ return False
155+
94156 def _traverse (self , token : AbstractToken ) -> Generator [Path , None , None ]:
95157 excludes_spec = pathspec .PathSpec .from_lines ('gitwildmatch' , self .exclude )
96158
97159 for path in self .paths :
160+ if not self ._check_token (token ):
161+ return
98162 base_path = Path (path )
99- if token :
100- for child_path in base_path .rglob ('*' ):
101- if (
102- (not self .only_files or child_path .is_file ())
103- and not (
104- excludes_spec .match_file (child_path )
105- or (child_path .is_dir () and excludes_spec .match_file (f'{ child_path } /' ))
106- )
107- and (self .extensions is None or child_path .suffix in self .extensions )
108- and (self .filter is None or self .filter (child_path ))
109- ):
110- yield child_path
111-
112- if not token :
113- break
114- else :
115- break
163+ for child_path in base_path .rglob ('*' ):
164+ if (
165+ (not self .only_files or child_path .is_file ())
166+ and not (
167+ excludes_spec .match_file (child_path )
168+ or (child_path .is_dir () and excludes_spec .match_file (f'{ child_path } /' ))
169+ )
170+ and (self .extensions is None or child_path .suffix in self .extensions )
171+ and (self .filter is None or self .filter (child_path ))
172+ ):
173+ yield child_path
174+
175+ if not self ._check_token (token ):
176+ return
177+ self ._check_token (token )
116178
117179 def go (self , token : AbstractToken = DefaultToken ()) -> Generator [Path , None , None ]: # noqa: B008
118- token = token + self .token
180+ instance_token = self .token
181+ token = token + instance_token
119182
120183 if self .frozen :
121184 snapshot = list (self ._traverse (token ))
122185 for path in snapshot :
123- if not token :
124- break
186+ if not self . _check_token ( token ) :
187+ return
125188 yield path
126189 else :
127190 yield from self ._traverse (token )
0 commit comments