25 Commits

Author SHA1 Message Date
f6fb28d488 Merge pull request #1 from infeeeee/dependabot/github_actions/dot-github/workflows/actions/download-artifact-4.1.7
Bump actions/download-artifact from 3 to 4.1.7 in /.github/workflows
2024-09-06 12:25:11 +02:00
dependabot[bot]
57b4477c39 Bump actions/download-artifact from 3 to 4.1.7 in /.github/workflows
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.1.7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4.1.7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-03 22:22:18 +00:00
dda1d98fb2 Fix installer, 0.4.1 2023-04-25 01:02:25 +02:00
b2d851f5bc Bump version 2023-04-25 00:45:20 +02:00
c3737f0eb3 Merge branch 'dev' 2023-04-25 00:39:58 +02:00
8909594a98 Update Tests 2023-04-25 00:39:24 +02:00
e2aecc684f Option arguments as kwargs 2023-04-24 20:05:54 +02:00
72cb52e0bf Rework exceptions, class methods for open files 2023-04-12 07:15:20 +02:00
5efae02594 Fix argument 2023-04-12 04:08:26 +02:00
1eed4ed198 Update workflow 2023-04-12 03:58:47 +02:00
922765c7eb Merge branch 'relative-path' 2023-04-12 03:55:11 +02:00
9a449b01fa Installer: add to start 2023-04-12 03:46:46 +02:00
bfcab5f46d Save relative path in python files 2023-03-30 04:25:48 +02:00
0c2174525e Fix FileNotFound not catched 2023-03-30 02:54:53 +02:00
2a6c42829f Fix installer build script 2023-03-30 02:11:42 +02:00
8b719bc3f9 Update Windows installer scripts 2023-03-30 02:02:45 +02:00
118425f994 Add example git hook 2023-03-30 01:28:51 +02:00
53712f76ce Added HEADLESS loglevel 2023-03-28 22:52:25 +02:00
d5fad2beb6 Bump version 2023-03-28 01:03:53 +02:00
4e4ada293e Merge commit '1e48aa144ec3f0ab1c4126a35de473bb4d8d8207' 2023-03-28 00:59:30 +02:00
1e48aa144e Fix installer 2023-03-28 00:53:35 +02:00
233fd5742d Update readme, release workflow 2023-03-28 00:29:13 +02:00
7d1a9e6310 Bump version 2023-03-28 00:22:39 +02:00
7194a335dd Windows installer, reorganize workflows 2023-03-28 00:12:32 +02:00
b3834edaee Windows Installer 2023-03-27 21:59:45 +02:00
24 changed files with 673 additions and 209 deletions

28
.github/workflows/build-exe.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
on:
workflow_call:
name: Workflow - Build exe
jobs:
build:
runs-on: windows-latest
name: Build Windows exe
steps:
- uses: actions/checkout@v3
name: Checkout
- uses: actions/setup-python@v4
name: Setup Python
with:
python-version: ${{ vars.PYTHON_VERSION}}
- name: Install deps
run: |
python -m pip install --upgrade pip
pip install .[build]
- name: Build
run: pyinstaller dyn2py.spec
- uses: actions/upload-artifact@v3
name: Upload artifact
with:
name: dyn2py.exe
path: dist/dyn2py.exe

29
.github/workflows/build-installer.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
on:
workflow_call:
name: Workflow - Build installer
jobs:
build-installer:
runs-on: windows-latest
name: Build Windows installer
steps:
- uses: actions/checkout@v3
name: Checkout
- name: Install Inno Setup
run: |
Invoke-WebRequest -Uri https://jrsoftware.org/download.php/is.exe -OutFile is.exe
.\is.exe /verysilent
- name: Download artifact
uses: actions/download-artifact@v4.1.7
with:
name: dyn2py.exe
- name: Build
run: |
.\dyn2py-installer.ps1
- uses: actions/upload-artifact@v3
name: Upload artifact
with:
name: dyn2py-installer.exe
path: Output/dyn2py-installer.exe

