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
5 changes: 5 additions & 0 deletions docs/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Fewer required dependencies: opening the products needs neither `scipy` nor `zar
`xarray-sentinel` command: the commands and their arguments are the same, the help
and the errors are plain text.

### Bug fixes

- Join the paths inside a product with `/` on Windows too: opening zipped products
failed with a `FileNotFoundError`, as did products on other non local file systems.

## v0.9.7 (2026-11-04)

The first release with the EOPF DataTree, in beta, the `xarray-sentinel` command line
Expand Down
4 changes: 2 additions & 2 deletions tests/test_20_sentinel1.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@
def test_get_fs_path() -> None:
fs, path = sentinel1.get_fs_path(str(SLC_IW))

assert path == str((SLC_IW / "manifest.safe").absolute())
assert path == (SLC_IW / "manifest.safe").absolute().as_posix()

fs, path = sentinel1.get_fs_path(SLC_IW, fs)

assert path == str(SLC_IW / "manifest.safe")
assert path == f"{SLC_IW}/manifest.safe"

with pytest.raises(TypeError):
sentinel1.get_fs_path("*", fs=fs, storage_options={})
Expand Down
2 changes: 1 addition & 1 deletion tests/test_30_sentinel1_fsspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_get_fs_path() -> None:
fs, path = sentinel1.get_fs_path(SLC_IW)

assert isinstance(fs, fsspec.AbstractFileSystem)
assert path == str(SLC_IW / "manifest.safe")
assert path == (SLC_IW / "manifest.safe").as_posix()

fs2, path2 = sentinel1.get_fs_path(path, fs=fs)

Expand Down
24 changes: 17 additions & 7 deletions tests/test_30_xarray_backends.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pathlib
import sys

import pytest
import xarray as xr
Expand Down Expand Up @@ -62,9 +63,12 @@ def test_open_dataset_root() -> None:
assert attr_name in res.attrs
assert res.attrs[attr_name] == COMMON_ATTRIBUTES[attr_name]

res = xr.open_dataset(product_path)
# xarray<=2026.09.0 can't guess the engine of a directory on Windows,
# fixed in https://github.com/pydata/xarray/pull/11802
if sys.platform != "win32":
res = xr.open_dataset(product_path)

assert isinstance(res, xr.Dataset)
assert isinstance(res, xr.Dataset)

product_path = product_path / "manifest.safe"

Expand All @@ -77,7 +81,7 @@ def test_open_dataset_root() -> None:
assert isinstance(res, xr.Dataset)

with pytest.raises((ValueError, FileNotFoundError)):
xr.open_dataset("")
xr.open_dataset("non-existing-file")


def test_open_dataset_wv() -> None:
Expand Down Expand Up @@ -262,9 +266,12 @@ def test_open_datatree() -> None:
assert "stac_discovery" in res[f"/{node}"].attrs
assert res.attrs["other_metadata"]["eopf_category"] == "eocontainer"

res = xr.open_datatree(product_path, check_files_exist=True)
# xarray<=2026.09.0 can't guess the engine of a directory on Windows,
# fixed in https://github.com/pydata/xarray/pull/11802
if sys.platform != "win32":
res = xr.open_datatree(product_path, check_files_exist=True)

assert isinstance(res, xr.DataTree)
assert isinstance(res, xr.DataTree)


def test_open_groups() -> None:
Expand All @@ -281,6 +288,9 @@ def test_open_groups() -> None:
assert isinstance(res[f"/{node}/measurements"], xr.Dataset)
assert "polarization" in res[f"/{node}/measurements"].sizes

res = xr.open_groups(product_path, check_files_exist=True)
# xarray<=2026.09.0 can't guess the engine of a directory on Windows,
# fixed in https://github.com/pydata/xarray/pull/11802
if sys.platform != "win32":
res = xr.open_groups(product_path, check_files_exist=True)

assert isinstance(res, dict)
assert isinstance(res, dict)
8 changes: 4 additions & 4 deletions xarray_sentinel/eopf_product.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import datetime
import io
import os
import posixpath
import re
import warnings
from collections.abc import Callable
Expand Down Expand Up @@ -178,10 +178,10 @@ def open_manifest(
) -> tuple[fsspec.AbstractFileSystem, str, io.BytesIO, str]:
"""Return the filesystem, the product path, the manifest and the product name."""
fs, manifest_path = sentinel1.get_fs_path(product_urlpath, fs, storage_options)
product_path = os.path.dirname(manifest_path)
product_path = posixpath.dirname(manifest_path)
manifest = io.BytesIO(fs.cat_file(manifest_path))
common_attrs, _ = esa_safe.parse_manifest_sentinel1(manifest)
safe_name, _ = os.path.splitext(os.path.basename(product_path))
safe_name, _ = posixpath.splitext(posixpath.basename(product_path))
product_name = make_product_name(safe_name, common_attrs)
return fs, product_path, manifest, product_name

Expand Down Expand Up @@ -246,7 +246,7 @@ def open_datatree(
swath, _, pol = group.partition("/")
swath_pols.setdefault(swath, []).append(pol)

safe_name = os.path.basename(product_path)
safe_name = posixpath.basename(product_path)
processing_history = eopf_metadata.build_processing_history(manifest, safe_name)
# NOTE: as the entry CPM 3.0 adds on conversion, but drops on the S01 product nodes
processing_history["Level-1 Product"].append(
Expand Down
5 changes: 3 additions & 2 deletions xarray_sentinel/esa_safe.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import functools
import importlib.resources
import os
import posixpath
import re
from collections.abc import Mapping
from typing import IO, Any
Expand Down Expand Up @@ -121,7 +122,7 @@ def findall(

def parse_annotation_filename(name: str) -> tuple[str, str, str, str]:
match = re.match(
r"([a-z-]*)s1[abcd]-([^-]*)-[^-]*-([^-]*)-([\dt]*)-", os.path.basename(name)
r"([a-z-]*)s1[abcd]-([^-]*)-[^-]*-([^-]*)-([\dt]*)-", posixpath.basename(name)
)
if match is None:
raise ValueError(f"cannot parse name {name!r}")
Expand Down Expand Up @@ -192,7 +193,7 @@ def parse_manifest_sentinel1(
if location_tag is not None:
file_href = location_tag.attrib["href"]
try:
description = parse_annotation_filename(os.path.basename(file_href))
description = parse_annotation_filename(posixpath.basename(file_href))
except ValueError:
continue
file_type = file_tag.attrib["repID"]
Expand Down
12 changes: 6 additions & 6 deletions xarray_sentinel/sentinel1.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from __future__ import annotations

import os
import posixpath
import warnings
from collections.abc import Sequence
from typing import Any, TypeVar
Expand Down Expand Up @@ -54,7 +54,7 @@ def get_fs_path(
path = str(urlpath_or_path)

if fs.isdir(path):
path = os.path.join(path, "manifest.safe")
path = posixpath.join(path, "manifest.safe")

return fs, path

Expand Down Expand Up @@ -512,7 +512,7 @@ def find_available_groups(
groups: dict[str, list[str]] = {}
for path, (type, _, swath, polarization, _) in product_files.items():
swath_pol_group = f"{swath}/{polarization}".upper()
abspath = os.path.join(product_path, os.path.normpath(path))
abspath = posixpath.join(product_path, posixpath.normpath(path))
if check_files_exist:
if not fs.exists(abspath):
continue
Expand Down Expand Up @@ -992,8 +992,8 @@ def do_override_product_files(
overridden_product_files = {}
for path, description in product_files.items():
type, prefix, swath, polarization, date = description
ext = os.path.splitext(path)[1]
dirname = os.path.dirname(path)
ext = posixpath.splitext(path)[1]
dirname = posixpath.dirname(path)
overridden_path = template.format(**locals())
overridden_product_files[overridden_path] = description
return overridden_product_files
Expand All @@ -1015,7 +1015,7 @@ def open_sentinel1_dataset(
warnings.warn("'drop_variables' is currently ignored")

fs, manifest_path = get_fs_path(product_urlpath, fs, storage_options)
product_path = os.path.dirname(manifest_path)
product_path = posixpath.dirname(manifest_path)

with fs.open(manifest_path) as file:
common_attrs, product_files = esa_safe.parse_manifest_sentinel1(file)
Expand Down
Loading