Build and test with PY2.7

PiperOrigin-RevId: 810827595
diff --git a/.gitattributes b/.gitattributes
index bbe7293..de14a00 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -51,3 +51,4 @@
 tests/testdata/empty.compressed !export-ignore
 tests/testdata/ukkonooa !export-ignore
 tests/testdata/ukkonooa.compressed !export-ignore
+tests/testdata/zerosukkanooa.compressed !export-ignore
diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml
index 81c5ecb..330579f 100644
--- a/.github/workflows/build_test.yml
+++ b/.github/workflows/build_test.yml
@@ -18,7 +18,7 @@
   cancel-in-progress: ${{ github.event_name == 'pull_request' }}
 
 jobs:
-  ubuntu_build:
+  build_test:
     name: Build and test ${{ matrix.name }}
     runs-on: ${{ matrix.os || 'ubuntu-latest' }}
     defaults:
@@ -188,13 +188,6 @@
         submodules: false
         fetch-depth: 1
 
-    #- name: Checkout VC9 for Python
-    #  if: ${{ runner.os == 'Windows' && matrix.build_system == 'python' &&  matrix.python_version == '2.7' }}
-    #  uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
-    #  with:
-    #    repository: reider-roque/sulley-win-installer
-    #    path: third_party/VCForPython27
-
     - name: Configure / Build / Test with CMake
       if: ${{ matrix.build_system == 'cmake' }}
       run: |
@@ -294,17 +287,36 @@
       with:
         python-version: ${{ matrix.python_version }}
 
-    # TODO: investigate, why msiexec hangs
-    #- name: Install VC9 for Python
-    #  if: ${{ runner.os == 'Windows' && matrix.build_system == 'python' &&  matrix.python_version == '2.7' }}
-    #  run: |
-    #    echo "070474db76a2e625513a5835df4595df9324d820f9cc97eab2a596dcbc2f5cbf  third_party/VCForPython27/VCForPython27.msi" | sha256sum --check --status
-    #    msiexec ALLUSERS=1 /qn /norestart /i third_party/VCForPython27/VCForPython27.msi /l*v ${RUNNER_TEMP}/msiexec.log
-    #    cat ${RUNNER_TEMP}/msiexec.log
-
     - name: Build / Test with Python
       if: ${{ matrix.build_system == 'python' }}
       run: |
         python -VV
         python -c "import sys; sys.exit('Invalid python version') if '.'.join(map(str,sys.version_info[0:2])) != '${{ matrix.python_version }}' else True"
         python setup.py ${{ matrix.py_setuptools_cmd || 'test'}}
