From 1092c92e478f25998a8116b1d2ab257bb211240d Mon Sep 17 00:00:00 2001 From: Thomas Bechtold Date: Fri, 1 Nov 2024 11:16:18 +0100 Subject: [PATCH] feat: make requirements.txt for extensions optional There are multiple ways within python to declare dependencies (eg. via pyproject.toml, setup.py or setup.cfg). Requiring a requirements.txt file in a specific path doesn't work for a lot of projects out there so make the requirements.txt file optional. --- .../reference/extensions/django-framework.rst | 4 +-- .../extensions/fastapi-framework.rst | 4 +-- docs/reference/extensions/flask-framework.rst | 4 +-- rockcraft/extensions/fastapi.py | 22 +----------- rockcraft/extensions/gunicorn.py | 29 +++++---------- tests/unit/extensions/test_fastapi.py | 12 ------- tests/unit/extensions/test_gunicorn.py | 36 ------------------- 7 files changed, 15 insertions(+), 96 deletions(-) diff --git a/docs/reference/extensions/django-framework.rst b/docs/reference/extensions/django-framework.rst index f16731f49..9ce84a1e7 100644 --- a/docs/reference/extensions/django-framework.rst +++ b/docs/reference/extensions/django-framework.rst @@ -22,8 +22,8 @@ Project requirements There are 2 requirements to be able to use the ``django-framework`` extension: -1. There must be a ``requirements.txt`` file in the root directory of the - project with ``Django`` declared as a dependency. +1. ``Django`` needs to be declared as a requirement either in a `requirements.txt` + file or within another `pip` supported requirement (eg. via `pyproject.toml`). 2. The project must be named the same as the ``name`` in ``rockcraft.yaml`` with any ``-`` replaced by ``_``, i.e., the ``manage.py`` must be located at ``.///manage.py`` diff --git a/docs/reference/extensions/fastapi-framework.rst b/docs/reference/extensions/fastapi-framework.rst index 1f06ee9e9..3bbb6c9f3 100644 --- a/docs/reference/extensions/fastapi-framework.rst +++ b/docs/reference/extensions/fastapi-framework.rst @@ -19,8 +19,8 @@ Project requirements There are 2 requirements to be able to use the ``fastapi-framework`` extension: -1. There must be a ``requirements.txt`` file in the root of the project with - ``fastapi`` declared as a dependency +1. ``fastapi`` needs to be declared as a requirement either in a `requirements.txt` + file or within another `pip` supported requirement (eg. via `pyproject.toml`). 2. The project must include a ASGI app in a variable called ``app`` in one of the following files relative to the project root (in order of priority): diff --git a/docs/reference/extensions/flask-framework.rst b/docs/reference/extensions/flask-framework.rst index 6902157b3..a07cd593e 100644 --- a/docs/reference/extensions/flask-framework.rst +++ b/docs/reference/extensions/flask-framework.rst @@ -21,8 +21,8 @@ Project requirements There are 2 requirements to be able to use the ``flask-framework`` extension: -1. There must be a ``requirements.txt`` file in the root of the project with - ``Flask`` declared as a dependency +1. ``Flask`` needs to be declared as a requirement either in a `requirements.txt` + file or within another `pip` supported requirement (eg. via `pyproject.toml`). 2. The project must include a WSGI app with the path ``app:app``. This means there must be an ``app.py`` file at the root of the project with the name of the Flask object is set to ``app`` diff --git a/rockcraft/extensions/fastapi.py b/rockcraft/extensions/fastapi.py index be7f9876a..2c2af541b 100644 --- a/rockcraft/extensions/fastapi.py +++ b/rockcraft/extensions/fastapi.py @@ -223,7 +223,7 @@ def _find_asgi_location(self) -> pathlib.Path: def _check_project(self) -> None: """Ensure this extension can apply to the current rockcraft project.""" - error_messages = self._requirements_txt_error_messages() + error_messages = [] if not self.yaml_data.get("services", {}).get("fastapi", {}).get("command"): error_messages += self._asgi_entrypoint_error_messages() if error_messages: @@ -233,26 +233,6 @@ def _check_project(self) -> None: logpath_report=False, ) - def _requirements_txt_error_messages(self) -> list[str]: - """Ensure the requirements.txt file exists and has fastapi or starlette deps.""" - requirements_file = self.project_root / "requirements.txt" - if not requirements_file.exists(): - return [ - "missing a requirements.txt file. The fastapi-framework extension requires this file with 'fastapi'/'starlette' specified as a dependency." - ] - - requirements_lines = requirements_file.read_text(encoding="utf-8").splitlines() - if not any( - dep in line.lower() - for line in requirements_lines - for dep in ("fastapi", "starlette") - ): - return [ - "missing fastapi or starlette package dependency in requirements.txt file." - ] - - return [] - def _asgi_entrypoint_error_messages(self) -> list[str]: try: self._find_asgi_location() diff --git a/rockcraft/extensions/gunicorn.py b/rockcraft/extensions/gunicorn.py index 890a6af29..3b3b27bab 100644 --- a/rockcraft/extensions/gunicorn.py +++ b/rockcraft/extensions/gunicorn.py @@ -77,7 +77,6 @@ def _gen_parts(self) -> dict: "stage-packages": stage_packages, "source": ".", "python-packages": ["gunicorn"], - "python-requirements": ["requirements.txt"], "build-environment": build_environment, }, f"{self.framework}-framework/install-app": self.gen_install_app_part(), @@ -95,6 +94,13 @@ def _gen_parts(self) -> dict: "source": "https://github.com/prometheus/statsd_exporter.git", }, } + + # add the optional requirements.txt file + if (self.project_root / "requirements.txt").exists(): + parts[f"{self.framework}-framework/dependencies"]["python-requirements"] = [ + "requirements.txt" + ] + if self.yaml_data["base"] == "bare": parts[f"{self.framework}-framework/runtime"] = { "plugin": "nil", @@ -259,24 +265,10 @@ def _wsgi_path_error_messages(self) -> list[str]: return [] - def _requirements_txt_error_messages(self) -> list[str]: - """Ensure the requirements.txt file is correct.""" - requirements_file = self.project_root / "requirements.txt" - if not requirements_file.exists(): - return [ - "missing a requirements.txt file. The flask-framework extension requires this file with 'flask' specified as a dependency." - ] - - requirements_lines = requirements_file.read_text(encoding="utf-8").splitlines() - if not any(("flask" in line.lower() for line in requirements_lines)): - return ["missing flask package dependency in requirements.txt file."] - - return [] - @override def check_project(self) -> None: """Ensure this extension can apply to the current rockcraft project.""" - error_messages = self._requirements_txt_error_messages() + error_messages = [] if not self.yaml_data.get("services", {}).get("flask", {}).get("command"): error_messages += self._wsgi_path_error_messages() if error_messages: @@ -346,10 +338,5 @@ def _check_wsgi_path(self) -> None: @override def check_project(self) -> None: """Ensure this extension can apply to the current rockcraft project.""" - if not (self.project_root / "requirements.txt").exists(): - raise ExtensionError( - "missing requirements.txt file, django-framework extension " - "requires this file with Django specified as a dependency" - ) if not self.yaml_data.get("services", {}).get("django", {}).get("command"): self._check_wsgi_path() diff --git a/tests/unit/extensions/test_fastapi.py b/tests/unit/extensions/test_fastapi.py index 2e1ec81f5..bcdbd1373 100644 --- a/tests/unit/extensions/test_fastapi.py +++ b/tests/unit/extensions/test_fastapi.py @@ -178,25 +178,13 @@ def test_fastapi_missing_asgi_entrypoint(tmp_path, fastapi_input_yaml): assert "- missing ASGI entrypoint" in str(exc.value) -@pytest.mark.usefixtures("fastapi_extension") -def test_fastapi_missing_requirements_txt(tmp_path, fastapi_input_yaml): - (tmp_path / "app.py").write_text("app = app") - with pytest.raises(ExtensionError) as exc: - extensions.apply_extensions(tmp_path, fastapi_input_yaml) - assert str(exc.value) == ( - "- missing a requirements.txt file. The fastapi-framework extension requires this file with 'fastapi'/'starlette' specified as a dependency." - ) - - @pytest.mark.usefixtures("fastapi_extension") def test_fastapi_check_no_correct_requirement_and_no_asgi_entrypoint( tmp_path, fastapi_input_yaml ): - (tmp_path / "requirements.txt").write_text("oneproject") with pytest.raises(ExtensionError) as exc: extensions.apply_extensions(tmp_path, fastapi_input_yaml) assert str(exc.value) == ( - "- missing fastapi or starlette package dependency in requirements.txt file.\n" "- missing ASGI entrypoint" ) diff --git a/tests/unit/extensions/test_gunicorn.py b/tests/unit/extensions/test_gunicorn.py index 8b7b545a1..e24562905 100644 --- a/tests/unit/extensions/test_gunicorn.py +++ b/tests/unit/extensions/test_gunicorn.py @@ -308,41 +308,6 @@ def test_flask_extension_bare(tmp_path): } -@pytest.mark.usefixtures("flask_extension") -def test_flask_extension_no_requirements_txt_error(tmp_path): - (tmp_path / "app.py").write_text("app = object()") - flask_input_yaml = { - "extensions": ["flask-framework"], - "base": "bare", - "build-base": "ubuntu@22.04", - "platforms": {"amd64": {}}, - } - with pytest.raises(ExtensionError) as exc: - extensions.apply_extensions(tmp_path, flask_input_yaml) - assert ( - str(exc.value) - == "- missing a requirements.txt file. The flask-framework extension requires this file with 'flask' specified as a dependency." - ) - - -@pytest.mark.usefixtures("flask_extension") -def test_flask_extension_requirements_txt_no_flask_error(tmp_path): - (tmp_path / "app.py").write_text("app = object()") - (tmp_path / "requirements.txt").write_text("") - flask_input_yaml = { - "extensions": ["flask-framework"], - "base": "bare", - "build-base": "ubuntu@22.04", - "platforms": {"amd64": {}}, - } - with pytest.raises(ExtensionError) as exc: - extensions.apply_extensions(tmp_path, flask_input_yaml) - - assert ( - str(exc.value) == "- missing flask package dependency in requirements.txt file." - ) - - @pytest.mark.usefixtures("flask_extension") def test_flask_extension_bad_app_py(tmp_path): bad_code = textwrap.dedent( @@ -382,7 +347,6 @@ def test_flask_extension_no_requirements_txt_no_app_py_error(tmp_path): with pytest.raises(ExtensionError) as exc: extensions.apply_extensions(tmp_path, flask_input_yaml) assert str(exc.value) == ( - "- missing a requirements.txt file. The flask-framework extension requires this file with 'flask' specified as a dependency.\n" "- flask application can not be imported from app:app, no app.py file found in the project root." )