Repository: stfc/fparser

Issue:
Add support for the open convert specifier (which is not part of the standard)
fparser fails to parse:
```
       open(unit       = MyUnit,       &
            file       = trim(filename), &
            form       = 'UNFORMATTED',  &
            access     = 'SEQUENTIAL',   &
            action     = 'WRITE',        &
            convert    = 'BIG_ENDIAN',   &
            iostat     = E_IO)
```

I believe this is because the "convert" is not part of the Fortran standard but it is supported by gfortran, nvfortran and ifort
https://gcc.gnu.org/onlinedocs/gfortran/CONVERT-specifier.html

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01cf9b6..7567f67 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,9 @@ Modifications by (in alphabetical order):
 * P. Vitt, University of Siegen, Germany
 * A. Voysey, UK Met Office
 
+29/01/2024 PR #435 for #426. Add support for the CONVERT extension of the open()
+           intrinsic.
+
 25/01/2024 PR #418 for #313. Allow intrinsic shadowing and improve fparser symbol table.
 
 11/01/2024 PR #439 for #432. Fix RTD build and clean up setuptools config.
diff --git a/doc/source/fparser2.rst b/doc/source/fparser2.rst
index c7a1722..661c756 100644
--- a/doc/source/fparser2.rst
+++ b/doc/source/fparser2.rst
@@ -1,4 +1,4 @@
-..  Copyright (c) 2017-2022 Science and Technology Facilities Council.
+..  Copyright (c) 2017-2024 Science and Technology Facilities Council.
 
     All rights reserved.
 
@@ -353,6 +353,19 @@ extension adds support for the dollar descriptor in fparser.
 For more information see
 https://software.intel.com/en-us/fortran-compiler-developer-guide-and-reference-dollar-sign-and-backslash-editing
 
+CONVERT argument to OPEN
+++++++++++++++++++++++++
+
+The CONVERT argument may be used to specify how unformatted data being read
+from file is to be converted before being stored. For example::
+
+  OPEN(unit=23, file="some_data", form='unformatted',access='sequential', &
+       convert="LITTLE_ENDIAN")
+
+This extension is supported by (at least) the Gnu, Intel and Cray compilers
+but is not a part of any Fortran standard. More details can be found at
+https://gcc.gnu.org/onlinedocs/gfortran/CONVERT-specifier.html
+
 Classes
 -------
 
