Skip to content

refactor: build paths with pathlib instead of os.path - #153

Merged
cmulk merged 1 commit into
sandialabs:mainfrom
adamsrnmsu:refactor-pathlib
Oct 10, 2026
Merged

cmulk merged 1 commit into
sandialabs:mainfrom
adamsrnmsu:refactor-pathlib

Conversation

@adamsrnmsu

@adamsrnmsu adamsrnmsu commented Oct 1, 2026 •

Copy link
Copy Markdown

Description

A mechanical os.path → pathlib conversion, limited to files that need no other change: the otsim config writer, the logger file sink, the SCORCH base class, the caldera, hoststats, iperf, kafka listener, opcexport, snort, trafficgen and vmstats components, and the wind_turbine app.

The SCORCH base class now keeps root_dir, files_dir and base_dir as Path objects. Components join onto them directly and convert to str only where minimega or a file-transfer helper needs one. The two components that built paths by string concatenation on base_dir (cc recv and ssh SFTP downloads) now join with pathlib. Those lines are written identically to #154's versions, so the two PRs still merge cleanly.

No behavior change. common/utils.py, SunSpec loading and the files whose path handling also gains validation are converted in #154 instead, next to the validation, because safe_join() returns a Path. That keeps the PRs from overlapping.

Related Issues/PRs

One of four independent PRs: #152, #154, #155. Each is a single commit on main and they can merge in any order.

Type of Change

  • Refactor (refactor)

Checklist

  • This PR conforms to the process detailed in the Contributing Guide.
  • I have included no proprietary/sensitive information in my code or the PR.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have made corresponding changes to the documentation.
  • I have tested my code (describe below).

Testing

make check and make test in src/python on Python 3.12: 697 passed, the same as main. I also merged all five open PRs (#152–#156) together: make check is clean and 744 tests pass.

Additional Notes

The vendored sunspec/models/smdx/manifest.py, which ruff excludes, is deliberately left alone.

🤖 Generated with Claude Code

https://claude.ai/code/session_016KAfcDUSerQCCWwBM9BxAo

@adamsrnmsu

Copy link
Copy Markdown
Author

This one is dedicated to you @GhostofGoes

@adamsrnmsu
adamsrnmsu requested a review from glattercj October 1, 2026 15:20
Comment on lines +104 to +105
cmd_src = str(Path(self.root_dir) / self.exp_name / cmd_file)
cmd_dst = str(PurePath("/tmp/miniccc/files") / self.exp_name / cmd_file)

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.

Why not make these Path objects? And then convert to strings in the places they need to be strings? Right now they're becoming strings, then being converted to Paths in a bunch of places.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. cmd_src and cmd_dst stay Path/PurePath, and str() only happens at the minimega call.

dst=os.path.join(
self.base_dir, os.path.basename(client["client_log_path"])
dst=str(
Path(self.base_dir)

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.

can self.base_dir be made into a Path?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. self.base_dir is a Path in ComponentBase, so the wrappers are gone.

e_dir = Path(self.exp_dir) / "opcexport"
if not e_dir.exists():
e_dir.mkdir()
opc_variables_file = str(e_dir / "opc_variables.json")

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.

make it a Path object?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. e_dir and opc_variables_file are Paths, and exp_dir is now a Path too.

script = config_snort["script"]
executor = config_snort["executor"]

logger.info(f"copying {os.path.basename(script)} to {hostname}")

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.

the conversion once, store in a variable, instead of doing 4 times

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. script_name is computed once and reused.

script = scripts["trafficServer"]

logger.info(f"copying {os.path.basename(script)} to {hostname}")
logger.info(f"copying {PurePath(script).name} to {hostname}")

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.

use a variable

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. script_name is a variable here too.

self.metadata: Box | None = self.extract_metadata()

self.root_dir: str = os.path.join(PHENIX_DIR, "images")
self.root_dir: str = str(Path(PHENIX_DIR) / "images")

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.

Make this a Path object

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. root_dir, files_dir and base_dir are all Paths.

self.root_dir: str = os.path.join(PHENIX_DIR, "images")
self.root_dir: str = str(Path(PHENIX_DIR) / "images")
self.files_dir: str = os.getenv(
"PHENIX_FILES_DIR", os.path.join(self.root_dir, self.exp_name, "files")

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.

make all of these path objects yeah?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

exp_dir is a Path now as well. I also dropped the leftover Path(self.base_dir) wraps in cc and ssh.

@adamsrnmsu

Copy link
Copy Markdown
Author

@GhostofGoes Thanks, all addressed in 468a5f7:

  • ComponentBase.root_dir, files_dir and base_dir are now Path objects, so components join onto them directly and only convert to str where minimega or a transfer helper needs a string (mm_cc_send_wait, mm_send, mm_recv).
  • caldera keeps cmd_src / cmd_dst as paths, iperf and opcexport use Path throughout, and snort and trafficgen compute each script name once into a variable.
  • cc recv and ssh SFTP downloads did self.base_dir + "/...", which would break with a Path, so they join with pathlib now. Those lines match fix: validate hostnames and paths built from scenario metadata #154 exactly, so the PRs stay independent.

Still one commit, still independent of the others. All five open PRs merged together pass make check and 744 tests.

utils.mm_recv(mm, hostname, f"/var/log/snort/{log}", str(log_path))

if log == "snort.stats" and os.path.exists(f"{self.base_dir}/{log}"):
if log == "snort.stats" and log_path.exists():

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.

Use 'is_file()' for file existence checks

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done.

Mechanical os.path -> pathlib conversion for files that need no other change:
the otsim config writer, the logger file sink, the SCORCH base class and the
caldera, hoststats, iperf, kafka listener, opcexport, snort, trafficgen and
vmstats components, and the wind_turbine app.

The SCORCH base class now keeps root_dir, files_dir and base_dir as Path
objects, so components join onto them directly and convert to str only at
the minimega and file-transfer boundaries. The two components that built
paths by string concatenation on base_dir (cc recv and ssh SFTP downloads)
now join with pathlib instead.

No behavior change. common/utils.py and the files whose path handling also
gains validation are converted in the hardening PRs, next to that validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016KAfcDUSerQCCWwBM9BxAo
@GhostofGoes

Copy link
Copy Markdown
Contributor

@adamsrnmsu @nblair2 @glattercj to make sure internal apps are updated to use pathlib after this one is merged

@nblair2

nblair2 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

It looks like this is using the syntactic surgar Path(foo) / "bar" / "baz.txt" . Should we update the ignition app to also use that syntax instead of Path(foo, "bar", "bas.txt") ?

@cmulk
cmulk merged commit c5c523a into sandialabs:main Oct 10, 2026
3 checks passed
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.

4 participants