diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 00000000..17bb058b --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,31 @@ +name: PyPi + +on: + push: + tags: + - 'v*' + release: + types: [published] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* \ No newline at end of file diff --git a/.github/workflows/python-coverage.yml b/.github/workflows/python-coverage.yml index 984cf783..306a87e3 100644 --- a/.github/workflows/python-coverage.yml +++ b/.github/workflows/python-coverage.yml @@ -1,6 +1,3 @@ -# Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO - name: Python coverage on: ["push"] diff --git a/.github/workflows/python-pylint.yml b/.github/workflows/python-pylint.yml index 354e95a0..d8c98dbf 100644 --- a/.github/workflows/python-pylint.yml +++ b/.github/workflows/python-pylint.yml @@ -1,6 +1,3 @@ -# Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO - name: Pylint on: ["push"] diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index abaee1c5..633a9d19 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -1,6 +1,3 @@ -# Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO - name: Pytest on: ["push"] diff --git a/.pylintrc b/.pylintrc index a01545e3..0edfbd9b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -3,18 +3,19 @@ # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. -extension-pkg-whitelist= +extension-pkg-whitelist=PyQt5, # Specify a score threshold to be exceeded before program exits with error. fail-under=10.0 # Add files or directories to the blacklist. They should be base names, not # paths. -ignore=CVS +ignore=CVS, + tests # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns=test_* +ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). @@ -60,7 +61,8 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use "--disable=all --enable=classes # --disable=W". -disable=inconsistent-return-statements, +disable=bad-continuation, + inconsistent-return-statements, unidiomatic-typecheck, attribute-defined-outside-init, pointless-statement, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90dc0fef..ad04f827 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,8 +13,8 @@ Feel free to fork the repository, implement your changes and create a merge requ First, make sure you have `python` installed. You will need to install all the `requirements` and the `requirements-dev` using the following commands: -* `pip install -r requirements.txt` -* `pip install -r requirements-dev.txt` +- `pip install -r requirements.txt` +- `pip install -r requirements-dev.txt` You can run the program with `python main.py` @@ -34,29 +34,29 @@ black . pytest --cov=pyflow --cov-report=html tests/unit ``` -We want to keep the *Pylint* score above *9.0*. +We want to keep the _Pylint_ score above _9.0_. The comments and docstrings should preferably follow [these](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) guidelines. ## Git Commit Messages -Commits should start with a Capital letter and should be written in present tense (e.g. ``:tada: Add cool new feature`` instead of ``:tada: Added cool new feature``). +Commits should start with a Capital letter and should be written in present tense (e.g. `:tada: Add cool new feature` instead of `:tada: Added cool new feature`). You should also start your commit message with one or two applicable emoji. This does not only look great but also makes you rethink what to add to a commit. Make many but small commits! - | Emoji | Description | - | --------------------------------------------------------- | -------------------------------------------------- | - | :tada: `:tada:` | When you add a cool new feature | - | :beetle: `:beetle:` | When you fixed a bug | - | :fire: `:fire:` | When you removed something | - | :truck: `:truck:` | When you moved/renamed something | - | :wrench: `:wrench:` | When you improved/refactored a small piece of code | - | :hammer: `:hammer:` | When you improved/refactored a large piece of code | - | :sparkles: `:sparkles:` | When you improved code quality (pylint, PEP, ...) | - | :art: `:art:` | When you improved/added design assets | - | :rocket: `:rocket:` | When you improved performance. | - | :memo: `:memo:` | When you wrote documentation. | - | :umbrella: `:umbrella:` | When you improved coverage | - | :twisted_rightwards_arrows: `:twisted_rightwards_arrows:` | When you merge a branch | +| Emoji | Description | +| --------------------------------------------------------- | -------------------------------------------------- | +| :tada: `:tada:` | When you add a cool new feature | +| :beetle: `:beetle:` | When you fixed a bug | +| :fire: `:fire:` | When you removed something | +| :truck: `:truck:` | When you moved/renamed something | +| :wrench: `:wrench:` | When you improved/refactored a small piece of code | +| :hammer: `:hammer:` | When you improved/refactored a large piece of code | +| :sparkles: `:sparkles:` | When you improved code quality (pylint, PEP, ...) | +| :art: `:art:` | When you improved/added design assets | +| :rocket: `:rocket:` | When you improved performance. | +| :memo: `:memo:` | When you wrote documentation. | +| :umbrella: `:umbrella:` | When you improved coverage | +| :twisted_rightwards_arrows: `:twisted_rightwards_arrows:` | When you merge a branch | This section was inspired by [This repository](https://github.com/schneegans/dynamic-badges-action). @@ -70,6 +70,6 @@ create a new block type. Version numbers will be assigned according to the [Semantic Versioning](https://semver.org/) scheme. This means, given a version number MAJOR.MINOR.PATCH, we will increment the: -1. MAJOR version when we make incompatible API changes, -2. MINOR version when we add functionality in a backwards compatible manner, and -3. PATCH version when we make backwards compatible bug fixes. +1. MAJOR version when we make incompatible API changes, +2. MINOR version when we add functionality in a backwards compatible manner, and +3. PATCH version when we make backwards compatible bug fixes. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..d87e2bbf --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include pyflow/themes/* +include pyflow/blocks/blockfiles/* +include pyflow/qss/* +include PYPI_README.md +include VERSION +include requirements.txt \ No newline at end of file diff --git a/README.md b/README.md index 09af31a5..f7eb7356 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # PyFlow -[![Pytest badge](https://github.com/MathisFederico/opencodeblocks/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/MathisFederico/opencodeblocks/actions/workflows/python-tests.yml) -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/ddd03302fd7c4849b452959753bc0939)](https://www.codacy.com/gh/MathisFederico/opencodeblocks/dashboard?utm_source=github.com&utm_medium=referral&utm_content=MathisFederico/opencodeblocks&utm_campaign=Badge_Grade) -[![Pylint badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_pylint_badge.json)](https://github.com/MathisFederico/opencodeblocks/actions/workflows/python-pylint.yml) -[![Total coverage Codacy Badge](https://app.codacy.com/project/badge/Coverage/ddd03302fd7c4849b452959753bc0939)](https://www.codacy.com/gh/MathisFederico/opencodeblocks/dashboard?utm_source=github.com&utm_medium=referral&utm_content=MathisFederico/opencodeblocks&utm_campaign=Badge_Coverage) -[![Unit coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_unit_coverage_badge.json)](https://github.com/MathisFederico/opencodeblocks/actions/workflows/python-coverage.yml) -[![Integration coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_integration_coverage_badge.json)](https://github.com/MathisFederico/opencodeblocks/actions/workflows/python-coverage.yml) +[![Pytest badge](https://github.com/Bycelium/PyFlow/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/Bycelium/PyFlow/actions/workflows/python-tests.yml) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9874915d70e440418447f371c4bd5061)](https://www.codacy.com/gh/Bycelium/PyFlow/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Bycelium/PyFlow&utm_campaign=Badge_Grade) +[![Pylint badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_pylint_badge.json)](https://github.com/Bycelium/PyFlow/actions/workflows/python-pylint.yml) +[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/9874915d70e440418447f371c4bd5061)](https://www.codacy.com/gh/Bycelium/PyFlow/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Bycelium/PyFlow&utm_campaign=Badge_Coverage) +[![Unit coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_unit_coverage_badge.json)](https://github.com/Bycelium/PyFlow/actions/workflows/python-coverage.yml) +[![Integration coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_integration_coverage_badge.json)](https://github.com/Bycelium/PyFlow/actions/workflows/python-coverage.yml) [![Licence - GPLv3](https://img.shields.io/github/license/MathisFederico/Crafting?style=plastic)](https://www.gnu.org/licenses/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) @@ -21,47 +21,43 @@ Join our [Discord](https://discord.gg/xZq8Tp4srd) to beta-test features, share y ## Features -- Create blocks of code in which you can edit and run Python code +- Create blocks of code in which you can edit and run Python code

- +

-- Move and resize blocks on an infinite 2D plane +- Move and resize blocks on an infinite 2D plane

- +

-- Link blocks to highlight dependencies, Pyflow will then automatically run your blocks in the correct order +- Link blocks to highlight dependencies, Pyflow will then automatically run your blocks in the correct order

- +

-- Convert your Jupyter notebooks to Pyflow graphs and vice versa +- Convert your Jupyter notebooks to Pyflow graphs and vice versa

- +

