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
67 changes: 67 additions & 0 deletions src/openjd/sessions/_path_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,73 @@ def _ascii_lower(value: str) -> str:
where the value can be named."""


def to_host_path_separators(value: str) -> str:
Comment thread
leongdl marked this conversation as resolved.
"""Render ``value`` with this host's path separators.

A session is host scope, so a ``path`` there takes the host operating
system's semantics. openjd-rs applies that to every host-scope path value by
constructing it through ``ExprValue::new_path(.., PathFormat::host())``,
which calls ``normalize_path_separators``
(``crates/openjd-expr/src/value.rs``). This is that function for the host's
format, and the three cases are its three arms:

- a URI keeps forward slashes on every host, because its path portion is a
set of opaque identifiers rather than a filesystem path;
- a POSIX host changes nothing, because a backslash is a legal character in
a POSIX filename and rewriting one would corrupt the path;
- a Windows host replaces ``/`` with ``\\``.

Separators and nothing else. Rendering through ``PureWindowsPath`` would
also collapse ``//`` to ``\\``, drop a trailing separator, and turn ``""``
into ``"."`` -- and :meth:`PathMappingRule.apply` deliberately preserves a
trailing separator, so that one is a behaviour this must not undo.

Duplicated from Rust rather than called through ``openjd.expr``: the native
extension must not become a load-time requirement of a non-EXPR session (see
``test/openjd/test_import_purity.py``), and a PATH parameter reaches this on
the non-EXPR path. The URI test reuses :data:`_URI_SOURCE_RE`, which is this
module's existing spelling of the same ``<scheme>://`` rule, so the
duplication is of Rust's three-way branch only.

Note that a one-character scheme is a URI to both implementations, so
``C://Users/foo`` keeps its forward slashes while ``C:/Users/foo`` does not.
That looks like a drive-letter misclassification and is deliberate: RFC 3986
§3.1 admits a single-character scheme, ``openjd-rs``'s ``uri_path::is_uri``
accepts one, and the Expression Language's stated pattern
(``^[a-zA-Z][a-zA-Z0-9+.-]*://``) does too. Measured against the engine, the
two agree on this input and on ``x://y/z``. Tightening the regex here would
make this package diverge from the oracle it is being aligned with, so if the
behaviour is wrong it is wrong in the specification.
"""
# The three branches below are a transcription of openjd-expr's
# `normalize_path_separators`, in `crates/openjd-expr/src/value.rs`. Kept in
# the same order as the original so the two can be diffed by eye. Quoted here
# from openjd-expr 0.6.0; `openjd-model`'s `rust-bindings` links 0.5.0, and
# this function and the `uri_path::parse` behind its `is_uri` are byte
# identical in both (checked against the two crates.io sources, not assumed):
#
# pub fn normalize_path_separators(value: &str, format: PathFormat) -> String {
# if crate::uri_path::is_uri(value) {
# return value.to_string();
# }
# match format {
# PathFormat::Windows => value.replace('/', "\\"),
# PathFormat::Posix | PathFormat::Uri => value.to_string(),
# }
# }
#
# `format` is always the host's here, so the `Uri` arm of that `match` is
# unreachable from this caller and is folded into the POSIX one. If you change
# anything below, check it against that function first -- the whole reason this
# is a copy rather than a call is in the docstring, and a silent divergence is
# the cost that buys.
if _URI_SOURCE_RE.match(value) is not None:
Comment thread
leongdl marked this conversation as resolved.
return value
if os_name == "posix":
return value
return value.replace("/", "\\")


class PathFormat(str, Enum):
POSIX = "POSIX"
WINDOWS = "WINDOWS"
Expand Down
25 changes: 22 additions & 3 deletions src/openjd/sessions/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from ._embedded_files import EmbeddedFiles, EmbeddedFilesScope, _FileRecord, write_file_for_user
from ._logging import LOG, log_section_banner, LoggerAdapter, LogExtraInfo, LogContent
from ._os_checker import is_posix, is_windows
from ._path_mapping import PathMappingRule
from ._path_mapping import PathMappingRule, to_host_path_separators
from ._runner_base import (
ScriptRunnerBase,
apply_let_bindings,
Expand Down Expand Up @@ -1670,12 +1670,31 @@ def apply_mapping(path: str) -> str:
return path

def processed_parameter_value(param: ParameterValue) -> Any:
# The host format is applied to every PATH value, not only to a
# mapped one. `apply_mapping` chooses the host's separator for a
# rule's output, so before this it was the *only* thing that did:
# a value no rule matched reached the task in whatever form the
# submitter wrote it, and on a Windows host a POSIX-spelled
# parameter stayed POSIX-spelled. openjd-rs wraps both the mapped
# and the unmapped case in `ExprValue::new_path(.., host())`.
#
# For a POSIX- or WINDOWS-format rule this is idempotent, because
# `apply` rebuilds the result through `PureWindowsPath` and leaves no
# forward slash to replace. For a URI-format rule it is not:
# `_apply_uri` copies `destination_path` verbatim and uses the host
# separator only for the appended child parts, so a POSIX-spelled
# destination on a Windows host yields a mixed result
# ("/tmp/openjd\\scene\\out") that this then completes
# ("\\tmp\\openjd\\scene\\out"). That is the openjd-rs answer for the
# same input -- measured -- so completing it is the point rather than
# a side effect. `TestPathParameterFormatAndPathMappingAgree` pins
# both cases.
if param.type == ParameterValueType.PATH:
return apply_mapping(param.value)
return to_host_path_separators(apply_mapping(param.value))
Comment thread
leongdl marked this conversation as resolved.
if param.type == ParameterValueType.LIST_PATH and isinstance(param.value, list):
# openjd-rs maps each element of a LIST[PATH] parameter at
# session scope; mirror that element-wise mapping.
return [apply_mapping(p) for p in param.value]
return [to_host_path_separators(apply_mapping(p)) for p in param.value]
return param.value

def record_expr_types(
Expand Down
Loading
Loading