Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make requirements.txt for extensions optional #744

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/reference/extensions/django-framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ 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
``./<Rock name with - replaced by _>/<Rock name with - replaced by _>/manage.py``
Expand Down
5 changes: 3 additions & 2 deletions docs/reference/extensions/fastapi-framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ 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):

Expand Down
5 changes: 3 additions & 2 deletions docs/reference/extensions/flask-framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ 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``
Expand Down
22 changes: 1 addition & 21 deletions rockcraft/extensions/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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()
Expand Down
29 changes: 8 additions & 21 deletions rockcraft/extensions/gunicorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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",
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
16 changes: 1 addition & 15 deletions tests/unit/extensions/test_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,27 +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"
)
assert str(exc.value) == ("- missing ASGI entrypoint")


@pytest.mark.usefixtures("fastapi_extension")
Expand Down
36 changes: 0 additions & 36 deletions tests/unit/extensions/test_gunicorn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]",
"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": "[email protected]",
"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(
Expand Down Expand Up @@ -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."
)

Expand Down
Loading