Option arguments as kwargs

This commit is contained in:
2023-04-24 20:05:54 +02:00
parent 72cb52e0bf
commit e2aecc684f
5 changed files with 151 additions and 44 deletions

View File

@@ -91,46 +91,66 @@ dyn2py --filter py --backup path/to/pythonfiles
#### Git hooks #### 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. 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 ### As a python module
Full API documentation available here: https://infeeeee.github.io/dyn2py 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 ```python
import dyn2py import dyn2py
# Open a Dynamo graph:
dynamo_file = dyn2py.DynamoFile("path/to/dynamofile.dyn") dynamo_file = dyn2py.DynamoFile("path/to/dynamofile.dyn")
# Extract python nodes:
dynamo_file.extract_python() dynamo_file.extract_python()
# Save all python nodes as separate files:
dyn2py.PythonFile.write_open_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 ```python
import dyn2py import dyn2py
# Create a backup on overwrite, read python files from a different folder: # Open a python file:
options = dyn2py.Options( python_file = dyn2py.PythonFile("path/to/pythonfile.py")
backup=True,
python_folder="path/to/pythonfiles")
# 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") 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: # 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: # Save open Dynamo graphs:
dynamo_file.write(options) 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) 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. 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 ## Development
### Installation ### Installation
@@ -166,7 +188,7 @@ With venv:
``` ```
git clone https://github.com/infeeeee/dyn2py git clone https://github.com/infeeeee/dyn2py
cd dyn2py cd dyn2py
venv .venv python -m venv .venv
. ./.venv/bin/activate . ./.venv/bin/activate
pip install -e . pip install -e .
``` ```
@@ -204,7 +226,7 @@ python -m unittest discover -v -s ./tests -p "test_*.py"
### New release ### New release
1. Update version number in `pyproject.toml` 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 ## License

View File

@@ -202,6 +202,6 @@ def run(options: Options) -> None:
# Write files at the end: # Write files at the end:
try: try:
File.write_open_files() File.write_open_files(options)
except File.Error as e: except File.Error as e:
logging.error(f"Cannot save file! {e.file.filepath}") logging.error(f"Cannot save file! {e.file.filepath}")

View File

@@ -112,21 +112,26 @@ class File():
""" """
return bool(self.extension == ".py") 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: """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! Should be called on subclasses!
Args: Args:
options (Options | None, optional): Run options. Defaults to None. options (Options | None, optional): Run options. Defaults to None.
**option_args: Options() arguments
Raises: Raises:
ValueError: Both options and other arguments given
TypeError: If called on a File object TypeError: If called on a File object
File.Error: Target folder does not exist File.Error: Target folder does not exist
""" """
if not options: 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: # This should only work on subclasses:
if type(self).__name__ == "File": if type(self).__name__ == "File":
@@ -178,14 +183,22 @@ class File():
isinstance(f, cls)} isinstance(f, cls)}
@classmethod @classmethod
def write_open_files(cls, options: Options | None = None) -> None: def write_open_files(cls, options: Options | None = None, **option_args) -> None:
"""Write open files of this class and subclasses """Write open files of this class and subclasses
Args: Args:
options (Options | None, optional): Run options. Defaults to None. options (Options | None, optional): Run options. Defaults to None.
**option_args: Options() arguments
Raises:
ValueError: Both options and other arguments given
""" """
if not options: 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!")
for f in cls.get_open_files(): for f in cls.get_open_files():
f.write(options) f.write(options)
@@ -219,18 +232,25 @@ class DynamoFile(File):
python_nodes: set[PythonNode] python_nodes: set[PythonNode]
"""Python node objects, read from this file.""" """Python node objects, read from this file."""
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 """Extract python files from Dynamo graphs, add them to open_files
Args: Args:
options (Options | None, optional): Run options. Defaults to None. options (Options | None, optional): Run options. Defaults to None.
**option_args: Options() arguments
Raises:
ValueError: Both options and other arguments given
Returns: Returns:
list[PythonFile]: List of PythonFile objects extracted from this DynamoFile list[PythonFile]: List of PythonFile objects extracted from this DynamoFile
""" """
if not options: 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}") logging.info(f"Extracting from file: {self.filepath}")
python_files = [] python_files = []
@@ -372,17 +392,24 @@ class DynamoFile(File):
with open(self.filepath, "w", encoding="utf-8", newline="") as output_file: with open(self.filepath, "w", encoding="utf-8", newline="") as output_file:
json.dump(self.full_dict, output_file, indent=2, use_decimal=True) json.dump(self.full_dict, output_file, indent=2, use_decimal=True)
def get_related_python_files(self, options: Options | None = None) -> list[PythonFile]: def get_related_python_files(self, options: Options | None = None, **option_args) -> list[PythonFile]:
"""Get python files exported from this Dynamo file """Get python files exported from this Dynamo file
Args: Args:
options (Options | None, optional): Run options. Defaults to None. options (Options | None, optional): Run options. Defaults to None.
**option_args: Options() arguments
Raises:
ValueError: Both options and other arguments given
Returns: Returns:
list[PythonFile]: A list of PythonFile objects list[PythonFile]: A list of PythonFile objects
""" """
if not options: 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 # Find the folder of the python files
if options.python_folder: if options.python_folder:
@@ -552,15 +579,23 @@ class PythonFile(File):
logging.debug(f"Header data from python file: {self.header_data}") logging.debug(f"Header data from python file: {self.header_data}")
# logging.debug(f"Code from python file: {self.code}") # 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 """Update a the source Dynamo graph from this python script
Args: Args:
options (Options | None, optional): Run options. Defaults to None. options (Options | None, optional): Run options. Defaults to None.
**option_args: Options() arguments
Raises:
ValueError: Both options and other arguments given
""" """
if not options: 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() dynamo_file = self.get_source_dynamo_file()
@@ -653,7 +688,7 @@ class PythonNode():
python_file (PythonFile, optional): The python file to be converted to node. Defaults to None. python_file (PythonFile, optional): The python file to be converted to node. Defaults to None.
Raises: Raises:
Error: Wrong arguments were given PythonNode.Error: Wrong arguments were given
""" """
# Initialize from dynamo file: # Initialize from dynamo file:
if node_dict_from_dyn and dynamo_file and not python_file: if node_dict_from_dyn and dynamo_file and not python_file:
@@ -707,4 +742,5 @@ class PythonNode():
self.checksum = hashlib.md5("".join(checksums).encode()).hexdigest() self.checksum = hashlib.md5("".join(checksums).encode()).hexdigest()
class Error(Exception): class Error(Exception):
"""Something wrong with this node"""
pass pass

View File

@@ -26,16 +26,13 @@ class Options(argparse.Namespace):
Args: Args:
source (list[pathlib.Path | str], optional): List of files to run on. Defaults to []. source (list[pathlib.Path | str], optional): List of files to run on. Defaults to [].
loglevel (str, optional): log level. Defaults to DEFAULT_LOGLEVEL. loglevel (str, optional): Log level. Defaults to DEFAULT_LOGLEVEL.
dry_run (bool, optional): If it's a dry run. Defaults to False. 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. force (bool, optional): Overwrite files, even if they are older. Defaults to False.
backup (bool, optional): Create backup of modified files. 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 "". 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. 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. 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 = [] self.source = []
@@ -45,23 +42,58 @@ class Options(argparse.Namespace):
else: else:
self.source.append(s) self.source.append(s)
if loglevel.upper() in LOGLEVELS: self.loglevel = self.sanitize_option_string("loglevel", loglevel)
self.loglevel = loglevel.upper()
else:
raise ValueError
self.dry_run = dry_run self.dry_run = dry_run
self.force = force self.force = force
self.backup = backup self.backup = backup
self.filter = self.sanitize_option_string("filter", filter)
if not filter or filter in FILTERS:
self.filter = filter
else:
raise ValueError
self.update = update self.update = update
if isinstance(python_folder, str): if isinstance(python_folder, str):
self.python_folder = pathlib.Path(python_folder) self.python_folder = pathlib.Path(python_folder)
else: else:
self.python_folder = python_folder 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

View File

@@ -46,6 +46,15 @@ class TestCommandLine(unittest.TestCase):
@staticmethod @staticmethod
def run_command(args: list = []) -> dict: 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) argstring = " ".join(args)
process = subprocess.run(f"dyn2py -l WARNING {argstring}", process = subprocess.run(f"dyn2py -l WARNING {argstring}",
capture_output=True, shell=True) capture_output=True, shell=True)
@@ -138,9 +147,11 @@ class TestCommandLine(unittest.TestCase):
shutil.copy(f"{INPUT_DIR}/{filename}", shutil.copy(f"{INPUT_DIR}/{filename}",
f"{OUTPUT_DIR}/{filename}") f"{OUTPUT_DIR}/{filename}")
# Open files normally
file_open = self.run_command( file_open = self.run_command(
[s["pfolder_arg"], s['filepath']]) [s["pfolder_arg"], s['filepath']])
# Open without error:
self.assertFalse( self.assertFalse(
bool(file_open["stderr"]), msg=file_open["stderr"]) bool(file_open["stderr"]), msg=file_open["stderr"])
self.assertEqual( self.assertEqual(
@@ -150,10 +161,12 @@ class TestCommandLine(unittest.TestCase):
file_no_overwrite = self.run_command( file_no_overwrite = self.run_command(
[s["pfolder_arg"], s['filepath']]) [s["pfolder_arg"], s['filepath']])
# Should give error, because they already exist:
self.assertTrue(bool(file_no_overwrite["stderr"])) self.assertTrue(bool(file_no_overwrite["stderr"]))
self.assertEqual( self.assertEqual(
len(file_no_overwrite["python_file_mtimes"]), s["py_file_count"]) len(file_no_overwrite["python_file_mtimes"]), s["py_file_count"])
# The modify time shouldn't change as they were not overwritten:
for p in file_no_overwrite["python_file_mtimes"]: for p in file_no_overwrite["python_file_mtimes"]:
self.assertEqual( self.assertEqual(
file_no_overwrite["python_file_mtimes"][p], file_open["python_file_mtimes"][p]) file_no_overwrite["python_file_mtimes"][p], file_open["python_file_mtimes"][p])
@@ -162,10 +175,12 @@ class TestCommandLine(unittest.TestCase):
file_force = self.run_command( file_force = self.run_command(
[s["pfolder_arg"], s["force_arg"], s['filepath']]) [s["pfolder_arg"], s["force_arg"], s['filepath']])
# Should not have an error:
self.assertFalse(bool(file_force["stderr"])) self.assertFalse(bool(file_force["stderr"]))
self.assertEqual( self.assertEqual(
len(file_force["python_file_mtimes"]), s["py_file_count"]) len(file_force["python_file_mtimes"]), s["py_file_count"])
#Modify time should be higher as they were replaced
for p in file_force["python_file_mtimes"]: for p in file_force["python_file_mtimes"]:
self.assertTrue( self.assertTrue(
file_force["python_file_mtimes"][p] > file_open["python_file_mtimes"][p] file_force["python_file_mtimes"][p] > file_open["python_file_mtimes"][p]
@@ -178,7 +193,9 @@ class TestCommandLine(unittest.TestCase):
self.assertFalse(bool(file_backup["stderr"])) self.assertFalse(bool(file_backup["stderr"]))
self.assertEqual( self.assertEqual(
len(file_backup["python_file_mtimes"]), s["py_file_count"] * 2) len(file_backup["python_file_mtimes"]), s["py_file_count"] * 2,
msg=f""
)
for p in file_force["python_file_mtimes"]: for p in file_force["python_file_mtimes"]:
self.assertTrue( self.assertTrue(