View File

@@ -10,39 +10,29 @@ permissions:
jobs:
test:
name: Test
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
uses: ./.github/workflows/test.yml
with:
python-version: ${{ vars.PYTHON_VERSION}}
python-version: ${{ vars.PYTHON_VERSION }}
os: ${{ matrix.os }}
build:
runs-on: windows-latest
build-exe:
name: Build Windows exe
needs: test
steps:
- uses: actions/checkout@v3
name: Checkout
- uses: actions/setup-python@v4
name: Setup Python
with:
python-version: ${{ vars.PYTHON_VERSION}}
- name: Install deps
run: |
python -m pip install --upgrade pip
pip install .[build]
- name: Build
run: pyinstaller dyn2py.spec
- uses: actions/upload-artifact@v3
name: Upload artifact
with:
name: dyn2py.exe
path: dist/dyn2py.exe
uses: ./.github/workflows/build-exe.yml
build-installer:
name: Build Windows installer
needs: build-exe
uses: ./.github/workflows/build-installer.yml
release:
name: Create Github release
runs-on: ubuntu-latest
needs: build
needs: build-installer
steps:
- uses: actions/checkout@v3
name: Checkout
@@ -51,26 +41,33 @@ jobs:
uses: metcalfc/changelog-generator@v4.1.0
with:
myToken: ${{ secrets.GITHUB_TOKEN }}
- name: Download artifact
uses: actions/download-artifact@v3
- name: Download exe
uses: actions/download-artifact@v4.1.7
with:
name: dyn2py.exe
- name: Download installer
uses: actions/download-artifact@v4.1.7
with:
name: dyn2py-installer.exe
- name: Release
uses: softprops/action-gh-release@v1
with:
files: dyn2py.exe
body: ${{ steps.modified.outputs.log }}
files: |
dyn2py.exe
dyn2py-installer.exe
body: ${{ steps.changelog.outputs.changelog }}
pip:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: release
needs: test
steps:
- uses: actions/checkout@v3
name: Checkout
- uses: actions/setup-python@v4
name: Setup Python
with:
python-version: ${{ vars.PYTHON_VERSION}}
python-version: ${{ vars.PYTHON_VERSION }}
- name: Install deps
run: |
python -m pip install --upgrade pip

View File

@@ -8,11 +8,15 @@ on:
required: true
type: string
name: Workflow - Test
jobs:
test:
name: Run tests
runs-on: ${{ inputs.os }}
steps:
- uses: actions/checkout@v3
name: Checkout
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v4
with:
@@ -21,5 +25,5 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install .
- name: Test
run: python -m unittest discover -v -s ./tests -p "test_*.py"
- name: Run tests
run: python -m unittest discover -v -s ./tests -p "test_*.py"

View File

@@ -5,12 +5,14 @@ on:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
permissions:
contents: read
jobs:
tests:
name: Unit tests
strategy:
matrix:
os: [ubuntu-latest, windows-latest]

View File

@@ -5,7 +5,6 @@ on:
branches:
- main
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# security: restrict permissions for CI jobs.
@@ -18,19 +17,22 @@ concurrency:
cancel-in-progress: true
jobs:
# Build the documentation and upload the static HTML files as an artifact.
build:
name: Build documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
name: Checkout
- uses: actions/setup-python@v4
name: Setup Python
with:
python-version: "3.11"
python-version: ${{ vars.PYTHON_VERSION }}
- run: pip install -e .[doc]
name: Install deps
- run: pdoc -d google -o docs dyn2py
name: Generate docs
- uses: actions/upload-pages-artifact@v1
name: Upload artifact
with:
path: docs/
@@ -38,6 +40,7 @@ jobs:
# This is a separate job so that only actions/deploy-pages has the necessary permissions.
deploy:
needs: build
name: Publish documentation
runs-on: ubuntu-latest
permissions:
pages: write
@@ -47,4 +50,5 @@ jobs:
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
name: Deploy page
uses: actions/deploy-pages@v1

