Skip to content

Commit 4d6b2ea

Browse files
committed
enforce requirements.txt existing
1 parent ee209aa commit 4d6b2ea

File tree

4 files changed

+72
-16
lines changed

4 files changed

+72
-16
lines changed

rsconnect/environment.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,13 @@ def create_python_environment(
139139
module_file = app_file
140140

141141
_warn_on_ignored_manifest(directory)
142-
_warn_if_no_requirements_file(directory, requirements_file)
143142
_warn_if_environment_directory(directory)
144143

145144
python_version_requirement = pyproject.detect_python_version_requirement(directory)
146145
_warn_on_missing_python_version(python_version_requirement)
147146

147+
_check_requirements_file(directory, requirements_file)
148+
148149
if python is not None:
149150
# TODO: Remove the option in a future release
150151
logger.warning(
@@ -317,9 +318,9 @@ def _warn_on_ignored_manifest(directory: str) -> None:
317318
)
318319

319320

320-
def _warn_if_no_requirements_file(directory: str, requirements_file: typing.Optional[str]) -> None:
321+
def _check_requirements_file(directory: str, requirements_file: typing.Optional[str]) -> None:
321322
"""
322-
Check that a requirements file exists, and that it lives inside the deployment directory.
323+
Verify that a requirements file exists inside the deployment directory.
323324
324325
:param directory: the directory to check in.
325326
:param requirements_file: the name of the requirements file, or None to skip the check.
@@ -336,10 +337,10 @@ def _warn_if_no_requirements_file(directory: str, requirements_file: typing.Opti
336337
)
337338

338339
if not requirements_file_path.exists():
339-
click.secho(
340-
" Warning: Capturing the environment using 'pip freeze'.\n"
341-
" Consider creating a %s file instead." % requirements_file,
342-
fg="yellow",
340+
raise RSConnectException(
341+
"The requirements file '%s' does not exist in '%s'.\n"
342+
"Please create the file or specify a different file with --requirements-file.\n"
343+
"To use pip freeze instead, pass --force-generate." % (requirements_file, directory)
343344
)
344345

345346

rsconnect/subprocesses/inspect_environment.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ def detect_environment(dirname: str, requirements_file: Optional[str] = "require
7878
elif os.path.basename(requirements_file) == "uv.lock":
7979
result = uv_export(dirname, requirements_file)
8080
else:
81-
result = output_file(dirname, requirements_file, "pip") or pip_freeze()
81+
result = output_file(dirname, requirements_file, "pip")
82+
if result is None:
83+
raise EnvironmentException(
84+
"The requirements file '%s' was not found in '%s'. "
85+
"Please create it or use --force-generate to use pip freeze." % (requirements_file, dirname)
86+
)
8287

8388
if result is not None:
8489
result["python"] = get_python_version()

tests/test_bundle.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def test_make_notebook_source_bundle2(self):
154154
# the test environment. Don't do this in the production code, which
155155
# runs in the notebook server. We need the introspection to run in
156156
# the kernel environment and not the notebook server environment.
157-
environment = Environment.create_python_environment(directory)
157+
environment = Environment.create_python_environment(directory, requirements_file=None)
158158

159159
with make_notebook_source_bundle(
160160
nb_path,
@@ -314,7 +314,7 @@ def test_make_quarto_source_bundle_from_simple_project(self):
314314
# input file.
315315
create_fake_quarto_rendered_output(temp_proj, "myquarto")
316316

317-
environment = Environment.create_python_environment(temp_proj)
317+
environment = Environment.create_python_environment(temp_proj, requirements_file=None)
318318

319319
# mock the result of running of `quarto inspect <project_dir>`
320320
inspect = {
@@ -411,7 +411,7 @@ def test_make_quarto_source_bundle_from_complex_project(self):
411411
create_fake_quarto_rendered_output(site_dir, "index")
412412
create_fake_quarto_rendered_output(site_dir, "about")
413413

414-
environment = Environment.create_python_environment(temp_proj)
414+
environment = Environment.create_python_environment(temp_proj, requirements_file=None)
415415

416416
# mock the result of running of `quarto inspect <project_dir>`
417417
inspect = {

tests/test_environment.py

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from rsconnect.exception import RSConnectException
1212
from rsconnect.environment import Environment, which_python
1313
from rsconnect.subprocesses.inspect_environment import (
14+
EnvironmentException,
1415
detect_environment,
1516
filter_pip_freeze_output,
1617
get_default_locale,
@@ -83,7 +84,7 @@ def test_requirements_override(self):
8384
assert result.source == "file"
8485

8586
def test_pip_freeze(self):
86-
result = Environment.create_python_environment(get_dir("pip2"))
87+
result = Environment.create_python_environment(get_dir("pip2"), requirements_file=None)
8788

8889
# these are the dependencies declared in our pyproject.toml
8990
self.assertIn("six", result.contents)
@@ -223,22 +224,30 @@ def test_is_not_executable(self):
223224

224225
class TestPythonVersionRequirements:
225226
def test_pyproject_toml(self):
226-
env = Environment.create_python_environment(os.path.join(TESTDATA, "python-project", "using_pyproject"))
227+
env = Environment.create_python_environment(
228+
os.path.join(TESTDATA, "python-project", "using_pyproject"), requirements_file=None
229+
)
227230
assert env.python_interpreter == sys.executable
228231
assert env.python_version_requirement == ">=3.8"
229232

230233
def test_python_version(self):
231-
env = Environment.create_python_environment(os.path.join(TESTDATA, "python-project", "using_pyversion"))
234+
env = Environment.create_python_environment(
235+
os.path.join(TESTDATA, "python-project", "using_pyversion"), requirements_file=None
236+
)
232237
assert env.python_interpreter == sys.executable
233238
assert env.python_version_requirement == ">=3.8,<3.12"
234239

235240
def test_all_of_them(self):
236-
env = Environment.create_python_environment(os.path.join(TESTDATA, "python-project", "allofthem"))
241+
env = Environment.create_python_environment(
242+
os.path.join(TESTDATA, "python-project", "allofthem"), requirements_file=None
243+
)
237244
assert env.python_interpreter == sys.executable
238245
assert env.python_version_requirement == ">=3.8,<3.12"
239246

240247
def test_missing(self):
241-
env = Environment.create_python_environment(os.path.join(TESTDATA, "python-project", "empty"))
248+
env = Environment.create_python_environment(
249+
os.path.join(TESTDATA, "python-project", "empty"), requirements_file=None
250+
)
242251
assert env.python_interpreter == sys.executable
243252
assert env.python_version_requirement is None
244253

@@ -393,3 +402,44 @@ def test_python_interpreter(self):
393402
"Please use a .python-version file to force a specific interpreter version."
394403
)
395404
assert result.python == current_python_version
405+
406+
407+
class TestRequirementsFileRequired:
408+
"""When a requirements file is requested (the default), it must exist.
409+
When force-generate is used (requirements_file=None), pip freeze is used regardless."""
410+
411+
def test_requirements_requested_and_file_exists(self):
412+
"""Default behavior with requirements.txt present succeeds and reads the file."""
413+
result = Environment.create_python_environment(get_dir("pip1"))
414+
assert result.source == "file"
415+
assert result.filename == "requirements.txt"
416+
417+
def test_requirements_requested_and_file_missing(self):
418+
"""Default behavior without requirements.txt raises an error."""
419+
with pytest.raises(RSConnectException, match="does not exist"):
420+
Environment.create_python_environment(get_dir("pip2"))
421+
422+
def test_force_generate_and_file_exists(self):
423+
"""Force-generate ignores the existing requirements.txt and uses pip freeze."""
424+
result = Environment.create_python_environment(get_dir("pip1"), requirements_file=None)
425+
assert result.source == "pip_freeze"
426+
427+
def test_force_generate_and_file_missing(self):
428+
"""Force-generate works even without requirements.txt."""
429+
result = Environment.create_python_environment(get_dir("pip2"), requirements_file=None)
430+
assert result.source == "pip_freeze"
431+
432+
def test_detect_environment_requires_file(self):
433+
"""Subprocess-level: detect_environment errors when the file is missing."""
434+
with pytest.raises(EnvironmentException, match="was not found"):
435+
detect_environment(get_dir("pip2"), requirements_file="requirements.txt")
436+
437+
def test_detect_environment_reads_file(self):
438+
"""Subprocess-level: detect_environment reads the file when present."""
439+
result = detect_environment(get_dir("pip1"), requirements_file="requirements.txt")
440+
assert result.source == "file"
441+
442+
def test_detect_environment_force_generate(self):
443+
"""Subprocess-level: detect_environment uses pip freeze when requirements_file=None."""
444+
result = detect_environment(get_dir("pip2"), requirements_file=None)
445+
assert result.source == "pip_freeze"

0 commit comments

Comments
 (0)