## Installation Make sure you have Python 3 installed. You can download it from [here](https://www.python.org/downloads/) -Clone the current repo: +### Install PyFlow -```bash -git clone https://github.com/MathisFederico/Pyflow/ -``` - -Install the dependencies +Using pip: ```bash -pip install -r requirements.txt +pip install byc-pyflow ``` -Run ! +### Run PyFlow ```bash python -m pyflow diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..537aabf7 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0-beta \ No newline at end of file diff --git a/coverage_score.py b/coverage_score.py index d9ffc243..9596e359 100644 --- a/coverage_score.py +++ b/coverage_score.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module to get coverage score. """ +""" Module to get coverage score.""" import sys from xml.dom import minidom diff --git a/examples/linear_classifier.ipyg b/examples/linear_classifier.ipyg index a02f6f4a..4e3eb925 100644 --- a/examples/linear_classifier.ipyg +++ b/examples/linear_classifier.ipyg @@ -4,7 +4,7 @@ { "id": 2034638878464, "title": "", - "block_type": "OCBMarkdownBlock", + "block_type": "MarkdownBlock", "splitter_pos": [ 0, 200 @@ -28,7 +28,7 @@ { "id": 2034686482320, "title": "Show the data", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 0, 272 @@ -77,12 +77,12 @@ } ], "source": "import matplotlib.pyplot as plt\r\n\r\num = 10 * m\r\nub = 2 * b\r\nuy = [um*i+ub for i in x]\r\n\r\nplt.plot(x,uy)\r\nplt.scatter(x,y)", - "stdout": "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\n" + "stdout": "iVBORw0KGgoAAAANSUhEUgAAAXAAAAD4CAYAAAD1jb0+AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAAAdkklEQVR4nO3dfXyU5Z3v8c+PECCgEikpC0EaixZWpYCN2oKrgg+IuJbaHm3P6urWltq12/qwWIJW0YqgaNU9p8cjra2PrboWY1etSAHr9qygwSCgiA+ArgEhVmJFIoTwO3/MJGYmM5lJmHvuefi+Xy9e5L7mHuc3vvDrxXVfD+buiIhI/ukVdgEiItIzCnARkTylABcRyVMKcBGRPKUAFxHJU72z+WGDBw/2qqqqbH6kiEjeW7Vq1fvuXhHfntUAr6qqoq6uLpsfKSKS98zs7UTtaQ2hmNlmM1trZqvNrC7aNsfMGqJtq83sjEwWLCIiXetOD3ySu78f13abu9+SyYJERCQ9eogpIpKn0g1wB54xs1VmNqND+w/MbI2Z/crMDk70RjObYWZ1ZlbX2Ni43wWLiEhEugF+vLsfDUwFLjGzE4A7gZHAOGArcGuiN7r7QnevdvfqiopOD1FFRKSH0hoDd/eG6O/bzewx4Fh3f67tdTP7BfBEMCWKiOS22voGFizewJamZoaVlzFzyiimj68M/HNT9sDNbICZHdj2M3AasM7Mhna47WvAumBKFBHJXbX1DdQsWktDUzMONDQ1U7NoLbX1DYF/djo98CHAY2bWdv9v3P1pM7vfzMYRGR/fDHwvqCJFRHLVnN+/QnNLa0xbc0srCxZvCLwXnjLA3X0jMDZB+/mBVCQikidq6xtoam5J+NqWpubAPz+rKzFFRArJgsUbkr42rLwMCHZ8XAEuItJDXfWyZ04Z1T4+3jbE0jY+DmQkxLWQR0Skh9p62fEO7l/K9PGVLFi8Ien4eCYowEVEemjmlFGUlZbEtJWVlnDt3x8JJO+hZ2p8XAEuItJD08dXMu/sMVSWl2FAZXkZ884e0z48MnRgv4TvS9Zz7y6NgYuI7Ifp4ysTjmc/ve49tnz4Saf2stISZk4ZlZHPVoCLiGTQR5+0MGbOM+3Xn68YwCd7Wtn64SeahSIikqv+bekb/GzJ6+3Xiy89gVF/c2Bgn6cAFxHZT39+433Ou3tl+/V3jj+Uq888IvDPVYCLSNHJ1OIad+fQmqdi2l76yakMGtAnU6V2SQEuIkUlU4trzrnreV7Y9EH79YA+Jbxy/emZLTYFBbiIFJWuFtekE+CNH+3mmLl/jGl7+drTGFhWmtE606EAF5GC1nG4pLx/KTt29XzzqapZT8ZcTz3qb7jzvC9lpM6eUICLSMGKHy5JFt7Q9eKaZzds58JfvxjTtmneGUS32Q6NAlxEClai4ZJkJo1OfORjfK/73741nrPGDtvv2jJBAS4iBas7e44sfy320PWf1K7j/hVvx7Rtnj8tI3VlSloBbmabgY+AVmCvu1eb2SDgYaCKyIk857j7jmDKFBHpntr6BnqZ0eqe1v1tYd+8p5W/vebpmNf+88pJHDKof8Zr3F/d6YFPcvf3O1zPApa6+3wzmxW9/nFGqxMR6YG2se90wxsiY+Cfr3mSfR3eUvWZ/jw7c1IAFWbG/gyhfBU4KfrzvcCzKMBFJAckG/s2g7LevdjVsi+mvW/vXjTEDbe8OXcqvUtye8PWdKtz4BkzW2VmM6JtQ9x9a/Tn94gcftyJmc0wszozq2tsbEx0i4hIRiUd+3Z49adTuf3cce1bwALs3vtpoM+cMorN86flfHhD+j3w4929wcw+Cywxs9c6vujubmYJ/67i7guBhQDV1dXp/31GRKSHhpWXdepRt7VDZMVl/Ts7uPf53H5ImUpaAe7uDdHft5vZY8CxwDYzG+ruW81sKLA9wDpFRID09jGZOWVUzPxv+HQf7tZ9zsjZsfuX1F4ykXGHlGej/IxK+XcEMxtgZge2/QycBqwDfg9cEL3tAuDxoIoUEYFPH042NDXjfLqPSW19Q8x9yU7KufTh1Z3Ce/P8aXkZ3pBeD3wI8Fh0xVFv4Dfu/rSZvQg8YmYXAW8D5wRXpohI8n1MrnjkZSB2M6qOJ+W8se0jTr3tuZj3rb7mVMr7Z2fXwKCkDHB33wiMTdD+F+DkIIoSEUkk2cPJVvekOwrGr6T8fMUAll1xUiD1ZZtWYopIXki1MCd+R8ErH32ZR+rejbkn3x5SpqIAF5Gcl+7CnLYeenyv++ITRzJr6ujA6guLAlxEcl66m1I5ncO70HrdHSnARSSn1dY3JJzTncqif57A0SMODqCi3KEAF5Gc1TZ0kkxJkjHxQu51d6QAF5Gc1dXQSWmJ0dIaG95vzJ1KaR4sgc8UBbiIZF26p8J3tZ93x/AeclBfVs4+JZBac5kCXESyqjunwifb06SjYhkuSaR4/q4hIjmhq1Ph482cMoqy0pKE/5yrp/1tUYc3qAcuIgGLHy5J1qNO1D59fCWXPry6U3uxB3cb9cBFJDCJNp/q6hz3q2s/nXHy4uYPOs3pfmH2yQrvDtQDF5HAJBou6Wot5YMr3qH6c4PU606TAlxEAtOdU+EhEu7x4a3gTk5DKCISmLYTcOJ1NYzS5rwvj1B4p6AAF5HAJJpFUtrL6NWr6wjfPH8aN0wfE2RpBUFDKCISmLZ53R1noezas5cdu1oS3t+3dy9u+voXs1liXks7wM2sBKgDGtz9TDO7BzgR+DB6y4XuvjrjFYpIXut4Mg7AoXEzSzq66etfTLgiUxLrTg/8R8B64KAObTPd/dHMliQi+ayrZfLn/XJl0lkoleVlCu9uSivAzWw4MA2YC1weaEUikre6WiafaGpgm7YT46V70n2IeTtwJbAvrn2uma0xs9vMrG+iN5rZDDOrM7O6xsbG/ShVRHJdsmXy8eF9+7njOp0Yr95396XsgZvZmcB2d19lZid1eKkGeA/oAywEfgxcH/9+d18YfZ3q6uquz0MSkbyWat73hROqmHPWkUDnjauk+9IZQpkInGVmZwD9gIPM7AF3Py/6+m4z+zXwr0EVKSL5oau9TjSnO/NSDqG4e427D3f3KuCbwDJ3P8/MhgKYmQHTgXVBFioiuW/sIQM7tfXt3Yvbzx2X/WKKwP7MA3/QzCqILKpaDVyckYpEJC/FbzwFkfHtZIc1yP7rVoC7+7PAs9GfJwdQj4jkmUTBreGS7NBSehHpEXfvFN5mCu9s0lJ6Eek29bpzgwJcRNK2cuNfOHfhipi2X/xjNaceMSSkioqbAlxE0qJed+5RgItIlybOX9ZpbvdbN55BSYotYSV4CnARSUq97tymABeRThTc+UHTCEWk3fs7d3cK7wsnVCm8c5R64CIFpqv9uLuiXnf+MffsbRBYXV3tdXV1Wfs8kWITvx93m/KyUuacdWTCIJ/31Hruem5jTNsLV53MZw/sF2itkj4zW+Xu1fHt6oGLFJBE+3EDNDW3ULNoLXVvf8Dy1xrbe+eJdg5Urzt/KMBFCkhX+3E3t7Ty4Ip32o80iw9vBXf+0UNMkQIyrLysy9cTDZj20v4leUsBLlJAZk4ZRVlpSbfek8XHYJJhGkIRKSBtDymv+49X2LGrJa33pOq1S+5SD1ykwEwfX0n9NaeldQqOToPPb2n3wM2sBKgDGtz9TDM7FHgI+AywCjjf3fcEU6aIdMdxN/6RbX/dHdO2ad4ZPL56S4/miEtu6s4Qyo+A9cBB0eubgNvc/SEz+7/ARcCdGa5PRLqpqwU508dXKrALSFoBbmbDgWnAXODy6EHGk4H/Gb3lXmAOCnCRjOjJakqtpCw+6fbAbweuBA6MXn8GaHL3vdHrd4GEf7rMbAYwA2DEiBE9LlSkWMSvpmxoaqZm0VqAhCG+7a+fcNyNS2PaZk0dzcUnjgy+WAlVygA3szOB7e6+ysxO6u4HuPtCYCFEltJ39/0ixSbRasrmllYWLN7QKcDV6y5u6fTAJwJnmdkZQD8iY+B3AOVm1jvaCx8ONARXpkjxSLaasmP7DU+8yi//vCnm9ZevOY2B/UsDrU1yS8pphO5e4+7D3b0K+CawzN3/AVgOfCN62wXA44FVKVJEks3LbmuvmvVkp/DePH+awrsI7c9Cnh8DD5nZDUA9cHdmShIpLvEPLCeNruB3qxpihlHKSktoaGruNGSi4ZLipu1kRUKUaPvXstISjh4xkBUbd9DqTi9gX9z7jqk6mH+/eEJWa5XwaDtZkRyU7IHlf731QfvGU/HhrV63tNFSepEQJXtgmejvxRUH9FV4SwwFuEiIurOR1Ps7d6e+SYqKAlwkRN3Z/lW7Bko8jYGLhKhtYc6lD6/u8j7tGiiJKMBFQpRsJWVPT5aX4qIAFwlQsiDe0tTMhPnLYu695X+M5RtfGg5o10BJj+aBiwQk2RzvRKfGa3aJdEXzwEWyLNkc745evX4K/fvoP0PpGf3JEQlIsjnebdTrlv2laYQiAUk27a+yvEzhLRmhABcJwO69rTQk6IFrOqBkkoZQRHoo2QyTRFMDDTQdUDJOs1BEeiDRDJPSEqOlNfa/p2cuO4EvDDkw/u0i3aJZKCIZlGiGSXx4a5xbgqYAF+mBrmaYKLglW1I+xDSzfmb2gpm9bGavmNl10fZ7zGyTma2O/hoXeLUiOaKrGSYi2ZJOD3w3MNndd5pZKfBnM/tD9LWZ7v5ocOWJ5IaODyyTPTXSDBPJtpQB7pGnnDujl6XRX9l78ikSskQPLNsM6t+HHbv2aIaJhCKtMXAzKwFWAYcBP3f3lWb2fWCumV0DLAVmubt2nJeCk+iBJUSGS/7frMkhVCQSkdZCHndvdfdxwHDgWDM7CqgBRgPHAIOInFLfiZnNMLM6M6trbGzMTNUiWTLvD+sTLsiB1EvlRYLWrZWY7t4ELAdOd/etHrEb+DVwbJL3LHT3anevrqio2O+CRbKlataT3PWnjUlf1wk5EraUQyhmVgG0uHuTmZUBpwI3mdlQd99qZgZMB9YFW6pIdiRaSRm/DaweWEouSGcMfChwb3QcvBfwiLs/YWbLouFuwGrg4uDKFAneJy2tjP7J0zFtP5x8GJefNkon5EhO0lJ6EZIfbSaSC7SUXiSBxa+8x/fuXxXT9nzNZIYO1Pi25D4FuBQt9bol3ynApehMmLeULR9+EtOm4JZ8pACXouHuHFrzVEzb+BHlPPbPE0OqSGT/KMClKGi4RAqRAlwK2sbGnUy+9U8xbb/57nFMGDk4pIpEMkcBLgVLvW4pdApwKThX167lgRXvxLS9OXcqvUt0hrcUFgW4FBT1uqWYKMClICi4pRgpwCWvfbx7L0deuzimrWbqaL534siQKhLJHgW45C31uqXYKcAl79TWN3Dpw6tj2l686hQqDuwbTkEiIVGAS15Rr1vkUwpwyQtHXbuYnbv3xrQpuKXYKcAlVKkOSki0f8kJX6jgvm8nPMFPpKgowCU0tfUN1Cxa235UWUNTMzWL1gIwfXylhktEUkjnTMx+wHNA3+j9j7r7tWZ2KPAQ8BlgFXC+u+8JslgpLAsWb4g5ZxKguaWVG59a3+kh5e++P4Evfe7gLFYnkvvSWVu8G5js7mOBccDpZvZl4CbgNnc/DNgBXBRYlVKQtjQ1J2zf/tHumOvN86cpvEUSSBngHrEzelka/eXAZODRaPu9RE6mF0motr6BifOXceisJ5k4fxm19Q0MK099bFlZaQm19Q1ZqFAk/6S1u4+ZlZjZamA7sAR4C2hy97ZpAe8CCY/oNrMZZlZnZnWNjY0ZKFnyTdtYd0NTM86nY92TRldQVlrS5XubW1pZsHhDdgoVyTNpPcR091ZgnJmVA48Bo9P9AHdfCCyEyKn0PahR8kSyGSXJxrrjdwxMJtlQi0ix69YsFHdvMrPlwFeAcjPrHe2FDwf099wi1tWMkv0N4HSGWkSKUcohFDOriPa8MbMy4FRgPbAc+Eb0tguAxwOqUfJAsl72gsUbUgZwZRevl5WWMHPKqIzUKFJo0hkDHwosN7M1wIvAEnd/AvgxcLmZvUlkKuHdwZUpuS5ZL3tLUzMzp4yiT4LDFOafPYbN86cxc8qohGPh5WWlzDt7TMzCHhH5VMohFHdfA4xP0L4R0HI4ASLDHA0JQnxYeVmnOd0At587rj2Y237vakWmiHRm7tl7rlhdXe11dXVZ+zzJnvgxcIBeBvvi/nhpJaVI95nZKnevjm/XUnrJiI696LaeeMfw1iELIpmnAJeMmT6+MuFwiXrdIsFQgEtGvLl9J6f87E8xbf955SQOGdQ/pIpECp8CXPabdg0UCYcCXHrs5qdf4/88+1ZM26Z5Z2BmIVUkUlwU4NIj8b3u4w8bzAPfOS6kakSKkwJcgNQn47TRcIlI7lCAS8qTcQA+bG5h7HXPxLzv3m8fy4lfqMhusSLSTgEuXe5joqPNRHKXAlyS7mPS0NTcKbzXX386ZX263sNbRLIjrQMdpLClu13r5vnTFN4iOUQBLkl3A2yzef40DZmI5CANoQjTx1eyz53LH3k5pl37l4jkNgW46CGlSJ5SgBextxp3cvKtsfuXrJx9MkMO6hdSRSLSHSkD3MwOAe4DhgAOLHT3O8xsDvBdoO2o+dnu/lRQhUpmqdctkv/S6YHvBa5w95fM7EBglZktib52m7vfElx5kmk/X/4mCxZviGnT/iUi+SmdI9W2AlujP39kZusBnXWVh+J73SccPpj7LtL+JSL5qlvTCM2sisj5mCujTT8wszVm9iszOzjJe2aYWZ2Z1TU2Nia6RQJ2+FVPJRwyeXHzDmrrG0KoSEQyIe0AN7MDgN8Bl7r7X4E7gZHAOCI99FsTvc/dF7p7tbtXV1Ro34xs+nBXC1WznqSlNfG5p23L5UUkP6U1C8XMSomE94PuvgjA3bd1eP0XwBOBVCg9kqjHnUiyZfQikvtS9sAt8nTrbmC9u/+sQ/vQDrd9DViX+fKkO2rrGzj6p0s6hffrN0ylMsly+XSX0YtI7klnCGUicD4w2cxWR3+dAdxsZmvNbA0wCbgsyEKla7X1DVz68Go++HhPe5sZ3H7uOPr07pVwuXxZaQkzp4zKdqkikiHpzEL5M5BojpnmfOeIn9Su4/4Vb3dqd6d9S9i2fb3TObRBRPKDVmLmsdZ9zsjZXf9/tOMYd8cgF5H8pwDPU+k+pNQYt0jh0nayeWbz+x93Cu+XrzmN288dpzFukSKjHngeiQ/ukRUDWHrFSQAa4xYpQgrwPHD/irf5SW3sLM1EG09pjFukuCjAc1x8r/u6s47kgglV4RQjIjlFAR6i2vqGpEMep932J17ftjPmfm33KiIdKcBDUlvfQM2itTS3tAKRE+BrFq1l1569zH4sdrhk+b+exKGDB4RRpojkMAV4SBYs3tAe3m2aW1o7hbd63SKSjAI8JKk2kXpz7lR6l2iWp4gkp4QISbIFNv37lLB5/jSFt4ikpJQIyfgR5Z3aykpLuPFrY7JfjIjkJQ2hZNne1n0cdtUfOrVXauGNiHSTAjyLTr71Wd5q/DimTQ8pRaSnFOBZ0NDUzMT5y2LaXr1+Cv376F+/iPScEiQDulqQE7+S8lvHjmDe2RrnFpH9lzLAzewQ4D5gCODAQne/w8wGAQ8DVcBm4Bx33xFcqbkp2YKcTe9/zB1L34i5V8MlIpJJ6cxC2Qtc4e5HAF8GLjGzI4BZwFJ3PxxYGr0uOskW5MSHd2V5GbX1DdksTUQKXMoAd/et7v5S9OePgPVAJfBV4N7obfcC0wOqMad1tSCn4zl0bT1zhbiIZEq35oGbWRUwHlgJDHH3rdGX3iMyxJLoPTPMrM7M6hobG/en1pyUbEFOLyLjTR01t7SyYPGGwGsSkeKQd2QHA74BL3f2vHV9zd6dzXrW9ttDdq929uqKiYr+KzUUH9O38GKGstIR9Se5PtYReRCRdaQW4mZUSCe8H3X1RtHmbmQ2Nvj4U2B5MiblpY+NOqmY9yYZtH8W0V5aXMe/sMVQm6ZnrjEoRyZR0ZqEYcDew3t1/1uGl3wMXAPOjvz8eSIU5KH5q4D3/dAwnjfpsp/s6zk4BnVEpIpmVzjzwicD5wFozWx1tm00kuB8xs4uAt4FzAqkwhzy5ZiuX/Oal9msz2DQv8dRAnVEpIkGzyPB1dlRXV3tdXV3WPi9TWvc5I2c/FdP2X7MmazhERLLCzFa5e3V8u1ZipnB17VoeWPFO+/Xfjx3G//rW+BArEhGJUIAn0fjRbo6Z+8eYttdvmEqf3tqBV0RygwI8gS/9dAl/+XhP+/XN3/gi51QfEmJFIiKdKcA7eGHTB5xz1/Mxbdq/RERylQIccHcOrYl9SPnUD/+OI4YdFFJFIiKpFX2A3/nsW9z09Gvt12MqB/If/3J8iBWJiKSnaAP84917OfLaxTFta+acxkH9SkOqSESke4oywM+563le2PRB+/Vlp3yBH51yeIgViYh0X8EGeKJTco4cdhCn3vZczH2b5p1BZLcAEZH8UpABnuiUnEsfXh1zz4PfOY6Jhw0OoToRkcwoyABPdEpOm/59Snj1+tOzXJGISOYV5LLCrvbcVniLSKEoyAA/eECfhO3J9ugWEclHBTWEsnP3XsZe9wyt+zrvsKi9uEWk0BRMD/zny9/kqGsXt4f3zNNGUVlehvHpKTnai1tECkne98D/+4Nd/N3Ny9uvL5xQxZyzjgTgksmHhVWWiEjg0jlS7VfAmcB2dz8q2jYH+C7Qdsz8bHd/KvE/IRjuzvcfeImnX3mvva3u6lMYfEDfbJYhIhKadHrg9wD/G7gvrv02d78l4xWlYeXGv3DuwhXt1zd9fQznHjMijFJEREKTMsDd/Tkzq8pCLQl1XFE5dGA/Pt7TyofNLQAMP7iMpVecSN/eJWGVJyISmv0ZA/+Bmf0jUAdc4e47Et1kZjOAGQAjRnSvlxy/onLLh5+0v/bvF3+FY6oG9axyEZEC0NNZKHcCI4FxwFbg1mQ3uvtCd6929+qKiopufUiyFZXDBvZTeItI0etRgLv7Nndvdfd9wC+AYzNbVkSyFZVbO/TERUSKVY8C3MyGdrj8GrAuM+XEGpZk5WSydhGRYpIywM3st8DzwCgze9fMLgJuNrO1ZrYGmARcFkRxM6eMoqw09gGlVlSKiESkMwvlWwma7w6glk7aVk7G7+utFZUiInmwEnP6+EoFtohIAgWzF4qISLFRgIuI5CkFuIhInlKAi4jkKQW4iEieMvfOp9cE9mFmjcDbWfvA7hkMvB92ESHS9y/e71/M3x3y4/t/zt077UWS1QDPZWZW5+7VYdcRFn3/4v3+xfzdIb+/v4ZQRETylAJcRCRPKcA/tTDsAkKm71+8ivm7Qx5/f42Bi4jkKfXARUTylAJcRCRPKcABMysxs3ozeyLsWrLNzMrN7FEze83M1pvZV8KuKZvM7DIze8XM1pnZb82sX9g1BcnMfmVm281sXYe2QWa2xMzeiP5+cJg1BinJ918Q/fO/xsweM7PyEEvsFgV4xI+A9WEXEZI7gKfdfTQwliL692BmlcAPgWp3PwooAb4ZblWBuwc4Pa5tFrDU3Q8HlkavC9U9dP7+S4Cj3P2LwOtATbaL6qmiD3AzGw5MA34Zdi3ZZmYDgROIHtDh7nvcvSnUorKvN1BmZr2B/sCWkOsJlLs/B3wQ1/xV4N7oz/cC07NZUzYl+v7u/oy7741ergCGZ72wHir6AAduB64E9oVcRxgOBRqBX0eHkH5pZgPCLipb3L0BuAV4B9gKfOjuz4RbVSiGuPvW6M/vAUPCLCZk3wb+EHYR6SrqADezM4Ht7r4q7FpC0hs4GrjT3ccDH1PYf32OER3r/SqR/5ENAwaY2XnhVhUuj8wrLsq5xWZ2FbAXeDDsWtJV1AEOTATOMrPNwEPAZDN7INySsupd4F13Xxm9fpRIoBeLU4BN7t7o7i3AImBCyDWFYZuZDQWI/r495HqyzswuBM4E/sHzaHFMUQe4u9e4+3B3ryLy8GqZuxdND8zd3wP+28xGRZtOBl4NsaRsewf4spn1NzMj8v2L5iFuB78HLoj+fAHweIi1ZJ2ZnU5kGPUsd98Vdj3dkfOHGkvg/gV40Mz6ABuBfwq5nqxx95Vm9ijwEpG/OteTx8uq02FmvwVOAgab2bvAtcB84BEzu4jIds/nhFdhsJJ8/xqgL7Ak8v9xVrj7xaEV2Q1aSi8ikqeKeghFRCSfKcBFRPKUAlxEJE8pwEVE8pQCXEQkTynARUTylAJcRCRP/X/WlkqiYtOsfQAAAABJRU5ErkJggg==\n" }, { "id": 2034723533728, "title": "Generate some data to plot", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 189, 85 @@ -136,7 +136,7 @@ { "id": 2034723677808, "title": "Slider", - "block_type": "OCBSliderBlock", + "block_type": "SliderBlock", "splitter_pos": [], "position": [ -890.625, @@ -187,7 +187,7 @@ { "id": 2034723714816, "title": "Slider", - "block_type": "OCBSliderBlock", + "block_type": "SliderBlock", "splitter_pos": [], "position": [ -901.1874999999999, @@ -238,7 +238,7 @@ { "id": 2034879162976, "title": "Regression", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 0, 276 @@ -292,7 +292,7 @@ { "id": 2034879286288, "title": "Show user input", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 0, 220 @@ -346,7 +346,7 @@ { "id": 2034886210608, "title": "Create a new linear model", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 90, 85 @@ -400,7 +400,7 @@ { "id": 2136886539168, "title": "Show user input", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 0, 278 diff --git a/examples/mnist.ipyg b/examples/mnist.ipyg index d8fd5740..a32f151a 100644 --- a/examples/mnist.ipyg +++ b/examples/mnist.ipyg @@ -4,7 +4,7 @@ { "id": 2039122444152, "title": "Load MNIST dataset", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 131, 0 @@ -58,7 +58,7 @@ { "id": 2039123153688, "title": "Evaluation", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 75, 75 @@ -112,7 +112,7 @@ { "id": 2039123177336, "title": "Prediction example", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 0, 281 @@ -166,7 +166,7 @@ { "id": 2039123183800, "title": "Training", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 85, 229 @@ -220,7 +220,7 @@ { "id": 2039123243512, "title": "Build Keras CNN", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 253, 0 @@ -274,7 +274,7 @@ { "id": 2039171273928, "title": "Plot image example", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 0, 277 @@ -328,7 +328,7 @@ { "id": 2039171312808, "title": "Normalize dataset", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 73, 73 diff --git a/pyflow/__init__.py b/pyflow/__init__.py index 8fdd529a..59068cb4 100644 --- a/pyflow/__init__.py +++ b/pyflow/__init__.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Pyflow: An open-source tool for modular visual programing in python """ __appname__ = "Pyflow" __author__ = "Mathïs Fédérico" -__version__ = "0.0.1" +__version__ = "1.0.0-beta" diff --git a/pyflow/__main__.py b/pyflow/__main__.py index 7d13e2c8..01401c4b 100644 --- a/pyflow/__main__.py +++ b/pyflow/__main__.py @@ -1,10 +1,8 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium # pylint:disable=wrong-import-position -""" -Pyflow main module, run this to launch Pyflow -""" +""" Pyflow main module. """ import os import sys @@ -13,19 +11,20 @@ if os.name == "nt": # If on windows asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) -from qtpy.QtWidgets import QApplication -from pyflow.graphics.window import OCBWindow +from PyQt5.QtWidgets import QApplication +from pyflow.graphics.window import Window +from pyflow import __version__ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) if __name__ == "__main__": app = QApplication(sys.argv) app.setStyle("Fusion") - wnd = OCBWindow() + wnd = Window() if len(sys.argv) > 1: wnd.createNewMdiChild(sys.argv[1]) - wnd.setWindowTitle("Pyflow Beta v0.1") + wnd.setWindowTitle(f"Pyflow {__version__}") wnd.show() sys.exit(app.exec_()) diff --git a/pyflow/blocks/__init__.py b/pyflow/blocks/__init__.py index a22024d9..5d646f21 100644 --- a/pyflow/blocks/__init__.py +++ b/pyflow/blocks/__init__.py @@ -1,10 +1,10 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the OCB Blocks of different types. """ +""" Module for the Blocks of different types. """ -from pyflow.blocks.sliderblock import OCBSliderBlock -from pyflow.blocks.codeblock import OCBCodeBlock -from pyflow.blocks.markdownblock import OCBMarkdownBlock -from pyflow.blocks.drawingblock import OCBDrawingBlock -from pyflow.blocks.containerblock import OCBContainerBlock +from pyflow.blocks.sliderblock import SliderBlock +from pyflow.blocks.codeblock import CodeBlock +from pyflow.blocks.markdownblock import MarkdownBlock +from pyflow.blocks.drawingblock import DrawingBlock +from pyflow.blocks.containerblock import ContainerBlock diff --git a/pyflow/blocks/block.py b/pyflow/blocks/block.py index 917b0b4f..49e89d3f 100644 --- a/pyflow/blocks/block.py +++ b/pyflow/blocks/block.py @@ -1,8 +1,8 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium # pylint:disable=unused-argument -""" Module for the base OCB Block. """ +""" Module for the base Block.""" from typing import TYPE_CHECKING, Optional, OrderedDict, Tuple, Union @@ -17,16 +17,16 @@ ) from pyflow.core.serializable import Serializable -from pyflow.core.socket import OCBSocket -from pyflow.blocks.widgets import OCBSplitter, OCBSizeGrip, OCBTitle +from pyflow.core.socket import Socket +from pyflow.blocks.widgets import Splitter, SizeGrip, Title if TYPE_CHECKING: - from pyflow.scene.scene import OCBScene + from pyflow.scene.scene import Scene BACKGROUND_COLOR = QColor("#E3212121") -class OCBBlock(QGraphicsItem, Serializable): +class Block(QGraphicsItem, Serializable): """Base class for blocks in Pyflow.""" @@ -49,7 +49,7 @@ def __init__( width: int = DEFAULT_DATA["width"], height: int = DEFAULT_DATA["height"], edge_size: float = 10.0, - title: Union[OCBTitle, str] = DEFAULT_DATA["title"], + title: Union[Title, str] = DEFAULT_DATA["title"], parent: Optional["QGraphicsItem"] = None, ): """Base class for blocks in Pyflow. @@ -86,14 +86,14 @@ def __init__( self.root.setAttribute(Qt.WA_TranslucentBackground) self.root.setGeometry(0, 0, int(width), int(height)) - self.title_widget = OCBTitle(title, parent=self.root) + self.title_widget = Title(title, parent=self.root) self.title_widget.setAttribute(Qt.WA_TranslucentBackground) - self.splitter = OCBSplitter(self, Qt.Vertical, self.root) + self.splitter = Splitter(self, Qt.Vertical, self.root) - self.size_grip = OCBSizeGrip(self, self.root) + self.size_grip = SizeGrip(self, self.root) - if type(self) == OCBBlock: + if type(self) == Block: # This has to be called at the end of the constructor of # every class inheriting this. self.holder.setWidget(self.root) @@ -107,8 +107,8 @@ def __init__( self.moved = False self.metadata = {} - def scene(self) -> "OCBScene": - """Get the current OCBScene containing the block.""" + def scene(self) -> "Scene": + """Get the current Scene containing the block.""" return super().scene() def boundingRect(self) -> QRectF: @@ -139,12 +139,12 @@ def paint( 0, 0, self.width, self.height, self.edge_size, self.edge_size ) painter.setPen( - self._pen_outline_selected if self.isSelected() else self._pen_outline + self._pen_outline_selected if self.isSelected() else self.pen_outline ) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawPath(path_outline.simplified()) - def get_socket_pos(self, socket: OCBSocket) -> Tuple[float]: + def get_socket_pos(self, socket: Socket) -> Tuple[float]: """Get a socket position to place them on the block sides.""" if socket.socket_type == "input": x = 0 @@ -166,7 +166,7 @@ def update_sockets(self): for socket in self.sockets_in + self.sockets_out: socket.setPos(*self.get_socket_pos(socket)) - def add_socket(self, socket: OCBSocket): + def add_socket(self, socket: Socket): """Add a socket to the block.""" if socket.socket_type == "input": self.sockets_in.append(socket) @@ -174,7 +174,7 @@ def add_socket(self, socket: OCBSocket): self.sockets_out.append(socket) self.update_sockets() - def remove_socket(self, socket: OCBSocket): + def remove_socket(self, socket: Socket): """Remove a socket from the block.""" if socket.socket_type == "input": self.sockets_in.remove(socket) @@ -184,14 +184,14 @@ def remove_socket(self, socket: OCBSocket): self.update_sockets() def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent): - """OCBBlock reaction to a mouseReleaseEvent.""" + """Block reaction to a mouseReleaseEvent.""" if self.moved: self.moved = False self.scene().history.checkpoint("Moved block", set_modified=True) super().mouseReleaseEvent(event) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): - """OCBBlock reaction to a mouseMoveEvent.""" + """Block reaction to a mouseMoveEvent.""" super().mouseMoveEvent(event) self.moved = True @@ -204,7 +204,7 @@ def remove(self): scene.removeItem(self) def update_splitter(self): - """Change the geometry of the splitter to match the block""" + """Change the geometry of the splitter to match the block.""" # We make the resizing of splitter only affect # the last element of the split view self.splitter.setGeometry( @@ -215,7 +215,7 @@ def update_splitter(self): ) def update_title(self): - """Change the geometry of the title to match the block""" + """Change the geometry of the title to match the block.""" self.title_widget.setGeometry( int(self.edge_size), int(self.edge_size / 2), @@ -224,7 +224,7 @@ def update_title(self): ) def update_size_grip(self): - """Change the geometry of the size grip to match the block""" + """Change the geometry of the size grip to match the block.""" self.size_grip.setGeometry( int(self.width - self.edge_size * 2), int(self.height - self.edge_size * 2), @@ -267,8 +267,13 @@ def height(self): def height(self, value: float): self.root.setGeometry(0, 0, self.root.width(), int(value)) + @property + def pen_outline(self) -> QPen: + """The current pen used to draw the outline of the Block.""" + return self._pen_outline + def serialize(self) -> OrderedDict: - """Return a serialized version of this widget""" + """Return a serialized version of this widget.""" self.metadata.update({"title_metadata": self.title_widget.serialize()}) metadata = OrderedDict(sorted(self.metadata.items())) return OrderedDict( @@ -292,7 +297,7 @@ def serialize(self) -> OrderedDict: ) def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None: - """Restore the block from serialized data""" + """Restore the block from serialized data.""" if restore_id and "id" in data: self.id = data["id"] @@ -317,7 +322,7 @@ def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None # Deserialize new sockets for socket_data in data["sockets"]: - socket = OCBSocket(block=self) + socket = Socket(block=self) socket.deserialize(socket_data, hashmap, restore_id) self.add_socket(socket) if hashmap is not None: diff --git a/blocks/empty.pfb b/pyflow/blocks/blockfiles/empty.pfb similarity index 90% rename from blocks/empty.pfb rename to pyflow/blocks/blockfiles/empty.pfb index 8469d16b..a068cf06 100644 --- a/blocks/empty.pfb +++ b/pyflow/blocks/blockfiles/empty.pfb @@ -1,6 +1,6 @@ { "title": "Code", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "source": "", "stdout": "", "image": "", diff --git a/blocks/markdown.pfb b/pyflow/blocks/blockfiles/markdown.pfb similarity index 88% rename from blocks/markdown.pfb rename to pyflow/blocks/blockfiles/markdown.pfb index b47dc633..ab379a48 100644 --- a/blocks/markdown.pfb +++ b/pyflow/blocks/blockfiles/markdown.pfb @@ -1,6 +1,6 @@ { "title": "Markdown", - "block_type": "OCBMarkdownBlock", + "block_type": "MarkdownBlock", "text": "", "splitter_pos": [88,41], "width": 618, diff --git a/pyflow/blocks/codeblock.py b/pyflow/blocks/codeblock.py index 96b54de3..2329b3e6 100644 --- a/pyflow/blocks/codeblock.py +++ b/pyflow/blocks/codeblock.py @@ -1,29 +1,23 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the base OCB Code Block. """ +""" Module for the base Code Block.""" -from typing import OrderedDict, Optional -from PyQt5.QtWidgets import ( - QPushButton, - QTextEdit, - QWidget, - QStyleOptionGraphicsItem, -) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPen, QColor, QPainter, QPainterPath +from typing import OrderedDict +from PyQt5.QtWidgets import QPushButton, QTextEdit +from PyQt5.QtGui import QPen, QColor from ansi2html import Ansi2HTMLConverter -from pyflow.blocks.block import OCBBlock +from pyflow.blocks.block import Block -from pyflow.blocks.executableblock import OCBExecutableBlock +from pyflow.blocks.executableblock import ExecutableBlock from pyflow.core.pyeditor import PythonEditor conv = Ansi2HTMLConverter() -class OCBCodeBlock(OCBExecutableBlock): +class CodeBlock(ExecutableBlock): """ Code Block @@ -36,16 +30,17 @@ class OCBCodeBlock(OCBExecutableBlock): """ DEFAULT_DATA = { - **OCBBlock.DEFAULT_DATA, + **Block.DEFAULT_DATA, "source": "", } - MANDATORY_FIELDS = OCBBlock.MANDATORY_FIELDS + MANDATORY_FIELDS = Block.MANDATORY_FIELDS def __init__(self, source: str = "", **kwargs): """ - Create a new OCBCodeBlock. - Initialize all the child widgets specific to this block type + Create a new CodeBlock. + Initialize all the child widgets specific to this block type. + """ super().__init__(**kwargs) @@ -66,13 +61,10 @@ def __init__(self, source: str = "", **kwargs): self.has_been_run = False self.blocks_to_run = [] - self._pen_outline = QPen(QColor("#7F000000")) - self._pen_outline_running = QPen(QColor("#FF0000")) - self._pen_outline_transmitting = QPen(QColor("#00ff00")) self._pen_outlines = [ - self._pen_outline, - self._pen_outline_running, - self._pen_outline_transmitting, + QPen(QColor("#7F000000")), # Idle + QPen(QColor("#FF0000")), # Running + QPen(QColor("#00ff00")), # Transmitting ] # Add output pannel @@ -92,14 +84,14 @@ def __init__(self, source: str = "", **kwargs): self.update_all() # Set the geometry of display and source_editor def init_output_panel(self): - """Initialize the output display widget: QLabel""" + """Initialize the output display widget: QLabel.""" output_panel = QTextEdit() output_panel.setReadOnly(True) output_panel.setFont(self.source_editor.font()) return output_panel def init_run_button(self): - """Initialize the run button""" + """Initialize the run button.""" run_button = QPushButton(">", self.root) run_button.move(int(self.edge_size), int(self.edge_size / 2)) run_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) @@ -107,7 +99,7 @@ def init_run_button(self): return run_button def init_run_all_button(self): - """Initialize the run all button""" + """Initialize the run all button.""" run_all_button = QPushButton(">>", self.root) run_all_button.setFixedSize(int(3 * self.edge_size), int(3 * self.edge_size)) run_all_button.clicked.connect(self.handle_run_right) @@ -116,21 +108,21 @@ def init_run_all_button(self): return run_all_button def handle_run_right(self): - """Called when the button for "Run All" was pressed""" - if self.run_color != 0: + """Called when the button for "Run All" was pressed.""" + if self.run_state != 0: self._interrupt_execution() else: self.run_right() def handle_run_left(self): - """Called when the button for "Run Left" was pressed""" - if self.run_color != 0: + """Called when the button for "Run Left" was pressed.""" + if self.run_state != 0: self._interrupt_execution() else: self.run_left() def run_code(self): - """Run the code in the block""" + """Run the code in the block.""" # Reset stdout self._cached_stdout = "" @@ -141,8 +133,13 @@ def run_code(self): super().run_code() # actually run the code + def execution_finished(self): + super().execution_finished() + self.run_button.setText(">") + self.run_all_button.setText(">>") + def update_title(self): - """Change the geometry of the title widget""" + """Change the geometry of the title widget.""" self.title_widget.setGeometry( int(self.edge_size) + self.run_button.width(), int(self.edge_size / 2), @@ -151,7 +148,7 @@ def update_title(self): ) def update_output_panel(self): - """Change the geometry of the output panel""" + """Change the geometry of the output panel.""" # Close output panel if no output if self.stdout == "": self.previous_splitter_size = self.splitter.sizes() @@ -159,50 +156,21 @@ def update_output_panel(self): self.splitter.setSizes([1, 0]) def update_run_all_button(self): - """Change the geometry of the run all button""" + """Change the geometry of the run all button.""" self.run_all_button.move( int(self.width - self.edge_size - self.run_button.width()), int(self.edge_size / 2), ) def update_all(self): - """Update the code block parts""" + """Update the code block parts.""" super().update_all() self.update_output_panel() self.update_run_all_button() - def paint( - self, - painter: QPainter, - option: QStyleOptionGraphicsItem, - widget: Optional[QWidget] = None, - ): - """Paint the code block""" - path_content = QPainterPath() - path_content.setFillRule(Qt.FillRule.WindingFill) - path_content.addRoundedRect( - 0, 0, self.width, self.height, self.edge_size, self.edge_size - ) - painter.setPen(Qt.PenStyle.NoPen) - painter.setBrush(self._brush_background) - painter.drawPath(path_content.simplified()) - - # outline - path_outline = QPainterPath() - path_outline.addRoundedRect( - 0, 0, self.width, self.height, self.edge_size, self.edge_size - ) - painter.setPen( - self._pen_outline_selected - if self.isSelected() - else self._pen_outlines[self.run_color] - ) - painter.setBrush(Qt.BrushStyle.NoBrush) - painter.drawPath(path_outline.simplified()) - @property def source(self) -> str: - """Source code""" + """Source code.""" return self._source @source.setter @@ -217,19 +185,13 @@ def source(self, value: str): self._source = value @property - def run_color(self) -> int: - """Run color""" - return self._run_color - - @run_color.setter - def run_color(self, value: int): - self._run_color = value - # Update to force repaint - self.update() + def pen_outline(self) -> QPen: + """The current pen used to draw the outline of the CodeBlock.""" + return self._pen_outlines[self.run_state] @property def stdout(self) -> str: - """Access the content of the output panel of the block""" + """Access the content of the output panel of the block.""" return self._stdout @stdout.setter @@ -254,8 +216,8 @@ def stdout(self, value: str): self.splitter.setSizes([1, 0]) @staticmethod - def str_to_html(text: str): - """Format text so that it's properly displayed by the code block""" + def str_to_html(text: str) -> str: + """Format text so that it's properly displayed by the code block.""" # Remove carriage returns and backspaces text = text.replace("\x08", "") text = text.replace("\r", "") @@ -268,7 +230,7 @@ def str_to_html(text: str): return text def handle_stdout(self, value: str): - """Handle the stdout signal""" + """Handle the stdout signal.""" # If there is a new line # Save every line but the last one @@ -281,16 +243,16 @@ def handle_stdout(self, value: str): self.stdout = self._cached_stdout + value @staticmethod - def b64_to_html(image: str): - """Transform a base64 encoded image into a html image""" + def b64_to_html(image: str) -> str: + """Transform a base64 encoded image into a html image.""" return f'' def handle_image(self, image: str): - """Handle the image signal""" + """Handle the image signal.""" self.stdout = "" + image def serialize(self): - """Serialize the code block""" + """Serialize the code block.""" base_dict = super().serialize() base_dict["source"] = self.source base_dict["stdout"] = self.stdout @@ -300,7 +262,7 @@ def serialize(self): def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): - """Restore a codeblock from it's serialized state""" + """Restore a codeblock from it's serialized state.""" self.complete_with_default(data) diff --git a/pyflow/blocks/containerblock.py b/pyflow/blocks/containerblock.py index b2aec37b..af385ba6 100644 --- a/pyflow/blocks/containerblock.py +++ b/pyflow/blocks/containerblock.py @@ -1,12 +1,17 @@ -""" -Exports OCBContainerBlock. +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for the ContainerBlock. + +A block that can contain other blocks. + """ from PyQt5.QtWidgets import QVBoxLayout -from pyflow.blocks.block import OCBBlock +from pyflow.blocks.block import Block -class OCBContainerBlock(OCBBlock): +class ContainerBlock(Block): """ A block that can contain other blocks. """ @@ -18,10 +23,9 @@ def __init__(self, **kwargs): # Due to the overall structure of the code, this cannot be removed, as the # scene should be able to serialize blocks. # This is not due to bad code design and should not be removed. - from pyflow.graphics.view import ( - OCBView, - ) # pylint: disable=cyclic-import - from pyflow.scene.scene import OCBScene # pylint: disable=cyclic-import + # pylint: disable=import-outside-toplevel, cyclic-import + from pyflow.graphics.view import View + from pyflow.scene.scene import Scene self.layout = QVBoxLayout(self.root) self.layout.setContentsMargins( @@ -31,8 +35,8 @@ def __init__(self, **kwargs): self.edge_size * 2, ) - self.child_scene = OCBScene() - self.child_view = OCBView(self.child_scene) + self.child_scene = Scene() + self.child_view = View(self.child_scene) self.layout.addWidget(self.child_view) self.holder.setWidget(self.root) diff --git a/pyflow/blocks/drawingblock.py b/pyflow/blocks/drawingblock.py index 7a7b3a64..4ae08cfe 100644 --- a/pyflow/blocks/drawingblock.py +++ b/pyflow/blocks/drawingblock.py @@ -1,6 +1,12 @@ +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium # pylint:disable=unused-argument -""" Module for the base OCB Drawing Block. """ +""" Module for the Drawing Block. + +A block in which you can draw. + +""" from math import floor import json @@ -9,19 +15,19 @@ from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtGui import QColor, QMouseEvent, QPaintEvent, QPainter from PyQt5.QtWidgets import QPushButton, QWidget -from pyflow.blocks.executableblock import OCBExecutableBlock +from pyflow.blocks.executableblock import ExecutableBlock eps = 1 class DrawableWidget(QWidget): - """A drawable widget is a canvas like widget on which you can doodle""" + """A drawable widget is a canvas like widget on which you can doodle.""" on_value_changed = pyqtSignal() def __init__(self, parent: QWidget): - """Create a new Drawable widget""" + """Create a new Drawable widget.""" super().__init__(parent) self.setAttribute(Qt.WA_PaintOnScreen) self.pixel_width = 24 @@ -35,13 +41,13 @@ def __init__(self, parent: QWidget): self.color_buffer[-1].append(0) def clearDrawing(self): - """Clear the drawing""" + """Clear the drawing.""" for i in range(self.pixel_width): for j in range(self.pixel_height): self.color_buffer[i][j] = 0 def paintEvent(self, evt: QPaintEvent): - """Draw the content of the widget""" + """Draw the content of the widget.""" painter = QPainter(self) for i in range(self.pixel_width): @@ -61,7 +67,7 @@ def paintEvent(self, evt: QPaintEvent): ) def mouseMoveEvent(self, evt: QMouseEvent): - """Change the drawing when dragging the mouse around""" + """Change the drawing when dragging the mouse around.""" if self.mouse_down: x = floor(evt.x() / self.width() * self.pixel_width) y = floor(evt.y() / self.height() * self.pixel_height) @@ -71,20 +77,20 @@ def mouseMoveEvent(self, evt: QMouseEvent): self.on_value_changed.emit() def mousePressEvent(self, evt: QMouseEvent): - """Signal that the drawing starts""" + """Signal that the drawing starts.""" self.mouse_down = True def mouseReleaseEvent(self, evt: QMouseEvent): - """Signal that the drawing stops""" + """Signal that the drawing stops.""" self.mouse_down = False -class OCBDrawingBlock(OCBExecutableBlock): +class DrawingBlock(ExecutableBlock): - """An OCBBlock on which you can draw, to test your CNNs for example""" + """An Block on which you can draw, to test your CNNs for example.""" def __init__(self, **kwargs): - """Create a new OCBBlock""" + """Create a new Block.""" super().__init__(**kwargs) self.draw_area = DrawableWidget(self.root) @@ -103,7 +109,7 @@ def __init__(self, **kwargs): @property def drawing(self): - """A json-encoded representation of the drawing""" + """A json-encoded representation of the drawing.""" return json.dumps(self.draw_area.color_buffer) @drawing.setter @@ -111,7 +117,7 @@ def drawing(self, value: str): self.draw_area.color_buffer = json.loads(value) def serialize(self): - """Return a serialized version of this widget""" + """Return a serialized version of this widget.""" base_dict = super().serialize() base_dict["drawing"] = self.drawing @@ -125,7 +131,7 @@ def valueChanged(self): @property def source(self): - """The "source code" of the drawingblock i.e an assignement to the drawing buffer""" + """The "source code" of the drawingblock i.e an assignement to the drawing buffer.""" python_code = f"{self.var_name} = {repr(self.draw_area.color_buffer)}" return python_code @@ -136,7 +142,7 @@ def source(self, value: str): def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): - """Restore a markdown block from it's serialized state""" + """Restore a markdown block from it's serialized state.""" for dataname in ["drawing"]: if dataname in data: setattr(self, dataname, data[dataname]) diff --git a/pyflow/blocks/executableblock.py b/pyflow/blocks/executableblock.py index 590a0e0b..8867098f 100644 --- a/pyflow/blocks/executableblock.py +++ b/pyflow/blocks/executableblock.py @@ -1,15 +1,22 @@ -""" Module for the executable block class """ +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for the abstract ExecutableBlock class. + +An abstract block that allows for execution, like CodeBlocks and Sliders. + +""" from typing import OrderedDict from abc import abstractmethod from PyQt5.QtCore import QTimer from PyQt5.QtWidgets import QApplication -from pyflow.blocks.block import OCBBlock -from pyflow.core.socket import OCBSocket +from pyflow.blocks.block import Block +from pyflow.core.socket import Socket -class OCBExecutableBlock(OCBBlock): +class ExecutableBlock(Block): """ Executable Block @@ -17,8 +24,8 @@ class OCBExecutableBlock(OCBBlock): This block type is not meant to be instanciated ! It's an abstract class that represents blocks that can be executed like: - - OCBCodeBlock - - OCBSlider + - CodeBlock + - Slider """ @@ -30,9 +37,7 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self.has_been_run = False - - # 0 for normal, 1 for running, 2 for transmitting - self.run_color = 0 + self._run_state = 0 # Each element is a list of blocks/edges to be animated # Running will paint each element one after the other @@ -42,31 +47,31 @@ def __init__(self, **kwargs): # Add execution flow sockets exe_sockets = ( - OCBSocket(self, socket_type="input", flow_type="exe"), - OCBSocket(self, socket_type="output", flow_type="exe"), + Socket(self, socket_type="input", flow_type="exe"), + Socket(self, socket_type="output", flow_type="exe"), ) for socket in exe_sockets: self.add_socket(socket) - if type(self) == OCBExecutableBlock: - raise RuntimeError("OCBExecutableBlock should not be instanciated directly") + if type(self) == ExecutableBlock: + raise RuntimeError("ExecutableBlock should not be instanciated directly") def has_input(self) -> bool: - """Checks whether a block has connected input blocks""" + """Checks whether a block has connected input blocks.""" for input_socket in self.sockets_in: - if len(input_socket.edges) != 0: + if input_socket.edges: return True return False def has_output(self) -> bool: - """Checks whether a block has connected output blocks""" + """Checks whether a block has connected output blocks.""" for output_socket in self.sockets_out: - if len(output_socket.edges) != 0: + if output_socket.edges: return True return False def run_code(self): - """Run the code in the block""" + """Run the code in the block.""" # Queue the code to execute code = self.source @@ -78,14 +83,12 @@ def run_code(self): self.has_been_run = True def execution_finished(self): - """Reset the text of the run buttons""" - self.run_color = 0 - self.run_button.setText(">") - self.run_all_button.setText(">>") + """Reset the text of the run buttons.""" + self.run_state = 0 self.blocks_to_run = [] def _interrupt_execution(self): - """Interrupt an execution, reset the blocks in the queue""" + """Interrupt an execution, reset the blocks in the queue.""" for block, _ in self.scene().kernel.execution_queue: # Reset the blocks that have not been run block.reset_has_been_run() @@ -104,7 +107,7 @@ def transmitting_animation_in(self): """ for elem in self.transmitting_queue[0]: # Set color to transmitting - elem.run_color = 2 + elem.run_state = 2 QApplication.processEvents() QTimer.singleShot(self.transmitting_delay, self.transmitting_animation_out) @@ -117,13 +120,13 @@ def transmitting_animation_out(self): # Reset color only if the block will not be run if hasattr(elem, "has_been_run"): if elem.has_been_run is True: - elem.run_color = 0 + elem.run_state = 0 else: - elem.run_color = 0 + elem.run_state = 0 QApplication.processEvents() self.transmitting_queue.pop(0) - if len(self.transmitting_queue) != 0: + if self.transmitting_queue: # If the queue is not empty, move forward in the animation self.transmitting_animation_in() else: @@ -148,7 +151,7 @@ def custom_bfs(self, start_node, reverse=False): to_transmit = [[start_node]] to_visit = [start_node] - while len(to_visit) != 0: + while to_visit: # Remove duplicates to_visit = list(set(to_visit)) @@ -203,7 +206,7 @@ def right_traversal(self): next_edges = [] next_blocks = [] - while len(to_visit_input) != 0 or len(to_visit_output) != 0: + while to_visit_input or to_visit_output: for block in to_visit_input.copy(): # Check input edges and blocks for input_socket in block.sockets_in: @@ -243,7 +246,7 @@ def right_traversal(self): return to_transmit def run_blocks(self): - """Run a list of blocks""" + """Run a list of blocks.""" for block in self.blocks_to_run[::-1]: if not block.has_been_run: block.run_code() @@ -251,13 +254,13 @@ def run_blocks(self): self.run_code() def run_left(self): - """Run all of the block's dependencies and then run the block""" + """Run all of the block's dependencies and then run the block.""" # Reset has_been_run to make sure that the self is run again self.has_been_run = False # To avoid crashing when spamming the button - if len(self.transmitting_queue) != 0: + if self.transmitting_queue: return # Gather dependencies @@ -274,10 +277,10 @@ def run_left(self): self.transmitting_animation_in() def run_right(self): - """Run all of the output blocks and all their dependencies""" + """Run all of the output blocks and all their dependencies.""" # To avoid crashing when spamming the button - if len(self.transmitting_queue) != 0: + if self.transmitting_queue: return # Create transmitting queue @@ -301,13 +304,13 @@ def run_right(self): self.transmitting_animation_in() def reset_has_been_run(self): - """Called when the output is an error""" + """Called when the output is an error.""" self.has_been_run = False @property @abstractmethod def source(self) -> str: - """Source code""" + """Source code.""" raise NotImplementedError("source(self) should be overriden") @source.setter @@ -315,18 +318,36 @@ def source(self) -> str: def source(self, value: str): raise NotImplementedError("source(self) should be overriden") + @property + def run_state(self) -> int: + """Run state. + + Describe the current state of the ExecutableBlock: + - 0: idle. + - 1: running. + - 2: transmitting. + + """ + return self._run_state + + @run_state.setter + def run_state(self, value: int): + self._run_state = value + # Update to force repaint + self.update() + def handle_stdout(self, value: str): - """Handle the stdout signal""" + """Handle the stdout signal.""" def handle_image(self, image: str): - """Handle the image signal""" + """Handle the image signal.""" def serialize(self): - """Return a serialized version of this block""" + """Return a serialized version of this block.""" return super().serialize() def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): - """Restore a codeblock from it's serialized state""" + """Restore a codeblock from it's serialized state.""" super().deserialize(data, hashmap, restore_id) diff --git a/pyflow/blocks/markdownblock.py b/pyflow/blocks/markdownblock.py index d184030c..1b1474bd 100644 --- a/pyflow/blocks/markdownblock.py +++ b/pyflow/blocks/markdownblock.py @@ -1,5 +1,10 @@ -""" -Exports OCBMarkdownBlock. +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for the MarkdownBlock. + +A block able to render Markdown. + """ from typing import OrderedDict @@ -9,16 +14,16 @@ from PyQt5.Qsci import QsciLexerMarkdown, QsciScintilla from PyQt5.QtCore import Qt from PyQt5.QtGui import QColor, QFont -from pyflow.blocks.block import OCBBlock +from pyflow.blocks.block import Block from pyflow.graphics.theme_manager import theme_manager -class OCBMarkdownBlock(OCBBlock): - """A block that is able to render markdown text""" +class MarkdownBlock(Block): + """A block that is able to render markdown text.""" def __init__(self, **kwargs): """ - Create a new OCBMarkdownBlock, a block that renders markdown + Create a new MarkdownBlock, a block that renders markdown """ super().__init__(**kwargs) @@ -53,7 +58,7 @@ def __init__(self, **kwargs): self.holder.setWidget(self.root) def valueChanged(self): - """Update markdown rendering when the content of the markdown editor changes""" + """Update markdown rendering when the content of the markdown editor changes.""" t = self.editor.text() dark_theme = """ @@ -69,7 +74,7 @@ def valueChanged(self): @property def text(self) -> str: - """The content of the markdown block""" + """The content of the markdown block.""" return self.editor.text() @text.setter @@ -86,7 +91,7 @@ def serialize(self): def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): - """Restore a markdown block from it's serialized state""" + """Restore a markdown block from it's serialized state.""" for dataname in ["text"]: if dataname in data: setattr(self, dataname, data[dataname]) diff --git a/pyflow/blocks/sliderblock.py b/pyflow/blocks/sliderblock.py index e64615fe..fb101a97 100644 --- a/pyflow/blocks/sliderblock.py +++ b/pyflow/blocks/sliderblock.py @@ -1,16 +1,19 @@ # Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for the SliderBlock. + +A block that can allows dynamic value modification using a slider. -""" -Exports OCBSliderBlock. """ from typing import OrderedDict from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QSlider, QVBoxLayout -from pyflow.blocks.executableblock import OCBExecutableBlock +from pyflow.blocks.executableblock import ExecutableBlock -class OCBSliderBlock(OCBExecutableBlock): +class SliderBlock(ExecutableBlock): """ Features a slider ranging from 0 to 1 and an area to choose what value to assign the slider to. """ @@ -45,7 +48,7 @@ def __init__(self, **kwargs): self.holder.setWidget(self.root) def valueChanged(self): - """This is called when the value of the slider changes""" + """This is called when the value of the slider changes.""" self.variable_value.setText(f"{self.value}") # Make sure that the slider is initialized before trying to run it. if self.scene() is not None: @@ -53,7 +56,7 @@ def valueChanged(self): @property def source(self): - """The "source code" of the slider i.e an assignement to the value of the slider""" + """The "source code" of the slider i.e an assignement to the value of the slider.""" python_code = f"{self.var_name} = {self.value}" return python_code @@ -63,7 +66,7 @@ def source(self, value: str): @property def value(self): - """The value of the slider""" + """The value of the slider.""" return str(self.slider.value() / 100) @value.setter @@ -72,7 +75,7 @@ def value(self, value: str): @property def var_name(self): - """The name of the python variable associated with the slider""" + """The name of the python variable associated with the slider.""" return self.variable_text.text() @var_name.setter @@ -80,7 +83,7 @@ def var_name(self, value: str): self.variable_text.setText(value) def serialize(self): - """Return a serialized version of this widget""" + """Return a serialized version of this widget.""" base_dict = super().serialize() base_dict["value"] = self.value base_dict["var_name"] = self.var_name @@ -90,7 +93,7 @@ def serialize(self): def deserialize( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True ): - """Restore a slider block from it's serialized state""" + """Restore a slider block from it's serialized state.""" for dataname in ["value", "var_name"]: if dataname in data: setattr(self, dataname, data[dataname]) diff --git a/pyflow/blocks/widgets/__init__.py b/pyflow/blocks/widgets/__init__.py index bcbaef76..4d6a5b5a 100644 --- a/pyflow/blocks/widgets/__init__.py +++ b/pyflow/blocks/widgets/__init__.py @@ -1,8 +1,8 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the OCB Blocks Widgets. """ +""" Module for the Blocks Widgets. """ -from pyflow.blocks.widgets.blocksplitter import OCBSplitter -from pyflow.blocks.widgets.blocktitle import OCBTitle -from pyflow.blocks.widgets.blocksizegrip import OCBSizeGrip +from pyflow.blocks.widgets.blocksplitter import Splitter +from pyflow.blocks.widgets.blocktitle import Title +from pyflow.blocks.widgets.blocksizegrip import SizeGrip diff --git a/pyflow/blocks/widgets/blocksizegrip.py b/pyflow/blocks/widgets/blocksizegrip.py index 6b65fa53..439f9c75 100644 --- a/pyflow/blocks/widgets/blocksizegrip.py +++ b/pyflow/blocks/widgets/blocksizegrip.py @@ -1,5 +1,7 @@ -""" -Implements the SizeGrip Widget for the Blocks. +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for SizeGrip block widget. The size grip is the little icon at the bottom right of a block that is used to resize a block. @@ -10,15 +12,15 @@ from PyQt5.QtGui import QMouseEvent -class OCBSizeGrip(QSizeGrip): - """A grip to resize a block""" +class SizeGrip(QSizeGrip): + """A grip to resize a block.""" def __init__(self, block: QGraphicsItem, parent: QWidget = None): """ Constructor for BlockSizeGrip block is the QGraphicsItem holding the QSizeGrip. - It's usually an OCBBlock + It's usually an Block """ super().__init__(parent) self.mouseX = 0 @@ -27,7 +29,7 @@ def __init__(self, block: QGraphicsItem, parent: QWidget = None): self.resizing = False def mousePressEvent(self, mouseEvent: QMouseEvent): - """Start the resizing""" + """Start the resizing.""" self.mouseX = mouseEvent.globalX() self.mouseY = mouseEvent.globalY() self.resizing = True @@ -35,17 +37,17 @@ def mousePressEvent(self, mouseEvent: QMouseEvent): def mouseReleaseEvent( self, mouseEvent: QMouseEvent ): # pylint:disable=unused-argument - """Stop the resizing""" + """Stop the resizing.""" self.resizing = False self.block.scene().history.checkpoint("Resized block", set_modified=True) @property def _zoom(self) -> float: - """Returns how much the scene is""" + """Returns how much the scene is.""" return self.block.scene().views()[0].zoom def mouseMoveEvent(self, mouseEvent: QMouseEvent): - """Performs resizing of the root widget""" + """Performs resizing of the root widget.""" transformed_pt1 = self.block.mapFromScene(QPoint(0, 0)) transformed_pt2 = self.block.mapFromScene(QPoint(1, 1)) diff --git a/pyflow/blocks/widgets/blocksplitter.py b/pyflow/blocks/widgets/blocksplitter.py index a7eee746..d04db5e8 100644 --- a/pyflow/blocks/widgets/blocksplitter.py +++ b/pyflow/blocks/widgets/blocksplitter.py @@ -1,31 +1,35 @@ -""" -Module defining a Splitter, the widget that contains multiple areas inside -a block and allows the user to resize those areas. +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for the Splitter block widget. + +The Splitter contains multiple areas inside a block +and allows the user to resize those areas. """ from PyQt5.QtGui import QMouseEvent from PyQt5.QtWidgets import QSplitter, QSplitterHandle, QWidget -class OCBSplitterHandle(QSplitterHandle): - """A handle for splitters with undoable events""" +class SplitterHandle(QSplitterHandle): + """A handle for splitters with undoable events.""" def mouseReleaseEvent(self, evt: QMouseEvent): - """When releasing the handle, save the state to history""" + """When releasing the handle, save the state to history.""" scene = self.parent().block.scene() if scene is not None: scene.history.checkpoint("Resize block", set_modified=True) return super().mouseReleaseEvent(evt) -class OCBSplitter(QSplitter): - """A spliter with undoable events""" +class Splitter(QSplitter): + """A spliter with undoable events.""" def __init__(self, block: QWidget, orientation: int, parent: QWidget): - """Create a new OCBSplitter""" + """Create a new Splitter.""" super().__init__(orientation, parent) self.block = block def createHandle(self): - """Return the middle handle of the splitter""" - return OCBSplitterHandle(self.orientation(), self) + """Return the middle handle of the splitter.""" + return SplitterHandle(self.orientation(), self) diff --git a/pyflow/blocks/widgets/blocktitle.py b/pyflow/blocks/widgets/blocktitle.py index de187992..e5538c3a 100644 --- a/pyflow/blocks/widgets/blocktitle.py +++ b/pyflow/blocks/widgets/blocktitle.py @@ -1,7 +1,10 @@ +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium # pylint:disable=unused-argument -""" -Module defining the widget for the title of blocks. -It's a QLineEdit modified so that editing it requires a double click. + +""" Module for the Title block widget. + +The Title is a modified QLineEdit for PyFlow purpose. """ import time @@ -13,8 +16,8 @@ from pyflow.core.serializable import Serializable -class OCBTitle(QLineEdit, Serializable): - """The title of an OCBBlock. Needs to be double clicked to interact""" +class Title(QLineEdit, Serializable): + """The title of an Block. Needs to be double clicked to interact.""" def __init__( self, @@ -24,7 +27,7 @@ def __init__( size: int = 12, parent: QWidget = None, ): - """Create a new title for an OCBBlock""" + """Create a new title for an Block.""" Serializable.__init__(self) QLineEdit.__init__(self, text, parent) self.clickTime = None @@ -33,7 +36,7 @@ def __init__( self.setCursorPosition(0) def init_ui(self, color: str, font: str, size: int): - """Apply the style given to the title""" + """Apply the style given to the title.""" self.color = color self.setStyleSheet( f""" @@ -62,19 +65,19 @@ def mousePressEvent(self, event: QMouseEvent): self.clickTime = time.time() def focusOutEvent(self, event: QFocusEvent): - """The title is read-only when focused is lost""" + """The title is read-only when focused is lost.""" self.setReadOnly(True) self.setCursorPosition(0) self.deselect() def mouseDoubleClickEvent(self, event: QMouseEvent): - """Toggle readonly mode when double clicking""" + """Toggle readonly mode when double clicking.""" self.setReadOnly(not self.isReadOnly()) if not self.isReadOnly(): self.setFocus(Qt.MouseFocusReason) def serialize(self) -> OrderedDict: - """Return a serialized version of this widget""" + """Return a serialized version of this widget.""" return OrderedDict( [ ("color", self.color), @@ -84,7 +87,7 @@ def serialize(self) -> OrderedDict: ) def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): - """Restore a title from serialized data""" + """Restore a title from serialized data.""" if restore_id: self.id = data.get("id", id(self)) self.init_ui(data["color"], data["font"], data["size"]) diff --git a/pyflow/core/__init__.py b/pyflow/core/__init__.py index a474fac2..cbf0a267 100644 --- a/pyflow/core/__init__.py +++ b/pyflow/core/__init__.py @@ -1,2 +1,2 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium diff --git a/pyflow/core/edge.py b/pyflow/core/edge.py index 7f1b297e..d3fc31dd 100644 --- a/pyflow/core/edge.py +++ b/pyflow/core/edge.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the OCB Edge. """ +""" Module for the base Edge.""" from __future__ import annotations @@ -16,10 +16,10 @@ ) from pyflow.core.serializable import Serializable -from pyflow.core.socket import OCBSocket +from pyflow.core.socket import Socket -class OCBEdge(QGraphicsPathItem, Serializable): +class Edge(QGraphicsPathItem, Serializable): """Base class for directed edges in Pyflow.""" @@ -36,8 +36,8 @@ def __init__( edge_transmitting_color="#00ff00", source: QPointF = QPointF(0, 0), destination: QPointF = QPointF(0, 0), - source_socket: OCBSocket = None, - destination_socket: OCBSocket = None, + source_socket: Socket = None, + destination_socket: Socket = None, ): """Base class for edges in Pyflow. @@ -74,7 +74,7 @@ def __init__( self.pens = [self._pen, self._pen_running, self._pen_transmitting] # 0 for normal, 1 for running, 2 for transmitting - self.run_color = 0 + self.run_state = 0 self.setFlag(QGraphicsPathItem.GraphicsItemFlag.ItemIsSelectable) self.setZValue(-1) @@ -96,7 +96,7 @@ def remove_from_socket(self, socket_type="source"): """ socket_name = f"{socket_type}_socket" - socket = getattr(self, socket_name, OCBSocket) + socket = getattr(self, socket_name, Socket) if socket is not None: socket.remove_edge(self) setattr(self, socket_name, None) @@ -126,7 +126,7 @@ def paint( elif self.destination_socket is None: pen = self._pen_dragging else: - pen = self.pens[self.run_color] + pen = self.pens[self.run_state] painter.setPen(pen) painter.setBrush(Qt.BrushStyle.NoBrush) painter.drawPath(self.path()) @@ -161,12 +161,12 @@ def source(self, value: QPointF): pass @property - def source_socket(self) -> OCBSocket: + def source_socket(self) -> Socket: """Source socket of the directed edge.""" return self._source_socket @source_socket.setter - def source_socket(self, value: OCBSocket): + def source_socket(self, value: Socket): self._source_socket = value if value is not None: self.source_socket.add_edge(self, is_destination=False) @@ -188,12 +188,12 @@ def destination(self, value: QPointF): pass @property - def destination_socket(self) -> OCBSocket: + def destination_socket(self) -> Socket: """Destination socket of the directed edge.""" return self._destination_socket @destination_socket.setter - def destination_socket(self, value: OCBSocket): + def destination_socket(self, value: Socket): self._destination_socket = value if value is not None: self.destination_socket.add_edge(self, is_destination=True) @@ -259,12 +259,19 @@ def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True): self.remove() @property - def run_color(self) -> int: - """Run color""" - return self._run_color + def run_state(self) -> int: + """Run state. - @run_color.setter - def run_color(self, value: int): - self._run_color = value + Describe the current state of the Edge: + - 0: idle. + - 1: running. + - 2: transmitting. + + """ + return self._run_state + + @run_state.setter + def run_state(self, value: int): + self._run_state = value # Update to force repaint self.update() diff --git a/pyflow/core/kernel.py b/pyflow/core/kernel.py index e4348466..c98be92f 100644 --- a/pyflow/core/kernel.py +++ b/pyflow/core/kernel.py @@ -1,4 +1,7 @@ -""" Module to create and manage ipython kernels """ +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module to create and manage ipython kernels.""" import queue from typing import Tuple @@ -9,7 +12,7 @@ class Kernel: - """jupyter_client kernel used to execute code and return output""" + """jupyter_client kernel used to execute code and return output.""" def __init__(self): self.kernel_manager, self.client = start_new_kernel() @@ -60,12 +63,12 @@ def run_block(self, block, code: str): Also calls run_queue when finished Args: - block: OCBCodeBlock to send the output to + block: CodeBlock to send the output to code: String representing a piece of Python code to execute """ worker = Worker(self, code) # Change color to running - block.run_color = 1 + block.run_state = 1 worker.signals.stdout.connect(block.handle_stdout) worker.signals.image.connect(block.handle_image) worker.signals.finished.connect(self.run_queue) @@ -74,9 +77,9 @@ def run_block(self, block, code: str): block.scene().threadpool.start(worker) def run_queue(self): - """Runs the next code in the queue""" + """Runs the next code in the queue.""" self.busy = True - if self.execution_queue == []: + if not self.execution_queue: self.busy = False return None block, code = self.execution_queue.pop(0) diff --git a/pyflow/core/pyeditor.py b/pyflow/core/pyeditor.py index ce65dfa8..a3ccd676 100644 --- a/pyflow/core/pyeditor.py +++ b/pyflow/core/pyeditor.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for OCB in block python editor. """ +""" Module for the PyFlow python editor.""" from typing import TYPE_CHECKING, List from PyQt5.QtCore import Qt @@ -16,10 +16,10 @@ from PyQt5.Qsci import QsciScintilla, QsciLexerPython from pyflow.graphics.theme_manager import theme_manager -from pyflow.blocks.block import OCBBlock +from pyflow.blocks.block import Block if TYPE_CHECKING: - from pyflow.graphics.view import OCBView + from pyflow.graphics.view import View POINT_SIZE = 11 @@ -28,7 +28,7 @@ class PythonEditor(QsciScintilla): """In-block python editor for Pyflow.""" - def __init__(self, block: OCBBlock): + def __init__(self, block: Block): """In-block python editor for Pyflow. Args: @@ -67,7 +67,7 @@ def __init__(self, block: OCBBlock): self.setWindowFlags(Qt.WindowType.FramelessWindowHint) def update_theme(self): - """Change the font and colors of the editor to match the current theme""" + """Change the font and colors of the editor to match the current theme.""" font = QFont() font.setFamily(theme_manager().recommended_font_family) font.setFixedPitch(True) @@ -89,19 +89,19 @@ def update_theme(self): lexer.setFont(font) self.setLexer(lexer) - def views(self) -> List["OCBView"]: + def views(self) -> List["View"]: """Get the views in which the python_editor is present.""" return self.block.scene().views() def wheelEvent(self, event: QWheelEvent) -> None: - """How PythonEditor handles wheel events""" + """How PythonEditor handles wheel events.""" if self.mode == "EDITING" and event.angleDelta().x() == 0: event.accept() return super().wheelEvent(event) @property def mode(self) -> int: - """PythonEditor current mode""" + """PythonEditor current mode.""" return self._mode @mode.setter diff --git a/pyflow/core/serializable.py b/pyflow/core/serializable.py index de75c58e..4c412f00 100644 --- a/pyflow/core/serializable.py +++ b/pyflow/core/serializable.py @@ -1,9 +1,9 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Module for the Serializable base class """ -from typing import OrderedDict, Set +from typing import Any, Dict, OrderedDict class Serializable: @@ -11,7 +11,7 @@ class Serializable: """Serializable base for serializable objects.""" MANDATORY_FIELDS: OrderedDict = {} - DEFAULT_DATA: Set[str] = {} + DEFAULT_DATA: Dict[str, Any] = {} def __init__(self): self.id = id(self) @@ -35,11 +35,11 @@ def deserialize( raise NotImplementedError() def complete_with_default(self, data: OrderedDict) -> None: - """Add default data in place when fields are missing""" + """Add default data in place when fields are missing.""" for key in self.MANDATORY_FIELDS: if key not in data: raise ValueError(f"{key} of the socket is missing") - for key in self.DEFAULT_DATA: + for key, val in self.DEFAULT_DATA.items(): if key not in data: - data[key] = self.DEFAULT_DATA[key] + data[key] = val diff --git a/pyflow/core/socket.py b/pyflow/core/socket.py index 866ef168..a90e8eba 100644 --- a/pyflow/core/socket.py +++ b/pyflow/core/socket.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for OCB Sockets """ +""" Module for base Sockets.""" from __future__ import annotations from typing import List, Optional, OrderedDict, TYPE_CHECKING @@ -14,11 +14,11 @@ from pyflow.core.serializable import Serializable if TYPE_CHECKING: - from pyflow.core.edge import OCBEdge - from pyflow.blocks.block import OCBBlock + from pyflow.core.edge import Edge + from pyflow.blocks.block import Block -class OCBSocket(QGraphicsItem, Serializable): +class Socket(QGraphicsItem, Serializable): """Base class for sockets in Pyflow.""" @@ -35,7 +35,7 @@ class OCBSocket(QGraphicsItem, Serializable): def __init__( self, - block: "OCBBlock", + block: "Block", socket_type: str = DEFAULT_DATA["type"], flow_type: str = "exe", radius: float = DEFAULT_DATA["metadata"]["radius"], @@ -59,7 +59,7 @@ def __init__( self.block = block QGraphicsItem.__init__(self, parent=self.block) - self.edges: List["OCBEdge"] = [] + self.edges: List["Edge"] = [] self.socket_type = socket_type self.flow_type = flow_type @@ -75,7 +75,7 @@ def __init__( "linecolor": linecolor, } - def add_edge(self, edge: "OCBEdge", is_destination: bool): + def add_edge(self, edge: "Edge", is_destination: bool): """Add a given edge to the socket edges.""" if not self._allow_multiple_edges: for prev_edge in self.edges: @@ -88,7 +88,7 @@ def add_edge(self, edge: "OCBEdge", is_destination: bool): return self.edges.append(edge) - def remove_edge(self, edge: "OCBEdge"): + def remove_edge(self, edge: "Edge"): """Remove a given edge from the socket edges.""" self.edges.remove(edge) diff --git a/pyflow/core/worker.py b/pyflow/core/worker.py index f87ba6cc..60912843 100644 --- a/pyflow/core/worker.py +++ b/pyflow/core/worker.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module to create and manage multi-threading workers """ +""" Module to create and manage multi-threading workers.""" import asyncio from PyQt5.QtCore import QObject, pyqtSignal, QRunnable @@ -18,7 +18,7 @@ class WorkerSignals(QObject): class Worker(QRunnable): - """Worker thread""" + """Worker thread.""" def __init__(self, kernel, code): """Initialize the worker object.""" @@ -29,7 +29,7 @@ def __init__(self, kernel, code): self.signals = WorkerSignals() async def run_code(self): - """Run the code in the block""" + """Run the code in the block.""" # Execute the code self.kernel.client.execute(self.code) done = False diff --git a/pyflow/graphics/__init__.py b/pyflow/graphics/__init__.py index a474fac2..cbf0a267 100644 --- a/pyflow/graphics/__init__.py +++ b/pyflow/graphics/__init__.py @@ -1,2 +1,2 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium diff --git a/pyflow/graphics/theme.py b/pyflow/graphics/theme.py index 30182858..7d3d15c5 100644 --- a/pyflow/graphics/theme.py +++ b/pyflow/graphics/theme.py @@ -1,6 +1,10 @@ -""" -This module defined Theme, a class that -contains the details of the theme +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for Theme. + +Theme is a class that contains the details of the coloring theme. + """ import json @@ -9,7 +13,7 @@ class Theme: - """Class holding the details of a specific theme""" + """Class holding the details of a specific theme.""" def __init__(self, name: str, json_str: str = "{}"): """ @@ -33,7 +37,7 @@ def __init__(self, name: str, json_str: str = "{}"): self.name = name def apply_to_lexer(self, lexer: QsciLexerPython): - """Make the given lexer follow the theme""" + """Make the given lexer follow the theme.""" lexer.setDefaultPaper(QColor("#1E1E1E")) lexer.setDefaultColor(QColor("#D4D4D4")) diff --git a/pyflow/graphics/theme_manager.py b/pyflow/graphics/theme_manager.py index d967a76f..5b3c18d0 100644 --- a/pyflow/graphics/theme_manager.py +++ b/pyflow/graphics/theme_manager.py @@ -1,4 +1,8 @@ -""" +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for the ThemeManager. + This module provides `theme_manager()`, a method that returns a handle to the theme manager of the application. @@ -6,21 +10,25 @@ of the text areas containing code. """ import os +import pathlib from typing import List from PyQt5.QtGui import QFontDatabase from PyQt5.QtCore import pyqtSignal, QObject from pyflow.graphics.theme import Theme +from pyflow import __file__ as INIT_PATH + +PACKAGE_PATH = pathlib.Path(INIT_PATH).parent class ThemeManager(QObject): - """Class loading theme files and providing the options set in those files""" + """Class loading theme files and providing the options set in those files.""" themeChanged = pyqtSignal() def __init__(self, parent=None): - """Load the default themes and the fonts available to construct the ThemeManager""" + """Load the default themes and the fonts available to construct the ThemeManager.""" super().__init__(parent) self._preferred_fonts = ["Inconsolata", "Roboto Mono", "Courier"] self.recommended_font_family = "Monospace" @@ -33,7 +41,7 @@ def __init__(self, parent=None): self._themes = [] self._selected_theme_index = 0 - theme_path = "./themes" + theme_path = os.path.join(PACKAGE_PATH, "themes") theme_paths = os.listdir(theme_path) for p in theme_paths: full_path = os.path.join(theme_path, p) @@ -45,7 +53,7 @@ def __init__(self, parent=None): @property def selected_theme_index(self): - """Return the index of the selected theme""" + """Return the index of the selected theme.""" return self._selected_theme_index @selected_theme_index.setter @@ -54,11 +62,11 @@ def selected_theme_index(self, value: int): self.themeChanged.emit() def list_themes(self) -> List[str]: - """List the themes""" + """List the themes.""" return [theme.name for theme in self._themes] def current_theme(self) -> Theme: - """Return the current theme""" + """Return the current theme.""" return self._themes[self.selected_theme_index] @@ -66,7 +74,7 @@ def current_theme(self) -> Theme: def theme_manager(): - """Retreive the theme manager of the application""" + """Retreive the theme manager of the application.""" global theme_handle if theme_handle is None: theme_handle = ThemeManager() diff --git a/pyflow/graphics/view.py b/pyflow/graphics/view.py index d706644e..97f31cad 100644 --- a/pyflow/graphics/view.py +++ b/pyflow/graphics/view.py @@ -1,10 +1,11 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the OCB View """ +""" Module for View.""" import json import os +import pathlib from typing import List, Tuple from PyQt5.QtCore import QEvent, QPoint, QPointF, Qt @@ -13,18 +14,22 @@ from PyQt5.sip import isdeleted -from pyflow.scene import OCBScene -from pyflow.core.socket import OCBSocket -from pyflow.core.edge import OCBEdge -from pyflow.blocks.block import OCBBlock -from pyflow.blocks.codeblock import OCBCodeBlock +from pyflow.scene import Scene +from pyflow.core.socket import Socket +from pyflow.core.edge import Edge +from pyflow.blocks.block import Block +from pyflow.blocks.codeblock import CodeBlock +from pyflow.blocks import __file__ as BLOCK_INIT_PATH + +BLOCK_PATH = pathlib.Path(BLOCK_INIT_PATH).parent +BLOCKFILES_PATH = os.path.join(BLOCK_PATH, "blockfiles") EPS: float = 1e-10 # To check if blocks are of size 0 -class OCBView(QGraphicsView): +class View(QGraphicsView): - """View for the OCB Window.""" + """View for the Window.""" MODE_NOOP = 0 MODE_EDGE_DRAG = 1 @@ -38,7 +43,7 @@ class OCBView(QGraphicsView): def __init__( self, - scene: OCBScene, + scene: Scene, parent=None, zoom_step: float = 1.25, zoom_min: float = 0.2, @@ -57,7 +62,7 @@ def __init__( self.setScene(scene) def init_ui(self): - """Initialize the custom OCB View UI.""" + """Initialize the custom View UI.""" # Antialiasing self.setRenderHints( QPainter.RenderHint.Antialiasing @@ -75,8 +80,8 @@ def init_ui(self): # Selection box self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) - def scene(self) -> OCBScene: - """Get current OCBScene.""" + def scene(self) -> Scene: + """Get current Scene.""" return super().scene() def mousePressEvent(self, event: QMouseEvent): @@ -89,7 +94,7 @@ def mousePressEvent(self, event: QMouseEvent): super().mousePressEvent(event) def mouseReleaseEvent(self, event: QMouseEvent): - """Dispatch Qt's mouseRelease events to corresponding functions below""" + """Dispatch Qt's mouseRelease events to corresponding functions below.""" if event.button() == Qt.MouseButton.MiddleButton: self.middleMouseButtonRelease(event) elif event.button() == Qt.MouseButton.LeftButton: @@ -98,23 +103,23 @@ def mouseReleaseEvent(self, event: QMouseEvent): super().mouseReleaseEvent(event) def mouseMoveEvent(self, event: QMouseEvent) -> None: - """OCBView reaction to mouseMoveEvent.""" + """View reaction to mouseMoveEvent.""" self.lastMousePos = self.mapToScene(event.pos()) self.drag_edge(event, "move") if event is not None: super().mouseMoveEvent(event) def leftMouseButtonPress(self, event: QMouseEvent): - """OCBView reaction to leftMouseButtonPress event.""" + """View reaction to leftMouseButtonPress event.""" # If clicked on a block, bring it forward. item_at_click = self.itemAt(event.pos()) if item_at_click is not None: while item_at_click.parentItem() is not None: - if isinstance(item_at_click, OCBBlock): + if isinstance(item_at_click, Block): break item_at_click = item_at_click.parentItem() - if isinstance(item_at_click, OCBBlock): + if isinstance(item_at_click, Block): self.bring_block_forward(item_at_click) # If clicked on a socket, start dragging an edge. @@ -123,19 +128,19 @@ def leftMouseButtonPress(self, event: QMouseEvent): super().mousePressEvent(event) def leftMouseButtonRelease(self, event: QMouseEvent): - """OCBView reaction to leftMouseButtonRelease event.""" + """View reaction to leftMouseButtonRelease event.""" event = self.drag_edge(event, "release") if event is not None: super().mouseReleaseEvent(event) def middleMouseButtonPress(self, event: QMouseEvent): - """OCBView reaction to middleMouseButtonPress event.""" + """View reaction to middleMouseButtonPress event.""" if self.itemAt(event.pos()) is None: event = self.drag_scene(event, "press") super().mousePressEvent(event) def middleMouseButtonRelease(self, event: QMouseEvent): - """OCBView reaction to middleMouseButtonRelease event.""" + """View reaction to middleMouseButtonRelease event.""" event = self.drag_scene(event, "release") super().mouseReleaseEvent(event) self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) @@ -166,9 +171,9 @@ def moveToItems(self) -> bool: if len(self.scene().selectedItems()) > 0: items = self.scene().selectedItems() - code_blocks: List[OCBBlock] = [i for i in items if isinstance(i, OCBBlock)] + code_blocks: List[Block] = [i for i in items if isinstance(i, Block)] - if len(code_blocks) == 0: + if not code_blocks: return False # Get the blocks with min and max x and y coordinates @@ -196,7 +201,7 @@ def moveToItems(self) -> bool: return True def getDistanceToCenter(self, x: float, y: float) -> Tuple[float]: - """Return the vector from the (x,y) position given to the center of the view""" + """Return the vector from the (x,y) position given to the center of the view.""" ypos = self.verticalScrollBar().value() xpos = self.horizontalScrollBar().value() return ( @@ -206,13 +211,13 @@ def getDistanceToCenter(self, x: float, y: float) -> Tuple[float]: def moveViewOnArrow(self, event: QKeyEvent) -> bool: """ - OCBView reaction to an arrow key being pressed. + View reaction to an arrow key being pressed. Returns True if the event was handled. """ # The focusItem has priority for this event if it is a source editor if self.scene().focusItem() is not None: parent = self.scene().focusItem().parentItem() - if isinstance(parent, OCBCodeBlock) and parent.source_editor.hasFocus(): + if isinstance(parent, CodeBlock) and parent.source_editor.hasFocus(): return False n_selected_items = len(self.scene().selectedItems()) @@ -222,13 +227,11 @@ def moveViewOnArrow(self, event: QKeyEvent) -> bool: code_blocks = [ i for i in self.scene().items() - if isinstance(i, OCBBlock) and not i.isSelected() + if isinstance(i, Block) and not i.isSelected() ] reference = None - if n_selected_items == 1 and isinstance( - self.scene().selectedItems()[0], OCBBlock - ): + if n_selected_items == 1 and isinstance(self.scene().selectedItems()[0], Block): selected_item = self.scene().selectedItems()[0] reference = QPoint( selected_item.x() + selected_item.width / 2, @@ -262,7 +265,7 @@ def in_region(x, y, key): key_id = event.key() dist_array = filter(lambda pos: in_region(pos[2], pos[3], key_id), dist_array) dist_array = list(dist_array) - if len(dist_array) == 0: + if not dist_array: return False def oriented_distance(x, y, key): @@ -277,14 +280,14 @@ def oriented_distance(x, y, key): item_to_navigate = self.scene().itemAt( block_center_x, block_center_y, self.transform() ) - if isinstance(item_to_navigate.parentItem(), OCBBlock): + if isinstance(item_to_navigate.parentItem(), Block): item_to_navigate.parentItem().setSelected(True) self.centerView(block_center_x, block_center_y) return True def keyPressEvent(self, event: QKeyEvent): - """OCBView reaction to a key being pressed""" + """View reaction to a key being pressed.""" key_id = event.key() if key_id in [ Qt.Key.Key_Up, @@ -299,10 +302,10 @@ def keyPressEvent(self, event: QKeyEvent): def retreiveBlockTypes(self) -> List[Tuple[str]]: """Retreive the list of stored blocks.""" - block_type_files = os.listdir("blocks") + block_type_files = os.listdir(BLOCKFILES_PATH) block_types = [] - for b in block_type_files: - filepath = os.path.join("blocks", b) + for blockfile_name in block_type_files: + filepath = os.path.join(BLOCKFILES_PATH, blockfile_name) with open(filepath, "r", encoding="utf-8") as file: data = json.loads(file.read()) title = "New Block" @@ -315,7 +318,7 @@ def retreiveBlockTypes(self) -> List[Tuple[str]]: return block_types def contextMenuEvent(self, event: QContextMenuEvent): - """Displays the context menu when inside a view""" + """Displays the context menu when inside a view.""" super().contextMenuEvent(event) # If somebody has already accepted the event, don't handle it. if event.isAccepted(): @@ -349,7 +352,7 @@ def wheelEvent(self, event: QWheelEvent): super().wheelEvent(event) def setZoom(self, new_zoom: float): - """Set the zoom to the appropriate level""" + """Set the zoom to the appropriate level.""" zoom_factor = new_zoom / self.zoom self.scale(zoom_factor, zoom_factor) self.zoom = new_zoom @@ -361,7 +364,7 @@ def deleteSelected(self): selected_item.remove() scene.history.checkpoint("Delete selected elements", set_modified=True) - def bring_block_forward(self, block: OCBBlock): + def bring_block_forward(self, block: Block): """Move the selected block in front of other blocks. Args: @@ -411,12 +414,12 @@ def drag_edge(self, event: QMouseEvent, action="press"): scene = self.scene() if action == "press": if ( - isinstance(item_at_click, OCBSocket) + isinstance(item_at_click, Socket) and self.mode != self.MODE_EDGE_DRAG and item_at_click.socket_type != "input" ): self.mode = self.MODE_EDGE_DRAG - self.edge_drag = OCBEdge( + self.edge_drag = Edge( source_socket=item_at_click, destination=self.mapToScene(event.pos()), ) @@ -425,7 +428,7 @@ def drag_edge(self, event: QMouseEvent, action="press"): elif action == "release": if self.mode == self.MODE_EDGE_DRAG: if ( - isinstance(item_at_click, OCBSocket) + isinstance(item_at_click, Socket) and item_at_click is not self.edge_drag.source_socket and item_at_click.socket_type != "output" ): diff --git a/pyflow/graphics/widget.py b/pyflow/graphics/widget.py index db89e000..f593c8ca 100644 --- a/pyflow/graphics/widget.py +++ b/pyflow/graphics/widget.py @@ -1,20 +1,20 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the OCB Widget """ +""" Module for the base PyFlow Widget.""" import os from PyQt5.QtWidgets import QVBoxLayout, QWidget from PyQt5.QtCore import Qt -from pyflow.scene import OCBScene -from pyflow.graphics.view import OCBView +from pyflow.scene import Scene +from pyflow.graphics.view import View -class OCBWidget(QWidget): +class Widget(QWidget): - """Window for the OCB application.""" + """Window for a graph visualisation.""" def __init__(self, parent=None): super().__init__(parent) @@ -25,11 +25,11 @@ def __init__(self, parent=None): self.setLayout(self.layout) # Graphics Scene - self.scene = OCBScene() + self.scene = Scene() self.scene.addHasBeenModifiedListener(self.updateTitle) # Graphics View - self.view = OCBView(self.scene) + self.view = View(self.scene) self.layout.addWidget(self.view) self.savepath = None @@ -63,7 +63,7 @@ def save(self): self.scene.save(self.savepath) def saveAsJupyter(self, filepath: str): - """Save the current graph notebook as a regular python notebook""" + """Save the current graph notebook as a regular python notebook.""" self.scene.save_to_ipynb(filepath) def load(self, filepath: str): diff --git a/pyflow/graphics/window.py b/pyflow/graphics/window.py index 9ca5559d..14d94cab 100644 --- a/pyflow/graphics/window.py +++ b/pyflow/graphics/window.py @@ -1,13 +1,14 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO -# pylint:disable=too-many-instance-attributes +# Copyright (C) 2021-2022 Bycelium +# pylint:disable=too-many-instance-attributes, unsubscriptable-object -""" Module for the OCB Window """ +""" Module for the base Window.""" import os +import pathlib + from PyQt5.QtCore import QPoint, QSettings, QSize, Qt, QSignalMapper from PyQt5.QtGui import QCloseEvent, QKeySequence - from PyQt5.QtWidgets import ( QWidget, QAction, @@ -17,28 +18,27 @@ QMdiArea, ) -from pyflow.graphics.widget import OCBWidget +from pyflow.graphics.widget import Widget from pyflow.graphics.theme_manager import theme_manager from pyflow.qss import loadStylesheets +from pyflow.qss import __file__ as QSS_INIT_PATH + +QSS_PATH = pathlib.Path(QSS_INIT_PATH).parent -class OCBWindow(QMainWindow): +class Window(QMainWindow): """Main window of the Pyflow Qt-based application.""" def __init__(self): super().__init__() - self.stylesheet_filename = os.path.join( - os.path.dirname(__file__), "..", "qss", "ocb.qss" - ) - loadStylesheets( - ( - os.path.join(os.path.dirname(__file__), "..", "qss", "ocb_dark.qss"), - self.stylesheet_filename, - ) - ) + self.stylesheets = [ + os.path.join(QSS_PATH, "pyflow.qss"), + os.path.join(QSS_PATH, "pyflow_dark.qss"), + ] + loadStylesheets(self.stylesheets) self.mdiArea = QMdiArea() self.mdiArea.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) @@ -278,12 +278,12 @@ def updateWindowMenu(self): def createNewMdiChild(self, filename: str = None): """Create a new graph subwindow loading a file if a path is given.""" - ocb_widget = OCBWidget() + _widget = Widget() if filename is not None: - ocb_widget.scene.load(filename) + _widget.scene.load(filename) if filename.split(".")[-1] == "ipyg": - ocb_widget.savepath = filename - return self.mdiArea.addSubWindow(ocb_widget) + _widget.savepath = filename + return self.mdiArea.addSubWindow(_widget) def onFileNew(self): """Create a new file.""" @@ -362,15 +362,15 @@ def onFileSaveAsJupyter(self) -> bool: return True return False - def saveWindow(self, window: OCBWidget): - """Save the given window""" + def saveWindow(self, window: Widget): + """Save the given window.""" window.save() self.statusbar.showMessage( f"Successfully saved ipygraph at {window.savepath}", 2000 ) @staticmethod - def is_not_editing(current_window: OCBWidget): + def is_not_editing(current_window: Widget): """True if current_window exists and is not in editing mode.""" return current_window is not None and not current_window.view.is_mode("EDITING") @@ -451,8 +451,8 @@ def maybeSave(self) -> bool: return True return False - def activeMdiChild(self) -> OCBWidget: - """Get the active OCBWidget if existing.""" + def activeMdiChild(self) -> Widget: + """Get the active Widget if existing.""" activeSubWindow = self.mdiArea.activeSubWindow() if activeSubWindow is not None: return activeSubWindow.widget() @@ -486,7 +486,7 @@ def onMoveToItems(self): If items are selected, then make all the selected items visible instead """ current_window = self.activeMdiChild() - if current_window is not None and isinstance(current_window, OCBWidget): + if current_window is not None and isinstance(current_window, Widget): current_window.moveToItems() def setTheme(self, theme_index): diff --git a/pyflow/qss/__init__.py b/pyflow/qss/__init__.py index 8b1d3ea3..edd63e00 100644 --- a/pyflow/qss/__init__.py +++ b/pyflow/qss/__init__.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the OCB qss and styles. """ +""" Module for qss and styles. """ from typing import List diff --git a/pyflow/qss/dark_resources.py b/pyflow/qss/dark_resources.py index 9a1f60f9..699e3ff7 100644 --- a/pyflow/qss/dark_resources.py +++ b/pyflow/qss/dark_resources.py @@ -8,7 +8,7 @@ WARNING! All changes made in this file will be lost! """ -from qtpy import QtCore +from PyQt5 import QtCore qt_resource_data = b"\ \x00\x00\x05\x1a\ diff --git a/pyflow/qss/ocb.qss b/pyflow/qss/pyflow.qss similarity index 100% rename from pyflow/qss/ocb.qss rename to pyflow/qss/pyflow.qss diff --git a/pyflow/qss/ocb_dark.qss b/pyflow/qss/pyflow_dark.qss similarity index 100% rename from pyflow/qss/ocb_dark.qss rename to pyflow/qss/pyflow_dark.qss diff --git a/pyflow/scene/__init__.py b/pyflow/scene/__init__.py index 593113ba..8498c9ec 100644 --- a/pyflow/scene/__init__.py +++ b/pyflow/scene/__init__.py @@ -1,6 +1,6 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the OCBScene creation and manipulations. """ +""" Module for the Scene creation and manipulations.""" -from pyflow.scene.scene import OCBScene +from pyflow.scene.scene import Scene diff --git a/pyflow/scene/clipboard.py b/pyflow/scene/clipboard.py index 85bfb210..7fbf606a 100644 --- a/pyflow/scene/clipboard.py +++ b/pyflow/scene/clipboard.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the handling an OCBScene clipboard operations. """ +""" Module for the handling of scene clipboard operations. """ from typing import TYPE_CHECKING, OrderedDict from warnings import warn @@ -9,19 +9,19 @@ import json from PyQt5.QtWidgets import QApplication -from pyflow.core.edge import OCBEdge +from pyflow.core.edge import Edge if TYPE_CHECKING: - from pyflow.scene import OCBScene - from pyflow.graphics.view import OCBView + from pyflow.scene import Scene + from pyflow.graphics.view import View class SceneClipboard: - """Helper object to handle clipboard operations on an OCBScene.""" + """Helper object to handle clipboard operations on an Scene.""" - def __init__(self, scene: "OCBScene"): - """Helper object to handle clipboard operations on an OCBScene. + def __init__(self, scene: "Scene"): + """Helper object to handle clipboard operations on an Scene. Args: scene: Scene reference. @@ -111,7 +111,7 @@ def _deserializeData(self, data: OrderedDict, set_selected=True): # Create edges for edge_data in data["edges"]: - edge = OCBEdge() + edge = Edge() edge.deserialize(edge_data, hashmap, restore_id=False) if set_selected: diff --git a/pyflow/scene/from_ipynb_conversion.py b/pyflow/scene/from_ipynb_conversion.py index 3f918056..a6d17781 100644 --- a/pyflow/scene/from_ipynb_conversion.py +++ b/pyflow/scene/from_ipynb_conversion.py @@ -1,4 +1,7 @@ -""" Module for converting ipynb data to ipyg data """ +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for converting notebook (.ipynb) data to pygraph (.ipyg) data.""" from typing import OrderedDict, List @@ -118,12 +121,12 @@ def get_blocks_data( def is_title(block_data: OrderedDict) -> bool: - """Checks if the block is a one-line markdown block which could correspond to a title""" + """Checks if the block is a one-line markdown block which could correspond to a title.""" if block_data["block_type"] != BLOCK_TYPE_TO_NAME["markdown"]: return False if "\n" in block_data["text"]: return False - if len(block_data["text"]) == 0 or len(block_data["text"]) > TITLE_MAX_LENGTH: + if not block_data["text"] or len(block_data["text"]) > TITLE_MAX_LENGTH: return False # Headings, quotes, bold or italic text are not considered to be headings if block_data["text"][0] in {"#", "*", "`"}: @@ -154,7 +157,7 @@ def adujst_markdown_blocks_width(blocks_data: OrderedDict) -> None: def get_edges_data(blocks_data: OrderedDict) -> OrderedDict: - """Add sockets to the blocks (in place) and returns the edge list""" + """Add sockets to the blocks (in place) and returns the edge list.""" code_blocks: List[OrderedDict] = [ block for block in blocks_data @@ -186,7 +189,7 @@ def get_edges_data(blocks_data: OrderedDict) -> OrderedDict: def get_input_socket_data(socket_id: int) -> OrderedDict: - """Returns the input socket's data with the corresponding id""" + """Returns the input socket's data with the corresponding id.""" return { "id": socket_id, "type": "input", @@ -213,7 +216,7 @@ def get_edge_data( edge_end_block_id: int, edge_end_socket_id: int, ) -> OrderedDict: - """Return the ordered dict corresponding to the given parameters""" + """Return the ordered dict corresponding to the given parameters.""" return { "id": edge_id, "source": {"block": edge_start_block_id, "socket": edge_start_socket_id}, diff --git a/pyflow/scene/history.py b/pyflow/scene/history.py index 676c3cd1..6f26ce7d 100644 --- a/pyflow/scene/history.py +++ b/pyflow/scene/history.py @@ -1,16 +1,16 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the handling an OCBScene history. """ +""" Module for the handling a scene history. """ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from pyflow.scene import OCBScene + from pyflow.scene import Scene class SceneHistory: - """Helper object to handle undo/redo operations on an OCBScene. + """Helper object to handle undo/redo operations on an Scene. Args: scene: Scene reference. @@ -18,7 +18,7 @@ class SceneHistory: """ - def __init__(self, scene: "OCBScene", max_stack: int = 50): + def __init__(self, scene: "Scene", max_stack: int = 50): self.scene = scene self.history_stack = [] self.current = -1 diff --git a/pyflow/scene/ipynb_conversion_constants.py b/pyflow/scene/ipynb_conversion_constants.py index 65880903..93baa6fe 100644 --- a/pyflow/scene/ipynb_conversion_constants.py +++ b/pyflow/scene/ipynb_conversion_constants.py @@ -1,4 +1,7 @@ -""" Module with the constants used to converter to ipynb and from ipynb """ +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module with the constants used to convert to and from notebooks.""" from typing import Dict @@ -15,11 +18,11 @@ DEFAULT_TEXT_WIDTH = 618 BLOCK_TYPE_TO_NAME: Dict[str, str] = { - "code": "OCBCodeBlock", - "markdown": "OCBMarkdownBlock", + "code": "CodeBlock", + "markdown": "MarkdownBlock", } -BLOCK_TYPE_SUPPORTED_FOR_IPYG_TO_IPYNB = {"OCBCodeBlock", "OCBMarkdownBlock"} +BLOCK_TYPE_SUPPORTED_FOR_IPYG_TO_IPYNB = {"CodeBlock", "MarkdownBlock"} DEFAULT_NOTEBOOK_DATA = { "cells": [], diff --git a/pyflow/scene/scene.py b/pyflow/scene/scene.py index 9ee8fe6d..e7d08fc2 100644 --- a/pyflow/scene/scene.py +++ b/pyflow/scene/scene.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for the OCB Scene """ +""" Module for the base Scene.""" import math import json @@ -14,8 +14,8 @@ from PyQt5.QtWidgets import QGraphicsScene from pyflow.core.serializable import Serializable -from pyflow.blocks.block import OCBBlock -from pyflow.core.edge import OCBEdge +from pyflow.blocks.block import Block +from pyflow.core.edge import Edge from pyflow.scene.clipboard import SceneClipboard from pyflow.scene.history import SceneHistory from pyflow.core.kernel import Kernel @@ -24,9 +24,9 @@ from pyflow import blocks -class OCBScene(QGraphicsScene, Serializable): +class Scene(QGraphicsScene, Serializable): - """Scene for the OCB Window.""" + """Scene for the Window.""" def __init__( self, @@ -76,23 +76,23 @@ def addHasBeenModifiedListener(self, callback: FunctionType): """Add a callback that will trigger when the scene has been modified.""" self._has_been_modified_listeners.append(callback) - def sortedSelectedItems(self) -> List[Union[OCBBlock, OCBEdge]]: + def sortedSelectedItems(self) -> List[Union[Block, Edge]]: """Returns the selected blocks and selected edges in two separate lists.""" selected_blocks, selected_edges = [], [] for item in self.selectedItems(): - if isinstance(item, OCBBlock): + if isinstance(item, Block): selected_blocks.append(item) - if isinstance(item, OCBEdge): + if isinstance(item, Edge): selected_edges.append(item) return selected_blocks, selected_edges def drawBackground(self, painter: QPainter, rect: QRectF): - """Draw the Scene background""" + """Draw the Scene background.""" super().drawBackground(painter, rect) self.drawGrid(painter, rect) def drawGrid(self, painter: QPainter, rect: QRectF): - """Draw the background grid""" + """Draw the background grid.""" left = int(math.floor(rect.left())) top = int(math.floor(rect.top())) right = int(math.ceil(rect.right())) @@ -144,7 +144,7 @@ def save_to_ipyg(self, filepath: str): file.write(json.dumps(self.serialize(), indent=4)) def save_to_ipynb(self, filepath: str): - """Save the scene into filepath as ipynb""" + """Save the scene into filepath as ipynb.""" if "." not in filepath: filepath += ".ipynb" @@ -204,9 +204,9 @@ def serialize(self) -> OrderedDict: blocks = [] edges = [] for item in self.items(): - if isinstance(item, OCBBlock): + if isinstance(item, Block): blocks.append(item) - elif isinstance(item, OCBEdge): + elif isinstance(item, Edge): edges.append(item) blocks.sort(key=lambda x: x.id) edges.sort(key=lambda x: x.id) @@ -219,7 +219,7 @@ def serialize(self) -> OrderedDict: ) def create_block_from_file(self, filepath: str, x: float = 0, y: float = 0): - """Create a new block from a .ocbb file""" + """Create a new block from a .b file.""" with open(filepath, "r", encoding="utf-8") as file: data = json.loads(file.read()) data["position"] = [x, y] @@ -228,8 +228,8 @@ def create_block_from_file(self, filepath: str, x: float = 0, y: float = 0): def create_block( self, data: OrderedDict, hashmap: dict = None, restore_id: bool = True - ) -> OCBBlock: - """Create a new block from an OrderedDict""" + ) -> Block: + """Create a new block from an OrderedDict.""" block = None @@ -266,7 +266,7 @@ def deserialize( # Create edges for edge_data in data["edges"]: - edge = OCBEdge() + edge = Edge() edge.deserialize(edge_data, hashmap, restore_id) self.addItem(edge) hashmap.update({edge_data["id"]: edge}) diff --git a/pyflow/scene/to_ipynb_conversion.py b/pyflow/scene/to_ipynb_conversion.py index a079cf99..8cfb6c57 100644 --- a/pyflow/scene/to_ipynb_conversion.py +++ b/pyflow/scene/to_ipynb_conversion.py @@ -1,4 +1,7 @@ -""" Module for converting ipyg data to ipynb data """ +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +""" Module for converting pygraph (.ipyg) data to notebook (.ipynb) data.""" from typing import OrderedDict, List @@ -8,7 +11,7 @@ def ipyg_to_ipynb(data: OrderedDict) -> OrderedDict: - """Convert ipyg data (as ordered dict) into ipynb data (as ordered dict)""" + """Convert ipyg data (as ordered dict) into ipynb data (as ordered dict).""" ordered_data: OrderedDict = get_block_in_order(data) ipynb_data: OrderedDict = copy.deepcopy(DEFAULT_NOTEBOOK_DATA) @@ -21,14 +24,14 @@ def ipyg_to_ipynb(data: OrderedDict) -> OrderedDict: def get_block_in_order(data: OrderedDict) -> OrderedDict: - """Changes the order of the blocks from random to the naturel flow of the text""" + """Changes the order of the blocks from random to the naturel flow of the text.""" # Not implemented yet return data def block_to_ipynb_cell(block_data: OrderedDict) -> OrderedDict: - """Convert a ipyg block into its corresponding ipynb cell""" + """Convert a ipyg block into its corresponding ipynb cell.""" if block_data["block_type"] == BLOCK_TYPE_TO_NAME["code"]: cell_data: OrderedDict = copy.deepcopy(DEFAULT_CODE_CELL) cell_data["source"] = split_lines_and_add_newline(block_data["source"]) @@ -45,7 +48,7 @@ def block_to_ipynb_cell(block_data: OrderedDict) -> OrderedDict: def split_lines_and_add_newline(text: str) -> List[str]: """Split the text and add a \\n at the end of each line - This is the jupyter notebook default formatting for source, outputs and text""" + This is the jupyter notebook default formatting for source, outputs and text.""" lines = text.split("\n") for i in range(len(lines) - 1): lines[i] += "\n" diff --git a/themes/default.theme b/pyflow/themes/default.theme similarity index 100% rename from themes/default.theme rename to pyflow/themes/default.theme diff --git a/themes/monokai.theme b/pyflow/themes/monokai.theme similarity index 100% rename from themes/monokai.theme rename to pyflow/themes/monokai.theme diff --git a/pylint_score.py b/pylint_score.py index b983c8e9..1c7aee7a 100644 --- a/pylint_score.py +++ b/pylint_score.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module to get pylint score. """ +""" Module to get pylint score.""" import sys import html diff --git a/pypi-readme.md b/pypi-readme.md new file mode 100644 index 00000000..db26cedd --- /dev/null +++ b/pypi-readme.md @@ -0,0 +1,55 @@ +# PyFlow + +[![Pytest badge](https://github.com/Bycelium/PyFlow/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/Bycelium/PyFlow/actions/workflows/python-tests.yml) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9874915d70e440418447f371c4bd5061)](https://www.codacy.com/gh/Bycelium/PyFlow/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Bycelium/PyFlow&utm_campaign=Badge_Grade) +[![Pylint badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_pylint_badge.json)](https://github.com/Bycelium/PyFlow/actions/workflows/python-pylint.yml) +[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/9874915d70e440418447f371c4bd5061)](https://www.codacy.com/gh/Bycelium/PyFlow/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Bycelium/PyFlow&utm_campaign=Badge_Coverage) +[![Unit coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_unit_coverage_badge.json)](https://github.com/Bycelium/PyFlow/actions/workflows/python-coverage.yml) +[![Integration coverage badge](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FMathisFederico%2F00ce73155619a4544884ca6d251954b3%2Fraw%2Fopencodeblocks_integration_coverage_badge.json)](https://github.com/Bycelium/PyFlow/actions/workflows/python-coverage.yml) +[![Licence - GPLv3](https://img.shields.io/github/license/MathisFederico/Crafting?style=plastic)](https://www.gnu.org/licenses/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) + +PyFlow is an open-source graph-structured interactive Python development tool + +Although for now Pyflow is in closed Beta and features are coming in bit by bit, stay tuned for the first release soon ! + +![](media/mnist_example.gif) + +## Community + +Join our [Discord](https://discord.gg/xZq8Tp4srd) to beta-test features, share your ideas, contribute or just to have a chat with us. + +## Features + +- Create blocks of code in which you can edit and run Python code +- Move and resize blocks on an infinite 2D plane +- Link blocks to highlight dependencies, Pyflow will then automatically run your blocks in the correct order +- Convert your Jupyter notebooks to Pyflow graphs and vice versa + +## Installation + +Make sure you have Python 3 installed. You can download it from [here](https://www.python.org/downloads/) + +### Install PyFlow + +Using pip: + +```bash +pip install byc-pyflow +``` + +### Run PyFlow + +```bash +python -m pyflow +``` + +## Contributing + +If you are interested in contributing to the project, see [CONTRIBUTING.md](CONTRIBUTING.md). + +You can also join our [Discord](https://discord.gg/xZq8Tp4srd) to get in touch with us. + +## License + +See [LICENSE](LICENSE) diff --git a/requirements.txt b/requirements.txt index 51ee9172..28e0c2e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ pyqt5>=5.15.4 -QtPy>=1.9.0 qscintilla>=2.13.0 Ipython>=7.27.0 jupyter_client>=7.0.6 diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..fd717865 --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + +"""Setup for the python package build.""" + +import pathlib +from setuptools import setup, find_packages + + +def get_version(): + """Load version from file.""" + version_file = open("VERSION") + return version_file.read().strip() + + +def get_requirements(): + """Load requirements from file.""" + requirements_file = open("requirements.txt") + return requirements_file.readlines() + + +HERE = pathlib.Path(__file__).parent # The directory containing this file +README = (HERE / "pypi-readme.md").read_text() +VERSION = get_version() +REQUIREMENTS = get_requirements() + +setup( + name="byc-pyflow", + version=VERSION, + author="Bycelium", + author_email="mathis.federico@bycelium.com", + description="An open-source tool for modular visual programing in python", + long_description=README, + long_description_content_type="text/markdown", + url="https://github.com/Bycelium/PyFlow", + packages=find_packages(exclude=["*test*", "*docs*"]), + include_package_data=True, + install_requires=REQUIREMENTS, + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", + ], + python_requires=">=3.7", +) diff --git a/tests/__init__.py b/tests/__init__.py index fc97eed2..ba5e7a8f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Tests for the pyflow package. """ diff --git a/tests/assets/example_graph1.ipyg b/tests/assets/example_graph1.ipyg index c96688ed..c3743874 100644 --- a/tests/assets/example_graph1.ipyg +++ b/tests/assets/example_graph1.ipyg @@ -4,7 +4,7 @@ { "id": 1523300599264, "title": "test1", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 292, 0 diff --git a/tests/assets/flow_test.ipyg b/tests/assets/flow_test.ipyg index 0a20333b..710be340 100644 --- a/tests/assets/flow_test.ipyg +++ b/tests/assets/flow_test.ipyg @@ -4,7 +4,7 @@ { "id": 2047380828024, "title": "Test flow 5", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 203, 0 @@ -58,7 +58,7 @@ { "id": 2047509975832, "title": "Test flow 1", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 187, 0 @@ -112,7 +112,7 @@ { "id": 2047510054808, "title": "Test flow 2", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 154, 0 @@ -166,7 +166,7 @@ { "id": 2047510222024, "title": "Test flow 3", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 191, 0 @@ -220,7 +220,7 @@ { "id": 2047510394344, "title": "Test flow 4", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 165, 0 @@ -274,7 +274,7 @@ { "id": 2047510572632, "title": "Test flow 6", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 202, 0 @@ -328,7 +328,7 @@ { "id": 2047511620200, "title": "Test flow 7", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 211, 0 @@ -382,7 +382,7 @@ { "id": 2047511818888, "title": "Test flow 8", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 204, 0 @@ -436,7 +436,7 @@ { "id": 2047514048984, "title": "Markdown", - "block_type": "OCBMarkdownBlock", + "block_type": "MarkdownBlock", "splitter_pos": [ 0, 130 @@ -460,7 +460,7 @@ { "id": 2047515370984, "title": "Test no connection 1", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 199, 0 @@ -514,7 +514,7 @@ { "id": 2047515582680, "title": "Test input only 1", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 250, 0 @@ -568,7 +568,7 @@ { "id": 2047516568312, "title": "Test input only 2", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 236, 0 @@ -622,7 +622,7 @@ { "id": 2047517406264, "title": "Test output only 1", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 209, 0 @@ -676,7 +676,7 @@ { "id": 2047520151448, "title": "Test output only 2", - "block_type": "OCBCodeBlock", + "block_type": "CodeBlock", "splitter_pos": [ 202, 0 @@ -730,7 +730,7 @@ { "id": 2047565442936, "title": "Markdown", - "block_type": "OCBMarkdownBlock", + "block_type": "MarkdownBlock", "splitter_pos": [ 0, 131 diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py index 419e85cc..3e50fa28 100644 --- a/tests/integration/__init__.py +++ b/tests/integration/__init__.py @@ -1,8 +1,8 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ -Integration tests for the OCB package. +Integration tests for the package. We use xvfb to perform the tests without opening any windows. We use pyautogui to move the mouse and interact with the application. diff --git a/tests/integration/blocks/__init__.py b/tests/integration/blocks/__init__.py index e0d043db..5afedbbd 100644 --- a/tests/integration/blocks/__init__.py +++ b/tests/integration/blocks/__init__.py @@ -1,5 +1,5 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Integration tests for blocks behavior. diff --git a/tests/integration/blocks/test_block.py b/tests/integration/blocks/test_block.py index 39ab970d..d6647201 100644 --- a/tests/integration/blocks/test_block.py +++ b/tests/integration/blocks/test_block.py @@ -1,8 +1,8 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ -Integration tests for the OCBBlocks. +Integration tests for the Blocks. """ import pytest @@ -11,7 +11,7 @@ from PyQt5.QtCore import QPointF -from pyflow.blocks.block import OCBBlock +from pyflow.blocks.block import Block from tests.integration.utils import apply_function_inapp, CheckingQueue, start_app @@ -21,18 +21,18 @@ class TestBlocks: def setup(self): """Setup reused variables.""" start_app(self) - self.block = OCBBlock(title="Testing block") + self.block = Block(title="Testing block") def test_create_blocks(self, qtbot: QtBot): """can be added to the scene.""" - self.ocb_widget.scene.addItem(self.block) + self._widget.scene.addItem(self.block) def test_move_blocks(self, qtbot: QtBot): """can be dragged around with the mouse.""" - self.ocb_widget.scene.addItem(self.block) - self.ocb_widget.view.horizontalScrollBar().setValue(self.block.x()) - self.ocb_widget.view.verticalScrollBar().setValue( - self.block.y() - self.ocb_widget.view.height() + self.block.height + self._widget.scene.addItem(self.block) + self._widget.view.horizontalScrollBar().setValue(self.block.x()) + self._widget.view.verticalScrollBar().setValue( + self.block.y() - self._widget.view.height() + self.block.height ) def testing_drag(msgQueue: CheckingQueue): @@ -47,8 +47,8 @@ def testing_drag(msgQueue: CheckingQueue): ) pos_block.setY(pos_block.y() + self.block.title_widget.height() / 2) - pos_block = self.ocb_widget.view.mapFromScene(pos_block) - pos_block = self.ocb_widget.view.mapToGlobal(pos_block) + pos_block = self._widget.view.mapFromScene(pos_block) + pos_block = self._widget.view.mapToGlobal(pos_block) pyautogui.moveTo(pos_block.x(), pos_block.y()) pyautogui.mouseDown(button="left") @@ -64,8 +64,8 @@ def testing_drag(msgQueue: CheckingQueue): move_amount = [self.block.pos().x(), self.block.pos().y()] # rectify because the scene can be zoomed : - move_amount[0] = move_amount[0] * self.ocb_widget.view.zoom - move_amount[1] = move_amount[1] * self.ocb_widget.view.zoom + move_amount[0] = move_amount[0] * self._widget.view.zoom + move_amount[1] = move_amount[1] * self._widget.view.zoom msgQueue.check_equal( move_amount, expected_move_amount, "Block moved by the correct amound" diff --git a/tests/integration/blocks/test_codeblock.py b/tests/integration/blocks/test_codeblock.py index 9418caf4..5ddd26b8 100644 --- a/tests/integration/blocks/test_codeblock.py +++ b/tests/integration/blocks/test_codeblock.py @@ -1,8 +1,8 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ -Integration tests for the OCBCodeBlocks. +Integration tests for the CodeBlocks. """ import time @@ -12,7 +12,7 @@ from PyQt5.QtCore import QPointF -from pyflow.blocks.codeblock import OCBCodeBlock +from pyflow.blocks.codeblock import CodeBlock from tests.integration.utils import apply_function_inapp, CheckingQueue, start_app @@ -31,8 +31,8 @@ def test_run_python(self): SOURCE_TEST = f"""print({EXPRESSION})""" expected_result = str(3 + 5 * 2) - test_block = OCBCodeBlock(title="CodeBlock test", source=SOURCE_TEST) - self.ocb_widget.scene.addItem(test_block) + test_block = CodeBlock(title="CodeBlock test", source=SOURCE_TEST) + self._widget.scene.addItem(test_block) def testing_run(msgQueue: CheckingQueue): @@ -43,8 +43,8 @@ def testing_run(msgQueue: CheckingQueue): pos_run_button.x() + test_block.run_button.width() / 2, pos_run_button.y() + test_block.run_button.height() / 2, ) - pos_run_button = self.ocb_widget.view.mapFromScene(pos_run_button) - pos_run_button = self.ocb_widget.view.mapToGlobal(pos_run_button) + pos_run_button = self._widget.view.mapFromScene(pos_run_button) + pos_run_button = self._widget.view.mapToGlobal(pos_run_button) # Run the block by pressung the run button pyautogui.moveTo(pos_run_button.x(), pos_run_button.y()) @@ -52,7 +52,7 @@ def testing_run(msgQueue: CheckingQueue): pyautogui.mouseUp(button="left") time.sleep((test_block.transmitting_duration / 1000) + 0.2) - while test_block.run_color != 0: + while test_block.run_state != 0: time.sleep(0.1) msgQueue.check_equal(test_block.stdout.strip(), expected_result) @@ -61,15 +61,15 @@ def testing_run(msgQueue: CheckingQueue): apply_function_inapp(self.window, testing_run) def test_run_block_with_path(self): - """runs blocks with the correct working directory for the kernel""" + """runs blocks with the correct working directory for the kernel.""" file_example_path = "./tests/assets/example_graph1.ipyg" asset_path = "./tests/assets/data.txt" - self.ocb_widget.scene.load(os.path.abspath(file_example_path)) + self._widget.scene.load(os.path.abspath(file_example_path)) def testing_path(msgQueue: CheckingQueue): - block_of_test: OCBCodeBlock = None - for item in self.ocb_widget.scene.items(): - if isinstance(item, OCBCodeBlock) and item.title == "test1": + block_of_test: CodeBlock = None + for item in self._widget.scene.items(): + if isinstance(item, CodeBlock) and item.title == "test1": block_of_test = item break msgQueue.check_equal( @@ -83,7 +83,7 @@ def run_block(): msgQueue.run_lambda(run_block) time.sleep(0.1) # wait for the lambda to complete. - while block_of_test.run_color != 0: + while block_of_test.run_state != 0: time.sleep(0.1) # wait for the execution to finish. time.sleep(0.1) diff --git a/tests/integration/blocks/test_flow.py b/tests/integration/blocks/test_flow.py index ac864559..672c5311 100644 --- a/tests/integration/blocks/test_flow.py +++ b/tests/integration/blocks/test_flow.py @@ -1,5 +1,5 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Integration tests for the execution flow. @@ -8,18 +8,18 @@ import pytest import time -from pyflow.blocks.codeblock import OCBCodeBlock +from pyflow.blocks.codeblock import CodeBlock from tests.integration.utils import apply_function_inapp, CheckingQueue, start_app -class TestCodeBlocks: +class TestCodeBlocksFlow: @pytest.fixture(autouse=True) def setup(self): """Setup reused variables.""" start_app(self) - self.ocb_widget.scene.load("tests/assets/flow_test.ipyg") + self._widget.scene.load("tests/assets/flow_test.ipyg") self.titles = [ "Test flow 5", @@ -29,26 +29,26 @@ def setup(self): "Test output only 1", ] self.blocks_to_run = [None] * 5 - for item in self.ocb_widget.scene.items(): - if isinstance(item, OCBCodeBlock): + for item in self._widget.scene.items(): + if isinstance(item, CodeBlock): if item.title in self.titles: self.blocks_to_run[self.titles.index(item.title)] = item def test_duplicated_run(self): - """Don't run a block twice when the execution flows""" + """run exactly one time pressing run right.""" for b in self.blocks_to_run: b.stdout = "" def testing_no_duplicates(msgQueue: CheckingQueue): - block_to_run: OCBCodeBlock = self.blocks_to_run[0] + block_to_run: CodeBlock = self.blocks_to_run[0] def run_block(): block_to_run.run_right() msgQueue.run_lambda(run_block) time.sleep((block_to_run.transmitting_duration / 1000) + 0.2) - while block_to_run.run_color != 0: + while block_to_run.run_state != 0: time.sleep(0.1) # 6 and not 6\n6 @@ -58,22 +58,22 @@ def run_block(): apply_function_inapp(self.window, testing_no_duplicates) def test_flow_left(self): - """Correct flow when pressing left run""" + """run its dependencies when pressing left run.""" for b in self.blocks_to_run: b.stdout = "" def testing_run(msgQueue: CheckingQueue): - block_to_run: OCBCodeBlock = self.blocks_to_run[0] - block_to_not_run: OCBCodeBlock = self.blocks_to_run[1] + block_to_run: CodeBlock = self.blocks_to_run[0] + block_to_not_run: CodeBlock = self.blocks_to_run[1] def run_block(): block_to_run.run_left() msgQueue.run_lambda(run_block) time.sleep((block_to_run.transmitting_duration / 1000) + 0.2) - while block_to_run.run_color != 0: + while block_to_run.run_state != 0: time.sleep(0.1) msgQueue.check_equal(block_to_run.stdout.strip(), "6") @@ -83,11 +83,11 @@ def run_block(): apply_function_inapp(self.window, testing_run) def test_no_connection_left(self): - """run block only when no previous connection.""" + """run itself only when has no dependecy and pressing left run.""" def testing_run(msgQueue: CheckingQueue): - block_to_run: OCBCodeBlock = self.blocks_to_run[ + block_to_run: CodeBlock = self.blocks_to_run[ self.titles.index("Test no connection 1") ] @@ -98,7 +98,7 @@ def run_block(): msgQueue.run_lambda(run_block) time.sleep((block_to_run.transmitting_duration / 1000) + 0.2) - while block_to_run.run_color != 0: + while block_to_run.run_state != 0: time.sleep(0.1) msgQueue.check_equal(block_to_run.stdout.strip(), "1") @@ -107,11 +107,11 @@ def run_block(): apply_function_inapp(self.window, testing_run) def test_no_connection_right(self): - """run block only when no next connection.""" + """run itself only when is not a dependecy and pressing right run.""" def testing_run(msgQueue: CheckingQueue): - block_to_run: OCBCodeBlock = self.blocks_to_run[ + block_to_run: CodeBlock = self.blocks_to_run[ self.titles.index("Test no connection 1") ] @@ -120,7 +120,7 @@ def run_block(): msgQueue.run_lambda(run_block) time.sleep((block_to_run.transmitting_duration / 1000) + 0.2) - while block_to_run.run_color != 0: + while block_to_run.run_state != 0: time.sleep(0.1) # Just check that it doesn't crash diff --git a/tests/integration/test_window.py b/tests/integration/test_window.py index 62cf5fd2..ee668aa1 100644 --- a/tests/integration/test_window.py +++ b/tests/integration/test_window.py @@ -1,31 +1,31 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ -Integration tests for the OCBWindow. +Integration tests for the Window. """ import os import pytest from pytest_mock import MockerFixture -from pyflow.graphics.window import OCBWindow +from pyflow.graphics.window import Window class TestWindow: @pytest.fixture(autouse=True) def setup(self, mocker: MockerFixture): """Setup reused variables.""" - self.window = OCBWindow() + self.window = Window() def test_open_file(self, qtbot): - """loads files""" - wnd = OCBWindow() + """loads files.""" + wnd = Window() file_example_path = "./tests/assets/example_graph1.ipyg" subwnd = wnd.createNewMdiChild(os.path.abspath(file_example_path)) subwnd.show() wnd.close() def test_window_close(self, qtbot): - """closes""" + """closes.""" self.window.close() diff --git a/tests/integration/utils.py b/tests/integration/utils.py index d34d8b9d..948b7b02 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -1,5 +1,5 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Utilities functions for integration testing. @@ -8,17 +8,21 @@ from typing import Callable import os -import asyncio +import warnings import threading import time from queue import Queue -from qtpy.QtWidgets import QApplication import pytest_check as check -import warnings -from pyflow.graphics.widget import OCBWidget +import asyncio -from pyflow.graphics.window import OCBWindow +from PyQt5.QtWidgets import QApplication + +from pyflow.graphics.widget import Widget +from pyflow.graphics.window import Window + +if os.name == "nt": # If on windows + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) STOP_MSG = "stop" CHECK_MSG = "check" @@ -37,45 +41,41 @@ def stop(self): class ExceptionForwardingThread(threading.Thread): - """A Thread class that forwards the exceptions to the calling thread""" + """A Thread class that forwards the exceptions to the calling thread.""" def __init__(self, *args, **kwargs): - """Create an exception forwarding thread""" + """Create an exception forwarding thread.""" super().__init__(*args, **kwargs) - self.e = None + self.exeption = None def run(self): - """Code ran in another thread""" + """Code ran in another thread.""" try: super().run() except Exception as e: - self.e = e + self.exeption = e def join(self): - """Used to sync the thread with the caller""" + """Used to sync the thread with the caller.""" super().join() - print("except: ", self.e) - if self.e != None: - raise self.e + print("except: ", self.exeption) + if self.exeption is not None: + raise self.exeption def start_app(obj): - """Create a new app for testing""" - obj.window = OCBWindow() - obj.ocb_widget = OCBWidget() - obj.subwindow = obj.window.mdiArea.addSubWindow(obj.ocb_widget) + """Create a new app for testing purpose.""" + obj.window = Window() + obj._widget = Widget() + obj.subwindow = obj.window.mdiArea.addSubWindow(obj._widget) obj.subwindow.show() -def apply_function_inapp(window: OCBWindow, run_func: Callable): - - if os.name == "nt": # If on windows - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - +def apply_function_inapp(window: Window, run_func: Callable): QApplication.processEvents() msgQueue = CheckingQueue() - t = ExceptionForwardingThread(target=run_func, args=(msgQueue,)) - t.start() + thread = ExceptionForwardingThread(target=run_func, args=(msgQueue,)) + thread.start() stop = False deadCounter = 0 @@ -92,7 +92,7 @@ def apply_function_inapp(window: OCBWindow, run_func: Callable): elif msg[0] == RUN_MSG: msg[1](*msg[2], **msg[3]) - if not t.is_alive() and not stop: + if not thread.is_alive() and not stop: deadCounter += 1 if deadCounter >= 3: # Test failed, close was not called @@ -100,4 +100,4 @@ def apply_function_inapp(window: OCBWindow, run_func: Callable): "Warning: you need to call CheckingQueue.stop() at the end of your test !" ) break - t.join() + thread.join() diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 2e33fcf8..f99ab9b4 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,4 +1,4 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Unit tests for the pyflow package. """ diff --git a/tests/unit/scene/__init__.py b/tests/unit/scene/__init__.py index 47614f0e..2352425e 100644 --- a/tests/unit/scene/__init__.py +++ b/tests/unit/scene/__init__.py @@ -1,4 +1,4 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Unit tests for the pyflow scene module. """ diff --git a/tests/unit/scene/test_clipboard.py b/tests/unit/scene/test_clipboard.py index cdf77d64..4321be3b 100644 --- a/tests/unit/scene/test_clipboard.py +++ b/tests/unit/scene/test_clipboard.py @@ -1,5 +1,5 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Unit tests for the pyflow history module. """ diff --git a/tests/unit/scene/test_history.py b/tests/unit/scene/test_history.py index bd2dd8a6..907e2fc2 100644 --- a/tests/unit/scene/test_history.py +++ b/tests/unit/scene/test_history.py @@ -1,5 +1,5 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium """ Unit tests for the pyflow history module. """ diff --git a/tests/unit/scene/test_ipynb_conversion.py b/tests/unit/scene/test_ipynb_conversion.py index 9376644b..0b3fa71c 100644 --- a/tests/unit/scene/test_ipynb_conversion.py +++ b/tests/unit/scene/test_ipynb_conversion.py @@ -1,3 +1,6 @@ +# Pyflow an open-source tool for modular visual programing in python +# Copyright (C) 2021-2022 Bycelium + """Unit tests for the conversion from and to ipynb.""" from typing import OrderedDict diff --git a/utils.py b/utils.py index 59141d56..cf48f705 100644 --- a/utils.py +++ b/utils.py @@ -1,7 +1,7 @@ # Pyflow an open-source tool for modular visual programing in python -# Copyright (C) 2021 Mathïs FEDERICO +# Copyright (C) 2021-2022 Bycelium -""" Module for badges colors """ +""" Module for badges colors.""" from colorsys import hsv_to_rgb