fparser Reference Guide  0.0.14
C99Preprocessor.py
1 # Copyright (c) 2020 Science and Technology Facilities Council
2 
3 # All rights reserved.
4 
5 # Modifications made as part of the fparser project are distributed
6 # under the following license:
7 
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions are
10 # met:
11 
12 # 1. Redistributions of source code must retain the above copyright
13 # notice, this list of conditions and the following disclaimer.
14 
15 # 2. Redistributions in binary form must reproduce the above copyright
16 # notice, this list of conditions and the following disclaimer in the
17 # documentation and/or other materials provided with the distribution.
18 
19 # 3. Neither the name of the copyright holder nor the names of its
20 # contributors may be used to endorse or promote products derived from
21 # this software without specific prior written permission.
22 
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
26 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
27 # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
31 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 
35 """C99 Preprocessor Syntax Rules.
36 """
37 # Author: Balthasar Reuter <balthasar.reuter@ecmwf.int>
38 # Based on previous work by Martin Schlipf (https://github.com/martin-schlipf)
39 # First version created: Jan 2020
40 
41 import re
42 import sys
43 
44 from fparser.common.readfortran import FortranReaderBase, CppDirective
45 from fparser.two import pattern_tools as pattern
46 from fparser.two.utils import Base, StringBase, WORDClsBase
47 
48 
49 # The list of classes that implement preprocessor directives
50 # This list is used in match_cpp_directive() and
51 # fparser.two.utils.BlockBase.match().
52 CPP_CLASS_NAMES = [
53  "Cpp_If_Stmt",
54  "Cpp_Elif_Stmt",
55  "Cpp_Else_Stmt",
56  "Cpp_Endif_Stmt",
57  "Cpp_Include_Stmt",
58  "Cpp_Macro_Stmt",
59  "Cpp_Undef_Stmt",
60  "Cpp_Line_Stmt",
61  "Cpp_Error_Stmt",
62  "Cpp_Warning_Stmt",
63  "Cpp_Null_Stmt",
64 ]
65 
66 
67 def match_cpp_directive(reader):
68  """Creates single-line C99 preprocessor directive object from the
69  current line, if any is found.
70 
71  :param reader: the fortran file reader containing the line \
72  of code that we are trying to match.
73  :type reader: :py:class:`fparser.common.readfortran.FortranFileReader` \
74  or \
75  :py:class:`fparser.common.readfortran.FortranStringReader`
76 
77  :return: the matched preprocessor directive object or `None`.
78  :rtype: one of (:py:class:`fparser.two.C99Preprocess.Cpp_*_Stmt`,) or \
79  `NoneType`
80 
81  """
82  # Assume we have potentially a CPP directive and only check for an
83  # instance of CppDirective if reader is a subclass of FortranReaderBase
84  is_potential_cpp_directive = True
85  if isinstance(reader, FortranReaderBase):
86  item = reader.get_item()
87  is_potential_cpp_directive = isinstance(item, CppDirective)
88  if item:
89  reader.put_item(item)
90  # Do not bail out early here to have a catch all return statement
91  # at the end (that would not be reachable by tests otherwise)
92  if is_potential_cpp_directive:
93  for cls in CPP_CLASS_NAMES:
94  obj = getattr(sys.modules[__name__], cls)(reader)
95  if obj:
96  return obj
97  return None
98 
99 
100 #
101 # ISO/IEC 9899: 1999 (C99)
102 #
103 
104 #
105 # 6.10 Preprocessing directives
106 #
107 
108 
110  """Generic class for preprocessor tokens that form the right hand
111  side of a preprocessor directive (such as #error, #line, #if, etc.).
112  """
113 
114  subclass_names = []
115 
116  @staticmethod
117  def match(string):
118  """Implements the matching for arbitrary preprocessor tokens
119  that form the right hand side of a preprocessor directive. It
120  does not impose any restrictions other than the string not
121  being empty.
122 
123  :param str string: the string to be matched as pp-tokens.
124 
125  :return: a 1-tuple containing the matched string or None.
126  :rtype: (str,) or NoneType
127 
128  """
129  if not string:
130  return None
131  line = string.strip()
132  if not line:
133  return None
134  return (line,)
135 
136  def tostr(self):
137  """
138  :return: this pp-tokens as a string.
139  :rtype: str
140  """
141  return self.items[0]
142 
143 
144 # 6.10.1 Conditional inclusion
145 # Deviating from the standard's definition, not entire if-else-endif
146 # blocks are treated as constructs but instead the relevant preprocessor
147 # directives are kept as single line nodes.
148 
149 
151  """
152  C99 6.10.1 Conditional inclusion
153 
154  if-stmt is # if constant-expression new-line
155  or ifdef identifier new-line
156  or ifndef identifier new-line
157 
158  """
159 
160  subclass_names = []
161  use_names = ["Cpp_Macro_Identifier", "Cpp_Pp_Tokens"]
162 
163  _regex = re.compile(r"#\s*(ifdef|ifndef|if)\b")
164  _if_pattern = pattern.Pattern("<if>", r"^\s*#\s*if\b", value="#if")
165  _def_pattern = (
166  pattern.Pattern("<ifdef>", r"^\s*#\s*ifdef\b", value="#ifdef"),
167  pattern.Pattern("<ifndef>", r"^\s*#\s*ifndef\b", value="#ifndef"),
168  )
169 
170  @staticmethod
171  def match(string):
172  """Implements the matching for an if preprocessor directive
173  (or its variations ifdef, ifndef). For ifdef and ifndef
174  statements it matches the macro identifier using
175  :py:class:`fparser.two.C99Preprocesser.Cpp_Macro_Identifier`
176  otherwise it uses :py:class:`fparser.two.C99Preprocessor.Cpp_Pp_Tokens`
177  to accept any non-empty string as rhs.
178 
179  :param str string: the string to match with as an if statement.
180 
181  :return: a tuple of size 2 containing the statement's keyword \
182  and the right hand side, or `None` if there is no match.
183  :rtype: \
184  (`str`, py:class:`fparser.two.C99Preprocessor.Cpp_Macro_Identifier`)\
185  or (`str`, py:class:`fparser.two.C99Preprocessor.Cpp_Pp_Tokens`) \
186  or `NoneType`
187 
188  """
189  if not string:
190  return None
191  result = WORDClsBase.match(
192  Cpp_If_Stmt._if_pattern,
193  Cpp_Pp_Tokens,
194  string,
195  colons=False,
196  require_cls=True,
197  )
198  return result or WORDClsBase.match(
199  Cpp_If_Stmt._def_pattern,
200  Cpp_Macro_Identifier,
201  string,
202  colons=False,
203  require_cls=True,
204  )
205 
206  def tostr(self):
207  """
208  :return: this if-stmt as a string.
209  :rtype: str
210  """
211  return "{0} {1}".format(*self.items)
212 
213 
215  """
216  C99 6.10.1 Conditional inclusion
217 
218  elif-stmt is # elif constant-expression new-line
219 
220  """
221 
222  subclass_names = []
223  use_names = ["Cpp_Pp_Tokens"]
224 
225  _pattern = pattern.Pattern("<elif-stmt>", r"^\s*#\s*elif\b", value="#elif")
226 
227  @staticmethod
228  def match(string):
229  """Implements the matching for an elif preprocessor directive.
230 
231  :param str string: the string to match with as an elif statement.
232 
233  :return: a tuple of size 2 containing the statements keyword and \
234  right hand side, or `None` if there is no match.
235  :rtype: \
236  (`str`, :py:class:`fparser.two.C99_Preprocessor.Cpp_Pp_Tokens`) \
237  or `NoneType`
238 
239  """
240  if not string:
241  return None
242  return WORDClsBase.match(
243  Cpp_Elif_Stmt._pattern,
244  Cpp_Pp_Tokens,
245  string,
246  colons=False,
247  require_cls=True,
248  )
249 
250  def tostr(self):
251  """
252  :return: this elif-stmt as a string.
253  :rtype: str
254  """
255  return "{0} {1}".format(*self.items)
256 
257 
259  """
260  C99 6.10.1 Conditional inclusion
261 
262  else-stmt is # else new-line
263 
264  """
265 
266  subclass_names = []
267 
268  _pattern = pattern.Pattern("<else>", r"^\s*#\s*else\b")
269 
270  @staticmethod
271  def match(string):
272  """Implements the matching for an else preprocessor directive.
273 
274  :param str string: the string to match with as an else statement.
275 
276  :return: a 1-tuple containing the matched string or `None` if \
277  there is no match.
278  :rtype: (str,) or NoneType
279 
280  """
281  if not string:
282  return None
283  return StringBase.match(Cpp_Else_Stmt._pattern, string)
284 
285  def tostr(self):
286  """
287  :return: this else-stmt as a string.
288  :rtype: str
289  """
290  return self.string
291 
292 
294  """
295  C99 6.10.1 Conditional inclusion
296 
297  endif-stmt is # endif new-line
298 
299  """
300 
301  subclass_names = []
302 
303  _pattern = pattern.Pattern("<endif>", r"^\s*#\s*endif\b", value="#endif")
304 
305  @staticmethod
306  def match(string):
307  """Implements the matching for an endif preprocessor directive.
308 
309  :param str string: the string to match with as an endif statement.
310 
311  :return: a 1-tuple containing the matched string or `None` if \
312  there is no match.
313  :rtype: (str,) or NoneType
314 
315  """
316  if not string:
317  return None
318  return StringBase.match(Cpp_Endif_Stmt._pattern, string)
319 
320  def tostr(self):
321  """
322  :return: this endif-stmt as a string.
323  :rtype: str
324  """
325  return self.string
326 
327 
328 class Cpp_Include_Stmt(Base): # 6.10.2 Source file inclusion
329  """
330  C99 6.10.2 Source file inclusion
331 
332  include_stmt is # include [ <h-char-sequence>
333  or "q-char-sequence"
334  or pp-tokens ] new-line
335  """
336 
337  _regex = re.compile(r"#\s*include\b")
338 
339  use_names = ["Include_Filename"]
340 
341  @staticmethod
342  def match(string):
343  """Implements the matching for an include statement.
344 
345  Allows for the filename to appear in double quotes or angle
346  brackets. Only very loose restrictions are enforced for the
347  filename, which is matched by
348  py:class:`fparser.two.Fortran2003.Include_Filename`.
349 
350  :param str string: the string to match with as an include statement.
351 
352  :return: a tuple of size 1 containing a \
353  py:class:`fparser.two.C99Preprocessor.Cpp_Include_Filename` \
354  object with the matched filename if there is a match, \
355  or `None` if there is not.
356  :rtype: (:py:class:`fparser.two.Fortran2003.Include_Filename`, ) \
357  or `NoneType`
358 
359  """
360  from fparser.two.Fortran2003 import Include_Filename
361 
362  if not string:
363  return None
364  line = string.strip()
365  found = Cpp_Include_Stmt._regex.match(line)
366  if not found:
367  # The line does not match an include statement
368  return None
369  rhs = line[found.end() :].strip()
370  if rhs is None or len(rhs) < 3:
371  # Either we didn't find any includes or the content after
372  # the include token is too short to be valid (it must at
373  # least contain quotes and one character.
374  return None
375  if not (rhs[0] == '"' and rhs[-1] == '"') and not (
376  rhs[0] == "<" and rhs[-1] == ">"
377  ):
378  # The filename should be surrounded by double
379  # quotes or '<...>' but this is not the case.
380  return None
381  # Remove the quotes.
382  file_name = rhs[1:-1]
383  # Pass the potential filename to the relevant class.
384  return (Include_Filename(file_name),)
385 
386  def tostr(self):
387  """
388  :return: this include-stmt as a string.
389  :rtype: str
390  """
391  return '#include "{0}"'.format(self.items[0])
392 
393 
394 class Cpp_Macro_Stmt(Base): # 6.10.3 Macro replacement
395  """
396  C99 6.10.3 Macro replacement
397 
398  macro_stmt is # define identifier [( [identifier-list] ) or (...) ]
399  [ replacement-list ] new-line
400 
401  Important: No preceding whitespace is allowed for the left parenthesis of
402  the optional identifier-list. If a preceding whitespace is encountered,
403  the bracket is considered part of the replacement-list.
404  """
405 
406  use_names = ["Cpp_Macro_Identifier", "Cpp_Macro_Identifier_List", "Cpp_Pp_Tokens"]
407 
408  _regex = re.compile(r"#\s*define\b")
409 
410  @staticmethod
411  def match(string):
412  """Implements the matching for a preprocessor macro definition.
413 
414  It matches define directives with macro identifier, optional
415  identifier list, and optional replacement list. The macro
416  identifier is matched using
417  :py:class:`fparser.two.C99Preprocessor.Cpp_Macro_Identifier`
418  and the optional argument identifier list using
419  :py:class:`fparser.two.C99Preprocessor.Cpp_Macro_Identifier_List`.
420 
421  Important: No preceding whitespace is allowed for the left
422  parentheses of the dentifier-list. If a preceding whitespace is
423  encountered, the it is considered part of the replacement-list.
424 
425  :param str string: the string to match with as an if statement.
426 
427  :return: a tuple of size 3 containing the macro identifier, \
428  identifier list or None, and replacement list or None, \
429  or `None` if there is no match.
430  :rtype: \
431  (py:class:`fparser.two.C99Preprocessor.Cpp_Macro_Identifier`, \
432  py:class:`fparser.two.C99Preprocessor.Cpp_Macro_Identifier_List` \
433  or NoneType, \
434  py:class:`fparser.two.C99Preprocessor.Cpp_Pp_Tokens` or NoneType) \
435  or `NoneType`
436 
437  """
438  if not string:
439  return None
440  line = string.strip()
441  found = Cpp_Macro_Stmt._regex.match(line)
442  if not found:
443  # The line does not match a define statement
444  return None
445  rhs = line[found.end() :].strip()
446  found = pattern.macro_name.match(rhs)
447  if not found:
448  return None
449  name = Cpp_Macro_Identifier(found.group())
450  definition = rhs[found.end() :]
451  # note no strip here because '#define MACRO(x)' and
452  # '#define MACRO (x)' are functionally different
453  if not definition:
454  return (name, None, None)
455  if definition[0] == "(":
456  found = Cpp_Macro_Identifier_List._pattern.match(definition)
457  if not found:
458  # The definition starts with a bracket (without preceding
459  # white space) but does not match an identifier list
460  return None
461  parameter_list = Cpp_Macro_Identifier_List(found.group())
462  # Note that the definition can potentially be an empty string
463  # which is nonetheless included explicitly to distinguish a
464  # macro definition with arguments from one without but with
465  # a definition
466  definition = definition[found.end() :]
467  else:
468  parameter_list = None
469  # now that definition only holds the replacement list, we can strip
470  definition = definition.strip()
471  if definition:
472  definition = Cpp_Pp_Tokens(definition)
473  else:
474  definition = None
475  return (name, parameter_list, definition)
476 
477  def tostr(self):
478  """
479  :return: this macro-stmt as a string.
480  :rtype: str
481  """
482  return "#define {0}{1}{2}{3}".format(
483  self.items[0],
484  self.items[1] or "",
485  " " if self.items[2] else "",
486  self.items[2] or "",
487  )
488 
489 
490 class Cpp_Macro_Identifier(StringBase): # pylint: disable=invalid-name
491  """Implements the matching of a macro identifier."""
492 
493  # There are no other classes. This is a simple string match.
494  subclass_names = []
495 
496  @staticmethod
497  def match(string):
498  """Implements the matching of a macro identifier.
499 
500  It matches the string with the regular expression abs_macro_name
501  in the pattern_tools file. The macro identifier may contain
502  only letters and underscore.
503 
504  :param str string: the string to match with the pattern rule.
505 
506  :return: a tuple of size 1 containing a string with the \
507  matched name if there is a match, or None if there is not.
508  :rtype: (str) or NoneType
509 
510  """
511  return StringBase.match(pattern.abs_macro_name, string.strip())
512 
513 
515  """Implements the matching of an identifier list in a macro definition.
516 
517  identifier-list is (identifier [, identifier-list or ...])
518  or (...)
519  """
520 
521  subclass_names = []
522 
523  _pattern = pattern.Pattern(
524  "<identifier-list>",
525  r"\((\s*[A-Za-z_]\w*"
526  r"(?:\s*,\s*[A-Za-z_]\w*)*"
527  r"(?:\s*,\s*\.{3})?|\.{3})?\s*\)",
528  )
529 
530  @staticmethod
531  def match(string):
532  """Implements the matching of a macro identifier list as part of
533  a macro definition. It must consist of one or more macro
534  identifier separated by comma, or "..." for a variadic argument
535  list, and must be surrouned by parentheses.
536 
537  For simplicity, the matched list is kept as a single string and not
538  matched as
539  :py:class:`fparser.two.C99Preprocessor.Cpp_Macro_Identifier`.
540 
541  :param str string: the string to match with the pattern rule.
542 
543  :return: a tuple of size 1 containing a string with the \
544  matched identifier list if there is a match, or None if \
545  there is not.
546  :rtype: (`str`,) or `NoneType`
547 
548  """
549  if not string:
550  return None
551  return StringBase.match(Cpp_Macro_Identifier_List._pattern, string)
552 
553  def tostr(self):
554  """
555  :return: this macro-identifier-list as a string.
556  :rtype: str
557  """
558  return self.string
559 
560 
562  """Implements the matching of a preprocessor undef statement for a macro.
563 
564  undef-stmt is # undef identifier new-line
565 
566  Strictly, this is part of 6.10.3 but since it is identified by a different
567  directive keyword (undef instead of define) we treat it separately.
568 
569  """
570 
571  subclass_names = []
572 
573  use_names = ["Cpp_Macro_Identifier"]
574 
575  _pattern = pattern.Pattern("<undef>", r"^\s*(#\s*undef)\b", value="#undef")
576 
577  @staticmethod
578  def match(string):
579  """Implements the matching for a preprocessor undef statement
580  for a macro. The macro identifier is matched using
581  :py:class:`fparser.two.C99Preprocessor.Cpp_Macro_Identifier`.
582 
583  :param str string: the string to match with as an if statement.
584 
585  :return: a tuple of size 1 containing the macro identifier, or\
586  `None` if there is no match.
587  :rtype: (py:class:`fparser.two.C99Preprocessor.Cpp_Macro_Identifier`) \
588  or `NoneType`
589 
590  """
591  if not string:
592  return None
593  return WORDClsBase.match(
594  Cpp_Undef_Stmt._pattern,
595  Cpp_Macro_Identifier,
596  string,
597  colons=False,
598  require_cls=True,
599  )
600 
601  def tostr(self):
602  """
603  :return: this undef-stmt as a string.
604  :rtype: str
605  """
606  return "{0} {1}".format(*self.items)
607 
608 
609 class Cpp_Line_Stmt(WORDClsBase): # 6.10.4 Line control
610  """
611  C99 6.10.4 Line control
612 
613  line-stmt is # line digit-sequence [ "s-char-sequence" ] new-line
614  or pp-tokens new-line
615  """
616 
617  subclass_names = []
618  use_names = ["Cpp_Pp_Tokens"]
619 
620  _pattern = pattern.Pattern("<line>", r"^\s*#\s*line\b", value="#line")
621 
622  @staticmethod
623  def match(string):
624  """Implements the matching for a line preprocessor directive.
625  The right hand side of the directive is not matched any further
626  but simply kept as a string.
627 
628  :param str string: the string to match with as a line statement.
629 
630  :return: a tuple of size 1 with the right hand side as a string, \
631  or `None` if there is no match.
632  :rtype: (`str`) or `NoneType`
633 
634  """
635  if not string:
636  return None
637  return WORDClsBase.match(
638  Cpp_Line_Stmt._pattern,
639  Cpp_Pp_Tokens,
640  string,
641  colons=False,
642  require_cls=True,
643  )
644 
645  def tostr(self):
646  """
647  :return: this line-stmt as a string.
648  :rtype: str
649  """
650  return "{0} {1}".format(*self.items)
651 
652 
653 class Cpp_Error_Stmt(WORDClsBase): # 6.10.5 Error directive
654  """
655  C99 6.10.5 Error directive
656 
657  error-stmt is # error [pp-tokens] new-line
658 
659  """
660 
661  subclass_names = []
662  use_names = ["Cpp_Pp_Tokens"]
663 
664  _pattern = pattern.Pattern("<error>", r"^\s*#\s*error\b", value="#error")
665 
666  @staticmethod
667  def match(string):
668  """Implements the matching for an error preprocessor directive.
669  The optional right hand side of the directive is not matched any
670  further but simply kept as a string.
671 
672  :param str string: the string to match with as a line statement.
673 
674  :return: an empty tuple or a tuple of size 1 with the right hand \
675  side as a string, or `None` if there is no match.
676  :rtype: () or (`str`) or `NoneType`
677 
678  """
679  if not string:
680  return None
681  return WORDClsBase.match(
682  Cpp_Error_Stmt._pattern,
683  Cpp_Pp_Tokens,
684  string,
685  colons=False,
686  require_cls=False,
687  )
688 
689  def tostr(self):
690  """
691  :return: this error-stmt as a string.
692  :rtype: str
693  """
694  if self.items[1]:
695  return "{0} {1}".format(*self.items)
696  return self.items[0]
697 
698 
700  """
701  Not actually part of C99 but supported by most preprocessors and
702  with syntax identical to Cpp_Error_Stmt
703 
704  warning-stmt is # warning [pp-tokens] new-line
705 
706  """
707 
708  subclass_names = []
709  use_names = ["Cpp_Pp_Tokens"]
710 
711  _pattern = pattern.Pattern("<warning>", r"^\s*#\s*warning\b", value="#warning")
712 
713  @staticmethod
714  def match(string):
715  """Implements the matching for a warning preprocessor directive.
716  The optional right hand side of the directive is not matched any
717  further but simply kept as a string.
718 
719  :param str string: the string to match with as a line statement.
720 
721  :return: an empty tuple or a tuple of size 1 with the right hand \
722  side as a string, or `None` if there is no match.
723  :rtype: () or (`str`) or `NoneType`
724 
725  """
726  if not string:
727  return None
728  return WORDClsBase.match(
729  Cpp_Warning_Stmt._pattern,
730  Cpp_Pp_Tokens,
731  string,
732  colons=False,
733  require_cls=False,
734  )
735 
736  def tostr(self):
737  """
738  :return: this warning-stmt as a string.
739  :rtype: str
740  """
741  if self.items[1]:
742  return "{0} {1}".format(*self.items)
743  return self.items[0]
744 
745 
746 # 6.10.6 Pragma directive
747 # Pragma Preprocessor directives not implemented since Fortran has its own
748 # Pragma syntax in the form of comments. For that reason, most preprocessors
749 # do not support C preprocess pragmas in Fortran code either.
750 
751 
752 class Cpp_Null_Stmt(Base): # 6.10.7 Null directive
753  """
754  C99 6.10.7 Null directive
755 
756  null-stmt is # new-line
757 
758  """
759 
760  subclass_names = []
761 
762  @staticmethod
763  def match(string):
764  """Implements the matching for a Null (empty) directive.
765 
766  :param str string: the string to match with as a line statement.
767 
768  :return: an empty tuple or `None` if there is no match.
769  :rtype: () or `NoneType`
770 
771  """
772  if not string:
773  return None
774  line = string.strip()
775  if not line == "#":
776  return None
777  return ()
778 
779  def tostr(self):
780  """
781  :return: this null-stmt as a string.
782  :rtype: str
783  """
784  return "#"
def match(keyword, cls, string, colons=False, require_cls=False)
Definition: utils.py:1608