Skip to content

Commit 2e0099f

Browse files
authored
fix: accept UTF-8 BOM in pyproject.toml (#905)
1 parent a58d5ea commit 2e0099f

5 files changed

Lines changed: 37 additions & 10 deletions

File tree

‎rsconnect/pyproject.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def parse_pyproject_python_requires(pyproject_file: pathlib.Path) -> typing.Opti
9797
9898
Returns None if the field is not found.
9999
"""
100-
content = pyproject_file.read_text()
100+
content = pyproject_file.read_text(encoding="utf-8-sig")
101101
pyproject = tomllib.loads(content)
102102

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

201201
tool = pyproject.get("tool")

‎rsconnect/subprocesses/inspect_environment.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ def pyproject_dependencies(dirname: str, pyproject_filename: str):
281281
raise EnvironmentException(f"pyproject.toml not found: {pyproject_filename}")
282282

283283
try:
284-
with open(pyproject_path, "r", encoding="utf-8") as f:
284+
with open(pyproject_path, "r", encoding="utf-8-sig") as f:
285285
pyproject = tomllib.loads(f.read())
286286
except Exception as exception:
287287
raise EnvironmentException(f"Error reading {pyproject_filename}: {exception}")

‎tests/test_environment.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ def test_uv_lock_export(tmp_path):
208208
assert env.package_manager == "uv"
209209

210210

211-
def test_pyproject_dependencies(tmp_path):
211+
@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
212+
def test_pyproject_dependencies(tmp_path, encoding):
212213
project_dir = tmp_path / "project"
213214
project_dir.mkdir()
214215
(project_dir / "pyproject.toml").write_text(
@@ -220,7 +221,7 @@ def test_pyproject_dependencies(tmp_path):
220221
" 'annotated-types>=0.7.0',\n"
221222
" 'rsconnect-python==1.0.0',\n"
222223
"]\n",
223-
encoding="utf-8",
224+
encoding=encoding,
224225
)
225226

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

238239

240+
@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
241+
def test_pyproject_dependencies_invalid_toml(tmp_path, encoding):
242+
(tmp_path / "pyproject.toml").write_text("[project\ndependencies =", encoding=encoding)
243+
244+
with pytest.raises(EnvironmentException, match="Error reading pyproject.toml"):
245+
detect_environment(str(tmp_path), requirements_file="pyproject.toml")
246+
247+
239248
@pytest.mark.parametrize(
240249
"dependencies",
241250
[

‎tests/test_pyproject.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,14 @@ def test_pyprojecttoml_python_requires(project_dir, expected):
9999
assert parse_pyproject_python_requires(pyproject_file) == expected
100100

101101

102+
@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
103+
def test_pyprojecttoml_python_requires_encoding(tmp_path, encoding):
104+
pyproject_file = tmp_path / "pyproject.toml"
105+
pyproject_file.write_text('[project]\nname = "café"\nrequires-python = ">=3.8"\n', encoding=encoding)
106+
107+
assert parse_pyproject_python_requires(pyproject_file) == ">=3.8"
108+
109+
102110
@pytest.mark.parametrize(
103111
"project_dir, expected",
104112
[

‎tests/test_write_manifest_pyproject.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,12 +352,19 @@ def test_write_manifest_pyproject_never_rewrites_explicit_requirements_source(
352352
pass
353353

354354

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

360-
result = runner.invoke(cli, ["write-manifest", "pyproject", str(project_dir)])
364+
args = ["write-manifest", command]
365+
if command == "api":
366+
args.extend(["-r", "pyproject.toml"])
367+
result = runner.invoke(cli, [*args, str(project_dir)])
361368

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

516523

517-
def test_write_manifest_pyproject_errors_on_malformed_pyproject(runner: CliRunner, project_dir: pathlib.Path):
518-
(project_dir / "pyproject.toml").write_text("[tool.rsconnect\napp_mode =")
524+
@pytest.mark.parametrize("encoding", ["utf-8", "utf-8-sig"])
525+
def test_write_manifest_pyproject_errors_on_malformed_pyproject(
526+
runner: CliRunner, project_dir: pathlib.Path, encoding: str
527+
):
528+
(project_dir / "pyproject.toml").write_text("[tool.rsconnect\napp_mode =", encoding=encoding)
519529
result = runner.invoke(cli, ["write-manifest", "pyproject", str(project_dir)])
520530
assert result.exit_code != 0
521531
assert "could not be parsed" in result.output

0 commit comments

Comments
 (0)