Skip to content

Commit

Permalink
feat: make requirements.txt for extensions optional
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
toabctl committed Nov 1, 2024
1 parent 5d77628 commit 1092c92
Show file tree
Hide file tree
Showing 7 changed files with 15 additions and 96 deletions.
4 changes: 2 additions & 2 deletions docs/reference/extensions/django-framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
``./<Rock name with - replaced by _>/<Rock name with - replaced by _>/manage.py``
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/extensions/fastapi-framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down
4 changes: 2 additions & 2 deletions docs/reference/extensions/flask-framework.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
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()
12 changes: 0 additions & 12 deletions tests/unit/extensions/test_fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

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

0 comments on commit 1092c92

Please sign in to comment.