Tests, documentation, fixes

This commit is contained in:
2023-03-10 20:45:07 +01:00
parent 62347eaf31
commit c9df6b5408
9 changed files with 547 additions and 34 deletions

View File

@@ -92,11 +92,19 @@ class File():
def write(self, options: Options) -> None: def write(self, options: Options) -> None:
"""Prepare writing file to the disk: """Prepare writing file to the disk:
create backup, process dry-run, call filetype specific write_file() methods create backup, process dry-run, call filetype specific write_file() methods
Should be called on subclasses!
Args: Args:
options(Options): Run options. options(Options): Run options.
Raises:
TypeError: If called on a File object
""" """
# This should only work on subclasses:
if type(self).__name__ == "File":
raise TypeError("This method shouldn't be called on File objects!")
if not self.modified: if not self.modified:
logging.debug("File not modified, not saving") logging.debug("File not modified, not saving")
return return
@@ -126,16 +134,16 @@ class DynamoFile(File):
"""A Dynamo file, subclass of File()""" """A Dynamo file, subclass of File()"""
full_dict: dict full_dict: dict
"""The contents of the Dynamo file, as dict.""" """The contents of the Dynamo file, as dict. Initialized with read()"""
uuid: str uuid: str
"""The uuid of the graph""" """The uuid of the graph. Initialized with read()"""
name: str name: str
"""The name of the graph, read from the file. Not the name of the file""" """The name of the graph, read from the file. Initialized with read()"""
python_nodes: list["PythonNode"] python_nodes: list["PythonNode"]
"""Python node objects, read from this file""" """Python node objects, read from this file. Initialized with get_python_nodes()"""
open_files: set["DynamoFile"] = set() open_files: set["DynamoFile"] = set()
"""A set of open Dynamo files, before saving""" """A set of open Dynamo files, before saving. Self added by read()"""
def extract_python(self, options: Options | None = None) -> None: def extract_python(self, options: Options | None = None) -> None:
"""Extract and write python files """Extract and write python files
@@ -174,6 +182,7 @@ class DynamoFile(File):
"""Read Dynamo graph to parameters """Read Dynamo graph to parameters
Raises: Raises:
FileNotFoundError: The file does not exist
DynamoFileException: If the file is a Dynamo 1 file DynamoFileException: If the file is a Dynamo 1 file
json.JSONDecodeError: If there are any other problem with the file json.JSONDecodeError: If there are any other problem with the file
""" """
@@ -204,6 +213,9 @@ class DynamoFile(File):
Returns: Returns:
list[PythonNode]: A list of PythonNodes in the file list[PythonNode]: A list of PythonNodes in the file
Raises:
DynamoFileException: If no Python nodes in the file
""" """
if not self in self.open_files: if not self in self.open_files:
self.read() self.read()
@@ -211,19 +223,24 @@ class DynamoFile(File):
full_python_nodes = [n for n in self.full_dict["Nodes"] full_python_nodes = [n for n in self.full_dict["Nodes"]
if n["NodeType"] == "PythonScriptNode"] if n["NodeType"] == "PythonScriptNode"]
self.python_nodes = [] # Check if it was already read:
try:
for p_node in full_python_nodes: self.python_nodes
# The name of the node is stored here: except AttributeError:
node_views = self.full_dict["View"]["NodeViews"] self.python_nodes = []
python_node = PythonNode(
node_dict_from_dyn=p_node,
full_nodeviews_dict_from_dyn=node_views,
source_dynamo_file=self)
self.python_nodes.append(python_node)
if not self.python_nodes: if not self.python_nodes:
raise DynamoFileException("No python nodes in this file!") for p_node in full_python_nodes:
# The name of the node is stored here:
node_views = self.full_dict["View"]["NodeViews"]
python_node = PythonNode(
node_dict_from_dyn=p_node,
full_nodeviews_dict_from_dyn=node_views,
source_dynamo_file=self)
self.python_nodes.append(python_node)
if not self.python_nodes:
raise DynamoFileException("No python nodes in this file!")
return self.python_nodes return self.python_nodes
@@ -326,14 +343,14 @@ class PythonFile(File):
"""A Python file, subclass of File()""" """A Python file, subclass of File()"""
code: str code: str
"""The python code as a string""" """The python code as a string. Initialized with read()"""
header_data: dict header_data: dict
"""Parsed dict from the header of a python file""" """Parsed dict from the header of a python file. Initialized with read()"""
text: str text: str
"""Full contents of the file before writing""" """Full contents of the file before writing. Initialized with generate_text()"""
open_files: set["PythonFile"] = set() open_files: set["PythonFile"] = set()
"""A set of open Python files""" """A set of open Python files. Self added by read()"""
def generate_text(self, dynamo_file: DynamoFile, python_node: "PythonNode") -> None: def generate_text(self, dynamo_file: DynamoFile, python_node: "PythonNode") -> None:
"""Generate full text to write with header """Generate full text to write with header
@@ -348,12 +365,17 @@ class PythonFile(File):
Do not edit this section, if you want to update the Dynamo graph!\ Do not edit this section, if you want to update the Dynamo graph!\
""" """
# Double escape path:
dyn_path_string = str(dynamo_file.realpath)
if "\\" in dyn_path_string:
dyn_path_string = dyn_path_string.replace("\\", "\\\\")
self.header_data = { self.header_data = {
"dyn2py_version": METADATA["Version"], "dyn2py_version": METADATA["Version"],
"dyn2py_extracted": datetime.now().isoformat(), "dyn2py_extracted": datetime.now().isoformat(),
"dyn_uuid": dynamo_file.uuid, "dyn_uuid": dynamo_file.uuid,
"dyn_name": dynamo_file.name, "dyn_name": dynamo_file.name,
"dyn_path": dynamo_file.realpath, "dyn_path": dyn_path_string,
"dyn_modified": dynamo_file.mtimeiso, "dyn_modified": dynamo_file.mtimeiso,
"py_id": python_node.id, "py_id": python_node.id,
"py_engine": python_node.engine "py_engine": python_node.engine
@@ -486,7 +508,7 @@ class PythonFile(File):
def write_file(self) -> None: def write_file(self) -> None:
"""Write this file to the disk. Should be called only from File.write()""" """Write this file to the disk. Should be called only from File.write()"""
with open(self.filepath, "w", encoding="utf-8") as output_file: with open(self.filepath, "w", encoding="utf-8", newline='') as output_file:
output_file.write(self.text) output_file.write(self.text)

View File

@@ -0,0 +1,200 @@
{
"Uuid": "76de5c79-17c5-4c74-9f90-ad99a213d339",
"IsCustomNode": false,
"Description": "",
"Name": "no_python",
"ElementResolver": {
"ResolutionMap": {}
},
"Inputs": [],
"Outputs": [],
"Nodes": [
{
"ConcreteType": "Dynamo.Graph.Nodes.ZeroTouch.DSFunction, DynamoCore",
"NodeType": "FunctionNode",
"FunctionSignature": "+@var[]..[],var[]..[]",
"Id": "f8f4d3310211429ea8f301284983849e",
"Inputs": [
{
"Id": "264e2d2a3442445180048463a555399a",
"Name": "x",
"Description": "Integer value, double value or string\n\nvar[]..[]",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
},
{
"Id": "f627c4088ee943aeba708e81f9945f1e",
"Name": "y",
"Description": "Integer value, double value or string\n\nvar[]..[]",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"Outputs": [
{
"Id": "72975df34c2542ff949630ef7253bed5",
"Name": "var",
"Description": "The sum of two input numbers, or the concatenation of two strings",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"Replication": "Auto",
"Description": "Returns addition of x and y\n\n+ (x: var[]..[], y: var[]..[]): var[]..[]"
},
{
"ConcreteType": "Dynamo.Graph.Nodes.CodeBlockNodeModel, DynamoCore",
"NodeType": "CodeBlockNode",
"Code": "1;",
"Id": "24cee4b3934f46309d34386a1b5a674a",
"Inputs": [],
"Outputs": [
{
"Id": "dffa5271656d49e0b61ba8133b92a05e",
"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": "CoreNodeModels.Watch, CoreNodeModels",
"NodeType": "ExtensionNode",
"Id": "26fc878922fd425a89112c6b14c9779b",
"Inputs": [
{
"Id": "1587eb8d7d8f41e6833e2b90f95b63fb",
"Name": "",
"Description": "Node to show output from",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"Outputs": [
{
"Id": "d70e73be40114b9cb129a91cd4777c4a",
"Name": "",
"Description": "Node output",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"Replication": "Disabled",
"Description": "Visualizes a node's output"
}
],
"Connectors": [
{
"Start": "72975df34c2542ff949630ef7253bed5",
"End": "1587eb8d7d8f41e6833e2b90f95b63fb",
"Id": "8d017b34cb5b47f1b900c1c6ed8fd8d0",
"IsHidden": "False"
},
{
"Start": "dffa5271656d49e0b61ba8133b92a05e",
"End": "264e2d2a3442445180048463a555399a",
"Id": "46f9099ffb1a4e08ad78d7e13d36d8bc",
"IsHidden": "False"
},
{
"Start": "dffa5271656d49e0b61ba8133b92a05e",
"End": "f627c4088ee943aeba708e81f9945f1e",
"Id": "f167bc9b5eed4d5c96bac03015664979",
"IsHidden": "False"
}
],
"Dependencies": [],
"NodeLibraryDependencies": [],
"Thumbnail": "",
"GraphDocumentationURL": null,
"ExtensionWorkspaceData": [
{
"ExtensionGuid": "28992e1d-abb9-417f-8b1b-05e053bee670",
"Name": "Properties",
"Version": "2.17",
"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.17.0.3472",
"RunType": "Automatic",
"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": [
{
"Name": "+",
"ShowGeometry": true,
"Id": "f8f4d3310211429ea8f301284983849e",
"IsSetAsInput": false,
"IsSetAsOutput": false,
"Excluded": false,
"X": 779.0,
"Y": 566.5
},
{
"Name": "Code Block",
"ShowGeometry": true,
"Id": "24cee4b3934f46309d34386a1b5a674a",
"IsSetAsInput": false,
"IsSetAsOutput": false,
"Excluded": false,
"X": 402.0,
"Y": 500.0
},
{
"Name": "Watch",
"ShowGeometry": true,
"Id": "26fc878922fd425a89112c6b14c9779b",
"IsSetAsInput": false,
"IsSetAsOutput": false,
"Excluded": false,
"X": 1102.0,
"Y": 587.0
}
],
"Annotations": [],
"X": 127.12026168573345,
"Y": 129.76719880218047,
"Zoom": 0.7658293928515516
}
}

View File

@@ -0,0 +1,106 @@
{
"Uuid": "76de5c79-17c5-4c74-9f90-ad99a213d339",
"IsCustomNode": false,
"Description": "",
"Name": "single_node",
"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\noutput = {\r\n \"python_version\": sys.version,\r\n \"asd_string\": \"asd_string\"\r\n}\r\n\r\n\r\n# Assign your output to the OUT variable.\r\nOUT = output",
"Engine": "CPython3",
"EngineName": "CPython3",
"VariableInputPorts": true,
"Id": "1c5d99792882409e97e132b3e9f814b0",
"Inputs": [
{
"Id": "7a29134d5d35492b80cd49a3ebac1847",
"Name": "IN[0]",
"Description": "Input #0",
"UsingDefaultValue": false,
"Level": 2,
"UseLevels": false,
"KeepListStructure": false
}
],
"Outputs": [
{
"Id": "a6a87605c777416bb759eaa87c976285",
"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.17",
"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.17.0.3472",
"RunType": "Automatic",
"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": [
{
"Name": "Python Script",
"ShowGeometry": true,
"Id": "1c5d99792882409e97e132b3e9f814b0",
"IsSetAsInput": false,
"IsSetAsOutput": false,
"Excluded": false,
"X": 208.0,
"Y": 367.0
}
],
"Annotations": [],
"X": 84.425000000000011,
"Y": -8.4125000000000227,
"Zoom": 1.1575
}
}

12
tests/support.py Normal file
View File

@@ -0,0 +1,12 @@
import pathlib
INPUT_DIR = "tests/input_files"
OUTPUT_DIR = "tests/output_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()

View File

@@ -1,6 +1,5 @@
import unittest import unittest
import subprocess import subprocess
import platform
class TestCommandLine(unittest.TestCase): class TestCommandLine(unittest.TestCase):

View File

@@ -2,16 +2,8 @@ import unittest
import dyn2py import dyn2py
import pathlib import pathlib
INPUT_DIR = "tests/input_files" from tests.support import *
OUTPUT_DIR = "tests/output_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()
class TestDynamoFile(unittest.TestCase): class TestDynamoFile(unittest.TestCase):
@@ -21,7 +13,7 @@ class TestDynamoFile(unittest.TestCase):
# update_python_node # update_python_node
# write # write
def test_read(self): def test_read_and_variables(self):
dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/python_nodes.dyn") dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/python_nodes.dyn")
dyn.read() dyn.read()
@@ -38,7 +30,7 @@ class TestDynamoFile(unittest.TestCase):
dyn2.read() dyn2.read()
def test_get_python_node(self): def test_get_python_nodes(self):
dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/python_nodes.dyn") dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/python_nodes.dyn")
py_nodes = dyn.get_python_nodes() py_nodes = dyn.get_python_nodes()
py_node = dyn.get_python_node_by_id("d7704617c75e4bf1a5c387b7c3f001ea") py_node = dyn.get_python_node_by_id("d7704617c75e4bf1a5c387b7c3f001ea")
@@ -46,11 +38,19 @@ class TestDynamoFile(unittest.TestCase):
self.assertEqual(len(py_nodes), 6) self.assertEqual(len(py_nodes), 6)
self.assertTrue(py_node) self.assertTrue(py_node)
self.assertTrue(py_node in py_nodes) self.assertTrue(py_node in py_nodes)
self.assertTrue(py_node in dyn.python_nodes)
self.assertEqual(py_node.checksum, "1f3d9e6153804fe1ed37571a9cda8e26") self.assertEqual(py_node.checksum, "1f3d9e6153804fe1ed37571a9cda8e26")
with self.assertRaises(dyn2py.PythonNodeNotFoundException): with self.assertRaises(dyn2py.PythonNodeNotFoundException):
dyn.get_python_node_by_id("wrongid") dyn.get_python_node_by_id("wrongid")
dyn2 = dyn2py.DynamoFile(f"{INPUT_DIR}/no_python.dyn")
# Raise error on file without python nodes:
with self.assertRaises(dyn2py.DynamoFileException):
dyn2.get_python_nodes()
def test_extract_python(self): def test_extract_python(self):
cleanup_output_dir() cleanup_output_dir()

92
tests/test_File.py Normal file
View File

@@ -0,0 +1,92 @@
import unittest
import dyn2py
import pathlib
import platform
from tests.support import *
class TestFile(unittest.TestCase):
# Methods to test:
# is_dynamo_file
# is_python_file
def test_init(self):
paths = [
f"{INPUT_DIR}/python_nodes.dyn",
pathlib.Path(f"{INPUT_DIR}/python_nodes.dyn")
]
if platform.system() == "Windows":
paths.extend([
fr"{INPUT_DIR}\python_nodes.dyn",
pathlib.Path(fr"{INPUT_DIR}\python_nodes.dyn")
])
for path in paths:
the_file = dyn2py.File(path)
self.assertEqual(the_file.filepath,
pathlib.Path(f"{INPUT_DIR}/python_nodes.dyn"))
self.assertEqual(the_file.basename, "python_nodes")
self.assertEqual(the_file.dirpath, pathlib.Path(INPUT_DIR))
self.assertEqual(the_file.realpath, pathlib.Path(path).resolve())
self.assertEqual(the_file.mtime, 1678287111.4672492)
self.assertEqual(the_file.mtimeiso, "2023-03-08T15:51:51.467249")
self.assertTrue(the_file.exists)
self.assertEqual(the_file.extension, ".dyn")
self.assertFalse(the_file.modified)
def test_init_newfile(self):
paths = [
f"{INPUT_DIR}/new_file.dyn",
pathlib.Path(f"{INPUT_DIR}/new_file.dyn")
]
if platform.system() == "Windows":
paths.extend([
fr"{INPUT_DIR}\new_file.dyn",
pathlib.Path(fr"{INPUT_DIR}\new_file.dyn")
])
for path in paths:
the_file = dyn2py.File(path)
self.assertEqual(the_file.filepath,
pathlib.Path(f"{INPUT_DIR}/new_file.dyn"))
self.assertEqual(the_file.basename, "new_file")
self.assertEqual(the_file.dirpath, pathlib.Path(INPUT_DIR))
self.assertEqual(the_file.realpath, pathlib.Path(path).resolve())
self.assertEqual(the_file.mtime, 0.0)
self.assertEqual(the_file.mtimeiso, "")
self.assertFalse(the_file.exists)
self.assertEqual(the_file.extension, ".dyn")
self.assertFalse(the_file.modified)
def test_newer(self):
older_file = dyn2py.File(f"{INPUT_DIR}/python_nodes.dyn")
newer_file = dyn2py.File(f"{INPUT_DIR}/no_python.dyn")
nonexisting_file = dyn2py.File(f"{INPUT_DIR}/new_file.dyn")
self.assertTrue(newer_file.is_newer(older_file))
self.assertTrue(newer_file.is_newer(nonexisting_file))
self.assertFalse(older_file.is_newer(newer_file))
self.assertTrue(older_file.is_newer(nonexisting_file))
self.assertFalse(nonexisting_file.is_newer(older_file))
self.assertFalse(nonexisting_file.is_newer(newer_file))
def test_write(self):
existing_file = dyn2py.File(f"{INPUT_DIR}/python_nodes.dyn")
nonexisting_file = dyn2py.File(f"{INPUT_DIR}/new_file.dyn")
options = dyn2py.Options()
with self.assertRaises(TypeError):
existing_file.write(options)
with self.assertRaises(TypeError):
nonexisting_file.write(options)

19
tests/test_PythonFile.py Normal file
View File

@@ -0,0 +1,19 @@
import unittest
from tests.support import *
class TestPythonFile(unittest.TestCase):
pass
# Missing variables:
# code
# header_data
# text
# open_files
# Missing methods:
# generate_text()
# update_dynamo()
# get_source_dynamo_file()
# read()
# write()

63
tests/test_PythonNode.py Normal file
View File

@@ -0,0 +1,63 @@
import unittest
import dyn2py
import hashlib
from tests.support import *
class TestPythonNode(unittest.TestCase):
def test_init_from_dyn(self):
dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/single_node.dyn")
dyn.read()
node_dict = next((n for n in dyn.full_dict["Nodes"]
if n["NodeType"] == "PythonScriptNode"), {})
# Found a node:
self.assertTrue(node_dict)
node_views = dyn.full_dict["View"]["NodeViews"]
node = dyn2py.PythonNode(
node_dict_from_dyn=node_dict,
full_nodeviews_dict_from_dyn=node_views,
source_dynamo_file=dyn
)
self.assertEqual(node.id, "1c5d99792882409e97e132b3e9f814b0")
self.assertEqual(node.engine, "CPython3")
self.assertEqual(node.checksum, "ec2c85a11ddbf8375da03f11272d427a")
self.assertEqual(node.name, "Python Script")
self.assertEqual(
node.filename, "single_node_1c5d99792882409e97e132b3e9f814b0.py")
def test_init_from_py(self):
cleanup_output_dir()
# Extract py:
options = dyn2py.Options(python_folder=OUTPUT_DIR)
dyn = dyn2py.DynamoFile(f"{INPUT_DIR}/single_node.dyn")
dyn.extract_python(options)
# Open the extracted file and replace a string:
with open(f"{OUTPUT_DIR}/single_node_1c5d99792882409e97e132b3e9f814b0.py") as orig_py, \
open(f"{OUTPUT_DIR}/single_node_mod.py", "w") as mod_py:
for line in orig_py:
if "asd_string" in line:
line = line.replace("asd_string", "qwe_string")
mod_py.write(line)
py = dyn2py.PythonFile(f"{OUTPUT_DIR}/single_node_mod.py")
py.read()
node = dyn2py.PythonNode(
node_id=py.header_data["py_id"],
engine=py.header_data["py_engine"],
code=py.code,
checksum=hashlib.md5(py.code.encode()).hexdigest()
)
self.assertEqual(node.id, "1c5d99792882409e97e132b3e9f814b0")
self.assertEqual(node.engine, "CPython3")
self.assertEqual(node.checksum, "bf0f039ef6f11c3043e0821143801d48")