diff --git a/rsconnect/pyproject.py b/rsconnect/pyproject.py index d0b591d7..ddf77fec 100644 --- a/rsconnect/pyproject.py +++ b/rsconnect/pyproject.py @@ -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) @@ -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") diff --git a/rsconnect/subprocesses/inspect_environment.py b/rsconnect/subprocesses/inspect_environment.py index db4e13fa..b24bf851 100644 --- a/rsconnect/subprocesses/inspect_environment.py +++ b/rsconnect/subprocesses/inspect_environment.py @@ -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}") diff --git a/tests/test_environment.py b/tests/test_environment.py index db75070e..ff841b64 100644 --- a/tests/test_environment.py +++ b/tests/test_environment.py @@ -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( @@ -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") @@ -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", [ diff --git a/tests/test_pyproject.py b/tests/test_pyproject.py index 2863de0c..a43bda96 100644 --- a/tests/test_pyproject.py +++ b/tests/test_pyproject.py @@ -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", [ diff --git a/tests/test_write_manifest_pyproject.py b/tests/test_write_manifest_pyproject.py index d809f3ab..e51d0391 100644 --- a/tests/test_write_manifest_pyproject.py +++ b/tests/test_write_manifest_pyproject.py @@ -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) @@ -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