diff --git a/README.md b/README.md index 8c28521..e3782d1 100644 --- a/README.md +++ b/README.md @@ -91,46 +91,66 @@ dyn2py --filter py --backup path/to/pythonfiles #### Git hooks -Git Hooks are a built-in feature of Git that allow developers to automate tasks throughout the Git workflow. Read more here: https://githooks.com/ +Git hooks are a built-in feature of Git that allow developers to automate tasks throughout the Git workflow. Read more here: https://githooks.com/ With the `pre-commit` hook it's possible to add more files to the currently initialized commit. -You can find an example pre-commit hook here: [pre-commit](pre-commit). Copy this file to the `.git/hooks` folder of your repo of Dynamo graph. This folder is hidden by default, but it should exist in all initialized git repo. Do not rename this file. +You can find an example pre-commit hook here: [pre-commit](pre-commit). Copy this file to the `.git/hooks` folder of your repo of Dynamo graphs. This folder is hidden by default, but it should exist in all initialized git repo. Do not rename this file. -This script will go through staged `.dyn` files and export python scripts from them, and add them to the current commit. Now you can check check changed lines in a diff tool! +This script will go through staged `.dyn` files and export python scripts from them, and add them to the current commit. Now you can check changed lines in a diff tool, you can see changed python code in a PR! ### As a python module Full API documentation available here: https://infeeeee.github.io/dyn2py -Most basic example to extract all nodes next to a Dynamo file: +#### Examples + +Extract all nodes from python nodes next to a Dynamo file: ```python import dyn2py +# Open a Dynamo graph: dynamo_file = dyn2py.DynamoFile("path/to/dynamofile.dyn") -python_files = dynamo_file.extract_python() -[python_file.write() for python_file in python_files] + +# Extract python nodes: +dynamo_file.extract_python() + +# Save all python nodes as separate files: +dyn2py.PythonFile.write_open_files() ``` -Change options like with the command line switches with the `Options` class: +Update python node from a python file: ```python import dyn2py -# Create a backup on overwrite, read python files from a different folder: -options = dyn2py.Options( - backup=True, - python_folder="path/to/pythonfiles") +# Open a python file: +python_file = dyn2py.PythonFile("path/to/pythonfile.py") +# Update the node in the graph: +python_file.update_dynamo() + +# Save modified Dynamo graph: +dyn2py.DynamoFile.write_open_files() +``` + +Update all python nodes of a Dynamo grapg from a different folder, save backups: + +```python +import dyn2py + +# open a Dynamo graph: dynamo_file = dyn2py.DynamoFile("path/to/dynamofile.dyn") -python_files = dynamo_file.get_related_python_files(options) + +# Get python files from a dofferent folder: +python_files = dynamo_file.get_related_python_files(python_folder="path/to/pythonfiles") # Read python files and update the graph: -[python_file.update_dynamo(options) for python_file in python_files] +[python_file.update_dynamo() for python_file in python_files] -# Don't forget to save at the end: -dynamo_file.write(options) +# Save open Dynamo graphs: +dyn2py.DynamoFile.write_open_files(backup=True) ``` For more examples check tests in the [tests folder on Github](https://github.com/infeeeee/dyn2py/tree/main/tests) @@ -149,6 +169,8 @@ Only supports Dynamo 2 files, Dynamo 1 files are reported and ignored. Please up Both IronPython2 and CPython3 nodes are supported! IronPython2 nodes won't be updated to CPython3, they will be imported as-is. +Cannot create new python nodes, only existing ones can be updated. + ## Development ### Installation @@ -166,7 +188,7 @@ With venv: ``` git clone https://github.com/infeeeee/dyn2py cd dyn2py -venv .venv +python -m venv .venv . ./.venv/bin/activate pip install -e . ``` @@ -204,7 +226,7 @@ python -m unittest discover -v -s ./tests -p "test_*.py" ### New release 1. Update version number in `pyproject.toml` -2. Create a publish a git tag with that number +2. Create and publish a git tag with that number ## License diff --git a/TODO.md b/TODO.md index 064a968..d91efa6 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,7 @@ - [x] DynamoFile - [x] PythonFile - [x] PythonNode +- [ ] Options - [ ] run() ## CI/CD diff --git a/dyn2py/__init__.py b/dyn2py/__init__.py index bf7bc8c..310a4e2 100644 --- a/dyn2py/__init__.py +++ b/dyn2py/__init__.py @@ -22,11 +22,7 @@ __all__ = [ "File", "DynamoFile", "PythonFile", - "PythonNode", - "DynamoFileException", - "PythonNodeNotFoundException", - "PythonNodeException", - "PythonFileException" + "PythonNode" ] @@ -155,13 +151,13 @@ def run(options: Options) -> None: for f in source_files: try: files.append(File(f)) - except DynamoFileException as e: + except DynamoFile.Error as e: # It's a dynamo1 file - logging.warning(e) + logging.warning(f"This is a Dynamo 1 file! {e.file.filepath}") continue - except PythonNodeNotFoundException as e: - # No python node in this file - logging.warning(e) + except DynamoFile.PythonNodeNotFound as e: + # No python nodes in this file + logging.warning(f"This file has no Python nodes! {e.file.filepath} ") continue # Dynamo files come first, sort sources: @@ -202,11 +198,10 @@ def run(options: Options) -> None: try: f.update_dynamo(options) except FileNotFoundError: - logging.error(f"Source Dynamo file not found! {f.filepath}") + logging.error(f"{f.filepath} Source Dynamo file not found! ") # Write files at the end: - for f in DynamoFile.open_files | PythonFile.open_files: - try: - f.write(options) - except FileNotFoundError: - logging.error(f"Cannot save file! {f.filepath}") + try: + File.write_open_files(options) + except File.Error as e: + logging.error(f"Cannot save file! {e.file.filepath}") diff --git a/dyn2py/exceptions.py b/dyn2py/exceptions.py deleted file mode 100644 index 148d420..0000000 --- a/dyn2py/exceptions.py +++ /dev/null @@ -1,16 +0,0 @@ -class DynamoFileException(Exception): - """Something wrong in this DynamoFile""" - pass - - -class PythonNodeNotFoundException(Exception): - """PythonNode not found""" - pass - - -class PythonNodeException(Exception): - pass - - -class PythonFileException(Exception): - pass diff --git a/dyn2py/files.py b/dyn2py/files.py index 4990af0..6eb7715 100644 --- a/dyn2py/files.py +++ b/dyn2py/files.py @@ -9,7 +9,6 @@ from decimal import Decimal from pathvalidate import sanitize_filename from importlib_metadata import metadata -from dyn2py.exceptions import * from dyn2py.options import Options @@ -20,6 +19,9 @@ HEADER_SEPARATOR = "*" * 60 class File(): """Base class for managing files""" + open_files: set[File] = set() + """A set of open files.""" + def __init__(self, filepath: pathlib.Path | str, read_from_disk: bool = True) -> None: """Generate a file object. If the path is correct it will become a DynamoFile or PythonFile object. Calls DynamoFile.read_file() and PythonFile.read_file() @@ -110,21 +112,26 @@ class File(): """ return bool(self.extension == ".py") - def write(self, options: Options | None = None) -> None: + def write(self, options: Options | None = None, **option_args) -> None: """Prepare writing file to the disk: create backup, process dry-run, call filetype specific write_file() methods Should be called on subclasses! Args: options (Options | None, optional): Run options. Defaults to None. + **option_args: Options() arguments Raises: + ValueError: Both options and other arguments given TypeError: If called on a File object - FileNotFoundError: Target folder does not exist + File.Error: Target folder does not exist """ if not options: - options = Options() + options = Options.from_kwargs(kwargs=option_args) + elif option_args: + # Should not give both options and arguments: + raise ValueError("Options object and extra arguments!") # This should only work on subclasses: if type(self).__name__ == "File": @@ -150,7 +157,7 @@ class File(): f"Should write file, but it's a dry-run: {self.filepath}") else: if not self.dirpath.exists(): - raise FileNotFoundError("File dir does not exist!") + raise File.Error("File dir does not exist!", self) logging.info(f"Writing file: {self.filepath}") self._write_file() if options.loglevel == "HEADLESS": @@ -165,6 +172,53 @@ class File(): raise NotImplementedError( "Should be called only on DynamoFile and PythonFile objects!") + @classmethod + def get_open_files(cls) -> set: + """Get open files of this class and subclasses + + Returns: + set: A set of open files + """ + return {f for f in File.open_files if + isinstance(f, cls)} + + @classmethod + def write_open_files(cls, options: Options | None = None, **option_args) -> None: + """Write open files of this class and subclasses + + Args: + options (Options | None, optional): Run options. Defaults to None. + **option_args: Options() arguments + + Raises: + ValueError: Both options and other arguments given + + """ + if not options: + options = Options.from_kwargs(kwargs=option_args) + elif option_args: + # Should not give both options and arguments: + raise ValueError("Options object and extra arguments!") + + for f in cls.get_open_files(): + f.write(options) + + @classmethod + def close_open_files(cls) -> None: + """Close open files of this class and subclasses""" + File.open_files = File.open_files - cls.get_open_files() + + class Error(Exception): + def __init__(self, message: str, file: File) -> None: + """There is some problem with this file + + Args: + message (str): The message to display + file (File): The problem File + """ + super().__init__(message) + self.file = file + class DynamoFile(File): """A Dynamo file, subclass of File()""" @@ -178,21 +232,25 @@ class DynamoFile(File): python_nodes: set[PythonNode] """Python node objects, read from this file.""" - open_files: set[DynamoFile] = set() - """A set of open Dynamo files, before saving. Self added by read()""" - - def extract_python(self, options: Options | None = None) -> list[PythonFile]: + def extract_python(self, options: Options | None = None, **option_args) -> list[PythonFile]: """Extract python files from Dynamo graphs, add them to open_files Args: options (Options | None, optional): Run options. Defaults to None. + **option_args: Options() arguments + + Raises: + ValueError: Both options and other arguments given Returns: list[PythonFile]: List of PythonFile objects extracted from this DynamoFile """ if not options: - options = Options() + options = Options.from_kwargs(kwargs=option_args) + elif option_args: + # Should not give both options and arguments: + raise ValueError("Options object and extra arguments!") logging.info(f"Extracting from file: {self.filepath}") python_files = [] @@ -229,9 +287,9 @@ class DynamoFile(File): Raises: FileNotFoundError: The file does not exist - DynamoFileException: If the file is a Dynamo 1 file + DynamoFile.Error: If the file is a Dynamo 1 file json.JSONDecodeError: If there are any other problem with the file - PythonNodeNotFoundException: No python nodes in the file + DynamoFile.PythonNodeNotFound: No python nodes in the file """ if not self.exists: @@ -249,7 +307,7 @@ class DynamoFile(File): except json.JSONDecodeError as e: with open(self.filepath, "r", encoding="utf-8") as input_json: if input_json.readline().startswith(" list[PythonFile]: + def get_related_python_files(self, options: Options | None = None, **option_args) -> list[PythonFile]: """Get python files exported from this Dynamo file Args: options (Options | None, optional): Run options. Defaults to None. + **option_args: Options() arguments + + Raises: + ValueError: Both options and other arguments given Returns: list[PythonFile]: A list of PythonFile objects """ if not options: - options = Options() + options = Options.from_kwargs(kwargs=option_args) + elif option_args: + # Should not give both options and arguments: + raise ValueError("Options object and extra arguments!") # Find the folder of the python files if options.python_folder: @@ -368,11 +434,24 @@ class DynamoFile(File): Returns: DynamoFile: The file. None if not found """ - f = next((d for d in DynamoFile.open_files if d.uuid == uuid), None) + f = next((d for d in DynamoFile.get_open_files() if d.uuid == uuid), None) if f: logging.debug(f"Found open file {f.uuid}") return f + class PythonNodeNotFound(Exception): + def __init__(self, message: str, file: DynamoFile, node_id: str) -> None: + """Python node not found with this id + + Args: + message (str): The message to display + file (DynamoFile): The problem DynamoFile + node_id (str): The missing id + """ + super().__init__(message) + self.file = file + self.node_id = node_id + class PythonFile(File): """A Python file, subclass of File()""" @@ -384,9 +463,6 @@ class PythonFile(File): text: str """Full contents of the file.""" - open_files: set[PythonFile] = set() - """A set of open Python files.""" - def __init__(self, filepath: pathlib.Path | str, dynamo_file: DynamoFile | None = None, @@ -411,7 +487,8 @@ class PythonFile(File): ]) # Calculate relative path, change to forward slash - dyn_path_string = os.path.relpath(dynamo_file.filepath, self.dirpath) + dyn_path_string = os.path.relpath( + dynamo_file.filepath, self.dirpath) if "\\" in dyn_path_string: dyn_path_string = dyn_path_string.replace("\\", "/") @@ -458,7 +535,7 @@ class PythonFile(File): Raises: FileNotFoundError: The file does not exist - PythonFileException: Some error reading the file + PythonFile.Error: Some error reading the file """ if not self.exists: raise FileNotFoundError @@ -493,7 +570,7 @@ class PythonFile(File): # Find the location of the separator sl = line.find(":") if sl == -1: - raise PythonFileException("Error reading header!") + raise self.Error("Error reading header!", self) self.header_data[line[0:sl]] = line[sl+1:] self.code = python_lines[code_start_line:] @@ -502,15 +579,23 @@ class PythonFile(File): logging.debug(f"Header data from python file: {self.header_data}") # logging.debug(f"Code from python file: {self.code}") - def update_dynamo(self, options: Options | None = None) -> None: + def update_dynamo(self, options: Options | None = None, **option_args) -> None: """Update a the source Dynamo graph from this python script Args: options (Options | None, optional): Run options. Defaults to None. + **option_args: Options() arguments + + Raises: + ValueError: Both options and other arguments given + """ if not options: - options = Options() + options = Options.from_kwargs(kwargs=option_args) + elif option_args: + # Should not give both options and arguments: + raise ValueError("Options object and extra arguments!") dynamo_file = self.get_source_dynamo_file() @@ -535,7 +620,7 @@ class PythonFile(File): """Get the source Dynamo file of this PythonFile Raises: - DynamoFileException: The uuid of the dynamo file changed + DynamoFile.Error: The uuid of the dynamo file changed Returns: DynamoFile: The DynamoFile @@ -551,17 +636,18 @@ class PythonFile(File): cwd = pathlib.Path(os.getcwd()).resolve() # Change to pythonfiles' dir: os.chdir(self.dirpath) - + dynpath = os.path.realpath(self.header_data["dyn_path"]) logging.debug(f"Resolved path: {dynpath}") - + # Change back to the original path: os.chdir(cwd) dynamo_file = DynamoFile(pathlib.Path(dynpath)) # Check if uuid is ok: if not dynamo_file.uuid == self.header_data["dyn_uuid"]: - raise DynamoFileException(f"Dynamo graph uuid changed!") + raise DynamoFile.Error( + "Dynamo graph uuid changed!", dynamo_file) return dynamo_file @@ -602,7 +688,7 @@ class PythonNode(): python_file (PythonFile, optional): The python file to be converted to node. Defaults to None. Raises: - PythonNodeException: Wrong arguments were given + PythonNode.Error: Wrong arguments were given """ # Initialize from dynamo file: if node_dict_from_dyn and dynamo_file and not python_file: @@ -642,9 +728,19 @@ class PythonNode(): self.filename = python_file.basename + ".py" self.filepath = python_file.filepath + elif python_file and node_dict_from_dyn and dynamo_file: + raise self.Error("Too much arguments given!") + + elif not python_file and not node_dict_from_dyn and not dynamo_file: + raise self.Error("No arguments given!") + else: - raise PythonNodeException + raise self.Error("Something wrong!") # Calculate checksum: checksums = [hashlib.md5(l.encode()).hexdigest() for l in self.code] self.checksum = hashlib.md5("".join(checksums).encode()).hexdigest() + + class Error(Exception): + """Something wrong with this node""" + pass diff --git a/dyn2py/options.py b/dyn2py/options.py index ae4355f..81c209a 100644 --- a/dyn2py/options.py +++ b/dyn2py/options.py @@ -26,16 +26,13 @@ class Options(argparse.Namespace): Args: source (list[pathlib.Path | str], optional): List of files to run on. Defaults to []. - loglevel (str, optional): log level. Defaults to DEFAULT_LOGLEVEL. - dry_run (bool, optional): If it's a dry run. Defaults to False. + loglevel (str, optional): Log level. Defaults to DEFAULT_LOGLEVEL. + dry_run (bool, optional): Dry run, do not save files. Defaults to False. force (bool, optional): Overwrite files, even if they are older. Defaults to False. backup (bool, optional): Create backup of modified files. Defaults to False. filter (str, optional): 'dyn' or 'py' file filter for running on folders. Defaults to "". update (bool, optional): Update mode, like inverse on Dynamo files. Defaults to False. python_folder (pathlib.Path | str | None, optional): Path to export python files to, or import from there. Defaults to None. - - Raises: - ValueError: If loglevel or filter is invalid """ self.source = [] @@ -45,23 +42,58 @@ class Options(argparse.Namespace): else: self.source.append(s) - if loglevel.upper() in LOGLEVELS: - self.loglevel = loglevel.upper() - else: - raise ValueError - + self.loglevel = self.sanitize_option_string("loglevel", loglevel) self.dry_run = dry_run self.force = force self.backup = backup - - if not filter or filter in FILTERS: - self.filter = filter - else: - raise ValueError - + self.filter = self.sanitize_option_string("filter", filter) self.update = update if isinstance(python_folder, str): self.python_folder = pathlib.Path(python_folder) else: self.python_folder = python_folder + + @staticmethod + def sanitize_option_string(arg: str, value: str) -> str: + """Sanitize string option values + + Args: + arg (str): The name of the argument + value (str): The value + + Raises: + ValueError: if the value is invalid + + Returns: + str: The correct string + """ + + if arg == "loglevel": + if value.upper() in LOGLEVELS: + sanitized_value = value.upper() + else: + raise ValueError("Invalid loglevel!") + elif arg == "filter": + if not value or value in FILTERS: + sanitized_value = value + else: + raise ValueError("Invalid filter!") + else: + sanitized_value = value + + return sanitized_value + + @classmethod + def from_kwargs(cls, **kwargs) -> Options: + """Initialize an Options object from kwargs + + Returns: + Options: The initialized object + """ + o = cls() + for key, value in kwargs.items(): + if isinstance(value, str): + value = cls.sanitize_option_string(key, value) + setattr(o, key, value) + return o diff --git a/tests/support.py b/tests/support.py index 2599f7e..bb6e72c 100644 --- a/tests/support.py +++ b/tests/support.py @@ -3,15 +3,17 @@ import dyn2py INPUT_DIR = "tests/input_files" OUTPUT_DIR = "tests/output_files" +TEMP_DIR = "tests/temp_files" -def cleanup_output_dir(): - output_dir = pathlib.Path(OUTPUT_DIR) - if output_dir.exists(): - for f in output_dir.iterdir(): - f.unlink() - else: - output_dir.mkdir() +def cleanup_dirs(): + for p in [OUTPUT_DIR, TEMP_DIR]: + the_dir = pathlib.Path(p) + if the_dir.exists(): + for f in the_dir.iterdir(): + f.unlink() + else: + the_dir.mkdir() def extract_single_node_dyn(modify_py: bool = False): @@ -23,7 +25,7 @@ def extract_single_node_dyn(modify_py: bool = False): modify_py (bool, optional): Also do some changes on the exported file. Defaults to False. """ - cleanup_output_dir() + cleanup_dirs() # Extract py: options = dyn2py.Options(python_folder=OUTPUT_DIR) @@ -31,8 +33,7 @@ def extract_single_node_dyn(modify_py: bool = False): pythonfiles = dyn.extract_python(options) pythonfiles[0].write() - dyn2py.PythonFile.open_files.clear() - dyn2py.DynamoFile.open_files.clear() + dyn2py.File.open_files.clear() if modify_py: # Open the extracted file and replace a string: diff --git a/tests/test_CommandLine.py b/tests/test_CommandLine.py index 2247d72..525808b 100644 --- a/tests/test_CommandLine.py +++ b/tests/test_CommandLine.py @@ -36,8 +36,12 @@ class TestCommandLine(unittest.TestCase): self.assertFalse(p.stderr) dyn_sources = [ - {"filename": "python_nodes.dyn", "py_file_count": 6}, - {"filename": "single_node.dyn", "py_file_count": 1} + {"filename": "python_nodes.dyn", "output_file_count": 6}, + {"filename": "single_node.dyn", "output_file_count": 1} + ] + + py_sources = [ + {"filename": "single_node_mod.py"}, ] dyn_sources_error = ["dynamo1file.dyn", @@ -46,6 +50,15 @@ class TestCommandLine(unittest.TestCase): @staticmethod def run_command(args: list = []) -> dict: + """_summary_ + + Args: + args (list, optional): list of arguments to run. Defaults to []. + + Returns: + dict["stderr"]: error message + dict["python_file_mtimes"]: exported file in the output dir, and modification times + """ argstring = " ".join(args) process = subprocess.run(f"dyn2py -l WARNING {argstring}", capture_output=True, shell=True) @@ -76,7 +89,7 @@ class TestCommandLine(unittest.TestCase): test_dicts[0]["filenames"] = [ test_dicts[0]["filename"]] - if i == 0: + if i == 0 and len(source_dict) > 1: # Create a multi file version on the first file: d = {} for key in source_dict: @@ -91,7 +104,7 @@ class TestCommandLine(unittest.TestCase): for test_dict in test_dicts: if pfolder_option: - file_dir = INPUT_DIR + file_dir = TEMP_DIR pfolder_arg = f"{pfolder_option} {OUTPUT_DIR}" else: file_dir = OUTPUT_DIR @@ -113,7 +126,7 @@ class TestCommandLine(unittest.TestCase): def test_dyn_error(self): for s in self.dyn_sources_error: - cleanup_output_dir() + cleanup_dirs() if pathlib.Path(f"{INPUT_DIR}/{s}").exists(): shutil.copy(f"{INPUT_DIR}/{s}", @@ -131,29 +144,40 @@ class TestCommandLine(unittest.TestCase): dyn_tests = self.generate_test_args(self.dyn_sources) for s in dyn_tests: - cleanup_output_dir() + cleanup_dirs() + # if no pythonfolder, everything should be in output ddir if not s["pfolder_arg"]: - for filename in s["filenames"]: - shutil.copy(f"{INPUT_DIR}/{filename}", - f"{OUTPUT_DIR}/{filename}") + source_dir = OUTPUT_DIR + else: + source_dir = TEMP_DIR + # copy source files: + for filename in s["filenames"]: + shutil.copy(f"{INPUT_DIR}/{filename}", + f"{source_dir}/{filename}") + + + # Open files normally file_open = self.run_command( [s["pfolder_arg"], s['filepath']]) + # Open without error: self.assertFalse( bool(file_open["stderr"]), msg=file_open["stderr"]) self.assertEqual( - len(file_open["python_file_mtimes"]), s["py_file_count"]) + len(file_open["python_file_mtimes"]), s["output_file_count"]) # Test no overwrite file_no_overwrite = self.run_command( [s["pfolder_arg"], s['filepath']]) + # Should give error, because they already exist: self.assertTrue(bool(file_no_overwrite["stderr"])) self.assertEqual( - len(file_no_overwrite["python_file_mtimes"]), s["py_file_count"]) + len(file_no_overwrite["python_file_mtimes"]), s["output_file_count"]) + # The modify time shouldn't change as they were not overwritten: for p in file_no_overwrite["python_file_mtimes"]: self.assertEqual( file_no_overwrite["python_file_mtimes"][p], file_open["python_file_mtimes"][p]) @@ -162,10 +186,12 @@ class TestCommandLine(unittest.TestCase): file_force = self.run_command( [s["pfolder_arg"], s["force_arg"], s['filepath']]) + # Should not have an error: self.assertFalse(bool(file_force["stderr"])) self.assertEqual( - len(file_force["python_file_mtimes"]), s["py_file_count"]) + len(file_force["python_file_mtimes"]), s["output_file_count"]) + # Modify time should be higher as they were replaced for p in file_force["python_file_mtimes"]: self.assertTrue( file_force["python_file_mtimes"][p] > file_open["python_file_mtimes"][p] @@ -178,18 +204,52 @@ class TestCommandLine(unittest.TestCase): self.assertFalse(bool(file_backup["stderr"])) self.assertEqual( - len(file_backup["python_file_mtimes"]), s["py_file_count"] * 2) + len(file_backup["python_file_mtimes"] + ), s["output_file_count"] * 2, + msg=f"" + ) for p in file_force["python_file_mtimes"]: self.assertTrue( file_backup["python_file_mtimes"][p] > file_force["python_file_mtimes"][p] ) + # def test_py(self): + # py_tests = self.generate_test_args(self.py_sources) + + # # TODO add more python files! + # self.assertEqual(len(py_tests), 1) + + # for s in py_tests: + # cleanup_dirs() + + # extract_single_node_dyn(modify_py=True) + + + # # if pythonfolder, python should be in the temp folder: + # if s["pfolder_arg"]: + # for filename in s["filenames"]: + # shutil.move(f"{OUTPUT_DIR}/{filename}", + # f"{TEMP_DIR}/{filename}") + + # # Open files normally + # file_open = self.run_command( + # [s["pfolder_arg"], s['filepath']]) + + + # # Open without error: + # self.assertFalse( + # bool(file_open["stderr"]), msg=file_open["stderr"]) + # self.assertEqual( + # len(file_open["python_file_mtimes"]), s["output_file_count"]) + + + def test_single_dyn_dryrun(self): for s in self.dyn_sources: for arg in ["-n", "--dry-run"]: - cleanup_output_dir() + cleanup_dirs() shutil.copy(f"{INPUT_DIR}/{s['filename']}", f"{OUTPUT_DIR}/{s['filename']}") diff --git a/tests/test_DynamoFile.py b/tests/test_DynamoFile.py index 0088beb..3bda1ee 100644 --- a/tests/test_DynamoFile.py +++ b/tests/test_DynamoFile.py @@ -19,7 +19,7 @@ class TestDynamoFile(unittest.TestCase): self.assertTrue(dyn in dyn2py.DynamoFile.open_files) # Dynamo 1 file: - with self.assertRaises(dyn2py.DynamoFileException): + with self.assertRaises(dyn2py.DynamoFile.Error): dyn1 = dyn2py.DynamoFile(f"{INPUT_DIR}/dynamo1file.dyn") # Not existing file: @@ -27,7 +27,7 @@ class TestDynamoFile(unittest.TestCase): dyn2 = dyn2py.DynamoFile(f"{INPUT_DIR}/not_existing.dyn") # No python nodes: - with self.assertRaises(dyn2py.PythonNodeNotFoundException): + with self.assertRaises(dyn2py.DynamoFile.PythonNodeNotFound): dyn2 = dyn2py.DynamoFile(f"{INPUT_DIR}/no_python.dyn") def test_get_python_nodes(self): @@ -39,21 +39,20 @@ class TestDynamoFile(unittest.TestCase): self.assertIn(py_node, dyn.python_nodes) self.assertEqual(py_node.checksum, "e830a6ae6b395bcfd4e5a40da48f3bfc") - with self.assertRaises(dyn2py.PythonNodeNotFoundException): + with self.assertRaises(dyn2py.DynamoFile.PythonNodeNotFound): dyn.get_python_node_by_id("wrongid") def test_extract_python(self): - cleanup_output_dir() + cleanup_dirs() dyn2py.PythonFile.open_files.clear() opt = dyn2py.Options(python_folder=OUTPUT_DIR) dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/python_nodes.dyn") dyn.extract_python(options=opt) - self.assertEqual(len(dyn2py.PythonFile.open_files), 6) + self.assertEqual(len(dyn2py.PythonFile.get_open_files()), 6) - for f in dyn2py.PythonFile.open_files: - f.write() + dyn2py.PythonFile.write_open_files() output_dir = pathlib.Path(OUTPUT_DIR) self.assertEqual(len(list(output_dir.iterdir())), 6) @@ -70,7 +69,7 @@ class TestDynamoFile(unittest.TestCase): dyn2py.DynamoFile.get_open_file_by_uuid("76de5c79-17c5-4c74-9f90-ad99a213d339")) def test_get_related_python_files(self): - cleanup_output_dir() + cleanup_dirs() opt = dyn2py.Options(python_folder=OUTPUT_DIR) dyn1 = dyn2py.DynamoFile(f"{INPUT_DIR}/python_nodes.dyn") @@ -92,7 +91,7 @@ class TestDynamoFile(unittest.TestCase): self.assertFalse(no_python_files) def test_write_same(self): - cleanup_output_dir() + cleanup_dirs() shutil.copy(f"{INPUT_DIR}/python_nodes.dyn", f"{OUTPUT_DIR}/python_nodes.dyn") @@ -139,6 +138,6 @@ class TestDynamoFile(unittest.TestCase): self.assertTrue(node2) self.assertEqual(node1.checksum, node2.checksum) - with self.assertRaises(dyn2py.PythonNodeNotFoundException): + with self.assertRaises(dyn2py.DynamoFile.PythonNodeNotFound): node2.id = "wrong_id" dyn2.update_python_node(node2) diff --git a/tests/test_File.py b/tests/test_File.py index c2f791e..34aeb52 100644 --- a/tests/test_File.py +++ b/tests/test_File.py @@ -8,8 +8,6 @@ from tests.support import * class TestFile(unittest.TestCase): - # Write methods should be tested in subclasses! - def test_init(self): paths = [ f"{INPUT_DIR}/python_nodes.dyn", @@ -71,7 +69,7 @@ class TestFile(unittest.TestCase): nonexisting_file = dyn2py.File(f"{INPUT_DIR}/new_file.py") # Extract a python file so it is always newer than the others: - cleanup_output_dir() + cleanup_dirs() opt = dyn2py.Options(python_folder=OUTPUT_DIR) older_file.extract_python(options=opt) # type: ignore for f in dyn2py.PythonFile.open_files: @@ -103,3 +101,15 @@ class TestFile(unittest.TestCase): self.assertEqual(file.is_dynamo_file(), f == "dyn") self.assertEqual(file.is_python_file(), f == "py") + + def test_write(self): + + # new empty file: + empty_filepath = pathlib.Path(f"{OUTPUT_DIR}/empty.txt") + empty_filepath.touch() + + empty_file = dyn2py.File(f"{OUTPUT_DIR}/empty.txt") + + self.assertTrue(empty_file.exists) + with self.assertRaises(TypeError): + empty_file.write() \ No newline at end of file diff --git a/tests/test_PythonFile.py b/tests/test_PythonFile.py index 89ece01..810cc34 100644 --- a/tests/test_PythonFile.py +++ b/tests/test_PythonFile.py @@ -12,9 +12,10 @@ class TestPythonFile(unittest.TestCase): def test_init(self): extract_single_node_dyn() - py1 = dyn2py.PythonFile(f"{OUTPUT_DIR}/single_node_1c5d99792882409e97e132b3e9f814b0.py") + dyn2py.File.open_files.clear() - dyn2py.DynamoFile.open_files.clear() + py1 = dyn2py.PythonFile( + f"{OUTPUT_DIR}/single_node_1c5d99792882409e97e132b3e9f814b0.py") dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/single_node.dyn") node = list(dyn.python_nodes)[0] py2 = dyn2py.PythonFile(filepath=node.filepath, @@ -23,9 +24,10 @@ class TestPythonFile(unittest.TestCase): for py in [py1, py2]: self.assertEqual(len(py.code), 17) - self.assertEqual(len(py.text.split(os.linesep)), 32, msg=py.filepath) + self.assertEqual(len(py.text.split(os.linesep)), + 32, msg=py.filepath) self.assertIs(type(py.header_data), dict) - self.assertTrue(py in dyn2py.PythonFile.open_files) + self.assertTrue(py in dyn2py.PythonFile.get_open_files()) def test_update_dynamo(self): extract_single_node_dyn(modify_py=True) @@ -68,14 +70,13 @@ class TestPythonFile(unittest.TestCase): def test_get_source_dynamo_file(self): extract_single_node_dyn() - dyn2py.DynamoFile.open_files.clear() - dyn2py.PythonFile.open_files.clear() + dyn2py.File.open_files.clear() py1 = dyn2py.PythonFile( f"{OUTPUT_DIR}/single_node_1c5d99792882409e97e132b3e9f814b0.py") dyn1 = py1.get_source_dynamo_file() - self.assertEqual(len(dyn2py.DynamoFile.open_files), 1) + self.assertEqual(len(dyn2py.DynamoFile.get_open_files()), 1) self.assertIn(dyn1, dyn2py.DynamoFile.open_files) dyn2 = py1.get_source_dynamo_file() @@ -83,14 +84,13 @@ class TestPythonFile(unittest.TestCase): dyn2py.DynamoFile.open_files.clear() - with self.assertRaises(dyn2py.DynamoFileException): + with self.assertRaises(dyn2py.DynamoFile.Error): py1.header_data["dyn_uuid"] = "wrong-uuid" py1.get_source_dynamo_file() def test_write(self): extract_single_node_dyn() - dyn2py.DynamoFile.open_files.clear() - dyn2py.PythonFile.open_files.clear() + dyn2py.File.open_files.clear() py1 = dyn2py.PythonFile( f"{OUTPUT_DIR}/single_node_1c5d99792882409e97e132b3e9f814b0.py") @@ -98,7 +98,7 @@ class TestPythonFile(unittest.TestCase): dyn1 = py1.get_source_dynamo_file() node = list(dyn1.python_nodes)[0] py2 = dyn2py.PythonFile( - node.filepath, dynamo_file=dyn1, python_node=node) + f"{OUTPUT_DIR}/{node.filename}", dynamo_file=dyn1, python_node=node) self.assertIsNot(py1, py2) self.assertEqual(py1.code, py2.code) for d in py1.header_data: diff --git a/tests/test_PythonNode.py b/tests/test_PythonNode.py index 7909d99..8395533 100644 --- a/tests/test_PythonNode.py +++ b/tests/test_PythonNode.py @@ -48,11 +48,12 @@ class TestPythonNode(unittest.TestCase): py = dyn2py.PythonFile(f"{OUTPUT_DIR}/single_node_mod.py") - with self.assertRaises(dyn2py.PythonNodeException): + with self.assertRaises(dyn2py.PythonNode.Error): node1 = dyn2py.PythonNode( node_dict_from_dyn=node_dict, dynamo_file=dyn, python_file=py ) - + + with self.assertRaises(dyn2py.PythonNode.Error): node2 = dyn2py.PythonNode()