diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index eba53b6..2ebdfa6 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -60,8 +60,7 @@ representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -[INSERT CONTACT METHOD]. +reported to the community leaders responsible for enforcement. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/README.rst b/README.rst index 56758ad..876ff49 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,59 @@ py5 --- -py5 is a new version of Processing_ for Python 3.8+. It makes the Processing_ Java libraries available to the CPython interpreter using JPype_. +.. image:: https://img.shields.io/pypi/dm/py5?label=py5%20PyPI%20downloads -This entire repository is created by the meta-programming project py5generator_. Therefore, this code should not be edited manually. Any issues, etc, should be directed to the py5generator_ repository. +.. image:: https://mybinder.org/badge_logo.svg + :target: https://mybinder.org/v2/gh/hx2A/py5examples/HEAD?urlpath=lab + +py5 is a new version of Processing_ for Python 3.8+. It makes the Processing_ Java libraries available to the CPython interpreter using JPype_. It can do just about everything Processing_ can do, except with Python instead of Java code. + +The goal of py5 is to create a new version of Processing that is integrated into the Python ecosystem. Built into the library are thoughtful choices about how to best get py5 to work with other popular Python libraries such as `numpy +`_ or `Pillow +`_. + +Here is a simple example of a working py5 Sketch: + +.. code:: + + import py5 + + + def setup(): + py5.size(200, 200) + py5.rect_mode(py5.CENTER) + + + def draw(): + py5.square(py5.mouse_x, py5.mouse_y, 10) + + + py5.run_sketch() + + + +If you have Java 11 installed on your computer, you can install py5 using pip: + +.. code:: + + pip install py5 + +`Detailed installation instructions +`_ are available on the documentation website. There are some `Special Notes for Mac Users +`_ that you should read if you use OSX. + +There are currently four basic ways to use py5. They are: + +- **module mode**, as shown above +- **class mode**: create a Python class inherited from ``py5.Sketch``, and support multiple Sketches running at the same time. +- **imported mode**: simplified code that omits the ``py5.`` prefix. This mode is supported by the py5 Jupyter notebook kernel and the ``run_sketch`` command line utility. +- **static mode**: functionless code to create static images. This mode is supported by the py5bot Jupyter notebook kernel and the ``%%py5bot`` IPython magic. + +The py5 library is created by the meta-programming project py5generator_. Therefore, the py5 code should not be changed manually. Any issues, etc, should be directed to the py5generator_ repository. The `py5 documentation website -`_ provides basic tutorials and reference documentation. See py5examples_ for example code. +`_ provides basic tutorials and reference documentation. The website is very much a work in progress. The `reference documentation +`_ is solid but the how-to's and tutorials need a lot of work. See the py5examples_ repository for some working examples. .. _Processing: https://github.com/processing/processing4 .. _JPype: https://github.com/jpype-project/jpype diff --git a/py5/__init__.py b/py5/__init__.py index 0fafd06..e412743 100644 --- a/py5/__init__.py +++ b/py5/__init__.py @@ -19,7 +19,7 @@ # ***************************************************************************** # -*- coding: utf-8 -*- """ -py5 makes Processing available to the CPython interpreter using JPype. +py5 is a version of Processing for Python 3.8+. It makes the Processing Java libraries available to the CPython interpreter using JPype. """ import sys from pathlib import Path @@ -27,7 +27,6 @@ from typing import overload, Any, Callable, Union, Dict, List, Tuple # noqa from nptyping import NDArray, Float, Int # noqa -# import json # noqa import numpy as np # noqa from PIL import Image # noqa from jpype import JClass # noqa @@ -52,6 +51,7 @@ from .render_helper import render_frame, render_frame_sequence, render, render_sequence # noqa from .create_font_tool import create_font_file # noqa from .image_conversion import register_image_conversion, NumpyImageArray # noqa +from py5_tools import split_setup as _split_setup from . import reference from . import java_conversion # noqa try: @@ -61,9 +61,9 @@ pass -__version__ = '0.4a2' +__version__ = '0.5a0' -_PY5_USE_IMPORTED_MODE = py5_tools.imported.get_imported_mode() +_PY5_USE_IMPORTED_MODE = py5_tools.get_imported_mode() java_conversion.init_jpype_converters() @@ -258,7 +258,7 @@ X = 0 Y = 1 Z = 2 -args: List[str] = None +pargs: List[str] = None display_height: int = None display_width: int = None finished: bool = None @@ -4990,8 +4990,8 @@ def create_graphics(w: int, h: int, /) -> Py5Graphics: Creates and returns a new ``Py5Graphics`` object. Use this class if you need to draw into an off-screen graphics buffer. The first two parameters define the width and height in pixels. The third, optional parameter specifies the - renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or SVG. If the third - parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` + renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or ``SVG``. If the + third parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` renderers require the filename parameter. It's important to consider the renderer used with ``create_graphics()`` in @@ -5061,8 +5061,8 @@ def create_graphics(w: int, h: int, renderer: str, /) -> Py5Graphics: Creates and returns a new ``Py5Graphics`` object. Use this class if you need to draw into an off-screen graphics buffer. The first two parameters define the width and height in pixels. The third, optional parameter specifies the - renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or SVG. If the third - parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` + renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or ``SVG``. If the + third parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` renderers require the filename parameter. It's important to consider the renderer used with ``create_graphics()`` in @@ -5133,8 +5133,8 @@ def create_graphics(w: int, h: int, renderer: str, Creates and returns a new ``Py5Graphics`` object. Use this class if you need to draw into an off-screen graphics buffer. The first two parameters define the width and height in pixels. The third, optional parameter specifies the - renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or SVG. If the third - parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` + renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or ``SVG``. If the + third parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` renderers require the filename parameter. It's important to consider the renderer used with ``create_graphics()`` in @@ -5203,8 +5203,8 @@ def create_graphics(*args): Creates and returns a new ``Py5Graphics`` object. Use this class if you need to draw into an off-screen graphics buffer. The first two parameters define the width and height in pixels. The third, optional parameter specifies the - renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or SVG. If the third - parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` + renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or ``SVG``. If the + third parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` renderers require the filename parameter. It's important to consider the renderer used with ``create_graphics()`` in @@ -7570,9 +7570,21 @@ def full_screen() -> None: Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -7613,9 +7625,21 @@ def full_screen(display: int, /) -> None: Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -7656,9 +7680,21 @@ def full_screen(renderer: str, /) -> None: Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -7699,9 +7735,21 @@ def full_screen(renderer: str, display: int, /) -> None: Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -7741,9 +7789,21 @@ def full_screen(*args): Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -9601,9 +9661,23 @@ def no_smooth() -> None: Draws all geometry and fonts with jagged (aliased) edges and images with hard edges between the pixels when enlarged rather than interpolating pixels. Note that ``smooth()`` is active by default, so it is necessary to call - ``no_smooth()`` to disable smoothing of geometry, fonts, and images. The - ``no_smooth()`` method can only be run once for each Sketch and must be called - in ``settings()``. + ``no_smooth()`` to disable smoothing of geometry, fonts, and images. + + The ``no_smooth()`` function can only be called once within a Sketch. It is + intended to be called from the ``settings()`` function. The ``smooth()`` + function follows the same rules. + + When programming in module mode and imported mode, py5 will allow calls to + ``no_smooth()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``no_smooth()``, or calls + to ``size()``, ``full_screen()``, ``smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. """ return _py5sketch.no_smooth() @@ -10023,18 +10097,29 @@ def pixel_density(density: int, /) -> None: This function makes it possible for py5 to render using all of the pixels on high resolutions screens like Apple Retina displays and Windows High-DPI - displays. This function can only be run once within a program and it must be - called in ``settings()``. The ``pixel_density()`` should only be used with - hardcoded numbers (in almost all cases this number will be 2) or in combination - with ``display_density()`` as in the second example. + displays. This function can only be run once within a program. It is intended to + be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``pixel_density()`` from the ``setup()`` function if it is called at the + beginning of ``setup()``. This allows the user to omit the ``settings()`` + function, much like what can be done while programming in the Processing IDE. + Py5 does this by inspecting the ``setup()`` function and attempting to split it + into synthetic ``settings()`` and ``setup()`` functions if both were not created + by the user and the real ``setup()`` function contains a call to + ``pixel_density()``, or calls to ``size()``, ``full_screen()``, ``smooth()``, or + ``no_smooth()``. Calls to those functions must be at the very beginning of + ``setup()``, before any other Python code (but comments are ok). This feature is + not available when programming in class mode. + + The ``pixel_density()`` should only be used with hardcoded numbers (in almost + all cases this number will be 2) or in combination with ``display_density()`` as + in the second example. When the pixel density is set to more than 1, it changes all of the pixel operations including the way ``get()``, ``blend()``, ``copy()``, ``update_pixels()``, and ``update_np_pixels()`` all work. See the reference for ``pixel_width`` and ``pixel_height`` for more information. - - To use variables as the arguments to ``pixel_density()`` function, place the - ``pixel_density()`` function within the ``settings()`` function. """ return _py5sketch.pixel_density(density) @@ -12371,7 +12456,19 @@ def size(width: int, height: int, /) -> None: ----- Defines the dimension of the display window width and height in units of pixels. - This must be called from the ``settings()`` function. + This is intended to be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``size()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``size()``, or calls to + ``full_screen()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls + to those functions must be at the very beginning of ``setup()``, before any + other Python code (but comments are ok). This feature is not available when + programming in class mode. The built-in variables ``width`` and ``height`` are set by the parameters passed to this function. For example, running ``size(640, 480)`` will assign 640 to the @@ -12451,7 +12548,19 @@ def size(width: int, height: int, renderer: str, /) -> None: ----- Defines the dimension of the display window width and height in units of pixels. - This must be called from the ``settings()`` function. + This is intended to be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``size()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``size()``, or calls to + ``full_screen()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls + to those functions must be at the very beginning of ``setup()``, before any + other Python code (but comments are ok). This feature is not available when + programming in class mode. The built-in variables ``width`` and ``height`` are set by the parameters passed to this function. For example, running ``size(640, 480)`` will assign 640 to the @@ -12531,7 +12640,19 @@ def size(width: int, height: int, renderer: str, path: str, /) -> None: ----- Defines the dimension of the display window width and height in units of pixels. - This must be called from the ``settings()`` function. + This is intended to be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``size()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``size()``, or calls to + ``full_screen()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls + to those functions must be at the very beginning of ``setup()``, before any + other Python code (but comments are ok). This feature is not available when + programming in class mode. The built-in variables ``width`` and ``height`` are set by the parameters passed to this function. For example, running ``size(640, 480)`` will assign 640 to the @@ -12610,7 +12731,19 @@ def size(*args): ----- Defines the dimension of the display window width and height in units of pixels. - This must be called from the ``settings()`` function. + This is intended to be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``size()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``size()``, or calls to + ``full_screen()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls + to those functions must be at the very beginning of ``setup()``, before any + other Python code (but comments are ok). This feature is not available when + programming in class mode. The built-in variables ``width`` and ``height`` are set by the parameters passed to this function. For example, running ``size(640, 480)`` will assign 640 to the @@ -12694,9 +12827,21 @@ def smooth() -> None: The other option for the default renderer is ``smooth(2)``, which is bilinear smoothing. - The ``smooth()`` function can only be set once within a Sketch. It must be - called from the `settings()`` function. The ``no_smooth()`` function also + The ``smooth()`` function can only be set once within a Sketch. It is intended + to be called from the ``settings()`` function. The ``no_smooth()`` function follows the same rules. + + When programming in module mode and imported mode, py5 will allow calls to + ``smooth()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``smooth()``, or calls to + ``size()``, ``full_screen()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. """ pass @@ -12739,9 +12884,21 @@ def smooth(level: int, /) -> None: The other option for the default renderer is ``smooth(2)``, which is bilinear smoothing. - The ``smooth()`` function can only be set once within a Sketch. It must be - called from the `settings()`` function. The ``no_smooth()`` function also + The ``smooth()`` function can only be set once within a Sketch. It is intended + to be called from the ``settings()`` function. The ``no_smooth()`` function follows the same rules. + + When programming in module mode and imported mode, py5 will allow calls to + ``smooth()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``smooth()``, or calls to + ``size()``, ``full_screen()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. """ pass @@ -12783,9 +12940,21 @@ def smooth(*args): The other option for the default renderer is ``smooth(2)``, which is bilinear smoothing. - The ``smooth()`` function can only be set once within a Sketch. It must be - called from the `settings()`` function. The ``no_smooth()`` function also + The ``smooth()`` function can only be set once within a Sketch. It is intended + to be called from the ``settings()`` function. The ``no_smooth()`` function follows the same rules. + + When programming in module mode and imported mode, py5 will allow calls to + ``smooth()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``smooth()``, or calls to + ``size()``, ``full_screen()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. """ return _py5sketch.smooth(*args) @@ -16563,146 +16732,6 @@ def year() -> int: """ return Sketch.year() -############################################################################## -# module functions from pixels.py -############################################################################## - - -def load_np_pixels() -> None: - """Loads the pixel data of the current display window into the ``np_pixels[]`` - array. - - Notes - ----- - - Loads the pixel data of the current display window into the ``np_pixels[]`` - array. This method must always be called before reading from or writing to - ``np_pixels[]``. Subsequent changes to the display window will not be reflected - in ``np_pixels[]`` until ``load_np_pixels()`` is called again. - - The ``load_np_pixels()`` method is similar to ``load_pixels()`` in that - ``load_np_pixels()`` must be called before reading from or writing to - ``np_pixels[]`` just as ``load_pixels()`` must be called before reading from or - writing to ``pixels[]``. - - Note that ``load_np_pixels()`` will as a side effect call ``load_pixels()``, so - if your code needs to read ``np_pixels[]`` and ``pixels[]`` simultaneously, - there is no need for a separate call to ``load_pixels()``. However, be aware - that modifying both ``np_pixels[]`` and ``pixels[]`` simultaneously will likely - result in the updates to ``pixels[]`` being discarded. - """ - return _py5sketch.load_np_pixels() - - -def update_np_pixels() -> None: - """Updates the display window with the data in the ``np_pixels[]`` array. - - Notes - ----- - - Updates the display window with the data in the ``np_pixels[]`` array. Use in - conjunction with ``load_np_pixels()``. If you're only reading pixels from the - array, there's no need to call ``update_np_pixels()`` — updating is only - necessary to apply changes. - - The ``update_np_pixels()`` method is similar to ``update_pixels()`` in that - ``update_np_pixels()`` must be called after modifying ``np_pixels[]`` just as - ``update_pixels()`` must be called after modifying ``pixels[]``. - """ - return _py5sketch.update_np_pixels() - - -np_pixels: np.ndarray = None - - -def set_np_pixels(array: np.ndarray, bands: str = 'ARGB') -> None: - """Set the entire contents of ``np_pixels[]`` to the contents of another properly - sized and typed numpy array. - - Parameters - ---------- - - array: np.ndarray - properly sized numpy array to be copied to np_pixels[] - - bands: str = 'ARGB' - color channels in the array's third dimension - - Notes - ----- - - Set the entire contents of ``np_pixels[]`` to the contents of another properly - sized and typed numpy array. The size of ``array``'s first and second dimensions - must match the height and width of the Sketch window, respectively. The array's - ``dtype`` must be ``np.uint8``. - - The ``bands`` parameter is used to interpret the ``array``'s color channel - dimension (the array's third dimension). It can be one of ``'L'`` (single- - channel grayscale), ``'ARGB'``, ``'RGB'``, or ``'RGBA'``. If there is no alpha - channel, ``array`` is assumed to have no transparency, but recall that the - display window's pixels can never be transparent so any transparency in - ``array`` will have no effect. If the ``bands`` parameter is ``'L'``, - ``array``'s third dimension is optional. - - This method makes its own calls to ``load_np_pixels()`` and - ``update_np_pixels()`` so there is no need to call either explicitly. - - This method exists because setting the array contents with the code - ``py5.np_pixels = array`` will cause an error, while the correct syntax, - ``py5.np_pixels[:] = array``, might also be unintuitive for beginners. - """ - return _py5sketch.set_np_pixels(array, bands=bands) - - -def save(filename: Union[str, - Path], - *, - format: str = None, - drop_alpha: bool = True, - use_thread: bool = True, - **params) -> None: - """Save image data to a file. - - Parameters - ---------- - - drop_alpha: bool = True - remove the alpha channel when saving the image - - filename: Union[str, Path] - output filename - - format: str = None - image format, if not determined from filename extension - - params - keyword arguments to pass to the PIL.Image save method - - use_thread: bool = True - write file in separate thread - - Notes - ----- - - Save image data to a file. This method uses the Python library Pillow to write - the image, so it can save images in any format that that library supports. - - Use the ``drop_alpha`` parameter to drop the alpha channel from the image. This - defaults to ``True``. Some image formats such as JPG do not support alpha - channels, and Pillow will throw an error if you try to save an image with the - alpha channel in that format. - - The ``use_thread`` parameter will save the image in a separate Python thread. - This improves performance by returning before the image has actually been - written to the file. - """ - return _py5sketch.save( - filename, - format=format, - drop_alpha=drop_alpha, - use_thread=use_thread, - **params) - SIMPLEX_NOISE = 1 PERLIN_NOISE = 2 @@ -18656,6 +18685,221 @@ def parse_json(serialized_json: Any, **kwargs: Dict[str, Any]) -> Any: """ return Sketch.parse_json(serialized_json, **kwargs) +############################################################################## +# module functions from print_tools.py +############################################################################## + + +def set_println_stream(println_stream: Any) -> None: + """Customize where the output of ``println()`` goes. + + Parameters + ---------- + + println_stream: Any + println stream object to be used by println method + + Notes + ----- + + Customize where the output of ``println()`` goes. + + When running a Sketch asynchronously through Jupyter Notebook, any ``print`` + statements using Python's builtin function will always appear in the output of + the currently active cell. This will rarely be desirable, as the active cell + will keep changing as the user executes code elsewhere in the notebook. The + ``println()`` method was created to provide users with print functionality in a + Sketch without having to cope with output moving from one cell to the next. Use + ``set_println_stream`` to change how the output is handled. The + ``println_stream`` object must provide ``init()`` and ``print()`` methods, as + shown in the example. The example demonstrates how to configure py5 to output + text to an IPython Widget. + """ + return _py5sketch.set_println_stream(println_stream) + + +def println( + *args, + sep: str = ' ', + end: str = '\n', + stderr: bool = False) -> None: + """Print text or other values to the screen. + + Parameters + ---------- + + args + values to be printed + + end: str = '\\n' + string appended after the last value, defaults to newline character + + sep: str = ' ' + string inserted between values, defaults to a space + + stderr: bool = False + use stderr instead of stdout + + Notes + ----- + + Print text or other values to the screen. For a Sketch running outside of a + Jupyter Notebook, this method will behave the same as the Python's builtin + ``print`` method. For Sketches running in a Jupyter Notebook, this will place + text in the output of the cell that made the ``run_sketch()`` call. + + When running a Sketch asynchronously through Jupyter Notebook, any ``print`` + statements using Python's builtin function will always appear in the output of + the currently active cell. This will rarely be desirable, as the active cell + will keep changing as the user executes code elsewhere in the notebook. This + method was created to provide users with print functionality in a Sketch without + having to cope with output moving from one cell to the next. + + Use ``set_println_stream()`` to customize the behavior of ``println()``. + """ + return _py5sketch.println(*args, sep=sep, end=end, stderr=stderr) + +############################################################################## +# module functions from pixels.py +############################################################################## + + +def load_np_pixels() -> None: + """Loads the pixel data of the current display window into the ``np_pixels[]`` + array. + + Notes + ----- + + Loads the pixel data of the current display window into the ``np_pixels[]`` + array. This method must always be called before reading from or writing to + ``np_pixels[]``. Subsequent changes to the display window will not be reflected + in ``np_pixels[]`` until ``load_np_pixels()`` is called again. + + The ``load_np_pixels()`` method is similar to ``load_pixels()`` in that + ``load_np_pixels()`` must be called before reading from or writing to + ``np_pixels[]`` just as ``load_pixels()`` must be called before reading from or + writing to ``pixels[]``. + + Note that ``load_np_pixels()`` will as a side effect call ``load_pixels()``, so + if your code needs to read ``np_pixels[]`` and ``pixels[]`` simultaneously, + there is no need for a separate call to ``load_pixels()``. However, be aware + that modifying both ``np_pixels[]`` and ``pixels[]`` simultaneously will likely + result in the updates to ``pixels[]`` being discarded. + """ + return _py5sketch.load_np_pixels() + + +def update_np_pixels() -> None: + """Updates the display window with the data in the ``np_pixels[]`` array. + + Notes + ----- + + Updates the display window with the data in the ``np_pixels[]`` array. Use in + conjunction with ``load_np_pixels()``. If you're only reading pixels from the + array, there's no need to call ``update_np_pixels()`` — updating is only + necessary to apply changes. + + The ``update_np_pixels()`` method is similar to ``update_pixels()`` in that + ``update_np_pixels()`` must be called after modifying ``np_pixels[]`` just as + ``update_pixels()`` must be called after modifying ``pixels[]``. + """ + return _py5sketch.update_np_pixels() + + +np_pixels: np.ndarray = None + + +def set_np_pixels(array: np.ndarray, bands: str = 'ARGB') -> None: + """Set the entire contents of ``np_pixels[]`` to the contents of another properly + sized and typed numpy array. + + Parameters + ---------- + + array: np.ndarray + properly sized numpy array to be copied to np_pixels[] + + bands: str = 'ARGB' + color channels in the array's third dimension + + Notes + ----- + + Set the entire contents of ``np_pixels[]`` to the contents of another properly + sized and typed numpy array. The size of ``array``'s first and second dimensions + must match the height and width of the Sketch window, respectively. The array's + ``dtype`` must be ``np.uint8``. + + The ``bands`` parameter is used to interpret the ``array``'s color channel + dimension (the array's third dimension). It can be one of ``'L'`` (single- + channel grayscale), ``'ARGB'``, ``'RGB'``, or ``'RGBA'``. If there is no alpha + channel, ``array`` is assumed to have no transparency, but recall that the + display window's pixels can never be transparent so any transparency in + ``array`` will have no effect. If the ``bands`` parameter is ``'L'``, + ``array``'s third dimension is optional. + + This method makes its own calls to ``load_np_pixels()`` and + ``update_np_pixels()`` so there is no need to call either explicitly. + + This method exists because setting the array contents with the code + ``py5.np_pixels = array`` will cause an error, while the correct syntax, + ``py5.np_pixels[:] = array``, might also be unintuitive for beginners. + """ + return _py5sketch.set_np_pixels(array, bands=bands) + + +def save(filename: Union[str, + Path], + *, + format: str = None, + drop_alpha: bool = True, + use_thread: bool = False, + **params) -> None: + """Save the drawing surface to an image file. + + Parameters + ---------- + + drop_alpha: bool = True + remove the alpha channel when saving the image + + filename: Union[str, Path] + output filename + + format: str = None + image format, if not determined from filename extension + + params + keyword arguments to pass to the PIL.Image save method + + use_thread: bool = False + write file in separate thread + + Notes + ----- + + Save the drawing surface to an image file. This method uses the Python library + Pillow to write the image, so it can save images in any format that that library + supports. + + Use the ``drop_alpha`` parameter to drop the alpha channel from the image. This + defaults to ``True``. Some image formats such as JPG do not support alpha + channels, and Pillow will throw an error if you try to save an image with the + alpha channel in that format. + + The ``use_thread`` parameter will save the image in a separate Python thread. + This improves performance by returning before the image has actually been + written to the file. + """ + return _py5sketch.save( + filename, + format=format, + drop_alpha=drop_alpha, + use_thread=use_thread, + **params) + ############################################################################## # module functions from threads.py ############################################################################## @@ -18941,80 +19185,6 @@ def list_threads() -> None: """ return _py5sketch.list_threads() -############################################################################## -# module functions from print_tools.py -############################################################################## - - -def set_println_stream(println_stream: Any) -> None: - """Customize where the output of ``println()`` goes. - - Parameters - ---------- - - println_stream: Any - println stream object to be used by println method - - Notes - ----- - - Customize where the output of ``println()`` goes. - - When running a Sketch asynchronously through Jupyter Notebook, any ``print`` - statements using Python's builtin function will always appear in the output of - the currently active cell. This will rarely be desirable, as the active cell - will keep changing as the user executes code elsewhere in the notebook. The - ``println()`` method was created to provide users with print functionality in a - Sketch without having to cope with output moving from one cell to the next. Use - ``set_println_stream`` to change how the output is handled. The - ``println_stream`` object must provide ``init()`` and ``print()`` methods, as - shown in the example. The example demonstrates how to configure py5 to output - text to an IPython Widget. - """ - return _py5sketch.set_println_stream(println_stream) - - -def println( - *args, - sep: str = ' ', - end: str = '\n', - stderr: bool = False) -> None: - """Print text or other values to the screen. - - Parameters - ---------- - - args - values to be printed - - end: str = '\\n' - string appended after the last value, defaults to newline character - - sep: str = ' ' - string inserted between values, defaults to a space - - stderr: bool = False - use stderr instead of stdout - - Notes - ----- - - Print text or other values to the screen. For a Sketch running outside of a - Jupyter Notebook, this method will behave the same as the Python's builtin - ``print`` method. For Sketches running in a Jupyter Notebook, this will place - text in the output of the cell that made the ``run_sketch()`` call. - - When running a Sketch asynchronously through Jupyter Notebook, any ``print`` - statements using Python's builtin function will always appear in the output of - the currently active cell. This will rarely be desirable, as the active cell - will keep changing as the user executes code elsewhere in the notebook. This - method was created to provide users with print functionality in a Sketch without - having to cope with output moving from one cell to the next. - - Use ``set_println_stream()`` to customize the behavior of ``println()``. - """ - return _py5sketch.println(*args, sep=sep, end=end, stderr=stderr) - ############################################################################## # module functions from sketch.py ############################################################################## @@ -19222,7 +19392,7 @@ def save_frame(filename: Union[str, *, format: str = None, drop_alpha: bool = True, - use_thread: bool = True, + use_thread: bool = False, **params) -> None: """Save the current frame as an image. @@ -19241,7 +19411,7 @@ def save_frame(filename: Union[str, params keyword arguments to pass to the PIL.Image save method - use_thread: bool = True + use_thread: bool = False write file in separate thread Notes @@ -19452,10 +19622,14 @@ def run_sketch(block: bool = None, *, determine if the Sketch is running in a Jupyter Notebook or an IPython shell. If it is, ``block`` will default to ``False``, and ``True`` otherwise. + Blocking is not supported on OSX. This is because of the (current) limitations + of py5 on OSX. If the ``block`` parameter is set to ``True``, a warning message + will appear and it will be changed to ``False``. + A list of strings passed to ``py5_options`` will be passed to the Processing PApplet class as arguments to specify characteristics such as the window's location on the screen. A list of strings passed to ``sketch_args`` will be - available to a running Sketch using ``args``. See the third example for an + available to a running Sketch using ``pargs``. See the third example for an example of how this can be used. When calling ``run_sketch()`` in module mode, py5 will by default search for @@ -19467,6 +19641,16 @@ def run_sketch(block: bool = None, *, in class mode. Don't forget you can always replace the ``draw()`` function in a running Sketch using ``hot_reload_draw()``. + When programming in module mode and imported mode, py5 will inspect the + ``setup()`` function and will attempt to split it into synthetic ``settings()`` + and ``setup()`` functions if both were not created by the user and the real + ``setup()`` function contains calls to ``size()``, ``full_screen()``, + ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to those functions + must be at the very beginning of ``setup()``, before any other Python code + (except for comments). This feature allows the user to omit the ``settings()`` + function, much like what can be done while programming in the Processing IDE. + This feature is not available when programming in class mode. + When running a Sketch asynchronously through Jupyter Notebook, any ``print`` statements using Python's builtin function will always appear in the output of the currently active cell. This will rarely be desirable, as the active cell @@ -19479,12 +19663,20 @@ def run_sketch(block: bool = None, *, error messages and warnings generated inside the Processing Jars cannot be controlled in the same way, and may appear in the output of the active cell or mixed in with the Jupyter Kernel logs.""" - if block is None: - block = not _in_ipython_session - - sketch_functions = sketch_functions or inspect.stack()[1].frame.f_locals - functions = dict([(e, sketch_functions[e]) - for e in reference.METHODS if e in sketch_functions and callable(sketch_functions[e])]) + caller_globals = inspect.stack()[1].frame.f_globals + caller_locals = inspect.stack()[1].frame.f_locals + if sketch_functions: + functions = dict([(e, sketch_functions[e]) + for e in reference.METHODS if e in sketch_functions and callable(sketch_functions[e])]) + else: + functions = dict([(e, caller_locals[e]) + for e in reference.METHODS if e in caller_locals and callable(caller_locals[e])]) + functions = _split_setup.transform( + functions, + caller_globals, + caller_locals, + println, + mode='imported' if _PY5_USE_IMPORTED_MODE else 'module') if not set(functions.keys()) & set(['settings', 'setup', 'draw']): print(("Unable to find settings, setup, or draw functions. " @@ -19501,7 +19693,7 @@ def run_sketch(block: bool = None, *, if _py5sketch.is_dead: _py5sketch = Sketch() - _prepare_dynamic_variables(sketch_functions) + _prepare_dynamic_variables(caller_locals) _py5sketch._run_sketch(functions, block, py5_options, sketch_args) diff --git a/py5/graphics.py b/py5/graphics.py index 2759ca9..a87cff0 100644 --- a/py5/graphics.py +++ b/py5/graphics.py @@ -28,7 +28,7 @@ from jpype import JClass from .base import Py5Base -from .mixins import PixelMixin +from .mixins import PixelPy5GraphicsMixin from .font import Py5Font # noqa from .shader import Py5Shader, _return_py5shader, _load_py5shader # noqa from .shape import Py5Shape, _return_py5shape, _load_py5shape # noqa @@ -49,7 +49,7 @@ def decorated(self_, *args): _Py5GraphicsHelper = JClass('py5.core.Py5GraphicsHelper') -class Py5Graphics(PixelMixin, Py5Base): +class Py5Graphics(PixelPy5GraphicsMixin, Py5Base): """Main graphics and rendering context, as well as the base ``API`` implementation for processing "core". @@ -65,6 +65,11 @@ class Py5Graphics(PixelMixin, Py5Base): ``Py5Graphics.end_draw()`` methods (see example) are necessary to set up the buffer and to finalize it. The fields and methods for this class are extensive. + It is critically important that calls to this object's drawing methods are only + used between ``Py5Graphics.begin_draw()`` and ``Py5Graphics.end_draw()``. + Forgetting to call ``Py5Graphics.begin_draw()`` will likely result in an ugly + and unhelpful Java exception. + To create a new graphics context, use the ``create_graphics()`` function. Do not use the syntax ``Py5Graphics()``. """ @@ -306,21 +311,19 @@ def _get_pixel_density(self) -> int: pixel_density: int = property(fget=_get_pixel_density) def _get_pixel_height(self) -> int: - """When ``pixel_density(2)`` was used in ``settings()`` to make use of a high - resolution display (called a Retina display on OSX or high-dpi on Windows and - Linux), the width and height of the Py5Graphics drawing surface does not change, - but the number of pixels is doubled. + """Height of the Py5Graphics drawing surface in pixels. Underlying Java field: PGraphics.pixelHeight Notes ----- - When ``pixel_density(2)`` was used in ``settings()`` to make use of a high - resolution display (called a Retina display on OSX or high-dpi on Windows and - Linux), the width and height of the Py5Graphics drawing surface does not change, - but the number of pixels is doubled. As a result, all operations that use pixels - (like ``Py5Graphics.load_pixels()``, ``Py5Graphics.get()``, etc.) happen in this + Height of the Py5Graphics drawing surface in pixels. When ``pixel_density(2)`` + was used in ``settings()`` to make use of a high resolution display (called a + Retina display on OSX or high-dpi on Windows and Linux), the width and height of + the Py5Graphics drawing surface does not change, but the number of pixels is + doubled. As a result, all operations that use pixels (like + ``Py5Graphics.load_pixels()``, ``Py5Graphics.get()``, etc.) happen in this doubled space. As a convenience, the variables ``Py5Graphics.pixel_width`` and ``pixel_height`` hold the actual width and height of the drawing surface in pixels. This is useful for any Py5Graphics objects that use the @@ -335,21 +338,19 @@ def _get_pixel_height(self) -> int: pixel_height: int = property(fget=_get_pixel_height) def _get_pixel_width(self) -> int: - """When ``pixel_density(2)`` was used in ``settings()`` to make use of a high - resolution display (called a Retina display on OSX or high-dpi on Windows and - Linux), the width and height of the Py5Graphics drawing surface does not change, - but the number of pixels is doubled. + """Width of the Py5Graphics drawing surface in pixels. Underlying Java field: PGraphics.pixelWidth Notes ----- - When ``pixel_density(2)`` was used in ``settings()`` to make use of a high - resolution display (called a Retina display on OSX or high-dpi on Windows and - Linux), the width and height of the Py5Graphics drawing surface does not change, - but the number of pixels is doubled. As a result, all operations that use pixels - (like ``Py5Graphics.load_pixels()``, ``Py5Graphics.get()``, etc.) happen in this + Width of the Py5Graphics drawing surface in pixels. When ``pixel_density(2)`` + was used in ``settings()`` to make use of a high resolution display (called a + Retina display on OSX or high-dpi on Windows and Linux), the width and height of + the Py5Graphics drawing surface does not change, but the number of pixels is + doubled. As a result, all operations that use pixels (like + ``Py5Graphics.load_pixels()``, ``Py5Graphics.get()``, etc.) happen in this doubled space. As a convenience, the variables ``pixel_width`` and ``Py5Graphics.pixel_height`` hold the actual width and height of the drawing surface in pixels. This is useful for any Py5Graphics objects that use the @@ -7220,6 +7221,9 @@ def hint(self, which: int, /) -> None: options might graduate to standard features instead of hints over time, or be added and removed between (major) releases. + Like other ``Py5Graphics`` methods, ``hint()`` can only be used between calls to + ``Py5Graphics.begin_draw()`` and ``Py5Graphics.end_draw()``. + Hints used by the Default Renderer ---------------------------------- diff --git a/py5/image.py b/py5/image.py index e4aac16..940bc01 100644 --- a/py5/image.py +++ b/py5/image.py @@ -23,7 +23,7 @@ from typing import overload, List, Union # noqa from .base import Py5Base -from .mixins import PixelMixin +from .mixins import PixelPy5ImageMixin def _return_py5image(f): @@ -37,7 +37,7 @@ def decorated(self_, *args): return decorated -class Py5Image(PixelMixin, Py5Base): +class Py5Image(PixelPy5ImageMixin, Py5Base): """Datatype for storing images. Underlying Java class: PImage.PImage @@ -117,6 +117,51 @@ def _get_height(self) -> int: return self._instance.height height: int = property(fget=_get_height) + def _get_pixel_density(self) -> int: + """Pixel density of the Py5Image object. + + Underlying Java field: PImage.pixelDensity + + Notes + ----- + + Pixel density of the Py5Image object. This will always be equal to 1, even if + the Sketch used ``pixel_density()`` to set the pixel density to a value greater + than 1. + """ + return self._instance.pixelDensity + pixel_density: int = property(fget=_get_pixel_density) + + def _get_pixel_height(self) -> int: + """Height of the Py5Image object in pixels. + + Underlying Java field: PImage.pixelHeight + + Notes + ----- + + Height of the Py5Image object in pixels. This will be the same as + ``Py5Image.height``, even if the Sketch used ``pixel_density()`` to set the + pixel density to a value greater than 1. + """ + return self._instance.pixelHeight + pixel_height: int = property(fget=_get_pixel_height) + + def _get_pixel_width(self) -> int: + """Width of the Py5Image object in pixels. + + Underlying Java field: PImage.pixelWidth + + Notes + ----- + + Width of the Py5Image object in pixels. This will be the same as + ``Py5Image.width``, even if the Sketch used ``pixel_density()`` to set the pixel + density to a value greater than 1. + """ + return self._instance.pixelWidth + pixel_width: int = property(fget=_get_pixel_width) + def _get_pixels(self) -> NDArray[(Any,), Int]: """The pixels[] array contains the values for all the pixels in the image. diff --git a/py5/jars/core.jar b/py5/jars/core.jar index db439bb..43a5796 100644 Binary files a/py5/jars/core.jar and b/py5/jars/core.jar differ diff --git a/py5/jars/dxf/dxf.jar b/py5/jars/dxf/dxf.jar index 93aae2d..7e56515 100644 Binary files a/py5/jars/dxf/dxf.jar and b/py5/jars/dxf/dxf.jar differ diff --git a/py5/jars/gluegen-rt-natives-linux-aarch64.jar b/py5/jars/gluegen-rt-natives-linux-aarch64.jar index 502a4c1..82671c4 100644 Binary files a/py5/jars/gluegen-rt-natives-linux-aarch64.jar and b/py5/jars/gluegen-rt-natives-linux-aarch64.jar differ diff --git a/py5/jars/gluegen-rt-natives-linux-amd64.jar b/py5/jars/gluegen-rt-natives-linux-amd64.jar index 826d6d6..4a44e28 100644 Binary files a/py5/jars/gluegen-rt-natives-linux-amd64.jar and b/py5/jars/gluegen-rt-natives-linux-amd64.jar differ diff --git a/py5/jars/gluegen-rt-natives-linux-armv6hf.jar b/py5/jars/gluegen-rt-natives-linux-armv6hf.jar index 5255e7f..debf9f8 100644 Binary files a/py5/jars/gluegen-rt-natives-linux-armv6hf.jar and b/py5/jars/gluegen-rt-natives-linux-armv6hf.jar differ diff --git a/py5/jars/gluegen-rt-natives-linux-i586.jar b/py5/jars/gluegen-rt-natives-linux-i586.jar index ac9182c..0e71f13 100644 Binary files a/py5/jars/gluegen-rt-natives-linux-i586.jar and b/py5/jars/gluegen-rt-natives-linux-i586.jar differ diff --git a/py5/jars/gluegen-rt-natives-macosx-universal.jar b/py5/jars/gluegen-rt-natives-macosx-universal.jar index 79bd94e..a47411e 100644 Binary files a/py5/jars/gluegen-rt-natives-macosx-universal.jar and b/py5/jars/gluegen-rt-natives-macosx-universal.jar differ diff --git a/py5/jars/gluegen-rt-natives-windows-amd64.jar b/py5/jars/gluegen-rt-natives-windows-amd64.jar index 2d1fe14..ce89c6f 100644 Binary files a/py5/jars/gluegen-rt-natives-windows-amd64.jar and b/py5/jars/gluegen-rt-natives-windows-amd64.jar differ diff --git a/py5/jars/gluegen-rt-natives-windows-i586.jar b/py5/jars/gluegen-rt-natives-windows-i586.jar index a0c6931..287412b 100644 Binary files a/py5/jars/gluegen-rt-natives-windows-i586.jar and b/py5/jars/gluegen-rt-natives-windows-i586.jar differ diff --git a/py5/jars/gluegen-rt.jar b/py5/jars/gluegen-rt.jar index aef08ef..d92fbcc 100644 Binary files a/py5/jars/gluegen-rt.jar and b/py5/jars/gluegen-rt.jar differ diff --git a/py5/jars/javafx-swt.jar b/py5/jars/javafx-swt.jar deleted file mode 100644 index e40db0b..0000000 Binary files a/py5/jars/javafx-swt.jar and /dev/null differ diff --git a/py5/jars/javafx.base.jar b/py5/jars/javafx.base.jar deleted file mode 100644 index c2e6d5c..0000000 Binary files a/py5/jars/javafx.base.jar and /dev/null differ diff --git a/py5/jars/javafx.controls.jar b/py5/jars/javafx.controls.jar deleted file mode 100644 index 90487f5..0000000 Binary files a/py5/jars/javafx.controls.jar and /dev/null differ diff --git a/py5/jars/javafx.fxml.jar b/py5/jars/javafx.fxml.jar deleted file mode 100644 index 5785c04..0000000 Binary files a/py5/jars/javafx.fxml.jar and /dev/null differ diff --git a/py5/jars/javafx.graphics.jar b/py5/jars/javafx.graphics.jar deleted file mode 100644 index 6ea1620..0000000 Binary files a/py5/jars/javafx.graphics.jar and /dev/null differ diff --git a/py5/jars/javafx.media.jar b/py5/jars/javafx.media.jar deleted file mode 100644 index 1a375da..0000000 Binary files a/py5/jars/javafx.media.jar and /dev/null differ diff --git a/py5/jars/javafx.swing.jar b/py5/jars/javafx.swing.jar deleted file mode 100644 index a45fdc7..0000000 Binary files a/py5/jars/javafx.swing.jar and /dev/null differ diff --git a/py5/jars/javafx.web.jar b/py5/jars/javafx.web.jar deleted file mode 100644 index cbe55be..0000000 Binary files a/py5/jars/javafx.web.jar and /dev/null differ diff --git a/py5/jars/jogl-all-natives-linux-aarch64.jar b/py5/jars/jogl-all-natives-linux-aarch64.jar index 22fd2c0..a0deef3 100644 Binary files a/py5/jars/jogl-all-natives-linux-aarch64.jar and b/py5/jars/jogl-all-natives-linux-aarch64.jar differ diff --git a/py5/jars/jogl-all-natives-linux-amd64.jar b/py5/jars/jogl-all-natives-linux-amd64.jar index 55953e2..388bcb0 100644 Binary files a/py5/jars/jogl-all-natives-linux-amd64.jar and b/py5/jars/jogl-all-natives-linux-amd64.jar differ diff --git a/py5/jars/jogl-all-natives-linux-armv6hf.jar b/py5/jars/jogl-all-natives-linux-armv6hf.jar index 56fecdb..db9c07c 100644 Binary files a/py5/jars/jogl-all-natives-linux-armv6hf.jar and b/py5/jars/jogl-all-natives-linux-armv6hf.jar differ diff --git a/py5/jars/jogl-all-natives-linux-i586.jar b/py5/jars/jogl-all-natives-linux-i586.jar index 979fa36..cde6472 100644 Binary files a/py5/jars/jogl-all-natives-linux-i586.jar and b/py5/jars/jogl-all-natives-linux-i586.jar differ diff --git a/py5/jars/jogl-all-natives-macosx-universal.jar b/py5/jars/jogl-all-natives-macosx-universal.jar index 124ef6d..a95f524 100644 Binary files a/py5/jars/jogl-all-natives-macosx-universal.jar and b/py5/jars/jogl-all-natives-macosx-universal.jar differ diff --git a/py5/jars/jogl-all-natives-windows-amd64.jar b/py5/jars/jogl-all-natives-windows-amd64.jar index 4384250..82c8f3e 100644 Binary files a/py5/jars/jogl-all-natives-windows-amd64.jar and b/py5/jars/jogl-all-natives-windows-amd64.jar differ diff --git a/py5/jars/jogl-all-natives-windows-i586.jar b/py5/jars/jogl-all-natives-windows-i586.jar index 63b5ade..e500fe9 100644 Binary files a/py5/jars/jogl-all-natives-windows-i586.jar and b/py5/jars/jogl-all-natives-windows-i586.jar differ diff --git a/py5/jars/jogl-all.jar b/py5/jars/jogl-all.jar index dbbde55..e11abbf 100644 Binary files a/py5/jars/jogl-all.jar and b/py5/jars/jogl-all.jar differ diff --git a/py5/jars/pdf/pdf.jar b/py5/jars/pdf/pdf.jar index 806739b..5543e7f 100644 Binary files a/py5/jars/pdf/pdf.jar and b/py5/jars/pdf/pdf.jar differ diff --git a/py5/jars/py5.jar b/py5/jars/py5.jar index f431643..1556ed5 100644 Binary files a/py5/jars/py5.jar and b/py5/jars/py5.jar differ diff --git a/py5/jars/svg/svg.jar b/py5/jars/svg/svg.jar index 2ae591d..def4d32 100644 Binary files a/py5/jars/svg/svg.jar and b/py5/jars/svg/svg.jar differ diff --git a/py5/java_conversion.py b/py5/java_conversion.py index 53f7e9e..3c6cc68 100644 --- a/py5/java_conversion.py +++ b/py5/java_conversion.py @@ -28,10 +28,12 @@ def init_jpype_converters(): data = [ ("processing.core.PImage", Py5Image), ("processing.core.PImage", Py5Graphics), + ("processing.core.PGraphics", Py5Graphics), ("processing.core.PFont", Py5Font), ("processing.core.PShape", Py5Shape), ("processing.opengl.PShader", Py5Shader), ("processing.core.PApplet", Sketch), + ("py5.core.Sketch", Sketch), ] def convert(jcls, obj): diff --git a/py5/methods.py b/py5/methods.py index b351812..af94a6b 100644 --- a/py5/methods.py +++ b/py5/methods.py @@ -18,28 +18,31 @@ # # ***************************************************************************** import sys +import re from pathlib import Path from collections import defaultdict from typing import Union import line_profiler -from jpype import JImplements, JOverride, JString, JClass +from jpype import JImplements, JOverride, JString import stackprinter +import py5_tools from . import custom_exceptions - -_JavaNullPointerException = JClass('java.lang.NullPointerException') - # *** stacktrace configuration *** # set stackprinter color style. Default is plaintext. Other choices are darkbg, # darkbg2, darkbg3, lightbg, lightbg2, lightbg3. _stackprinter_style = 'plaintext' # prune tracebacks to only show only show stack levels in the user's py5 code. _prune_tracebacks = True -_module_install_dir = str(Path(__file__).parent) +_MODULE_INSTALL_DIR = str(Path(__file__).parent) +_PY5TOOLS_MODULE_INSTALL_DIR = str(Path(py5_tools.__file__).parent) + +_PY5_STATIC_CODE_FILENAME_REGEX = re.compile( + r'File "[^\"]*?_PY5_STATIC_(SETUP|SETTINGS|FRAMEWORK)_CODE_\.py", line \d+, in .*') _EXCEPTION_MSGS = { **custom_exceptions.CUSTOM_EXCEPTION_MSGS, @@ -78,11 +81,16 @@ def handle_exception(println, exc_type, exc_value, exc_tb): tb = exc_tb.tb_next while hasattr(tb, 'tb_next') and hasattr(tb, 'tb_frame'): f_code = tb.tb_frame.f_code - if f_code.co_filename.startswith(_module_install_dir): - py5info.append((Path(f_code.co_filename[(len(_module_install_dir) + 1):]).parts, + if f_code.co_filename.startswith(_MODULE_INSTALL_DIR): + py5info.append((Path(f_code.co_filename[(len(_MODULE_INSTALL_DIR) + 1):]).parts, f_code.co_name)) if trim_tb is None: trim_tb = prev_tb + elif f_code.co_filename.startswith(_PY5TOOLS_MODULE_INSTALL_DIR): + py5info.append((Path(f_code.co_filename[( + len(_PY5TOOLS_MODULE_INSTALL_DIR) + 1):]).parts, f_code.co_name)) + if trim_tb is None: + trim_tb = prev_tb prev_tb = tb tb = tb.tb_next if trim_tb: @@ -97,7 +105,8 @@ def handle_exception(println, exc_type, exc_value, exc_tb): show_vals='line', style=_stackprinter_style, suppressed_paths=[r"lib/python.*?/site-packages/numpy/", - r"lib/python.*?/site-packages/py5/"]) + r"lib/python.*?/site-packages/py5/", + r"lib/python.*?/site-packages/py5tools/"]) if _prune_tracebacks: errmsg = errmsg.replace( @@ -108,6 +117,11 @@ def handle_exception(println, exc_type, exc_value, exc_tb): str(exc_value), py5info)) + m = _PY5_STATIC_CODE_FILENAME_REGEX.search(errmsg) + if m: + errmsg = "py5 encountered an error in your code:" + \ + errmsg[m.span()[1]:] + println(errmsg, stderr=True) sys.last_type, sys.last_value, sys.last_traceback = exc_type, exc_value, exc_tb diff --git a/py5/mixins/__init__.py b/py5/mixins/__init__.py index 7640a01..65b6cab 100644 --- a/py5/mixins/__init__.py +++ b/py5/mixins/__init__.py @@ -20,5 +20,5 @@ from .math import MathMixin # noqa from .data import DataMixin # noqa from .threads import ThreadsMixin # noqa -from .pixels import PixelMixin # noqa +from .pixels import PixelMixin, PixelPy5GraphicsMixin, PixelPy5ImageMixin # noqa from .print_tools import PrintlnStream, _WidgetPrintlnStream, _DefaultPrintlnStream, _DisplayPubPrintlnStream # noqa diff --git a/py5/mixins/pixels.py b/py5/mixins/pixels.py index daa12a1..6a3dd68 100644 --- a/py5/mixins/pixels.py +++ b/py5/mixins/pixels.py @@ -193,9 +193,9 @@ def save(self, *, format: str = None, drop_alpha: bool = True, - use_thread: bool = True, + use_thread: bool = False, **params) -> None: - """Save image data to a file. + """Save the drawing surface to an image file. Parameters ---------- @@ -212,14 +212,15 @@ def save(self, params keyword arguments to pass to the PIL.Image save method - use_thread: bool = True + use_thread: bool = False write file in separate thread Notes ----- - Save image data to a file. This method uses the Python library Pillow to write - the image, so it can save images in any format that that library supports. + Save the drawing surface to an image file. This method uses the Python library + Pillow to write the image, so it can save images in any format that that library + supports. Use the ``drop_alpha`` parameter to drop the alpha channel from the image. This defaults to ``True``. Some image formats such as JPG do not support alpha @@ -251,3 +252,367 @@ def _save(arr, filename, format, params): t.start() else: Image.fromarray(arr).save(filename, format=format, **params) + + # *** END METHODS *** + + +class PixelPy5GraphicsMixin(PixelMixin): + + def load_np_pixels(self) -> None: + """Loads the pixel data of the current Py5Graphics drawing surface into the + ``Py5Graphics.np_pixels[]`` array. + + Notes + ----- + + Loads the pixel data of the current Py5Graphics drawing surface into the + ``Py5Graphics.np_pixels[]`` array. This method must always be called before + reading from or writing to ``Py5Graphics.np_pixels[]``. It should only be used + between calls to ``Py5Graphics.begin_draw()`` and ``Py5Graphics.end_draw()``. + Subsequent changes to the Py5Graphics drawing surface will not be reflected in + ``Py5Graphics.np_pixels[]`` until ``load_np_pixels()`` is called again. + + The ``load_np_pixels()`` method is similar to ``Py5Graphics.load_pixels()`` in + that ``load_np_pixels()`` must be called before reading from or writing to + ``Py5Graphics.np_pixels[]`` just as ``Py5Graphics.load_pixels()`` must be called + before reading from or writing to ``Py5Graphics.pixels[]``. + + Note that ``load_np_pixels()`` will as a side effect call + ``Py5Graphics.load_pixels()``, so if your code needs to read + ``Py5Graphics.np_pixels[]`` and ``Py5Graphics.pixels[]`` simultaneously, there + is no need for a separate call to ``Py5Graphics.load_pixels()``. However, be + aware that modifying both ``Py5Graphics.np_pixels[]`` and + ``Py5Graphics.pixels[]`` simultaneously will likely result in the updates to + ``Py5Graphics.pixels[]`` being discarded. + + This method is the same as ``load_np_pixels()`` but linked to a ``Py5Graphics`` + object.""" + return super().load_np_pixels() + + def update_np_pixels(self) -> None: + """Updates the Py5Graphics drawing surface with the data in the + ``Py5Graphics.np_pixels[]`` array. + + Notes + ----- + + Updates the Py5Graphics drawing surface with the data in the + ``Py5Graphics.np_pixels[]`` array. Use in conjunction with + ``Py5Graphics.load_np_pixels()``. If you're only reading pixels from the array, + there's no need to call ``update_np_pixels()`` — updating is only necessary to + apply changes. Working with ``Py5Graphics.np_pixels[]`` can only be done between + calls to ``Py5Graphics.begin_draw()`` and ``Py5Graphics.end_draw()``. + + The ``update_np_pixels()`` method is similar to ``Py5Graphics.update_pixels()`` + in that ``update_np_pixels()`` must be called after modifying + ``Py5Graphics.np_pixels[]`` just as ``Py5Graphics.update_pixels()`` must be + called after modifying ``Py5Graphics.pixels[]``. + + This method is the same as ``update_np_pixels()`` but linked to a + ``Py5Graphics`` object.""" + return super().update_np_pixels() + + def _get_np_pixels(self) -> np.ndarray: + """The ``np_pixels[]`` array contains the values for all the pixels in the + Py5Graphics drawing surface. + + Notes + ----- + + The ``np_pixels[]`` array contains the values for all the pixels in the + Py5Graphics drawing surface. Unlike the one dimensional array + ``Py5Graphics.pixels[]``, the ``np_pixels[]`` array organizes the color data in + a 3 dimensional numpy array. The size of the array's dimensions are defined by + the size of the Py5Graphics drawing surface. The first dimension is the height, + the second is the width, and the third represents the color channels. The color + channels are ordered alpha, red, green, blue (ARGB). Every value in + ``np_pixels[]`` is an integer between 0 and 255. + + This numpy array is very similar to the image arrays used by other popular + Python image libraries, but note that some of them like opencv will by default + order the color channels as RGBA. + + When the pixel density is set to higher than 1 with the + ``Py5Graphics.pixel_density`` function, the size of ``np_pixels[]``'s height and + width dimensions will change. See the reference for ``Py5Graphics.pixel_width`` + or ``Py5Graphics.pixel_height`` for more information. Nothing about + ``np_pixels[]`` will change as a result of calls to + ``Py5Graphics.color_mode()``. + + Much like the ``Py5Graphics.pixels[]`` array, there are load and update methods + that must be called before and after making changes to the data in + ``np_pixels[]``. Before accessing ``np_pixels[]``, the data must be loaded with + the ``Py5Graphics.load_np_pixels()`` method. If this is not done, ``np_pixels`` + will be equal to ``None`` and your code will likely result in Python exceptions. + After ``np_pixels[]`` has been modified, the ``Py5Graphics.update_np_pixels()`` + method must be called to update the content of the Py5Graphics drawing surface. + + Working with ``Py5Graphics.np_pixels[]`` can only be done between calls to + ``Py5Graphics.begin_draw()`` and ``Py5Graphics.end_draw()``. + + To set the entire contents of ``np_pixels[]`` to the contents of another + properly sized numpy array, consider using ``Py5Graphics.set_np_pixels()``. + + This field is the same as ``np_pixels[]`` but linked to a ``Py5Graphics`` + object.""" + return super()._get_np_pixels() + np_pixels: np.ndarray = property(fget=_get_np_pixels) + + def set_np_pixels(self, array: np.ndarray, bands: str = 'ARGB') -> None: + """Set the entire contents of ``Py5Graphics.np_pixels[]`` to the contents of + another properly sized and typed numpy array. + + Parameters + ---------- + + array: np.ndarray + properly sized numpy array to be copied to np_pixels[] + + bands: str = 'ARGB' + color channels in the array's third dimension + + Notes + ----- + + Set the entire contents of ``Py5Graphics.np_pixels[]`` to the contents of + another properly sized and typed numpy array. The size of ``array``'s first and + second dimensions must match the height and width of the Py5Graphics drawing + surface, respectively. The array's ``dtype`` must be ``np.uint8``. This must be + used after ``Py5Graphics.begin_draw()`` but can be used after + ``Py5Graphics.end_draw()``. + + The ``bands`` parameter is used to interpret the ``array``'s color channel + dimension (the array's third dimension). It can be one of ``'L'`` (single- + channel grayscale), ``'ARGB'``, ``'RGB'``, or ``'RGBA'``. If there is no alpha + channel, ``array`` is assumed to have no transparency. Unlike the main drawing + window, a Py5Graphics drawing surface's pixels can be transparent so using the + alpha channel will work properly. If the ``bands`` parameter is ``'L'``, + ``array``'s third dimension is optional. + + This method makes its own calls to ``Py5Graphics.load_np_pixels()`` and + ``Py5Graphics.update_np_pixels()`` so there is no need to call either + explicitly. + + This method exists because setting the array contents with the code + ``g.np_pixels = array`` will cause an error, while the correct syntax, + ``g.np_pixels[:] = array``, might also be unintuitive for beginners. + + This method is the same as ``set_np_pixels()`` but linked to a ``Py5Graphics`` + object.""" + return super().set_np_pixels(array, bands) + + def save(self, + filename: Union[str, + Path], + *, + format: str = None, + drop_alpha: bool = True, + use_thread: bool = False, + **params) -> None: + """Save the Py5Graphics drawing surface to an image file. + + Parameters + ---------- + + drop_alpha: bool = True + remove the alpha channel when saving the image + + filename: Union[str, Path] + output filename + + format: str = None + image format, if not determined from filename extension + + params + keyword arguments to pass to the PIL.Image save method + + use_thread: bool = False + write file in separate thread + + Notes + ----- + + Save the Py5Graphics drawing surface to an image file. This method uses the + Python library Pillow to write the image, so it can save images in any format + that that library supports. + + Use the ``drop_alpha`` parameter to drop the alpha channel from the image. This + defaults to ``True``. Some image formats such as JPG do not support alpha + channels, and Pillow will throw an error if you try to save an image with the + alpha channel in that format. + + The ``use_thread`` parameter will save the image in a separate Python thread. + This improves performance by returning before the image has actually been + written to the file. + + This method is the same as ``save()`` but linked to a ``Py5Graphics`` object. To + see example code for how it can be used, see ``save()``.""" + return super().save( + filename, + format=format, + drop_alpha=drop_alpha, + use_thread=use_thread, + **params) + + +class PixelPy5ImageMixin(PixelMixin): + + def load_np_pixels(self) -> None: + """Loads the pixel data of the image into the ``Py5Image.np_pixels[]`` array. + + Notes + ----- + + Loads the pixel data of the image into the ``Py5Image.np_pixels[]`` array. This + method must always be called before reading from or writing to + ``Py5Image.np_pixels[]``. Subsequent changes to the image will not be reflected + in ``Py5Image.np_pixels[]`` until ``py5image_load_np_pixels()`` is called again. + + The ``load_np_pixels()`` method is similar to ``Py5Image.load_pixels()`` in that + ``load_np_pixels()`` must be called before reading from or writing to + ``Py5Image.np_pixels[]`` just as ``Py5Image.load_pixels()`` must be called + before reading from or writing to ``Py5Image.pixels[]``. + + Note that ``load_np_pixels()`` will as a side effect call + ``Py5Image.load_pixels()``, so if your code needs to read + ``Py5Image.np_pixels[]`` and ``Py5Image.pixels[]`` simultaneously, there is no + need for a separate call to ``Py5Image.load_pixels()``. However, be aware that + modifying both ``Py5Image.np_pixels[]`` and ``Py5Image.pixels[]`` simultaneously + will likely result in the updates to ``Py5Image.pixels[]`` being discarded.""" + return super().load_np_pixels() + + def update_np_pixels(self) -> None: + """Updates the image with the data in the ``Py5Image.np_pixels[]`` array. + + Notes + ----- + + Updates the image with the data in the ``Py5Image.np_pixels[]`` array. Use in + conjunction with ``Py5Image.load_np_pixels()``. If you're only reading pixels + from the array, there's no need to call ``update_np_pixels()`` — updating is + only necessary to apply changes. + + The ``update_np_pixels()`` method is similar to ``Py5Image.update_pixels()`` in + that ``update_np_pixels()`` must be called after modifying + ``Py5Image.np_pixels[]`` just as ``Py5Image.update_pixels()`` must be called + after modifying ``Py5Image.pixels[]``.""" + return super().update_np_pixels() + + def _get_np_pixels(self) -> np.ndarray: + """The ``np_pixels[]`` array contains the values for all the pixels in the image. + + Notes + ----- + + The ``np_pixels[]`` array contains the values for all the pixels in the image. + Unlike the one dimensional array ``Py5Image.pixels[]``, the ``np_pixels[]`` + array organizes the color data in a 3 dimensional numpy array. The size of the + array's dimensions are defined by the size of the image. The first dimension is + the height, the second is the width, and the third represents the color + channels. The color channels are ordered alpha, red, green, blue (ARGB). Every + value in ``np_pixels[]`` is an integer between 0 and 255. + + This numpy array is very similar to the image arrays used by other popular + Python image libraries, but note that some of them like opencv will by default + order the color channels as RGBA. + + Much like the ``Py5Image.pixels[]`` array, there are load and update methods + that must be called before and after making changes to the data in + ``np_pixels[]``. Before accessing ``np_pixels[]``, the data must be loaded with + the ``Py5Image.load_np_pixels()`` method. If this is not done, ``np_pixels`` + will be equal to ``None`` and your code will likely result in Python exceptions. + After ``np_pixels[]`` has been modified, the ``Py5Image.update_np_pixels()`` + method must be called to update the content of the display window. + + To set the entire contents of ``np_pixels[]`` to the contents of another equally + sized numpy array, consider using ``Py5Image.set_np_pixels()``.""" + return super()._get_np_pixels() + np_pixels: np.ndarray = property(fget=_get_np_pixels) + + def set_np_pixels(self, array: np.ndarray, bands: str = 'ARGB') -> None: + """Set the entire contents of ``Py5Image.np_pixels[]`` to the contents of another + properly sized and typed numpy array. + + Parameters + ---------- + + array: np.ndarray + properly sized numpy array to be copied to np_pixels[] + + bands: str = 'ARGB' + color channels in the array's third dimension + + Notes + ----- + + Set the entire contents of ``Py5Image.np_pixels[]`` to the contents of another + properly sized and typed numpy array. The size of ``array``'s first and second + dimensions must match the height and width of the image, respectively. The + array's ``dtype`` must be ``np.uint8``. + + The ``bands`` parameter is used to interpret the ``array``'s color channel + dimension (the array's third dimension). It can be one of ``'L'`` (single- + channel grayscale), ``'ARGB'``, ``'RGB'``, or ``'RGBA'``. If there is no alpha + channel, ``array`` is assumed to have no transparency. If the ``bands`` + parameter is ``'L'``, ``array``'s third dimension is optional. + + This method makes its own calls to ``Py5Image.load_np_pixels()`` and + ``Py5Image.update_np_pixels()`` so there is no need to call either explicitly. + + This method exists because setting the array contents with the code + ``img.np_pixels = array`` will cause an error, while the correct syntax, + ``img.np_pixels[:] = array``, might also be unintuitive for beginners. + + Note that the ``convert_image()`` method can also be used to convert a numpy + array into a new Py5Image object.""" + return super().set_np_pixels(array, bands) + + def save(self, + filename: Union[str, + Path], + *, + format: str = None, + drop_alpha: bool = True, + use_thread: bool = False, + **params) -> None: + """Save the Py5Image object to an image file. + + Parameters + ---------- + + drop_alpha: bool = True + remove the alpha channel when saving the image + + filename: Union[str, Path] + output filename + + format: str = None + image format, if not determined from filename extension + + params + keyword arguments to pass to the PIL.Image save method + + use_thread: bool = False + write file in separate thread + + Notes + ----- + + Save the Py5Image object to an image file. This method uses the Python library + Pillow to write the image, so it can save images in any format that that library + supports. + + Use the ``drop_alpha`` parameter to drop the alpha channel from the image. This + defaults to ``True``. Some image formats such as JPG do not support alpha + channels, and Pillow will throw an error if you try to save an image with the + alpha channel in that format. + + The ``use_thread`` parameter will save the image in a separate Python thread. + This improves performance by returning before the image has actually been + written to the file.""" + return super().save( + filename, + format=format, + drop_alpha=drop_alpha, + use_thread=use_thread, + **params) diff --git a/py5/mixins/threads.py b/py5/mixins/threads.py index 38747da..ce8d2b7 100644 --- a/py5/mixins/threads.py +++ b/py5/mixins/threads.py @@ -60,7 +60,7 @@ def __call__(self): try: self.f(*self.args, **self.kwargs) except Exception: - methods.handle_exception(*sys.exc_info()) + methods.handle_exception(self.sketch.println, *sys.exc_info()) self.sketch._terminate_sketch() @@ -77,7 +77,7 @@ def __call__(self): try: self.promise._set_result(self.f(*self.args, **self.kwargs)) except Exception: - methods.handle_exception(*sys.exc_info()) + methods.handle_exception(self.sketch.println, *sys.exc_info()) self.sketch._terminate_sketch() @@ -102,7 +102,7 @@ def __call__(self): self.e.wait(max(0, start_time + self.delay - time.time())) except Exception: self.stop() - methods.handle_exception(*sys.exc_info()) + methods.handle_exception(self.sketch.println, *sys.exc_info()) self.sketch._terminate_sketch() diff --git a/py5/reference.py b/py5/reference.py index 8795607..bc35433 100644 --- a/py5/reference.py +++ b/py5/reference.py @@ -209,10 +209,6 @@ (('Sketch', 'vertex'), ['(x: float, y: float, /) -> None', '(x: float, y: float, z: float, /) -> None', '(x: float, y: float, u: float, v: float, /) -> None', '(x: float, y: float, z: float, u: float, v: float, /) -> None', '(v: NDArray[(Any,), Float], /) -> None']), (('Sketch', 'vertices'), ['(coordinates: NDArray[(Any, Any), Float], /) -> None']), (('Sketch', 'year'), ['() -> int']), - (('Sketch', 'load_np_pixels'), ['() -> None']), - (('Sketch', 'update_np_pixels'), ['() -> None']), - (('Sketch', 'set_np_pixels'), ["(array: np.ndarray, bands: str = 'ARGB') -> None"]), - (('Sketch', 'save'), ['(filename: Union[str, Path], *, format: str = None, drop_alpha: bool = True, use_thread: bool = True, **params) -> None']), (('Sketch', 'sin'), ['(angle: float) -> float']), (('Sketch', 'cos'), ['(angle: float) -> float']), (('Sketch', 'tan'), ['(angle: float) -> float']), @@ -246,6 +242,12 @@ (('Sketch', 'load_json'), ['(json_path: Union[str, Path], **kwargs: Dict[str, Any]) -> Any']), (('Sketch', 'save_json'), ['(json_data: Any, filename: Union[str, Path], **kwargs: Dict[str, Any]) -> None']), (('Sketch', 'parse_json'), ['(serialized_json: Any, **kwargs: Dict[str, Any]) -> Any']), + (('Sketch', 'set_println_stream'), ['(println_stream: Any) -> None']), + (('Sketch', 'println'), ["(*args, sep: str = ' ', end: str = '\\n', stderr: bool = False) -> None"]), + (('Sketch', 'load_np_pixels'), ['() -> None']), + (('Sketch', 'update_np_pixels'), ['() -> None']), + (('Sketch', 'set_np_pixels'), ["(array: np.ndarray, bands: str = 'ARGB') -> None"]), + (('Sketch', 'save'), ['(filename: Union[str, Path], *, format: str = None, drop_alpha: bool = True, use_thread: bool = False, **params) -> None']), (('Sketch', 'launch_thread'), ['(f: Callable, name: str = None, *, daemon: bool = True, args: Tuple = None, kwargs: Dict = None) -> str']), (('Sketch', 'launch_promise_thread'), ['(f: Callable, name: str = None, *, daemon: bool = True, args: Tuple = None, kwargs: Dict = None) -> Py5Promise']), (('Sketch', 'launch_repeating_thread'), ['(f: Callable, name: str = None, *, time_delay: float = 0, daemon: bool = True, args: Tuple = None, kwargs: Dict = None) -> str']), @@ -253,14 +255,12 @@ (('Sketch', 'stop_thread'), ['(name: str, wait: bool = False) -> None']), (('Sketch', 'stop_all_threads'), ['(wait: bool = False) -> None']), (('Sketch', 'list_threads'), ['() -> None']), - (('Sketch', 'set_println_stream'), ['(println_stream: Any) -> None']), - (('Sketch', 'println'), ["(*args, sep: str = ' ', end: str = '\\n', stderr: bool = False) -> None"]), (('Sketch', 'sketch_path'), ['() -> Path', '(where: str, /) -> Path']), (('Sketch', 'hot_reload_draw'), ['(draw: Callable) -> None']), (('Sketch', 'profile_functions'), ['(function_names: List[str]) -> None']), (('Sketch', 'profile_draw'), ['() -> None']), (('Sketch', 'print_line_profiler_stats'), ['() -> None']), - (('Sketch', 'save_frame'), ['(filename: Union[str, Path], *, format: str = None, drop_alpha: bool = True, use_thread: bool = True, **params) -> None']), + (('Sketch', 'save_frame'), ['(filename: Union[str, Path], *, format: str = None, drop_alpha: bool = True, use_thread: bool = False, **params) -> None']), (('Sketch', 'create_image_from_numpy'), ["(array: np.array, bands: str = 'ARGB', *, dst: Py5Image = None) -> Py5Image"]), (('Sketch', 'convert_image'), ['(obj: Any, *, dst: Py5Image = None) -> Py5Image']), (('Sketch', 'load_image'), ['(image_path: Union[str, Path], *, dst: Py5Image = None) -> Py5Image']), diff --git a/py5/render_helper.py b/py5/render_helper.py index 25568c2..bf026f1 100644 --- a/py5/render_helper.py +++ b/py5/render_helper.py @@ -1,3 +1,4 @@ +import sys import functools from typing import Callable, Tuple, Dict, List, NewType @@ -28,13 +29,6 @@ def __init__( draw_args=None, draw_kwargs=None): super().__init__() - if renderer not in [ - Sketch.HIDDEN, - Sketch.JAVA2D, - Sketch.P2D, - Sketch.P3D]: - raise RuntimeError( - f'Processing Renderer {renderer} not yet supported') self._setup = setup self._draw = draw self._width = width @@ -77,13 +71,6 @@ def __init__( draw_args=None, draw_kwargs=None): super().__init__() - if renderer not in [ - Sketch.HIDDEN, - Sketch.JAVA2D, - Sketch.P2D, - Sketch.P3D]: - raise RuntimeError( - f'Processing Renderer {renderer} not yet supported') self._setup = setup self._draw = draw self._width = width @@ -123,6 +110,37 @@ def draw(self): self.exit_sketch() +def _check_allowed_renderer(renderer): + renderer_name = { + Sketch.SVG: 'SVG', + Sketch.PDF: 'PDF', + Sketch.DXF: 'DXF', + Sketch.P2D: 'P2D', + Sketch.P3D: 'P3D'}.get( + renderer, + renderer) + renderers = [ + Sketch.HIDDEN, + Sketch.JAVA2D] if sys.platform == 'darwin' else [ + Sketch.HIDDEN, + Sketch.JAVA2D, + Sketch.P2D, + Sketch.P3D] + if renderer not in renderers: + return f'Sorry, the render helper tools do not support the {renderer_name} renderer' + ( + ' on OSX.' if sys.platform == 'darwin' else '.') + else: + return None + + +def _osx_renderer_check(renderer): + if sys.platform == 'darwin' and renderer == Sketch.JAVA2D: + print('The render helper tools do not support the JAVA2D renderer on OSX. Switching to the default option instead.') + return Sketch.HIDDEN + else: + return renderer + + def render_frame(draw: Callable, width: int, height: int, renderer: str = Sketch.HIDDEN, *, draw_args: Tuple = None, draw_kwargs: Dict = None, @@ -167,7 +185,8 @@ def render_frame(draw: Callable, width: int, height: int, desired values as ``render_frame``'s ``draw_args`` and ``draw_kwargs`` arguments. - Currently, only the default and OpenGL renderers are supported. + On OSX, only the default renderer is currently supported. Other platforms + support the default renderer and the OpenGL renderers (P2D and P3D). The rendered frame can have transparent pixels if and only if the ``use_py5graphics`` parameter is ``True`` because only a ``py5.Py5Graphics`` @@ -180,6 +199,11 @@ def render_frame(draw: Callable, width: int, height: int, discouraged, and may fail catastrophically. This function is available in decorator form as ``@render()``.""" + if msg := _check_allowed_renderer(renderer): + print(msg, file=sys.stderr) + return None + renderer = _osx_renderer_check(renderer) + HelperClass = RenderHelperGraphicsCanvas if use_py5graphics else RenderHelperSketch ahs = HelperClass(None, draw, width, height, renderer, draw_args=draw_args, draw_kwargs=draw_kwargs) @@ -248,7 +272,8 @@ def render_frame_sequence(draw: Callable, width: int, height: int, desired values to ``render_frame_sequence``'s ``draw_args`` and ``draw_kwargs`` arguments. - Currently, only the default and OpenGL renderers are supported. + On OSX, only the default renderer is currently supported. Other platforms + support the default renderer and the OpenGL renderers (P2D and P3D). The rendered frames can have transparent pixels if and only if the ``use_py5graphics`` parameter is ``True`` because only a ``py5.Py5Graphics`` @@ -267,6 +292,11 @@ def render_frame_sequence(draw: Callable, width: int, height: int, discouraged, and may fail catastrophically. This function is available in decorator form as ``@render_sequence()``.""" + if msg := _check_allowed_renderer(renderer): + print(msg, file=sys.stderr) + return None + renderer = _osx_renderer_check(renderer) + HelperClass = RenderHelperGraphicsCanvas if use_py5graphics else RenderHelperSketch ahs = HelperClass(setup, draw, width, height, renderer, limit=limit, setup_args=setup_args, setup_kwargs=setup_kwargs, @@ -310,7 +340,8 @@ def render(width: int, height: int, renderer: str = Sketch.HIDDEN, *, use them, pass the desired values when you call the decorated function as you would to any other Python function. - Currently, only the default and OpenGL renderers are supported. + On OSX, only the default renderer is currently supported. Other platforms + support the default renderer and the OpenGL renderers (P2D and P3D). The rendered frame can have transparent pixels if and only if the ``use_py5graphics`` parameter is ``True`` because only a ``py5.Py5Graphics`` @@ -323,6 +354,10 @@ def render(width: int, height: int, renderer: str = Sketch.HIDDEN, *, discouraged, and may fail catastrophically. This function is available in non-decorator form as ``render_frame()``.""" + if msg := _check_allowed_renderer(renderer): + raise RuntimeError(msg) + renderer = _osx_renderer_check(renderer) + def decorator(draw): @functools.wraps(draw) def run_render_frame(*draw_args, **draw_kwargs): @@ -385,7 +420,8 @@ def render_sequence(width: int, height: int, renderer: str = Sketch.HIDDEN, *, once, just like it would for any other py5 Sketch. The type of the first parameter must also depend on the ``use_py5graphics`` parameter. - Currently, only the default and OpenGL renderers are supported. + On OSX, only the default renderer is currently supported. Other platforms + support the default renderer and the OpenGL renderers (P2D and P3D). The rendered frames can have transparent pixels if and only if the ``use_py5graphics`` parameter is ``True`` because only a ``py5.Py5Graphics`` @@ -399,6 +435,10 @@ def render_sequence(width: int, height: int, renderer: str = Sketch.HIDDEN, *, discouraged, and may fail catastrophically. This function is available in non-decorator form as ``render_frame_sequence()``.""" + if msg := _check_allowed_renderer(renderer): + raise RuntimeError(msg) + renderer = _osx_renderer_check(renderer) + def decorator(draw): @functools.wraps(draw) def run_render_frames(*draw_args, **draw_kwargs): diff --git a/py5/sketch.py b/py5/sketch.py index d7131ed..5734da1 100644 --- a/py5/sketch.py +++ b/py5/sketch.py @@ -19,6 +19,7 @@ # ***************************************************************************** import time import os +import sys from pathlib import Path import functools from typing import overload, Any, Callable, Union, Dict, List # noqa @@ -54,6 +55,9 @@ from ipykernel.zmqshell import ZMQInteractiveShell _ipython_shell = get_ipython() # type: ignore _in_jupyter_zmq_shell = isinstance(_ipython_shell, ZMQInteractiveShell) + if sys.platform == 'darwin' and _ipython_shell.active_eventloop != 'osx': + print("Importing py5 on OSX but the necessary Jupyter OSX event loop not been activated. I'll activate it for you, but next time, execute `%gui osx` before importing this library.") + _ipython_shell.run_line_magic('gui', 'osx') except NameError: _in_ipython_session = False _ipython_shell = None @@ -127,6 +131,8 @@ def __init__(self, *args, **kwargs): self._py5_methods = None self.set_println_stream(_DisplayPubPrintlnStream( ) if _in_jupyter_zmq_shell else _DefaultPrintlnStream()) + self._instance.setPy5IconPath( + str(Path(__file__).parent.parent / 'py5_tools/kernel/resources/logo-64x64.png')) # attempt to instantiate Py5Utilities self.utils = None @@ -166,10 +172,14 @@ def run_sketch(self, block: bool = None, *, determine if the Sketch is running in a Jupyter Notebook or an IPython shell. If it is, ``block`` will default to ``False``, and ``True`` otherwise. + Blocking is not supported on OSX. This is because of the (current) limitations + of py5 on OSX. If the ``block`` parameter is set to ``True``, a warning message + will appear and it will be changed to ``False``. + A list of strings passed to ``py5_options`` will be passed to the Processing PApplet class as arguments to specify characteristics such as the window's location on the screen. A list of strings passed to ``sketch_args`` will be - available to a running Sketch using ``args``. See the third example for an + available to a running Sketch using ``pargs``. See the third example for an example of how this can be used. When calling ``run_sketch()`` in module mode, py5 will by default search for @@ -181,6 +191,16 @@ def run_sketch(self, block: bool = None, *, in class mode. Don't forget you can always replace the ``draw()`` function in a running Sketch using ``hot_reload_draw()``. + When programming in module mode and imported mode, py5 will inspect the + ``setup()`` function and will attempt to split it into synthetic ``settings()`` + and ``setup()`` functions if both were not created by the user and the real + ``setup()`` function contains calls to ``size()``, ``full_screen()``, + ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to those functions + must be at the very beginning of ``setup()``, before any other Python code + (except for comments). This feature allows the user to omit the ``settings()`` + function, much like what can be done while programming in the Processing IDE. + This feature is not available when programming in class mode. + When running a Sketch asynchronously through Jupyter Notebook, any ``print`` statements using Python's builtin function will always appear in the output of the currently active cell. This will rarely be desirable, as the active cell @@ -193,9 +213,6 @@ def run_sketch(self, block: bool = None, *, error messages and warnings generated inside the Processing Jars cannot be controlled in the same way, and may appear in the output of the active cell or mixed in with the Jupyter Kernel logs.""" - if block is None: - block = not _in_ipython_session - if not hasattr(self, '_instance'): raise RuntimeError( ('py5 internal problem: did you create a class with an `__init__()` ' @@ -235,11 +252,21 @@ def _run_sketch(self, str(e), stderr=True) - if block: + if sys.platform == 'darwin' and _in_ipython_session and block: + if (renderer := self._instance.getRendererName()) in [ + 'JAVA2D', 'P2D', 'P3D', 'FX2D']: + self.println( + "On OSX, blocking is not allowed in Jupyter when using the", + renderer, + "renderer.", + stderr=True) + block = False + + if block or (block is None and not _in_ipython_session): # wait for the sketch to finish surface = self.get_surface() if surface._instance is not None: - while not surface.is_stopped(): + while not surface.is_stopped() and not hasattr(self, '_shutdown_initiated'): time.sleep(0.25) # Wait no more than 1 second for any shutdown tasks to complete. @@ -259,9 +286,8 @@ def _shutdown(self): super()._shutdown() def _terminate_sketch(self): - surface = self.get_surface() - if surface._instance is not None: - surface.stop_thread() + self._instance.noLoop() + self._shutdown_initiated = True self._shutdown() def _add_pre_hook(self, method_name, hook_name, function): @@ -418,7 +444,7 @@ def _get_is_running(self) -> bool: # Sketch has not been run yet return False else: - return not surface.is_stopped() + return not surface.is_stopped() and not hasattr(self, '_shutdown_initiated') is_running: bool = property(fget=_get_is_running) def _get_is_dead(self) -> bool: @@ -439,7 +465,7 @@ def _get_is_dead(self) -> bool: if surface._instance is None: # Sketch has not been run yet return False - return surface.is_stopped() + return surface.is_stopped() or hasattr(self, '_shutdown_initiated') is_dead: bool = property(fget=_get_is_dead) def _get_is_dead_from_error(self) -> bool: @@ -594,7 +620,7 @@ def save_frame(self, *, format: str = None, drop_alpha: bool = True, - use_thread: bool = True, + use_thread: bool = False, **params) -> None: """Save the current frame as an image. @@ -613,7 +639,7 @@ def save_frame(self, params keyword arguments to pass to the PIL.Image save method - use_thread: bool = True + use_thread: bool = False write file in separate thread Notes @@ -1020,7 +1046,7 @@ def request_image(self, image_path: Union[str, Path]) -> Py5Promise: Z = 2 @_return_list_str - def _get_args(self) -> List[str]: + def _get_pargs(self) -> List[str]: """List of strings passed to the Sketch through the call to ``run_sketch()``. Underlying Java field: PApplet.args @@ -1033,7 +1059,7 @@ def _get_args(self) -> List[str]: to make this more useful. """ return self._instance.args - args: List[str] = property(fget=_get_args) + pargs: List[str] = property(fget=_get_pargs) def _get_display_height(self) -> int: """System variable that stores the height of the entire screen display. @@ -1043,8 +1069,9 @@ def _get_display_height(self) -> int: Notes ----- - System variable that stores the height of the entire screen display. This is - used to run a full-screen program on any display size. + System variable that stores the height of the entire screen display. This can be + used to run a full-screen program on any display size, but calling + ``full_screen()`` is usually a better choice. """ return self._instance.displayHeight display_height: int = property(fget=_get_display_height) @@ -1057,8 +1084,9 @@ def _get_display_width(self) -> int: Notes ----- - System variable that stores the width of the entire screen display. This is used - to run a full-screen program on any display size. + System variable that stores the width of the entire screen display. This can be + used to run a full-screen program on any display size, but calling + ``full_screen()`` is usually a better choice. """ return self._instance.displayWidth display_width: int = property(fget=_get_display_width) @@ -1288,47 +1316,45 @@ def _get_mouse_y(self) -> int: mouse_y: int = property(fget=_get_mouse_y) def _get_pixel_height(self) -> int: - """When ``pixel_density(2)`` is used to make use of a high resolution display - (called a Retina display on OSX or high-dpi on Windows and Linux), the width and - height of the Sketch do not change, but the number of pixels is doubled. + """Height of the display window in pixels. Underlying Java field: PApplet.pixelHeight Notes ----- - When ``pixel_density(2)`` is used to make use of a high resolution display - (called a Retina display on OSX or high-dpi on Windows and Linux), the width and - height of the Sketch do not change, but the number of pixels is doubled. As a - result, all operations that use pixels (like ``load_pixels()``, ``get()``, etc.) - happen in this doubled space. As a convenience, the variables ``pixel_width`` - and ``pixel_height`` hold the actual width and height of the Sketch in pixels. - This is useful for any Sketch that use the ``pixels[]`` or ``np_pixels[]`` - arrays, for instance, because the number of elements in each array will be - ``pixel_width*pixel_height``, not ``width*height``. + Height of the display window in pixels. When ``pixel_density(2)`` is used to + make use of a high resolution display (called a Retina display on OSX or high- + dpi on Windows and Linux), the width and height of the Sketch do not change, but + the number of pixels is doubled. As a result, all operations that use pixels + (like ``load_pixels()``, ``get()``, etc.) happen in this doubled space. As a + convenience, the variables ``pixel_width`` and ``pixel_height`` hold the actual + width and height of the Sketch in pixels. This is useful for any Sketch that use + the ``pixels[]`` or ``np_pixels[]`` arrays, for instance, because the number of + elements in each array will be ``pixel_width*pixel_height``, not + ``width*height``. """ return self._instance.pixelHeight pixel_height: int = property(fget=_get_pixel_height) def _get_pixel_width(self) -> int: - """When ``pixel_density(2)`` is used to make use of a high resolution display - (called a Retina display on OSX or high-dpi on Windows and Linux), the width and - height of the Sketch do not change, but the number of pixels is doubled. + """Width of the display window in pixels. Underlying Java field: PApplet.pixelWidth Notes ----- - When ``pixel_density(2)`` is used to make use of a high resolution display - (called a Retina display on OSX or high-dpi on Windows and Linux), the width and - height of the Sketch do not change, but the number of pixels is doubled. As a - result, all operations that use pixels (like ``load_pixels()``, ``get()``, etc.) - happen in this doubled space. As a convenience, the variables ``pixel_width`` - and ``pixel_height`` hold the actual width and height of the Sketch in pixels. - This is useful for any Sketch that use the ``pixels[]`` or ``np_pixels[]`` - arrays, for instance, because the number of elements in each array will be - ``pixel_width*pixel_height``, not ``width*height``. + Width of the display window in pixels. When ``pixel_density(2)`` is used to make + use of a high resolution display (called a Retina display on OSX or high-dpi on + Windows and Linux), the width and height of the Sketch do not change, but the + number of pixels is doubled. As a result, all operations that use pixels (like + ``load_pixels()``, ``get()``, etc.) happen in this doubled space. As a + convenience, the variables ``pixel_width`` and ``pixel_height`` hold the actual + width and height of the Sketch in pixels. This is useful for any Sketch that use + the ``pixels[]`` or ``np_pixels[]`` arrays, for instance, because the number of + elements in each array will be ``pixel_width*pixel_height``, not + ``width*height``. """ return self._instance.pixelWidth pixel_width: int = property(fget=_get_pixel_width) @@ -6076,8 +6102,8 @@ def create_graphics(self, w: int, h: int, /) -> Py5Graphics: Creates and returns a new ``Py5Graphics`` object. Use this class if you need to draw into an off-screen graphics buffer. The first two parameters define the width and height in pixels. The third, optional parameter specifies the - renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or SVG. If the third - parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` + renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or ``SVG``. If the + third parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` renderers require the filename parameter. It's important to consider the renderer used with ``create_graphics()`` in @@ -6146,8 +6172,8 @@ def create_graphics(self, w: int, h: int, renderer: str, /) -> Py5Graphics: Creates and returns a new ``Py5Graphics`` object. Use this class if you need to draw into an off-screen graphics buffer. The first two parameters define the width and height in pixels. The third, optional parameter specifies the - renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or SVG. If the third - parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` + renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or ``SVG``. If the + third parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` renderers require the filename parameter. It's important to consider the renderer used with ``create_graphics()`` in @@ -6217,8 +6243,8 @@ def create_graphics(self, w: int, h: int, renderer: str, Creates and returns a new ``Py5Graphics`` object. Use this class if you need to draw into an off-screen graphics buffer. The first two parameters define the width and height in pixels. The third, optional parameter specifies the - renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or SVG. If the third - parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` + renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or ``SVG``. If the + third parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` renderers require the filename parameter. It's important to consider the renderer used with ``create_graphics()`` in @@ -6287,8 +6313,8 @@ def create_graphics(self, *args): Creates and returns a new ``Py5Graphics`` object. Use this class if you need to draw into an off-screen graphics buffer. The first two parameters define the width and height in pixels. The third, optional parameter specifies the - renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or SVG. If the third - parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` + renderer. It can be defined as ``P2D``, ``P3D``, ``PDF``, or ``SVG``. If the + third parameter isn't used, the default renderer is set. The ``PDF`` and ``SVG`` renderers require the filename parameter. It's important to consider the renderer used with ``create_graphics()`` in @@ -8606,9 +8632,21 @@ def full_screen(self) -> None: Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -8648,9 +8686,21 @@ def full_screen(self, display: int, /) -> None: Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -8690,9 +8740,21 @@ def full_screen(self, renderer: str, /) -> None: Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -8732,9 +8794,21 @@ def full_screen(self, renderer: str, display: int, /) -> None: Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -8773,9 +8847,21 @@ def full_screen(self, *args): Notes ----- - Open a Sketch using the full size of the computer's display. This function must - be called in ``settings()``. The ``size()`` and ``full_screen()`` functions - cannot both be used in the same program. + Open a Sketch using the full size of the computer's display. This is intended to + be called from the ``settings()`` function. The ``size()`` and ``full_screen()`` + functions cannot both be used in the same program. + + When programming in module mode and imported mode, py5 will allow calls to + ``full_screen()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``full_screen()``, or calls + to ``size()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. When ``full_screen()`` is used without a parameter on a computer with multiple monitors, it will (probably) draw the Sketch to the primary display. When it is @@ -10598,9 +10684,23 @@ def no_smooth(self) -> None: Draws all geometry and fonts with jagged (aliased) edges and images with hard edges between the pixels when enlarged rather than interpolating pixels. Note that ``smooth()`` is active by default, so it is necessary to call - ``no_smooth()`` to disable smoothing of geometry, fonts, and images. The - ``no_smooth()`` method can only be run once for each Sketch and must be called - in ``settings()``. + ``no_smooth()`` to disable smoothing of geometry, fonts, and images. + + The ``no_smooth()`` function can only be called once within a Sketch. It is + intended to be called from the ``settings()`` function. The ``smooth()`` + function follows the same rules. + + When programming in module mode and imported mode, py5 will allow calls to + ``no_smooth()`` from the ``setup()`` function if it is called at the beginning + of ``setup()``. This allows the user to omit the ``settings()`` function, much + like what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``no_smooth()``, or calls + to ``size()``, ``full_screen()``, ``smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. """ return self._instance.noSmooth() @@ -11010,18 +11110,29 @@ def pixel_density(self, density: int, /) -> None: This function makes it possible for py5 to render using all of the pixels on high resolutions screens like Apple Retina displays and Windows High-DPI - displays. This function can only be run once within a program and it must be - called in ``settings()``. The ``pixel_density()`` should only be used with - hardcoded numbers (in almost all cases this number will be 2) or in combination - with ``display_density()`` as in the second example. + displays. This function can only be run once within a program. It is intended to + be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``pixel_density()`` from the ``setup()`` function if it is called at the + beginning of ``setup()``. This allows the user to omit the ``settings()`` + function, much like what can be done while programming in the Processing IDE. + Py5 does this by inspecting the ``setup()`` function and attempting to split it + into synthetic ``settings()`` and ``setup()`` functions if both were not created + by the user and the real ``setup()`` function contains a call to + ``pixel_density()``, or calls to ``size()``, ``full_screen()``, ``smooth()``, or + ``no_smooth()``. Calls to those functions must be at the very beginning of + ``setup()``, before any other Python code (but comments are ok). This feature is + not available when programming in class mode. + + The ``pixel_density()`` should only be used with hardcoded numbers (in almost + all cases this number will be 2) or in combination with ``display_density()`` as + in the second example. When the pixel density is set to more than 1, it changes all of the pixel operations including the way ``get()``, ``blend()``, ``copy()``, ``update_pixels()``, and ``update_np_pixels()`` all work. See the reference for ``pixel_width`` and ``pixel_height`` for more information. - - To use variables as the arguments to ``pixel_density()`` function, place the - ``pixel_density()`` function within the ``settings()`` function. """ return self._instance.pixelDensity(density) @@ -13299,7 +13410,19 @@ def size(self, width: int, height: int, /) -> None: ----- Defines the dimension of the display window width and height in units of pixels. - This must be called from the ``settings()`` function. + This is intended to be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``size()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``size()``, or calls to + ``full_screen()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls + to those functions must be at the very beginning of ``setup()``, before any + other Python code (but comments are ok). This feature is not available when + programming in class mode. The built-in variables ``width`` and ``height`` are set by the parameters passed to this function. For example, running ``size(640, 480)`` will assign 640 to the @@ -13378,7 +13501,19 @@ def size(self, width: int, height: int, renderer: str, /) -> None: ----- Defines the dimension of the display window width and height in units of pixels. - This must be called from the ``settings()`` function. + This is intended to be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``size()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``size()``, or calls to + ``full_screen()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls + to those functions must be at the very beginning of ``setup()``, before any + other Python code (but comments are ok). This feature is not available when + programming in class mode. The built-in variables ``width`` and ``height`` are set by the parameters passed to this function. For example, running ``size(640, 480)`` will assign 640 to the @@ -13458,7 +13593,19 @@ def size(self, width: int, height: int, ----- Defines the dimension of the display window width and height in units of pixels. - This must be called from the ``settings()`` function. + This is intended to be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``size()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``size()``, or calls to + ``full_screen()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls + to those functions must be at the very beginning of ``setup()``, before any + other Python code (but comments are ok). This feature is not available when + programming in class mode. The built-in variables ``width`` and ``height`` are set by the parameters passed to this function. For example, running ``size(640, 480)`` will assign 640 to the @@ -13536,7 +13683,19 @@ def size(self, *args): ----- Defines the dimension of the display window width and height in units of pixels. - This must be called from the ``settings()`` function. + This is intended to be called from the ``settings()`` function. + + When programming in module mode and imported mode, py5 will allow calls to + ``size()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``size()``, or calls to + ``full_screen()``, ``smooth()``, ``no_smooth()``, or ``pixel_density()``. Calls + to those functions must be at the very beginning of ``setup()``, before any + other Python code (but comments are ok). This feature is not available when + programming in class mode. The built-in variables ``width`` and ``height`` are set by the parameters passed to this function. For example, running ``size(640, 480)`` will assign 640 to the @@ -13619,9 +13778,21 @@ def smooth(self) -> None: The other option for the default renderer is ``smooth(2)``, which is bilinear smoothing. - The ``smooth()`` function can only be set once within a Sketch. It must be - called from the `settings()`` function. The ``no_smooth()`` function also + The ``smooth()`` function can only be set once within a Sketch. It is intended + to be called from the ``settings()`` function. The ``no_smooth()`` function follows the same rules. + + When programming in module mode and imported mode, py5 will allow calls to + ``smooth()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``smooth()``, or calls to + ``size()``, ``full_screen()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. """ pass @@ -13663,9 +13834,21 @@ def smooth(self, level: int, /) -> None: The other option for the default renderer is ``smooth(2)``, which is bilinear smoothing. - The ``smooth()`` function can only be set once within a Sketch. It must be - called from the `settings()`` function. The ``no_smooth()`` function also + The ``smooth()`` function can only be set once within a Sketch. It is intended + to be called from the ``settings()`` function. The ``no_smooth()`` function follows the same rules. + + When programming in module mode and imported mode, py5 will allow calls to + ``smooth()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``smooth()``, or calls to + ``size()``, ``full_screen()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. """ pass @@ -13706,9 +13889,21 @@ def smooth(self, *args): The other option for the default renderer is ``smooth(2)``, which is bilinear smoothing. - The ``smooth()`` function can only be set once within a Sketch. It must be - called from the `settings()`` function. The ``no_smooth()`` function also + The ``smooth()`` function can only be set once within a Sketch. It is intended + to be called from the ``settings()`` function. The ``no_smooth()`` function follows the same rules. + + When programming in module mode and imported mode, py5 will allow calls to + ``smooth()`` from the ``setup()`` function if it is called at the beginning of + ``setup()``. This allows the user to omit the ``settings()`` function, much like + what can be done while programming in the Processing IDE. Py5 does this by + inspecting the ``setup()`` function and attempting to split it into synthetic + ``settings()`` and ``setup()`` functions if both were not created by the user + and the real ``setup()`` function contains a call to ``smooth()``, or calls to + ``size()``, ``full_screen()``, ``no_smooth()``, or ``pixel_density()``. Calls to + those functions must be at the very beginning of ``setup()``, before any other + Python code (but comments are ok). This feature is not available when + programming in class mode. """ return self._instance.smooth(*args) diff --git a/py5/surface.py b/py5/surface.py index db1c7c9..995e67f 100644 --- a/py5/surface.py +++ b/py5/surface.py @@ -158,8 +158,8 @@ def set_icon(self, icon: Py5Image, /) -> None: bar. The default window icon is the same as Processing's. This method will not work for the ``P2D`` or ``P3D`` renderers. Setting the icon - for those renderers is a bit tricky; see the second example to learn how to do - that. + for those renderers is a bit tricky; the icon must be a PNG file and it must be + done in ``settings()``. See the second example to learn how to do that. """ return self._instance.setIcon(icon) diff --git a/py5_tools/__init__.py b/py5_tools/__init__.py index ee34370..74ca661 100644 --- a/py5_tools/__init__.py +++ b/py5_tools/__init__.py @@ -20,15 +20,10 @@ """ Utilities and accessory tools for py5. """ -from . import imported # noqa -from .imported import set_imported_mode # noqa +from .imported import set_imported_mode, get_imported_mode # noqa from .jvm import * # noqa from .libraries import * # noqa from .hooks import * # noqa -from . import magics # noqa -from . import parsing # noqa -from . import utilities # noqa -from . import testing # noqa -__version__ = '0.4a2' +__version__ = '0.5a0' diff --git a/py5_tools/hooks/frame_hooks.py b/py5_tools/hooks/frame_hooks.py index 0ba216c..1edf2d3 100644 --- a/py5_tools/hooks/frame_hooks.py +++ b/py5_tools/hooks/frame_hooks.py @@ -316,3 +316,6 @@ def capture_frames(count: float, *, period: float = 0.0, sketch: Sketch = None, return [PIL.Image.fromarray(arr, mode='RGB') for arr in hook.frames] elif hook.is_terminated and hook.exception: raise RuntimeError('error running magic: ' + str(hook.exception)) + + +__all__ = ['screenshot', 'save_frames', 'animated_gif', 'capture_frames'] diff --git a/py5_tools/hooks/hooks.py b/py5_tools/hooks/hooks.py index 5ee4a44..ecaa1c8 100644 --- a/py5_tools/hooks/hooks.py +++ b/py5_tools/hooks/hooks.py @@ -77,7 +77,7 @@ def __call__(self, sketch): num = sketch.frame_count - self.num_offset frame_filename = sketch._insert_frame( str(self.dirname / self.filename), num=num) - sketch.save_frame(frame_filename) + sketch.save_frame(frame_filename, use_thread=True) self.filenames.append(frame_filename) self.last_frame_time = time.time() if len(self.filenames) == self.limit: diff --git a/py5_tools/hooks/zmq_hooks.py b/py5_tools/hooks/zmq_hooks.py index 76b0aaf..1b5c339 100644 --- a/py5_tools/hooks/zmq_hooks.py +++ b/py5_tools/hooks/zmq_hooks.py @@ -40,7 +40,7 @@ def sketch_portal( throttle_frame_rate: float = None, scale: float = 1.0, quality: int = 75, - portal_widget: Py5SketchPortal = None, + portal: Py5SketchPortal = None, sketch: Sketch = None, hook_post_draw: bool = False) -> None: """Creates a portal widget to continuously stream frames from a running Sketch into @@ -155,11 +155,11 @@ def sketch_portal( if scale <= 0: raise RuntimeError('The scale parameter must be greater than zero') - if portal_widget is None: - portal_widget = Py5SketchPortal() - portal_widget.layout.width = f'{sketch.width}px' - portal_widget.layout.height = f'{sketch.height}px' - portal_widget.layout.border = '1px solid gray' + if portal is None: + portal = Py5SketchPortal() + portal.layout.width = f'{int(scale * sketch.width)}px' + portal.layout.height = f'{int(scale * sketch.height)}px' + portal.layout.border = '1px solid gray' def displayer(frame): img = PIL.Image.fromarray(frame) @@ -167,7 +167,7 @@ def displayer(frame): img = img.resize(tuple(int(scale * x) for x in img.size)) b = io.BytesIO() img.save(b, format='JPEG', quality=quality) - portal_widget.value = b.getvalue() + portal.value = b.getvalue() hook = SketchPortalHook(displayer, throttle_frame_rate, time_limit) @@ -176,4 +176,10 @@ def displayer(frame): hook.hook_name, hook) - return portal_widget + exit_button = widgets.Button(description='exit_sketch()') + exit_button.on_click(lambda x: sketch.exit_sketch()) + + return widgets.VBox([portal, exit_button]) + + +__all__ = ['sketch_portal'] diff --git a/py5_tools/hooks/zmq_hooks_fail.py b/py5_tools/hooks/zmq_hooks_fail.py index 2eb7a17..e933bfa 100644 --- a/py5_tools/hooks/zmq_hooks_fail.py +++ b/py5_tools/hooks/zmq_hooks_fail.py @@ -109,3 +109,6 @@ def sketch_portal( existing portal.""" raise RuntimeError( 'The sketch_widget() function can only be used with IPython and ZMQInteractiveShell (such as Jupyter Lab)') + + +__all__ = ['sketch_portal'] diff --git a/py5_tools/imported.py b/py5_tools/imported.py index 83d0624..852677c 100644 --- a/py5_tools/imported.py +++ b/py5_tools/imported.py @@ -23,10 +23,9 @@ from multiprocessing import Process from pathlib import Path import re -import textwrap from . import jvm -from .magics import util +from .py5bot import py5bot from . import parsing @@ -42,6 +41,42 @@ def get_imported_mode() -> bool: return _imported_mode +_STATIC_CODE_FRAMEWORK = """ +import ast as _PY5BOT_ast + +import py5_tools +py5_tools.set_imported_mode(True) +import py5_tools.parsing as _PY5BOT_parsing +from py5 import * + + +def settings(): + with open('{0}', 'r') as f: + exec( + compile( + _PY5BOT_parsing.transform_py5_code( + _PY5BOT_ast.parse(f.read(), filename='{0}', mode='exec'), + ), + filename='{0}', + mode='exec' + ) + ) + + +def setup(): + with open('{1}', 'r') as f: + exec( + compile( + _PY5BOT_parsing.transform_py5_code( + _PY5BOT_ast.parse(f.read(), filename='{1}', mode='exec'), + ), + filename='{1}', + mode='exec' + ) + ) +""" + + _CODE_FRAMEWORK = """ {0} @@ -53,56 +88,15 @@ def get_imported_mode() -> bool: SETTINGS_REGEX = re.compile(r'^def settings\(\):', flags=re.MULTILINE) SETUP_REGEX = re.compile(r'^def setup\(\):', flags=re.MULTILINE) -SETUP_CODE_REGEX = re.compile( - r'^def setup\(\):.*?(?=^\w|\Z)', - flags=re.MULTILINE | re.DOTALL) DRAW_REGEX = re.compile(r'^def draw\(\):', flags=re.MULTILINE) -CODE_REGEXES = { - f: re.compile( - r'^\s*(' + f + r'\([^\)]*\))', - flags=re.MULTILINE) for f in [ - 'size', - 'full_screen', - 'smooth', - 'no_smooth', - 'pixel_density']} - - -# TODO: this is ugly and should be done with ast instead -def prepare_code(code): - "transform functionless or setttings-less py5 code into code that runs" - if SETTINGS_REGEX.search(code): - return False, code - no_setup = SETUP_REGEX.search(code) is None - no_draw = DRAW_REGEX.search(code) is None - # get just the setup function if it is defined - code2 = code if no_setup else SETUP_CODE_REGEX.search(code).group() - # find the key lines in the relevant code - matches = [m for m in [r.search(code2) - for r in CODE_REGEXES.values()] if m] - - # if anything was found, build the settings function - if matches: - lines = [(m.start(), m.group(1)) for m in matches] - settings = 'def settings():\n' - for start, line in sorted(lines): - settings += f' {line}\n' - # replace the original line so it doesn't get called in setup - code = code.replace(line, f'pass # moved to settings(): {line}') - else: - settings = '' - if no_setup and no_draw: - # put all of the remaining code into a setup function - remaining_code = 'def setup():\n' + textwrap.indent(code, prefix=' ') - remaining_code = util.fix_triple_quote_str(remaining_code) - else: - # remaining code has been modified with key lines moved from setup to - # settings - remaining_code = code +def is_static_mode(code): + no_settings = SETTINGS_REGEX.search(code) is None + no_setup = SETUP_REGEX.search(code) is None + no_draw = DRAW_REGEX.search(code) is None - return True, f'{settings.strip()}\n\n{remaining_code.strip()}\n' + return no_settings and no_setup and no_draw def run_code( @@ -117,13 +111,38 @@ def run_code( with open(sketch_path, 'r') as f: code = f.read() - tranformed, code = prepare_code(code) - if tranformed: - temp_py = sketch_path.with_suffix('.tmp.py') - with open(temp_py, 'w') as f: - f.write(code) - sketch_path = temp_py + if is_static_mode(code): + _run_static_code( + code, + sketch_path, + classpath, + new_process, + exit_if_error) + else: + _run_code(sketch_path, classpath, new_process, exit_if_error) + + +def _run_static_code(code, sketch_path, classpath, new_process, exit_if_error): + py5bot_mgr = py5bot.Py5BotManager() + success, result = py5bot.check_for_problems(code, sketch_path) + if success: + py5bot_settings, py5bot_setup = result + py5bot_mgr.write_code( + py5bot_settings, py5bot_setup, len( + code.splitlines())) + new_sketch_path = py5bot_mgr.tempdir / '_PY5_STATIC_FRAMEWORK_CODE_.py' + with open(new_sketch_path, 'w') as f: + f.write( + _STATIC_CODE_FRAMEWORK.format( + py5bot_mgr.settings_filename, + py5bot_mgr.setup_filename)) + _run_code(new_sketch_path, classpath, new_process, exit_if_error) + else: + print(result, file=sys.stderr) + + +def _run_code(sketch_path, classpath, new_process, exit_if_error): def _run_sketch(sketch_path, classpath, exit_if_error): if not jvm.is_jvm_running(): if classpath: @@ -140,16 +159,13 @@ def _run_sketch(sketch_path, classpath, exit_if_error): with open(sketch_path, 'r') as f: sketch_code = _CODE_FRAMEWORK.format(f.read(), exit_if_error) - sketch_ast = ast.parse(sketch_code, mode='exec') + sketch_ast = ast.parse(sketch_code, filename=sketch_path, mode='exec') problems = parsing.check_reserved_words(sketch_code, sketch_ast) if problems: - if len(problems) == 1: - msg = 'There is a problem with your Sketch code' - else: - msg = f'There are {len(problems)} problems with your Sketch code' + msg = 'There ' + ('is a problem' if len(problems) == + 1 else f'are {len(problems)} problems') + ' with your Sketch code' + msg += '\n' + '=' * len(msg) + '\n' + '\n'.join(problems) print(msg) - print('=' * len(msg)) - print('\n'.join(problems)) return sketch_compiled = compile( @@ -173,5 +189,3 @@ def _run_sketch(sketch_path, classpath, exit_if_error): return p else: _run_sketch(sketch_path, classpath, exit_if_error) - if tranformed: - os.remove(temp_py) diff --git a/py5_tools/kernel/kernel.py b/py5_tools/kernel/kernel.py index 586aead..5c650cd 100644 --- a/py5_tools/kernel/kernel.py +++ b/py5_tools/kernel/kernel.py @@ -48,6 +48,7 @@ import py5_tools py5_tools.set_imported_mode(True) from py5 import * +from py5_tools import sketch_portal """ _KERNEL_STARTUP = (_MACOSX_PRE_STARTUP if sys.platform == @@ -74,7 +75,7 @@ class Py5Kernel(IPythonKernel): *_PY5_HELP_LINKS]).tag(config=True) implementation = 'py5' - implementation_version = '0.4a2' + implementation_version = '0.5a0' class Py5App(IPKernelApp): @@ -87,4 +88,7 @@ class Py5App(IPKernelApp): _KERNEL_STARTUP ]).tag(config=True) - extensions = List(Unicode(), ['py5_tools.magics']).tag(config=True) + extensions = List( + Unicode(), [ + 'py5_tools.magics', 'py5_tools.py5bot']).tag( + config=True) diff --git a/py5_tools/magics/__init__.py b/py5_tools/magics/__init__.py index 6321371..cd9633f 100644 --- a/py5_tools/magics/__init__.py +++ b/py5_tools/magics/__init__.py @@ -17,8 +17,12 @@ # along with this library. If not, see . # # ***************************************************************************** -from .drawing import DrawingMagics +import sys + +from .drawing import DrawingMagics, DXFDrawingMagic def load_ipython_extension(ipython): ipython.register_magics(DrawingMagics) + if sys.platform != 'darwin': + ipython.register_magics(DXFDrawingMagic) diff --git a/py5_tools/magics/drawing.py b/py5_tools/magics/drawing.py index 12a2099..74a27d4 100644 --- a/py5_tools/magics/drawing.py +++ b/py5_tools/magics/drawing.py @@ -19,40 +19,46 @@ # ***************************************************************************** import sys import re +import ast import io from pathlib import Path import tempfile -import textwrap from IPython.display import display, SVG, Image from IPython.core.magic import Magics, magics_class, cell_magic from IPython.core.magic_arguments import parse_argstring, argument, magic_arguments, kwds +import stackprinter import PIL -from .util import fix_triple_quote_str, CellMagicHelpFormatter +from .util import CellMagicHelpFormatter, filename_check, variable_name_check from .. import imported +from .. import parsing -_CODE_FRAMEWORK = """ +_CODE_FRAMEWORK_BEGIN = """ import py5 +import py5_tools.parsing as _PY5BOT_parsing +import ast as _PY5BOT_ast +""" -with open('{0}', 'r') as f: - eval(compile(f.read(), '{0}', 'exec')) -py5.run_sketch(block=True, sketch_functions=dict(settings=_py5_settings, setup=_py5_setup)) -if py5.is_dead_from_error: - py5.exit_sketch() +_CODE_FRAMEWORK_IMPORTED_BEGIN = """ +from py5 import * +import py5_tools.parsing as _PY5BOT_parsing +import ast as _PY5BOT_ast """ -_CODE_FRAMEWORK_IMPORTED_MODE = """ -with open('{0}', 'r') as f: - eval(compile(f.read(), '{0}', 'exec')) +_CODE_TEMPLATE_END = """ +py5.run_sketch(block=True, sketch_functions=dict(settings=_py5_settings, setup=_py5_setup)) +if py5.is_dead_from_error: + py5.exit_sketch() -run_sketch(block=True, sketch_functions=dict(settings=_py5_settings, setup=_py5_setup)) -if is_dead_from_error: - exit_sketch() +del _PY5BOT_parsing +del _PY5BOT_ast +del _py5_settings +del _py5_setup """ @@ -62,7 +68,18 @@ def _py5_settings(): def _py5_setup(): -{4} + with open('{4}', 'r') as f: + exec( + compile( + _PY5BOT_parsing.transform_py5_code( # TRANSFORM + _PY5BOT_ast.parse(f.read(), filename='{4}', mode='exec'), + ), # TRANSFORM + filename='{4}', + mode='exec' + ), + globals(), + _py5_user_ns + ) py5.get(0, 0, {0}, {1}).save("{3}", use_thread=False) py5.exit_sketch() @@ -75,7 +92,18 @@ def _py5_settings(): def _py5_setup(): -{4} + with open('{4}', 'r') as f: + exec( + compile( + _PY5BOT_parsing.transform_py5_code( # TRANSFORM + _PY5BOT_ast.parse(f.read(), filename='{4}', mode='exec'), + ), # TRANSFORM + filename='{4}', + mode='exec' + ), + globals(), + _py5_user_ns + ) py5.exit_sketch() """ @@ -89,7 +117,18 @@ def _py5_settings(): def _py5_setup(): py5.begin_raw(py5.DXF, "{3}") -{4} + with open('{4}', 'r') as f: + exec( + compile( + _PY5BOT_parsing.transform_py5_code( # TRANSFORM + _PY5BOT_ast.parse(f.read(), filename='{4}', mode='exec'), + ), # TRANSFORM + filename='{4}', + mode='exec' + ), + globals(), + _py5_user_ns + ) py5.end_raw() py5.exit_sketch() @@ -98,19 +137,19 @@ def _py5_setup(): def _run_sketch(renderer, code, width, height, user_ns, safe_exec): if renderer == 'SVG': - template = _SAVE_OUTPUT_CODE_TEMPLATE + template = _SAVE_OUTPUT_CODE_TEMPLATE + _CODE_TEMPLATE_END suffix = '.svg' read_mode = 'r' elif renderer == 'PDF': - template = _SAVE_OUTPUT_CODE_TEMPLATE + template = _SAVE_OUTPUT_CODE_TEMPLATE + _CODE_TEMPLATE_END suffix = '.pdf' read_mode = 'rb' elif renderer == 'DXF': - template = _DXF_CODE_TEMPLATE + template = _DXF_CODE_TEMPLATE + _CODE_TEMPLATE_END suffix = '.dxf' read_mode = 'r' else: - template = _STANDARD_CODE_TEMPLATE + template = _STANDARD_CODE_TEMPLATE + _CODE_TEMPLATE_END suffix = '.png' read_mode = 'rb' @@ -123,30 +162,49 @@ def _run_sketch(renderer, code, width, height, user_ns, safe_exec): file=sys.stderr) return None - if imported.get_imported_mode(): - template = template.replace('py5.', '') - code_framework = _CODE_FRAMEWORK_IMPORTED_MODE - else: - code_framework = _CODE_FRAMEWORK + # does the code parse? if not, return an error message + try: + sketch_ast = ast.parse(code, filename='', mode='exec') + except Exception as e: + msg = stackprinter.format(e) + m = re.search(r'^SyntaxError:', msg, flags=re.MULTILINE) + if m: + msg = msg[m.start(0):] + print('There is a problem with your code:\n' + msg, file=sys.stderr) + return None - if safe_exec: - prepared_code = textwrap.indent(code, ' ') - prepared_code = fix_triple_quote_str(prepared_code) + if imported.get_imported_mode(): + # check for assignments to or deletions of reserved words + problems = parsing.check_reserved_words(code, sketch_ast) + if problems: + msg = 'There ' + ('is a problem' if len(problems) == + 1 else f'are {len(problems)} problems') + ' with your code.\n' + msg += '=' * len(msg) + '\n' + '\n'.join(problems) + print(msg, file=sys.stderr) + return None + + code_framework = _CODE_FRAMEWORK_IMPORTED_BEGIN + \ + template.replace('py5.', '') else: - user_ns['_py5_user_ns'] = user_ns - code = code.replace('"""', r'\"\"\"') - prepared_code = f' exec("""{code}""", _py5_user_ns)' + code_framework = _CODE_FRAMEWORK_BEGIN + \ + '\n'.join([l for l in template.splitlines() if l.find('# TRANSFORM') == -1]) with tempfile.TemporaryDirectory() as tempdir: - temp_py = Path(tempdir) / 'py5_code.py' + temp_py = Path(tempdir) / '_PY5_STATIC_SETUP_CODE_.py' temp_out = Path(tempdir) / ('output' + suffix) with open(temp_py, 'w') as f: - code = template.format( - width, height, renderer, temp_out.as_posix(), prepared_code) f.write(code) - exec(code_framework.format(temp_py.as_posix()), user_ns) + user_ns['_py5_user_ns'] = {} if safe_exec else user_ns + exec( + code_framework.format( + width, + height, + renderer, + temp_out.as_posix(), + temp_py.as_posix()), + user_ns) if temp_out.exists(): with open(temp_out, read_mode) as f: @@ -163,15 +221,6 @@ def _run_sketch(renderer, code, width, height, user_ns, safe_exec): @magics_class class DrawingMagics(Magics): - def _filename_check(self, filename): - filename = Path(filename) - if not filename.parent.exists(): - filename.parent.mkdir(parents=True) - return filename - - def _variable_name_check(self, varname): - return re.match('^[a-zA-Z_]\\w*' + chr(36), varname) - @magic_arguments() @argument('width', type=int, help='width of PDF output') @argument('height', type=int, help='height of PDF output') @@ -206,50 +255,11 @@ def py5drawpdf(self, line, cell): pdf = _run_sketch('PDF', cell, args.width, args.height, self.shell.user_ns, not args.unsafe) if pdf: - filename = self._filename_check(args.filename) + filename = filename_check(args.filename) with open(filename, 'wb') as f: f.write(pdf) print(f'PDF written to {filename}') - @magic_arguments() - @argument('width', type=int, help='width of DXF output') - @argument('height', type=int, help='height of DXF output') - @argument('filename', type=str, help='filename for DXF output') - @argument('--unsafe', dest='unsafe', action='store_true', - help='allow new variables to enter the global namespace') - @kwds(formatter_class=CellMagicHelpFormatter) - @cell_magic - def py5drawdxf(self, line, cell): - """Notes - ----- - - Create a DXF file with py5. - - For users who are familiar with Processing and py5 programming, you can pretend - the code in this cell will be executed in a Sketch with no ``draw()`` function - and your code in the ``setup()`` function. It will use the ``DXF`` renderer. - - As this is creating a DXF file, your code will be limited to the capabilities of - that renderer. - - Code used in this cell can reference functions and variables defined in other - cells. By default, variables and functions created in this cell will be local to - only this cell because to do otherwise would be unsafe. If you understand the - risks, you can use the ``global`` keyword to add a single function or variable - to the notebook namespace or the ``--unsafe`` argument to add everything to the - notebook namespace. Either option may be very useful to you, but be aware that - using py5 objects in a different notebook cell or reusing them in another Sketch - can result in nasty errors and bizzare consequences.""" - args = parse_argstring(self.py5drawdxf, line) - - dxf = _run_sketch('DXF', cell, args.width, args.height, - self.shell.user_ns, not args.unsafe) - if dxf: - filename = self._filename_check(args.filename) - with open(filename, 'w') as f: - f.write(dxf) - print(f'DXF written to {filename}') - @magic_arguments() @argument('width', type=int, help='width of SVG drawing') @argument('height', type=int, help='height of SVG drawing') @@ -263,7 +273,7 @@ def py5drawsvg(self, line, cell): """Notes ----- - Create a SVG drawing with py5 and embed result in the notebook. + Create a SVG drawing with py5 and embed the result in the notebook. For users who are familiar with Processing and py5 programming, you can pretend the code in this cell will be executed in a Sketch with no ``draw()`` function @@ -286,7 +296,7 @@ def py5drawsvg(self, line, cell): self.shell.user_ns, not args.unsafe) if svg: if args.filename: - filename = self._filename_check(args.filename) + filename = filename_check(args.filename) with open(filename, 'w') as f: f.write(svg) print(f'SVG drawing written to {filename}') @@ -307,13 +317,21 @@ def py5draw(self, line, cell): """Notes ----- - Create a PNG image with py5 and embed result in the notebook. + Create a PNG image with py5 and embed the result in the notebook. For users who are familiar with Processing and py5 programming, you can pretend the code in this cell will be executed in a Sketch with no ``draw()`` function and your code in the ``setup()`` function. By default it will use the default Processing renderer. + On OSX, only the default renderer is currently supported. Other platforms + support the default renderer and the OpenGL renderers (P2D and P3D). + + Internally this magic command creates a static Sketch using the user provided + code. The static Sketch drawing surface does not allow transparency. If you want + to quickly create an image that has transparency, consider using ``@render()`` + or ``render_frame()`` with the ``use_py5graphics`` parameter. + Code used in this cell can reference functions and variables defined in other cells. By default, variables and functions created in this cell will be local to only this cell because to do otherwise would be unsafe. If you understand the @@ -324,9 +342,20 @@ def py5draw(self, line, cell): can result in nasty errors and bizzare consequences.""" args = parse_argstring(self.py5draw, line) + if sys.platform == 'darwin': + if args.renderer in ['P2D', 'P3D', 'DXF']: + print( + f'Sorry, py5 magics do not support the {args.renderer} renderer on OSX.', + file=sys.stderr) + return + if args.renderer == 'JAVA2D': + args.renderer = 'HIDDEN' if args.renderer == 'SVG': print('please use %%py5drawsvg for SVG drawings.', file=sys.stderr) return + if args.renderer == 'DXF': + print('please use %%py5drawdxf for DXF output.', file=sys.stderr) + return if args.renderer == 'PDF': print('please use %%py5drawpdf for PDFs.', file=sys.stderr) return @@ -340,11 +369,11 @@ def py5draw(self, line, cell): if args.filename or args.variable: pil_img = PIL.Image.open(io.BytesIO(png)).convert(mode='RGB') if args.filename: - filename = self._filename_check(args.filename) + filename = filename_check(args.filename) pil_img.save(filename) print(f'PNG file written to {filename}') if args.variable: - if self._variable_name_check(args.variable): + if variable_name_check(args.variable): self.shell.user_ns[args.variable] = pil_img print(f'PIL Image assigned to {args.variable}') else: @@ -352,3 +381,48 @@ def py5draw(self, line, cell): f'Invalid variable name {args.variable}', file=sys.stderr) display(Image(png)) + + +@magics_class +class DXFDrawingMagic(Magics): + + @magic_arguments() + @argument('width', type=int, help='width of DXF output') + @argument('height', type=int, help='height of DXF output') + @argument('filename', type=str, help='filename for DXF output') + @argument('--unsafe', dest='unsafe', action='store_true', + help='allow new variables to enter the global namespace') + @kwds(formatter_class=CellMagicHelpFormatter) + @cell_magic + def py5drawdxf(self, line, cell): + """Notes + ----- + + Create a DXF file with py5. + + For users who are familiar with Processing and py5 programming, you can pretend + the code in this cell will be executed in a Sketch with no ``draw()`` function + and your code in the ``setup()`` function. It will use the ``DXF`` renderer. + + As this is creating a DXF file, your code will be limited to the capabilities of + that renderer. + + This magic is not available on OSX. + + Code used in this cell can reference functions and variables defined in other + cells. By default, variables and functions created in this cell will be local to + only this cell because to do otherwise would be unsafe. If you understand the + risks, you can use the ``global`` keyword to add a single function or variable + to the notebook namespace or the ``--unsafe`` argument to add everything to the + notebook namespace. Either option may be very useful to you, but be aware that + using py5 objects in a different notebook cell or reusing them in another Sketch + can result in nasty errors and bizzare consequences.""" + args = parse_argstring(self.py5drawdxf, line) + + dxf = _run_sketch('DXF', cell, args.width, args.height, + self.shell.user_ns, not args.unsafe) + if dxf: + filename = filename_check(args.filename) + with open(filename, 'w') as f: + f.write(dxf) + print(f'DXF written to {filename}') diff --git a/py5_tools/magics/util.py b/py5_tools/magics/util.py index fa3d7c6..7cae84b 100644 --- a/py5_tools/magics/util.py +++ b/py5_tools/magics/util.py @@ -19,6 +19,7 @@ # ***************************************************************************** import time import re +from pathlib import Path from IPython.core.magic_arguments import MagicHelpFormatter @@ -48,4 +49,20 @@ def wait(wait_time, sketch): time.sleep(0.1) -__all__ = ['CellMagicHelpFormatter', 'fix_triple_quote_str', 'wait'] +def filename_check(filename): + filename = Path(filename) + if not filename.parent.exists(): + filename.parent.mkdir(parents=True) + return filename + + +def variable_name_check(varname): + return re.match('^[a-zA-Z_]\\w*' + chr(36), varname) + + +__all__ = [ + 'CellMagicHelpFormatter', + 'fix_triple_quote_str', + 'wait', + 'filename_check', + 'variable_name_check'] diff --git a/py5_tools/py5bot/__init__.py b/py5_tools/py5bot/__init__.py new file mode 100644 index 0000000..7e65ff9 --- /dev/null +++ b/py5_tools/py5bot/__init__.py @@ -0,0 +1,25 @@ +# ***************************************************************************** +# +# Part of the py5 library +# Copyright (C) 2020-2021 Jim Schmitz +# +# This library is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2.1 of the License, or (at +# your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . +# +# ***************************************************************************** +from .kernel import Py5BotKernel # noqa +from .py5bot import Py5BotMagics + + +def load_ipython_extension(ipython): + ipython.register_magics(Py5BotMagics) diff --git a/py5_tools/py5bot/__main__.py b/py5_tools/py5bot/__main__.py new file mode 100644 index 0000000..5e8ab7d --- /dev/null +++ b/py5_tools/py5bot/__main__.py @@ -0,0 +1,22 @@ +# ***************************************************************************** +# +# Part of the py5 library +# Copyright (C) 2020-2021 Jim Schmitz +# +# This library is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2.1 of the License, or (at +# your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . +# +# ***************************************************************************** +from .kernel import Py5BotApp + +Py5BotApp.launch_instance() diff --git a/py5_tools/py5bot/install.py b/py5_tools/py5bot/install.py new file mode 100644 index 0000000..16d58db --- /dev/null +++ b/py5_tools/py5bot/install.py @@ -0,0 +1,89 @@ +# ***************************************************************************** +# +# Part of the py5 library +# Copyright (C) 2020-2021 Jim Schmitz +# +# This library is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2.1 of the License, or (at +# your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . +# +# ***************************************************************************** +import os +import sys +import shutil +from pathlib import Path +import argparse +import json + +from jupyter_client.kernelspec import KernelSpecManager +from IPython.utils.tempdir import TemporaryDirectory + + +kernel_json = { + "argv": [ + sys.executable, + "-m", + "py5_tools.py5bot", + "-f", + "{connection_file}"], + "display_name": "py5bot", + "language": "python", +} + + +def install_py5bot_kernel_spec(user=True, prefix=None): + with TemporaryDirectory() as td: + os.chmod(td, 0o755) # Starts off as 700, not user readable + with open(Path(td) / 'kernel.json', 'w') as f: + json.dump(kernel_json, f, sort_keys=True) + + # Copy any resources + for file in (Path(__file__).parent / 'resources').glob('*'): + shutil.copy(file, Path(td) / file.name) + + print('Installing py5bot Jupyter kernel spec') + KernelSpecManager().install_kernel_spec( + td, 'py5bot', user=user, prefix=prefix) + + +def _is_root(): + try: + return os.geteuid() == 0 + except AttributeError: + return False # assume not an admin on non-Unix platforms + + +def main(argv=None): + ap = argparse.ArgumentParser() + ap.add_argument( + '--user', + action='store_true', + help="Install to the per-user kernels registry. Default if not root.") + ap.add_argument( + '--sys-prefix', + action='store_true', + help="Install to sys.prefix (e.g. a virtualenv or conda env)") + ap.add_argument( + '--prefix', help="Install to the given prefix. " + "Kernelspec will be installed in {PREFIX}/share/jupyter/kernels/") + args = ap.parse_args(argv) + + if args.sys_prefix: + args.prefix = sys.prefix + if not args.prefix and not _is_root(): + args.user = True + + install_py5bot_kernel_spec(user=args.user, prefix=args.prefix) + + +if __name__ == '__main__': + main() diff --git a/py5_tools/py5bot/kernel.py b/py5_tools/py5bot/kernel.py new file mode 100644 index 0000000..9bacef9 --- /dev/null +++ b/py5_tools/py5bot/kernel.py @@ -0,0 +1,112 @@ +# ***************************************************************************** +# +# Part of the py5 library +# Copyright (C) 2020-2021 Jim Schmitz +# +# This library is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2.1 of the License, or (at +# your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . +# +# ***************************************************************************** +import sys + +from ipykernel.zmqshell import ZMQInteractiveShell +from IPython.core.interactiveshell import InteractiveShellABC +from ipykernel.kernelapp import IPKernelApp + +from traitlets import Type, Instance, Unicode, List + +from ..kernel.kernel import Py5Kernel +from .. import split_setup +from . import py5bot + +from ..parsing import TransformDynamicVariablesToCalls, ReservedWordsValidation + + +class Py5BotShell(ZMQInteractiveShell): + + ast_transformers = List( + [TransformDynamicVariablesToCalls(), ReservedWordsValidation()]).tag(config=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._py5bot_mgr = py5bot.Py5BotManager() + + banner2 = Unicode("Activating py5bot").tag(config=True) + + def run_cell( + self, + raw_cell, + store_history=False, + silent=False, + shell_futures=True): + # check for special code that should bypass py5bot processing + if raw_cell.strip().startswith('%%python\n'): + return super( + Py5BotShell, + self).run_cell( + raw_cell.replace( + '%%python\n', + ''), + store_history=store_history, + silent=silent, + shell_futures=shell_futures) + + success, result = py5bot.check_for_problems(raw_cell, "") + if success: + py5bot_settings, py5bot_setup = result + if split_setup.count_noncomment_lines(py5bot_settings) == 0: + py5bot_settings = 'size(100, 100, HIDDEN)' + self._py5bot_mgr.write_code( + py5bot_settings, py5bot_setup, len( + raw_cell.splitlines())) + + return super( + Py5BotShell, + self).run_cell( + self._py5bot_mgr.run_code, + store_history=store_history, + silent=silent, + shell_futures=shell_futures) + else: + print(result, file=sys.stderr) + + return super( + Py5BotShell, + self).run_cell( + 'None', + store_history=store_history, + silent=silent, + shell_futures=shell_futures) + + +InteractiveShellABC.register(Py5BotShell) + + +class Py5BotKernel(Py5Kernel): + shell = Instance('IPython.core.interactiveshell.InteractiveShellABC', + allow_none=True) + shell_class = Type(Py5BotShell) + + implementation = 'py5bot' + implementation_version = '0.5a0' + + +class Py5BotApp(IPKernelApp): + name = 'py5bot-kernel' + + kernel_class = Type('py5_tools.py5bot.Py5BotKernel', + klass='ipykernel.kernelbase.Kernel').tag(config=True) + + exec_lines = List(Unicode(), [ + '%%python\n' + py5bot.PY5BOT_CODE_STARTUP + ]).tag(config=True) diff --git a/py5_tools/py5bot/py5bot.py b/py5_tools/py5bot/py5bot.py new file mode 100644 index 0000000..55141a3 --- /dev/null +++ b/py5_tools/py5bot/py5bot.py @@ -0,0 +1,253 @@ +# ***************************************************************************** +# +# Part of the py5 library +# Copyright (C) 2020-2021 Jim Schmitz +# +# This library is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2.1 of the License, or (at +# your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . +# +# ***************************************************************************** +import sys +import ast +import re +from pathlib import Path +import tempfile + +from IPython.display import display +from IPython.core.magic import Magics, magics_class, cell_magic +from IPython.core.magic_arguments import parse_argstring, argument, magic_arguments, kwds + +import stackprinter + +from .. import parsing +from .. import split_setup +from ..magics.util import CellMagicHelpFormatter, filename_check, variable_name_check + + +PY5BOT_CODE_STARTUP = """ +import py5_tools +py5_tools.set_imported_mode(True) +from py5 import * + +import sys +import functools +import ast as _PY5BOT_ast + +import py5_tools.parsing as _PY5BOT_parsing + + +@functools.wraps(size) +def _PY5BOT_altered_size(*args): + if len(args) == 2: + args = *args, HIDDEN + elif len(args) >= 3 and isinstance(renderer := args[2], str): + renderers = [HIDDEN, JAVA2D] if sys.platform == 'darwin' else [HIDDEN, JAVA2D, P2D, P3D] + if renderer not in renderers: + renderer_name = {SVG: 'SVG', PDF: 'PDF', DXF: 'DXF', P2D: 'P2D', P3D: 'P3D'}.get(renderer, renderer) + print(f'Sorry, py5bot does not support the {renderer_name} renderer' + (' on OSX.' if sys.platform == 'darwin' else '.'), file=sys.stderr) + args = *args[:2], HIDDEN, *args[3:] + if sys.platform == 'darwin': + args = *args[:2], HIDDEN, *args[3:] + size(*args) +return validate_renderer + + +del sys +del functools +""" + + +PY5BOT_CODE = """ +_PY5BOT_OUTPUT_ = None + +def _py5bot_settings(): + exec("size = _PY5BOT_altered_size") + + with open('{0}', 'r') as f: + exec( + compile( + _PY5BOT_parsing.transform_py5_code( + _PY5BOT_ast.parse(f.read(), filename='{0}', mode='exec'), + ), + filename='{0}', + mode='exec' + ) + ) + + +def _py5bot_setup(): + global _PY5BOT_OUTPUT_ + + with open('{1}', 'r') as f: + exec( + compile( + _PY5BOT_parsing.transform_py5_code( + _PY5BOT_ast.parse(f.read(), filename='{1}', mode='exec'), + ), + filename='{1}', + mode='exec' + ) + ) + + from PIL import Image + load_np_pixels() + _PY5BOT_OUTPUT_ = Image.fromarray(np_pixels()[:, :, 1:]) + + exit_sketch() + + +run_sketch(sketch_functions=dict(settings=_py5bot_settings, setup=_py5bot_setup), block=True) +if is_dead_from_error: + exit_sketch() + +_PY5BOT_OUTPUT_ +""" + + +def check_for_problems(code, filename): + # does the code parse? if not, return an error message + try: + sketch_ast = ast.parse(code, filename=filename, mode='exec') + except Exception as e: + msg = stackprinter.format(e) + m = re.search(r'^SyntaxError:', msg, flags=re.MULTILINE) + if m: + msg = msg[m.start(0):] + msg = 'There is a problem with your code:\n' + msg + return False, msg + + # check for assignments to or deletions of reserved words + problems = parsing.check_reserved_words(code, sketch_ast) + if problems: + msg = 'There ' + ('is a problem' if len(problems) == + 1 else f'are {len(problems)} problems') + ' with your code.\n' + msg += '=' * len(msg) + '\n' + '\n'.join(problems) + return False, msg + + cutoff = split_setup.find_cutoff(code, 'imported') + py5bot_settings = '\n'.join(code.splitlines()[:cutoff]) + py5bot_setup = '\n'.join(code.splitlines()[cutoff:]) + + # check for calls to size, etc, that were not at the beginning of the code + problems = split_setup.check_for_special_functions( + py5bot_setup, 'imported') + if problems: + msg = 'There ' + ('is a problem' if len(problems) == + 1 else f'are {len(problems)} problems') + ' with your code.\n' + msg += 'The function ' + \ + ('call' if len(problems) == 1 else 'calls') + ' to ' + problems = [f'{name} (on line {i + 1})' for i, name in problems] + if len(problems) == 1: + msg += problems[0] + elif len(problems) == 2: + msg += f'{problems[0]} and {problems[1]}' + else: + msg += ', and '.join(', '.join(problems).rsplit(', ', maxsplit=1)) + msg += ' must be moved to the beginning of your code, before any other code.' + return False, msg + + return True, (py5bot_settings, py5bot_setup) + + +class Py5BotManager: + + def __init__(self): + self.tempdir = Path(tempfile.TemporaryDirectory().name) + self.tempdir.mkdir(parents=True, exist_ok=True) + self.settings_filename = self.tempdir / '_PY5_STATIC_SETTINGS_CODE_.py' + self.setup_filename = self.tempdir / '_PY5_STATIC_SETUP_CODE_.py' + self.startup_code = PY5BOT_CODE_STARTUP + self.run_code = PY5BOT_CODE.format( + self.settings_filename, self.setup_filename) + + def write_code(self, settings_code, setup_code, orig_line_count): + with open(self.settings_filename, 'w') as f: + f.write(settings_code) + + with open(self.setup_filename, 'w') as f: + f.write('\n' * (orig_line_count - len(setup_code.splitlines()))) + f.write(setup_code) + + +@magics_class +class Py5BotMagics(Magics): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._py5bot_mgr = Py5BotManager() + + @magic_arguments() + @argument('-f', '--filename', dest='filename', help='save image to file') + @argument('-v', '--var', dest='variable', help='assign image to variable') + @kwds(formatter_class=CellMagicHelpFormatter) + @cell_magic + def py5bot(self, line, cell): + """Notes + ----- + + Create a PNG image using py5bot and embed the result in the notebook. + + This cell magic uses the same rendering mechanism as the py5bot kernel. For + users who are familiar with Processing and py5 programming, you can pretend the + code in this cell will be executed as a static Sketch with no ``draw()`` + function and your code in the ``setup()`` function. The first line in the cell + should be a call to ``size()``. + + This magic is similar to ``%%py5draw`` in that both can be used to create a + static Sketch. One key difference is that ``%%py5bot`` requires the user to + begin the code with a call to ``size()``, while ``%%py5draw`` calls ``size()`` + for you based on the magic's arguments. + + This magic supports the default renderer and the ``P2D`` and ``P3D`` renderers. + Note that both of the OpenGL renderers will briefly open a window on your + screen. This magic is only available when using the py5 kernel and coding in + imported mode. The ``P2D`` and ``P3D`` renderers are not available when the py5 + kernel is hosted on an OSX computer. + + Code used in this cell can reference functions and variables defined in other + cells. By default, variables and functions created in this cell will be local to + only this cell because to do otherwise would be unsafe. If you understand the + risks, you can use the ``global`` keyword to add a single function or variable + to the notebook namespace.""" + args = parse_argstring(self.py5bot, line) + + success, result = check_for_problems(cell, "") + if success: + py5bot_settings, py5bot_setup = result + if split_setup.count_noncomment_lines(py5bot_settings) == 0: + py5bot_settings = 'size(100, 100, HIDDEN)' + self._py5bot_mgr.write_code( + py5bot_settings, py5bot_setup, len( + cell.splitlines())) + + ns = self.shell.user_ns + exec(self._py5bot_mgr.startup_code + self._py5bot_mgr.run_code, ns) + png = ns['_PY5BOT_OUTPUT_'] + + if args.filename: + filename = filename_check(args.filename) + png.save(filename) + print(f'PNG file written to {filename}') + if args.variable: + if variable_name_check(args.variable): + self.shell.user_ns[args.variable] = png + print(f'PIL Image assigned to {args.variable}') + else: + print( + f'Invalid variable name {args.variable}', + file=sys.stderr) + + display(png) + del ns['_PY5BOT_OUTPUT_'] + else: + print(result, file=sys.stderr) diff --git a/py5_tools/py5bot/resources/logo-32x32.png b/py5_tools/py5bot/resources/logo-32x32.png new file mode 100644 index 0000000..1a3e992 Binary files /dev/null and b/py5_tools/py5bot/resources/logo-32x32.png differ diff --git a/py5_tools/py5bot/resources/logo-64x64.png b/py5_tools/py5bot/resources/logo-64x64.png new file mode 100644 index 0000000..dad6ad2 Binary files /dev/null and b/py5_tools/py5bot/resources/logo-64x64.png differ diff --git a/py5_tools/reference.py b/py5_tools/reference.py index 3fcb7e6..69083ce 100644 --- a/py5_tools/reference.py +++ b/py5_tools/reference.py @@ -38,7 +38,6 @@ 'ARC', 'arc', 'ARGB', - 'args', 'ARGS_BGCOLOR', 'ARGS_DENSITY', 'ARGS_DISABLE_AWT', @@ -303,6 +302,7 @@ 'OVERLAY', 'P2D', 'P3D', + 'pargs', 'parse_json', 'PATH', 'PDF', @@ -923,7 +923,6 @@ ] PY5_DYNAMIC_VARIABLES = [ - 'args', 'display_height', 'display_width', 'finished', @@ -944,6 +943,7 @@ 'mouse_x', 'mouse_y', 'np_pixels', + 'pargs', 'pixel_height', 'pixel_width', 'pixels', diff --git a/py5_tools/split_setup.py b/py5_tools/split_setup.py new file mode 100644 index 0000000..d8233c3 --- /dev/null +++ b/py5_tools/split_setup.py @@ -0,0 +1,175 @@ +# ***************************************************************************** +# +# Part of the py5 library +# Copyright (C) 2020-2021 Jim Schmitz +# +# This library is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2.1 of the License, or (at +# your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see . +# +# ***************************************************************************** +import re +import ast +import inspect + +import py5_tools.parsing as parsing + + +COMMENT_LINE = re.compile(r'^\s*#.*' + chr(36), flags=re.MULTILINE) +DOCSTRING = re.compile(r'^\s*"""[^"]*"""', flags=re.MULTILINE | re.DOTALL) +MODULE_MODE_METHOD_LINE = re.compile(r'^\s*py5\.(\w+)\([^\)]*\)') +IMPORTED_MODE_METHOD_LINE = re.compile(r'^\s*(\w+)\([^\)]*\)') + + +def _get_method_line_regex(mode): + if mode == 'module': + return MODULE_MODE_METHOD_LINE + elif mode == 'imported': + return IMPORTED_MODE_METHOD_LINE + else: + raise RuntimeError('only module mode and imported mode are supported') + + +def _remove_comments(code): + # remove # comments + code = COMMENT_LINE.sub('', code) + # remove docstrings + for docstring in DOCSTRING.findall(code): + code = code.replace(docstring, (len(docstring.split('\n')) - 1) * '\n') + + return code + + +def find_cutoff(code, mode): + method_line = _get_method_line_regex(mode) + code = _remove_comments(code) + + # find the cutoff point + for i, line in enumerate(code.split('\n')): + if line == 'def setup():': + continue + if line.strip() and not ((m := method_line.match(line)) and m.groups()[0] in [ + 'size', 'full_screen', 'smooth', 'no_smooth', 'pixel_density']): + cutoff = i + break + else: + cutoff = i + 1 + + return cutoff + + +def check_for_special_functions(code, mode): + method_line = _get_method_line_regex(mode) + code = _remove_comments(code) + + out = [] + for i, line in enumerate(code.split('\n')): + m = method_line.match(line) + if m and m.groups()[0] in [ + 'size', + 'full_screen', + 'smooth', + 'no_smooth', + 'pixel_density']: + out.append((i, m.groups()[0])) + + return out + + +def count_noncomment_lines(code): + stripped_code = COMMENT_LINE.sub('', code).strip() + return len(stripped_code.split('\n')) if stripped_code else 0 + + +def transform(functions, sketch_globals, sketch_locals, println, *, mode): + """if appropriate, transform setup() into settings() and (maybe) setup() + + This mimics the Processing functionality to allow users to put calls to + size() in the setup() method instead of settings(), where truthfully it + belongs. The Processing IDE will do some code transformation before Sketch + execution to adjust the code and make it seem like the call to size() can + be in setup(). This does the same thing. + + This only works for module mode and imported mode. + """ + # return if there is nothing to do + if 'settings' in functions or 'setup' not in functions: + return functions + + try: + setup = functions['setup'] + cutoff = find_cutoff(inspect.getsource(setup).strip(), mode) + + # build the fake code + lines, lineno = inspect.getsourcelines(setup) + filename = inspect.getfile(setup) + fake_settings_code = ( + lineno - 1) * '\n' + "def _py5_faux_settings():\n" + ''.join(lines[1:cutoff]) + fake_setup_code = (lineno - 1) * '\n' + "def _py5_faux_setup():\n" + \ + (cutoff - 1) * '\n' + ''.join(lines[cutoff:]) + + # if the fake settings code is empty, there's no need to change + # anything + if count_noncomment_lines(fake_settings_code) > 1: + # parse the fake settings code and transform it if using imported + # mode + fake_settings_ast = ast.parse( + fake_settings_code, filename=filename, mode='exec') + if mode == 'imported': + fake_settings_ast = parsing.transform_py5_code( + fake_settings_ast) + # compile the fake code + exec( + compile( + fake_settings_ast, + filename=filename, + mode='exec'), + sketch_globals, + sketch_locals) + # extract the results and cleanup + functions['settings'] = sketch_locals['_py5_faux_settings'] + del sketch_globals['_py5_faux_settings'] + + # if the fake setup code is empty, get rid of it. otherwise, + # compile it + if count_noncomment_lines(fake_setup_code) == 1: + del functions['setup'] + else: + # parse the fake setup code and transform it if using imported + # mode + fake_setup_ast = ast.parse( + fake_setup_code, filename=filename, mode='exec') + if mode == 'imported': + fake_setup_ast = parsing.transform_py5_code(fake_setup_ast) + # compile the fake code + exec( + compile( + fake_setup_ast, + filename=filename, + mode='exec'), + sketch_globals, + sketch_locals) + # extract the results and cleanup + functions['setup'] = sketch_locals['_py5_faux_setup'] + del sketch_globals['_py5_faux_setup'] + + except OSError as e: + println( + "Unable to obtain source code for setup(). Either make it obtainable or create a settings() function for calls to size(), fullscreen(), etc.", + stderr=True) + except Exception as e: + println( + "Exception thrown while analyzing setup() function:", + str(e), + stderr=True) + + return functions diff --git a/py5_tools/testing.py b/py5_tools/testing.py index 8e1c5cc..9ab2f7c 100644 --- a/py5_tools/testing.py +++ b/py5_tools/testing.py @@ -21,14 +21,14 @@ _DRAW_WRAPPER_CODE_TEMPLATE = """ -if _PY5_HAS_DRAW_: +if {1}: draw_ = draw def draw(): - if _PY5_HAS_DRAW_: + if {1}: draw_() - if _PY5_SAVE_FRAME_: + if {2}: py5.save_frame("{0}", use_thread=False) py5.exit_sketch() """ @@ -53,15 +53,19 @@ def run_code(code: str, image: Path) -> bool: import py5 ns = dict(py5=py5) - exec("py5.reset_py5()", ns) - exec(code, ns) - ns['_PY5_HAS_DRAW_'] = 'draw' in ns - ns['_PY5_SAVE_FRAME_'] = image is not None + code = 'py5.reset_py5()\n\n' + code + '\n\n' if code.find("py5.run_sketch") >= 0: - exec(_EXIT_SKETCH, ns) + code += _EXIT_SKETCH else: - exec(_DRAW_WRAPPER_CODE_TEMPLATE.format(image), ns) - exec(_RUN_SKETCH_CODE, ns) + code += _DRAW_WRAPPER_CODE_TEMPLATE.format(image, + code.find("def draw():") >= 0, + image is not None) + '\n\n' + _RUN_SKETCH_CODE + + # writing code to file so inspect.getsource() works correctly + with open('/tmp/test_file.py', 'w') as f: + f.write(code) + + exec(compile(code, filename='/tmp/test_file.py', mode="exec"), ns) return not py5.is_dead_from_error diff --git a/py5_tools/tools/py5utils.py b/py5_tools/tools/py5utils.py index f0ffa70..3d93a0e 100644 --- a/py5_tools/tools/py5utils.py +++ b/py5_tools/tools/py5utils.py @@ -19,7 +19,7 @@ # ***************************************************************************** import argparse -import py5_tools +import py5_tools.utilities parser = argparse.ArgumentParser(description="Generate Py5Utilities framework") diff --git a/py5_tools/utilities.py b/py5_tools/utilities.py index ebb2587..85627a2 100644 --- a/py5_tools/utilities.py +++ b/py5_tools/utilities.py @@ -77,35 +77,40 @@ class Py5Utilities { """ BUILD_XML_TEMPLATE = """ - + compile and build the py5 utilities jar. - - - + + + + + + + + - - + + + - + - + - - - + + @@ -120,8 +125,8 @@ def generate_utilities_framework(output_dir=None, jars_dir=None): py5_classpath = Path(py5.__file__).parent / 'jars' template_params = { - x: f'{chr(36)}{{{x}}}' for x in [ - 'build', 'dist', 'src']} + x: f'{chr(36)}{{project.{x}}}' for x in [ + 'bin', 'dist', 'src']} template_params['path'] = py5_classpath.as_posix() template_params['jars'] = ant_build_path.absolute( ).as_posix() if output_dir else ant_build_path.as_posix() diff --git a/setup.py b/setup.py index e6c04e2..90b08ae 100644 --- a/setup.py +++ b/setup.py @@ -23,9 +23,12 @@ with open('README.rst') as f: README = f.read() -VERSION = '0.4a2' +VERSION = '0.5a0' INSTALL_REQUIRES = [ + 'ipykernel>=5.3', + 'ipython>=7.22', + 'ipywidgets>=7.6', 'jpype1>=1.2', 'line_profiler>=2.1.2', 'noise>=1.2', @@ -35,6 +38,7 @@ 'pillow>=8.1', 'requests>=2.25', 'stackprinter>=0.2.4', + 'traitlets>=5.0', ] pjoin = os.path.join @@ -53,7 +57,7 @@ py_modules=['setup'], package_data={ "py5": ['jars/*.jar', 'jars/*/*.jar'], - "py5_tools": ['kernel/resources/*.png'], + "py5_tools": ['kernel/resources/*.png', 'py5bot/resources/*.png'], }, python_requires='>3.8', install_requires=INSTALL_REQUIRES,