17
.github/workflows/windows-build.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Build Windows exe and installer
on:
workflow_dispatch:
pull_request:
branches: ["main"]
# push:
jobs:
build-exe:
uses: ./.github/workflows/build-exe.yml
name: Build Windows exe
build-installer:
name: Build Windows installer
needs: build-exe
uses: ./.github/workflows/build-installer.yml

4
.gitignore vendored
View File

@@ -5,4 +5,6 @@ dyn2py.egg-info
build
dist
docs
tests/output_files
tests/output_files
Output
dyn2py.exe

103
README.md
View File

@@ -3,7 +3,6 @@
[![GitHub Release Date](https://img.shields.io/github/release-date/infeeeee/dyn2py?style=flat-square)](https://github.com/infeeeee/dyn2py/releases/latest)
[![GitHub last commit (branch)](https://img.shields.io/github/last-commit/infeeeee/dyn2py/main?style=flat-square)](https://github.com/infeeeee/dyn2py/commits/main)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/infeeeee/dyn2py/unittests.yml?label=tests&style=flat-square)](https://github.com/infeeeee/dyn2py/actions/workflows/unittests.yml)
![GitHub top language](https://img.shields.io/github/languages/top/infeeeee/dyn2py?style=flat-square)
[![GitHub](https://img.shields.io/github/license/infeeeee/dyn2py?style=flat-square)](https://github.com/infeeeee/dyn2py/blob/main/LICENSE)
# dyn2py
@@ -17,14 +16,16 @@ Use cases:
## Installation
### Windows portable
### Windows portable and installer
Prebuilt exe available from github releases.
Prebuilt portable exe and installer available from github releases.
No requirements, just download `dyn2py.exe` from release assets:
No requirements, just download `dyn2py.exe` or `dyn2py-installer.exe` from release assets:
https://github.com/infeeeee/dyn2py/releases/latest
Installer automatically adds the install folder to the path, so simply `dyn2py` can be called from anywhere.
### With pip
For usage as a module or as a command line program
@@ -52,7 +53,7 @@ options:
-h, --help show this help message and exit
-v, --version show program's version number and exit
-l LOGLEVEL, --loglevel LOGLEVEL
set log level, possible options: CRITICAL, ERROR, WARNING, INFO, DEBUG
set log level, possible options: HEADLESS, CRITICAL, ERROR, WARNING, INFO, DEBUG
-n, --dry-run do not modify files, only show log
-F, --force overwrite even if the files are older
-b, --backup create a backup for updated files
@@ -67,68 +68,89 @@ dynamo options, only for processing Dynamo graphs:
The script by default overwrites older files with newer files.
Do not move the source Dynamo graphs, or update won't work with them later.
Multiple sources are supported, separate them by spaces.
HEADLESS loglevel only prints modified filenames.
```
#### Examples
*Notes: In Windows cmd use backward slashes as path separators, in any other shells use forward slashes. Powershell accepts both of them. Wrap paths with spaces in double quotes.*
Extract all nodes next to a Dynamo file:
```
```shell
# Extract all nodes next to a Dynamo file:
dyn2py path/to/dynamofile.dyn
```
Update a Dynamo file from previously exported and modified python files:
```
# Update a Dynamo file from previously exported and modified python files:
dyn2py --update path/to/dynamofile.dyn
```
Extract python nodes to a specific folder, process multiple Dynamo files:
```
# Extract python nodes to a specific folder, process multiple Dynamo files:
dyn2py --python-folder path/to/pythonfiles path/to/dynamofile1.dyn path/to/dynamofile2.dyn
```
Update Dynamo files from python files from a folder. Only check python files, create backups:
```
# Update Dynamo files from python files from a folder. Only check python files, create backups:
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/
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 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 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)
@@ -147,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
@@ -164,18 +188,24 @@ With venv:
```
git clone https://github.com/infeeeee/dyn2py
cd dyn2py
venv .venv
python -m venv .venv
. ./.venv/bin/activate
pip install -e .
```
### Build
### Build for Windows
```
pip install -e .[build]
pyinstaller dyn2py.spec
```
### Create installer for Windows
- Install Inno Setup: https://jrsoftware.org/isdl.php
- Build an exe
- Run `dyn2py-installer.ps1` in powershell
### Live module documentation
```
@@ -193,6 +223,11 @@ To run them manually:
python -m unittest discover -v -s ./tests -p "test_*.py"
```
### New release
1. Update version number in `pyproject.toml`
2. Create and publish a git tag with that number
## License
GPL-3.0

View File

@@ -7,6 +7,7 @@
- [x] DynamoFile
- [x] PythonFile
- [x] PythonNode
- [ ] Options
- [ ] run()
## CI/CD
@@ -15,16 +16,16 @@
- [x] Tests on Windows
- [x] Windows Build
- [x] Pip
- [x] Windows Installer
## Documentation
- [x] API docs
- [x] Installation in readme
- [x] Terminal examples in readme
- [ ] About git hooks in readme
- [x] About git hooks in readme
## Extra features maybe later
- [ ] Windows Installer
- [ ] Autocomplete
- [ ] Winget

86
dyn2py-installer.iss Normal file
View File

@@ -0,0 +1,86 @@
[Setup]
AppId={{E924F481-6909-43F8-8469-11155A5EB9A2}
AppName=dyn2py
AppVersion=x.x.x
AppPublisher=infeeeee
AppPublisherURL=https://github.com/infeeeee/dyn2py
AppSupportURL=https://github.com/infeeeee/dyn2py/issues
AppUpdatesURL=https://github.com/infeeeee/dyn2py/releases/latest
DefaultDirName={autopf}\dyn2py
DisableProgramGroupPage=auto
DefaultGroupName=dyn2py
LicenseFile=LICENSE
PrivilegesRequired=admin
OutputBaseFilename=dyn2py-installer
Compression=lzma
SolidCompression=yes
WizardStyle=modern
ChangesEnvironment=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Files]
Source: "dyn2py.exe"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\dyn2py (cmd)"; Filename: "{cmd}"; WorkingDir: "{userdocs}"; Parameters: "/k dyn2py -h"
Name: "{group}\dyn2py (powershell)"; Filename: "powershell"; WorkingDir: "{userdocs}"; Parameters: "-noexit -command dyn2py -h"
[Code]
const EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
procedure EnvAddPath(Path: string);
var
Paths: string;
begin
{ Retrieve current path (use empty string if entry not exists) }
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Paths := '';
{ Skip if string already found in path }
if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit;
{ App string to the end of the path variable }
Paths := Paths + ';'+ Path +';'
{ Overwrite (or create if missing) path environment variable }
if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] added to PATH: [%s]', [Path, Paths]))
else Log(Format('Error while adding the [%s] to PATH: [%s]', [Path, Paths]));
end;
procedure EnvRemovePath(Path: string);
var
Paths: string;
P: Integer;
begin
{ Skip if registry entry not exists }
if not RegQueryStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths) then
exit;
{ Skip if string not found in path }
P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';');
if P = 0 then exit;
{ Update path variable }
Delete(Paths, P - 1, Length(Path) + 1);
{ Overwrite path environment variable }
if RegWriteStringValue(HKEY_LOCAL_MACHINE, EnvironmentKey, 'Path', Paths)
then Log(Format('The [%s] removed from PATH: [%s]', [Path, Paths]))
else Log(Format('Error while removing the [%s] from PATH: [%s]', [Path, Paths]));
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if CurStep = ssPostInstall
then EnvAddPath(ExpandConstant('{app}'));
end;
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
begin
if CurUninstallStep = usPostUninstall
then EnvRemovePath(ExpandConstant('{app}'));
end;

24
dyn2py-installer.ps1 Normal file
View File

@@ -0,0 +1,24 @@
$InnoSetupPath = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
# Check if innosetup installed
if (-not (Test-Path -Path $InnoSetupPath -PathType Leaf)) {
throw "Innosetup not found!"
}
# Copy dyn2py.exe from default folder:
if (Test-Path -Path ".\dist\dyn2py.exe" -PathType Leaf) {
Copy-Item ".\dist\dyn2py.exe" -Destination "." -Force
}
# Check if dyn2py.exe exists at all
if (-not(Test-Path -Path "dyn2py.exe" -PathType Leaf)) {
throw "dyn2py.exe not found!"
}
# Read version number from pyproject.toml and update in innosetup:
$regex = Select-String -Path pyproject.toml -Pattern '^version = "((?:\d\.){2}\d)"$'
$version = $regex.Matches.Groups[1].Value
(Get-Content dyn2py-installer.iss).Replace("x.x.x", $version) | Set-Content dyn2py-installer.iss
# Build:
& $InnoSetupPath -Qp $(Join-Path $PWD.Path dyn2py-installer.iss)

View File

@@ -22,11 +22,7 @@ __all__ = [
"File",
"DynamoFile",
"PythonFile",
"PythonNode",
"DynamoFileException",
"PythonNodeNotFoundException",
"PythonNodeException",
"PythonFileException"
"PythonNode"
]
@@ -45,6 +41,7 @@ def __command_line() -> None:
The script by default overwrites older files with newer files.
Do not move the source Dynamo graphs, or update won't work with them later.
Multiple sources are supported, separate them by spaces.
HEADLESS loglevel only prints modified filenames.
""")
)
@@ -117,8 +114,13 @@ def run(options: Options) -> None:
from_command_line = bool(inspect.stack()[1].function == "__command_line")
# Set up logging:
if options.loglevel == "HEADLESS":
loglevel = "CRITICAL"
else:
loglevel = options.loglevel
logging.basicConfig(format='%(levelname)s: %(message)s',
level=options.loglevel)
level=loglevel)
logging.debug(f"Run options: {vars(options)}")
# Set up sources:
@@ -149,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:
@@ -193,8 +195,13 @@ def run(options: Options) -> None:
elif f.is_python_file():
logging.debug("Source is a Python file")
f.update_dynamo(options)
try:
f.update_dynamo(options)
except FileNotFoundError:
logging.error(f"{f.filepath} Source Dynamo file not found! ")
# Write files at the end:
for f in DynamoFile.open_files | PythonFile.open_files:
f.write(options)
try:
File.write_open_files(options)
except File.Error as e:
logging.error(f"Cannot save file! {e.file.filepath}")

View File

@@ -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

View File

@@ -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,20 +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
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":
@@ -136,18 +144,24 @@ class File():
# Create backup:
if not options.dry_run and self.filepath.exists() and options.backup:
backup_filename = sanitize_filename(
f"{self.basename}_{self.mtimeiso}{self.extension}")
filename=f"{self.basename}_{self.mtimeiso}{self.extension}")
backup_path = self.dirpath.joinpath(backup_filename)
logging.info(f"Creating backup to {backup_path}")
self.filepath.rename(backup_path)
if options.loglevel == "HEADLESS":
print(backup_path)
# Call filetype specific methods:
if options.dry_run:
logging.info(
f"Should write file, but it's a dry-run: {self.filepath}")
else:
if not self.dirpath.exists():
raise File.Error("File dir does not exist!", self)
logging.info(f"Writing file: {self.filepath}")
self._write_file()
if options.loglevel == "HEADLESS":
print(self.filepath)
def _write_file(self):
"""Should be implemented in subclasses
@@ -158,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()"""
@@ -171,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 = []
@@ -222,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:
@@ -242,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("<Workspace Version="):
raise DynamoFileException("This is a Dynamo 1 file!")
raise self.Error("This is a Dynamo 1 file!", self)
else:
raise e
@@ -257,8 +322,8 @@ class DynamoFile(File):
node_views = self.full_dict["View"]["NodeViews"]
if not full_python_nodes:
raise PythonNodeNotFoundException(
"No python nodes in this file!")
raise self.PythonNodeNotFound(
"No python nodes in this file!", self, "")
self.python_nodes = set()
@@ -279,7 +344,7 @@ class DynamoFile(File):
PythonNode: The PythonNode with the given id
Raises:
PythonNodeNotFoundException: No python node with this id
DynamoFile.PythonNodeNotFound: No python node with this id
"""
python_node = next((
@@ -287,8 +352,8 @@ class DynamoFile(File):
), None)
if not python_node:
raise PythonNodeNotFoundException(
f"Node not found with id {node_id}")
raise self.PythonNodeNotFound(
"Node not found", self, node_id)
return python_node
@@ -299,7 +364,7 @@ class DynamoFile(File):
python_node (PythonNode): The new node
Raises:
PythonNodeNotFoundException: Existing node not found
DynamoFile.PythonNodeNotFound: Existing node not found
"""
# Find the old node:
@@ -309,8 +374,9 @@ class DynamoFile(File):
n for n in self.full_dict["Nodes"] if n["Id"] == python_node.id
), {})
if not node_dict or not python_node_in_file:
raise PythonNodeNotFoundException()
if not node_dict:
raise self.PythonNodeNotFound(
"Existing node not found in file", self, python_node.id)
# Remove the old and add the new:
self.python_nodes.remove(python_node_in_file)
@@ -326,17 +392,24 @@ class DynamoFile(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)
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
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:
@@ -361,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()"""
@@ -377,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,
@@ -403,10 +486,11 @@ class PythonFile(File):
"Do not edit this section, if you want to update the Dynamo graph!"
])
# Double escape path:
dyn_path_string = str(dynamo_file.realpath)
# Calculate relative path, change to forward slash
dyn_path_string = os.path.relpath(
dynamo_file.filepath, self.dirpath)
if "\\" in dyn_path_string:
dyn_path_string = dyn_path_string.replace("\\", "\\\\")
dyn_path_string = dyn_path_string.replace("\\", "/")
self.header_data = {
"dyn2py_version": METADATA["Version"],
@@ -451,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
@@ -486,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:]
@@ -495,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()
@@ -528,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
@@ -541,12 +633,21 @@ class PythonFile(File):
# Open if it's the first time:
if not dynamo_file:
dynamo_file = DynamoFile(
pathlib.Path(self.header_data["dyn_path"]))
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
@@ -587,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:
@@ -627,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

View File

@@ -3,7 +3,7 @@ import argparse
import pathlib
LOGLEVELS = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
LOGLEVELS = ["HEADLESS", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"]
DEFAULT_LOGLEVEL = "INFO"
FILTERS = ["py", "dyn"]
@@ -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

27
pre-commit Normal file
View File

@@ -0,0 +1,27 @@
#!/bin/sh
# Create a list of files from staged files:
mapfile -t NEW_FILES <<<$(git diff --name-only --cached)
# Go through staged files:
for f in "${NEW_FILES[@]}"; do
# Export python files, only from Dynamo files.
# On Windows line ending is always CRLF, so remove CR with tr.
mapfile -t PY_FILES <<<$(dyn2py --force --filter dyn --loglevel HEADLESS "$f" | tr -d "\r")
# Check if something was exported:
if [[ "${PY_FILES[@]}" ]]; then
# Go through exported files:
for p in "${PY_FILES[@]}"; do
# Check if file exists:
if [ -f "$p" ]; then
# Stage file:
git add "$p"
fi
done
fi
done

View File

@@ -1,6 +1,6 @@
[project]
name = "dyn2py"
version = "0.3.1"
version = "0.4.1"
description = "Extract python code from Dynamo graphs"
readme = "README.md"
requires-python = ">=3.8"
@@ -34,3 +34,6 @@ dyn2py = "dyn2py:__command_line"
[build-system]
requires = ["setuptools", "wheel"]
[tool.setuptools]
packages = ["dyn2py"]

View File

@@ -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:

View File

@@ -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']}")

View File

@@ -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)

View File

@@ -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()

View File

@@ -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:

View File

@@ -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()