From d7a1499da1678f25319b8d4489f405f01a95d504 Mon Sep 17 00:00:00 2001 From: infeeeee Date: Wed, 8 Mar 2023 02:49:56 +0100 Subject: [PATCH] Basic tests, small fixes --- .gitignore | 3 +- .vscode/settings.json | 11 + dyn2py/files.py | 42 +++- dyn2py/options.py | 18 +- tests/input_files/python_nodes.dyn | 367 +++++++++++++++++++++++++++++ tests/test_DynamoFile.py | 49 ++++ 6 files changed, 464 insertions(+), 26 deletions(-) create mode 100644 .vscode/settings.json create mode 100755 tests/input_files/python_nodes.dyn create mode 100644 tests/test_DynamoFile.py diff --git a/.gitignore b/.gitignore index c88485e..b028691 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ __pycache__ dyn2py.egg-info build dist -docs \ No newline at end of file +docs +tests/output_files \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e9e6a80 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + "./tests", + "-p", + "test_*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/dyn2py/files.py b/dyn2py/files.py index e7e2297..cf4c0a6 100644 --- a/dyn2py/files.py +++ b/dyn2py/files.py @@ -130,6 +130,8 @@ class DynamoFile(File): """The uuid of the graph""" name: str """The name of the graph, read from the file. Not the name of the file""" + python_nodes: list["PythonNode"] + """Python node objects, read from this file""" open_files: set["DynamoFile"] = set() """A set of open Dynamo files, before saving""" @@ -195,10 +197,13 @@ class DynamoFile(File): Returns: list[PythonNode]: A list of PythonNodes in the file """ + if not self in self.open_files: + self.read() + full_python_nodes = [n for n in self.full_dict["Nodes"] if n["NodeType"] == "PythonScriptNode"] - python_nodes = [] + self.python_nodes = [] for p_node in full_python_nodes: # The name of the node is stored here: @@ -207,12 +212,12 @@ class DynamoFile(File): node_dict_from_dyn=p_node, full_nodeviews_dict_from_dyn=node_views, source_dynamo_file=self) - python_nodes.append(python_node) + self.python_nodes.append(python_node) - if not python_nodes: + if not self.python_nodes: raise DynamoFileException("No python nodes in this file!") - return python_nodes + return self.python_nodes def get_python_node_by_id(self, node_id: str) -> "PythonNode": """Get a PythonNode object from this Dynamo graph, by its id @@ -223,15 +228,28 @@ class DynamoFile(File): Returns: PythonNode: The PythonNode with the given id """ - python_node_dict = next(( - n for n in self.full_dict["Nodes"] if n["Id"] == node_id - ), {}) - if not python_node_dict: - raise PythonNodeNotFoundException( - f"Node not found with id {node_id}") + if not self in self.open_files: + self.read() - python_node = PythonNode( - node_dict_from_dyn=python_node_dict) + # Find the node, if the nodes are not read yet: + if not self.python_nodes: + python_node_dict = next(( + n for n in self.full_dict["Nodes"] if n["Id"] == node_id + ), {}) + if not python_node_dict: + raise PythonNodeNotFoundException( + f"Node not found with id {node_id}") + + python_node = PythonNode( + node_dict_from_dyn=python_node_dict) + else: + python_node = next(( + p for p in self.python_nodes if p.id == node_id + ), None) + + if not python_node: + raise PythonNodeNotFoundException( + f"Node not found with id {node_id}") return python_node diff --git a/dyn2py/options.py b/dyn2py/options.py index 0b557e0..6b58550 100644 --- a/dyn2py/options.py +++ b/dyn2py/options.py @@ -6,13 +6,16 @@ LOGLEVELS = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] DEFAULT_LOGLEVEL = "INFO" FILTERS = ["py", "dyn"] + class Options(argparse.Namespace): """Class for options for running a conversion like from the command line""" + def __init__( self, source: list[pathlib.Path | str] = [], loglevel: str = DEFAULT_LOGLEVEL, dry_run: bool = False, + force: bool = False, backup: bool = False, filter: str = "", update: bool = False, @@ -24,6 +27,7 @@ class Options(argparse.Namespace): 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. + 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. @@ -46,6 +50,7 @@ class Options(argparse.Namespace): raise ValueError self.dry_run = dry_run + self.force = force self.backup = backup if not filter or filter in FILTERS: @@ -59,16 +64,3 @@ class Options(argparse.Namespace): self.python_folder = pathlib.Path(python_folder) else: self.python_folder = python_folder - - # super().__init__( - # source=self.source, - # loglevel=self.loglevel, - # dry_run=self.dry_run, - # backup=self.backup, - # filter=self.filter, - # update=self.update, - # python_folder=self.python_folder, - # ) - - - diff --git a/tests/input_files/python_nodes.dyn b/tests/input_files/python_nodes.dyn new file mode 100755 index 0000000..f75be23 --- /dev/null +++ b/tests/input_files/python_nodes.dyn @@ -0,0 +1,367 @@ +{ + "Uuid": "3c3b4c05-9716-4e93-9360-ca0637cb5486", + "IsCustomNode": false, + "Description": "", + "Name": "python_nodes", + "ElementResolver": { + "ResolutionMap": {} + }, + "Inputs": [], + "Outputs": [], + "Nodes": [ + { + "ConcreteType": "PythonNodeModels.PythonNode, PythonNodeModels", + "NodeType": "PythonScriptNode", + "Code": "# Load the Python Standard and DesignScript Libraries\r\nimport sys\r\nimport clr\r\nclr.AddReference('ProtoGeometry')\r\nfrom Autodesk.DesignScript.Geometry import *\r\n\r\n# The inputs to this node will be stored as a list in the IN variables.\r\ndataEnteringNode = IN\r\n\r\n\r\n#Not renamed, Cpython3\r\n\r\n# Place your code below this line\r\n\r\n# Assign your output to the OUT variable.\r\nOUT = sys.version", + "Engine": "CPython3", + "EngineName": "CPython3", + "VariableInputPorts": true, + "Id": "ff087a3611b0478b95252f67e87be507", + "Inputs": [ + { + "Id": "ac5bdff0617e4246b9b03101ec2ecaf1", + "Name": "IN[0]", + "Description": "Input #0", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "974d45015c8442d1a3b9f77859d827a8", + "Name": "OUT", + "Description": "Result of the python script", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Disabled", + "Description": "Runs an embedded Python script." + }, + { + "ConcreteType": "PythonNodeModels.PythonNode, PythonNodeModels", + "NodeType": "PythonScriptNode", + "Code": "\"\"\"\r\nMultiline comment \r\nblablabla\r\n\"\"\"\r\n\r\n# Load the Python Standard and DesignScript Libraries\r\nimport sys\r\nimport clr\r\nclr.AddReference('ProtoGeometry')\r\nfrom Autodesk.DesignScript.Geometry import *\r\n\r\n# The inputs to this node will be stored as a list in the IN variables.\r\ndataEnteringNode = IN\r\n\r\n#Renamed, Cpython3\r\n\r\n# Place your code below this line\r\n\r\n# Assign your output to the OUT variable.\r\nOUT = 0", + "Engine": "CPython3", + "EngineName": "CPython3", + "VariableInputPorts": true, + "Id": "d7704617c75e4bf1a5c387b7c3f001ea", + "Inputs": [ + { + "Id": "c98cd3c591ae463eae69ff040a3989eb", + "Name": "IN[0]", + "Description": "Input #0", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "13c1e8c1f34d40ca97f25950269acd19", + "Name": "OUT", + "Description": "Result of the python script", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Disabled", + "Description": "Runs an embedded Python script." + }, + { + "ConcreteType": "PythonNodeModels.PythonNode, PythonNodeModels", + "NodeType": "PythonScriptNode", + "Code": "# Load the Python Standard and DesignScript Libraries\r\nimport sys\r\nimport clr\r\nclr.AddReference('ProtoGeometry')\r\nfrom Autodesk.DesignScript.Geometry import *\r\n\r\n# The inputs to this node will be stored as a list in the IN variables.\r\ndataEnteringNode = IN\r\n\r\n#Not renamed, IronPython2\r\n\r\n# Place your code below this line\r\n\r\n# Assign your output to the OUT variable.\r\nOUT = sys.version", + "Engine": "IronPython2", + "EngineName": "IronPython2", + "VariableInputPorts": true, + "Id": "2047ce859d424496963582fbb7b6417f", + "Inputs": [ + { + "Id": "6728601fd8054c6ca9cc414655c6912d", + "Name": "IN[0]", + "Description": "Input #0", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "beb9265bbf8641df8d46fb80e78f8269", + "Name": "OUT", + "Description": "Result of the python script", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Disabled", + "Description": "Runs an embedded Python script." + }, + { + "ConcreteType": "PythonNodeModels.PythonNode, PythonNodeModels", + "NodeType": "PythonScriptNode", + "Code": "# Load the Python Standard and DesignScript Libraries\r\nimport sys\r\nimport clr\r\nclr.AddReference('ProtoGeometry')\r\nfrom Autodesk.DesignScript.Geometry import *\r\n\r\n# The inputs to this node will be stored as a list in the IN variables.\r\ndataEnteringNode = IN\r\n\r\n#Renamed, IPY2\r\n\r\n# Place your code below this line\r\n\r\n# Assign your output to the OUT variable.\r\nOUT = 0", + "Engine": "CPython3", + "EngineName": "CPython3", + "VariableInputPorts": true, + "Id": "d89c158e2e824a909753d7137fcdf56e", + "Inputs": [ + { + "Id": "2a0f4220761341deac9e67ac0b97e02b", + "Name": "IN[0]", + "Description": "Input #0", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "780c6be36b994a4eb103162a49bbdad8", + "Name": "OUT", + "Description": "Result of the python script", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Disabled", + "Description": "Runs an embedded Python script." + }, + { + "ConcreteType": "Dynamo.Graph.Nodes.CodeBlockNodeModel, DynamoCore", + "NodeType": "CodeBlockNode", + "Code": "<>:\"/\\|?*", + "Id": "048f431a6a734d5d93a1bcc3d384dae4", + "Inputs": [ + { + "Id": "18fc02088224473486e6e3cc732ec492", + "Name": "h", + "Description": "h", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "9c669d4dabef4f88bbc4a9f19700ff11", + "Name": "", + "Description": "Value of expression at line 1", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Disabled", + "Description": "Allows for DesignScript code to be authored directly" + }, + { + "ConcreteType": "PythonNodeModels.PythonNode, PythonNodeModels", + "NodeType": "PythonScriptNode", + "Code": "# Load the Python Standard and DesignScript Libraries\r\nimport sys\r\nimport clr\r\nclr.AddReference('ProtoGeometry')\r\nfrom Autodesk.DesignScript.Geometry import *\r\n\r\n# The inputs to this node will be stored as a list in the IN variables.\r\ndataEnteringNode = IN\r\n\r\n#Renamed, IPY2\r\n\r\n# Place your code below this line\r\n\r\n# Assign your output to the OUT variable.\r\nOUT = 0", + "Engine": "CPython3", + "EngineName": "CPython3", + "VariableInputPorts": true, + "Id": "d65fb4d27998455c8bf15c92883b7213", + "Inputs": [ + { + "Id": "ed16a01bf82f46f68f279cfbdb28173a", + "Name": "IN[0]", + "Description": "Input #0", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "60947b135fd54247a9fcc0b04575a293", + "Name": "OUT", + "Description": "Result of the python script", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Disabled", + "Description": "Runs an embedded Python script." + }, + { + "ConcreteType": "PythonNodeModels.PythonNode, PythonNodeModels", + "NodeType": "PythonScriptNode", + "Code": "# Load the Python Standard and DesignScript Libraries\r\nimport sys\r\nimport clr\r\nclr.AddReference('ProtoGeometry')\r\nfrom Autodesk.DesignScript.Geometry import *\r\n\r\n# The inputs to this node will be stored as a list in the IN variables.\r\ndataEnteringNode = IN\r\n\r\n#Not renamed, IronPython2\r\n\r\n# Place your code below this line\r\n\r\n# Assign your output to the OUT variable.\r\nOUT = sys.version", + "Engine": "IronPython2", + "EngineName": "IronPython2", + "VariableInputPorts": true, + "Id": "4680a2f79a084cf9a61cbb11e3ce6af4", + "Inputs": [ + { + "Id": "4e9c4364066b41a4991c4ff2860f4c12", + "Name": "IN[0]", + "Description": "Input #0", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "d29abf7c1ac44d7ba10c2dcf020a8f09", + "Name": "OUT", + "Description": "Result of the python script", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Disabled", + "Description": "Runs an embedded Python script." + } + ], + "Connectors": [], + "Dependencies": [], + "NodeLibraryDependencies": [], + "Thumbnail": "", + "GraphDocumentationURL": null, + "ExtensionWorkspaceData": [ + { + "ExtensionGuid": "28992e1d-abb9-417f-8b1b-05e053bee670", + "Name": "Properties", + "Version": "2.12", + "Data": {} + }, + { + "ExtensionGuid": "DFBD9CC0-DB40-457A-939E-8C8555555A9D", + "Name": "Generative Design", + "Version": "3.0", + "Data": {} + } + ], + "Author": "", + "Linting": { + "activeLinter": "None", + "activeLinterId": "7b75fb44-43fd-4631-a878-29f4d5d8399a", + "warningCount": 0, + "errorCount": 0 + }, + "Bindings": [], + "View": { + "Dynamo": { + "ScaleFactor": 1.0, + "HasRunWithoutCrash": true, + "IsVisibleInDynamoLibrary": true, + "Version": "2.16.1.2727", + "RunType": "Manual", + "RunPeriod": "1000" + }, + "Camera": { + "Name": "Background Preview", + "EyeX": -17.0, + "EyeY": 24.0, + "EyeZ": 50.0, + "LookX": 12.0, + "LookY": -13.0, + "LookZ": -58.0, + "UpX": 0.0, + "UpY": 1.0, + "UpZ": 0.0 + }, + "ConnectorPins": [], + "NodeViews": [ + { + "Id": "ff087a3611b0478b95252f67e87be507", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Name": "Python Script", + "ShowGeometry": true, + "Excluded": false, + "X": 330.854857997459, + "Y": 370.30582545813525 + }, + { + "Id": "d7704617c75e4bf1a5c387b7c3f001ea", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Name": "Renamed Cpython", + "ShowGeometry": true, + "Excluded": false, + "X": 332.19386487425072, + "Y": 553.74976757860486 + }, + { + "Id": "2047ce859d424496963582fbb7b6417f", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Name": "Python Script", + "ShowGeometry": true, + "Excluded": false, + "X": 786.04189521113619, + "Y": 262.8632861477945 + }, + { + "Id": "d89c158e2e824a909753d7137fcdf56e", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Name": "<>:\"/\\|?*", + "ShowGeometry": true, + "Excluded": false, + "X": 1033.1078482745011, + "Y": 551.97820171368517 + }, + { + "Id": "048f431a6a734d5d93a1bcc3d384dae4", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Name": "Code Block", + "ShowGeometry": true, + "Excluded": false, + "X": 614.48767528598637, + "Y": 656.55546226164051 + }, + { + "Id": "d65fb4d27998455c8bf15c92883b7213", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Name": "", + "ShowGeometry": true, + "Excluded": false, + "X": 1021.3666290027875, + "Y": 401.8583267396354 + }, + { + "Id": "4680a2f79a084cf9a61cbb11e3ce6af4", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Name": "Python Script Renamed", + "ShowGeometry": true, + "Excluded": false, + "X": 934.48445314636979, + "Y": 833.98973786470992 + } + ], + "Annotations": [], + "X": -97.703074094655562, + "Y": -321.86387971712088, + "Zoom": 1.1923804228516888 + } +} \ No newline at end of file diff --git a/tests/test_DynamoFile.py b/tests/test_DynamoFile.py new file mode 100644 index 0000000..e7df33d --- /dev/null +++ b/tests/test_DynamoFile.py @@ -0,0 +1,49 @@ +import os +import unittest +import dyn2py +import pathlib + + +INPUT_DIR = "tests/input_files" +OUTPUT_DIR = "tests/output_files" + +def cleanup(): + output_dir = pathlib.Path(OUTPUT_DIR) + if output_dir.exists(): + for f in output_dir.iterdir(): + f.unlink() + else: + output_dir.mkdir() + +class TestDynamoFile(unittest.TestCase): + + def test_read(self): + dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/python_nodes.dyn") + dyn.read() + + self.assertEqual(dyn.uuid, "3c3b4c05-9716-4e93-9360-ca0637cb5486") + self.assertEqual(dyn.name, "python_nodes") + self.assertTrue(dyn in dyn.open_files) + + def test_get_python_node(self): + dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/python_nodes.dyn") + py_nodes = dyn.get_python_nodes() + py_node = dyn.get_python_node_by_id("d7704617c75e4bf1a5c387b7c3f001ea") + + self.assertEqual(len(py_nodes), 6) + self.assertTrue(py_node) + self.assertTrue(py_node in py_nodes) + self.assertEqual(py_node.checksum, "1f3d9e6153804fe1ed37571a9cda8e26") + + with self.assertRaises(dyn2py.PythonNodeNotFoundException): + dyn.get_python_node_by_id("wrongid") + + def test_extract_python(self): + cleanup() + + opt = dyn2py.Options(python_folder="tests/output_files") + dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/python_nodes.dyn") + dyn.extract_python(options=opt) + + output_dir = pathlib.Path("tests/output_files") + self.assertEqual(len(list(output_dir.iterdir())), 6)