From 3b184a7b4a0f48de2ca2cafbd0085a64ae8a5f12 Mon Sep 17 00:00:00 2001 From: Jacob Wilkins Date: Mon, 27 Jan 2025 17:03:08 +0000 Subject: [PATCH 1/3] Convert os.path to pathlib based Paths --- MDANSE/Src/MDANSE/Chemistry/Databases.py | 22 +- MDANSE/Src/MDANSE/Core/Platform.py | 200 +++++++----------- .../Configurators/AseInputFileConfigurator.py | 3 +- .../HDFTrajectoryConfigurator.py | 4 +- .../Configurators/InputFileConfigurator.py | 4 +- .../McStasOptionsConfigurator.py | 16 +- .../MultiInputFileConfigurator.py | 3 +- .../OutputDirectoryConfigurator.py | 2 +- .../Configurators/OutputFilesConfigurator.py | 23 +- .../OutputStructureConfigurator.py | 11 +- .../OutputTrajectoryConfigurator.py | 17 +- .../SingleOutputFileConfigurator.py | 21 +- .../Framework/Configurators/__init__.py | 16 +- .../MDANSE/Framework/Converters/Gromacs.py | 4 +- .../MDANSE/Framework/Converters/__init__.py | 17 +- .../Src/MDANSE/Framework/Formats/HDFFormat.py | 12 +- .../Src/MDANSE/Framework/Formats/SVGFormat.py | 14 +- .../MDANSE/Framework/Formats/TextFormat.py | 17 +- .../Src/MDANSE/Framework/Formats/__init__.py | 16 +- .../Src/MDANSE/Framework/Handlers/__init__.py | 16 +- .../Framework/InputData/InputFileData.py | 8 +- .../MDANSE/Framework/InputData/__init__.py | 16 +- .../InstrumentResolutions/__init__.py | 16 +- .../MDANSE/Framework/Jobs/AverageStructure.py | 4 +- MDANSE/Src/MDANSE/Framework/Jobs/IJob.py | 63 +++--- .../Framework/Jobs/McStasVirtualInstrument.py | 44 ++-- MDANSE/Src/MDANSE/Framework/Jobs/__init__.py | 17 +- .../Framework/OutputVariables/__init__.py | 16 +- .../MDANSE/Framework/Projectors/__init__.py | 17 +- .../Src/MDANSE/Framework/QVectors/__init__.py | 17 +- .../Framework/Session/CurrentSession.py | 7 +- MDANSE/Src/MDANSE/Framework/Units.py | 7 +- .../MDANSE/Framework/UserDefinitionStore.py | 7 +- MDANSE/Src/MDANSE/IO/TextFile.py | 61 +++--- .../MDANSE/MolecularDynamics/Trajectory.py | 29 ++- .../NeutronInstruments/Coverage/__init__.py | 16 +- .../NeutronInstruments/Method/__init__.py | 16 +- .../NeutronInstruments/Resolution/__init__.py | 16 +- .../NeutronInstruments/Spectrum/__init__.py | 16 +- .../Src/MDANSE/NeutronInstruments/__init__.py | 16 +- MDANSE/Src/MDANSE/Scripts/mdanse.py | 25 +-- .../Src/MDANSE/Trajectory/H5MDTrajectory.py | 14 +- .../Src/MDANSE/Trajectory/MdanseTrajectory.py | 24 +-- MDANSE/Tests/UnitTests/test_databases.py | 7 +- 44 files changed, 388 insertions(+), 529 deletions(-) diff --git a/MDANSE/Src/MDANSE/Chemistry/Databases.py b/MDANSE/Src/MDANSE/Chemistry/Databases.py index df6f51204c..e7b52792d4 100644 --- a/MDANSE/Src/MDANSE/Chemistry/Databases.py +++ b/MDANSE/Src/MDANSE/Chemistry/Databases.py @@ -15,8 +15,8 @@ # import copy -import os from typing import Union, ItemsView, Dict, Any +from pathlib import Path import json @@ -30,8 +30,8 @@ class _Database(metaclass=Singleton): Base class for all the databases. """ - _DEFAULT_DATABASE: str - _USER_DATABASE: str + _DEFAULT_DATABASE: Path + _USER_DATABASE: Path def __init__(self): """ @@ -65,7 +65,11 @@ def __iter__(self): for v in self._data.values(): yield copy.deepcopy(v) - def _load(self, user_database: str = None, default_database: str = None) -> None: + def _load( + self, + user_database: Union[Path, str, None] = None, + default_database: Union[Path, str, None] = None, + ) -> None: """ Load the database. This method should never be called elsewhere than __init__ or unit testing. @@ -77,10 +81,14 @@ def _load(self, user_database: str = None, default_database: str = None) -> None """ if user_database is None: user_database = self._USER_DATABASE + else: + user_database = Path(user_database) if default_database is None: default_database = self._DEFAULT_DATABASE + else: + default_database = Path(default_database) - if os.path.exists(user_database): + if user_database.exists(): database_path = user_database else: database_path = default_database @@ -164,10 +172,10 @@ class AtomsDatabase(_Database): >>> atoms = ATOMS_DATABASE.atoms() """ - _DEFAULT_DATABASE = os.path.join(os.path.dirname(__file__), "atoms.json") + _DEFAULT_DATABASE = Path(__file__).parent / "atoms.json" # The user path - _USER_DATABASE = os.path.join(PLATFORM.application_directory(), "atoms.json") + _USER_DATABASE = PLATFORM.application_directory() / "atoms.json" # The python types supported by the database _TYPES = {"str": str, "int": int, "float": float, "list": list} diff --git a/MDANSE/Src/MDANSE/Core/Platform.py b/MDANSE/Src/MDANSE/Core/Platform.py index 9a0fb9d92e..e00316fc6b 100644 --- a/MDANSE/Src/MDANSE/Core/Platform.py +++ b/MDANSE/Src/MDANSE/Core/Platform.py @@ -20,13 +20,16 @@ import getpass import inspect import os +import platform import re import subprocess -import tempfile - +from pathlib import Path +from typing import Optional, Union from MDANSE.Core.Error import Error +PathLike = Union[Path, str] + class PlatformError(Error): """ @@ -58,28 +61,28 @@ def __new__(cls, *args, **kwargs): return cls.__instance @abc.abstractmethod - def application_directory(self): + def application_directory(self) -> Path: """ Returns the path for MDANSE application directory. The directory data used by MDANSE for storing preferences, databses, jobs temporary files ... :return: the path for MDANSE application directory. - :rtype: str + :rtype: Path """ pass - def doc_path(self): + def doc_path(self) -> Path: """ Returns the path for MDANSE documentation root directory. :return: the path for MDANSE documentation root directory - :rtype: str + :rtype: Path """ - return os.path.join(self.package_directory(), "Doc") + return self.package_directory() / "Doc" - def jobs_launch_delay(self): + def jobs_launch_delay(self) -> float: """ Returns the delay (in seconds) for a job to launch. This is used to determine the delay before updating the GUI and suppressing a job status file @@ -89,27 +92,27 @@ def jobs_launch_delay(self): """ return 2.0 - def api_path(self): + def api_path(self) -> Path: """ Returns the path for MDANSE HTML API. :return: the path for MDANSE HTML documentation - :rtype: str + :rtype: Path """ - return os.path.join(self.package_directory(), "Doc", "api", "html") + return self.package_directory() / "Doc" / "api" / "html" - def help_path(self): + def help_path(self) -> Path: """ Returns the path for MDANSE HTML help. :return: the path for MDANSE HTML documentation - :rtype: str + :rtype: Path """ - return os.path.join(self.package_directory(), "Doc", "help", "html") + return self.package_directory() / "Doc" / "help" / "html" - def full_dotted_module(self, obj): + def full_dotted_module(self, obj) -> Optional[str]: """ Returns the fully dotted name of a module given the module object itself or a class stored in this module. @@ -121,25 +124,22 @@ def full_dotted_module(self, obj): """ if inspect.ismodule(obj): - path = obj.__file__ + path = Path(obj.__file__) elif inspect.isclass(obj): - path = inspect.getmodule(obj).__file__ + path = Path(inspect.getmodule(obj).__file__) else: raise PlatformError("Invalid query object type.") - basepath = os.path.join(os.path.dirname(self.package_directory()), "") + basepath = self.package_directory().parent - s = path.split(basepath) - if len(s) != 2: + try: + relativePath = path.relative_to(basepath) + except ValueError: return None - _, relativePath = path.split(basepath) + return ".".join(relativePath.with_suffix("").parts) - relativePath = os.path.splitext(relativePath)[0] - - return ".".join(relativePath.split(os.path.sep)) - - def change_directory(self, directory): + def change_directory(self, directory: PathLike) -> None: """ Change the current directory to a new directory. @@ -150,7 +150,7 @@ def change_directory(self, directory): os.chdir(directory) @classmethod - def is_file_writable(cls, filepath: str) -> bool: + def is_file_writable(cls, filepath: PathLike) -> bool: """Check if the directories can be created and a file can be written into it. @@ -164,39 +164,15 @@ def is_file_writable(cls, filepath: str) -> bool: bool True if a file can be written. """ - dirname = cls.get_path(os.path.dirname(filepath)) + filepath = cls.get_path(filepath) - def recursive_check(head_0: str) -> bool: - """Builds the directories up and tests if the file can be - written and then removes everything so that no changes are - made to the filesystem. - """ - if os.path.exists(dirname): - try: - open(filepath, "w").close() - os.remove(filepath) - except OSError: - return False - return True - - head, tail = os.path.split(head_0) - if os.path.exists(head): - try: - os.mkdir(head_0) - except OSError: - return False - writable = recursive_check(dirname) - os.rmdir(head_0) - return writable - else: - return recursive_check(head) - - if os.path.isfile(filepath): - return os.access(filepath, os.W_OK) - else: - return recursive_check(dirname) + for direc in filepath.parents: + if direc.exists(): + return os.access(direc, os.W_OK) - def create_directory(self, path): + return False + + def create_directory(self, path: PathLike) -> None: """ Creates a directory. @@ -206,32 +182,26 @@ def create_directory(self, path): path = self.get_path(path) - if os.path.exists(path): - return - - # Try to make the directory. try: - os.makedirs(path) - + path.mkdir(parents=True, exist_ok=True) # An error occured. - except OSError as e: + except OSError as err: raise PlatformError( - "The following exception was raised while trying to create a directory at " - "{0}: /n {1}".format(str(path), e) - ) + f"Problem trying to create a directory at {path}" + ) from err @classmethod - def get_path(cls, path): + def get_path(cls, path: PathLike) -> Path: """ Return a normalized and absolute version of a given path :param path: the path of the file to be normalized and made absolute - :type path: str + :type path: Path :return: the normalized and absolute version of the input path - :rtype: str + :rtype: Path """ - return os.path.abspath(os.path.expanduser(path)) + return Path(path).expanduser().absolute() def database_default_path(self): """ @@ -241,7 +211,7 @@ def database_default_path(self): :rtype: string """ - return os.path.join(self.package_directory(), "Data", "elements_database.csv") + return self.package_directory() / "Data" / "elements_database.csv" def database_user_path(self): """ @@ -251,7 +221,7 @@ def database_user_path(self): :rtype: string """ - return os.path.join(self.application_directory(), "elements_database.csv") + return self.application_directory() / "elements_database.csv" @abc.abstractmethod def get_processes_info(self): @@ -291,7 +261,7 @@ def example_data_directory(self): :rtype: str """ - return os.path.join(os.path.dirname(self.package_directory()), "Data") + return self.package_directory().parent / "Data" def base_directory(self): """ @@ -301,59 +271,57 @@ def base_directory(self): @rtype: str """ - return os.path.dirname(os.path.dirname(os.path.dirname(__file__))) + return Path(__file__).parents[2] - def package_directory(self): + def package_directory(self) -> Path: """ Returns the path for MDANSE package. @return: the path for MDANSE package. - @rtype: str + @rtype: Path """ - return os.path.dirname(os.path.dirname(__file__)) + return Path(__file__).parent.parent - def macros_directory(self): + def macros_directory(self) -> Path: """ Returns the path of the directory where the MDANSE macros will be searched. :return: the path of the directory where the MDANSE macros will be searched. - :rtype: str + :rtype: Path """ - macrosDir = os.path.join(self.application_directory(), "macros") - - return macrosDir + return self.application_directory() / "macros" - def logfiles_directory(self): + def logfiles_directory(self) -> Path: """ Returns the path of the directory where the MDANSE job logfiles are stored. :return: the path of the directory where the MDANSE job logfiles are stored. - :rtype: str + :rtype: Path """ - path = os.path.join(self.application_directory(), "logfiles") + path = self.application_directory() / "logfiles" self.create_directory(path) return path - def temporary_files_directory(self): + def temporary_files_directory(self) -> Path: """ Returns the path of the directory where the temporary MDANSE job status files are stored. :return: the path of the directory where the temporary MDANSE job status files are stored - :rtype: str + :rtype: Path """ - path = os.path.join(self.application_directory(), "temporary_files") + path = self.application_directory() / "temporary_files" self.create_directory(path) return path - def username(self): + def username(self) -> str: """ Returns the name of the user that run MDANSE. @@ -411,11 +379,10 @@ def application_directory(self): :rtype: str """ - basedir = os.path.join(os.environ["HOME"], ".mdanse") + basedir = Path(os.environ["HOME"]) / ".mdanse" # If the application directory does not exist, create it. - if not os.path.exists(basedir): - os.makedirs(basedir) + basedir.mkdir(exist_ok=True, parents=True) return basedir @@ -449,18 +416,15 @@ def get_processes_info(self): """ # Get all the active processes using the Unix ps command - procs = subprocess.Popen(["ps", "-eo", "pid,etime"], stdout=subprocess.PIPE) - - # The output of the ps command is splitted according to line feeds. - procs = procs.communicate()[0].decode("utf-8").split("\n")[1:] - - # The list of (pid,executable). - procs = [p.split() for p in procs if p] - - # A mapping between the active processes pid and their corresponding exectuable. - procs = dict( - [(int(p[0].strip()), self.etime_to_ctime(p[1].strip())) for p in procs] + process = subprocess.run( + ["ps", "-eo", "pid,etime"], + capture_output=True, + check=True, + text=True, + shell=True, ) + procs = map(str.split, filter(None, process.stdout.splitlines())) + procs = {int(pid): self.etime_to_ctime(etime.strip()) for pid, etime in procs} return procs @@ -488,25 +452,24 @@ class PlatformWin(Platform): name = "windows" - def application_directory(self): + def application_directory(self) -> Path: """ Returns the path for MDANSE application directory. The directory data used by MDANSE for storing preferences, databses, jobs temporary files ... :return: the path for MDANSE application directory. - :rtype: str + :rtype: Path """ - basedir = os.path.join(os.environ["APPDATA"], "mdanse") + basedir = Path(os.environ["APPDATA"]) / "mdanse" # If the application directory does not exist, create it. - if not os.path.exists(basedir): - os.makedirs(basedir) + os.makedirs(basedir) return basedir - def get_process_creation_time(self, process): + def get_process_creation_time(self, process) -> int: """ Return the creation time of a given process. @@ -521,7 +484,7 @@ def get_process_creation_time(self, process): exittime = ctypes.c_ulonglong() kerneltime = ctypes.c_ulonglong() usertime = ctypes.c_ulonglong() - rc = ctypes.windll.kernel32.GetProcessTimes( + ctypes.windll.kernel32.GetProcessTimes( process, ctypes.byref(creationtime), ctypes.byref(exittime), @@ -534,7 +497,7 @@ def get_process_creation_time(self, process): return creationtime.value - def get_processes_info(self): + def get_processes_info(self) -> dict: """ Returns the current active processes. @@ -563,7 +526,7 @@ def get_processes_info(self): # Number of processes returned nReturned = cbNeeded.value // ctypes.sizeof(ctypes.c_ulong()) - pidProcess = [i for i in aProcesses][:nReturned] + pidProcess = list(aProcesses)[:nReturned] for pid in pidProcess: # Get handle to the process based on PID @@ -593,15 +556,15 @@ def get_processes_info(self): return processes - def home_directory(self): + def home_directory(self) -> Path: """ Returns the home directory of the user that runs MDANSE. @return: the home directory - @rtype: str + @rtype: Path """ - return os.environ["USERPROFILE"] + return Path(os.environ["USERPROFILE"]) def kill_process(self, pid): """ @@ -623,9 +586,8 @@ def kill_process(self, pid): ctypes.windll.kernel32.CloseHandle(handle) -import platform - system = platform.system() +PLATFORM: Platform # Instantiate the proper platform class depending on the OS on which MDANSE runs if system == "Linux": diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/AseInputFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/AseInputFileConfigurator.py index ecefe18fee..d69e05bbfe 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/AseInputFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/AseInputFileConfigurator.py @@ -13,7 +13,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import os from ase.io.formats import all_formats @@ -63,7 +62,7 @@ def configure(self, values): value = PLATFORM.get_path(value) - if not os.path.exists(value): + if not value.exists(): LOG.error(f"FILE MISSING in {self._name}") self.error_status = f"The file {value} does not exist" return diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py index b58d8b1987..be357df2de 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/HDFTrajectoryConfigurator.py @@ -14,8 +14,6 @@ # along with this program. If not, see . # -import os - from MDANSE import PLATFORM from MDANSE.Framework.Configurators.InputFileConfigurator import InputFileConfigurator @@ -60,7 +58,7 @@ def configure(self, value): self["filename"] = PLATFORM.get_path(inputTraj.filename) - self["basename"] = os.path.basename(self["filename"]) + self["basename"] = self["filename"].name self["length"] = len(self["instance"]) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/InputFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/InputFileConfigurator.py index cd186bebac..eca5c5675b 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/InputFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/InputFileConfigurator.py @@ -14,8 +14,6 @@ # along with this program. If not, see . # -import os - from MDANSE import PLATFORM from MDANSE.Framework.Configurators.IConfigurator import IConfigurator @@ -54,7 +52,7 @@ def configure(self, value): value = PLATFORM.get_path(value) - if not os.path.exists(value): + if not value.exists(): self.error_status = f"The file {value} does not exist" return diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/McStasOptionsConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/McStasOptionsConfigurator.py index 933ae97b4b..c9cf35aaa9 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/McStasOptionsConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/McStasOptionsConfigurator.py @@ -14,10 +14,10 @@ # along with this program. If not, see . # -import os import tempfile import time from typing import Dict, Any +from pathlib import Path from MDANSE import PLATFORM from MDANSE.Framework.Configurators.IConfigurator import IConfigurator @@ -50,10 +50,10 @@ class McStasOptionsConfigurator(IConfigurator): _default = { "ncount": 10000, - "dir": os.path.join( - tempfile.gettempdir(), - "mcstas_output", - time.strftime("%d.%m.%Y-%H:%M:%S", time.localtime()), + "dir": ( + Path(tempfile.gettempdir()) + / "mcstas_output" + / time.strftime("%d.%m.%Y-%H:%M:%S", time.localtime()) ), } @@ -81,12 +81,12 @@ def configure(self, value): for k, v in list(options.items()): if k == "dir": # If the output directory already exists, defines a 'unique' output directory name because otherwise McStas throws. - if os.path.exists(v): + if Path(v).exists(): v = self._default["dir"] - self["mcstas_output_directory"] = v + self["mcstas_output_directory"] = Path(v) tmp.append("--%s=%s" % (k, v)) - dirname = os.path.dirname(self["mcstas_output_directory"]) + dirname = self["mcstas_output_directory"].parent try: PLATFORM.create_directory(dirname) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/MultiInputFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/MultiInputFileConfigurator.py index 59497623fb..bd327045ab 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/MultiInputFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/MultiInputFileConfigurator.py @@ -14,7 +14,6 @@ # along with this program. If not, see . # import ast -import os from typing import Union from MDANSE import PLATFORM @@ -73,7 +72,7 @@ def configure(self, setting: Union[str, list]): none_exist = [] for value in values: - if not os.path.isfile(value): + if not value.is_file(): none_exist.append(value) if none_exist: diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/OutputDirectoryConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/OutputDirectoryConfigurator.py index 62f74c32e4..00bb8b7e91 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/OutputDirectoryConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/OutputDirectoryConfigurator.py @@ -55,7 +55,7 @@ def configure(self, value): value = PLATFORM.get_path(value) if self._new: - if os.path.exists(value): + if value.exists(): self.error_status = "the output directory must not exist" return diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/OutputFilesConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/OutputFilesConfigurator.py index 194b4c555b..145ab6e294 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/OutputFilesConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/OutputFilesConfigurator.py @@ -14,8 +14,7 @@ # along with this program. If not, see . # -import os -from pathlib import PurePath +from pathlib import Path from MDANSE import PLATFORM from MDANSE.Framework.Configurators.IConfigurator import IConfigurator @@ -66,6 +65,7 @@ def configure(self, value): self._original_input = value root, formats, logs = value + root = Path(root) if logs not in self.log_options: self.error_status = "log level option not recognised" @@ -96,23 +96,18 @@ def configure(self, value): self["root"] = root self["formats"] = formats - self["files"] = [] - for extension in [IFormat.create(f).extension for f in formats]: - if extension in root[-len(extension) :]: - self["files"].append(root) - else: - self["files"].append(root + extension) + self["files"] = [ + root if root.suffix == ext else root.with_suffix(root.suffix + ext) + for ext in (IFormat.create(f).extension for f in formats) + ] for file in self["files"]: - if PurePath(os.path.abspath(file)) in self._forbidden_files: + if file.absolute() in self._forbidden_files: self.error_status = f"File {file} is either open or being written into. Please pick another name." return self["value"] = self["files"] self["log_level"] = logs - if logs == "no logs": - self["write_logs"] = False - else: - self["write_logs"] = True + self["write_logs"] = logs != "no logs" self.error_status = "OK" @property @@ -137,7 +132,7 @@ def get_information(self): info = ["Input files:\n"] for f in self["files"]: - info.append(f) + info.append(str(f)) info.append("\n") return "".join(info) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/OutputStructureConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/OutputStructureConfigurator.py index 24c7b16a71..c55f0d1e39 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/OutputStructureConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/OutputStructureConfigurator.py @@ -14,8 +14,7 @@ # along with this program. If not, see . # -import os -from pathlib import PurePath +from pathlib import Path from ase.io.formats import ioformats @@ -65,6 +64,7 @@ def configure(self, value): self._original_input = value root, format, logs = value + root = Path(root) if logs not in self.log_options: self.error_status = "log level option not recognised" @@ -85,14 +85,11 @@ def configure(self, value): self["root"] = root self["format"] = format self["file"] = root - if PurePath(os.path.abspath(self["file"])) in self._forbidden_files: + if self["file"].absolute() in self._forbidden_files: self.error_status = f"File {self['file']} is either open or being written into. Please pick another name." return self["log_level"] = logs - if logs == "no logs": - self["write_logs"] = False - else: - self["write_logs"] = True + self["write_logs"] = logs != "no logs" self["value"] = self["file"] self.error_status = "OK" diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py index c2bec2e22a..e92988de9e 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/OutputTrajectoryConfigurator.py @@ -14,8 +14,7 @@ # along with this program. If not, see . # -import os -from pathlib import PurePath +from pathlib import Path import numpy as np @@ -62,6 +61,7 @@ def configure(self, value: tuple): self._original_input = value root, dtype, chunk_size, compression, logs = value + root = Path(root) if logs not in self.log_options: self.error_status = "log level option not recognised" @@ -93,20 +93,19 @@ def configure(self, value: tuple): self["format"] = self._format self["extension"] = IFormat.create(self._format).extension temp_name = root - if not self["extension"] in temp_name[-5:]: # capture most extension lengths - temp_name += self["extension"] + if self["extension"] != root.suffix: # capture most extension lengths + temp_name = temp_name.with_suffix(temp_name.suffix + self["extension"]) self["file"] = temp_name - if PurePath(os.path.abspath(self["file"])) in self._forbidden_files: + + if self["file"].absolute() in self._forbidden_files: self.error_status = f"File {self['file']} is either open or being written into. Please pick another name." return + self["dtype"] = self._dtype self["compression"] = self._compression self["chunk_size"] = self._chunk_limit self["log_level"] = logs - if logs == "no logs": - self["write_logs"] = False - else: - self["write_logs"] = True + self["write_logs"] = logs != "no logs" self.error_status = "OK" @property diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/SingleOutputFileConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/SingleOutputFileConfigurator.py index 872c7a5708..7e26065e36 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/SingleOutputFileConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/SingleOutputFileConfigurator.py @@ -13,7 +13,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import os + +from typing import Tuple +from pathlib import Path + from MDANSE import PLATFORM from MDANSE.Framework.Configurators.IConfigurator import IConfigurator from MDANSE.Framework.Formats.IFormat import IFormat @@ -49,23 +52,25 @@ def __init__(self, name, format=None, **kwargs): format if format is not None else SingleOutputFileConfigurator._default[-1] ) - def configure(self, value): + def configure(self, value: Tuple[str, str]): """ Configure a set of output files for an analysis. - :param value: the output files specifications. Must be a 3-tuple whose 1st element \ - is the output directory, 2nd element the basename and 3rd element a list of file formats. - :type value: 3-tuple + :param value: the output files specifications. + Must be a 2-tuple whose 1st element is + the file and 2nd element a list of file formats. + :type value: 2-tuple """ self._original_input = value root, format = value + root = Path(root) if not root: self.error_status = "empty root name for the output file." return - dirname = os.path.dirname(root) + dirname = root.parent try: PLATFORM.create_directory(dirname) @@ -91,8 +96,8 @@ def configure(self, value): self["format"] = format self["extension"] = IFormat.create(format).extension temp_name = root - if not self["extension"] in temp_name[-5:]: # capture most extension lengths - temp_name += self["extension"] + if self["extension"] != root.suffix: + temp_name = temp_name.with_suffix(temp_name.suffix + self["extension"]) self["file"] = temp_name self.error_status = "OK" diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/__init__.py b/MDANSE/Src/MDANSE/Framework/Configurators/__init__.py index e36d77d78c..aacbe6b787 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/__init__.py @@ -13,23 +13,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module("." + name, "MDANSE.Framework.Configurators") except ModuleNotFoundError: diff --git a/MDANSE/Src/MDANSE/Framework/Converters/Gromacs.py b/MDANSE/Src/MDANSE/Framework/Converters/Gromacs.py index 800b8d7dec..951ac123f6 100644 --- a/MDANSE/Src/MDANSE/Framework/Converters/Gromacs.py +++ b/MDANSE/Src/MDANSE/Framework/Converters/Gromacs.py @@ -78,12 +78,12 @@ def initialize(self): data_to_be_written = ["configuration", "time"] # Create XTC or TRR object depending on which kind of trajectory was loaded - if self.configuration["xtc_file"]["filename"][-4:] == ".xtc": + if self.configuration["xtc_file"]["filename"].suffix == ".xtc": self._xdr_file = XTCTrajectoryFile( self.configuration["xtc_file"]["filename"], "r" ) self._xtc = True - elif self.configuration["xtc_file"]["filename"][-4:] == ".trr": + elif self.configuration["xtc_file"]["filename"].suffix == ".trr": self._xdr_file = TRRTrajectoryFile( self.configuration["xtc_file"]["filename"], "r" ) diff --git a/MDANSE/Src/MDANSE/Framework/Converters/__init__.py b/MDANSE/Src/MDANSE/Framework/Converters/__init__.py index a614730227..54a12bba0e 100644 --- a/MDANSE/Src/MDANSE/Framework/Converters/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/Converters/__init__.py @@ -13,25 +13,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob import importlib -import os - +from pathlib import Path from MDANSE.MLogging import LOG -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module("." + name, "MDANSE.Framework.Converters") except ModuleNotFoundError: diff --git a/MDANSE/Src/MDANSE/Framework/Formats/HDFFormat.py b/MDANSE/Src/MDANSE/Framework/Formats/HDFFormat.py index 7daf059098..1289454986 100644 --- a/MDANSE/Src/MDANSE/Framework/Formats/HDFFormat.py +++ b/MDANSE/Src/MDANSE/Framework/Formats/HDFFormat.py @@ -13,8 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import os -from typing import TYPE_CHECKING, Dict +from pathlib import Path +from typing import TYPE_CHECKING, Dict, Union from importlib import metadata import h5py @@ -46,7 +46,7 @@ class HDFFormat(IFormat): @classmethod def write( cls, - filename: str, + filename: Union[Path, str], data: Dict[str, "IOutputVariable"], header: str = "", run_instance: "IJob" = None, @@ -67,12 +67,10 @@ def write( """ string_dt = h5py.special_dtype(vlen=str) - filename = os.path.splitext(filename)[0] - - filename = "%s%s" % (filename, extension) + filename = Path(filename).with_suffix(extension) # The HDF output file is opened for writing. - PLATFORM.create_directory(os.path.dirname(filename)) + PLATFORM.create_directory(filename.parent) outputFile = h5py.File(filename, "w") if header: diff --git a/MDANSE/Src/MDANSE/Framework/Formats/SVGFormat.py b/MDANSE/Src/MDANSE/Framework/Formats/SVGFormat.py index 6b2a187615..e7acd8170c 100644 --- a/MDANSE/Src/MDANSE/Framework/Formats/SVGFormat.py +++ b/MDANSE/Src/MDANSE/Framework/Formats/SVGFormat.py @@ -14,10 +14,11 @@ # along with this program. If not, see . # -import os import re import io import tarfile +from pathlib import Path +from typing import Union import numpy as np @@ -45,7 +46,7 @@ class SVGFormat(IFormat): extensions = [".svg"] @classmethod - def write(cls, filename, data, header=""): + def write(cls, filename: Union[Path, str], data, header=""): """ Write a set of output variables into a set of SVG files. @@ -59,8 +60,7 @@ def write(cls, filename, data, header=""): :type header: str """ - filename = os.path.splitext(filename)[0] - filename = "%s.tar" % filename + filename = Path(filename).with_suffix(".tar") tf = tarfile.open(filename, "w") @@ -79,9 +79,7 @@ def write(cls, filename, data, header=""): pl = Poly(list(zip(axis, var)), stroke="blue") - svgfilename = os.path.join( - os.path.dirname(filename), "%s%s" % (var.varname, cls.extensions[0]) - ) + svgfilename = filename.parent / f"{var.varname}{cls.extensions[0]}" Frame( min(axis), @@ -95,7 +93,7 @@ def write(cls, filename, data, header=""): tf.add(svgfilename, arcname="%s%s" % (var.varname, cls.extensions[0])) - os.remove(svgfilename) + svgfilename.unlink() if header: tempStr = io.StringIO() diff --git a/MDANSE/Src/MDANSE/Framework/Formats/TextFormat.py b/MDANSE/Src/MDANSE/Framework/Formats/TextFormat.py index 8a62a82751..7c259740f2 100644 --- a/MDANSE/Src/MDANSE/Framework/Formats/TextFormat.py +++ b/MDANSE/Src/MDANSE/Framework/Formats/TextFormat.py @@ -14,12 +14,12 @@ # along with this program. If not, see . # -import os import io import tarfile import codecs import time -from typing import TYPE_CHECKING +from pathlib import Path +from typing import TYPE_CHECKING, Union from importlib import metadata import numpy as np @@ -47,7 +47,13 @@ class TextFormat(IFormat): extensions = [".dat", ".txt"] @classmethod - def write(cls, filename, data, header: str = "", run_instance: "IJob" = None): + def write( + cls, + filename: Union[Path, str], + data, + header: str = "", + run_instance: "IJob" = None, + ): """ Write a set of output variables into a set of Text files. @@ -61,10 +67,9 @@ def write(cls, filename, data, header: str = "", run_instance: "IJob" = None): :type header: str """ - filename = os.path.splitext(filename)[0] - filename = "%s_text.tar" % filename + filename = Path(filename).with_suffix(".tar") - PLATFORM.create_directory(os.path.dirname(filename)) + PLATFORM.create_directory(filename.parent) tf = tarfile.open(filename, "w") if header: diff --git a/MDANSE/Src/MDANSE/Framework/Formats/__init__.py b/MDANSE/Src/MDANSE/Framework/Formats/__init__.py index 7739e3979c..3b55689e9c 100644 --- a/MDANSE/Src/MDANSE/Framework/Formats/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/Formats/__init__.py @@ -13,23 +13,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module("." + name, "MDANSE.Framework.Formats") except ModuleNotFoundError: diff --git a/MDANSE/Src/MDANSE/Framework/Handlers/__init__.py b/MDANSE/Src/MDANSE/Framework/Handlers/__init__.py index 68d24466c0..8247aa0c81 100644 --- a/MDANSE/Src/MDANSE/Framework/Handlers/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/Handlers/__init__.py @@ -13,23 +13,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module("." + name, "MDANSE.Framework.Handlers") except ModuleNotFoundError: diff --git a/MDANSE/Src/MDANSE/Framework/InputData/InputFileData.py b/MDANSE/Src/MDANSE/Framework/InputData/InputFileData.py index 132978c99e..ecafeede2e 100644 --- a/MDANSE/Src/MDANSE/Framework/InputData/InputFileData.py +++ b/MDANSE/Src/MDANSE/Framework/InputData/InputFileData.py @@ -15,7 +15,7 @@ # import abc -import os +from pathlib import Path from MDANSE.Framework.InputData.IInputData import IInputData @@ -23,9 +23,9 @@ class InputFileData(IInputData): def __init__(self, filename, load=True): IInputData.__init__(self, filename) - - self._basename = os.path.basename(filename) - self._dirname = os.path.dirname(filename) + filename = Path(filename) + self._basename = filename.name + self._dirname = filename.parent if load: self.load() diff --git a/MDANSE/Src/MDANSE/Framework/InputData/__init__.py b/MDANSE/Src/MDANSE/Framework/InputData/__init__.py index b97a9c724f..c6ad9aba40 100644 --- a/MDANSE/Src/MDANSE/Framework/InputData/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/InputData/__init__.py @@ -13,23 +13,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module("." + name, "MDANSE.Framework.InputData") except ModuleNotFoundError: diff --git a/MDANSE/Src/MDANSE/Framework/InstrumentResolutions/__init__.py b/MDANSE/Src/MDANSE/Framework/InstrumentResolutions/__init__.py index 0003dbb3b2..611b7484a7 100644 --- a/MDANSE/Src/MDANSE/Framework/InstrumentResolutions/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/InstrumentResolutions/__init__.py @@ -13,23 +13,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module( "." + name, "MDANSE.Framework.InstrumentResolutions" diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/AverageStructure.py b/MDANSE/Src/MDANSE/Framework/Jobs/AverageStructure.py index d91602e680..f30e5ebdcd 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/AverageStructure.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/AverageStructure.py @@ -13,8 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import os import collections +from pathlib import Path import numpy as np from ase.io import write as ase_write @@ -184,7 +184,7 @@ def finalize(self): self._ase_atoms.set_scaled_positions(temp - correction) PLATFORM.create_directory( - os.path.dirname(self.configuration["output_files"]["file"]) + Path(self.configuration["output_files"]["file"]).parent ) ase_write( self.configuration["output_files"]["file"], diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/IJob.py b/MDANSE/Src/MDANSE/Framework/Jobs/IJob.py index 447157bc9e..f41191ce24 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/IJob.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/IJob.py @@ -18,7 +18,6 @@ from logging.handlers import QueueHandler, QueueListener import abc -import glob import os import multiprocessing import queue @@ -28,6 +27,7 @@ import time import sys import traceback +from pathlib import Path from MDANSE import PLATFORM from MDANSE.Core.Error import Error @@ -107,8 +107,7 @@ def define_unique_name(): # The list of the registered jobs. registeredJobs = [ - os.path.basename(f) - for f in glob.glob(os.path.join(PLATFORM.temporary_files_directory(), "*")) + f.name for f in PLATFORM.temporary_files_directory().glob("*") ] while True: @@ -164,7 +163,7 @@ def initialize(self): "output_files" in self.configuration and self.configuration["output_files"]["write_logs"] ): - log_filename = self.configuration["output_files"]["root"] + ".log" + log_filename = str(self.configuration["output_files"]["root"]) + ".log" self.add_log_file_handler( log_filename, self.configuration["output_files"]["log_level"] ) @@ -430,13 +429,13 @@ def save_template(cls, shortname, classname): "A job with %r name is already stored in the registry" % shortname ) - templateFile = os.path.join(PLATFORM.macros_directory(), "%s.py" % classname) + templateFile = PLATFORM.macros_directory() / f"{classname}.py" try: - f = open(templateFile, "w") + with templateFile.open("w") as f: - f.write( - '''import collections + f.write( + '''import collections from MDANSE.Framework.Jobs.IJob import IJob @@ -444,13 +443,13 @@ class %(classname)s(IJob): """ You should enter the description of your job here ... """ - + # You should enter the label under which your job will be viewed from the gui. label = %(label)r # You should enter the category under which your job will be references. category = ('My jobs',) - + ancestor = ["hdf_trajectory"] # You should enter the configuration of your job here @@ -459,7 +458,7 @@ class %(classname)s(IJob): settings['trajectory']=('hdf_trajectory',{}) settings['frames']=('frames', {"dependencies":{'trajectory':'trajectory'}}) settings['output_files']=('output_files', {"formats":["HDFFormat","netcdf","TextFormat"]}) - + def initialize(self): """ Initialize the input parameters and analysis self variables @@ -468,7 +467,7 @@ def initialize(self): # Compulsory. You must enter the number of steps of your job. # Here for example the number of selected frames self.numberOfSteps = self.configuration['frames']['number'] - + # Create an output data for the selected frames. self._outputData.add("time", "LineOutputVariable", self.configuration['frames']['time'], units='ps') @@ -477,40 +476,38 @@ def run_step(self, index): """ Runs a single step of the job. """ - + return index, None - - + + def combine(self, index, x): """ Synchronize the output of each individual run_step output. - """ - + """ + def finalize(self): """ Finalizes the job (e.g. averaging the total term, output files creations ...). - """ + """ # The output data are written self._outputData.write(self.configuration['output_files']['root'], self.configuration['output_files']['formats'], self._info, self.output_configuration()) - + # The trajectory is closed - self.configuration['trajectory']['instance'].close() - -''' - % { - "classname": classname, - "label": "label of the class", - "shortname": shortname, - } - ) + self.configuration['trajectory']['instance'].close() + + ''' + % { + "classname": classname, + "label": "label of the class", + "shortname": shortname, + } + ) except IOError: return None - else: - f.close() - return templateFile + return templateFile def add_log_file_handler(self, filename: str, level: str) -> None: """Adds a file handle which is used to write the jobs logs. @@ -523,8 +520,8 @@ def add_log_file_handler(self, filename: str, level: str) -> None: The log level. """ self._log_filename = filename - PLATFORM.create_directory(os.path.dirname(filename)) - fh = FileHandler(filename, mode="w") + PLATFORM.create_directory(Path(self._log_filename).parent) + fh = FileHandler(self._log_filename, mode="w") # set the name so that we can track it and then close it later, # tracking the fh by storing it in this object causes issues # with multiprocessing jobs diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/McStasVirtualInstrument.py b/MDANSE/Src/MDANSE/Framework/Jobs/McStasVirtualInstrument.py index 8ef6dc5fdd..89fdf873d4 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/McStasVirtualInstrument.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/McStasVirtualInstrument.py @@ -14,11 +14,12 @@ # along with this program. If not, see . # import collections -import os import shutil import subprocess import tempfile import io +from pathlib import Path +from typing import Union import numpy as np @@ -117,6 +118,13 @@ class McStasVirtualInstrument(IJob): {"formats": ["MDAFormat", "TextFormat"]}, ) + @property + def mcstas_output_dir(self) -> Path: + """ + Output directory as path. + """ + return Path(self.configuration["options"]["mcstas_output_directory"]) + def initialize(self): """ Initialize the input parameters and analysis self variables @@ -284,13 +292,8 @@ def run_step(self, index): if "ERROR" in line.decode(encoding="utf-8"): raise McStasError("An error occured during McStas run: %s" % out) - with open( - os.path.join( - self.configuration["options"]["mcstas_output_directory"], - "mcstas_mdanse.mvi", - ), - "w", - ) as f: + out_file = self.mcstas_output_dir / "mcstas_mdanse.mvi" + with out_file.open("w") as f: f.write(out.decode(encoding="utf-8")) return index, None @@ -310,14 +313,8 @@ def finalize(self): """ # Rename and move to the result dir the SQW file input - for typ, fname in list(self.outFile.items()): - shutil.move( - fname, - os.path.join( - self.configuration["options"]["mcstas_output_directory"], - typ + ".sqw", - ), - ) + for typ, fname in self.outFile.items(): + shutil.move(fname, self.mcstas_output_dir / f"{typ}.sqw") # Convert McStas output files into NetCDF format self.convert(self.configuration["options"]["mcstas_output_directory"]) @@ -347,20 +344,21 @@ def unique(self, key, d, value=None): i += 1 return key - def convert(self, sim_dir): + def convert(self, sim_dir: Union[Path, str]): """ Convert McStas data set to netCDF File Format """ + sim_dir = Path(sim_dir) typique_sim_fnames = ["mccode.sim", "mcstas.sim"] - sim_file = "" + for sim_fname in typique_sim_fnames: - sim_file = os.path.join(sim_dir, sim_fname) - if os.path.isfile(sim_file): + sim_file = sim_dir / sim_fname + if sim_file.is_file(): break if not sim_file: - raise Exception("Dataset " + sim_file + " does not exist!") + raise Exception(f"Dataset {sim_file} does not exist!") isBegin = lambda line: line.strip().startswith("begin") isCompFilename = lambda line: line.strip().startswith("filename:") @@ -387,7 +385,7 @@ def convert(self, sim_dir): Scanfile = list(filter(isFilename, open(sim_file).readlines())) Scanfile = Scanfile[0].split(": ") - Scanfile = os.path.join(sim_dir, Scanfile[1].strip()) + Scanfile = sim_dir / Scanfile[1].strip() # Proceed to load scan datafile FS = self.read_monitor(Scanfile) L = (len(FS["variables"].split()) - 1) / 2 @@ -402,7 +400,7 @@ def convert(self, sim_dir): for j in range(0, L): MonFile = MonFiles[j].split(":") MonFile = MonFile[1].strip() - MonFile = os.path.join(sim_dir, MonFile) + MonFile = sim_dir / MonFile FS = self.read_monitor(MonFile) FSlist[len(FSlist) :] = [FS] FSlist[j] = self.save_single(FS) diff --git a/MDANSE/Src/MDANSE/Framework/Jobs/__init__.py b/MDANSE/Src/MDANSE/Framework/Jobs/__init__.py index 807908a128..1a84524377 100644 --- a/MDANSE/Src/MDANSE/Framework/Jobs/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/Jobs/__init__.py @@ -13,25 +13,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob import importlib -import os - +from pathlib import Path from MDANSE.MLogging import LOG -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module("." + name, "MDANSE.Framework.Jobs") except ModuleNotFoundError: diff --git a/MDANSE/Src/MDANSE/Framework/OutputVariables/__init__.py b/MDANSE/Src/MDANSE/Framework/OutputVariables/__init__.py index c0eab43316..d896026c9f 100644 --- a/MDANSE/Src/MDANSE/Framework/OutputVariables/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/OutputVariables/__init__.py @@ -13,23 +13,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module( "." + name, "MDANSE.Framework.OutputVariables" diff --git a/MDANSE/Src/MDANSE/Framework/Projectors/__init__.py b/MDANSE/Src/MDANSE/Framework/Projectors/__init__.py index f057eb2ad6..b13afdf3c8 100644 --- a/MDANSE/Src/MDANSE/Framework/Projectors/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/Projectors/__init__.py @@ -13,23 +13,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob + import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module("." + name, "MDANSE.Framework.Projectors") except ModuleNotFoundError: diff --git a/MDANSE/Src/MDANSE/Framework/QVectors/__init__.py b/MDANSE/Src/MDANSE/Framework/QVectors/__init__.py index 8ca3a8bf2e..1c2ce1ee0a 100644 --- a/MDANSE/Src/MDANSE/Framework/QVectors/__init__.py +++ b/MDANSE/Src/MDANSE/Framework/QVectors/__init__.py @@ -13,23 +13,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import glob + import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module("." + name, "MDANSE.Framework.QVectors") except ModuleNotFoundError: diff --git a/MDANSE/Src/MDANSE/Framework/Session/CurrentSession.py b/MDANSE/Src/MDANSE/Framework/Session/CurrentSession.py index 844b7eb8a3..74619200a0 100644 --- a/MDANSE/Src/MDANSE/Framework/Session/CurrentSession.py +++ b/MDANSE/Src/MDANSE/Framework/Session/CurrentSession.py @@ -14,9 +14,8 @@ # along with this program. If not, see . # -import os -from os.path import expanduser import json +from pathlib import Path from abc import ABC, abstractmethod @@ -35,7 +34,7 @@ def load_session(self, fname: str): class SessionSettings(AbstractSession): def __init__(self): super().__init__() - self.main_path = os.path.abspath(".") + self.main_path = Path(".").absolute() def create_structured_project(self): self.relative_paths = { @@ -47,7 +46,7 @@ def create_structured_project(self): class CurrentSession: def __init__(self, fname=None): - self.settings_dir = os.path.join(expanduser("~"), ".MDANSE") + self.settings_dir = Path("~").expanduser() / ".MDANSE" if fname is not None: self.loadSettings(fname) diff --git a/MDANSE/Src/MDANSE/Framework/Units.py b/MDANSE/Src/MDANSE/Framework/Units.py index 53e7addcdf..b8dab49fe6 100644 --- a/MDANSE/Src/MDANSE/Framework/Units.py +++ b/MDANSE/Src/MDANSE/Framework/Units.py @@ -16,7 +16,6 @@ import copy import math import numbers -import os import json @@ -694,11 +693,11 @@ def default(self, obj): class UnitsManager(metaclass=Singleton): _UNITS = {} - _DEFAULT_DATABASE = os.path.join( - PLATFORM.base_directory(), "MDANSE", "Framework", "units.json" + _DEFAULT_DATABASE = ( + PLATFORM.base_directory() / "MDANSE" / "Framework" / "units.json" ) - _USER_DATABASE = os.path.join(PLATFORM.application_directory(), "units.json") + _USER_DATABASE = PLATFORM.application_directory() / "units.json" def __init__(self): self.load() diff --git a/MDANSE/Src/MDANSE/Framework/UserDefinitionStore.py b/MDANSE/Src/MDANSE/Framework/UserDefinitionStore.py index 15fc4f620c..49c9cbee72 100644 --- a/MDANSE/Src/MDANSE/Framework/UserDefinitionStore.py +++ b/MDANSE/Src/MDANSE/Framework/UserDefinitionStore.py @@ -14,7 +14,6 @@ # along with this program. If not, see . # import pickle -import os from MDANSE import PLATFORM from MDANSE.Core.Error import Error @@ -36,7 +35,7 @@ class UserDefinitionStore(object, metaclass=Singleton): user definitions are loaded when MDANSE starts through a cPickle file that will store these definitions. """ - UD_PATH = os.path.join(PLATFORM.application_directory(), "user_definitions_md3.ud") + UD_PATH = PLATFORM.application_directory() / "user_definitions_md3.ud" def __init__(self): self._definitions = {} @@ -52,12 +51,12 @@ def load(self): Load the user definitions. """ - if not os.path.exists(UserDefinitionStore.UD_PATH): + if not UserDefinitionStore.UD_PATH.exists(): return # Try to open the UD file. try: - f = open(UserDefinitionStore.UD_PATH, "rb") + f = UserDefinitionStore.UD_PATH.open("rb") UD = pickle.load(f) # If for whatever reason the pickle file loading failed do not even try to restore it diff --git a/MDANSE/Src/MDANSE/IO/TextFile.py b/MDANSE/Src/MDANSE/IO/TextFile.py index 34abdcdbb9..6892a2d0d9 100644 --- a/MDANSE/Src/MDANSE/IO/TextFile.py +++ b/MDANSE/Src/MDANSE/IO/TextFile.py @@ -18,12 +18,13 @@ Text files with line iteration and transparent compression """ -import os, string, sys +import os +import sys +from pathlib import Path # Use the gzip module for Python version 1.5.2 or higher -gzip = None try: - _version = [int(c) for c in string.split(string.split(sys.version)[0], ".")] + _version = [int(c) for c in sys.version.split()[0].split(".")] if _version >= [1, 5, 2]: try: @@ -31,7 +32,7 @@ except ImportError: gzip = None except: - pass + gzip = None class TextFile: @@ -63,57 +64,67 @@ def __init__(self, filename, mode="r"): if filename.find(":/") > 1: # URL if mode != "r": raise IOError("can't write to a URL") - import urllib.request, urllib.parse, urllib.error + import urllib self.file = urllib.request.urlopen(filename) else: - filename = os.path.expanduser(filename) + filename = Path(filename).expanduser() if mode in ["r", "rt"]: - if not os.path.exists(filename): - raise IOError((2, "No such file or directory: " + filename)) - if filename[-2:] == ".Z": - self.file = os.popen("uncompress -c " + filename, mode) - elif filename[-3:] == ".gz": + if not filename.exists(): + raise IOError((2, f"No such file or directory: {filename}")) + + if filename.suffix == ".Z": + self.file = os.popen(f"uncompress -c {filename}", mode) + + elif filename.suffix == ".gz": if gzip is None: - self.file = os.popen("gunzip -c " + filename, mode) + self.file = os.popen(f"gunzip -c {filename}", mode) else: self.file = gzip.GzipFile(filename, "rb") - elif filename[-4:] == ".bz2": - self.file = os.popen("bzip2 -dc " + filename, mode) + + elif filename.suffix == ".bz2": + self.file = os.popen(f"bzip2 -dc {filename}", mode) + else: try: self.file = open(filename, mode) except IOError as details: - if type(details) == type(()): + if isinstance(details, tuple): details = details + (filename,) raise IOError(details) + elif mode == "w": - if filename[-2:] == ".Z": - self.file = os.popen("compress > " + filename, mode) - elif filename[-3:] == ".gz": + if filename.suffix == ".Z": + self.file = os.popen(f"compress > {filename}", mode) + + elif filename.suffix == ".gz": if gzip is None: - self.file = os.popen("gzip > " + filename, mode) + self.file = os.popen(f"gzip > {filename}", mode) else: self.file = gzip.GzipFile(filename, "wb") - elif filename[-4:] == ".bz2": - self.file = os.popen("bzip2 > " + filename, mode) + + elif filename.suffix == ".bz2": + self.file = os.popen(f"bzip2 > {filename}", mode) + else: try: self.file = open(filename, mode) except IOError as details: - if type(details) == type(()): + if isinstance(details, tuple): details = details + (filename,) raise IOError(details) + elif mode == "a": - if filename[-2:] == ".Z": + if filename.suffix == ".Z": raise IOError((0, "Can't append to .Z files")) - elif filename[-3:] == ".gz": + elif filename.suffix == ".gz": if gzip is None: - self.file = os.popen("gzip >> " + filename, "w") + self.file = os.popen(f"gzip >> {filename}", "w") else: self.file = gzip.GzipFile(filename, "ab") else: self.file = open(filename, mode) + else: raise IOError((0, "Illegal mode: " + repr(mode))) diff --git a/MDANSE/Src/MDANSE/MolecularDynamics/Trajectory.py b/MDANSE/Src/MDANSE/MolecularDynamics/Trajectory.py index 0abda4616c..839f94d94d 100644 --- a/MDANSE/Src/MDANSE/MolecularDynamics/Trajectory.py +++ b/MDANSE/Src/MDANSE/MolecularDynamics/Trajectory.py @@ -13,27 +13,23 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import os -from ast import operator -from typing import Collection, List, Dict, TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Dict, List if TYPE_CHECKING: from MDANSE.Chemistry.Databases import AtomsDatabase + import math +import operator +from collections.abc import Collection +from pathlib import Path +from typing import Union -import numpy as np import h5py - -from MDANSE.MLogging import LOG -from MDANSE.Trajectory.MdanseTrajectory import MdanseTrajectory -from MDANSE.Trajectory.H5MDTrajectory import H5MDTrajectory +import numpy as np +from MDANSE import PLATFORM from MDANSE.Chemistry import ATOMS_DATABASE from MDANSE.Chemistry.ChemicalSystem import ChemicalSystem -from MDANSE.MolecularDynamics.Configuration import ( - RealConfiguration, -) -from MDANSE import PLATFORM - +from MDANSE.MolecularDynamics.Configuration import RealConfiguration available_formats = { "MDANSE": MdanseTrajectory, @@ -461,7 +457,7 @@ class TrajectoryWriter: def __init__( self, - h5_filename, + h5_filename: Union[Path, str], chemical_system: ChemicalSystem, n_steps, selected_atoms=None, @@ -482,8 +478,8 @@ def __init__( :type selected_atoms: list of MDANSE.Chemistry.ChemicalSystem.Atom """ - self._h5_filename = h5_filename - PLATFORM.create_directory(os.path.dirname(h5_filename)) + self._h5_filename = Path(h5_filename) + PLATFORM.create_directory(self._h5_filename.parent) self._h5_file = h5py.File(self._h5_filename, "w") self._chemical_system = chemical_system @@ -1007,6 +1003,7 @@ def read_atoms_trajectory( if __name__ == "__main__": + from MDANSE.Chemistry.ChemicalEntity import Atom from MDANSE.MolecularDynamics.Configuration import RealConfiguration cs = ChemicalSystem() diff --git a/MDANSE/Src/MDANSE/NeutronInstruments/Coverage/__init__.py b/MDANSE/Src/MDANSE/NeutronInstruments/Coverage/__init__.py index 4caf3ac941..002ad78093 100644 --- a/MDANSE/Src/MDANSE/NeutronInstruments/Coverage/__init__.py +++ b/MDANSE/Src/MDANSE/NeutronInstruments/Coverage/__init__.py @@ -20,23 +20,17 @@ a specific instrument can access. """ -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module( "." + name, "MDANSE.NeutronInstruments.Coverage" diff --git a/MDANSE/Src/MDANSE/NeutronInstruments/Method/__init__.py b/MDANSE/Src/MDANSE/NeutronInstruments/Method/__init__.py index b326884607..30a570b634 100644 --- a/MDANSE/Src/MDANSE/NeutronInstruments/Method/__init__.py +++ b/MDANSE/Src/MDANSE/NeutronInstruments/Method/__init__.py @@ -26,23 +26,17 @@ direct and indirect spectrometry.""" -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module( "." + name, "MDANSE.NeutronInstruments.Method" diff --git a/MDANSE/Src/MDANSE/NeutronInstruments/Resolution/__init__.py b/MDANSE/Src/MDANSE/NeutronInstruments/Resolution/__init__.py index b7b0c32c4a..c01f68581c 100644 --- a/MDANSE/Src/MDANSE/NeutronInstruments/Resolution/__init__.py +++ b/MDANSE/Src/MDANSE/NeutronInstruments/Resolution/__init__.py @@ -23,23 +23,17 @@ resolution will depend on the source-sample and sample-detector distances, the chopper speeds, and the Ei/Ef ratio.""" -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module( "." + name, "MDANSE.NeutronInstruments.Resolution" diff --git a/MDANSE/Src/MDANSE/NeutronInstruments/Spectrum/__init__.py b/MDANSE/Src/MDANSE/NeutronInstruments/Spectrum/__init__.py index a80b96f6a0..89cf30be3d 100644 --- a/MDANSE/Src/MDANSE/NeutronInstruments/Spectrum/__init__.py +++ b/MDANSE/Src/MDANSE/NeutronInstruments/Spectrum/__init__.py @@ -22,23 +22,17 @@ different neutron wavelength to the total observed scattering signal. """ -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module( "." + name, "MDANSE.NeutronInstruments.Spectrum" diff --git a/MDANSE/Src/MDANSE/NeutronInstruments/__init__.py b/MDANSE/Src/MDANSE/NeutronInstruments/__init__.py index 5db818526e..f25ce9821f 100644 --- a/MDANSE/Src/MDANSE/NeutronInstruments/__init__.py +++ b/MDANSE/Src/MDANSE/NeutronInstruments/__init__.py @@ -31,23 +31,17 @@ effects in the calculation. """ -import glob import importlib -import os +from pathlib import Path -current_path, _ = os.path.split(__file__) +current_path = Path(__file__).parent -modnames = [] -fnames = glob.glob(current_path + "/*.py") -for fname in fnames: - _, newname = os.path.split(fname) - newname = newname.split(".py")[0] - modnames.append(newname) +modnames = ( + fname.stem for fname in current_path.glob("*.py") if fname.stem != "__init__" +) globdict = globals() for name in modnames: - if name in ["__init__"]: - continue try: tempmod = importlib.import_module("." + name, "MDANSE.NeutronInstruments") except ModuleNotFoundError: diff --git a/MDANSE/Src/MDANSE/Scripts/mdanse.py b/MDANSE/Src/MDANSE/Scripts/mdanse.py index f24110110a..cdb13c6ef3 100644 --- a/MDANSE/Src/MDANSE/Scripts/mdanse.py +++ b/MDANSE/Src/MDANSE/Scripts/mdanse.py @@ -14,12 +14,11 @@ # along with this program. If not, see . # import pickle -import glob import optparse -import os import subprocess import sys import textwrap +from pathlib import Path from MDANSE.Core.Error import Error from MDANSE import PLATFORM @@ -118,9 +117,9 @@ def check_job(self, option, opt_str, value, parser): basename = parser.rargs[0] - filename = os.path.join(PLATFORM.temporary_files_directory(), basename) + filename = PLATFORM.temporary_files_directory() / basename - if not os.path.exists(filename): + if not filename.exists(): raise CommandLineParserError("Invalid job name") # Open the job temporary file @@ -183,17 +182,13 @@ def display_jobs_list(self, option, opt_str, value, parser): "Invalid number of arguments for %r option" % opt_str ) - jobs = [ - f - for f in glob.glob(os.path.join(PLATFORM.temporary_files_directory(), "*")) - ] + jobs = PLATFORM.temporary_files_directory().glob("*") for j in jobs: # Open the job temporary file try: - f = open(j, "rb") - info = pickle.load(f) - f.close() + with j.open("rb") as f: + info = pickle.load(f) # If the file could not be opened/unpickled for whatever reason, try at the next checkpoint except: @@ -205,7 +200,7 @@ def display_jobs_list(self, option, opt_str, value, parser): if not isinstance(info, JobState): continue - LOG.info("%-20s [%s]" % (os.path.basename(j), info["state"])) + LOG.info("%-20s [%s]", j.stem, info["state"]) def display_trajectory_contents(self, option, opt_str, value, parser): """Displays trajectory contents @@ -283,9 +278,9 @@ def run_job(self, option, opt_str, value, parser): "Invalid number of arguments for %r option" % opt_str ) - filename = parser.rargs[0] + filename = Path(parser.rargs[0]) - if not os.path.exists(filename): + if not filename.exists(): raise CommandLineParserError( "The job file %r could not be executed" % filename ) @@ -319,7 +314,7 @@ def save_job(self, option, opt_str, value, parser): name = parser.rargs[0] # A name for the template is built. - filename = os.path.abspath("template_%s.py" % name.lower()) + filename = Path(f"template_{name.lower()}.py").absolute() # Try to save the template for the job. try: diff --git a/MDANSE/Src/MDANSE/Trajectory/H5MDTrajectory.py b/MDANSE/Src/MDANSE/Trajectory/H5MDTrajectory.py index 2684e54bfe..8243ca0816 100644 --- a/MDANSE/Src/MDANSE/Trajectory/H5MDTrajectory.py +++ b/MDANSE/Src/MDANSE/Trajectory/H5MDTrajectory.py @@ -14,8 +14,8 @@ # along with this program. If not, see . # -from typing import List -import os +from pathlib import Path +from typing import Union, List import numpy as np import h5py @@ -42,14 +42,14 @@ class H5MDTrajectory: H5MD files created by MDMC. """ - def __init__(self, h5_filename): + def __init__(self, h5_filename: Union[Path, str]): """Constructor. :param h5_filename: the trajectory filename :type h5_filename: str """ - self._h5_filename = h5_filename + self._h5_filename = Path(h5_filename) self._h5_file = h5py.File(self._h5_filename, "r") @@ -60,10 +60,8 @@ def __init__(self, h5_filename): ] except KeyError: chemical_elements = self._h5_file["/particles/all/species"] - self._chemical_system = ChemicalSystem( - os.path.splitext(os.path.basename(self._h5_filename))[0] - ) - self._chemical_system.initialise_atoms(chemical_elements) + self._chemical_system = ChemicalSystem(self._h5_filename.stem) + self._chemical_system.from_element_list(chemical_elements) # Load all the unit cells self._load_unit_cells() diff --git a/MDANSE/Src/MDANSE/Trajectory/MdanseTrajectory.py b/MDANSE/Src/MDANSE/Trajectory/MdanseTrajectory.py index eab9d9dd8e..f0554977fa 100644 --- a/MDANSE/Src/MDANSE/Trajectory/MdanseTrajectory.py +++ b/MDANSE/Src/MDANSE/Trajectory/MdanseTrajectory.py @@ -13,9 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import os -from typing import List -import traceback +from pathlib import Path +from typing import List, Union import numpy as np import h5py @@ -41,28 +40,27 @@ class MdanseTrajectory: is the original implementation of the Mdanse HDF5 format. """ - def __init__(self, h5_filename): + def __init__(self, h5_filename: Union[Path, str]): """Constructor. :param h5_filename: the trajectory filename :type h5_filename: str """ - self._h5_filename = h5_filename + self._h5_filename = Path(h5_filename) self._h5_file = h5py.File(self._h5_filename, "r") # Load the chemical system - self._chemical_system = ChemicalSystem( - os.path.splitext(os.path.basename(self._h5_filename))[0], self - ) - self._chemical_system.load(self._h5_file) + self._chemical_system = ChemicalSystem(self._h5_filename.stem, self) + self._chemical_system.load(self._h5_filename) # Load all the unit cells self._load_unit_cells() @classmethod - def file_is_right(self, filename): + def file_is_right(self, filename: Union[Path, str]): + filename = Path(filename) result = True try: file_object = h5py.File(filename) @@ -70,10 +68,8 @@ def file_is_right(self, filename): result = False else: try: - temp_cs = ChemicalSystem( - os.path.splitext(os.path.basename(filename))[0] - ) - temp_cs.load(file_object) + temp_cs = ChemicalSystem(filename.stem) + temp_cs.load(filename) except Exception: LOG.warning( f"Could not load ChemicalSystem from {filename}. MDANSE will try to read it as H5MD next." diff --git a/MDANSE/Tests/UnitTests/test_databases.py b/MDANSE/Tests/UnitTests/test_databases.py index c508e255a6..4b10ac15d0 100644 --- a/MDANSE/Tests/UnitTests/test_databases.py +++ b/MDANSE/Tests/UnitTests/test_databases.py @@ -14,7 +14,6 @@ # along with this program. If not, see . # import json -import os import unittest from unittest.mock import patch, mock_open, ANY @@ -86,7 +85,7 @@ def test__load_default_database(self): "atoms": {"H": {"family": "non-metal"}}, } ), - ) as m: + ) as _m: ATOMS_DATABASE._load() self.assertDictEqual({"family": "str"}, ATOMS_DATABASE._properties) self.assertDictEqual({"H": {"family": "non-metal"}}, ATOMS_DATABASE._data) @@ -102,7 +101,7 @@ def test__load_user_database(self): } ), ) as m: - with patch("os.path.exists", spec=True): + with patch("Path.exists", spec=True): ATOMS_DATABASE._load("user.json") m.assert_called_with("user.json", "r") self.assertDictEqual({"family": "str"}, ATOMS_DATABASE._properties) @@ -126,7 +125,7 @@ def test___getitem__(self): ATOMS_DATABASE["H"], ) with self.assertRaises(AtomsDatabaseError): - a = ATOMS_DATABASE["INVALID"] + _a = ATOMS_DATABASE["INVALID"] def test___iter__(self): generator = iter(ATOMS_DATABASE) From 72442d08296db1be2b0a34cd78505e64d4dbe1cb Mon Sep 17 00:00:00 2001 From: Jacob Wilkins Date: Wed, 29 Jan 2025 11:24:08 +0000 Subject: [PATCH 2/3] Actually use name set/get --- MDANSE/Src/MDANSE/Chemistry/ChemicalSystem.py | 12 ++++++------ MDANSE/Src/MDANSE/Core/Platform.py | 2 +- MDANSE/Src/MDANSE/Framework/Converters/Gromacs.py | 14 ++++++-------- MDANSE/Src/MDANSE/Framework/Formats/TextFormat.py | 3 ++- MDANSE/Src/MDANSE/MolecularDynamics/Trajectory.py | 8 ++++---- MDANSE/Src/MDANSE/Trajectory/H5MDTrajectory.py | 2 +- MDANSE/Src/MDANSE/Trajectory/MdanseTrajectory.py | 4 ++-- MDANSE/Tests/UnitTests/test_databases.py | 5 +++-- 8 files changed, 25 insertions(+), 25 deletions(-) diff --git a/MDANSE/Src/MDANSE/Chemistry/ChemicalSystem.py b/MDANSE/Src/MDANSE/Chemistry/ChemicalSystem.py index d411393640..9b8348f913 100644 --- a/MDANSE/Src/MDANSE/Chemistry/ChemicalSystem.py +++ b/MDANSE/Src/MDANSE/Chemistry/ChemicalSystem.py @@ -36,7 +36,7 @@ def __init__(self, name: str = "", trajectory=None): :type name: str """ - self._name = name + self.name = str(name) self._database = ATOMS_DATABASE if trajectory is not None: self._database = trajectory @@ -55,7 +55,7 @@ def __init__(self, name: str = "", trajectory=None): self._unique_elements = set() def __str__(self): - return f"ChemicalSystem {self._name} consisting of {len(self._atom_types)} atoms in {len(self._clusters)} molecules" + return f"ChemicalSystem {self.name} consisting of {len(self._atom_types)} atoms in {len(self._clusters)} molecules" def initialise_atoms(self, element_list: List[str], name_list: List[str] = None): self._atom_indices = [ @@ -191,7 +191,7 @@ def copy(self) -> "ChemicalSystem": :return: Copy of the ChemicalSystem instance :rtype: MDANSE.Chemistry.ChemicalSystem.ChemicalSystem """ - cs = ChemicalSystem(self._name) + cs = ChemicalSystem(self.name) for attribute_name, attribute_value in self.__dict__.items(): if attribute_name in ["rdkit_mol", "_configuration"]: @@ -248,7 +248,7 @@ def serialize(self, h5_file: h5py.File) -> None: string_dt = h5py.special_dtype(vlen=str) grp = h5_file.create_group("/composition") - grp.attrs["name"] = self._name + grp.attrs["name"] = self.name try: grp.create_dataset("atom_types", data=self._atom_types, dtype=string_dt) @@ -292,7 +292,7 @@ def load(self, trajectory: str): self.rdkit_mol = Chem.RWMol() grp = source["/composition"] - self._name = grp.attrs["name"] + self.name = grp.attrs["name"] atom_types = [binary.decode("utf-8") for binary in grp["atom_types"][:]] atom_names = None @@ -326,7 +326,7 @@ def legacy_load(self, trajectory: str): self.rdkit_mol = Chem.RWMol() grp = source["/chemical_system"] - self._name = grp.attrs["name"] + self.name = grp.attrs["name"] atoms = grp["atoms"] element_list = [line[0].decode("utf-8").strip("'") for line in atoms] self.initialise_atoms(element_list) diff --git a/MDANSE/Src/MDANSE/Core/Platform.py b/MDANSE/Src/MDANSE/Core/Platform.py index e00316fc6b..96077c3fd3 100644 --- a/MDANSE/Src/MDANSE/Core/Platform.py +++ b/MDANSE/Src/MDANSE/Core/Platform.py @@ -465,7 +465,7 @@ def application_directory(self) -> Path: basedir = Path(os.environ["APPDATA"]) / "mdanse" # If the application directory does not exist, create it. - os.makedirs(basedir) + basedir.mkdir(parents=True, exist_ok=True) return basedir diff --git a/MDANSE/Src/MDANSE/Framework/Converters/Gromacs.py b/MDANSE/Src/MDANSE/Framework/Converters/Gromacs.py index 951ac123f6..a01cac5320 100644 --- a/MDANSE/Src/MDANSE/Framework/Converters/Gromacs.py +++ b/MDANSE/Src/MDANSE/Framework/Converters/Gromacs.py @@ -15,6 +15,7 @@ # import collections +from pathlib import Path import numpy as np from mdtraj.formats.xtc import XTCTrajectoryFile @@ -77,16 +78,13 @@ def initialize(self): data_to_be_written = ["configuration", "time"] + filename = Path(self.configuration["xtc_file"]["filename"]) # Create XTC or TRR object depending on which kind of trajectory was loaded - if self.configuration["xtc_file"]["filename"].suffix == ".xtc": - self._xdr_file = XTCTrajectoryFile( - self.configuration["xtc_file"]["filename"], "r" - ) + if filename.suffix == ".xtc": + self._xdr_file = XTCTrajectoryFile(bytes(filename), "r") self._xtc = True - elif self.configuration["xtc_file"]["filename"].suffix == ".trr": - self._xdr_file = TRRTrajectoryFile( - self.configuration["xtc_file"]["filename"], "r" - ) + elif filename.suffix == ".trr": + self._xdr_file = TRRTrajectoryFile(bytes(filename), "r") self._xtc = False # Extract information about whether velocities and forces are present in the TRR file diff --git a/MDANSE/Src/MDANSE/Framework/Formats/TextFormat.py b/MDANSE/Src/MDANSE/Framework/Formats/TextFormat.py index 7c259740f2..e355f733c5 100644 --- a/MDANSE/Src/MDANSE/Framework/Formats/TextFormat.py +++ b/MDANSE/Src/MDANSE/Framework/Formats/TextFormat.py @@ -67,7 +67,8 @@ def write( :type header: str """ - filename = Path(filename).with_suffix(".tar") + filename = Path(filename) + filename = filename.parent / (filename.stem + "_text.tar") PLATFORM.create_directory(filename.parent) tf = tarfile.open(filename, "w") diff --git a/MDANSE/Src/MDANSE/MolecularDynamics/Trajectory.py b/MDANSE/Src/MDANSE/MolecularDynamics/Trajectory.py index 839f94d94d..9cc8a5b725 100644 --- a/MDANSE/Src/MDANSE/MolecularDynamics/Trajectory.py +++ b/MDANSE/Src/MDANSE/MolecularDynamics/Trajectory.py @@ -13,16 +13,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from typing import TYPE_CHECKING, Any, Dict, List +import operator +from typing import TYPE_CHECKING, Any, Collection, Dict, List, Union if TYPE_CHECKING: from MDANSE.Chemistry.Databases import AtomsDatabase import math -import operator -from collections.abc import Collection from pathlib import Path -from typing import Union import h5py import numpy as np @@ -30,6 +28,8 @@ from MDANSE.Chemistry import ATOMS_DATABASE from MDANSE.Chemistry.ChemicalSystem import ChemicalSystem from MDANSE.MolecularDynamics.Configuration import RealConfiguration +from MDANSE.Trajectory.H5MDTrajectory import H5MDTrajectory +from MDANSE.Trajectory.MdanseTrajectory import MdanseTrajectory available_formats = { "MDANSE": MdanseTrajectory, diff --git a/MDANSE/Src/MDANSE/Trajectory/H5MDTrajectory.py b/MDANSE/Src/MDANSE/Trajectory/H5MDTrajectory.py index 8243ca0816..02aa55b937 100644 --- a/MDANSE/Src/MDANSE/Trajectory/H5MDTrajectory.py +++ b/MDANSE/Src/MDANSE/Trajectory/H5MDTrajectory.py @@ -61,7 +61,7 @@ def __init__(self, h5_filename: Union[Path, str]): except KeyError: chemical_elements = self._h5_file["/particles/all/species"] self._chemical_system = ChemicalSystem(self._h5_filename.stem) - self._chemical_system.from_element_list(chemical_elements) + self._chemical_system.initialise_atoms(chemical_elements) # Load all the unit cells self._load_unit_cells() diff --git a/MDANSE/Src/MDANSE/Trajectory/MdanseTrajectory.py b/MDANSE/Src/MDANSE/Trajectory/MdanseTrajectory.py index f0554977fa..b2641d9b56 100644 --- a/MDANSE/Src/MDANSE/Trajectory/MdanseTrajectory.py +++ b/MDANSE/Src/MDANSE/Trajectory/MdanseTrajectory.py @@ -53,7 +53,7 @@ def __init__(self, h5_filename: Union[Path, str]): # Load the chemical system self._chemical_system = ChemicalSystem(self._h5_filename.stem, self) - self._chemical_system.load(self._h5_filename) + self._chemical_system.load(self._h5_file) # Load all the unit cells self._load_unit_cells() @@ -69,7 +69,7 @@ def file_is_right(self, filename: Union[Path, str]): else: try: temp_cs = ChemicalSystem(filename.stem) - temp_cs.load(filename) + temp_cs.load(file_object) except Exception: LOG.warning( f"Could not load ChemicalSystem from {filename}. MDANSE will try to read it as H5MD next." diff --git a/MDANSE/Tests/UnitTests/test_databases.py b/MDANSE/Tests/UnitTests/test_databases.py index 4b10ac15d0..c384d2b156 100644 --- a/MDANSE/Tests/UnitTests/test_databases.py +++ b/MDANSE/Tests/UnitTests/test_databases.py @@ -14,6 +14,7 @@ # along with this program. If not, see . # import json +from pathlib import Path import unittest from unittest.mock import patch, mock_open, ANY @@ -101,9 +102,9 @@ def test__load_user_database(self): } ), ) as m: - with patch("Path.exists", spec=True): + with patch("pathlib.Path.exists", spec=True): ATOMS_DATABASE._load("user.json") - m.assert_called_with("user.json", "r") + m.assert_called_with(Path("user.json"), "r") self.assertDictEqual({"family": "str"}, ATOMS_DATABASE._properties) self.assertDictEqual( {"H": {"family": "non-metal"}}, ATOMS_DATABASE._data From bb87ec06dc8d0974c026c66d9b2bf1dc04d0aecb Mon Sep 17 00:00:00 2001 From: Jacob Wilkins Date: Tue, 4 Feb 2025 17:01:24 +0000 Subject: [PATCH 3/3] Add custom JSONEncoder to handle Paths --- .../Framework/Configurators/IConfigurator.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/MDANSE/Src/MDANSE/Framework/Configurators/IConfigurator.py b/MDANSE/Src/MDANSE/Framework/Configurators/IConfigurator.py index c77ea5c795..9372e7be0f 100644 --- a/MDANSE/Src/MDANSE/Framework/Configurators/IConfigurator.py +++ b/MDANSE/Src/MDANSE/Framework/Configurators/IConfigurator.py @@ -16,12 +16,22 @@ import abc import json +from pathlib import Path from MDANSE.Core.Error import Error from MDANSE.Core.SubclassFactory import SubclassFactory +class CustomEncoder(json.JSONEncoder): + """Custom JSON encoder to encode paths as strings.""" + + def default(self, obj): + if isinstance(obj, Path): + return str(obj) + return super().default(obj) + + class ConfiguratorError(Error): """ This class handles any exception related to Configurator-derived object @@ -82,7 +92,7 @@ class IConfigurator(dict, metaclass=SubclassFactory): _default = None - _encoder = json.encoder.JSONEncoder() + _encoder = CustomEncoder() _decoder = json.decoder.JSONDecoder() _doc_ = "undocumented" @@ -90,12 +100,12 @@ class IConfigurator(dict, metaclass=SubclassFactory): def __init__(self, name, **kwargs): """ Initializes a configurator object. - + :param name: the name of this configurator. :type name: str :param dependencies: the other configurators on which this configurator depends on to be configured. \ This has to be input as a dictionary that maps the name under which the dependency will be used within \ - the configurator implementation to the actual name of the configurator on which this configurator is depending on. + the configurator implementation to the actual name of the configurator on which this configurator is depending on. :type dependencies: (str,str)-dict :param default: the default value of this configurator. :type default: any python object