33import threading
44import json
55import time
6+ import re
67from collections import defaultdict
78from enum import Enum
89from contextlib import suppress , contextmanager
910from copy import copy
10- from typing import Callable , Mapping , Optional , Union
11+ from typing import Callable , Mapping , Optional , Union , Pattern
1112from ssl import SSLContext
1213import abc
1314
@@ -230,13 +231,27 @@ def _create_query_matcher(query_string: Union[None, QueryMatcher, str, bytes, Ma
230231 raise TypeError ("Unable to cast this type to QueryMatcher: {!r}" .format (type (query_string )))
231232
232233
234+ class URIPattern (abc .ABC ):
235+ @abc .abstractmethod
236+ def match (self , uri : str ) -> bool :
237+ """
238+ Matches the provided URI.
239+
240+ :param uri: URI of the request. This is an absolute path startting
241+ with "/" and does not contain the query part.
242+ :return: True if there's a match, False otherwise
243+ """
244+ pass
245+
246+
233247class RequestMatcher :
234248 """
235249 Matcher object for the incoming request.
236250
237251 It defines various parameters to match the incoming request.
238252
239- :param uri: URI of the request. This must be an absolute path starting with ``/``.
253+ :param uri: URI of the request. This must be an absolute path starting with ``/``, a
254+ :py:class:`URIPattern` object, or a regular expression compiled by re.compile.
240255 :param method: HTTP method of the request. If not specified (or `METHOD_ALL`
241256 specified), all HTTP requests will match.
242257 :param data: payload of the HTTP request. This could be a string (utf-8 encoded
@@ -253,7 +268,7 @@ class RequestMatcher:
253268
254269 def __init__ (
255270 self ,
256- uri : str ,
271+ uri : Union [ str , URIPattern , Pattern [ str ]] ,
257272 method : str = METHOD_ALL ,
258273 data : Union [str , bytes , None ] = None ,
259274 data_encoding : str = "utf-8" ,
@@ -300,6 +315,27 @@ def match_data(self, request: Request) -> bool:
300315 return True
301316 return request .data == self .data
302317
318+ def match_uri (self , request : Request ) -> bool :
319+ path = request .path
320+
321+ if isinstance (self .uri , URIPattern ):
322+ return self .uri .match (path )
323+
324+ # this is python version depending
325+ # in python 3.7 and above: it is re.Pattern
326+ # below python 3.7 it is _sre.SRE_Pattern which cannot be accessed directly
327+ elif isinstance (self .uri , re .compile ("" ).__class__ ):
328+ return bool (self .uri .match (path ))
329+
330+ else :
331+ # there could be a guard isinstance(self.uri, str) been here
332+ # but we want to allow any object which provides the __eq__ parameter
333+ # (note: in this case it will be not typeing correct)
334+ #
335+ # also, python will raise TypeError when self.uri is a conflicting type
336+
337+ return self .uri == URI_DEFAULT or path == self .uri
338+
303339 def difference (self , request : Request ) -> list :
304340 """
305341 Calculates the difference between the matcher and the request.
@@ -313,7 +349,8 @@ def difference(self, request: Request) -> list:
313349 """
314350
315351 retval = []
316- if self .uri != URI_DEFAULT and request .path != self .uri :
352+
353+ if not self .match_uri (request ):
317354 retval .append (("uri" , request .path , self .uri ))
318355
319356 if self .method != METHOD_ALL and self .method != request .method :
0 commit comments