Skip to content
Merged
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
20 changes: 11 additions & 9 deletions rsconnect/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def create_python_environment(
python_version_requirement = pyproject.detect_python_version_requirement(directory)
_warn_on_missing_python_version(python_version_requirement)

_check_requirements_file(directory, requirements_file)
requirements_file = _check_requirements_file(directory, requirements_file)

if python is not None:
# TODO: Remove the option in a future release
Expand Down Expand Up @@ -314,7 +314,7 @@ def _warn_on_ignored_manifest(directory: str) -> None:
warn_user(" Warning: the existing manifest.json file will not be used or considered.")


def _check_requirements_file(directory: str, requirements_file: typing.Optional[str]) -> None:
def _check_requirements_file(directory: str, requirements_file: typing.Optional[str]) -> typing.Optional[str]:
"""
Verify that a requirements file exists inside the deployment directory.

Expand All @@ -324,13 +324,14 @@ def _check_requirements_file(directory: str, requirements_file: typing.Optional[
if requirements_file is None:
return

directory_path = pathlib.Path(directory)
requirements_file_path = directory_path / pathlib.Path(requirements_file)
if directory_path not in requirements_file_path.parents:
warn_user(
" Warning: The requirements file '%s' is outside of the deployment directory.\n" % requirements_file,
fg="red",
)
directory_path = pathlib.Path(directory).resolve()
requirements_file_path = (directory_path / requirements_file).resolve()
try:
relative_path = requirements_file_path.relative_to(directory_path)
except ValueError:
raise RSConnectException(
"The requirements file '%s' is outside of the deployment directory '%s'." % (requirements_file, directory)
) from None

if not requirements_file_path.exists():
raise RSConnectException(
Expand All @@ -339,6 +340,7 @@ def _check_requirements_file(directory: str, requirements_file: typing.Optional[
"To have the requirements file generated using pip freeze, pass --force-generate."
% (requirements_file, directory)
)
return str(relative_path)


def _warn_if_environment_directory(directory: typing.Union[str, pathlib.Path]) -> None:
Expand Down
40 changes: 36 additions & 4 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2169,18 +2169,50 @@ def test_force_generate_requirements(self, tmp_path, existing_requirements):
assert requirements.read_text().startswith("# requirements.txt generated by rsconnect-python")
assert manifest["files"]["requirements.txt"]["checksum"] == file_checksum(requirements)

def test_explicit_requirements_file(self, tmp_path):
@pytest.mark.parametrize("path_kind", ["relative", "absolute", "normalized"])
def test_explicit_requirements_file(self, tmp_path, path_kind):
(tmp_path / "app.py").write_text("app = object()\n")
requirements = tmp_path / "custom-requirements.txt"
(tmp_path / "dependencies").mkdir()
requirements = tmp_path / "dependencies" / "custom-requirements.txt"
requirements.write_text("example-package==1.0\n")
relative_path = os.path.join("dependencies", requirements.name)
requirement_paths = {
"relative": relative_path,
"absolute": str(requirements),
"normalized": os.path.join("dependencies", "..", relative_path),
}

runner = CliRunner()
result = runner.invoke(cli, ["write-manifest", "api", "-r", requirements.name, str(tmp_path)])
result = runner.invoke(cli, ["write-manifest", "api", "-r", requirement_paths[path_kind], str(tmp_path)])
assert result.exit_code == 0, result.output

manifest = json.loads((tmp_path / "manifest.json").read_text())
assert requirements.read_text() == "example-package==1.0\n"
assert manifest["files"][requirements.name]["checksum"] == file_checksum(requirements)
assert manifest["python"]["package_manager"]["package_file"] == relative_path
assert manifest["files"][relative_path]["checksum"] == file_checksum(requirements)
assert set(manifest["files"]) == {"app.py", relative_path}
assert f"{relative_path} already exists and will not be overwritten" in result.output

@pytest.mark.parametrize("path_kind", ["absolute", "relative", "symlink"])
def test_requirements_file_outside_project_is_rejected(self, tmp_path, path_kind):
project = tmp_path / "project"
project.mkdir()
(project / "app.py").write_text("app = object()\n")
outside = tmp_path / "requirements.txt"
outside.write_text("example-package==1.0\n")
if path_kind == "symlink":
(project / "linked.txt").symlink_to(outside)
requirement_paths = {
"absolute": str(outside),
"relative": os.path.join("..", outside.name),
"symlink": "linked.txt",
}

result = CliRunner().invoke(cli, ["write-manifest", "api", "-r", requirement_paths[path_kind], str(project)])

assert result.exit_code != 0
assert "outside of the deployment directory" in result.output
assert not (project / "manifest.json").exists()


class TestWriteManifestExcludeRenv:
Expand Down
Loading