Repository: canonical/operator

Issue:
FileNotFoundError: '.../requirements.txt' during `charmcraft pack` with 2.1.0
Since ops 2.1.0 released, I started getting this error when I `charmcraft pack`:
```
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 :: Running external command ['/root/parts/charm/build/staging-venv/bin/pip3', 'install', '--upgrade', '--no-binary', ':all:', '--requirement=requir
ements.txt']
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    :: Collecting ops>=1.4.0
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::   Using cached ops-2.1.0.tar.gz (228 kB)
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::   Installing build dependencies: started
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::   Installing build dependencies: finished with status 'done'
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::   Getting requirements to build wheel: started
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::   Getting requirements to build wheel: finished with status 'error'
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::   error: subprocess-exited-with-error
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::   × Getting requirements to build wheel did not run successfully.
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::   │ exit code: 1
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::   ╰─> [18 lines of output]
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::       Traceback (most recent call last):
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::         File "/root/parts/charm/build/staging-venv/lib/python3.10/site-packages/pip/_vendor/pep517/in_process/_in_process.py", line 363, i
n <module>
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::           main()
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::         File "/root/parts/charm/build/staging-venv/lib/python3.10/site-packages/pip/_vendor/pep517/in_process/_in_process.py", line 345, i
n main
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::           json_out['return_val'] = hook(**hook_input['kwargs'])
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::         File "/root/parts/charm/build/staging-venv/lib/python3.10/site-packages/pip/_vendor/pep517/in_process/_in_process.py", line 130, i
n get_requires_for_build_wheel
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::           return hook(config_settings)
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::         File "/tmp/pip-build-env-hbo3tdnh/overlay/lib/python3.10/site-packages/setuptools/build_meta.py", line 338, in get_requires_for_bu
ild_wheel
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::           return self._get_build_requires(config_settings, requirements=['wheel'])
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::         File "/tmp/pip-build-env-hbo3tdnh/overlay/lib/python3.10/site-packages/setuptools/build_meta.py", line 320, in _get_build_requires
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::           self.run_setup()
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::         File "/tmp/pip-build-env-hbo3tdnh/overlay/lib/python3.10/site-packages/setuptools/build_meta.py", line 484, in run_setup
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::           super(_BuildMetaLegacyBackend,
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::         File "/tmp/pip-build-env-hbo3tdnh/overlay/lib/python3.10/site-packages/setuptools/build_meta.py", line 335, in run_setup
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::           exec(code, locals())
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::         File "<string>", line 90, in <module>
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::         File "<string>", line 34, in _requirements
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::       FileNotFoundError: [Errno 2] No such file or directory: '/tmp/pip-install-pmyuzv7y/ops_cc674be2568d44cfb1452b7731e01ff8/requirements
.txt'
2023-02-28 15:57:44.361 :: 2023-02-28 23:57:44.064 ::    ::       [end of output]
```

Was able to get myself unblocked by pinning to 2.0, though.

Discussion:

Patch:
diff --git a/setup.py b/setup.py
index 7efdfc5..2810a4d 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,6 @@
 
 from importlib.util import module_from_spec, spec_from_file_location
 from pathlib import Path
-from typing import List
 
 from setuptools import find_packages, setup
 
@@ -28,19 +27,6 @@ def _read_me() -> str:
     return readme
 
 
-def _requirements() -> List[str]:
-    """Return the required packages to run the project."""
-    reqs = []
-    with open(Path(__file__).parent / 'requirements.txt', encoding='utf-8') as fh:
-        for line in fh.readlines():
-            # TODO(tinvaan): DRY, consider setuptools offering for requirements parsing
-            # https://setuptools.pypa.io/en/latest/pkg_resources.html#requirements-parsing
-            line = line.strip()
-            if line and not line.startswith("#"):
-                reqs.append(line)
-    return reqs
-
-
 def _get_version() -> str:
     """Get the version via ops/version.py, without loading ops/__init__.py."""
     spec = spec_from_file_location('ops.version', 'ops/version.py')
@@ -87,7 +73,10 @@ version = {version!r}
             "Operating System :: POSIX :: Linux",
         ],
         python_requires='>=3.8',
-        install_requires=_requirements(),
+        install_requires=[  # must stay in sync with requirements.txt (see test_install_requires)
+            'PyYAML==6.*',
+            'websocket-client==1.*',
+        ],
         package_data={'ops': ['py.typed']},
     )
 


Test patch:
diff --git a/test/test_infra.py b/test/test_infra.py
index 09f2d59..be9e0da 100644
--- a/test/test_infra.py
+++ b/test/test_infra.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import ast
 import itertools
 import os
 import re
@@ -92,6 +93,26 @@ class InfrastructureTests(unittest.TestCase):
     def test_check(self):
         self._run_setup('check', '--strict')
 
+    def test_install_requires(self):
+        """Ensure that requirements.txt stays in sync with install_requires in setup.py."""
+        with open('requirements.txt', encoding='utf-8') as f:
+            requirements = [line.strip() for line in f
+                            if line.strip() and not line.startswith('#')]
+
+        # For some reason "setup.py --requires" doesn't work, so do this the hard way
+        with open('setup.py', encoding='utf-8') as f:
+            lines = []
+            for line in f:
+                if 'install_requires=[' in line:
+                    break
+            for line in f:
+                if line.strip() == '],':
+                    break
+                lines.append(line)
+            install_requires = ast.literal_eval('[' + '\n'.join(lines) + ']')
+
+        self.assertEqual(requirements, install_requires)
+
 
 class ImportersTestCase(unittest.TestCase):