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
4 changes: 2 additions & 2 deletions rsconnect/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def parse_pyproject_python_requires(pyproject_file: pathlib.Path) -> typing.Opti

Returns None if the field is not found.
"""
content = pyproject_file.read_text()
content = pyproject_file.read_text(encoding="utf-8-sig")
pyproject = tomllib.loads(content)

return pyproject.get("project", {}).get("requires-python", None)
Expand Down Expand Up @@ -195,7 +195,7 @@ def read_tool_rsconnect(pyproject_file: pathlib.Path) -> typing.Mapping[str, typ
missing or when required ``app_mode`` / ``entrypoint`` fields are absent or
not non-empty strings.
"""
content = pyproject_file.read_text()
content = pyproject_file.read_text(encoding="utf-8-sig")
pyproject = tomllib.loads(content)

tool = pyproject.get("tool")
Expand Down
2 changes: 1 addition & 1 deletion rsconnect/subprocesses/inspect_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ def pyproject_dependencies(dirname: str, pyproject_filename: str):
raise EnvironmentException(f"pyproject.toml not found: {pyproject_filename}")

try:
with open(pyproject_path, "r", encoding="utf-8") as f:
with open(pyproject_path, "r", encoding="utf-8-sig") as f:
pyproject = tomllib.loads(f.read())
except Exception as exception:
raise EnvironmentException(f"Error reading {pyproject_filename}: {exception}")
Expand Down
13 changes: 11 additions & 2 deletions tests/test_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ def test_uv_lock_export(tmp_path):
assert env.package_manager == "uv"


def test_pyproject_dependencies(tmp_path):
@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
def test_pyproject_dependencies(tmp_path, encoding):
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "pyproject.toml").write_text(
Expand All @@ -220,7 +221,7 @@ def test_pyproject_dependencies(tmp_path):
" 'annotated-types>=0.7.0',\n"
" 'rsconnect-python==1.0.0',\n"
"]\n",
encoding="utf-8",
encoding=encoding,
)

env = detect_environment(str(project_dir), requirements_file="pyproject.toml")
Expand All @@ -236,6 +237,14 @@ def test_pyproject_dependencies(tmp_path):
assert env.package_manager == "pip"


@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
def test_pyproject_dependencies_invalid_toml(tmp_path, encoding):
(tmp_path / "pyproject.toml").write_text("[project\ndependencies =", encoding=encoding)

with pytest.raises(EnvironmentException, match="Error reading pyproject.toml"):
detect_environment(str(tmp_path), requirements_file="pyproject.toml")


@pytest.mark.parametrize(
"dependencies",
[
Expand Down
8 changes: 8 additions & 0 deletions tests/test_pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ def test_pyprojecttoml_python_requires(project_dir, expected):
assert parse_pyproject_python_requires(pyproject_file) == expected


@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
def test_pyprojecttoml_python_requires_encoding(tmp_path, encoding):
pyproject_file = tmp_path / "pyproject.toml"
pyproject_file.write_text('[project]\nname = "café"\nrequires-python = ">=3.8"\n', encoding=encoding)

assert parse_pyproject_python_requires(pyproject_file) == ">=3.8"


@pytest.mark.parametrize(
"project_dir, expected",
[
Expand Down
20 changes: 15 additions & 5 deletions tests/test_write_manifest_pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,12 +352,19 @@ def test_write_manifest_pyproject_never_rewrites_explicit_requirements_source(
pass


def test_write_manifest_pyproject_unmocked_end_to_end(runner: CliRunner, project_dir: pathlib.Path):
@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
@pytest.mark.parametrize("command", ["pyproject", "api"])
def test_write_manifest_pyproject_unmocked_end_to_end(
runner: CliRunner, project_dir: pathlib.Path, encoding: str, command: str
):
"""Real environment inspection: default config must yield a deployable manifest."""
_write_pyproject(project_dir, _REQ_PYPROJECT)
(project_dir / "pyproject.toml").write_text(textwrap.dedent(_REQ_PYPROJECT), encoding=encoding)
(project_dir / "app.py").write_text("app = None\n")

result = runner.invoke(cli, ["write-manifest", "pyproject", str(project_dir)])
args = ["write-manifest", command]
if command == "api":
args.extend(["-r", "pyproject.toml"])
result = runner.invoke(cli, [*args, str(project_dir)])

assert result.exit_code == 0, result.output
manifest = _read_manifest(project_dir)
Expand Down Expand Up @@ -514,8 +521,11 @@ def test_write_manifest_pyproject_errors_on_missing_pyproject(runner: CliRunner,
assert "quickstart" in result.output.lower()


def test_write_manifest_pyproject_errors_on_malformed_pyproject(runner: CliRunner, project_dir: pathlib.Path):
(project_dir / "pyproject.toml").write_text("[tool.rsconnect\napp_mode =")
@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
def test_write_manifest_pyproject_errors_on_malformed_pyproject(
runner: CliRunner, project_dir: pathlib.Path, encoding: str
):
(project_dir / "pyproject.toml").write_text("[tool.rsconnect\napp_mode =", encoding=encoding)
result = runner.invoke(cli, ["write-manifest", "pyproject", str(project_dir)])
assert result.exit_code != 0
assert "could not be parsed" in result.output
Expand Down
Loading