Skip to content

Combine step exporters with storage interface#1182

Open
dwhswenson wants to merge 5 commits into
openpathsampling:masterfrom
dwhswenson:traj-export-via-storage-interface
Open

Combine step exporters with storage interface#1182
dwhswenson wants to merge 5 commits into
openpathsampling:masterfrom
dwhswenson:traj-export-via-storage-interface

Conversation

@dwhswenson

Copy link
Copy Markdown
Member

This should, in principle, make it so that we're no longer requiring that exports go to files. This will help us add flexibility in the future to export to non-filesystem setups (e.g., S3 or similar).

Assisted-by: Codex.app:GPT-5.5

This should, in principle, make it so that we're no longer requiring
that exports go to files.

Assisted-by: Codex.app:GPT-5.5

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors trajectory/step exporting to target a generic storage interface (not just filesystem paths), enabling exports to alternate backends (e.g., in-memory or object stores) while keeping compatibility with path-based writer libraries via staging.

Changes:

  • Extend StorageInterface with force= overwrite semantics plus store_bytes/load_bytes, open, and as_path helpers for byte and staged-path workflows.
  • Update TrajectoryWriter to support writing to a StorageInterface (and keep write_file for direct filesystem output).
  • Introduce a StepExporter base class and update SymLinkStepExporter to inherit it, plus add tests for new storage + base_dir behaviors.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
openpathsampling/utils/storage_interfaces.py Adds byte-based and staged-path APIs; implements/extends local + memory storage backends.
openpathsampling/exports/trajectories/core.py Adds storage-aware TrajectoryWriter.write and refactors file writing through LocalFileStorageInterface.
openpathsampling/exports/steps/core.py New abstract StepExporter interface and default export_step orchestration.
openpathsampling/exports/steps/symlink_step_exporter.py Makes symlink exporter a StepExporter and adds base_dir path resolution.
openpathsampling/tests/utils/test_storage_interfaces.py Expands coverage for force, bytes APIs, open, and as_path (including atomic behavior).
openpathsampling/tests/exports/trajectories/test_trrtrajectorywriter.py Adds storage-interface export test coverage for TRR writer.
openpathsampling/tests/exports/trajectories/test_mdtrajtrajectorywriter.py Adds storage-interface export test coverage for MDTraj writer.
openpathsampling/tests/exports/trajectories/test_core.py Adds storage-interface export tests for core trajectory writer behavior.
openpathsampling/tests/exports/steps/test_symlink_step_exporter.py Adds tests for StepExporter inheritance and base_dir behavior without chdir.
openpathsampling/tests/exports/steps/test_step_exporter_core.py New unit test for StepExporter.export_step orchestration ordering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +178 to +182
def _local_path(self, storage_label):
storage_label = pathlib.Path(storage_label)
if storage_label.is_absolute():
raise ValueError("Storage labels must be relative paths")
return self.root / storage_label

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 17ad9ff

Comment on lines 199 to 200
_logger.debug("Copying file from {str(local_path)} "
f"to {str(target_path)}")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 96f12d8

Comment on lines 209 to 210
@@ -105,17 +210,88 @@ def delete(self, storage_label):
os.remove(obj)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 96f12d8

Comment on lines +220 to +225
self._check_can_write(storage_label, force)
target_path = self._local_path(storage_label)
target_path.parent.mkdir(parents=True, exist_ok=True)
if force and target_path.exists():
os.remove(target_path)
shutil.move(source_path, target_path)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 17ad9ff

if 'b' not in mode:
raise ValueError("StorageInterface.open only supports binary "
"modes")
if _mode_writes(mode):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b62127a

Comment on lines +133 to +144
suffix = suffix or pathlib.Path(storage_label).suffix
fd, tmp_path = tempfile.mkstemp(suffix=suffix)
os.close(fd)
tmp_path = pathlib.Path(tmp_path)
write_mode = _mode_writes(mode)
try:
if write_mode:
if not force and storage_label in self:
raise FileExistsError(f"Storage label {storage_label} "
"already exists")
else:
self.load(storage_label, tmp_path)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in b62127a

Comment on lines 169 to 172
# ensure parent directory exists
pathlib.Path(raw_data_path).parent.mkdir(parents=True, exist_ok=True)
raw_data_path.parent.mkdir(parents=True, exist_ok=True)
writer = self._get_writer(sample)
writer(sample.trajectory, raw_data_path)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 96f12d8

Ensure that paths are actually inside the root; avoid anchored paths
(storage labels should not be absolute).

Assisted-by: Codex.app:GPT-5.4
Assisted-by: Codex.app:GPT-5.4
This should make it easier for downstream writers to use this.

Assisted-by: Codex.app:GPT-5.4
@codecov

codecov Bot commented May 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.23853% with 30 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.57%. Comparing base (644f3ed) to head (fe04e39).

Files with missing lines Patch % Lines
openpathsampling/utils/storage_interfaces.py 85.27% 24 Missing ⚠️
openpathsampling/exports/trajectories/core.py 76.19% 5 Missing ⚠️
...athsampling/exports/steps/symlink_step_exporter.py 95.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1182      +/-   ##
==========================================
+ Coverage   81.55%   81.57%   +0.01%     
==========================================
  Files         148      149       +1     
  Lines       15915    16093     +178     
==========================================
+ Hits        12980    13128     +148     
- Misses       2935     2965      +30     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants