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
16 changes: 9 additions & 7 deletions rsconnect/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,13 @@ def create_python_environment(
module_file = app_file

_warn_on_ignored_manifest(directory)
_warn_if_no_requirements_file(directory, requirements_file)
_warn_if_environment_directory(directory)

python_version_requirement = pyproject.detect_python_version_requirement(directory)
_warn_on_missing_python_version(python_version_requirement)

_check_requirements_file(directory, requirements_file)

if python is not None:
# TODO: Remove the option in a future release
logger.warning(
Expand Down Expand Up @@ -317,9 +318,9 @@ def _warn_on_ignored_manifest(directory: str) -> None:
)


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

:param directory: the directory to check in.
:param requirements_file: the name of the requirements file, or None to skip the check.
Expand All @@ -336,10 +337,11 @@ def _warn_if_no_requirements_file(directory: str, requirements_file: typing.Opti
)

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


Expand Down
8 changes: 7 additions & 1 deletion rsconnect/subprocesses/inspect_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,17 @@ def detect_environment(dirname: str, requirements_file: Optional[str] = "require
"""

if requirements_file is None:
# --force-generate sets requirements_file to None
result = pip_freeze()
elif os.path.basename(requirements_file) == "uv.lock":
result = uv_export(dirname, requirements_file)
else:
result = output_file(dirname, requirements_file, "pip") or pip_freeze()
result = output_file(dirname, requirements_file, "pip")
if result is None:
raise EnvironmentException(
"The requirements file '%s' was not found in '%s'. "
"Please create it or use --force-generate to use pip freeze." % (requirements_file, dirname)
)

if result is not None:
result["python"] = get_python_version()
Expand Down
6 changes: 3 additions & 3 deletions tests/test_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ def test_make_notebook_source_bundle2(self):
# the test environment. Don't do this in the production code, which
# runs in the notebook server. We need the introspection to run in
# the kernel environment and not the notebook server environment.
environment = Environment.create_python_environment(directory)
environment = Environment.create_python_environment(directory, requirements_file=None)

with make_notebook_source_bundle(
nb_path,
Expand Down Expand Up @@ -314,7 +314,7 @@ def test_make_quarto_source_bundle_from_simple_project(self):
# input file.
create_fake_quarto_rendered_output(temp_proj, "myquarto")

environment = Environment.create_python_environment(temp_proj)
environment = Environment.create_python_environment(temp_proj, requirements_file=None)

# mock the result of running of `quarto inspect <project_dir>`
inspect = {
Expand Down Expand Up @@ -411,7 +411,7 @@ def test_make_quarto_source_bundle_from_complex_project(self):
create_fake_quarto_rendered_output(site_dir, "index")
create_fake_quarto_rendered_output(site_dir, "about")

environment = Environment.create_python_environment(temp_proj)
environment = Environment.create_python_environment(temp_proj, requirements_file=None)

# mock the result of running of `quarto inspect <project_dir>`
inspect = {
Expand Down
60 changes: 55 additions & 5 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from rsconnect.exception import RSConnectException
from rsconnect.environment import Environment, which_python
from rsconnect.subprocesses.inspect_environment import (
EnvironmentException,
detect_environment,
filter_pip_freeze_output,
get_default_locale,
Expand Down Expand Up @@ -83,7 +84,7 @@ def test_requirements_override(self):
assert result.source == "file"

def test_pip_freeze(self):
result = Environment.create_python_environment(get_dir("pip2"))
result = Environment.create_python_environment(get_dir("pip2"), requirements_file=None)

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

class TestPythonVersionRequirements:
def test_pyproject_toml(self):
env = Environment.create_python_environment(os.path.join(TESTDATA, "python-project", "using_pyproject"))
env = Environment.create_python_environment(
os.path.join(TESTDATA, "python-project", "using_pyproject"), requirements_file=None
)
assert env.python_interpreter == sys.executable
assert env.python_version_requirement == ">=3.8"

def test_python_version(self):
env = Environment.create_python_environment(os.path.join(TESTDATA, "python-project", "using_pyversion"))
env = Environment.create_python_environment(
os.path.join(TESTDATA, "python-project", "using_pyversion"), requirements_file=None
)
assert env.python_interpreter == sys.executable
assert env.python_version_requirement == ">=3.8,<3.12"

def test_all_of_them(self):
env = Environment.create_python_environment(os.path.join(TESTDATA, "python-project", "allofthem"))
env = Environment.create_python_environment(
os.path.join(TESTDATA, "python-project", "allofthem"), requirements_file=None
)
assert env.python_interpreter == sys.executable
assert env.python_version_requirement == ">=3.8,<3.12"

def test_missing(self):
env = Environment.create_python_environment(os.path.join(TESTDATA, "python-project", "empty"))
env = Environment.create_python_environment(
os.path.join(TESTDATA, "python-project", "empty"), requirements_file=None
)
assert env.python_interpreter == sys.executable
assert env.python_version_requirement is None

Expand Down Expand Up @@ -393,3 +402,44 @@ def test_python_interpreter(self):
"Please use a .python-version file to force a specific interpreter version."
)
assert result.python == current_python_version


class TestRequirementsFileRequired:
"""When a requirements file is requested (the default), it must exist.
When force-generate is used (requirements_file=None), pip freeze is used regardless."""

def test_requirements_requested_and_file_exists(self):
"""Default behavior with requirements.txt present succeeds and reads the file."""
result = Environment.create_python_environment(get_dir("pip1"))
assert result.source == "file"
assert result.filename == "requirements.txt"

def test_requirements_requested_and_file_missing(self):
"""Default behavior without requirements.txt raises an error."""
with pytest.raises(RSConnectException, match="does not exist"):
Environment.create_python_environment(get_dir("pip2"))

def test_force_generate_and_file_exists(self):
"""Force-generate ignores the existing requirements.txt and uses pip freeze."""
result = Environment.create_python_environment(get_dir("pip1"), requirements_file=None)
assert result.source == "pip_freeze"

def test_force_generate_and_file_missing(self):
"""Force-generate works even without requirements.txt."""
result = Environment.create_python_environment(get_dir("pip2"), requirements_file=None)
assert result.source == "pip_freeze"

def test_detect_environment_requires_file(self):
"""Subprocess-level: detect_environment errors when the file is missing."""
with pytest.raises(EnvironmentException, match="was not found"):
detect_environment(get_dir("pip2"), requirements_file="requirements.txt")

def test_detect_environment_reads_file(self):
"""Subprocess-level: detect_environment reads the file when present."""
result = detect_environment(get_dir("pip1"), requirements_file="requirements.txt")
assert result.source == "file"

def test_detect_environment_force_generate(self):
"""Subprocess-level: detect_environment uses pip freeze when requirements_file=None."""
result = detect_environment(get_dir("pip2"), requirements_file=None)
assert result.source == "pip_freeze"
Loading