From 1eedb235d4e691470b9765176a6d0404c151aef7 Mon Sep 17 00:00:00 2001 From: Jim Date: Mon, 21 Nov 2022 17:54:52 -0500 Subject: [PATCH] release 0.8.3a1 --- README.md | 5 +- py5/__init__.py | 424 +++++++++++++++++++++++++-------- py5/bridge.py | 9 + py5/graphics.py | 33 ++- py5/jars/py5.jar | Bin 34259 -> 34263 bytes py5/mixins/data.py | 263 +++++++++++++++++++- py5/mixins/threads.py | 33 ++- py5/reference.py | 15 +- py5/sketch.py | 62 ++++- py5_tools/__init__.py | 2 +- py5_tools/hooks/frame_hooks.py | 14 +- py5_tools/imported.py | 10 +- py5_tools/kernel/kernel.py | 10 +- py5_tools/py5bot/kernel.py | 2 +- py5_tools/reference.py | 16 ++ py5_tools/split_setup.py | 4 +- py5_tools/utilities.py | 14 +- setup.py | 6 +- 18 files changed, 765 insertions(+), 157 deletions(-) diff --git a/README.md b/README.md index 70476f3..a79bc7f 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ If you have Java 17 installed on your computer, you can install py5 using pip: pip install py5 ``` -[Detailed installation instructions](https://py5.ixora.io/content/install.html) are available on the documentation website. There are some [Special Notes for Mac Users](https://py5.ixora.io/content/osx_users.html) that you should read if you use OSX. +[Detailed installation instructions](https://py5coding.org/content/install.html) are available on the documentation website. There are some [Special Notes for Mac Users](https://py5coding.org/content/osx_users.html) that you should read if you use OSX. ## Getting Started @@ -53,7 +53,7 @@ There are currently four basic ways to use py5. They are: * **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, the `%%py5bot` IPython magic, and the `run_sketch` command line utility. -The documentation website, [https://py5.ixora.io/](https://py5.ixora.io/), is a work in progress. The reference documentation is solid but the how-to's and tutorials are incomplete. +The documentation website, [https://py5coding.org/](https://py5coding.org/), is a work in progress. The reference documentation is solid but the how-to's and tutorials are incomplete. [py5generator][py5_generator_repo] is a meta-programming project that creates the py5 library. To view the actual installed py5 library code, look at the [py5 repository][py5_repo]. All py5 library development is done through py5generator. @@ -62,6 +62,7 @@ The documentation website, [https://py5.ixora.io/](https://py5.ixora.io/), is a Have a comment or question? We'd love to hear from you! The best ways to reach out are: * github [discussions](https://github.com/py5coding/py5generator/discussions) and [issues](https://github.com/py5coding/py5generator/issues) +* Mastodon fosstodon.org/@py5coding * twitter [@py5coding](https://twitter.com/py5coding) * [processing foundation discourse](https://discourse.processing.org/) diff --git a/py5/__init__.py b/py5/__init__.py index b0c15d6..ba9c52c 100644 --- a/py5/__init__.py +++ b/py5/__init__.py @@ -88,7 +88,7 @@ pass -__version__ = '0.8.2a1' +__version__ = '0.8.3a1' _PY5_USE_IMPORTED_MODE = py5_tools.get_imported_mode() py5_tools._lock_imported_mode() @@ -140,7 +140,6 @@ CROSS = 1 CURVE_VERTEX = 3 DARKEST = 16 -DEG_TO_RAD = 0.017453292 DELETE = '\u007f' DIAMETER = 3 DIFFERENCE = 32 @@ -184,7 +183,6 @@ FX2D = "processing.javafx.PGraphicsFX2D" GRAY = 12 GROUP = 0 -HALF_PI = 1.5707964 HAND = 12 HARD_LIGHT = 1024 HIDDEN = "py5.core.graphics.HiddenPy5GraphicsJava2D" @@ -215,7 +213,6 @@ P3D = "processing.opengl.PGraphics3D" PATH = 21 PDF = "processing.pdf.PGraphicsPDF" -PI = 3.1415927 PIE = 3 POINT = 2 POINTS = 3 @@ -227,9 +224,7 @@ QUADS = 17 QUAD_BEZIER_VERTEX = 2 QUAD_STRIP = 18 -QUARTER_PI = 0.7853982 RADIUS = 2 -RAD_TO_DEG = 57.295776 RECT = 30 REPEAT = 1 REPLACE = 0 @@ -248,16 +243,13 @@ SUBTRACT = 4 SVG = "processing.svg.PGraphicsSVG" TAB = '\t' -TAU = 6.2831855 TEXT = 2 -THIRD_PI = 1.0471976 THRESHOLD = 16 TOP = 101 TRIANGLE = 8 TRIANGLES = 9 TRIANGLE_FAN = 11 TRIANGLE_STRIP = 10 -TWO_PI = 6.2831855 UP = 38 VERTEX = 0 WAIT = 3 @@ -7766,6 +7758,21 @@ def apply_filter(*args): return _py5sketch.apply_filter(*args) +def flush() -> None: + """Flush drawing commands to the renderer. + + Underlying Processing method: Sketch.flush + + Notes + ----- + + Flush drawing commands to the renderer. For most renderers, this method does + absolutely nothing. There are not a lot of good reasons to use this method, but + if you need it, it is available for your use. + """ + return _py5sketch.flush() + + def frame_rate(fps: float, /) -> None: """Specifies the number of frames to be displayed every second. @@ -17445,94 +17452,6 @@ def year() -> int: """ return Sketch.year() -############################################################################## -# module functions from data.py -############################################################################## - - -def load_json(json_path: Union[str, Path], **kwargs: dict[str, Any]) -> Any: - """Load a JSON data file from a file or URL. - - Parameters - ---------- - - json_path: Union[str, Path] - url or file path for JSON data file - - kwargs: dict[str, Any] - keyword arguments - - Notes - ----- - - Load a JSON data file from a file or URL. When loading a file, the path can be - in the data directory, relative to the current working directory - (``sketch_path()``), or an absolute path. When loading from a URL, the - ``json_path`` parameter must start with ``http://`` or ``https://``. - - When loading JSON data from a URL, the data is retrieved using the Python - requests library with the ``get`` method, and the ``kwargs`` parameter is passed - along to that method. When loading JSON data from a file, the data is loaded - using the Python json library with the ``load`` method, and again the ``kwargs`` - parameter passed along to that method. - """ - return _py5sketch.load_json(json_path, **kwargs) - - -def save_json(json_data: Any, - filename: Union[str, - Path], - **kwargs: dict[str, - Any]) -> None: - """Save JSON data to a file. - - Parameters - ---------- - - filename: Union[str, Path] - filename to save JSON data object to - - json_data: Any - json data object - - kwargs: dict[str, Any] - keyword arguments - - Notes - ----- - - Save JSON data to a file. If ``filename`` is not an absolute path, it will be - saved relative to the current working directory (``sketch_path()``). - - The JSON data is saved using the Python json library with the ``dump`` method, - and the ``kwargs`` parameter is passed along to that method. - """ - return _py5sketch.save_json(json_data, filename, **kwargs) - - -def parse_json(serialized_json: Any, **kwargs: dict[str, Any]) -> Any: - """Parse serialized JSON data from a string. - - Parameters - ---------- - - kwargs: dict[str, Any] - keyword arguments - - serialized_json: Any - JSON data object that has been serialized as a string - - Notes - ----- - - Parse serialized JSON data from a string. When reading JSON data from a file, - ``load_json()`` is the better choice. - - The JSON data is parsed using the Python json library with the ``loads`` method, - and the ``kwargs`` parameter is passed along to that method. - """ - return Sketch.parse_json(serialized_json, **kwargs) - ############################################################################## # module functions from math.py ############################################################################## @@ -19865,6 +19784,273 @@ def println( """ return _py5sketch.println(*args, sep=sep, end=end, stderr=stderr) +############################################################################## +# module functions from data.py +############################################################################## + + +def load_json(json_path: Union[str, Path], **kwargs: dict[str, Any]) -> Any: + """Load a JSON data file from a file or URL. + + Parameters + ---------- + + json_path: Union[str, Path] + url or file path for JSON data file + + kwargs: dict[str, Any] + keyword arguments + + Notes + ----- + + Load a JSON data file from a file or URL. When loading a file, the path can be + in the data directory, relative to the current working directory + (``sketch_path()``), or an absolute path. When loading from a URL, the + ``json_path`` parameter must start with ``http://`` or ``https://``. + + When loading JSON data from a URL, the data is retrieved using the Python + requests library with the ``get`` method, and any extra keyword arguments (the + ``kwargs`` parameter) are passed along to that method. When loading JSON data + from a file, the data is loaded using the Python json library with the ``load`` + method, and again any extra keyword arguments are passed along to that method. + """ + return _py5sketch.load_json(json_path, **kwargs) + + +def save_json(json_data: Any, + filename: Union[str, + Path], + **kwargs: dict[str, + Any]) -> None: + """Save JSON data to a file. + + Parameters + ---------- + + filename: Union[str, Path] + filename to save JSON data object to + + json_data: Any + json data object + + kwargs: dict[str, Any] + keyword arguments + + Notes + ----- + + Save JSON data to a file. If ``filename`` is not an absolute path, it will be + saved relative to the current working directory (``sketch_path()``). The saved + file can be reloaded with ``load_json()``. + + The JSON data is saved using the Python json library with the ``dump`` method, + and the ``kwargs`` parameter is passed along to that method. + """ + return _py5sketch.save_json(json_data, filename, **kwargs) + + +def parse_json(serialized_json: Any, **kwargs: dict[str, Any]) -> Any: + """Parse serialized JSON data from a string. + + Parameters + ---------- + + kwargs: dict[str, Any] + keyword arguments + + serialized_json: Any + JSON data object that has been serialized as a string + + Notes + ----- + + Parse serialized JSON data from a string. When reading JSON data from a file, + ``load_json()`` is the better choice. + + The JSON data is parsed using the Python json library with the ``loads`` method, + and the ``kwargs`` parameter is passed along to that method. + """ + return Sketch.parse_json(serialized_json, **kwargs) + + +def load_strings(string_path: Union[str, Path], + **kwargs: dict[str, Any]) -> list[str]: + """Load a list of strings from a file or URL. + + Underlying Processing method: Sketch.loadStrings + + Parameters + ---------- + + kwargs: dict[str, Any] + keyword arguments + + string_path: Union[str, Path] + url or file path for string data file + + Notes + ----- + + Load a list of strings from a file or URL. When loading a file, the path can be + in the data directory, relative to the current working directory + (``sketch_path()``), or an absolute path. When loading from a URL, the + ``string_path`` parameter must start with ``http://`` or ``https://``. + + When loading string data from a URL, the data is retrieved using the Python + requests library with the ``get`` method, and any extra keyword arguments (the + ``kwargs`` parameter) are passed along to that method. When loading string data + from a file, the ``kwargs`` parameter is not used. + """ + return _py5sketch.load_strings(string_path, **kwargs) + + +def save_strings(string_data: list[str], + filename: Union[str, + Path], + *, + end: str = '\n') -> None: + """Save a list of strings to a file. + + Underlying Processing method: Sketch.saveStrings + + Parameters + ---------- + + end: str = '\\n' + line terminator for each string + + filename: Union[str, Path] + filename to save string data to + + string_data: list[str] + string data to save in a file + + Notes + ----- + + Save a list of strings to a file. If ``filename`` is not an absolute path, it + will be saved relative to the current working directory (``sketch_path()``). If + the contents of the list are not already strings, it will be converted to + strings with the Python builtin ``str``. The saved file can be reloaded with + ``load_strings()``. + + Use the ``end`` parameter to set the line terminator for each string in the + list. If items in the list of strings already have line terminators, set the + ``end`` parameter to ``''`` to keep the output file from being saved with a + blank line after each item. + """ + return _py5sketch.save_strings(string_data, filename, end=end) + + +def load_bytes(bytes_path: Union[str, Path], ** + kwargs: dict[str, Any]) -> bytearray: + """Load byte data from a file or URL. + + Underlying Processing method: Sketch.loadBytes + + Parameters + ---------- + + bytes_path: Union[str, Path] + url or file path for bytes data file + + kwargs: dict[str, Any] + keyword arguments + + Notes + ----- + + Load byte data from a file or URL. When loading a file, the path can be in the + data directory, relative to the current working directory (``sketch_path()``), + or an absolute path. When loading from a URL, the ``bytes_path`` parameter must + start with ``http://`` or ``https://``. + + When loading byte data from a URL, the data is retrieved using the Python + requests library with the ``get`` method, and any extra keyword arguments (the + ``kwargs`` parameter) are passed along to that method. When loading byte data + from a file, the ``kwargs`` parameter is not used. + """ + return _py5sketch.load_bytes(bytes_path, **kwargs) + + +def save_bytes(bytes_data: Union[bytes, bytearray], + filename: Union[str, Path]) -> None: + """Save byte data to a file. + + Underlying Processing method: Sketch.saveBytes + + Parameters + ---------- + + bytes_data: Union[bytes, bytearray] + byte data to save in a file + + filename: Union[str, Path] + filename to save byte data to + + Notes + ----- + + Save byte data to a file. If ``filename`` is not an absolute path, it will be + saved relative to the current working directory (``sketch_path()``). The saved + file can be reloaded with ``load_bytes()``. + """ + return _py5sketch.save_bytes(bytes_data, filename) + + +def load_pickle(pickle_path: Union[str, Path]) -> Any: + """Load a pickled Python object from a file. + + Underlying Processing method: Sketch.loadPickle + + Parameters + ---------- + + pickle_path: Union[str, Path] + file path for pickle object file + + Notes + ----- + + Load a pickled Python object from a file. The path can be in the data directory, + relative to the current working directory (``sketch_path()``), or an absolute + path. + + There are security risks associated with Python pickle files. A pickle file can + contain malicious code, so never load a pickle file from an untrusted source. + """ + return _py5sketch.load_pickle(pickle_path) + + +def save_pickle(obj: Any, filename: Union[str, Path]) -> None: + """Pickle a Python object to a file. + + Underlying Processing method: Sketch.savePickle + + Parameters + ---------- + + filename: Union[str, Path] + filename to save pickled object to + + obj: Any + any non-py5 Python object + + Notes + ----- + + Pickle a Python object to a file. If ``filename`` is not an absolute path, it + will be saved relative to the current working directory (``sketch_path()``). The + saved file can be reloaded with ``load_pickle()``. + + Object "pickling" is a method for serializing objects and saving them to a file + for later retrieval. The recreated objects will be clones of the original + objects. Not all Python objects can be saved to a Python pickle file. This + limitation prevents any py5 object from being pickled. + """ + return _py5sketch.save_pickle(obj, filename) + ############################################################################## # module functions from threads.py ############################################################################## @@ -20083,6 +20269,36 @@ def has_thread(name: str) -> None: return _py5sketch.has_thread(name) +def join_thread(name: str, *, timeout: float = None) -> bool: + """Join the Python thread associated with the given thread name. + + Parameters + ---------- + + name: str + name of thread + + timeout: float = None + maximum time in seconds to wait for the thread to join + + Notes + ----- + + Join the Python thread associated with the given thread name. The + ``join_thread()`` method will wait until the named thread has finished executing + before returning. Use the ``timeout`` parameter to set an upper limit for the + number of seconds to wait. This method will return right away if the named + thread does not exist or the thread has already finished executing. You can get + the list of all currently running threads with ``list_threads()``. + + This method will return ``True`` if the named thread has completed execution and + ``False`` if the named thread is still executing. It will only return ``False`` + if you use the ``timeout`` parameter and the method is not able to join with the + thread within that time limit. + """ + return _py5sketch.join_thread(name, timeout=timeout) + + def stop_thread(name: str, wait: bool = False) -> None: """Stop a thread of a given name. @@ -20150,6 +20366,15 @@ def list_threads() -> None: """ return _py5sketch.list_threads() + +PI = np.pi +HALF_PI = np.pi / 2 +THIRD_PI = np.pi / 3 +QUARTER_PI = np.pi / 4 +TWO_PI = 2 * np.pi +TAU = 2 * np.pi +RAD_TO_DEG = 180 / np.pi +DEG_TO_RAD = np.pi / 180 ############################################################################## # module functions from sketch.py ############################################################################## @@ -20752,10 +20977,11 @@ def run_sketch(block: bool = None, *, println, mode='imported' if _PY5_USE_IMPORTED_MODE else 'module') - if not set(functions.keys()) & set(['settings', 'setup', 'draw']): + if not set(functions.keys()) & set( + ['settings', 'setup', 'draw']) and not _jclassname: warnings.warn( ("Unable to find settings, setup, or draw functions. " - "Your sketch will be a small boring gray square. " + "Your sketch will be a small gray square. " "If that isn't what you intended, you need to make sure " "your implementation of those functions are available in " "the local namespace that made the `run_sketch()` call."), stacklevel=2) diff --git a/py5/bridge.py b/py5/bridge.py index 0c0450d..3faefcd 100644 --- a/py5/bridge.py +++ b/py5/bridge.py @@ -171,6 +171,7 @@ def __init__(self, sketch): self._pre_hooks = defaultdict(dict) self._post_hooks = defaultdict(dict) self._profiler = line_profiler.LineProfiler() + self._current_running_method = None self._is_terminated = False from .java_conversion import convert_to_python_types @@ -255,6 +256,8 @@ def terminate_sketch(self): def run_method(self, method_name, params): try: if method_name in self._functions: + self._current_running_method = method_name + # first run the pre-hooks, if any if method_name in self._pre_hooks: for hook in list(self._pre_hooks[method_name].values()): @@ -273,6 +276,12 @@ def run_method(self, method_name, params): handle_exception(self._sketch.println, *sys.exc_info()) self.terminate_sketch() return False + finally: + self._current_running_method = None + + def _get_current_running_method(self): + return self._current_running_method + current_running_method = property(fget=_get_current_running_method) @JOverride def call_function(self, key, params): diff --git a/py5/graphics.py b/py5/graphics.py index 9a075fd..eacbbfe 100644 --- a/py5/graphics.py +++ b/py5/graphics.py @@ -89,6 +89,15 @@ class Py5Graphics(PixelPy5GraphicsMixin, Py5Base): """ _py5_object_cache = weakref.WeakSet() + PI = np.pi + HALF_PI = np.pi / 2 + THIRD_PI = np.pi / 3 + QUARTER_PI = np.pi / 4 + TWO_PI = 2 * np.pi + TAU = 2 * np.pi + RAD_TO_DEG = 180 / np.pi + DEG_TO_RAD = np.pi / 180 + def __new__(cls, pgraphics): for o in cls._py5_object_cache: if pgraphics == o._instance: @@ -172,7 +181,6 @@ def quadratic_vertices(self, coordinates): CROSS = 1 CURVE_VERTEX = 3 DARKEST = 16 - DEG_TO_RAD = 0.017453292 DELETE = '\u007f' DIAMETER = 3 DIFFERENCE = 32 @@ -215,7 +223,6 @@ def quadratic_vertices(self, coordinates): GRAY = 12 GREEN_MASK = 65280 GROUP = 0 - HALF_PI = 1.5707964 HAND = 12 HARD_LIGHT = 1024 HSB = 3 @@ -245,7 +252,6 @@ def quadratic_vertices(self, coordinates): P3D = "processing.opengl.PGraphics3D" PATH = 21 PDF = "processing.pdf.PGraphicsPDF" - PI = 3.1415927 PIE = 3 POINT = 2 POINTS = 3 @@ -257,9 +263,7 @@ def quadratic_vertices(self, coordinates): QUADS = 17 QUAD_BEZIER_VERTEX = 2 QUAD_STRIP = 18 - QUARTER_PI = 0.7853982 RADIUS = 2 - RAD_TO_DEG = 57.295776 RECT = 30 RED_MASK = 16711680 REPEAT = 1 @@ -279,16 +283,13 @@ def quadratic_vertices(self, coordinates): SUBTRACT = 4 SVG = "processing.svg.PGraphicsSVG" TAB = '\t' - TAU = 6.2831855 TEXT = 2 - THIRD_PI = 1.0471976 THRESHOLD = 16 TOP = 101 TRIANGLE = 8 TRIANGLES = 9 TRIANGLE_FAN = 11 TRIANGLE_STRIP = 10 - TWO_PI = 6.2831855 UP = 38 VERTEX = 0 WAIT = 3 @@ -6923,6 +6924,22 @@ def apply_filter(self, *args): """ return self._instance.filter(*args) + def flush(self) -> None: + """Flush drawing commands to the renderer. + + Underlying Processing method: PGraphics.flush + + Notes + ----- + + Flush drawing commands to the renderer. For most renderers, this method does + absolutely nothing. There are not a lot of good reasons to use this method, but + if you need it, it is available for your use. + + This method is the same as ``flush()`` but linked to a ``Py5Graphics`` object. + """ + return self._instance.flush() + def frustum(self, left: float, right: float, bottom: float, top: float, near: float, far: float, /) -> None: """Sets a perspective matrix as defined by the parameters. diff --git a/py5/jars/py5.jar b/py5/jars/py5.jar index 9a17ba11009b504a8b729bbb8c904ca309efb569..be424b98bfa5fbd9150259294ac356c7773aa644 100644 GIT binary patch delta 10079 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 Any: The JSON data is parsed using the Python json library with the ``loads`` method, and the ``kwargs`` parameter is passed along to that method.""" return json.loads(serialized_json, **kwargs) + + def load_strings( + self, string_path: Union[str, Path], **kwargs: dict[str, Any]) -> list[str]: + """Load a list of strings from a file or URL. + + Underlying Processing method: Sketch.loadStrings + + Parameters + ---------- + + kwargs: dict[str, Any] + keyword arguments + + string_path: Union[str, Path] + url or file path for string data file + + Notes + ----- + + Load a list of strings from a file or URL. When loading a file, the path can be + in the data directory, relative to the current working directory + (``sketch_path()``), or an absolute path. When loading from a URL, the + ``string_path`` parameter must start with ``http://`` or ``https://``. + + When loading string data from a URL, the data is retrieved using the Python + requests library with the ``get`` method, and any extra keyword arguments (the + ``kwargs`` parameter) are passed along to that method. When loading string data + from a file, the ``kwargs`` parameter is not used.""" + if isinstance( + string_path, + str) and re.match( + r'https?://', + string_path.lower()): + response = requests.get(string_path, **kwargs) + if response.status_code == 200: + return response.text.splitlines() + else: + raise RuntimeError( + 'Unable to download URL: ' + + response.reason) + else: + path = Path(string_path) + if not path.is_absolute(): + cwd = self.sketch_path() + if (cwd / 'data' / string_path).exists(): + path = cwd / 'data' / string_path + else: + path = cwd / string_path + if path.exists(): + with open(path, 'r', encoding='utf8') as f: + return f.read().splitlines() + else: + raise RuntimeError('Unable to find file ' + str(string_path)) + + def save_strings(self, + string_data: list[str], + filename: Union[str, + Path], + *, + end: str = '\n') -> None: + """Save a list of strings to a file. + + Underlying Processing method: Sketch.saveStrings + + Parameters + ---------- + + end: str = '\\n' + line terminator for each string + + filename: Union[str, Path] + filename to save string data to + + string_data: list[str] + string data to save in a file + + Notes + ----- + + Save a list of strings to a file. If ``filename`` is not an absolute path, it + will be saved relative to the current working directory (``sketch_path()``). If + the contents of the list are not already strings, it will be converted to + strings with the Python builtin ``str``. The saved file can be reloaded with + ``load_strings()``. + + Use the ``end`` parameter to set the line terminator for each string in the + list. If items in the list of strings already have line terminators, set the + ``end`` parameter to ``''`` to keep the output file from being saved with a + blank line after each item.""" + path = Path(filename) + if not path.is_absolute(): + path = self.sketch_path() / filename + if not path.parent.exists(): + path.parent.mkdir(parents=True) + with open(path, 'w') as f: + f.write(end.join(str(s) for s in string_data)) + + def load_bytes( + self, bytes_path: Union[str, Path], **kwargs: dict[str, Any]) -> bytearray: + """Load byte data from a file or URL. + + Underlying Processing method: Sketch.loadBytes + + Parameters + ---------- + + bytes_path: Union[str, Path] + url or file path for bytes data file + + kwargs: dict[str, Any] + keyword arguments + + Notes + ----- + + Load byte data from a file or URL. When loading a file, the path can be in the + data directory, relative to the current working directory (``sketch_path()``), + or an absolute path. When loading from a URL, the ``bytes_path`` parameter must + start with ``http://`` or ``https://``. + + When loading byte data from a URL, the data is retrieved using the Python + requests library with the ``get`` method, and any extra keyword arguments (the + ``kwargs`` parameter) are passed along to that method. When loading byte data + from a file, the ``kwargs`` parameter is not used.""" + if isinstance( + bytes_path, + str) and re.match( + r'https?://', + bytes_path.lower()): + response = requests.get(bytes_path, **kwargs) + if response.status_code == 200: + return bytearray(response.content) + else: + raise RuntimeError( + 'Unable to download URL: ' + + response.reason) + else: + path = Path(bytes_path) + if not path.is_absolute(): + cwd = self.sketch_path() + if (cwd / 'data' / bytes_path).exists(): + path = cwd / 'data' / bytes_path + else: + path = cwd / bytes_path + if path.exists(): + with open(path, 'rb') as f: + return bytearray(f.read()) + else: + raise RuntimeError('Unable to find file ' + str(bytes_path)) + + def save_bytes(self, + bytes_data: Union[bytes, + bytearray], + filename: Union[str, + Path]) -> None: + """Save byte data to a file. + + Underlying Processing method: Sketch.saveBytes + + Parameters + ---------- + + bytes_data: Union[bytes, bytearray] + byte data to save in a file + + filename: Union[str, Path] + filename to save byte data to + + Notes + ----- + + Save byte data to a file. If ``filename`` is not an absolute path, it will be + saved relative to the current working directory (``sketch_path()``). The saved + file can be reloaded with ``load_bytes()``.""" + path = Path(filename) + if not path.is_absolute(): + path = self.sketch_path() / filename + if not path.parent.exists(): + path.parent.mkdir(parents=True) + with open(path, 'wb') as f: + f.write(bytes_data) + + def load_pickle(self, pickle_path: Union[str, Path]) -> Any: + """Load a pickled Python object from a file. + + Underlying Processing method: Sketch.loadPickle + + Parameters + ---------- + + pickle_path: Union[str, Path] + file path for pickle object file + + Notes + ----- + + Load a pickled Python object from a file. The path can be in the data directory, + relative to the current working directory (``sketch_path()``), or an absolute + path. + + There are security risks associated with Python pickle files. A pickle file can + contain malicious code, so never load a pickle file from an untrusted source.""" + path = Path(pickle_path) + if not path.is_absolute(): + cwd = self.sketch_path() + if (cwd / 'data' / pickle_path).exists(): + path = cwd / 'data' / pickle_path + else: + path = cwd / pickle_path + if path.exists(): + with open(path, 'rb') as f: + return pickle.load(f) + else: + raise RuntimeError('Unable to find file ' + str(pickle_path)) + + def save_pickle(self, obj: Any, filename: Union[str, Path]) -> None: + """Pickle a Python object to a file. + + Underlying Processing method: Sketch.savePickle + + Parameters + ---------- + + filename: Union[str, Path] + filename to save pickled object to + + obj: Any + any non-py5 Python object + + Notes + ----- + + Pickle a Python object to a file. If ``filename`` is not an absolute path, it + will be saved relative to the current working directory (``sketch_path()``). The + saved file can be reloaded with ``load_pickle()``. + + Object "pickling" is a method for serializing objects and saving them to a file + for later retrieval. The recreated objects will be clones of the original + objects. Not all Python objects can be saved to a Python pickle file. This + limitation prevents any py5 object from being pickled.""" + path = Path(filename) + if not path.is_absolute(): + path = self.sketch_path() / filename + if not path.parent.exists(): + path.parent.mkdir(parents=True) + with open(path, 'wb') as f: + pickle.dump(obj, f) diff --git a/py5/mixins/threads.py b/py5/mixins/threads.py index da405d1..8daa138 100644 --- a/py5/mixins/threads.py +++ b/py5/mixins/threads.py @@ -23,7 +23,7 @@ import time import threading from collections.abc import Iterable -from typing import Callable, Any +from typing import Callable, Any, Union from .. import bridge @@ -365,6 +365,37 @@ def has_thread(self, name: str) -> None: self._remove_dead_threads() return name in self._py5threads + def join_thread(self, name: str, *, timeout: float = None) -> bool: + """Join the Python thread associated with the given thread name. + + Parameters + ---------- + + name: str + name of thread + + timeout: float = None + maximum time in seconds to wait for the thread to join + + Notes + ----- + + Join the Python thread associated with the given thread name. The + ``join_thread()`` method will wait until the named thread has finished executing + before returning. Use the ``timeout`` parameter to set an upper limit for the + number of seconds to wait. This method will return right away if the named + thread does not exist or the thread has already finished executing. You can get + the list of all currently running threads with ``list_threads()``. + + This method will return ``True`` if the named thread has completed execution and + ``False`` if the named thread is still executing. It will only return ``False`` + if you use the ``timeout`` parameter and the method is not able to join with the + thread within that time limit.""" + self._remove_dead_threads() + if name in self._py5threads: + self._py5threads[name][0].join(timeout) + return not self.has_thread(name) + def stop_thread(self, name: str, wait: bool = False) -> None: """Stop a thread of a given name. diff --git a/py5/reference.py b/py5/reference.py index 65cb9dd..8474c05 100644 --- a/py5/reference.py +++ b/py5/reference.py @@ -116,6 +116,7 @@ (('Sketch', 'exit_sketch'), ['() -> None']), (('Sketch', 'fill'), ['(gray: float, /) -> None', '(gray: float, alpha: float, /) -> None', '(v1: float, v2: float, v3: float, /) -> None', '(v1: float, v2: float, v3: float, alpha: float, /) -> None', '(rgb: int, /) -> None', '(rgb: int, alpha: float, /) -> None']), (('Sketch', 'apply_filter'), ['(kind: int, /) -> None', '(kind: int, param: float, /) -> None', '(shader: Py5Shader, /) -> None']), + (('Sketch', 'flush'), ['() -> None']), (('Sketch', 'frame_rate'), ['(fps: float, /) -> None']), (('Sketch', 'frustum'), ['(left: float, right: float, bottom: float, top: float, near: float, far: float, /) -> None']), (('Sketch', 'full_screen'), ['() -> None', '(display: int, /) -> None', '(renderer: str, /) -> None', '(renderer: str, display: int, /) -> None']), @@ -235,9 +236,6 @@ (('Sketch', 'window_resize'), ['(new_width: int, new_height: int, /) -> None']), (('Sketch', 'window_title'), ['(title: str, /) -> None']), (('Sketch', 'year'), ['() -> int']), - (('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', 'hex_color'), ['(color: int) -> str']), (('Sketch', 'sin'), ['(angle: Union[float, npt.ArrayLike]) -> Union[float, npt.NDArray]']), (('Sketch', 'cos'), ['(angle: Union[float, npt.ArrayLike]) -> Union[float, npt.NDArray]']), @@ -273,10 +271,20 @@ (('Sketch', 'save'), ['(filename: Union[str, Path, BytesIO], *, format: str = None, drop_alpha: bool = True, use_thread: bool = False, **params) -> None']), (('Sketch', 'set_println_stream'), ['(println_stream: Any) -> None']), (('Sketch', 'println'), ["(*args, sep: str = ' ', end: str = '\\n', stderr: bool = False) -> None"]), + (('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', 'load_strings'), ['(string_path: Union[str, Path], **kwargs: dict[str, Any]) -> list[str]']), + (('Sketch', 'save_strings'), ["(string_data: list[str], filename: Union[str, Path], *, end: str = '\\n') -> None"]), + (('Sketch', 'load_bytes'), ['(bytes_path: Union[str, Path], **kwargs: dict[str, Any]) -> bytearray']), + (('Sketch', 'save_bytes'), ['(bytes_data: Union[bytes, bytearray], filename: Union[str, Path]) -> None']), + (('Sketch', 'load_pickle'), ['(pickle_path: Union[str, Path]) -> Any']), + (('Sketch', 'save_pickle'), ['(obj: Any, filename: Union[str, Path]) -> 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']), (('Sketch', 'has_thread'), ['(name: str) -> None']), + (('Sketch', 'join_thread'), ['(name: str, *, timeout: float = None) -> bool']), (('Sketch', 'stop_thread'), ['(name: str, wait: bool = False) -> None']), (('Sketch', 'stop_all_threads'), ['(wait: bool = False) -> None']), (('Sketch', 'list_threads'), ['() -> None']), @@ -459,6 +467,7 @@ (('Py5Graphics', 'end_shape'), ['() -> None', '(mode: int, /) -> None']), (('Py5Graphics', 'fill'), ['(gray: float, /) -> None', '(gray: float, alpha: float, /) -> None', '(v1: float, v2: float, v3: float, /) -> None', '(v1: float, v2: float, v3: float, alpha: float, /) -> None', '(rgb: int, /) -> None', '(rgb: int, alpha: float, /) -> None']), (('Py5Graphics', 'apply_filter'), ['(kind: int, /) -> None', '(kind: int, param: float, /) -> None', '(shader: Py5Shader, /) -> None']), + (('Py5Graphics', 'flush'), ['() -> None']), (('Py5Graphics', 'frustum'), ['(left: float, right: float, bottom: float, top: float, near: float, far: float, /) -> None']), (('Py5Graphics', 'get'), ['() -> Py5Image', '(x: int, y: int, /) -> int', '(x: int, y: int, w: int, h: int, /) -> Py5Image']), (('Py5Graphics', 'get_matrix'), ['() -> npt.NDArray[np.floating]', '(target: npt.NDArray[np.floating], /) -> npt.NDArray[np.floating]']), diff --git a/py5/sketch.py b/py5/sketch.py index 62ed4c6..1edf808 100644 --- a/py5/sketch.py +++ b/py5/sketch.py @@ -90,6 +90,21 @@ def decorated(self_, *args): return decorated +def _settings_only(name): + def _decorator(f): + @functools.wraps(f) + def decorated(self_, *args): + if self_._py5_bridge.current_running_method == 'settings': + return f(self_, *args) + else: + raise RuntimeError( + "Cannot call the " + + name + + "() method here. Either move it to a settings() function or move it to closer to the start of setup().") + return decorated + return _decorator + + class Sketch( MathMixin, DataMixin, @@ -193,7 +208,7 @@ def __init__(self, *args, **kwargs): # attempt to instantiate Py5Utilities self.utils = None try: - self.utils = jpype.JClass('py5.utils.Py5Utilities')(self._instance) + self.utils = jpype.JClass('py5utils.Py5Utilities')(self._instance) except Exception: pass @@ -422,6 +437,15 @@ def _remove_post_hook(self, method_name, hook_name): # *** BEGIN METHODS *** + PI = np.pi # CODEBUILDER INCLUDE + HALF_PI = np.pi / 2 # CODEBUILDER INCLUDE + THIRD_PI = np.pi / 3 # CODEBUILDER INCLUDE + QUARTER_PI = np.pi / 4 # CODEBUILDER INCLUDE + TWO_PI = 2 * np.pi # CODEBUILDER INCLUDE + TAU = 2 * np.pi # CODEBUILDER INCLUDE + RAD_TO_DEG = 180 / np.pi # CODEBUILDER INCLUDE + DEG_TO_RAD = np.pi / 180 # CODEBUILDER INCLUDE + @overload def sketch_path(self) -> Path: """The Sketch's current path. @@ -512,13 +536,20 @@ def sketch_path(self, *args) -> Path: Result will be relative to Python's current working directory (``os.getcwd()``) unless it was specifically set to something else with the ``run_sketch()`` call by including a ``--sketch-path`` argument in the ``py5_options`` parameters.""" + if not self.is_running: + msg = ( + "Calling method sketch_path() when Sketch is not running. " + + "The returned value will not be correct on all platforms. Consider " + + "calling this after setup() or perhaps using the Python standard " + + "library methods os.getcwd() or pathlib.Path.cwd().") + warnings.warn(msg) if len(args) <= 1: return Path(str(self._instance.sketchPath(*args))) else: # this exception will be replaced with a more informative one by # the custom exception handler raise TypeError( - 'The parameters are invalid for method sketch_path') + 'The parameters are invalid for method sketch_path()') def _get_is_ready(self) -> bool: """Boolean value reflecting if the Sketch is in the ready state. @@ -1243,7 +1274,6 @@ def request_image(self, image_path: Union[str, Path]) -> Py5Promise: CROSS = 1 CURVE_VERTEX = 3 DARKEST = 16 - DEG_TO_RAD = 0.017453292 DELETE = '\u007f' DIAMETER = 3 DIFFERENCE = 32 @@ -1287,7 +1317,6 @@ def request_image(self, image_path: Union[str, Path]) -> Py5Promise: FX2D = "processing.javafx.PGraphicsFX2D" GRAY = 12 GROUP = 0 - HALF_PI = 1.5707964 HAND = 12 HARD_LIGHT = 1024 HIDDEN = "py5.core.graphics.HiddenPy5GraphicsJava2D" @@ -1318,7 +1347,6 @@ def request_image(self, image_path: Union[str, Path]) -> Py5Promise: P3D = "processing.opengl.PGraphics3D" PATH = 21 PDF = "processing.pdf.PGraphicsPDF" - PI = 3.1415927 PIE = 3 POINT = 2 POINTS = 3 @@ -1330,9 +1358,7 @@ def request_image(self, image_path: Union[str, Path]) -> Py5Promise: QUADS = 17 QUAD_BEZIER_VERTEX = 2 QUAD_STRIP = 18 - QUARTER_PI = 0.7853982 RADIUS = 2 - RAD_TO_DEG = 57.295776 RECT = 30 REPEAT = 1 REPLACE = 0 @@ -1351,16 +1377,13 @@ def request_image(self, image_path: Union[str, Path]) -> Py5Promise: SUBTRACT = 4 SVG = "processing.svg.PGraphicsSVG" TAB = '\t' - TAU = 6.2831855 TEXT = 2 - THIRD_PI = 1.0471976 THRESHOLD = 16 TOP = 101 TRIANGLE = 8 TRIANGLES = 9 TRIANGLE_FAN = 11 TRIANGLE_STRIP = 10 - TWO_PI = 6.2831855 UP = 38 VERTEX = 0 WAIT = 3 @@ -9736,6 +9759,20 @@ def apply_filter(self, *args): """ return self._instance.filter(*args) + def flush(self) -> None: + """Flush drawing commands to the renderer. + + Underlying Processing method: Sketch.flush + + Notes + ----- + + Flush drawing commands to the renderer. For most renderers, this method does + absolutely nothing. There are not a lot of good reasons to use this method, but + if you need it, it is available for your use. + """ + return self._instance.flush() + def frame_rate(self, fps: float, /) -> None: """Specifies the number of frames to be displayed every second. @@ -10026,6 +10063,7 @@ def full_screen(self, renderer: str, display: int, /) -> None: """ pass + @_settings_only('full_screen') def full_screen(self, *args): """Open a Sketch using the full size of the computer's display. @@ -11836,6 +11874,7 @@ def no_loop(self) -> None: """ return self._instance.noLoop() + @_settings_only('no_smooth') 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. @@ -12431,6 +12470,7 @@ def perspective(self, *args): """ return self._instance.perspective(*args) + @_settings_only('pixel_density') 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 @@ -14972,6 +15012,7 @@ def size(self, width: int, height: int, """ pass + @_settings_only('size') def size(self, *args): """Defines the dimension of the display window width and height in units of pixels. @@ -15182,6 +15223,7 @@ def smooth(self, level: int, /) -> None: """ pass + @_settings_only('smooth') def smooth(self, *args): """Draws all geometry with smooth (anti-aliased) edges. diff --git a/py5_tools/__init__.py b/py5_tools/__init__.py index 9e6897d..770a343 100644 --- a/py5_tools/__init__.py +++ b/py5_tools/__init__.py @@ -28,4 +28,4 @@ from . import translators # noqa -__version__ = '0.8.2a1' +__version__ = '0.8.3a1' diff --git a/py5_tools/hooks/frame_hooks.py b/py5_tools/hooks/frame_hooks.py index 3afaead..5b0c3aa 100644 --- a/py5_tools/hooks/frame_hooks.py +++ b/py5_tools/hooks/frame_hooks.py @@ -23,7 +23,7 @@ import time from pathlib import Path import tempfile -from typing import Callable, Any +from typing import Callable import numpy as np import numpy.typing as npt @@ -70,8 +70,8 @@ def screenshot( parameter to make this function run after ``post_draw()`` instead of ``draw()``. This is important when using Processing libraries that support ``post_draw()`` such as Camera3D or ColorBlindness.""" + import py5 if sketch is None: - import py5 sketch = py5.get_current_sketch() using_current_sketch = True else: @@ -84,7 +84,7 @@ def screenshot( raise RuntimeError(msg) if py5.bridge.check_run_method_callstack(): - msg = 'Calling py5_tools.screenshot() from within a py5 user function is not allowed. Please move this code to outside the Sketch.' + msg = 'Calling py5_tools.screenshot() from within a py5 user function is not allowed. Please move this code to outside the Sketch or consider using save_frame() instead.' raise RuntimeError(msg) with tempfile.TemporaryDirectory() as tempdir: @@ -161,8 +161,8 @@ def save_frames(dirname: str, *, filename: str = 'frame_####.png', parameter to make this function run after ``post_draw()`` instead of ``draw()``. This is important when using Processing libraries that support ``post_draw()`` such as Camera3D or ColorBlindness.""" + import py5 if sketch is None: - import py5 sketch = py5.get_current_sketch() using_current_sketch = True else: @@ -281,8 +281,8 @@ def offline_frame_processing(func: Callable[[npt.NDArray[np.uint8]], None], *, use the ``hook_post_draw`` parameter to make this function run after ``post_draw()`` instead of ``draw()``. This is important when using Processing libraries that support ``post_draw()`` such as Camera3D or ColorBlindness.""" + import py5 if sketch is None: - import py5 sketch = py5.get_current_sketch() using_current_sketch = True else: @@ -368,8 +368,8 @@ def animated_gif(filename: str, count: int, period: float, duration: float, *, parameter to make this function run after ``post_draw()`` instead of ``draw()``. This is important when using Processing libraries that support ``post_draw()`` such as Camera3D or ColorBlindness.""" + import py5 if sketch is None: - import py5 sketch = py5.get_current_sketch() using_current_sketch = True else: @@ -463,8 +463,8 @@ def capture_frames(count: float, parameter to make this function run after ``post_draw()`` instead of ``draw()``. This is important when using Processing libraries that support ``post_draw()`` such as Camera3D or ColorBlindness.""" + import py5 if sketch is None: - import py5 sketch = py5.get_current_sketch() using_current_sketch = True else: diff --git a/py5_tools/imported.py b/py5_tools/imported.py index 714bb45..0798b93 100644 --- a/py5_tools/imported.py +++ b/py5_tools/imported.py @@ -212,11 +212,17 @@ def _run_sketch(sketch_path, classpath, exit_if_error): sketch_args_str = str(sketch_args) with open(sketch_path, 'r', encoding='utf8') as f: - sketch_code = _CODE_FRAMEWORK.format( - f.read(), exit_if_error, py5_options_str, sketch_args_str) + user_code = f.read() # does the code parse? if not, display an error message try: + # this will make sure indentation and syntax errors are correctly + # attributed to the user's code and not the _CODE_FRAMEWORK + # template + ast.parse(user_code, filename=sketch_path, mode='exec') + # now do the real parsing + sketch_code = _CODE_FRAMEWORK.format( + user_code, exit_if_error, py5_options_str, sketch_args_str) sketch_ast = ast.parse( sketch_code, filename=sketch_path, mode='exec') except IndentationError as e: diff --git a/py5_tools/kernel/kernel.py b/py5_tools/kernel/kernel.py index c4ffeea..b9f4738 100644 --- a/py5_tools/kernel/kernel.py +++ b/py5_tools/kernel/kernel.py @@ -31,12 +31,12 @@ _PY5_HELP_LINKS = [ { - 'text': 'py5 Reference', - 'url': 'http://py5.ixora.io/reference/' + 'text': 'py5 Documentation', + 'url': 'http://py5coding.org/' }, { - 'text': 'py5 Tutorials', - 'url': 'http://py5.ixora.io/tutorials/' + 'text': 'py5 Function Reference', + 'url': 'http://py5coding.org/reference/sketch.html' }, ] @@ -80,7 +80,7 @@ class Py5Kernel(IPythonKernel): *_PY5_HELP_LINKS]).tag(config=True) implementation = 'py5' - implementation_version = '0.8.2a1' + implementation_version = '0.8.3a1' class Py5App(IPKernelApp): diff --git a/py5_tools/py5bot/kernel.py b/py5_tools/py5bot/kernel.py index da6a714..7fc0230 100644 --- a/py5_tools/py5bot/kernel.py +++ b/py5_tools/py5bot/kernel.py @@ -87,7 +87,7 @@ class Py5BotKernel(Py5Kernel): shell_class = Type(Py5BotShell) implementation = 'py5bot' - implementation_version = '0.8.2a1' + implementation_version = '0.8.3a1' class Py5BotApp(IPKernelApp): diff --git a/py5_tools/reference.py b/py5_tools/reference.py index 1390d82..6405219 100644 --- a/py5_tools/reference.py +++ b/py5_tools/reference.py @@ -181,6 +181,7 @@ 'fill', 'finished', 'floor', + 'flush', 'focused', 'frame_count', 'frame_rate', @@ -223,6 +224,7 @@ 'java_platform', 'java_version_name', 'JClass', + 'join_thread', 'key', 'key_code', 'launch_promise_thread', @@ -242,13 +244,16 @@ 'LINES', 'lines', 'list_threads', + 'load_bytes', 'load_font', 'load_image', 'load_json', 'load_np_pixels', + 'load_pickle', 'load_pixels', 'load_shader', 'load_shape', + 'load_strings', 'log', 'loop', 'mag', @@ -395,8 +400,11 @@ 'rwidth', 'saturation', 'save', + 'save_bytes', 'save_frame', 'save_json', + 'save_pickle', + 'save_strings', 'scale', 'SCREEN', 'screen_x', @@ -644,6 +652,7 @@ 'EXTERNAL_STOP', 'fill', 'floor', + 'flush', 'frame_rate', 'frustum', 'full_screen', @@ -674,6 +683,7 @@ 'INVERT', 'JAVA2D', 'JClass', + 'join_thread', 'launch_promise_thread', 'launch_repeating_thread', 'launch_thread', @@ -691,13 +701,16 @@ 'LINES', 'lines', 'list_threads', + 'load_bytes', 'load_font', 'load_image', 'load_json', 'load_np_pixels', + 'load_pickle', 'load_pixels', 'load_shader', 'load_shape', + 'load_strings', 'log', 'loop', 'mag', @@ -827,8 +840,11 @@ 'run_sketch', 'saturation', 'save', + 'save_bytes', 'save_frame', 'save_json', + 'save_pickle', + 'save_strings', 'scale', 'SCREEN', 'screen_x', diff --git a/py5_tools/split_setup.py b/py5_tools/split_setup.py index 46e9ff4..3a8314f 100644 --- a/py5_tools/split_setup.py +++ b/py5_tools/split_setup.py @@ -27,8 +27,8 @@ COMMENT_LINE = re.compile(r'^\s*#.*' + chr(36), flags=re.MULTILINE) DOCSTRING = re.compile(r'^\s*""".*?"""', flags=re.MULTILINE | re.DOTALL) SETUP_LINE = re.compile(r'^def setup[^:]*:') -MODULE_MODE_METHOD_LINE = re.compile(r'^\s*py5\.(\w+)\([^\)]*\)') -IMPORTED_MODE_METHOD_LINE = re.compile(r'^\s*(\w+)\([^\)]*\)') +MODULE_MODE_METHOD_LINE = re.compile(r'^\s*py5\.(\w+)\s*\([^\)]*\)') +IMPORTED_MODE_METHOD_LINE = re.compile(r'^\s*(\w+)\s*\([^\)]*\)') GLOBAL_STATEMENT_LINE = re.compile( r'^\s*global\s+.*' + chr(36), flags=re.MULTILINE) diff --git a/py5_tools/utilities.py b/py5_tools/utilities.py index e18024d..ce62463 100644 --- a/py5_tools/utilities.py +++ b/py5_tools/utilities.py @@ -21,11 +21,11 @@ from pathlib import Path -PY5_UTILITIES_CLASS = """package py5.utils; +PY5_UTILITIES_CLASS = """package py5utils; import py5.core.Sketch; -class Py5Utilities { +public class Py5Utilities { public Sketch sketch; @@ -47,7 +47,7 @@ class Py5Utilities { 0.1 py5utilities - https://py5.ixora.io/ + https://py5coding.org/ UTF-8 17 @@ -59,21 +59,21 @@ class Py5Utilities { py5 py5-processing4 - 0.8.2a1 + 0.8.3a1 system ${{jarlocation}}/core.jar py5 py5-jogl - 0.8.2a1 + 0.8.3a1 system ${{jarlocation}}/jogl-all.jar py5 py5 - 0.8.2a1 + 0.8.3a1 system ${{jarlocation}}/py5.jar @@ -137,7 +137,7 @@ def generate_utilities_framework(output_dir=None): f.write(POM_TEMPLATE.format(classpath=py5_classpath)) utils_filename = java_dir / \ - Path('src/main/java/py5/utils/Py5Utilities.java') + Path('src/main/java/py5utils/Py5Utilities.java') utils_filename.parent.mkdir(parents=True, exist_ok=True) with open(utils_filename, 'w') as f: f.write(PY5_UTILITIES_CLASS) diff --git a/setup.py b/setup.py index 425bb6d..1567f9f 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ with open('README.md') as f: README = f.read() -VERSION = '0.8.2a1' +VERSION = '0.8.3a1' INSTALL_REQUIRES = [ 'autopep8>=1.5', @@ -66,13 +66,13 @@ description='Processing for CPython', long_description=README, long_description_content_type='text/markdown', - url='https://py5.ixora.io/', + url='https://py5coding.org/', author='Jim Schmitz', author_email='jim@ixora.io', download_url='https://pypi.org/project/py5', project_urls={ "Bug Tracker": 'https://github.com/py5coding/py5generator/issues', - "Documentation": 'https://py5.ixora.io/', + "Documentation": 'https://py5coding.org/', "Source Code": 'https://github.com/py5coding/py5', }, platforms=["Windows", "Linux", "Mac OS-X"],