diff --git a/src/fparser/two/Fortran2003.py b/src/fparser/two/Fortran2003.py
index 2832bfb..02bba4b 100644
--- a/src/fparser/two/Fortran2003.py
+++ b/src/fparser/two/Fortran2003.py
@@ -101,6 +101,7 @@ from fparser.two.utils import (
     DynamicImport,
 )
 from fparser.two.utils import (
+    EXTENSIONS,
     NoMatchError,
     FortranSyntaxError,
     InternalSyntaxError,
@@ -323,7 +324,6 @@ class Program(BlockBase):  # R201
 
 
 class Include_Filename(StringBase):  # pylint: disable=invalid-name
-
     """Implements the matching of a filename from an include statement."""
 
     # There are no other classes. This is a simple string match.
@@ -346,7 +346,6 @@ class Include_Filename(StringBase):  # pylint: disable=invalid-name
 
 
 class Include_Stmt(Base):  # pylint: disable=invalid-name
-
     """Implements the matching of a Fortran include statement. There is no
     rule for this as the compiler is expected to inline any content
     from an include statement when one is found. However, for a parser
@@ -2602,7 +2601,6 @@ class Generic_Binding(StmtBase):
 
 
 class Binding_Attr(STRINGBase):  # pylint: disable=invalid-name
-
     """
     Fortran2003 Rule R453::
 
@@ -2632,7 +2630,6 @@ class Binding_Attr(STRINGBase):  # pylint: disable=invalid-name
 
 
 class Final_Binding(StmtBase, WORDClsBase):  # pylint: disable=invalid-name
-
     """
     Fortran2003 Rule R454::
 
@@ -4232,9 +4229,7 @@ class Cray_Pointer_Stmt(StmtBase, WORDClsBase):  # pylint: disable=invalid-name
         :rtype: (str, Cray_Pointer_Decl_List) or None
 
         """
-        from fparser.two.utils import EXTENSIONS
-
-        if "cray-pointer" not in EXTENSIONS:
+        if "cray-pointer" not in EXTENSIONS():
             return None
         return WORDClsBase.match(
             "POINTER", Cray_Pointer_Decl_List, string, require_cls=True
@@ -8546,7 +8541,9 @@ class Internal_File_Variable(Base):  # R903
 
 class Open_Stmt(StmtBase, CALLBase):  # R904
     """
-    <open-stmt> = OPEN ( <connect-spec-list> )
+    R904 is:
+
+    open-stmt is OPEN ( connect-spec-list )
     """
 
     subclass_names = []
@@ -8561,27 +8558,32 @@ class Open_Stmt(StmtBase, CALLBase):  # R904
 
 class Connect_Spec(KeywordValueBase):
     """
-    R905::
+    R905 is:
 
-        <connect-spec> = [ UNIT = ] <file-unit-number>
-                         | ACCESS = <scalar-default-char-expr>
-                         | ACTION = <scalar-default-char-expr>
-                         | ASYNCHRONOUS = <scalar-default-char-expr>
-                         | BLANK = <scalar-default-char-expr>
-                         | DECIMAL = <scalar-default-char-expr>
-                         | DELIM = <scalar-default-char-expr>
-                         | ENCODING = <scalar-default-char-expr>
-                         | ERR = <label>
-                         | FILE = <file-name-expr>
-                         | FORM = <scalar-default-char-expr>
-                         | IOMSG = <iomsg-variable>
-                         | IOSTAT = <scalar-int-variable>
-                         | PAD = <scalar-default-char-expr>
-                         | POSITION = <scalar-default-char-expr>
-                         | RECL = <scalar-int-expr>
-                         | ROUND = <scalar-default-char-expr>
-                         | SIGN = <scalar-default-char-expr>
-                         | STATUS = <scalar-default-char-expr>
+    connect-spec is [ UNIT = ] file-unit-number
+                 or ACCESS = scalar-default-char-expr
+                 or ACTION = scalar-default-char-expr
+                 or ASYNCHRONOUS = scalar-default-char-expr
+                 or BLANK = scalar-default-char-expr
+                 [ or CONVERT = scalar-default-char-expr ]
+                 or DECIMAL = scalar-default-char-expr
+                 or DELIM = scalar-default-char-expr
+                 or ENCODING = scalar-default-char-expr
+                 or ERR = label
+                 or FILE = file-name-expr
+                 or FORM = scalar-default-char-expr
+                 or IOMSG = iomsg-variable
+                 or IOSTAT = scalar-int-variable
+                 or PAD = scalar-default-char-expr
+                 or POSITION = scalar-default-char-expr
+                 or RECL = scalar-int-expr
+                 or ROUND = scalar-default-char-expr
+                 or SIGN = scalar-default-char-expr
+                 or STATUS = scalar-default-char-expr
+
+    Note that CONVERT is not a part of the Fortran standard but is supported
+    by several major compilers (Gnu, Intel, Cray etc.) and thus is matched
+    by fparser if the utils.EXTENSIONS() list includes the string 'open-convert'.
 
     """
 
@@ -8596,44 +8598,64 @@ class Connect_Spec(KeywordValueBase):
         "Scalar_Int_Variable",
     ]
 
-    @staticmethod
-    def match(string):
-        """
+    @classmethod
+    def _keyword_value_list(cls):
+        """
+        Defines the valid keywords and corresponding classes to match against.
+        This has to be a method rather than a class property as those classes
+        are generated after this class has been created.
+
+        :returns: list of keyword, class pairs to match against.
+        :rtype: list[tuple[str, type]]
+
+        """
+        result = [
+            ("ACCESS", Scalar_Default_Char_Expr),
+            ("ACTION", Scalar_Default_Char_Expr),
+            ("ASYNCHRONOUS", Scalar_Default_Char_Expr),
+            ("BLANK", Scalar_Default_Char_Expr),
+            ("DECIMAL", Scalar_Default_Char_Expr),
+            ("DELIM", Scalar_Default_Char_Expr),
+            ("ENCODING", Scalar_Default_Char_Expr),
+            ("FORM", Scalar_Default_Char_Expr),
+            ("PAD", Scalar_Default_Char_Expr),
+            ("POSITION", Scalar_Default_Char_Expr),
+            ("ROUND", Scalar_Default_Char_Expr),
+            ("SIGN", Scalar_Default_Char_Expr),
+            ("STATUS", Scalar_Default_Char_Expr),
+            ("ERR", Label),
+            ("FILE", File_Name_Expr),
+            ("IOSTAT", Scalar_Int_Variable),
+            ("IOMSG", Iomsg_Variable),
+            ("RECL", Scalar_Int_Expr),
+            ("UNIT", File_Unit_Number),
+        ]
+        if "open-convert" in EXTENSIONS():
+            # The CONVERT keyword is a non-standard extension supported by
+            # many compilers.
+            result.append(("CONVERT", Scalar_Default_Char_Expr))
+        return result
+
+    @classmethod
+    def match(cls, string):
+        """Implements the matching for connect-spec.
+
+        Note that this is implemented as a `classmethod` (not a
+        `staticmethod`), using attribute keywords from the list provided
+        as a class method. This allows expanding this list for
+        Fortran 2008 without having to reimplement the matching.
+
         :param str string: Fortran code to check for a match
         :return: 2-tuple containing the keyword and value or None if the
                  supplied string is not a match
         :rtype: 2-tuple containing keyword (e.g. "UNIT") and associated value
+
         """
         if "=" not in string:
             # The only argument which need not be named is the unit number
             return "UNIT", File_Unit_Number(string)
         # We have a keyword-value pair. Check whether it is valid...
-        for keyword, value in [
-            (
-                [
-                    "ACCESS",
-                    "ACTION",
-                    "ASYNCHRONOUS",
-                    "BLANK",
-                    "DECIMAL",
-                    "DELIM",
-                    "ENCODING",
-                    "FORM",
-                    "PAD",
-                    "POSITION",
-                    "ROUND",
-                    "SIGN",
-                    "STATUS",
-                ],
-                Scalar_Default_Char_Expr,
-            ),
-            ("ERR", Label),
-            ("FILE", File_Name_Expr),
-            ("IOSTAT", Scalar_Int_Variable),
-            ("IOMSG", Iomsg_Variable),
-            ("RECL", Scalar_Int_Expr),
-            ("UNIT", File_Unit_Number),
-        ]:
+        for keyword, value in cls._keyword_value_list():
             try:
                 obj = KeywordValueBase.match(keyword, value, string, upper_lhs=True)
             except NoMatchError:
@@ -10056,9 +10078,7 @@ class Hollerith_Item(Base):  # pylint: disable=invalid-name
         :rtype: str
 
         """
-        from fparser.two.utils import EXTENSIONS
-
-        if "hollerith" not in EXTENSIONS:
+        if "hollerith" not in EXTENSIONS():
             return None
         if not string:
             return None
@@ -10573,9 +10593,7 @@ class Control_Edit_Desc(Base):  # pylint: disable=invalid-name
         if not strip_string:
             return None
         if len(strip_string) == 1 and strip_string in "/:$":
-            from fparser.two.utils import EXTENSIONS
-
-            if strip_string == "$" and "dollar-descriptor" not in EXTENSIONS:
+            if strip_string == "$" and "dollar-descriptor" not in EXTENSIONS():
                 return None
             return None, strip_string
         if strip_string[-1] == "/":
@@ -10684,9 +10702,7 @@ class Position_Edit_Desc(Base):  # R1013
             return start, number_obj
         if strip_string_upper[-1] == "X":
             # We match *X
-            from fparser.two.utils import EXTENSIONS
-
-            if "x-format" in EXTENSIONS and len(strip_string_upper) == 1:
+            if "x-format" in EXTENSIONS() and len(strip_string_upper) == 1:
                 # The match just contains 'X' which is not valid
                 # fortran 2003 but is an accepted extension
                 return None, "X"
diff --git a/src/fparser/two/Fortran2008/connect_spec_r905.py b/src/fparser/two/Fortran2008/connect_spec_r905.py
index 0123be7..ae011cc 100644
--- a/src/fparser/two/Fortran2008/connect_spec_r905.py
+++ b/src/fparser/two/Fortran2008/connect_spec_r905.py
@@ -1,7 +1,7 @@
 # -----------------------------------------------------------------------------
 # BSD 3-Clause License
 #
-# Copyright (c) 2023, Science and Technology Facilities Council.
+# Copyright (c) 2023-2024, Science and Technology Facilities Council.
 # All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -48,7 +48,8 @@ from fparser.two.utils import KeywordValueBase, NoMatchError
 
 class Connect_Spec(Connect_Spec_2003):
     """
-    Fortran2008 rule R905.
+    Fortran2008 rule R905. Extends the Fortran2003 definition with support for
+    the NEWUNIT specifier.
 
     connect-spec is [ UNIT = ] file-unit-number
                      or ACCESS = scalar-default-char-expr
@@ -104,58 +105,15 @@ class Connect_Spec(Connect_Spec_2003):
         "Scalar_Int_Variable",
     ]
 
-    @staticmethod
-    def match(string):
+    @classmethod
+    def _keyword_value_list(cls):
         """
-        :param str string: Fortran code to check for a match
+        Extends the list of keywords supported in Fortran2003 with NEWUNIT.
 
-        :returns: 2-tuple containing the keyword and value or None if the
-                  supplied string is not a match
-        :rtype: Optional[Tuple[str, Any]]
-        """
-        # Avoid circular dependencies by importing here.
-        # pylint: disable=import-outside-toplevel
-        from fparser.two.Fortran2008 import (
-            Scalar_Default_Char_Expr,
-            Scalar_Int_Variable,
-            Scalar_Int_Expr,
-        )
+        :returns: list of keyword, class pairs to match against.
+        :rtype: list[tuple[str, type]]
 
-        if "=" not in string:
-            # The only argument which need not be named is the unit number
-            return "UNIT", File_Unit_Number(string)
-        # We have a keyword-value pair. Check whether it is valid...
-        for keyword, value in [
-            (
-                [
-                    "ACCESS",
-                    "ACTION",
-                    "ASYNCHRONOUS",
-                    "BLANK",
-                    "DECIMAL",
-                    "DELIM",
-                    "ENCODING",
-                    "FORM",
-                    "PAD",
-                    "POSITION",
-                    "ROUND",
-                    "SIGN",
-                    "STATUS",
-                ],
-                Scalar_Default_Char_Expr,
-            ),
-            ("ERR", Label),
-            ("FILE", File_Name_Expr),
-            ("IOSTAT", Scalar_Int_Variable),
-            ("IOMSG", Iomsg_Variable),
-            ("RECL", Scalar_Int_Expr),
-            ("UNIT", File_Unit_Number),
-            ("NEWUNIT", File_Unit_Number),
-        ]:
-            try:
-                obj = KeywordValueBase.match(keyword, value, string, upper_lhs=True)
-            except NoMatchError:
-                obj = None
-            if obj is not None:
-                return obj
-        return None
+        """
+        result = Connect_Spec_2003._keyword_value_list()
+        result.append(("NEWUNIT", File_Unit_Number))
+        return result
diff --git a/src/fparser/two/utils.py b/src/fparser/two/utils.py
index d9546d9..04c6500 100644
--- a/src/fparser/two/utils.py
+++ b/src/fparser/two/utils.py
@@ -1,4 +1,4 @@
-# Modified work Copyright (c) 2017-2023 Science and Technology
+# Modified work Copyright (c) 2017-2024 Science and Technology
 # Facilities Council.
 # Original work Copyright (c) 1999-2008 Pearu Peterson
 
@@ -84,7 +84,7 @@ from fparser.two.symbol_table import SYMBOL_TABLES
 # far forward from the current position. The 'x-format' extension
 # allows the X edit descriptor to be specified without a preceeding
 # integer.
-EXTENSIONS = ["x-format"]
+_EXTENSIONS = ["x-format"]
 
 # Cray pointers are a well known extension to the Fortran
 # standard. See http://pubs.cray.com/content/S-3901/8.6/
@@ -92,7 +92,7 @@ EXTENSIONS = ["x-format"]
 # https://gcc.gnu.org/onlinedocs/gfortran/Cray-pointers.html for
 # example. If 'cray-pointer' is specified in EXTENSIONS then this
 # extension is allowed in fparser.
-EXTENSIONS += ["cray-pointer"]
+_EXTENSIONS += ["cray-pointer"]
 
 # A Hollerith constant is a way of specifying a string as a sequence
 # of characters preceded by the string length and separated by an 'H'
@@ -101,13 +101,27 @@ EXTENSIONS += ["cray-pointer"]
 # for example for more details. fparser currently supports Hollerith
 # constants specified in format statements when 'hollerith' is specified
 # in the EXTENSIONS list.
-EXTENSIONS += ["hollerith"]
+_EXTENSIONS += ["hollerith"]
 
 # Many compilers support the use of '$' in a fortran write statement
 # to indicate that the carriage return should be suppressed. This is
 # an extension to the Fortran standard and is supported by fparser if
 # 'dollar-descriptor' is specified in the EXTENSIONS list.
-EXTENSIONS += ["dollar-descriptor"]
+_EXTENSIONS += ["dollar-descriptor"]
+
+# Many compilers support the optional 'CONVERT' argument to OPEN(). This is
+# used to indicate any endian-related conversion that must be performed
+# when reading/writing data using unformatted IO.
+_EXTENSIONS += ["open-convert"]
+
+
+def EXTENSIONS():
+    """
+    :returns: the list of extensions (to the Fortran standard) currently active.
+    :rtype: list[str]
+    """
+    return _EXTENSIONS
+
 
 # Set this to True to get verbose output (on stdout) detailing the matches made
 # while parsing.


Test patch:
diff --git a/src/fparser/two/tests/fortran2003/test_control_edit_descriptor_r1011.py b/src/fparser/two/tests/fortran2003/test_control_edit_descriptor_r1011.py
index 3af5cde..165d8bc 100644
--- a/src/fparser/two/tests/fortran2003/test_control_edit_descriptor_r1011.py
+++ b/src/fparser/two/tests/fortran2003/test_control_edit_descriptor_r1011.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2019 Science and Technology Facilities Council
+# Copyright (c) 2019-2024 Science and Technology Facilities Council.
 
 # All rights reserved.
 
@@ -35,6 +35,7 @@
 """Tests for a Fortran 2003 R1011 control edit descriptor."""
 
 import pytest
+from fparser.two import utils
 from fparser.two.Fortran2003 import Control_Edit_Desc
 from fparser.two.utils import NoMatchError, InternalError
 
@@ -80,8 +81,6 @@ def test_dollar_valid(f2003_create, monkeypatch):
     example with spaces.
 
     """
-    from fparser.two import utils
-
     monkeypatch.setattr(utils, "EXTENSIONS", ["dollar-descriptor"])
     for my_input in ["$", " $ "]:
         ast = Control_Edit_Desc(my_input)
@@ -93,9 +92,7 @@ def test_dollar_invalid(f2003_create, monkeypatch):
     the 'dollar-format' extension is not in the EXTENSIONS list.
 
     """
-    from fparser.two import utils
-
-    monkeypatch.setattr(utils, "EXTENSIONS", [])
+    monkeypatch.setattr(utils, "_EXTENSIONS", [])
     for my_input in ["$", " $ "]:
         with pytest.raises(NoMatchError):
             _ = Control_Edit_Desc(my_input)
diff --git a/src/fparser/two/tests/fortran2003/test_cray_pointer_stmt.py b/src/fparser/two/tests/fortran2003/test_cray_pointer_stmt.py
index d4544eb..d02fd06 100644
--- a/src/fparser/two/tests/fortran2003/test_cray_pointer_stmt.py
+++ b/src/fparser/two/tests/fortran2003/test_cray_pointer_stmt.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2019 Science and Technology Facilities Council
+# Copyright (c) 2019-2024 Science and Technology Facilities Council.
 
 # All rights reserved.
 
@@ -39,6 +39,7 @@ Cray-pointer statement.
 
 import pytest
 from fparser.api import get_reader
+from fparser.two import utils
 from fparser.two.Fortran2003 import Cray_Pointer_Stmt
 from fparser.two.utils import NoMatchError
 
@@ -105,13 +106,11 @@ def test_invalid_cray_pointer(f2003_create, monkeypatch):
     exception if it is not named as a valid extension.
 
     """
-    from fparser.two import utils
-
-    monkeypatch.setattr(utils, "EXTENSIONS", [])
+    monkeypatch.setattr(utils, "_EXTENSIONS", [])
     myinput = "pointer (mypointer, mypointee)"
     with pytest.raises(NoMatchError) as excinfo:
         _ = Cray_Pointer_Stmt(myinput)
-        assert "Cray_Pointer_Stmt: '{0}'".format(myinput) in str(excinfo.value)
+    assert "Cray_Pointer_Stmt: '{0}'".format(myinput) in str(excinfo.value)
 
 
 def test_valid_cray_pointer(f2003_create, monkeypatch):
@@ -119,9 +118,7 @@ def test_valid_cray_pointer(f2003_create, monkeypatch):
     expected output if it is named as a valid extension.
 
     """
-    from fparser.two import utils
-
-    monkeypatch.setattr(utils, "EXTENSIONS", ["cray-pointer"])
+    monkeypatch.setattr(utils, "_EXTENSIONS", ["cray-pointer"])
     myinput = "pointer(mypointer, mypointee)"
     result = Cray_Pointer_Stmt(myinput)
     assert str(result).lower() == myinput
diff --git a/src/fparser/two/tests/fortran2003/test_hollerith_item.py b/src/fparser/two/tests/fortran2003/test_hollerith_item.py
index 1d8d324..8319c1d 100644
--- a/src/fparser/two/tests/fortran2003/test_hollerith_item.py
+++ b/src/fparser/two/tests/fortran2003/test_hollerith_item.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2019-2022 Science and Technology Facilities Council
+# Copyright (c) 2019-2024 Science and Technology Facilities Council
 
 # All rights reserved.
 
@@ -129,7 +129,7 @@ def test_invalid_hollerith(f2003_create, monkeypatch):
     """
     from fparser.two import utils
 
-    monkeypatch.setattr(utils, "EXTENSIONS", [])
+    monkeypatch.setattr(utils, "_EXTENSIONS", [])
     myinput = "2Hab"
     with pytest.raises(NoMatchError) as excinfo:
         _ = Hollerith_Item(myinput)
diff --git a/src/fparser/two/tests/fortran2003/test_open_connect_spec_r905.py b/src/fparser/two/tests/fortran2003/test_open_connect_spec_r905.py
new file mode 100644
index 0000000..5555bfd
--- /dev/null
+++ b/src/fparser/two/tests/fortran2003/test_open_connect_spec_r905.py
@@ -0,0 +1,138 @@
+# Modified work Copyright (c) 2017-2024 Science and Technology
+# Facilities Council.
+# Original work Copyright (c) 1999-2008 Pearu Peterson
+#
+# All rights reserved.
+#
+# Modifications made as part of the fparser project are distributed
+# under the following license:
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# --------------------------------------------------------------------
+
+# The original software (in the f2py project) was distributed under
+# the following license:
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+
+#   a. Redistributions of source code must retain the above copyright notice,
+#      this list of conditions and the following disclaimer.
+#   b. Redistributions in binary form must reproduce the above copyright
+#      notice, this list of conditions and the following disclaimer in the
+#      documentation and/or other materials provided with the distribution.
+#   c. Neither the name of the F2PY project nor the names of its
+#      contributors may be used to endorse or promote products derived from
+#      this software without specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+"""
+Module containing py.test tests for the connect-spec of a Fortran2003 OPEN
+statement (R905).
+
+"""
+
+import pytest
+
+from fparser.two import Fortran2003, utils
+
+# from fparser.two.Fortran2003 import Open_Stmt
+
+
+@pytest.mark.parametrize(
+    "keyword",
+    [
+        "ACCESS",
+        "ACTION",
+        "ASYNCHRONOUS",
+        "BLANK",
+        "CONVERT",
+        "DECIMAL",
+        "DELIM",
+        "ENCODING",
+        "FORM",
+        "PAD",
+        "POSITION",
+        "ROUND",
+        "SIGN",
+        "STATUS",
+    ],
+)
+def test_connect_spec_char_keyword_args(keyword):
+    """Test that fparser2 supports the various keywords specified in R905
+    for the connect-spec that expect a scalar, character value. Note
+    that CONVERT is not actually part of the standard but is supported
+    by at least Gnu, Intel and Cray.
+
+    """
+    tcls = Fortran2003.Connect_Spec
+    obj = tcls(f"{keyword}='some-arg'")
+    assert isinstance(obj, tcls)
+    assert str(obj) == f"{keyword.upper()} = 'some-arg'"
+
+
+def test_connect_spec_non_char_expr():
+    """Test the support for all keywords which expect something other than
+    a char expression."""
+    tcls = Fortran2003.Connect_Spec
+    obj = tcls("ERR = 905")
+    assert isinstance(obj.items[1], Fortran2003.Label)
+    obj = tcls("FILE = 'a_file.txt'")
+    assert isinstance(obj.items[1], Fortran2003.Char_Literal_Constant)
+    obj = tcls("IOSTAT=ierr")
+    assert isinstance(obj.items[1], Fortran2003.Name)
+    obj = tcls("IOMSG=msg")
+    assert isinstance(obj.items[1], Fortran2003.Name)
+    obj = tcls("RECL=28")
+    assert isinstance(obj.items[1], Fortran2003.Int_Literal_Constant)
+    obj = tcls("UNIT=23")
+    assert isinstance(obj.items[1], Fortran2003.Int_Literal_Constant)
+    # Wrong type of argument.
+    with pytest.raises(utils.NoMatchError) as err:
+        _ = tcls("UNIT='Newtons'")
+    assert "Connect_Spec: 'UNIT" in str(err)
+
+
+def test_convert_support_disabled(monkeypatch):
+    """Test that the support for the CONVERT keyword may be disabled."""
+    monkeypatch.setattr(utils, "_EXTENSIONS", [])
+    with pytest.raises(utils.NoMatchError) as err:
+        _ = Fortran2003.Connect_Spec("CONVERT='big-endian'")
+    assert "Connect_Spec: 'CONVERT=" in str(err)
diff --git a/src/fparser/two/tests/fortran2003/test_openstmt_r904.py b/src/fparser/two/tests/fortran2003/test_openstmt_r904.py
new file mode 100644
index 0000000..914000f
--- /dev/null
+++ b/src/fparser/two/tests/fortran2003/test_openstmt_r904.py
@@ -0,0 +1,101 @@
+# Modified work Copyright (c) 2017-2024 Science and Technology
+# Facilities Council.
+# Original work Copyright (c) 1999-2008 Pearu Peterson
+#
+# All rights reserved.
+#
+# Modifications made as part of the fparser project are distributed
+# under the following license:
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# --------------------------------------------------------------------
+
+# The original software (in the f2py project) was distributed under
+# the following license:
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+
+#   a. Redistributions of source code must retain the above copyright notice,
+#      this list of conditions and the following disclaimer.
+#   b. Redistributions in binary form must reproduce the above copyright
+#      notice, this list of conditions and the following disclaimer in the
+#      documentation and/or other materials provided with the distribution.
+#   c. Neither the name of the F2PY project nor the names of its
+#      contributors may be used to endorse or promote products derived from
+#      this software without specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+
+"""
+Module containing py.test tests for the Fortran2003 OPEN statement (R904).
+
+"""
+
+import pytest
+
+from fparser.two.Fortran2003 import Open_Stmt
+from fparser.two.utils import NoMatchError
+
+
+def test_open_stmt():
+    """Tests that we correctly parse and re-generate the various forms
+    of OPEN statement (R904)."""
+    tcls = Open_Stmt
+    obj = tcls("open(23, file='some_file.txt')")
+    assert isinstance(obj, tcls)
+    assert str(obj) == "OPEN(UNIT = 23, FILE = 'some_file.txt')"
+    obj = tcls("open(unit=23, file='some_file.txt')")
+    assert isinstance(obj, tcls)
+    assert str(obj) == "OPEN(UNIT = 23, FILE = 'some_file.txt')"
+    obj = tcls("open(unit=23, file='some_file.txt', convert=endianness)")
+    assert isinstance(obj, tcls)
+    assert str(obj) == "OPEN(UNIT = 23, FILE = 'some_file.txt', CONVERT = endianness)"
+    obj = tcls(
+        "OPEN (993,FILE=FNAMETAB,form='UNFORMATTED',convert=file_endian,IOSTAT=IERR,STATUS='OLD')"
+    )
+    assert isinstance(obj, tcls)
+    assert (
+        str(obj)
+        == "OPEN(UNIT = 993, FILE = FNAMETAB, FORM = 'UNFORMATTED', CONVERT = file_endian, IOSTAT = IERR, STATUS = 'OLD')"
+    )
+    # Incorrect spelling of OPEN.
+    with pytest.raises(NoMatchError) as err:
+        tcls("opn(23, file='yada')")
+    assert "Open_Stmt: 'opn(23," in str(err)
diff --git a/src/fparser/two/tests/fortran2003/test_position_edit_desc_r1013.py b/src/fparser/two/tests/fortran2003/test_position_edit_desc_r1013.py
index 27d2eca..36e963b 100644
--- a/src/fparser/two/tests/fortran2003/test_position_edit_desc_r1013.py
+++ b/src/fparser/two/tests/fortran2003/test_position_edit_desc_r1013.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2018-2022 Science and Technology Facilities Council
+# Copyright (c) 2018-2024 Science and Technology Facilities Council
 
 # All rights reserved.
 
@@ -38,6 +38,8 @@ Position_Edit_Desc class.
 """
 
 import pytest
+
+import fparser.two.utils as utils
 from fparser.two.utils import NoMatchError, InternalError
 from fparser.two.Fortran2003 import Position_Edit_Desc
 
@@ -91,10 +93,7 @@ def test_invalid_x_descriptor1(f2003_create, monkeypatch):
     is not named as a valid extension.
 
     """
-
-    import fparser.two.utils as utils
-
-    monkeypatch.setattr(utils, "EXTENSIONS", [])
+    monkeypatch.setattr(utils, "_EXTENSIONS", [])
     for descriptor in ["X", "  X  ", "x"]:
         with pytest.raises(NoMatchError) as excinfo:
             _ = Position_Edit_Desc(descriptor)
@@ -106,9 +105,6 @@ def test_valid_x_descriptor2(f2003_create, monkeypatch):
     output if it is named as a valid extension.
 
     """
-
-    import fparser.two.utils as utils
-
     monkeypatch.setattr(utils, "EXTENSIONS", ["x-format"])
     for descriptor in ["X", "  X  ", "x"]:
         result = Position_Edit_Desc(descriptor)
diff --git a/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py b/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py
index e928592..da682f0 100644
--- a/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py
+++ b/src/fparser/two/tests/fortran2008/test_open_stmt_r904.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2022-2023 Science and Technology Facilities Council.
+# Copyright (c) 2022-2024 Science and Technology Facilities Council.
 
 # All rights reserved.
 
@@ -41,6 +41,7 @@
                     or ACTION = scalar-default-char-expr
                     or ASYNCHRONOUS = scalar-default-char-expr
                     or BLANK = scalar-default-char-expr
+                    [ or CONVERT = scalar-default-char-expr ]
                     or DECIMAL = scalar-default-char-expr
                     or DELIM = scalar-default-char-expr
                     or ENCODING = scalar-default-char-expr
@@ -57,6 +58,9 @@
                     or SIGN = scalar-default-char-expr
                     or STATUS = scalar-default-char-expr
 
+Note that the CONVERT argument is non-standard but supported by several major
+compilers.
+
 R906 file-name-expr is scalar-default-char-expr
 R907 iomsg-variable is scalar-default-char-variable
 
@@ -85,6 +89,7 @@ from fparser.two.utils import NoMatchError, walk
         "23, file='hello', err=36",
         "23, file='hello', status='OLD'",
         "unit=23, file='hello', action='read'",
+        "unit=23, file='hello', convert=endianess, action='read'",
     ],
 )
 @pytest.mark.usefixtures("f2008_parser")
diff --git a/src/fparser/two/tests/test_fortran2003.py b/src/fparser/two/tests/test_fortran2003.py
index 0ee3529..1afe174 100644
--- a/src/fparser/two/tests/test_fortran2003.py
+++ b/src/fparser/two/tests/test_fortran2003.py
@@ -2380,18 +2380,6 @@ def test_inquire_spec_list():
     assert "Inquire_Spec_List: 'unit=23, afile=" in str(excinfo.value)
 
 
-def test_open_stmt():
-    """Tests that we correctly parse and re-generate the various forms
-    of OPEN statement (R904)."""
-    tcls = Open_Stmt
-    obj = tcls("open(23, file='some_file.txt')")
-    assert isinstance(obj, tcls)
-    assert str(obj) == "OPEN(UNIT = 23, FILE = 'some_file.txt')"
-    obj = tcls("open(unit=23, file='some_file.txt')")
-    assert isinstance(obj, tcls)
-    assert str(obj) == "OPEN(UNIT = 23, FILE = 'some_file.txt')"
-
-
 def test_connect_spec():
     """Tests for individual elements of Connect_Spec (R905)."""
     tcls = Connect_Spec