2
2
import glob
3
3
import logging
4
4
import re
5
+ from copy import deepcopy
5
6
from difflib import context_diff
6
7
from typing import List , MutableMapping , Optional
7
8
@@ -27,6 +28,7 @@ def __init__(
27
28
self .serialize = file_cfg .serialize or version_config .serialize_formats
28
29
self .search = search or file_cfg .search or version_config .search
29
30
self .replace = replace or file_cfg .replace or version_config .replace
31
+ self .no_regex = file_cfg .no_regex or False
30
32
self .ignore_missing_version = file_cfg .ignore_missing_version or False
31
33
self .version_config = VersionConfig (
32
34
self .parse , self .serialize , self .search , self .replace , version_config .part_configs
@@ -62,7 +64,7 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool:
62
64
Returns:
63
65
True if the version number is in fact present.
64
66
"""
65
- search_expression = self .search . format ( ** context )
67
+ search_expression = self .get_search_pattern ( context )
66
68
67
69
if self .contains (search_expression ):
68
70
return True
@@ -75,44 +77,33 @@ def contains_version(self, version: Version, context: MutableMapping) -> bool:
75
77
# very specific parts of the file
76
78
search_pattern_is_default = self .search == self .version_config .search
77
79
78
- if search_pattern_is_default and self .contains (version .original ):
79
- # original version is present, and we're not looking for something
80
+ if search_pattern_is_default and self .contains (re . compile ( re . escape ( version .original )) ):
81
+ # The original version is present, and we're not looking for something
80
82
# more specific -> this is accepted as a match
81
83
return True
82
84
83
85
# version not found
84
86
if self .ignore_missing_version :
85
87
return False
86
- raise VersionNotFoundError (f"Did not find '{ search_expression } ' in file: '{ self .path } '" )
88
+ raise VersionNotFoundError (f"Did not find '{ search_expression . pattern } ' in file: '{ self .path } '" )
87
89
88
- def contains (self , search : str ) -> bool :
90
+ def contains (self , search : re . Pattern ) -> bool :
89
91
"""Does the work of the contains_version method."""
90
92
if not search :
91
93
return False
92
94
93
- f = self .get_file_contents ()
94
- search_lines = search .splitlines ()
95
- lookbehind = []
96
-
97
- for lineno , line in enumerate (f .splitlines (keepends = True )):
98
- lookbehind .append (line .rstrip ("\n " ))
99
-
100
- if len (lookbehind ) > len (search_lines ):
101
- lookbehind = lookbehind [1 :]
102
-
103
- if (
104
- search_lines [0 ] in lookbehind [0 ]
105
- and search_lines [- 1 ] in lookbehind [- 1 ]
106
- and search_lines [1 :- 1 ] == lookbehind [1 :- 1 ]
107
- ):
108
- logger .info (
109
- "Found '%s' in %s at line %s: %s" ,
110
- search ,
111
- self .path ,
112
- lineno - (len (lookbehind ) - 1 ),
113
- line .rstrip (),
114
- )
115
- return True
95
+ contents = self .get_file_contents ()
96
+
97
+ for m in re .finditer (search , contents ):
98
+ line_no = contents .count ("\n " , 0 , m .start (0 )) + 1
99
+ logger .info (
100
+ "Found '%s' in %s at line %s: %s" ,
101
+ search ,
102
+ self .path ,
103
+ line_no ,
104
+ m .string [m .start () : m .end (0 )],
105
+ )
106
+ return True
116
107
return False
117
108
118
109
def replace_version (
@@ -124,24 +115,17 @@ def replace_version(
124
115
context ["current_version" ] = self .version_config .serialize (current_version , context )
125
116
if new_version :
126
117
context ["new_version" ] = self .version_config .serialize (new_version , context )
127
- re_context = {key : re .escape (str (value )) for key , value in context .items ()}
128
118
129
- search_for = self .version_config .search .format (** re_context )
130
- search_for_re = self .compile_regex (search_for )
119
+ search_for = self .get_search_pattern (context )
131
120
replace_with = self .version_config .replace .format (** context )
132
121
133
- if search_for_re :
134
- file_content_after = search_for_re .sub (replace_with , file_content_before )
135
- else :
136
- file_content_after = file_content_before .replace (search_for , replace_with )
122
+ file_content_after = search_for .sub (replace_with , file_content_before )
137
123
138
124
if file_content_before == file_content_after and current_version .original :
139
- search_for_original_formatted = self .version_config .search .format (current_version = current_version .original )
140
- search_for_original_formatted_re = self .compile_regex (re .escape (search_for_original_formatted ))
141
- if search_for_original_formatted_re :
142
- file_content_after = search_for_original_formatted_re .sub (replace_with , file_content_before )
143
- else :
144
- file_content_after = file_content_before .replace (search_for_original_formatted , replace_with )
125
+ og_context = deepcopy (context )
126
+ og_context ["current_version" ] = current_version .original
127
+ search_for_og = self .get_search_pattern (og_context )
128
+ file_content_after = search_for_og .sub (replace_with , file_content_before )
145
129
146
130
if file_content_before != file_content_after :
147
131
logger .info ("%s file %s:" , "Would change" if dry_run else "Changing" , self .path )
@@ -164,14 +148,25 @@ def replace_version(
164
148
if not dry_run : # pragma: no-coverage
165
149
self .write_file_contents (file_content_after )
166
150
167
- def compile_regex (self , pattern : str ) -> Optional [re .Pattern ]:
168
- """Compile the regex if it is valid, otherwise return None."""
151
+ def get_search_pattern (self , context : MutableMapping ) -> re .Pattern :
152
+ """Compile and return the regex if it is valid, otherwise return the string."""
153
+ # the default search pattern is escaped, so we can still use it in a regex
154
+ default = re .compile (re .escape (self .version_config .search .format (** context )), re .MULTILINE | re .DOTALL )
155
+ if self .no_regex :
156
+ logger .debug ("No RegEx flag detected. Searching for the default pattern: '%s'" , default .pattern )
157
+ return default
158
+
159
+ re_context = {key : re .escape (str (value )) for key , value in context .items ()}
160
+ regex_pattern = self .version_config .search .format (** re_context )
169
161
try :
170
- search_for_re = re .compile (pattern )
162
+ search_for_re = re .compile (regex_pattern , re .MULTILINE | re .DOTALL )
163
+ logger .debug ("Searching for the regex: '%s'" , search_for_re .pattern )
171
164
return search_for_re
172
165
except re .error as e :
173
- logger .error ("Invalid regex '%s' for file %s: %s. Treating it as a regular string." , pattern , self .path , e )
174
- return None
166
+ logger .error ("Invalid regex '%s' for file %s: %s." , default , self .path , e )
167
+
168
+ logger .debug ("Searching for the default pattern: '%s'" , default .pattern )
169
+ return default
175
170
176
171
def __str__ (self ) -> str : # pragma: no-coverage
177
172
return self .path
0 commit comments