+
+  build_test_py27:
+    name: Build and test with Python 2.7
+    runs-on: ubuntu-latest
+    container:
+      image: ubuntu:22.04
+    steps:
+
+    - name: Install deps
+      run: |
+        apt update
+        apt install -y curl gcc python2.7 python2.7-dev
+        curl https://bootstrap.pypa.io/pip/2.7/get-pip.py --output get-pip.py
+        python2.7 get-pip.py
+        python2.7 -m pip install distutils-pytest==0.1
+
+    - name: Checkout the source
+      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+      with:
+        submodules: false
+        fetch-depth: 1
+
+    - name: Build / Test
+      run: |
+        python2.7 -VV
+        python2.7 -c "import sys; sys.exit('Invalid python version') if '.'.join(map(str,sys.version_info[0:2])) != '2.7' else True"
+        python2.7 setup.py test
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 74e08df..1dfac68 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -255,13 +255,16 @@
     tests/testdata/*.compressed*)
 
   foreach(INPUT ${COMPATIBILITY_INPUTS})
-    add_test(NAME "${BROTLI_TEST_PREFIX}compatibility/${INPUT}"
-      COMMAND "${CMAKE_COMMAND}"
-        -DBROTLI_WRAPPER=${BROTLI_WRAPPER}
-        -DBROTLI_WRAPPER_LD_PREFIX=${BROTLI_WRAPPER_LD_PREFIX}
-        -DBROTLI_CLI=$<TARGET_FILE:brotli>
-        -DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/${INPUT}
-        -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/run-compatibility-test.cmake)
+    string(REGEX REPLACE "([a-zA-Z0-9\\.]+)\\.compressed(\\.[0-9]+)?$" "\\1" UNCOMPRESSED_INPUT "${INPUT}")
+    if (EXISTS ${UNCOMPRESSED_INPUT})
+      add_test(NAME "${BROTLI_TEST_PREFIX}compatibility/${INPUT}"
+        COMMAND "${CMAKE_COMMAND}"
+          -DBROTLI_WRAPPER=${BROTLI_WRAPPER}
+          -DBROTLI_WRAPPER_LD_PREFIX=${BROTLI_WRAPPER_LD_PREFIX}
+          -DBROTLI_CLI=$<TARGET_FILE:brotli>
+          -DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/${INPUT}
+          -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/run-compatibility-test.cmake)
+    endif()
   endforeach()
 endif()  # BROTLI_DISABLE_TESTS
 
diff --git a/python/_brotli.c b/python/_brotli.c
index 137ec68..1f9150e 100644
--- a/python/_brotli.c
+++ b/python/_brotli.c
@@ -854,18 +854,6 @@
      brotli_decompress__doc__},
     {NULL, NULL, 0, NULL}};
 
-static struct PyModuleDef brotli_module = {
-    PyModuleDef_HEAD_INIT,
-    "_brotli",      /* m_name */
-    brotli_doc,     /* m_doc */
-    0,              /* m_size */
-    brotli_methods, /* m_methods */
-    NULL,           /* m_reload */
-    NULL,           /* m_traverse */
-    NULL,           /* m_clear */
-    NULL            /* m_free */
-};
-
 static PyMethodDef brotli_Compressor_methods[] = {
     {"process", (PyCFunction)brotli_Compressor_process, METH_VARARGS,
      brotli_Compressor_process_doc},
@@ -889,6 +877,18 @@
 
 #if PY_MAJOR_VERSION >= 3
 
+static struct PyModuleDef brotli_module = {
+    PyModuleDef_HEAD_INIT,
+    "_brotli",      /* m_name */
+    brotli_doc,     /* m_doc */
+    0,              /* m_size */
+    brotli_methods, /* m_methods */
+    NULL,           /* m_reload */
+    NULL,           /* m_traverse */
+    NULL,           /* m_clear */
+    NULL            /* m_free */
+};
+
 static PyType_Slot brotli_Compressor_slots[] = {
     {Py_tp_dealloc, (destructor)brotli_Compressor_dealloc},
     {Py_tp_doc, (void*)brotli_Compressor_doc},
diff --git a/python/tests/_test_utils.py b/python/tests/_test_utils.py
index 713738a..36102b8 100644
--- a/python/tests/_test_utils.py
+++ b/python/tests/_test_utils.py
@@ -97,7 +97,10 @@
       generate a separate test method.
   """
   if for_decompression:
-    paths = TESTDATA_PATHS_FOR_DECOMPRESSION
+    paths = [
+        path for path in TESTDATA_PATHS_FOR_DECOMPRESSION
+        if os.path.exists(path.replace('.compressed', ''))
+    ]
   else:
     paths = TESTDATA_PATHS
   opts = []
@@ -141,7 +144,7 @@
         os.unlink(get_temp_uncompressed_name(f))
       except OSError:
         pass
-    super().tearDown()
+    # super().tearDown()  # Requires Py3+
 
   def assert_files_match(self, first, second):
     self.assertTrue(
diff --git a/python/tests/compressor_test.py b/python/tests/compressor_test.py
index 2c08f8f..8f92405 100644
--- a/python/tests/compressor_test.py
+++ b/python/tests/compressor_test.py
@@ -20,7 +20,7 @@
 
   def tearDown(self):
     self.compressor = None
-    super().tearDown()
+    # super().tearDown()  # Requires Py3+
 
   def _check_decompression(self, test_data):
     # Write decompression to temp file and verify it matches the original.
@@ -71,28 +71,28 @@
 class TestCompressorQuality1(_TestCompressor, _test_utils.TestCase):
 
   def setUp(self):
-    super().setUp()
+    # super().setUp()  # Requires Py3+
     self.compressor = brotli.Compressor(quality=1)
 
 
 class TestCompressorQuality6(_TestCompressor, _test_utils.TestCase):
 
   def setUp(self):
-    super().setUp()
+    # super().setUp()  # Requires Py3+
     self.compressor = brotli.Compressor(quality=6)
 
 
 class TestCompressorQuality9(_TestCompressor, _test_utils.TestCase):
 
   def setUp(self):
-    super().setUp()
+    # super().setUp()  # Requires Py3+
     self.compressor = brotli.Compressor(quality=9)
 
 
 class TestCompressorQuality11(_TestCompressor, _test_utils.TestCase):
 
   def setUp(self):
-    super().setUp()
+    # super().setUp()  # Requires Py3+
     self.compressor = brotli.Compressor(quality=11)
 
 
diff --git a/python/tests/decompressor_test.py b/python/tests/decompressor_test.py
index 362c682..67844b3 100644
--- a/python/tests/decompressor_test.py
+++ b/python/tests/decompressor_test.py
@@ -22,12 +22,12 @@
   MIN_OUTPUT_BUFFER_SIZE = 32768  # Actually, several bytes less.
 
   def setUp(self):
-    super().setUp()
+    # super().setUp()  # Requires Py3+
     self.decompressor = brotli.Decompressor()
 
   def tearDown(self):
     self.decompressor = None
-    super().tearDown()
+    # super().tearDown()  # Requires Py3+
 
   def _check_decompression(self, test_data):
     # Verify decompression matches the original.
@@ -86,6 +86,7 @@
     test_data = os.path.join(
         _test_utils.TESTDATA_DIR, 'zerosukkanooa.compressed'
     )
+    check_output = os.path.exists(test_data.replace('.compressed', ''))
     temp_uncompressed = _test_utils.get_temp_uncompressed_name(test_data)
     with open(temp_uncompressed, 'wb') as out_file:
       with open(test_data, 'rb') as in_file:
@@ -98,7 +99,8 @@
         while not self.decompressor.can_accept_more_data():
           out_file.write(self.decompressor.process(b''))
         out_file.write(self.decompressor.process(compressed[-1:]))
-    self._check_decompression(test_data)
+    if check_output:
+      self._check_decompression(test_data)
 
   def test_garbage_appended(self):
     with self.assertRaises(brotli.error):
diff --git a/setup.py b/setup.py
index c44d8c5..88ca7c2 100644
--- a/setup.py
+++ b/setup.py
@@ -24,7 +24,7 @@
 CURR_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
 
 
-def bool_from_environ(key: str):
+def bool_from_environ(key):
     value = os.environ.get(key)
     if not value:
         return False
@@ -32,7 +32,7 @@
        return True
     if value == "0":
        return False
-    raise ValueError(f"Environment variable {key} has invalid value {value}. Please set it to 1, 0 or an empty string")
+    raise ValueError("Environment variable {} has invalid value {}. Please set it to 1, 0 or an empty string".format(key, value))
 
 
 def read_define(path, macro):