From 70298d3be948e4245e2f24481b33d8d877c45e1d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Aug 2024 06:38:46 +1000 Subject: [PATCH 01/52] Fix undefined variable --- src/libImaging/Quant.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libImaging/Quant.c b/src/libImaging/Quant.c index 55b5e7a55..a489a882d 100644 --- a/src/libImaging/Quant.c +++ b/src/libImaging/Quant.c @@ -314,8 +314,9 @@ test_sorted(PixelList *pl[3]) { for (i = 0; i < 3; i++) { l = 256; for (t = pl[i]; t; t = t->next[i]) { - if (l < t->p.a.v[i]) + if (l < t->p.a.v[i]) { return 0; + } l = t->p.a.v[i]; } } @@ -1009,7 +1010,8 @@ compute_palette_from_median_cut( uint32_t nPixels, HashTable *medianBoxHash, Pixel **palette, - uint32_t nPaletteEntries + uint32_t nPaletteEntries, + BoxNode *root ) { uint32_t i; uint32_t paletteEntry; @@ -1382,7 +1384,9 @@ quantize( fflush(stdout); timer = clock(); #endif - if (!compute_palette_from_median_cut(pixelData, nPixels, h, &p, nPaletteEntries)) { + if (!compute_palette_from_median_cut( + pixelData, nPixels, h, &p, nPaletteEntries, root + )) { goto error_3; } #ifdef DEBUG From 0d79a38e77ddcf5e386d2153e791f12e5d67a2f5 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 11 Aug 2024 15:14:29 +0400 Subject: [PATCH 02/52] Add missing TIFF CMYK;16B reader --- src/PIL/TiffImagePlugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index d9d1bab5a..deae199d5 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -259,6 +259,7 @@ OPEN_INFO = { (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"), (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"), + (MM, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16B"), (II, 6, (1,), 1, (8,), ()): ("L", "L"), (MM, 6, (1,), 1, (8,), ()): ("L", "L"), # JPEG compressed images handled by LibTiff and auto-converted to RGBX From c1e8375af834b190386cd2cfa30703c7975675a8 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 21:51:14 +0400 Subject: [PATCH 03/52] Require webpmux and webpdemux --- Tests/check_wheel.py | 1 - Tests/test_features.py | 5 --- Tests/test_file_webp_metadata.py | 5 +-- docs/handbook/image-file-formats.rst | 9 ++--- docs/installation/building-from-source.rst | 8 ++-- docs/reference/features.rst | 1 - setup.py | 46 ++++++++-------------- src/PIL/features.py | 2 - src/_webp.c | 33 ---------------- winbuild/build_prepare.py | 2 +- 10 files changed, 26 insertions(+), 86 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 4b91984f5..2e4498db6 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -28,7 +28,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { "webp_anim", - "webp_mux", "transp_webp", "raqm", "fribidi", diff --git a/Tests/test_features.py b/Tests/test_features.py index b7eefa09a..33509a346 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -57,11 +57,6 @@ def test_webp_transparency() -> None: assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY -@skip_unless_feature("webp") -def test_webp_mux() -> None: - assert features.check("webp_mux") == _webp.HAVE_WEBPMUX - - @skip_unless_feature("webp") def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index c3df4ad7b..52a84a83e 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -10,10 +10,7 @@ from PIL import Image from .helper import mark_if_feature_version, skip_unless_feature -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_mux"), -] +pytestmark = [skip_unless_feature("webp")] ElementTree: ModuleType | None try: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 1ec972149..8561cc4aa 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1252,16 +1252,13 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: Requires libwebp 0.5.0 or later. **icc_profile** - The ICC Profile to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The ICC Profile to include in the saved file. **exif** - The exif data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The exif data to include in the saved file. **xmp** - The XMP data to include in the saved file. Only supported if - the system WebP library was built with webpmux support. + The XMP data to include in the saved file. Saving sequences ~~~~~~~~~~~~~~~~ diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index 7f7dfa6ff..f47edbc5b 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -275,18 +275,18 @@ Build Options * Config settings: ``-C zlib=disable``, ``-C jpeg=disable``, ``-C tiff=disable``, ``-C freetype=disable``, ``-C raqm=disable``, - ``-C lcms=disable``, ``-C webp=disable``, ``-C webpmux=disable``, + ``-C lcms=disable``, ``-C webp=disable``, ``-C jpeg2000=disable``, ``-C imagequant=disable``, ``-C xcb=disable``. Disable building the corresponding feature even if the development libraries are present on the building machine. * Config settings: ``-C zlib=enable``, ``-C jpeg=enable``, ``-C tiff=enable``, ``-C freetype=enable``, ``-C raqm=enable``, - ``-C lcms=enable``, ``-C webp=enable``, ``-C webpmux=enable``, + ``-C lcms=enable``, ``-C webp=enable``, ``-C jpeg2000=enable``, ``-C imagequant=enable``, ``-C xcb=enable``. Require that the corresponding feature is built. The build will raise - an exception if the libraries are not found. Webpmux (WebP metadata) - relies on WebP support. Tcl and Tk also must be used together. + an exception if the libraries are not found. Tcl and Tk must be used + together. * Config settings: ``-C raqm=vendor``, ``-C fribidi=vendor``. These flags are used to compile a modified version of libraqm and diff --git a/docs/reference/features.rst b/docs/reference/features.rst index c66193061..55c0b1200 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -55,7 +55,6 @@ Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. * ``transp_webp``: Support for transparency in WebP images. -* ``webp_mux``: (compile time) Support for EXIF data in WebP images. * ``webp_anim``: (compile time) Support for animated WebP images. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. diff --git a/setup.py b/setup.py index b26852b0b..8d0dfb6fa 100644 --- a/setup.py +++ b/setup.py @@ -295,7 +295,6 @@ class pil_build_ext(build_ext): "raqm", "lcms", "webp", - "webpmux", "jpeg2000", "imagequant", "xcb", @@ -794,29 +793,22 @@ class pil_build_ext(build_ext): if feature.want("webp"): _dbg("Looking for webp") - if _find_include_file(self, "webp/encode.h") and _find_include_file( - self, "webp/decode.h" + if all( + _find_include_file(self, src) + for src in ["webp/encode.h", "webp/mux.h", "webp/demux.h"] ): # In Google's precompiled zip it is call "libwebp": - if _find_library_file(self, "webp"): + if all( + _find_library_file(self, lib) + for lib in ["webp", "webpmux", "webpdemux"] + ): feature.webp = "webp" - elif _find_library_file(self, "libwebp"): + elif all( + _find_library_file(self, lib) + for lib in ["libwebp", "libwebpmux", "libwebpdemux"] + ): feature.webp = "libwebp" - if feature.want("webpmux"): - _dbg("Looking for webpmux") - if _find_include_file(self, "webp/mux.h") and _find_include_file( - self, "webp/demux.h" - ): - if _find_library_file(self, "webpmux") and _find_library_file( - self, "webpdemux" - ): - feature.webpmux = "webpmux" - if _find_library_file(self, "libwebpmux") and _find_library_file( - self, "libwebpdemux" - ): - feature.webpmux = "libwebpmux" - if feature.want("xcb"): _dbg("Looking for xcb") if _find_include_file(self, "xcb/xcb.h"): @@ -904,15 +896,12 @@ class pil_build_ext(build_ext): self._remove_extension("PIL._imagingcms") if feature.webp: - libs = [feature.webp] - defs = [] - - if feature.webpmux: - defs.append(("HAVE_WEBPMUX", None)) - libs.append(feature.webpmux) - libs.append(feature.webpmux.replace("pmux", "pdemux")) - - self._update_extension("PIL._webp", libs, defs) + libs = [ + feature.webp, + feature.webp + "mux", + feature.webp + "demux", + ] + self._update_extension("PIL._webp", libs, []) else: self._remove_extension("PIL._webp") @@ -953,7 +942,6 @@ class pil_build_ext(build_ext): (feature.raqm, "RAQM (Text shaping)", raqm_extra_info), (feature.lcms, "LITTLECMS2"), (feature.webp, "WEBP"), - (feature.webpmux, "WEBPMUX"), (feature.xcb, "XCB (X protocol)"), ] diff --git a/src/PIL/features.py b/src/PIL/features.py index 13908c4eb..7f6cdb161 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -120,7 +120,6 @@ def get_supported_codecs() -> list[str]: features = { "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), - "webp_mux": ("PIL._webp", "HAVE_WEBPMUX", None), "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), @@ -272,7 +271,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), ("transp_webp", "WEBP Transparency"), - ("webp_mux", "WEBPMUX"), ("webp_anim", "WEBP Animation"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), diff --git a/src/_webp.c b/src/_webp.c index e686ec820..ab229058b 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -4,8 +4,6 @@ #include #include #include - -#ifdef HAVE_WEBPMUX #include #include @@ -19,8 +17,6 @@ #define HAVE_WEBPANIM #endif -#endif - void ImagingSectionEnter(ImagingSectionCookie *cookie) { *cookie = (PyThreadState *)PyEval_SaveThread(); @@ -35,8 +31,6 @@ ImagingSectionLeave(ImagingSectionCookie *cookie) { /* WebP Muxer Error Handling */ /* -------------------------------------------------------------------- */ -#ifdef HAVE_WEBPMUX - static const char *const kErrorMessages[-WEBP_MUX_NOT_ENOUGH_DATA + 1] = { "WEBP_MUX_NOT_FOUND", "WEBP_MUX_INVALID_ARGUMENT", @@ -89,8 +83,6 @@ HandleMuxError(WebPMuxError err, char *chunk) { return NULL; } -#endif - /* -------------------------------------------------------------------- */ /* WebP Animation Support */ /* -------------------------------------------------------------------- */ @@ -693,13 +685,6 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { output = writer.mem; ret_size = writer.size; -#ifndef HAVE_WEBPMUX - if (ret_size > 0) { - PyObject *ret = PyBytes_FromStringAndSize((char *)output, ret_size); - free(output); - return ret; - } -#else { /* I want to truncate the *_size items that get passed into WebP data. Pypy2.1.0 had some issues where the Py_ssize_t items had @@ -775,7 +760,6 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { return ret; } } -#endif Py_RETURN_NONE; } @@ -809,9 +793,6 @@ WebPDecode_wrapper(PyObject *self, PyObject *args) { mode = "RGBA"; } -#ifndef HAVE_WEBPMUX - vp8_status_code = WebPDecode(webp, size, &config); -#else { int copy_data = 0; WebPData data = {webp, size}; @@ -849,7 +830,6 @@ WebPDecode_wrapper(PyObject *self, PyObject *args) { WebPDataClear(&image.bitstream); WebPMuxDelete(mux); } -#endif } if (vp8_status_code != VP8_STATUS_OK) { @@ -949,18 +929,6 @@ static PyMethodDef webpMethods[] = { {NULL, NULL} }; -void -addMuxFlagToModule(PyObject *m) { - PyObject *have_webpmux; -#ifdef HAVE_WEBPMUX - have_webpmux = Py_True; -#else - have_webpmux = Py_False; -#endif - Py_INCREF(have_webpmux); - PyModule_AddObject(m, "HAVE_WEBPMUX", have_webpmux); -} - void addAnimFlagToModule(PyObject *m) { PyObject *have_webpanim; @@ -991,7 +959,6 @@ setup_module(PyObject *m) { } #endif PyObject *d = PyModule_GetDict(m); - addMuxFlagToModule(m); addAnimFlagToModule(m); addTransparencyFlagToModule(m); diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 9837589b2..7129699eb 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -201,7 +201,7 @@ DEPS = { }, "build": [ *cmds_cmake( - "webp webpdemux webpmux", + "webp webpmux webpdemux", "-DBUILD_SHARED_LIBS:BOOL=OFF", "-DWEBP_LINK_STATIC:BOOL=OFF", ), From 9bed5b426437ad4aacd36288fcfb9422cac8c9c9 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 22:05:57 +0400 Subject: [PATCH 04/52] Remove _webp.WebPDecoderBuggyAlpha and _webp.HAVE_TRANSPARENCY --- Tests/check_wheel.py | 1 - Tests/test_features.py | 6 ------ Tests/test_file_webp.py | 1 - Tests/test_file_webp_alpha.py | 8 -------- docs/reference/features.rst | 1 - src/PIL/features.py | 2 -- src/_webp.c | 27 --------------------------- 7 files changed, 46 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 2e4498db6..f862e4fff 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -28,7 +28,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { "webp_anim", - "transp_webp", "raqm", "fribidi", "harfbuzz", diff --git a/Tests/test_features.py b/Tests/test_features.py index 33509a346..577e7a5d5 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -51,12 +51,6 @@ def test_version() -> None: test(feature, features.version_feature) -@skip_unless_feature("webp") -def test_webp_transparency() -> None: - assert features.check("transp_webp") != _webp.WebPDecoderBuggyAlpha() - assert features.check("transp_webp") == _webp.HAVE_TRANSPARENCY - - @skip_unless_feature("webp") def test_webp_anim() -> None: assert features.check("webp_anim") == _webp.HAVE_WEBPANIM diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index cbc905de4..f132d0a64 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -49,7 +49,6 @@ class TestFileWebp: def test_version(self) -> None: _webp.WebPDecoderVersion() - _webp.WebPDecoderBuggyAlpha() version = features.version_module("webp") assert version is not None assert re.search(r"\d+\.\d+\.\d+$", version) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index c74452121..e80ef7d4f 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -16,11 +16,6 @@ from .helper import ( _webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") -def setup_module() -> None: - if _webp.WebPDecoderBuggyAlpha(): - pytest.skip("Buggy early version of WebP installed, not testing transparency") - - def test_read_rgba() -> None: """ Can we read an RGBA mode file without error? @@ -81,9 +76,6 @@ def test_write_rgba(tmp_path: Path) -> None: pil_image = Image.new("RGBA", (10, 10), (255, 0, 0, 20)) pil_image.save(temp_file) - if _webp.WebPDecoderBuggyAlpha(): - return - with Image.open(temp_file) as image: image.load() diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 55c0b1200..d4fb340bd 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,7 +54,6 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. -* ``transp_webp``: Support for transparency in WebP images. * ``webp_anim``: (compile time) Support for animated WebP images. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. diff --git a/src/PIL/features.py b/src/PIL/features.py index 7f6cdb161..60590dc09 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -120,7 +120,6 @@ def get_supported_codecs() -> list[str]: features = { "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), - "transp_webp": ("PIL._webp", "HAVE_TRANSPARENCY", None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -270,7 +269,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), - ("transp_webp", "WEBP Transparency"), ("webp_anim", "WEBP Animation"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), diff --git a/src/_webp.c b/src/_webp.c index ab229058b..a10eb111c 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -896,20 +896,6 @@ WebPDecoderVersion_str(void) { return version; } -/* - * The version of webp that ships with (0.1.3) Ubuntu 12.04 doesn't handle alpha well. - * Files that are valid with 0.3 are reported as being invalid. - */ -int -WebPDecoderBuggyAlpha(void) { - return WebPGetDecoderVersion() == 0x0103; -} - -PyObject * -WebPDecoderBuggyAlpha_wrapper() { - return Py_BuildValue("i", WebPDecoderBuggyAlpha()); -} - /* -------------------------------------------------------------------- */ /* Module Setup */ /* -------------------------------------------------------------------- */ @@ -922,10 +908,6 @@ static PyMethodDef webpMethods[] = { {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, - {"WebPDecoderBuggyAlpha", - WebPDecoderBuggyAlpha_wrapper, - METH_NOARGS, - "WebPDecoderBuggyAlpha"}, {NULL, NULL} }; @@ -941,14 +923,6 @@ addAnimFlagToModule(PyObject *m) { PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); } -void -addTransparencyFlagToModule(PyObject *m) { - PyObject *have_transparency = PyBool_FromLong(!WebPDecoderBuggyAlpha()); - if (PyModule_AddObject(m, "HAVE_TRANSPARENCY", have_transparency)) { - Py_DECREF(have_transparency); - } -} - static int setup_module(PyObject *m) { #ifdef HAVE_WEBPANIM @@ -960,7 +934,6 @@ setup_module(PyObject *m) { #endif PyObject *d = PyModule_GetDict(m); addAnimFlagToModule(m); - addTransparencyFlagToModule(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); From a3468996c0b7b6df2b685ff21c4f515f5105ff8c Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Sun, 7 Jul 2024 22:47:06 +0400 Subject: [PATCH 05/52] Remove webp animations flags and conditions Removed: _webp.WebPDecode _webp.HAVE_WEBPANIM features.webp_anim --- Tests/check_wheel.py | 1 - Tests/test_features.py | 10 -- Tests/test_file_gif.py | 2 +- Tests/test_file_webp.py | 24 ++--- Tests/test_file_webp_alpha.py | 7 +- Tests/test_file_webp_animated.py | 5 +- Tests/test_file_webp_lossless.py | 3 - Tests/test_file_webp_metadata.py | 3 +- Tests/test_image.py | 1 - Tests/test_imagefile.py | 1 - Tests/test_imageops.py | 2 +- docs/handbook/image-file-formats.rst | 9 +- docs/reference/features.rst | 1 - setup.py | 7 +- src/PIL/WebPImagePlugin.py | 47 +++------ src/PIL/features.py | 2 - src/_webp.c | 150 +-------------------------- 17 files changed, 34 insertions(+), 241 deletions(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index f862e4fff..8fcb75dad 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -27,7 +27,6 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { - "webp_anim", "raqm", "fribidi", "harfbuzz", diff --git a/Tests/test_features.py b/Tests/test_features.py index 577e7a5d5..40466082f 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -10,11 +10,6 @@ from PIL import features from .helper import skip_unless_feature -try: - from PIL import _webp -except ImportError: - pass - def test_check() -> None: # Check the correctness of the convenience function @@ -51,11 +46,6 @@ def test_version() -> None: test(feature, features.version_feature) -@skip_unless_feature("webp") -def test_webp_anim() -> None: - assert features.check("webp_anim") == _webp.HAVE_WEBPANIM - - @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: version = features.version("libjpeg_turbo") diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 85b017d29..8cefdb628 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -978,7 +978,7 @@ def test_webp_background(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") # Test opaque WebP background - if features.check("webp") and features.check("webp_anim"): + if features.check("webp"): with Image.open("Tests/images/hopper.webp") as im: assert im.info["background"] == (255, 255, 255, 255) im.save(out) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index f132d0a64..6e9eea4ae 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -48,7 +48,6 @@ class TestFileWebp: self.rgb_mode = "RGB" def test_version(self) -> None: - _webp.WebPDecoderVersion() version = features.version_module("webp") assert version is not None assert re.search(r"\d+\.\d+\.\d+$", version) @@ -116,7 +115,6 @@ class TestFileWebp: hopper().save(buffer_method, format="WEBP", method=6) assert buffer_no_args.getbuffer() != buffer_method.getbuffer() - @skip_unless_feature("webp_anim") def test_save_all(self, tmp_path: Path) -> None: temp_file = str(tmp_path / "temp.webp") im = Image.new("RGB", (1, 1)) @@ -131,10 +129,9 @@ class TestFileWebp: def test_icc_profile(self, tmp_path: Path) -> None: self._roundtrip(tmp_path, self.rgb_mode, 12.5, {"icc_profile": None}) - if _webp.HAVE_WEBPANIM: - self._roundtrip( - tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} - ) + self._roundtrip( + tmp_path, self.rgb_mode, 12.5, {"icc_profile": None, "save_all": True} + ) def test_write_unsupported_mode_L(self, tmp_path: Path) -> None: """ @@ -164,10 +161,8 @@ class TestFileWebp: """ Calling encoder functions with no arguments should result in an error. """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimEncoder() + with pytest.raises(TypeError): + _webp.WebPAnimEncoder() with pytest.raises(TypeError): _webp.WebPEncode() @@ -175,12 +170,8 @@ class TestFileWebp: """ Calling decoder functions with no arguments should result in an error. """ - - if _webp.HAVE_WEBPANIM: - with pytest.raises(TypeError): - _webp.WebPAnimDecoder() with pytest.raises(TypeError): - _webp.WebPDecode() + _webp.WebPAnimDecoder() def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" @@ -199,7 +190,6 @@ class TestFileWebp: "background", (0, (0,), (-1, 0, 1, 2), (253, 254, 255, 256)), ) - @skip_unless_feature("webp_anim") def test_invalid_background( self, background: int | tuple[int, ...], tmp_path: Path ) -> None: @@ -208,7 +198,6 @@ class TestFileWebp: with pytest.raises(OSError): im.save(temp_file, save_all=True, append_images=[im], background=background) - @skip_unless_feature("webp_anim") def test_background_from_gif(self, tmp_path: Path) -> None: # Save L mode GIF with background with Image.open("Tests/images/no_palette_with_background.gif") as im: @@ -233,7 +222,6 @@ class TestFileWebp: difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) assert difference < 5 - @skip_unless_feature("webp_anim") def test_duration(self, tmp_path: Path) -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: assert im.info["duration"] == 1000 diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index e80ef7d4f..53cdfff03 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -85,12 +85,7 @@ def test_write_rgba(tmp_path: Path) -> None: image.load() image.getdata() - # Early versions of WebP are known to produce higher deviations: - # deal with it - if _webp.WebPDecoderVersion() <= 0x201: - assert_image_similar(image, pil_image, 3.0) - else: - assert_image_similar(image, pil_image, 1.0) + assert_image_similar(image, pil_image, 1.0) def test_keep_rgb_values_when_transparent(tmp_path: Path) -> None: diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index e0d7999e3..967a0aae8 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -15,10 +15,7 @@ from .helper import ( skip_unless_feature, ) -pytestmark = [ - skip_unless_feature("webp"), - skip_unless_feature("webp_anim"), -] +pytestmark = skip_unless_feature("webp") def test_n_frames() -> None: diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index 32e29de56..dc98ad3f0 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -13,9 +13,6 @@ RGB_MODE = "RGB" def test_write_lossless_rgb(tmp_path: Path) -> None: - if _webp.WebPDecoderVersion() < 0x0200: - pytest.skip("lossless not included") - temp_file = str(tmp_path / "temp.webp") hopper(RGB_MODE).save(temp_file, lossless=True) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 52a84a83e..4ef3d95f2 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -10,7 +10,7 @@ from PIL import Image from .helper import mark_if_feature_version, skip_unless_feature -pytestmark = [skip_unless_feature("webp")] +pytestmark = skip_unless_feature("webp") ElementTree: ModuleType | None try: @@ -133,7 +133,6 @@ def test_getxmp() -> None: ) -@skip_unless_feature("webp_anim") def test_write_animated_metadata(tmp_path: Path) -> None: iccp_data = b"" exif_data = b"" diff --git a/Tests/test_image.py b/Tests/test_image.py index d8372789b..719732d12 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -817,7 +817,6 @@ class TestImage: assert reloaded_exif[305] == "Pillow test" @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") def test_exif_webp(self, tmp_path: Path) -> None: with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 44a6e6a42..fe7d44785 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -94,7 +94,6 @@ class TestImageFile: assert (48, 48) == p.image.size @skip_unless_feature("webp") - @skip_unless_feature("webp_anim") def test_incremental_webp(self) -> None: with ImageFile.Parser() as p: with open("Tests/images/hopper.webp", "rb") as f: diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index e33e6d4c8..2fb2a60b6 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -390,7 +390,7 @@ def test_colorize_3color_offset() -> None: def test_exif_transpose() -> None: exts = [".jpg"] - if features.check("webp") and features.check("webp_anim"): + if features.check("webp"): exts.append(".webp") for ext in exts: with Image.open("Tests/images/hopper" + ext) as base_im: diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 8561cc4aa..49a17bb5c 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1220,8 +1220,7 @@ using the general tags available through tiffinfo. WebP ^^^^ -Pillow reads and writes WebP files. The specifics of Pillow's capabilities with -this format are currently undocumented. +Pillow reads and writes WebP files. Requires libwebp v0.5.0 or later. .. _webp-saving: @@ -1263,12 +1262,6 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: Saving sequences ~~~~~~~~~~~~~~~~ -.. note:: - - Support for animated WebP files will only be enabled if the system WebP - library is v0.5.0 or later. You can check webp animation support at - runtime by calling ``features.check("webp_anim")``. - When calling :py:meth:`~PIL.Image.Image.save` to write a WebP file, by default only the first frame of a multiframe image will be saved. If the ``save_all`` argument is present and true, then all frames will be saved, and the following diff --git a/docs/reference/features.rst b/docs/reference/features.rst index d4fb340bd..26c8ab8cd 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -54,7 +54,6 @@ Feature version numbers are available only where stated. Support for the following features can be checked: * ``libjpeg_turbo``: (compile time) Whether Pillow was compiled against the libjpeg-turbo version of libjpeg. Compile-time version number is available. -* ``webp_anim``: (compile time) Support for animated WebP images. * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. diff --git a/setup.py b/setup.py index 8d0dfb6fa..edfa9ee28 100644 --- a/setup.py +++ b/setup.py @@ -795,7 +795,12 @@ class pil_build_ext(build_ext): _dbg("Looking for webp") if all( _find_include_file(self, src) - for src in ["webp/encode.h", "webp/mux.h", "webp/demux.h"] + for src in [ + "webp/encode.h", + "webp/decode.h", + "webp/mux.h", + "webp/demux.h", + ] ): # In Google's precompiled zip it is call "libwebp": if all( diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index cec796340..fc8f4fc2e 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -45,22 +45,6 @@ class WebPImageFile(ImageFile.ImageFile): __logical_frame = 0 def _open(self) -> None: - if not _webp.HAVE_WEBPANIM: - # Legacy mode - data, width, height, self._mode, icc_profile, exif = _webp.WebPDecode( - self.fp.read() - ) - if icc_profile: - self.info["icc_profile"] = icc_profile - if exif: - self.info["exif"] = exif - self._size = width, height - self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.mode)] - self.n_frames = 1 - self.is_animated = False - return - # Use the newer AnimDecoder API to parse the (possibly) animated file, # and access muxed chunks like ICC/EXIF/XMP. self._decoder = _webp.WebPAnimDecoder(self.fp.read()) @@ -145,21 +129,20 @@ class WebPImageFile(ImageFile.ImageFile): self._get_next() # Advance to the requested frame def load(self) -> Image.core.PixelAccess | None: - if _webp.HAVE_WEBPANIM: - if self.__loaded != self.__logical_frame: - self._seek(self.__logical_frame) + if self.__loaded != self.__logical_frame: + self._seek(self.__logical_frame) - # We need to load the image data for this frame - data, timestamp, duration = self._get_next() - self.info["timestamp"] = timestamp - self.info["duration"] = duration - self.__loaded = self.__logical_frame + # We need to load the image data for this frame + data, timestamp, duration = self._get_next() + self.info["timestamp"] = timestamp + self.info["duration"] = duration + self.__loaded = self.__logical_frame - # Set tile - if self.fp and self._exclusive_fp: - self.fp.close() - self.fp = BytesIO(data) - self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] + # Set tile + if self.fp and self._exclusive_fp: + self.fp.close() + self.fp = BytesIO(data) + self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)] return super().load() @@ -167,9 +150,6 @@ class WebPImageFile(ImageFile.ImageFile): pass def tell(self) -> int: - if not _webp.HAVE_WEBPANIM: - return super().tell() - return self.__logical_frame @@ -357,7 +337,6 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: Image.register_open(WebPImageFile.format, WebPImageFile, _accept) if SUPPORTED: Image.register_save(WebPImageFile.format, _save) - if _webp.HAVE_WEBPANIM: - Image.register_save_all(WebPImageFile.format, _save_all) + Image.register_save_all(WebPImageFile.format, _save_all) Image.register_extension(WebPImageFile.format, ".webp") Image.register_mime(WebPImageFile.format, "image/webp") diff --git a/src/PIL/features.py b/src/PIL/features.py index 60590dc09..bbf7c641b 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -119,7 +119,6 @@ def get_supported_codecs() -> list[str]: features = { - "webp_anim": ("PIL._webp", "HAVE_WEBPANIM", None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -269,7 +268,6 @@ def pilinfo(out: IO[str] | None = None, supported_formats: bool = True) -> None: ("freetype2", "FREETYPE2"), ("littlecms2", "LITTLECMS2"), ("webp", "WEBP"), - ("webp_anim", "WEBP Animation"), ("jpg", "JPEG"), ("jpg_2000", "OPENJPEG (JPEG2000)"), ("zlib", "ZLIB (PNG/ZIP)"), diff --git a/src/_webp.c b/src/_webp.c index a10eb111c..8302925b0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -13,8 +13,8 @@ * very early versions had some significant differences, so we require later * versions, before enabling animation support. */ -#if WEBP_MUX_ABI_VERSION >= 0x0104 && WEBP_DEMUX_ABI_VERSION >= 0x0105 -#define HAVE_WEBPANIM +#if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 +#error libwebp 0.5.0 and above is required. Upgrade libwebp or build with --disable-webp flag #endif void @@ -87,8 +87,6 @@ HandleMuxError(WebPMuxError err, char *chunk) { /* WebP Animation Support */ /* -------------------------------------------------------------------- */ -#ifdef HAVE_WEBPANIM - // Encoder type typedef struct { PyObject_HEAD WebPAnimEncoder *enc; @@ -568,8 +566,6 @@ static PyTypeObject WebPAnimDecoder_Type = { 0, /*tp_getset*/ }; -#endif - /* -------------------------------------------------------------------- */ /* Legacy WebP Support */ /* -------------------------------------------------------------------- */ @@ -644,10 +640,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { config.quality = quality_factor; config.alpha_quality = alpha_quality_factor; config.method = method; -#if WEBP_ENCODER_ABI_VERSION >= 0x0209 - // the "exact" flag is only available in libwebp 0.5.0 and later config.exact = exact; -#endif // Validate the config if (!WebPValidateConfig(&config)) { @@ -763,124 +756,6 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { Py_RETURN_NONE; } -PyObject * -WebPDecode_wrapper(PyObject *self, PyObject *args) { - PyBytesObject *webp_string; - const uint8_t *webp; - Py_ssize_t size; - PyObject *ret = Py_None, *bytes = NULL, *pymode = NULL, *icc_profile = NULL, - *exif = NULL; - WebPDecoderConfig config; - VP8StatusCode vp8_status_code = VP8_STATUS_OK; - char *mode = "RGB"; - - if (!PyArg_ParseTuple(args, "S", &webp_string)) { - return NULL; - } - - if (!WebPInitDecoderConfig(&config)) { - Py_RETURN_NONE; - } - - PyBytes_AsStringAndSize((PyObject *)webp_string, (char **)&webp, &size); - - vp8_status_code = WebPGetFeatures(webp, size, &config.input); - if (vp8_status_code == VP8_STATUS_OK) { - // If we don't set it, we don't get alpha. - // Initialized to MODE_RGB - if (config.input.has_alpha) { - config.output.colorspace = MODE_RGBA; - mode = "RGBA"; - } - - { - int copy_data = 0; - WebPData data = {webp, size}; - WebPMuxFrameInfo image; - WebPData icc_profile_data = {0}; - WebPData exif_data = {0}; - - WebPMux *mux = WebPMuxCreate(&data, copy_data); - if (NULL == mux) { - goto end; - } - - if (WEBP_MUX_OK != WebPMuxGetFrame(mux, 1, &image)) { - WebPMuxDelete(mux); - goto end; - } - - webp = image.bitstream.bytes; - size = image.bitstream.size; - - vp8_status_code = WebPDecode(webp, size, &config); - - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "ICCP", &icc_profile_data)) { - icc_profile = PyBytes_FromStringAndSize( - (const char *)icc_profile_data.bytes, icc_profile_data.size - ); - } - - if (WEBP_MUX_OK == WebPMuxGetChunk(mux, "EXIF", &exif_data)) { - exif = PyBytes_FromStringAndSize( - (const char *)exif_data.bytes, exif_data.size - ); - } - - WebPDataClear(&image.bitstream); - WebPMuxDelete(mux); - } - } - - if (vp8_status_code != VP8_STATUS_OK) { - goto end; - } - - if (config.output.colorspace < MODE_YUV) { - bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.RGBA.rgba, config.output.u.RGBA.size - ); - } else { - // Skipping YUV for now. Need Test Images. - // UNDONE -- unclear if we'll ever get here if we set mode_rgb* - bytes = PyBytes_FromStringAndSize( - (char *)config.output.u.YUVA.y, config.output.u.YUVA.y_size - ); - } - - pymode = PyUnicode_FromString(mode); - ret = Py_BuildValue( - "SiiSSS", - bytes, - config.output.width, - config.output.height, - pymode, - NULL == icc_profile ? Py_None : icc_profile, - NULL == exif ? Py_None : exif - ); - -end: - WebPFreeDecBuffer(&config.output); - - Py_XDECREF(bytes); - Py_XDECREF(pymode); - Py_XDECREF(icc_profile); - Py_XDECREF(exif); - - if (Py_None == ret) { - Py_RETURN_NONE; - } - - return ret; -} - -// Return the decoder's version number, packed in hexadecimal using 8bits for -// each of major/minor/revision. E.g: v2.5.7 is 0x020507. -PyObject * -WebPDecoderVersion_wrapper() { - return Py_BuildValue("i", WebPGetDecoderVersion()); -} - // Version as string const char * WebPDecoderVersion_str(void) { @@ -901,39 +776,20 @@ WebPDecoderVersion_str(void) { /* -------------------------------------------------------------------- */ static PyMethodDef webpMethods[] = { -#ifdef HAVE_WEBPANIM {"WebPAnimDecoder", _anim_decoder_new, METH_VARARGS, "WebPAnimDecoder"}, {"WebPAnimEncoder", _anim_encoder_new, METH_VARARGS, "WebPAnimEncoder"}, -#endif {"WebPEncode", WebPEncode_wrapper, METH_VARARGS, "WebPEncode"}, - {"WebPDecode", WebPDecode_wrapper, METH_VARARGS, "WebPDecode"}, - {"WebPDecoderVersion", WebPDecoderVersion_wrapper, METH_NOARGS, "WebPVersion"}, {NULL, NULL} }; -void -addAnimFlagToModule(PyObject *m) { - PyObject *have_webpanim; -#ifdef HAVE_WEBPANIM - have_webpanim = Py_True; -#else - have_webpanim = Py_False; -#endif - Py_INCREF(have_webpanim); - PyModule_AddObject(m, "HAVE_WEBPANIM", have_webpanim); -} - static int setup_module(PyObject *m) { -#ifdef HAVE_WEBPANIM + PyObject *d = PyModule_GetDict(m); /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || PyType_Ready(&WebPAnimEncoder_Type) < 0) { return -1; } -#endif - PyObject *d = PyModule_GetDict(m); - addAnimFlagToModule(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); From 56ca359c657ee2f4e81426f780c4b4e2e281e404 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 8 Jul 2024 20:07:43 +0400 Subject: [PATCH 06/52] Bring back removed features, add deprecations and Release notes --- Tests/check_wheel.py | 3 +++ Tests/test_features.py | 12 ++++++++++++ docs/reference/features.rst | 3 +++ docs/releasenotes/11.0.0.rst | 8 ++++++++ src/PIL/features.py | 10 +++++++++- 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Tests/check_wheel.py b/Tests/check_wheel.py index 8fcb75dad..4b91984f5 100644 --- a/Tests/check_wheel.py +++ b/Tests/check_wheel.py @@ -27,6 +27,9 @@ def test_wheel_codecs() -> None: def test_wheel_features() -> None: expected_features = { + "webp_anim", + "webp_mux", + "transp_webp", "raqm", "fribidi", "harfbuzz", diff --git a/Tests/test_features.py b/Tests/test_features.py index 40466082f..cb8fd8688 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -46,6 +46,18 @@ def test_version() -> None: test(feature, features.version_feature) +def test_webp_transparency() -> None: + assert features.check("transp_webp") == features.check_module("webp") + + +def test_webp_mux() -> None: + assert features.check("webp_mux") == features.check_module("webp") + + +def test_webp_anim() -> None: + assert features.check("webp_anim") == features.check_module("webp") + + @skip_unless_feature("libjpeg_turbo") def test_libjpeg_turbo_version() -> None: version = features.version("libjpeg_turbo") diff --git a/docs/reference/features.rst b/docs/reference/features.rst index 26c8ab8cd..fcff96735 100644 --- a/docs/reference/features.rst +++ b/docs/reference/features.rst @@ -57,6 +57,9 @@ Support for the following features can be checked: * ``raqm``: Raqm library, required for ``ImageFont.Layout.RAQM`` in :py:func:`PIL.ImageFont.truetype`. Run-time version number is available for Raqm 0.7.0 or newer. * ``libimagequant``: (compile time) ImageQuant quantization support in :py:func:`PIL.Image.Image.quantize`. Run-time version number is available. * ``xcb``: (compile time) Support for X11 in :py:func:`PIL.ImageGrab.grab` via the XCB library. +* ``transp_webp``: Deprecated. Always ``True`` if WebP module is installed. +* ``webp_mux``: Deprecated. Always ``True`` if WebP module is installed. +* ``webp_anim``: Deprecated. Always ``True`` if WebP module is installed. .. autofunction:: PIL.features.check_feature .. autofunction:: PIL.features.version_feature diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index 1d1afcde5..bb707a044 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -58,6 +58,14 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). +Specific WebP Feature Checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The following features ``features.check("transp_webp")``, +``features.check("webp_mux")``, and ``features.check("webp_anim")`` are now +always ``True`` if the WebP module is installed and should not be used. +These checks will be removed in Pillow 12.0.0 (2025-10-15). + API Changes =========== diff --git a/src/PIL/features.py b/src/PIL/features.py index bbf7c641b..f594a2cdc 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -7,6 +7,7 @@ import warnings from typing import IO import PIL +from PIL import _deprecate from . import Image @@ -119,6 +120,9 @@ def get_supported_codecs() -> list[str]: features = { + "webp_anim": ("PIL._webp", True, None), + "webp_mux": ("PIL._webp", True, None), + "transp_webp": ("PIL._webp", True, None), "raqm": ("PIL._imagingft", "HAVE_RAQM", "raqm_version"), "fribidi": ("PIL._imagingft", "HAVE_FRIBIDI", "fribidi_version"), "harfbuzz": ("PIL._imagingft", "HAVE_HARFBUZZ", "harfbuzz_version"), @@ -144,7 +148,11 @@ def check_feature(feature: str) -> bool | None: try: imported_module = __import__(module, fromlist=["PIL"]) - return getattr(imported_module, flag) + if isinstance(flag, str): + return getattr(imported_module, flag) + else: + _deprecate.deprecate(f'check_feature("{feature}")', 12) + return flag except ModuleNotFoundError: return None except ImportError as ex: From 6180abc75c3dfa950276b2586cd1a8aab1bede70 Mon Sep 17 00:00:00 2001 From: Aleksandr Karpinskii Date: Mon, 8 Jul 2024 20:32:35 +0400 Subject: [PATCH 07/52] Remove WebP versions notes from docs --- docs/handbook/image-file-formats.rst | 1 - docs/installation/building-from-source.rst | 4 ---- 2 files changed, 5 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 49a17bb5c..861e09a43 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1248,7 +1248,6 @@ The :py:meth:`~PIL.Image.Image.save` method supports the following options: **exact** If true, preserve the transparent RGB values. Otherwise, discard invisible RGB values for better compression. Defaults to false. - Requires libwebp 0.5.0 or later. **icc_profile** The ICC Profile to include in the saved file. diff --git a/docs/installation/building-from-source.rst b/docs/installation/building-from-source.rst index f47edbc5b..71787311f 100644 --- a/docs/installation/building-from-source.rst +++ b/docs/installation/building-from-source.rst @@ -55,10 +55,6 @@ Many of Pillow's features require external libraries: * **libwebp** provides the WebP format. - * Pillow has been tested with version **0.1.3**, which does not read - transparent WebP files. Versions **0.3.0** and above support - transparency. - * **openjpeg** provides JPEG 2000 functionality. * Pillow has been tested with openjpeg **2.0.0**, **2.1.0**, **2.3.1**, From 924df9e60b39c6ced91553895497fa43db2d232d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 06:46:46 +1000 Subject: [PATCH 08/52] Moved line after early return Improve compiler advice Update src/PIL/features.py --- src/PIL/features.py | 5 ++--- src/_webp.c | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index f594a2cdc..a5487042e 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -148,11 +148,10 @@ def check_feature(feature: str) -> bool | None: try: imported_module = __import__(module, fromlist=["PIL"]) - if isinstance(flag, str): - return getattr(imported_module, flag) - else: + if isinstance(flag, bool): _deprecate.deprecate(f'check_feature("{feature}")', 12) return flag + return getattr(imported_module, flag) except ModuleNotFoundError: return None except ImportError as ex: diff --git a/src/_webp.c b/src/_webp.c index 8302925b0..306a290bd 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -14,7 +14,7 @@ * versions, before enabling animation support. */ #if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 -#error libwebp 0.5.0 and above is required. Upgrade libwebp or build with --disable-webp flag +#error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag #endif void @@ -784,13 +784,13 @@ static PyMethodDef webpMethods[] = { static int setup_module(PyObject *m) { - PyObject *d = PyModule_GetDict(m); /* Ready object types */ if (PyType_Ready(&WebPAnimDecoder_Type) < 0 || PyType_Ready(&WebPAnimEncoder_Type) < 0) { return -1; } + PyObject *d = PyModule_GetDict(m); PyObject *v = PyUnicode_FromString(WebPDecoderVersion_str()); PyDict_SetItemString(d, "webpdecoder_version", v ? v : Py_None); Py_XDECREF(v); From 93ce9ce0048639706eecd523fc54c7d6cfc15bef Mon Sep 17 00:00:00 2001 From: Alexander Karpinsky Date: Tue, 13 Aug 2024 09:52:07 +0400 Subject: [PATCH 09/52] Update features type Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index a5487042e..9421dbd3c 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -119,7 +119,7 @@ def get_supported_codecs() -> list[str]: return [f for f in codecs if check_codec(f)] -features = { +features: dict[str, tuple[str, str | bool, str | None]] = { "webp_anim": ("PIL._webp", True, None), "webp_mux": ("PIL._webp", True, None), "transp_webp": ("PIL._webp", True, None), From 55469948282fe2dd763dcb8e66a767b5c63adb54 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:03:14 +1000 Subject: [PATCH 10/52] Removed unnecessary variable --- Tests/test_file_webp_alpha.py | 2 +- Tests/test_file_webp_lossless.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_alpha.py b/Tests/test_file_webp_alpha.py index 53cdfff03..c88fe3589 100644 --- a/Tests/test_file_webp_alpha.py +++ b/Tests/test_file_webp_alpha.py @@ -13,7 +13,7 @@ from .helper import ( hopper, ) -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +pytest.importorskip("PIL._webp", reason="WebP support not installed") def test_read_rgba() -> None: diff --git a/Tests/test_file_webp_lossless.py b/Tests/test_file_webp_lossless.py index dc98ad3f0..80429715e 100644 --- a/Tests/test_file_webp_lossless.py +++ b/Tests/test_file_webp_lossless.py @@ -8,7 +8,7 @@ from PIL import Image from .helper import assert_image_equal, hopper -_webp = pytest.importorskip("PIL._webp", reason="WebP support not installed") +pytest.importorskip("PIL._webp", reason="WebP support not installed") RGB_MODE = "RGB" From f3aec6dd383bb27676e7e02a7749a5bff1cc30c2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:05:32 +1000 Subject: [PATCH 11/52] Simplified code --- setup.py | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/setup.py b/setup.py index edfa9ee28..863d102cf 100644 --- a/setup.py +++ b/setup.py @@ -794,25 +794,17 @@ class pil_build_ext(build_ext): if feature.want("webp"): _dbg("Looking for webp") if all( - _find_include_file(self, src) - for src in [ - "webp/encode.h", - "webp/decode.h", - "webp/mux.h", - "webp/demux.h", - ] + _find_include_file(self, "webp/" + include) + for include in ("encode.h", "decode.h", "mux.h", "demux.h") ): - # In Google's precompiled zip it is call "libwebp": - if all( - _find_library_file(self, lib) - for lib in ["webp", "webpmux", "webpdemux"] - ): - feature.webp = "webp" - elif all( - _find_library_file(self, lib) - for lib in ["libwebp", "libwebpmux", "libwebpdemux"] - ): - feature.webp = "libwebp" + # In Google's precompiled zip it is called "libwebp" + for prefix in ("", "lib"): + if all( + _find_library_file(self, prefix + library) + for library in ("webp", "webpmux", "webpdemux") + ): + feature.webp = prefix + "webp" + break if feature.want("xcb"): _dbg("Looking for xcb") @@ -901,12 +893,8 @@ class pil_build_ext(build_ext): self._remove_extension("PIL._imagingcms") if feature.webp: - libs = [ - feature.webp, - feature.webp + "mux", - feature.webp + "demux", - ] - self._update_extension("PIL._webp", libs, []) + libs = [feature.webp, feature.webp + "mux", feature.webp + "demux"] + self._update_extension("PIL._webp", libs) else: self._remove_extension("PIL._webp") From c7e6289b36d4eb1823b6bfcace27674826a58e43 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:07:01 +1000 Subject: [PATCH 12/52] Use relative import --- src/PIL/features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/features.py b/src/PIL/features.py index 9421dbd3c..e505adae0 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -7,9 +7,9 @@ import warnings from typing import IO import PIL -from PIL import _deprecate from . import Image +from ._deprecate import deprecate modules = { "pil": ("PIL._imaging", "PILLOW_VERSION"), @@ -149,7 +149,7 @@ def check_feature(feature: str) -> bool | None: try: imported_module = __import__(module, fromlist=["PIL"]) if isinstance(flag, bool): - _deprecate.deprecate(f'check_feature("{feature}")', 12) + deprecate(f'check_feature("{feature}")', 12) return flag return getattr(imported_module, flag) except ModuleNotFoundError: From ba82dff7bc6c489f299a63d2f2d3722dfe11852e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:10:54 +1000 Subject: [PATCH 13/52] Updated test name --- Tests/test_file_webp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 6e9eea4ae..6ccd489bb 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -166,7 +166,7 @@ class TestFileWebp: with pytest.raises(TypeError): _webp.WebPEncode() - def test_WebPDecode_with_invalid_args(self) -> None: + def test_WebPAnimDecoder_with_invalid_args(self) -> None: """ Calling decoder functions with no arguments should result in an error. """ From 66319fcce764f4f09e53900b2896ec15457f691f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:17:36 +1000 Subject: [PATCH 14/52] Animation support is no longer conditionally enabled --- src/_webp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_webp.c b/src/_webp.c index 306a290bd..d1943b3e0 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -11,7 +11,7 @@ * Check the versions from mux.h and demux.h, to ensure the WebPAnimEncoder and * WebPAnimDecoder APIs are present (initial support was added in 0.5.0). The * very early versions had some significant differences, so we require later - * versions, before enabling animation support. + * versions. */ #if WEBP_MUX_ABI_VERSION < 0x0106 || WEBP_DEMUX_ABI_VERSION < 0x0107 #error libwebp 0.5.0 and above is required. Upgrade libwebp or build Pillow with --disable-webp flag From 45552b5b4f6ea470639e5c7ff0d9efb96ad7db68 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 13 Aug 2024 19:24:34 +1000 Subject: [PATCH 15/52] Updated documentation --- docs/deprecations.rst | 10 ++++++++++ docs/releasenotes/11.0.0.rst | 8 ++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 058468cfe..a9498d5ed 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -126,6 +126,16 @@ JpegImageFile.huffman_ac and JpegImageFile.huffman_dc The ``huffman_ac`` and ``huffman_dc`` dictionaries on JPEG images were unused. They have been deprecated, and will be removed in Pillow 12 (2025-10-15). +Specific WebP Feature Checks +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 11.0.0 + +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` are now deprecated. They will always return +``True`` if the WebP module is installed, until they are removed in Pillow +12.0.0 (2025-10-15). + Removed features ---------------- diff --git a/docs/releasenotes/11.0.0.rst b/docs/releasenotes/11.0.0.rst index bb707a044..ac9237acf 100644 --- a/docs/releasenotes/11.0.0.rst +++ b/docs/releasenotes/11.0.0.rst @@ -61,10 +61,10 @@ have been deprecated, and will be removed in Pillow 12 (2025-10-15). Specific WebP Feature Checks ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The following features ``features.check("transp_webp")``, -``features.check("webp_mux")``, and ``features.check("webp_anim")`` are now -always ``True`` if the WebP module is installed and should not be used. -These checks will be removed in Pillow 12.0.0 (2025-10-15). +``features.check("transp_webp")``, ``features.check("webp_mux")`` and +``features.check("webp_anim")`` are now deprecated. They will always return +``True`` if the WebP module is installed, until they are removed in Pillow +12.0.0 (2025-10-15). API Changes =========== From 359d7592c7861729092434154d131310c7bf1d71 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 14 Aug 2024 18:41:39 +1000 Subject: [PATCH 16/52] Test deprecation warnings --- Tests/test_features.py | 21 ++++++++++++++++----- src/PIL/features.py | 12 +++++++++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index cb8fd8688..807782847 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -18,7 +18,11 @@ def test_check() -> None: for codec in features.codecs: assert features.check_codec(codec) == features.check(codec) for feature in features.features: - assert features.check_feature(feature) == features.check(feature) + if "webp" in feature: + with pytest.warns(DeprecationWarning): + assert features.check_feature(feature) == features.check(feature) + else: + assert features.check_feature(feature) == features.check(feature) def test_version() -> None: @@ -43,19 +47,26 @@ def test_version() -> None: for codec in features.codecs: test(codec, features.version_codec) for feature in features.features: - test(feature, features.version_feature) + if "webp" in feature: + with pytest.warns(DeprecationWarning): + test(feature, features.version_feature) + else: + test(feature, features.version_feature) def test_webp_transparency() -> None: - assert features.check("transp_webp") == features.check_module("webp") + with pytest.warns(DeprecationWarning): + assert features.check("transp_webp") == features.check_module("webp") def test_webp_mux() -> None: - assert features.check("webp_mux") == features.check_module("webp") + with pytest.warns(DeprecationWarning): + assert features.check("webp_mux") == features.check_module("webp") def test_webp_anim() -> None: - assert features.check("webp_anim") == features.check_module("webp") + with pytest.warns(DeprecationWarning): + assert features.check("webp_anim") == features.check_module("webp") @skip_unless_feature("libjpeg_turbo") diff --git a/src/PIL/features.py b/src/PIL/features.py index e505adae0..24c5ee978 100644 --- a/src/PIL/features.py +++ b/src/PIL/features.py @@ -180,7 +180,17 @@ def get_supported_features() -> list[str]: """ :returns: A list of all supported features. """ - return [f for f in features if check_feature(f)] + supported_features = [] + for f, (module, flag, _) in features.items(): + if flag is True: + for feature, (feature_module, _) in modules.items(): + if feature_module == module: + if check_module(feature): + supported_features.append(f) + break + elif check_feature(f): + supported_features.append(f) + return supported_features def check(feature: str) -> bool | None: From b14142462e1d4f0205b7e77c647a02c760b99a05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 05:11:00 +1000 Subject: [PATCH 17/52] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ae995e0a0..25dcb1887 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Remove WebP support without anim, mux/demux, and with buggy alpha #8213 + [homm, radarhere] + +- Add missing TIFF CMYK;16B reader #8298 + [homm] + - Remove all WITH_* flags from _imaging.c and other flags #8211 [homm] From 873770978197484b284a267d247d1b832afc2a24 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 13:32:59 +1000 Subject: [PATCH 18/52] Added return type to ImageFile.load() --- src/PIL/Image.py | 8 +++----- src/PIL/ImageFile.py | 19 ++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ebf4f46c4..a6eefff56 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -218,9 +218,10 @@ if hasattr(core, "DEFAULT_STRATEGY"): # Registries if TYPE_CHECKING: + import mmap from xml.etree.ElementTree import Element - from . import ImageFile, ImagePalette, TiffImagePlugin + from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ @@ -612,7 +613,7 @@ class Image: logger.debug("Error closing: %s", msg) if getattr(self, "map", None): - self.map = None + self.map: mmap.mmap | None = None # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image @@ -1336,9 +1337,6 @@ class Image: self.load() return self._new(self.im.expand(xmargin, ymargin)) - if TYPE_CHECKING: - from . import ImageFilter - def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: """ Filters this image using the given filter. For a list of diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 2a8846c1a..829082e94 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -174,7 +174,7 @@ class ImageFile(Image.Image): self.fp.close() self.fp = None - def load(self): + def load(self) -> Image.core.PixelAccess | None: """Load image data based on tile list""" if self.tile is None: @@ -185,7 +185,7 @@ class ImageFile(Image.Image): if not self.tile: return pixel - self.map = None + self.map: mmap.mmap | None = None use_mmap = self.filename and len(self.tile) == 1 # As of pypy 2.1.0, memory mapping was failing here. use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") @@ -193,17 +193,17 @@ class ImageFile(Image.Image): readonly = 0 # look for read/seek overrides - try: + if hasattr(self, "load_read"): read = self.load_read # don't use mmap if there are custom read/seek functions use_mmap = False - except AttributeError: + else: read = self.fp.read - try: + if hasattr(self, "load_seek"): seek = self.load_seek use_mmap = False - except AttributeError: + else: seek = self.fp.seek if use_mmap: @@ -243,11 +243,8 @@ class ImageFile(Image.Image): # sort tiles in file order self.tile.sort(key=_tilesort) - try: - # FIXME: This is a hack to handle TIFF's JpegTables tag. - prefix = self.tile_prefix - except AttributeError: - prefix = b"" + # FIXME: This is a hack to handle TIFF's JpegTables tag. + prefix = getattr(self, "tile_prefix", b"") # Remove consecutive duplicates that only differ by their offset self.tile = [ From 497080f63b62c9acbb427c7e554f222de70fe3a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 15:20:34 +1000 Subject: [PATCH 19/52] Added type hint to ImageFile._save tile parameter --- Tests/test_imagefile.py | 30 ++++++++++++++++++++++++------ src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 4 +++- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 8 ++++++-- src/PIL/IcoImagePlugin.py | 4 +++- src/PIL/ImImagePlugin.py | 4 +++- src/PIL/ImageFile.py | 4 ++-- src/PIL/Jpeg2KImagePlugin.py | 2 +- src/PIL/JpegImagePlugin.py | 4 +++- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PalmImagePlugin.py | 4 +++- src/PIL/PcxImagePlugin.py | 4 +++- src/PIL/PdfImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 8 ++++---- src/PIL/PpmImagePlugin.py | 4 +++- src/PIL/SpiderImagePlugin.py | 4 +++- src/PIL/TgaImagePlugin.py | 8 ++++++-- src/PIL/XbmImagePlugin.py | 2 +- 19 files changed, 72 insertions(+), 30 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index fe7d44785..95e91db83 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -317,7 +317,13 @@ class TestPyEncoder(CodecsTest): fp = BytesIO() ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + im, + fp, + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB" + ) + ], ) assert MockPyEncoder.last @@ -333,7 +339,7 @@ class TestPyEncoder(CodecsTest): im.tile = [("MOCK", None, 32, None)] fp = BytesIO() - ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) assert MockPyEncoder.last assert MockPyEncoder.last.state.xoff == 0 @@ -350,7 +356,9 @@ class TestPyEncoder(CodecsTest): MockPyEncoder.last = None with pytest.raises(ValueError): ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + im, + fp, + [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")], ) last: MockPyEncoder | None = MockPyEncoder.last assert last @@ -358,7 +366,9 @@ class TestPyEncoder(CodecsTest): with pytest.raises(ValueError): ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + im, + fp, + [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")], ) def test_oversize(self) -> None: @@ -371,14 +381,22 @@ class TestPyEncoder(CodecsTest): ImageFile._save( im, fp, - [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB" + ) + ], ) with pytest.raises(ValueError): ImageFile._save( im, fp, - [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB" + ) + ], ) def test_encode(self) -> None: diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 6d71049a9..569f2c9bf 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -477,7 +477,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(struct.pack(" None: if bits != 32: and_mask = Image.new("1", size) ImageFile._save( - and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] + and_mask, + image_io, + [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], ) else: frame.save(image_io, "png") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 2fb7ecd52..b94165089 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -360,7 +360,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: palette += im_palette[colors * i : colors * (i + 1)] palette += b"\x00" * (256 - colors) fp.write(palette) # 768 bytes - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))] + ) # diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 829082e94..73554fa53 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -93,7 +93,7 @@ def _tilesort(t: _Tile) -> int: class _Tile(NamedTuple): codec_name: str - extents: tuple[int, int, int, int] + extents: tuple[int, int, int, int] | None offset: int args: tuple[Any, ...] | str | None @@ -522,7 +522,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im: Image.Image, fp: IO[bytes], tile, bufsize: int = 0) -> None: +def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None: """Helper to save image based on tile list :param im: Image object. diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index ef9107f00..02c2e48cf 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -419,7 +419,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: plt, ) - ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) + ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)]) # ------------------------------------------------------------ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index fc897e2b9..bd4539be4 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -826,7 +826,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Ensure that our buffer is big enough. Same with the icc_profile block. bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) - ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) + ImageFile._save( + im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize + ) def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 0a75c868b..40e5fa435 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(o16(h)) # image body - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) # diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 1735070f8..62bf5f542 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -213,7 +213,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ) # now convert data to raw form - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))] + ) if hasattr(fp, "flush"): fp.flush() diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index dd42003b5..4fb04715b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -198,7 +198,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: assert fp.tell() == 128 - ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) + ImageFile._save( + im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))] + ) if im.mode == "P": # colour palette diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 7fc1108bb..e9c20ddc1 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -138,7 +138,7 @@ def _write_image( op = io.BytesIO() if decode_filter == "ASCIIHexDecode": - ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)]) elif decode_filter == "CCITTFaxDecode": im.save( op, diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 910fa9755..fc20b18a8 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1226,7 +1226,7 @@ def _write_multiple_frames( ImageFile._save( im, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + im.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], ) seq_num = 0 @@ -1263,14 +1263,14 @@ def _write_multiple_frames( ImageFile._save( im_frame, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + im_frame.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], ) else: fdat_chunks = _fdat(fp, chunk, seq_num) ImageFile._save( im_frame, cast(IO[bytes], fdat_chunks), - [("zip", (0, 0) + im_frame.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num return None @@ -1471,7 +1471,7 @@ def _save( ImageFile._save( single_im, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + single_im.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)], ) if info: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 16c9ccbba..7bdaa9fe7 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -353,7 +353,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: elif head == b"Pf": fp.write(b"-1.0\n") row_order = -1 if im.mode == "F" else 1 - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))] + ) # diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index a07101e54..7045ab566 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -278,7 +278,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.writelines(hdr) rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] + ) def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 39104aece..a43aae1ec 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -238,11 +238,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if rle: ImageFile._save( - im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] + im, + fp, + [ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))], ) else: ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))], ) # write targa version 2 footer diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 6d11bbfcf..6c2e32804 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0, None)]) fp.write(b"};\n") From 8afb7ddb4ed265caf67213f4fd5416cd29ae24a1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 08:08:43 +1000 Subject: [PATCH 20/52] Added type hints --- .ci/requirements-mypy.txt | 1 + docs/example/DdsImagePlugin.py | 29 +++++++++++++++------------ src/PIL/TiffImagePlugin.py | 36 ++++++++++++++++++++-------------- tox.ini | 2 +- winbuild/build_prepare.py | 12 ++++++------ 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 23792281b..47fc64399 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -5,6 +5,7 @@ ipython numpy packaging pytest +sphinx types-defusedxml types-olefile types-setuptools diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 2a2a0ba29..caa852b1f 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -14,6 +14,7 @@ from __future__ import annotations import struct from io import BytesIO +from typing import IO from PIL import Image, ImageFile @@ -94,26 +95,26 @@ DXT3_FOURCC = 0x33545844 DXT5_FOURCC = 0x35545844 -def _decode565(bits): +def _decode565(bits: int) -> tuple[int, int, int]: a = ((bits >> 11) & 0x1F) << 3 b = ((bits >> 5) & 0x3F) << 2 c = (bits & 0x1F) << 3 return a, b, c -def _c2a(a, b): +def _c2a(a: int, b: int) -> int: return (2 * a + b) // 3 -def _c2b(a, b): +def _c2b(a: int, b: int) -> int: return (a + b) // 2 -def _c3(a, b): +def _c3(a: int, b: int) -> int: return (2 * b + a) // 3 -def _dxt1(data, width, height): +def _dxt1(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -151,7 +152,7 @@ def _dxt1(data, width, height): return bytes(ret) -def _dxtc_alpha(a0, a1, ac0, ac1, ai): +def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int: if ai <= 12: ac = (ac0 >> ai) & 7 elif ai == 15: @@ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai): return alpha -def _dxt5(data, width, height): +def _dxt5(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -211,7 +212,7 @@ class DdsImageFile(ImageFile.ImageFile): format = "DDS" format_description = "DirectDraw Surface" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) @@ -242,19 +243,20 @@ class DdsImageFile(ImageFile.ImageFile): elif fourcc == b"DXT5": self.decoder = "DXT5" else: - msg = f"Unimplemented pixel format {fourcc}" + msg = f"Unimplemented pixel format {repr(fourcc)}" raise NotImplementedError(msg) self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass class DXT1Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -266,7 +268,8 @@ class DXT1Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -279,7 +282,7 @@ Image.register_decoder("DXT1", DXT1Decoder) Image.register_decoder("DXT5", DXT5Decoder) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"DDS " diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index deae199d5..2ce8a6c2a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -456,8 +456,11 @@ class IFDRational(Rational): __int__ = _delegate("__int__") -def _register_loader(idx: int, size: int): - def decorator(func): +_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] + + +def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]: + def decorator(func: _LoaderFunc) -> _LoaderFunc: from .TiffTags import TYPES if func.__name__.startswith("load_"): @@ -482,12 +485,13 @@ def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize(f"={fmt}") - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) + + def basic_handler( + self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True + ) -> tuple[Any, ...]: + return self._unpack(f"{len(data) // size}{fmt}", data) + + _load_dispatch[idx] = size, basic_handler # noqa: F821 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 b"".join(self._pack(fmt, value) for value in values) ) @@ -560,7 +564,7 @@ class ImageFileDirectory_v2(_IFDv2Base): """ - _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {} def __init__( @@ -653,10 +657,10 @@ class ImageFileDirectory_v2(_IFDv2Base): def __contains__(self, tag: object) -> bool: return tag in self._tags_v2 or tag in self._tagdata - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: self._setitem(tag, value, self.legacy_api) - def _setitem(self, tag: int, value, legacy_api: bool) -> None: + def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: basetypes = (Number, bytes, str) info = TiffTags.lookup(tag, self.group) @@ -744,10 +748,10 @@ class ImageFileDirectory_v2(_IFDv2Base): def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v2)) - def _unpack(self, fmt: str, data: bytes): + def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]: return struct.unpack(self._endian + fmt, data) - def _pack(self, fmt: str, *values) -> bytes: + def _pack(self, fmt: str, *values: Any) -> bytes: return struct.pack(self._endian + fmt, *values) list( @@ -824,7 +828,9 @@ class ImageFileDirectory_v2(_IFDv2Base): return value @_register_loader(10, 8) - def load_signed_rational(self, data: bytes, legacy_api: bool = True): + def load_signed_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: vals = self._unpack(f"{len(data) // 4}l", data) def combine(a: int, b: int) -> tuple[int, int] | IFDRational: @@ -1088,7 +1094,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v1)) - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: for legacy_api in (False, True): self._setitem(tag, value, legacy_api) diff --git a/tox.ini b/tox.ini index 4b4059455..70b8bf145 100644 --- a/tox.ini +++ b/tox.ini @@ -36,4 +36,4 @@ deps = extras = typing commands = - mypy src Tests {posargs} + mypy docs src winbuild Tests {posargs} diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 7129699eb..1021e4f22 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import re import shutil import struct import subprocess +from typing import Any def cmd_cd(path: str) -> str: @@ -43,21 +44,19 @@ def cmd_nmake( target: str = "", params: list[str] | None = None, ) -> str: - params = "" if params is None else " ".join(params) - return " ".join( [ "{nmake}", "-nologo", f'-f "{makefile}"' if makefile is not None else "", - f"{params}", + f'{" ".join(params)}' if params is not None else "", f'"{target}"', ] ) def cmds_cmake( - target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." + target: str | tuple[str, ...] | list[str], *params: str, build_dir: str = "." ) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -129,7 +128,7 @@ V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation -DEPS = { +DEPS: dict[str, dict[str, Any]] = { "libjpeg": { "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", @@ -538,7 +537,7 @@ def write_script( print(" " + line) -def get_footer(dep: dict) -> list[str]: +def get_footer(dep: dict[str, Any]) -> list[str]: lines = [] for out in dep.get("headers", []): lines.append(cmd_copy(out, "{inc_dir}")) @@ -583,6 +582,7 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: license_text += f.read() if "license_pattern" in dep: match = re.search(dep["license_pattern"], license_text, re.DOTALL) + assert match is not None license_text = "\n".join(match.groups()) assert len(license_text) > 50 with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: From d4c72da6b2ae5aabc45f679f8feeaf2726976a2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 14:10:31 +1000 Subject: [PATCH 21/52] Added type hints to example code --- docs/handbook/image-file-formats.rst | 12 +++++++----- docs/handbook/tutorial.rst | 6 +++--- docs/handbook/writing-your-own-image-plugin.rst | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 861e09a43..ca0e05eb6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1517,19 +1517,21 @@ To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF handler. :: - from PIL import Image + from typing import IO + + from PIL import Image, ImageFile from PIL import WmfImagePlugin - class WmfHandler: - def open(self, im): + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: ... - def load(self, im): + def load(self, im: ImageFile.StubImageFile) -> Image.Image: ... return image - def save(self, im, fp, filename): + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: ... diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index c36011362..3df8e0d20 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -186,7 +186,7 @@ Rolling an image :: - def roll(im, delta): + def roll(im: Image.Image, delta: int) -> Image.Image: """Roll an image sideways.""" xsize, ysize = im.size @@ -211,7 +211,7 @@ Merging images :: - def merge(im1, im2): + def merge(im1: Image.Image, im2: Image.Image) -> Image.Image: w = im1.size[0] + im2.size[0] h = max(im1.size[1], im2.size[1]) im = Image.new("RGBA", (w, h)) @@ -704,7 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality. import glob from PIL import Image - def compress_image(source_path, dest_path): + def compress_image(source_path: str, dest_path: str) -> None: with Image.open(source_path) as img: if img.mode != "RGB": img = img.convert("RGB") diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 956d63aa7..2e853224d 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -53,7 +53,7 @@ true color. from PIL import Image, ImageFile - def _accept(prefix): + def _accept(prefix: bytes) -> bool: return prefix[:4] == b"SPAM" @@ -62,7 +62,7 @@ true color. format = "SPAM" format_description = "Spam raster image" - def _open(self): + def _open(self) -> None: header = self.fp.read(128).split() @@ -82,7 +82,7 @@ true color. raise SyntaxError(msg) # data descriptor - self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] Image.register_open(SpamImageFile.format, SpamImageFile, _accept) From d5e3f6b51673564c16a8973a72a2b68e8ba96f0c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 14:46:23 +1000 Subject: [PATCH 22/52] If left and right sides meet, do not draw rectangle to fill gap --- ...d_rectangle_joined_x_different_corners.png | Bin 0 -> 411 bytes Tests/test_imagedraw.py | 21 ++++++++++++++++++ src/PIL/ImageDraw.py | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png diff --git a/Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png b/Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png new file mode 100644 index 0000000000000000000000000000000000000000..b225afc2dc1449375e748d59e2336eedd9ddceaf GIT binary patch literal 411 zcmeAS@N?(olHy`uVBq!ia0vp^DIm None: + # Arrange + im = Image.new("RGB", (W, H)) + draw = ImageDraw.Draw(im, "RGBA") + + # Act + draw.rounded_rectangle( + (20, 10, 80, 90), + 30, + fill="red", + outline="green", + width=5, + corners=(True, False, False, False), + ) + + # Assert + assert_image_equal_tofile( + im, "Tests/images/imagedraw_rounded_rectangle_joined_x_different_corners.png" + ) + + @pytest.mark.parametrize( "xy, radius, type", [ diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 6f56d0236..5609bf971 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -505,7 +505,7 @@ class ImageDraw: if full_x: self.draw.draw_rectangle((x0, y0 + r + 1, x1, y1 - r - 1), fill_ink, 1) - else: + elif x1 - r - 1 > x0 + r + 1: self.draw.draw_rectangle((x0 + r + 1, y0, x1 - r - 1, y1), fill_ink, 1) if not full_x and not full_y: left = [x0, y0, x0 + r, y1] From 0a03b77daf088e2aac0e32acbbc370704c749591 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Aug 2024 06:36:31 +1000 Subject: [PATCH 23/52] Update CHANGES.rst [ci skip] --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 25dcb1887..a7edc340c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,9 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304 + [radarhere] + - Remove WebP support without anim, mux/demux, and with buggy alpha #8213 [homm, radarhere] From 617699ffc76230d481d903c7a9b000d96b7e89e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 16 Aug 2024 12:03:02 +1000 Subject: [PATCH 24/52] Log value from tag_v2 --- src/PIL/TiffImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 2ce8a6c2a..7bbbc832e 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1393,7 +1393,7 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- photometric_interpretation: %s", photo) logger.debug("- planar_configuration: %s", self._planar_configuration) logger.debug("- fill_order: %s", fillorder) - logger.debug("- YCbCr subsampling: %s", self.tag.get(YCBCRSUBSAMPLING)) + logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size xsize = int(self.tag_v2.get(IMAGEWIDTH)) From 6f506d2ae3879256e8a893233fc28ba6fb2518d8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:59:46 +0300 Subject: [PATCH 25/52] Run all flake8-pytest-style except rules some that fail --- .pre-commit-config.yaml | 6 +++--- pyproject.toml | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74764752e..8ab478e84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.6.0 hooks: - id: ruff args: [--exit-non-zero-on-fix] - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.8.0 hooks: - id: black @@ -67,7 +67,7 @@ repos: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.18 + rev: v0.19 hooks: - id: validate-pyproject diff --git a/pyproject.toml b/pyproject.toml index 8bb21019c..d4b83ef7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PGH", # pygrep-hooks - "PT006", # pytest-parametrize-names-wrong-type + "PT", # flake8-pytest-style "PYI", # flake8-pyi "RUF100", # unused noqa (yesqa) "UP", # pyupgrade @@ -121,6 +121,14 @@ lint.ignore = [ "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator "E241", # Multiple spaces after ',' + "PT001", # pytest-fixture-incorrect-parentheses-style + "PT007", # pytest-parametrize-values-wrong-type + "PT011", # pytest-raises-too-broad + "PT012", # pytest-raises-with-multiple-statements + "PT014", # pytest-duplicate-parametrize-test-cases + "PT016", # pytest-fail-without-message + "PT017", # pytest-assert-in-except + "PT018", # pytest-composite-assertion "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 ] From 5c282d02991e6bca1993c974d85dd19bce734ddd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:47:11 +0300 Subject: [PATCH 26/52] Fix PT014: duplicate test cases in pytest.mark.parametrize --- Tests/test_file_jpeg2k.py | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a5cfa7c6c..5e11465ca 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -233,7 +233,7 @@ def test_layers() -> None: ("foo.jp2", {"no_jp2": True}, 0, b"\xff\x4f"), ("foo.j2k", {"no_jp2": False}, 0, b"\xff\x4f"), ("foo.jp2", {"no_jp2": False}, 4, b"jP"), - ("foo.jp2", {"no_jp2": False}, 4, b"jP"), + (None, {"no_jp2": False}, 4, b"jP"), ), ) def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: diff --git a/pyproject.toml b/pyproject.toml index d4b83ef7f..b42ad7424 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,7 +125,6 @@ lint.ignore = [ "PT007", # pytest-parametrize-values-wrong-type "PT011", # pytest-raises-too-broad "PT012", # pytest-raises-with-multiple-statements - "PT014", # pytest-duplicate-parametrize-test-cases "PT016", # pytest-fail-without-message "PT017", # pytest-assert-in-except "PT018", # pytest-composite-assertion From 5747267eb39961101d8ed6f18e77cefc1fc5c4b8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:52:56 +0300 Subject: [PATCH 27/52] Fix PT018: Assert only one thing --- Tests/test_file_jpeg.py | 9 ++++++--- Tests/test_imagechops.py | 3 ++- Tests/test_imagemorph.py | 3 ++- Tests/test_tiff_ifdrational.py | 4 ++-- pyproject.toml | 1 - 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index df589e24a..8e6221750 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -1019,13 +1019,16 @@ class TestFileJpeg: # SOI, EOI for marker in b"\xff\xd8", b"\xff\xd9": - assert marker in data[1] and marker in data[2] + assert marker in data[1] + assert marker in data[2] # DHT, DQT for marker in b"\xff\xc4", b"\xff\xdb": - assert marker in data[1] and marker not in data[2] + assert marker in data[1] + assert marker not in data[2] # SOF0, SOS, APP0 (JFIF header) for marker in b"\xff\xc0", b"\xff\xda", b"\xff\xe0": - assert marker not in data[1] and marker in data[2] + assert marker not in data[1] + assert marker in data[2] with Image.open(BytesIO(data[0])) as interchange_im: with Image.open(BytesIO(data[1] + data[2])) as combined_im: diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 4fc28cdb9..4309214f5 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -398,7 +398,8 @@ def test_logical() -> None: for y in (a, b): imy = Image.new("1", (1, 1), y) value = op(imx, imy).getpixel((0, 0)) - assert not isinstance(value, tuple) and value is not None + assert not isinstance(value, tuple) + assert value is not None out.append(value) return out diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 4363f456e..80d8c3815 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -46,7 +46,8 @@ def img_to_string(im: Image.Image) -> str: line = "" for c in range(im.width): value = im.getpixel((c, r)) - assert not isinstance(value, tuple) and value is not None + assert not isinstance(value, tuple) + assert value is not None line += chars[value > 0] result.append(line) return "\n".join(result) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 9d06a9332..13f1f9c80 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -54,8 +54,8 @@ def test_nonetype() -> None: assert xres.denominator is not None assert yres._val is not None - assert xres and 1 - assert xres and yres + assert xres + assert yres @pytest.mark.parametrize( diff --git a/pyproject.toml b/pyproject.toml index b42ad7424..9e53d4bfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,7 +127,6 @@ lint.ignore = [ "PT012", # pytest-raises-with-multiple-statements "PT016", # pytest-fail-without-message "PT017", # pytest-assert-in-except - "PT018", # pytest-composite-assertion "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 ] From f26b47595b10eaf3d781a8ea3f357f78f0a2774c Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 06:56:31 +1000 Subject: [PATCH 28/52] Get IPTC info from tag_v2 --- Tests/test_file_iptc.py | 10 ++++++++++ src/PIL/IptcImagePlugin.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py index b0ea2bf42..8a7c59fb1 100644 --- a/Tests/test_file_iptc.py +++ b/Tests/test_file_iptc.py @@ -77,6 +77,16 @@ def test_getiptcinfo_zero_padding() -> None: assert len(iptc) == 3 +def test_getiptcinfo_tiff() -> None: + # Arrange + with Image.open("Tests/images/hopper.Lab.tif") as im: + # Act + iptc = IptcImagePlugin.getiptcinfo(im) + + # Assert + assert iptc == {(1, 90): b"\x1b%G", (2, 0): b"\xcf\xc0"} + + def test_getiptcinfo_tiff_none() -> None: # Arrange with Image.open("Tests/images/hopper.tif") as im: diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 17243e705..153243519 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -213,7 +213,7 @@ def getiptcinfo( # get raw data from the IPTC/NAA tag (PhotoShop tags the data # as 4-byte integers, so we cannot use the get method...) try: - data = im.tag.tagdata[TiffImagePlugin.IPTC_NAA_CHUNK] + data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK] except (AttributeError, KeyError): pass From cd76b4853319619a0cb9726dcd7907551794f049 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Fri, 16 Aug 2024 09:08:18 -0500 Subject: [PATCH 29/52] move repeated code to private helper function --- src/PIL/TiffImagePlugin.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index deae199d5..7dda413e6 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -2080,38 +2080,34 @@ class AppendingTiffWriter: (value,) = struct.unpack(self.longFmt, self.f.read(4)) return value + @staticmethod + def _verify_bytes_written(bytes_written: int | None, expected: int) -> None: + if bytes_written is not None and bytes_written != expected: + msg = f"wrote only {bytes_written} bytes but wanted {expected}" + raise RuntimeError(msg) + def rewriteLastShortToLong(self, value: int) -> None: self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 4) def rewriteLastShort(self, value: int) -> None: self.f.seek(-2, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - if bytes_written is not None and bytes_written != 2: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 2) def rewriteLastLong(self, value: int) -> None: self.f.seek(-4, os.SEEK_CUR) bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 4) def writeShort(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.shortFmt, value)) - if bytes_written is not None and bytes_written != 2: - msg = f"wrote only {bytes_written} bytes but wanted 2" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 2) def writeLong(self, value: int) -> None: bytes_written = self.f.write(struct.pack(self.longFmt, value)) - if bytes_written is not None and bytes_written != 4: - msg = f"wrote only {bytes_written} bytes but wanted 4" - raise RuntimeError(msg) + self._verify_bytes_written(bytes_written, 4) def close(self) -> None: self.finalize() From 2467369c134a882473e46928eac952d7fab5f7a4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 20:50:45 +1000 Subject: [PATCH 30/52] Uninstall gradle and maven on macOS 13 --- .github/workflows/macos-install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index d35cfcd31..25943776f 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -2,6 +2,9 @@ set -e +if [[ "$ImageOS" == "macos13" ]]; then + brew uninstall gradle maven +fi brew install \ freetype \ ghostscript \ From d1d567bb59886f15e91522d937e5399395158692 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 08:47:35 +1000 Subject: [PATCH 31/52] Do not use sys.stdout in PSDraw --- Tests/test_psdraw.py | 11 +++-------- src/PIL/PSDraw.py | 11 ++++------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/Tests/test_psdraw.py b/Tests/test_psdraw.py index c3afa9089..a743d831f 100644 --- a/Tests/test_psdraw.py +++ b/Tests/test_psdraw.py @@ -5,8 +5,6 @@ import sys from io import BytesIO from pathlib import Path -import pytest - from PIL import Image, PSDraw @@ -49,15 +47,14 @@ def test_draw_postscript(tmp_path: Path) -> None: assert os.path.getsize(tempfile) > 0 -@pytest.mark.parametrize("buffer", (True, False)) -def test_stdout(buffer: bool) -> None: +def test_stdout() -> None: # Temporarily redirect stdout old_stdout = sys.stdout class MyStdOut: buffer = BytesIO() - mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() + mystdout = MyStdOut() sys.stdout = mystdout @@ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None: # Reset stdout sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): - mystdout = mystdout.buffer - assert mystdout.getvalue() != b"" + assert mystdout.buffer.getvalue() != b"" diff --git a/src/PIL/PSDraw.py b/src/PIL/PSDraw.py index 673eae1d1..02939d26b 100644 --- a/src/PIL/PSDraw.py +++ b/src/PIL/PSDraw.py @@ -17,7 +17,7 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING +from typing import IO, TYPE_CHECKING from . import EpsImagePlugin @@ -28,15 +28,12 @@ from . import EpsImagePlugin class PSDraw: """ Sets up printing to the given file. If ``fp`` is omitted, - ``sys.stdout.buffer`` or ``sys.stdout`` is assumed. + ``sys.stdout.buffer`` is assumed. """ - def __init__(self, fp=None): + def __init__(self, fp: IO[bytes] | None = None) -> None: if not fp: - try: - fp = sys.stdout.buffer - except AttributeError: - fp = sys.stdout + fp = sys.stdout.buffer self.fp = fp def begin_document(self, id: str | None = None) -> None: From 2ed8502d12a0f0efd7d4cd2c65b4c6a7e59aaa2a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 19 Aug 2024 22:55:07 +1000 Subject: [PATCH 32/52] Use ipython PrettyPrinter, rather than custom class --- .appveyor.yml | 2 +- .ci/install.sh | 1 + .github/workflows/macos-install.sh | 1 + .github/workflows/test-cygwin.yml | 1 + Tests/test_image.py | 17 +++++++++++------ src/PIL/Image.py | 4 +++- 6 files changed, 18 insertions(+), 8 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index e12987a5f..f490561cd 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -51,7 +51,7 @@ build_script: test_script: - cd c:\pillow -- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' +- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma' - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' diff --git a/.ci/install.sh b/.ci/install.sh index 8e65f64c4..b2d615866 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip python3 -m pip install --upgrade wheel python3 -m pip install coverage python3 -m pip install defusedxml +python3 -m pip install ipython python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov diff --git a/.github/workflows/macos-install.sh b/.github/workflows/macos-install.sh index 25943776f..ddb421230 100755 --- a/.github/workflows/macos-install.sh +++ b/.github/workflows/macos-install.sh @@ -23,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" python3 -m pip install coverage python3 -m pip install defusedxml +python3 -m pip install ipython python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index 8e2827099..0aa79e423 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -74,6 +74,7 @@ jobs: perl python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-devel + python3${{ matrix.python-minor-version }}-ipython python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-tkinter diff --git a/Tests/test_image.py b/Tests/test_image.py index 719732d12..2afcf2efd 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -42,6 +42,12 @@ try: except ImportError: ElementTree = None +PrettyPrinter: type | None +try: + from IPython.lib.pretty import PrettyPrinter +except ImportError: + PrettyPrinter = None + # Deprecation helper def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: @@ -91,16 +97,15 @@ class TestImage: # with pytest.raises(MemoryError): # Image.new("L", (1000000, 1000000)) + @pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed") def test_repr_pretty(self) -> None: - class Pretty: - def text(self, text: str) -> None: - self.pretty_output = text - im = Image.new("L", (100, 100)) - p = Pretty() + output = io.StringIO() + assert PrettyPrinter is not None + p = PrettyPrinter(output) im._repr_pretty_(p, False) - assert p.pretty_output == "" + assert output.getvalue() == "" def test_open_formats(self) -> None: PNGFILE = "Tests/images/hopper.png" diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a6eefff56..705d0a144 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -221,6 +221,8 @@ if TYPE_CHECKING: import mmap from xml.etree.ElementTree import Element + from IPython.lib.pretty import PrettyPrinter + from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] @@ -677,7 +679,7 @@ class Image: id(self), ) - def _repr_pretty_(self, p, cycle: bool) -> None: + def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None: """IPython plain text display support""" # Same as __repr__ but without unpredictable id(self), From e6e5ef5c5fbd83ac5dd63301e4d7d6860a7b2d09 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 21 Aug 2024 08:05:02 +1000 Subject: [PATCH 33/52] Added type hints --- Tests/test_file_tiff.py | 7 +++++ Tests/test_imagefile.py | 3 +- docs/reference/Image.rst | 1 + src/PIL/IcoImagePlugin.py | 4 +-- src/PIL/Image.py | 42 ++++++++++++++++++--------- src/PIL/ImageFile.py | 8 ++++-- src/PIL/IptcImagePlugin.py | 2 +- src/PIL/TiffImagePlugin.py | 59 ++++++++++++++++++++++---------------- 8 files changed, 80 insertions(+), 46 deletions(-) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 8cad25272..190f83f40 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -684,6 +684,13 @@ class TestFileTiff: with Image.open(outfile) as reloaded: assert_image_equal_tofile(reloaded, infile) + def test_invalid_tiled_dimensions(self) -> None: + with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp: + data = fp.read() + b = BytesIO(data[:144] + b"\x02" + data[145:]) + with pytest.raises(ValueError): + Image.open(b) + @pytest.mark.parametrize("mode", ("P", "PA")) def test_palette(self, mode: str, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 95e91db83..a8bd798c1 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -412,9 +412,8 @@ class TestPyEncoder(CodecsTest): with pytest.raises(NotImplementedError): encoder.encode_to_pyfd() - fh = BytesIO() with pytest.raises(NotImplementedError): - encoder.encode_to_file(fh, 0) + encoder.encode_to_file(0, 0) def test_zero_height(self) -> None: with pytest.raises(UnidentifiedImageError): diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 66c5e5422..02e714f20 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -363,6 +363,7 @@ Classes :show-inheritance: .. autoclass:: PIL.Image.ImagePointHandler .. autoclass:: PIL.Image.ImageTransformHandler +.. autoclass:: PIL.Image._E Protocols --------- diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index 3bf5d9c04..926562497 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -319,11 +319,11 @@ class IcoImageFile(ImageFile.ImageFile): self.load() @property - def size(self): + def size(self) -> tuple[int, int]: return self._size @size.setter - def size(self, value): + def size(self, value: tuple[int, int]) -> None: if value not in self.info["sizes"]: msg = "This is not one of the allowed sizes of this image" raise ValueError(msg) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 705d0a144..ffc6bec34 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -504,7 +504,7 @@ class _E: return _E(self.scale / other, self.offset / other) -def _getscaleoffset(expr) -> tuple[float, float]: +def _getscaleoffset(expr: Callable[[_E], _E | float]) -> tuple[float, float]: a = expr(_E(1, 0)) return (a.scale, a.offset) if isinstance(a, _E) else (0, a) @@ -1894,7 +1894,13 @@ class Image: def point( self, - lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler, + lut: ( + Sequence[float] + | NumpyArray + | Callable[[int], float] + | Callable[[_E], _E | float] + | ImagePointHandler + ), mode: str | None = None, ) -> Image: """ @@ -1930,10 +1936,10 @@ class Image: # check if the function can be used with point_transform # UNDONE wiredfool -- I think this prevents us from ever doing # a gamma function point transform on > 8bit images. - scale, offset = _getscaleoffset(lut) + scale, offset = _getscaleoffset(lut) # type: ignore[arg-type] return self._new(self.im.point_transform(scale, offset)) # for other modes, convert the function to a table - flatLut = [lut(i) for i in range(256)] * self.im.bands + flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type] else: flatLut = lut @@ -2869,11 +2875,11 @@ class Image: self, box: tuple[int, int, int, int], image: Image, - method, - data, + method: Transform, + data: Sequence[float], resample: int = Resampling.NEAREST, fill: bool = True, - ): + ) -> None: w = box[2] - box[0] h = box[3] - box[1] @@ -4008,15 +4014,19 @@ class Exif(_ExifBase): ifd[tag] = value return b"Exif\x00\x00" + head + ifd.tobytes(offset) - def get_ifd(self, tag): + def get_ifd(self, tag: int) -> dict[int, Any]: if tag not in self._ifds: if tag == ExifTags.IFD.IFD1: if self._info is not None and self._info.next != 0: - self._ifds[tag] = self._get_ifd_dict(self._info.next) + ifd = self._get_ifd_dict(self._info.next) + if ifd is not None: + self._ifds[tag] = ifd elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: offset = self._hidden_data.get(tag, self.get(tag)) if offset is not None: - self._ifds[tag] = self._get_ifd_dict(offset, tag) + ifd = self._get_ifd_dict(offset, tag) + if ifd is not None: + self._ifds[tag] = ifd elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: if ExifTags.IFD.Exif not in self._ifds: self.get_ifd(ExifTags.IFD.Exif) @@ -4073,7 +4083,9 @@ class Exif(_ExifBase): (offset,) = struct.unpack(">L", data) self.fp.seek(offset) - camerainfo = {"ModelID": self.fp.read(4)} + camerainfo: dict[str, int | bytes] = { + "ModelID": self.fp.read(4) + } self.fp.read(4) # Seconds since 2000 @@ -4089,16 +4101,18 @@ class Exif(_ExifBase): ][1] camerainfo["Parallax"] = handler( ImageFileDirectory_v2(), parallax, False - ) + )[0] self.fp.read(4) camerainfo["Category"] = self.fp.read(2) - makernote = {0x1101: dict(self._fixup_dict(camerainfo))} + makernote = {0x1101: camerainfo} self._ifds[tag] = makernote else: # Interop - self._ifds[tag] = self._get_ifd_dict(tag_data, tag) + ifd = self._get_ifd_dict(tag_data, tag) + if ifd is not None: + self._ifds[tag] = ifd ifd = self._ifds.get(tag, {}) if tag == ExifTags.IFD.Exif and self._hidden_data: ifd = { diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 73554fa53..fdeb81d7a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -31,6 +31,7 @@ from __future__ import annotations import abc import io import itertools +import os import struct import sys from typing import IO, Any, NamedTuple @@ -555,7 +556,7 @@ def _encode_tile( fp: IO[bytes], tile: list[_Tile], bufsize: int, - fh, + fh: int | None, exc: BaseException | None = None, ) -> None: for encoder_name, extents, offset, args in tile: @@ -577,6 +578,7 @@ def _encode_tile( break else: # slight speedup: compress to real file object + assert fh is not None errcode = encoder.encode_to_file(fh, bufsize) if errcode < 0: raise _get_oserror(errcode, encoder=True) from exc @@ -801,7 +803,7 @@ class PyEncoder(PyCodec): self.fd.write(data) return bytes_consumed, errcode - def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int: + def encode_to_file(self, fh: int, bufsize: int) -> int: """ :param fh: File handle. :param bufsize: Buffer size. @@ -814,5 +816,5 @@ class PyEncoder(PyCodec): while errcode == 0: status, errcode, buf = self.encode(bufsize) if status > 0: - fh.write(buf[status:]) + os.write(fh, buf[status:]) return errcode diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 153243519..6ccf28aa1 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -214,7 +214,7 @@ def getiptcinfo( # as 4-byte integers, so we cannot use the get method...) try: data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK] - except (AttributeError, KeyError): + except KeyError: pass if data is None: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 7bbbc832e..5c6996a65 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -624,12 +624,12 @@ class ImageFileDirectory_v2(_IFDv2Base): self._tagdata: dict[int, bytes] = {} self.tagtype = {} # added 2008-06-05 by Florian Hoech self._next = None - self._offset = None + self._offset: int | None = None def __str__(self) -> str: return str(dict(self)) - def named(self): + def named(self) -> dict[str, Any]: """ :returns: dict of name|key: value @@ -643,7 +643,7 @@ class ImageFileDirectory_v2(_IFDv2Base): def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v2)) - def __getitem__(self, tag): + def __getitem__(self, tag: int) -> Any: if tag not in self._tags_v2: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] @@ -855,7 +855,7 @@ class ImageFileDirectory_v2(_IFDv2Base): raise OSError(msg) return ret - def load(self, fp): + def load(self, fp: IO[bytes]) -> None: self.reset() self._offset = fp.tell() @@ -1098,7 +1098,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): for legacy_api in (False, True): self._setitem(tag, value, legacy_api) - def __getitem__(self, tag): + def __getitem__(self, tag: int) -> Any: if tag not in self._tags_v1: # unpack on the fly data = self._tagdata[tag] typ = self.tagtype[tag] @@ -1124,11 +1124,15 @@ class TiffImageFile(ImageFile.ImageFile): format_description = "Adobe TIFF" _close_exclusive_fp_after_loading = False - def __init__(self, fp=None, filename=None): - self.tag_v2 = None + def __init__( + self, + fp: StrOrBytesPath | IO[bytes] | None = None, + filename: str | bytes | None = None, + ) -> None: + self.tag_v2: ImageFileDirectory_v2 """ Image file directory (tag dictionary) """ - self.tag = None + self.tag: ImageFileDirectory_v1 """ Legacy tag entries """ super().__init__(fp, filename) @@ -1143,9 +1147,6 @@ class TiffImageFile(ImageFile.ImageFile): self.tag_v2 = ImageFileDirectory_v2(ifh) - # legacy IFD entries will be filled in later - self.ifd: ImageFileDirectory_v1 | None = None - # setup frame pointers self.__first = self.__next = self.tag_v2.next self.__frame = -1 @@ -1396,8 +1397,11 @@ class TiffImageFile(ImageFile.ImageFile): logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) # size - xsize = int(self.tag_v2.get(IMAGEWIDTH)) - ysize = int(self.tag_v2.get(IMAGELENGTH)) + xsize = self.tag_v2.get(IMAGEWIDTH) + ysize = self.tag_v2.get(IMAGELENGTH) + if not isinstance(xsize, int) or not isinstance(ysize, int): + msg = "Invalid dimensions" + raise ValueError(msg) self._size = xsize, ysize logger.debug("- size: %s", self.size) @@ -1545,8 +1549,12 @@ class TiffImageFile(ImageFile.ImageFile): else: # tiled image offsets = self.tag_v2[TILEOFFSETS] - w = self.tag_v2.get(TILEWIDTH) + tilewidth = self.tag_v2.get(TILEWIDTH) h = self.tag_v2.get(TILELENGTH) + if not isinstance(tilewidth, int) or not isinstance(h, int): + msg = "Invalid tile dimensions" + raise ValueError(msg) + w = tilewidth for offset in offsets: if x + w > xsize: @@ -1624,7 +1632,7 @@ SAVE_INFO = { } -def _save(im, fp, filename): +def _save(im: Image.Image, fp, filename: str | bytes) -> None: try: rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] except KeyError as e: @@ -1760,10 +1768,11 @@ def _save(im, fp, filename): if im.mode == "1": inverted_im = im.copy() px = inverted_im.load() - for y in range(inverted_im.height): - for x in range(inverted_im.width): - px[x, y] = 0 if px[x, y] == 255 else 255 - im = inverted_im + if px is not None: + for y in range(inverted_im.height): + for x in range(inverted_im.width): + px[x, y] = 0 if px[x, y] == 255 else 255 + im = inverted_im else: im = ImageOps.invert(im) @@ -1805,11 +1814,11 @@ def _save(im, fp, filename): ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) if im.mode == "YCbCr": - for tag, value in { + for tag, default_value in { YCBCRSUBSAMPLING: (1, 1), REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), }.items(): - ifd.setdefault(tag, value) + ifd.setdefault(tag, default_value) blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] if libtiff: @@ -1852,7 +1861,7 @@ def _save(im, fp, filename): ] # bits per sample is a single short in the tiff directory, not a list. - atts = {BITSPERSAMPLE: bits[0]} + atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]} # Merge the ones that we have with (optional) more bits from # the original file, e.g x,y resolution so that we can # save(load('')) == original file. @@ -1923,13 +1932,15 @@ def _save(im, fp, filename): offset = ifd.save(fp) ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))], ) # -- helper for multi-page save -- if "_debug_multipage" in encoderinfo: # just to access o32 and o16 (using correct byte order) - im._debug_multipage = ifd + setattr(im, "_debug_multipage", ifd) class AppendingTiffWriter: From 388fc78e36e60f45a9b5ff0cd3223e3b8bc2c307 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 15:32:06 -0400 Subject: [PATCH 34/52] Add/update project-makefile files --- Makefile | 4757 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 4644 insertions(+), 113 deletions(-) diff --git a/Makefile b/Makefile index 94f7565d8..4200f95da 100644 --- a/Makefile +++ b/Makefile @@ -1,125 +1,4656 @@ -.DEFAULT_GOAL := help +# Project Makefile +# +# A makefile to automate setup of a Wagtail CMS project and related tasks. +# +# https://github.com/aclark4life/project-makefile +# +# -------------------------------------------------------------------------------- +# Set the default goal to be `git commit -a -m $(GIT_MESSAGE)` and `git push` +# -------------------------------------------------------------------------------- -.PHONY: clean -clean: - rm src/PIL/*.so || true - rm -r build || true - find . -name __pycache__ | xargs rm -r || true +.DEFAULT_GOAL := git-commit -.PHONY: coverage -coverage: - python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq - rm -r htmlcov || true - python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage - python3 -m coverage report +# -------------------------------------------------------------------------------- +# Single line variables to be used by phony target rules +# -------------------------------------------------------------------------------- -.PHONY: doc -.PHONY: html -doc html: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . - $(MAKE) -C docs html +ADD_DIR := mkdir -pv +ADD_FILE := touch +AWS_OPTS := --no-cli-pager --output table +COPY_DIR := cp -rv +COPY_FILE := cp -v +DEL_DIR := rm -rv +DEL_FILE := rm -v +DJANGO_DB_COL = awk -F\= '{print $$2}' +DJANGO_DB_URL = eb ssh -c "source /opt/elasticbeanstalk/deployment/custom_env_var; env | grep DATABASE_URL" +DJANGO_DB_HOST = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["HOST"])') +DJANGO_DB_NAME = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["NAME"])') +DJANGO_DB_PASS = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["PASSWORD"])') +DJANGO_DB_USER = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ + python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["USER"])') +DJANGO_BACKEND_APPS_FILE := backend/apps.py +DJANGO_CUSTOM_ADMIN_FILE := backend/admin.py +DJANGO_FRONTEND_FILES = .babelrc .browserslistrc .eslintrc .nvmrc .stylelintrc.json frontend package-lock.json \ + package.json postcss.config.js +DJANGO_SETTINGS_DIR = backend/settings +DJANGO_SETTINGS_BASE_FILE = $(DJANGO_SETTINGS_DIR)/base.py +DJANGO_SETTINGS_DEV_FILE = $(DJANGO_SETTINGS_DIR)/dev.py +DJANGO_SETTINGS_PROD_FILE = $(DJANGO_SETTINGS_DIR)/production.py +DJANGO_SETTINGS_SECRET_KEY = $(shell openssl rand -base64 48) +DJANGO_URLS_FILE = backend/urls.py +EB_DIR_NAME := .elasticbeanstalk +EB_ENV_NAME ?= $(PROJECT_NAME)-$(GIT_BRANCH)-$(GIT_REV) +EB_PLATFORM ?= "Python 3.11 running on 64bit Amazon Linux 2023" +EC2_INSTANCE_MAX ?= 1 +EC2_INSTANCE_MIN ?= 1 +EC2_INSTANCE_PROFILE ?= aws-elasticbeanstalk-ec2-role +EC2_INSTANCE_TYPE ?= t4g.small +EC2_LB_TYPE ?= application +EDITOR_REVIEW = subl +GIT_ADD := git add +GIT_BRANCH = $(shell git branch --show-current) +GIT_BRANCHES = $(shell git branch -a) +GIT_CHECKOUT = git checkout +GIT_COMMIT_MSG = "Update $(PROJECT_NAME)" +GIT_COMMIT = git commit +GIT_PUSH = git push +GIT_PUSH_FORCE = git push --force-with-lease +GIT_REV = $(shell git rev-parse --short HEAD) +MAKEFILE_CUSTOM_FILE := project.mk +PACKAGE_NAME = $(shell echo $(PROJECT_NAME) | sed 's/-/_/g') +PAGER ?= less +PIP_ENSURE = python -m ensurepip +PIP_INSTALL_PLONE_CONSTRAINTS = https://dist.plone.org/release/6.0.11.1/constraints.txt +PROJECT_DIRS = backend contactpage home privacy siteuser +PROJECT_EMAIL := aclark@aclark.net +PROJECT_NAME = project-makefile +RANDIR := $(shell openssl rand -base64 12 | sed 's/\///g') +TMPDIR := $(shell mktemp -d) +UNAME := $(shell uname) +WAGTAIL_CLEAN_DIRS = backend contactpage dist frontend home logging_demo model_form_demo node_modules payments privacy search sitepage siteuser +WAGTAIL_CLEAN_FILES = .babelrc .browserslistrc .dockerignore .eslintrc .gitignore .nvmrc .stylelintrc.json Dockerfile db.sqlite3 docker-compose.yml manage.py package-lock.json package.json postcss.config.js requirements-test.txt requirements.txt -.PHONY: htmlview -htmlview: - python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . - $(MAKE) -C docs htmlview +# -------------------------------------------------------------------------------- +# Include $(MAKEFILE_CUSTOM_FILE) if it exists +# -------------------------------------------------------------------------------- -.PHONY: doccheck -doccheck: - $(MAKE) doc -# Don't make our tests rely on the links in the docs being up every single build. -# We don't control them. But do check, and update them to the target of their redirects. - $(MAKE) -C docs linkcheck || true +ifneq ($(wildcard $(MAKEFILE_CUSTOM_FILE)),) + include $(MAKEFILE_CUSTOM_FILE) +endif -.PHONY: docserve -docserve: - cd docs/_build/html && python3 -m http.server 2> /dev/null& +# -------------------------------------------------------------------------------- +# Multi-line variables to be used in phony target rules +# -------------------------------------------------------------------------------- -.PHONY: help -help: - @echo "Welcome to Pillow development. Please use \`make \` where is one of" - @echo " clean remove build products" - @echo " coverage run coverage test (in progress)" - @echo " doc make HTML docs" - @echo " docserve run an HTTP server on the docs directory" - @echo " html make HTML docs" - @echo " htmlview open the index page built by the html target in your browser" - @echo " install make and install" - @echo " install-coverage make and install with C coverage" - @echo " lint run the lint checks" - @echo " lint-fix run Ruff to (mostly) fix lint issues" - @echo " release-test run code and package tests before release" - @echo " test run tests on installed Pillow" +define DJANGO_ALLAUTH_BASE_TEMPLATE +{% extends 'base.html' %} +endef -.PHONY: install -install: - python3 -m pip -v install . - python3 selftest.py - -.PHONY: install-coverage -install-coverage: - CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . - python3 selftest.py - -.PHONY: debug -debug: -# make a debug version if we don't have a -dbg python. Leaves in symbols -# for our stuff, kills optimization, and redirects to dev null so we -# see any build failures. - make clean > /dev/null - CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null - -.PHONY: release-test -release-test: - python3 Tests/check_release_notes.py - python3 -m pip install -e .[tests] - python3 selftest.py - python3 -m pytest Tests - python3 -m pip install . - python3 -m pytest -qq - python3 -m check_manifest - python3 -m pyroma . - $(MAKE) readme - -.PHONY: sdist -sdist: - python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build - python3 -m build --sdist - python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine - python3 -m twine check --strict dist/* - -.PHONY: test -test: - python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest - python3 -m pytest -qq - -.PHONY: valgrind -valgrind: - python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind - PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ - --log-file=/tmp/valgrind-output \ - python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output - -.PHONY: readme -readme: - python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 - python3 -m markdown2 README.md > .long-description.html && open .long-description.html +define DJANGO_API_SERIALIZERS +from rest_framework import serializers +from siteuser.models import User -.PHONY: lint -lint: - python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox - python3 -m tox -e lint +class UserSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = User + fields = ["url", "username", "email", "is_staff"] +endef -.PHONY: lint-fix -lint-fix: - python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black - python3 -m black . - python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff - python3 -m ruff --fix . +define DJANGO_API_VIEWS +from ninja import NinjaAPI +from rest_framework import viewsets +from siteuser.models import User +from .serializers import UserSerializer -.PHONY: mypy -mypy: - python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox - python3 -m tox -e mypy +api = NinjaAPI() + + +@api.get("/hello") +def hello(request): + return "Hello world" + + +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + serializer_class = UserSerializer +endef + +define DJANGO_APP_TESTS +from django.test import TestCase +from django.urls import reverse +from .models import YourModel +from .forms import YourForm + + +class YourModelTest(TestCase): + def setUp(self): + self.instance = YourModel.objects.create(field1="value1", field2="value2") + + def test_instance_creation(self): + self.assertIsInstance(self.instance, YourModel) + self.assertEqual(self.instance.field1, "value1") + self.assertEqual(self.instance.field2, "value2") + + def test_str_method(self): + self.assertEqual(str(self.instance), "Expected String Representation") + + +class YourViewTest(TestCase): + def setUp(self): + self.instance = YourModel.objects.create(field1="value1", field2="value2") + + def test_view_url_exists_at_desired_location(self): + response = self.client.get("/your-url/") + self.assertEqual(response.status_code, 200) + + def test_view_url_accessible_by_name(self): + response = self.client.get(reverse("your-view-name")) + self.assertEqual(response.status_code, 200) + + def test_view_uses_correct_template(self): + response = self.client.get(reverse("your-view-name")) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, "your_template.html") + + def test_view_context(self): + response = self.client.get(reverse("your-view-name")) + self.assertEqual(response.status_code, 200) + self.assertIn("context_variable", response.context) + + +class YourFormTest(TestCase): + def test_form_valid_data(self): + form = YourForm(data={"field1": "value1", "field2": "value2"}) + self.assertTrue(form.is_valid()) + + def test_form_invalid_data(self): + form = YourForm(data={"field1": "", "field2": "value2"}) + self.assertFalse(form.is_valid()) + self.assertIn("field1", form.errors) + + def test_form_save(self): + form = YourForm(data={"field1": "value1", "field2": "value2"}) + if form.is_valid(): + instance = form.save() + self.assertEqual(instance.field1, "value1") + self.assertEqual(instance.field2, "value2") +endef + +define DJANGO_BACKEND_APPS +from django.contrib.admin.apps import AdminConfig + + +class CustomAdminConfig(AdminConfig): + default_site = "backend.admin.CustomAdminSite" +endef + +define DJANGO_BASE_TEMPLATE +{% load static webpack_loader %} + + + + + + {% block title %}{% endblock %} + {% block title_suffix %}{% endblock %} + + + {% stylesheet_pack 'app' %} + {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} + + {% include 'favicon.html' %} + {% csrf_token %} + + +
+
+ {% include 'header.html' %} + {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} +
+ {% block content %}{% endblock %} +
+
+ {% include 'footer.html' %} + {% include 'offcanvas.html' %} + {% javascript_pack 'app' %} + {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} + + +endef + +define DJANGO_CUSTOM_ADMIN +from django.contrib.admin import AdminSite + + +class CustomAdminSite(AdminSite): + site_header = "Project Makefile" + site_title = "Project Makefile" + index_title = "Project Makefile" + + +custom_admin_site = CustomAdminSite(name="custom_admin") +endef + +define DJANGO_DOCKERCOMPOSE +version: '3' + +services: + db: + image: postgres:latest + volumes: + - postgres_data:/var/lib/postgresql/data + environment: + POSTGRES_DB: project + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + + web: + build: . + command: sh -c "python manage.py migrate && gunicorn project.wsgi:application -b 0.0.0.0:8000" + volumes: + - .:/app + ports: + - "8000:8000" + depends_on: + - db + environment: + DATABASE_URL: postgres://admin:admin@db:5432/project + +volumes: + postgres_data: +endef + +define DJANGO_DOCKERFILE +FROM amazonlinux:2023 +RUN dnf install -y shadow-utils python3.11 python3.11-pip make nodejs20-npm nodejs postgresql15 postgresql15-server +USER postgres +RUN initdb -D /var/lib/pgsql/data +USER root +RUN useradd wagtail +EXPOSE 8000 +ENV PYTHONUNBUFFERED=1 PORT=8000 +COPY requirements.txt / +RUN python3.11 -m pip install -r /requirements.txt +WORKDIR /app +RUN chown wagtail:wagtail /app +COPY --chown=wagtail:wagtail . . +USER wagtail +RUN npm-20 install; npm-20 run build +RUN python3.11 manage.py collectstatic --noinput --clear +CMD set -xe; pg_ctl -D /var/lib/pgsql/data -l /tmp/logfile start; python3.11 manage.py migrate --noinput; gunicorn backend.wsgi:application +endef + +define DJANGO_FAVICON_TEMPLATE +{% load static %} + +endef + +define DJANGO_FOOTER_TEMPLATE +
+

© {% now "Y" %} {{ current_site.site_name|default:"Project Makefile" }}

+
    +
  • + Home +
  • + {% for child in current_site.root_page.get_children %} +
  • + {{ child }} +
  • + {% endfor %} +
+
+endef + +define DJANGO_FRONTEND_APP +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import 'bootstrap'; +import '@fortawesome/fontawesome-free/js/fontawesome'; +import '@fortawesome/fontawesome-free/js/solid'; +import '@fortawesome/fontawesome-free/js/regular'; +import '@fortawesome/fontawesome-free/js/brands'; +import getDataComponents from '../dataComponents'; +import UserContextProvider from '../context'; +import * as components from '../components'; +import "../styles/index.scss"; +import "../styles/theme-blue.scss"; +import "./config"; + +const { ErrorBoundary } = components; +const dataComponents = getDataComponents(components); +const container = document.getElementById('app'); +const root = createRoot(container); +const App = () => ( + + + {dataComponents} + + +); +root.render(); +endef + +define DJANGO_FRONTEND_APP_CONFIG +import '../utils/themeToggler.js'; +// import '../utils/tinymce.js'; +endef + +define DJANGO_FRONTEND_BABELRC +{ + "presets": [ + [ + "@babel/preset-react", + ], + [ + "@babel/preset-env", + { + "useBuiltIns": "usage", + "corejs": "3.0.0" + } + ] + ], + "plugins": [ + "@babel/plugin-syntax-dynamic-import", + "@babel/plugin-transform-class-properties" + ] +} +endef + +define DJANGO_FRONTEND_COMPONENTS +export { default as ErrorBoundary } from './ErrorBoundary'; +export { default as UserMenu } from './UserMenu'; +endef + +define DJANGO_FRONTEND_COMPONENT_CLOCK +// Via ChatGPT +import React, { useState, useEffect, useCallback, useRef } from 'react'; +import PropTypes from 'prop-types'; + +const Clock = ({ color = '#fff' }) => { + const [date, setDate] = useState(new Date()); + const [blink, setBlink] = useState(true); + const timerID = useRef(); + + const tick = useCallback(() => { + setDate(new Date()); + setBlink(prevBlink => !prevBlink); + }, []); + + useEffect(() => { + timerID.current = setInterval(() => tick(), 1000); + + // Return a cleanup function to be run on component unmount + return () => clearInterval(timerID.current); + }, [tick]); + + const formattedDate = date.toLocaleDateString(undefined, { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + }); + + const formattedTime = date.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: 'numeric', + }); + + return ( + <> +
{formattedDate} {formattedTime}
+ + ); +}; + +Clock.propTypes = { + color: PropTypes.string, +}; + +export default Clock; +endef + +define DJANGO_FRONTEND_COMPONENT_ERROR +import { Component } from 'react'; +import PropTypes from 'prop-types'; + +class ErrorBoundary extends Component { + constructor (props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError () { + return { hasError: true }; + } + + componentDidCatch (error, info) { + const { onError } = this.props; + console.error(error); + onError && onError(error, info); + } + + render () { + const { children = null } = this.props; + const { hasError } = this.state; + + return hasError ? null : children; + } +} + +ErrorBoundary.propTypes = { + onError: PropTypes.func, + children: PropTypes.node, +}; + +export default ErrorBoundary; +endef + +define DJANGO_FRONTEND_COMPONENT_USER_MENU +// UserMenu.js +import React from 'react'; +import PropTypes from 'prop-types'; + +function handleLogout() { + window.location.href = '/accounts/logout'; +} + +const UserMenu = ({ isAuthenticated, isSuperuser, textColor }) => { + return ( +
+ {isAuthenticated ? ( +
  • + + +
  • + ) : ( +
  • + +
  • + )} +
    + ); +}; + +UserMenu.propTypes = { + isAuthenticated: PropTypes.bool.isRequired, + isSuperuser: PropTypes.bool.isRequired, + textColor: PropTypes.string, +}; + +export default UserMenu; +endef + +define DJANGO_FRONTEND_CONTEXT_INDEX +export { UserContextProvider as default } from './UserContextProvider'; +endef + +define DJANGO_FRONTEND_CONTEXT_USER_PROVIDER +// UserContextProvider.js +import React, { createContext, useContext, useState } from 'react'; +import PropTypes from 'prop-types'; + +const UserContext = createContext(); + +export const UserContextProvider = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + const login = () => { + try { + // Add logic to handle login, set isAuthenticated to true + setIsAuthenticated(true); + } catch (error) { + console.error('Login error:', error); + // Handle error, e.g., show an error message to the user + } + }; + + const logout = () => { + try { + // Add logic to handle logout, set isAuthenticated to false + setIsAuthenticated(false); + } catch (error) { + console.error('Logout error:', error); + // Handle error, e.g., show an error message to the user + } + }; + + return ( + + {children} + + ); +}; + +UserContextProvider.propTypes = { + children: PropTypes.node.isRequired, +}; + +export const useUserContext = () => { + const context = useContext(UserContext); + + if (!context) { + throw new Error('useUserContext must be used within a UserContextProvider'); + } + + return context; +}; + +// Add PropTypes for the return value of useUserContext +useUserContext.propTypes = { + isAuthenticated: PropTypes.bool.isRequired, + login: PropTypes.func.isRequired, + logout: PropTypes.func.isRequired, +}; +endef + +define DJANGO_FRONTEND_ESLINTRC +{ + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "overrides": [ + { + "env": { + "node": true + }, + "files": [ + ".eslintrc.{js,cjs}" + ], + "parserOptions": { + "sourceType": "script" + } + } + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "react" + ], + "rules": { + "no-unused-vars": "off" + }, + settings: { + react: { + version: 'detect', + }, + }, +} +endef + +define DJANGO_FRONTEND_OFFCANVAS_TEMPLATE +
    + +
    + +
    +
    +endef + +define DJANGO_FRONTEND_PORTAL +// Via pwellever +import React from 'react'; +import { createPortal } from 'react-dom'; + +const parseProps = data => Object.entries(data).reduce((result, [key, value]) => { + if (value.toLowerCase() === 'true') { + value = true; + } else if (value.toLowerCase() === 'false') { + value = false; + } else if (value.toLowerCase() === 'null') { + value = null; + } else if (!isNaN(parseFloat(value)) && isFinite(value)) { + // Parse numeric value + value = parseFloat(value); + } else if ( + (value[0] === '[' && value.slice(-1) === ']') || (value[0] === '{' && value.slice(-1) === '}') + ) { + // Parse JSON strings + value = JSON.parse(value); + } + + result[key] = value; + return result; +}, {}); + +// This method of using portals instead of calling ReactDOM.render on individual components +// ensures that all components are mounted under a single React tree, and are therefore able +// to share context. + +export default function getPageComponents (components) { + const getPortalComponent = domEl => { + // The element's "data-component" attribute is used to determine which component to render. + // All other "data-*" attributes are passed as props. + const { component: componentName, ...rest } = domEl.dataset; + const Component = components[componentName]; + if (!Component) { + console.error(`Component "$${componentName}" not found.`); + return null; + } + const props = parseProps(rest); + domEl.innerHTML = ''; + + // eslint-disable-next-line no-unused-vars + const { ErrorBoundary } = components; + return createPortal( + + + , + domEl, + ); + }; + + return Array.from(document.querySelectorAll('[data-component]')).map(getPortalComponent); +} +endef + +define DJANGO_FRONTEND_STYLES +// If you comment out code below, bootstrap will use red as primary color +// and btn-primary will become red + +// $primary: red; + +@import "~bootstrap/scss/bootstrap.scss"; + +.jumbotron { + // should be relative path of the entry scss file + background-image: url("../../vendors/images/sample.jpg"); + background-size: cover; +} + +#theme-toggler-authenticated:hover { + cursor: pointer; /* Change cursor to pointer on hover */ + color: #007bff; /* Change color on hover */ +} + +#theme-toggler-anonymous:hover { + cursor: pointer; /* Change cursor to pointer on hover */ + color: #007bff; /* Change color on hover */ +} +endef + +define DJANGO_FRONTEND_THEME_BLUE +@import "~bootstrap/scss/bootstrap.scss"; + +[data-bs-theme="blue"] { + --bs-body-color: var(--bs-white); + --bs-body-color-rgb: #{to-rgb($$white)}; + --bs-body-bg: var(--bs-blue); + --bs-body-bg-rgb: #{to-rgb($$blue)}; + --bs-tertiary-bg: #{$$blue-600}; + + .dropdown-menu { + --bs-dropdown-bg: #{color-mix($$blue-500, $$blue-600)}; + --bs-dropdown-link-active-bg: #{$$blue-700}; + } + + .btn-secondary { + --bs-btn-bg: #{color-mix($gray-600, $blue-400, .5)}; + --bs-btn-border-color: #{rgba($$white, .25)}; + --bs-btn-hover-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 5%)}; + --bs-btn-hover-border-color: #{rgba($$white, .25)}; + --bs-btn-active-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 10%)}; + --bs-btn-active-border-color: #{rgba($$white, .5)}; + --bs-btn-focus-border-color: #{rgba($$white, .5)}; + + // --bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(255, 255, 255, 20%); + } +} +endef + +define DJANGO_FRONTEND_THEME_TOGGLER +document.addEventListener('DOMContentLoaded', function () { + const rootElement = document.documentElement; + const anonThemeToggle = document.getElementById('theme-toggler-anonymous'); + const authThemeToggle = document.getElementById('theme-toggler-authenticated'); + if (authThemeToggle) { + localStorage.removeItem('data-bs-theme'); + } + const anonSavedTheme = localStorage.getItem('data-bs-theme'); + if (anonSavedTheme) { + rootElement.setAttribute('data-bs-theme', anonSavedTheme); + } + if (anonThemeToggle) { + anonThemeToggle.addEventListener('click', function () { + const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + rootElement.setAttribute('data-bs-theme', newTheme); + localStorage.setItem('data-bs-theme', newTheme); + }); + } + if (authThemeToggle) { + const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; + authThemeToggle.addEventListener('click', function () { + const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; + const newTheme = currentTheme === 'light' ? 'dark' : 'light'; + fetch('/user/update_theme_preference/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken, // Include the CSRF token in the headers + }, + body: JSON.stringify({ theme: newTheme }), + }) + .then(response => response.json()) + .then(data => { + rootElement.setAttribute('data-bs-theme', newTheme); + }) + .catch(error => { + console.error('Error updating theme preference:', error); + }); + }); + } +}); +endef + +define DJANGO_HEADER_TEMPLATE +
    +
    + +
    +
    +endef + +define DJANGO_HOME_PAGE_ADMIN +from django.contrib import admin # noqa + +# Register your models here. +endef + +define DJANGO_HOME_PAGE_MODELS +from django.db import models # noqa + +# Create your models here. +endef + +define DJANGO_HOME_PAGE_TEMPLATE +{% extends "base.html" %} +{% block content %} +
    +
    +{% endblock %} +endef + +define DJANGO_HOME_PAGE_URLS +from django.urls import path +from .views import HomeView + +urlpatterns = [path("", HomeView.as_view(), name="home")] +endef + +define DJANGO_HOME_PAGE_VIEWS +from django.views.generic import TemplateView + + +class HomeView(TemplateView): + template_name = "home.html" +endef + +define DJANGO_LOGGING_DEMO_ADMIN +# Register your models here. +endef + +define DJANGO_LOGGING_DEMO_MODELS +from django.db import models # noqa + +# Create your models here. +endef + +define DJANGO_LOGGING_DEMO_SETTINGS +INSTALLED_APPS.append("logging_demo") # noqa +endef + +define DJANGO_LOGGING_DEMO_URLS +from django.urls import path +from .views import logging_demo + +urlpatterns = [ + path("", logging_demo, name="logging_demo"), +] +endef + +define DJANGO_LOGGING_DEMO_VIEWS +from django.http import HttpResponse +import logging + +logger = logging.getLogger(__name__) + + +def logging_demo(request): + logger.debug("Hello, world!") + return HttpResponse("Hello, world!") +endef + +define DJANGO_MANAGE_PY +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.dev") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() +endef + +define DJANGO_MODEL_FORM_DEMO_ADMIN +from django.contrib import admin +from .models import ModelFormDemo + + +@admin.register(ModelFormDemo) +class ModelFormDemoAdmin(admin.ModelAdmin): + pass +endef + +define DJANGO_MODEL_FORM_DEMO_FORMS +from django import forms +from .models import ModelFormDemo + + +class ModelFormDemoForm(forms.ModelForm): + class Meta: + model = ModelFormDemo + fields = ["name", "email", "age", "is_active"] +endef + +define DJANGO_MODEL_FORM_DEMO_MODEL +from django.db import models +from django.shortcuts import reverse + + +class ModelFormDemo(models.Model): + name = models.CharField(max_length=100, blank=True, null=True) + email = models.EmailField(blank=True, null=True) + age = models.IntegerField(blank=True, null=True) + is_active = models.BooleanField(default=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return self.name or f"test-model-{self.pk}" + + def get_absolute_url(self): + return reverse("model_form_demo_detail", kwargs={"pk": self.pk}) +endef + +define DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL +{% extends 'base.html' %} +{% block content %} +

    Test Model Detail: {{ model_form_demo.name }}

    +

    Name: {{ model_form_demo.name }}

    +

    Email: {{ model_form_demo.email }}

    +

    Age: {{ model_form_demo.age }}

    +

    Active: {{ model_form_demo.is_active }}

    +

    Created At: {{ model_form_demo.created_at }}

    + Edit Test Model +{% endblock %} +endef + +define DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM +{% extends 'base.html' %} +{% block content %} +

    + {% if form.instance.pk %} + Update Test Model + {% else %} + Create Test Model + {% endif %} +

    +
    + {% csrf_token %} + {{ form.as_p }} + +
    +{% endblock %} +endef + +define DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST +{% extends 'base.html' %} +{% block content %} +

    Test Models List

    + + Create New Test Model +{% endblock %} +endef + +define DJANGO_MODEL_FORM_DEMO_URLS +from django.urls import path +from .views import ( + ModelFormDemoListView, + ModelFormDemoCreateView, + ModelFormDemoUpdateView, + ModelFormDemoDetailView, +) + +urlpatterns = [ + path("", ModelFormDemoListView.as_view(), name="model_form_demo_list"), + path("create/", ModelFormDemoCreateView.as_view(), name="model_form_demo_create"), + path( + "/update/", + ModelFormDemoUpdateView.as_view(), + name="model_form_demo_update", + ), + path("/", ModelFormDemoDetailView.as_view(), name="model_form_demo_detail"), +] +endef + +define DJANGO_MODEL_FORM_DEMO_VIEWS +from django.views.generic import ListView, CreateView, UpdateView, DetailView +from .models import ModelFormDemo +from .forms import ModelFormDemoForm + + +class ModelFormDemoListView(ListView): + model = ModelFormDemo + template_name = "model_form_demo_list.html" + context_object_name = "model_form_demos" + + +class ModelFormDemoCreateView(CreateView): + model = ModelFormDemo + form_class = ModelFormDemoForm + template_name = "model_form_demo_form.html" + + def form_valid(self, form): + form.instance.created_by = self.request.user + return super().form_valid(form) + + +class ModelFormDemoUpdateView(UpdateView): + model = ModelFormDemo + form_class = ModelFormDemoForm + template_name = "model_form_demo_form.html" + + +class ModelFormDemoDetailView(DetailView): + model = ModelFormDemo + template_name = "model_form_demo_detail.html" + context_object_name = "model_form_demo" +endef + +define DJANGO_PAYMENTS_ADMIN +from django.contrib import admin +from .models import Product, Order + +admin.site.register(Product) +admin.site.register(Order) +endef + +define DJANGO_PAYMENTS_FORM +from django import forms + + +class PaymentsForm(forms.Form): + stripeToken = forms.CharField(widget=forms.HiddenInput()) + amount = forms.DecimalField( + max_digits=10, decimal_places=2, widget=forms.HiddenInput() + ) +endef + +define DJANGO_PAYMENTS_MIGRATION_0002 +from django.db import migrations +import os +import secrets +import logging + +logger = logging.getLogger(__name__) + + +def generate_default_key(): + return "sk_test_" + secrets.token_hex(24) + + +def set_stripe_api_keys(apps, schema_editor): + # Get the Stripe API Key model + APIKey = apps.get_model("djstripe", "APIKey") + + # Fetch the keys from environment variables or generate default keys + test_secret_key = os.environ.get("STRIPE_TEST_SECRET_KEY", generate_default_key()) + live_secret_key = os.environ.get("STRIPE_LIVE_SECRET_KEY", generate_default_key()) + + logger.info("STRIPE_TEST_SECRET_KEY: %s", test_secret_key) + logger.info("STRIPE_LIVE_SECRET_KEY: %s", live_secret_key) + + # Check if the keys are not already in the database + if not APIKey.objects.filter(secret=test_secret_key).exists(): + APIKey.objects.create(secret=test_secret_key, livemode=False) + logger.info("Added test secret key to the database.") + else: + logger.info("Test secret key already exists in the database.") + + if not APIKey.objects.filter(secret=live_secret_key).exists(): + APIKey.objects.create(secret=live_secret_key, livemode=True) + logger.info("Added live secret key to the database.") + else: + logger.info("Live secret key already exists in the database.") + + +class Migration(migrations.Migration): + + dependencies = [ + ("payments", "0001_initial"), + ] + + operations = [ + migrations.RunPython(set_stripe_api_keys), + ] +endef + +define DJANGO_PAYMENTS_MIGRATION_0003 +from django.db import migrations + + +def create_initial_products(apps, schema_editor): + Product = apps.get_model("payments", "Product") + Product.objects.create(name="T-shirt", description="A cool T-shirt", price=20.00) + Product.objects.create(name="Mug", description="A nice mug", price=10.00) + Product.objects.create(name="Hat", description="A stylish hat", price=15.00) + + +class Migration(migrations.Migration): + dependencies = [ + ( + "payments", + "0002_set_stripe_api_keys", + ), + ] + + operations = [ + migrations.RunPython(create_initial_products), + ] +endef + +define DJANGO_PAYMENTS_MODELS +from django.db import models + + +class Product(models.Model): + name = models.CharField(max_length=100) + description = models.TextField() + price = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return self.name + + +class Order(models.Model): + product = models.ForeignKey(Product, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + stripe_checkout_session_id = models.CharField(max_length=200) + + def __str__(self): + return f"Order {self.id} for {self.product.name}" +endef + +define DJANGO_PAYMENTS_TEMPLATE_CANCEL +{% extends "base.html" %} +{% block title %}Cancel{% endblock %} +{% block content %} +

    Payment Cancelled

    +

    Your payment was cancelled.

    +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_CHECKOUT +{% extends "base.html" %} +{% block title %}Checkout{% endblock %} +{% block content %} +

    Checkout

    +
    + {% csrf_token %} + +
    +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL +{% extends "base.html" %} +{% block title %}{{ product.name }}{% endblock %} +{% block content %} +

    {{ product.name }}

    +

    {{ product.description }}

    +

    Price: ${{ product.price }}

    +
    + {% csrf_token %} + + +
    +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST +{% extends "base.html" %} +{% block title %}Products{% endblock %} +{% block content %} +

    Products

    + +{% endblock %} +endef + +define DJANGO_PAYMENTS_TEMPLATE_SUCCESS +{% extends "base.html" %} +{% block title %}Success{% endblock %} +{% block content %} +

    Payment Successful

    +

    Thank you for your purchase!

    +{% endblock %} +endef + +define DJANGO_PAYMENTS_URLS +from django.urls import path +from .views import ( + CheckoutView, + SuccessView, + CancelView, + ProductListView, + ProductDetailView, +) + +urlpatterns = [ + path("", ProductListView.as_view(), name="product_list"), + path("product//", ProductDetailView.as_view(), name="product_detail"), + path("checkout/", CheckoutView.as_view(), name="checkout"), + path("success/", SuccessView.as_view(), name="success"), + path("cancel/", CancelView.as_view(), name="cancel"), +] +endef + +define DJANGO_PAYMENTS_VIEW +from django.conf import settings +from django.shortcuts import render, redirect, get_object_or_404 +from django.views.generic import TemplateView, View, ListView, DetailView +import stripe +from .models import Product, Order + +stripe.api_key = settings.STRIPE_TEST_SECRET_KEY + + +class ProductListView(ListView): + model = Product + template_name = "payments/product_list.html" + context_object_name = "products" + + +class ProductDetailView(DetailView): + model = Product + template_name = "payments/product_detail.html" + context_object_name = "product" + + +class CheckoutView(View): + template_name = "payments/checkout.html" + + def get(self, request, *args, **kwargs): + products = Product.objects.all() + return render(request, self.template_name, {"products": products}) + + def post(self, request, *args, **kwargs): + product_id = request.POST.get("product_id") + product = get_object_or_404(Product, id=product_id) + + session = stripe.checkout.Session.create( + payment_method_types=["card"], + line_items=[ + { + "price_data": { + "currency": "usd", + "product_data": { + "name": product.name, + }, + "unit_amount": int(product.price * 100), + }, + "quantity": 1, + } + ], + mode="payment", + success_url="http://localhost:8000/payments/success/", + cancel_url="http://localhost:8000/payments/cancel/", + ) + + Order.objects.create(product=product, stripe_checkout_session_id=session.id) + return redirect(session.url, code=303) + + +class SuccessView(TemplateView): + + template_name = "payments/success.html" + + +class CancelView(TemplateView): + + template_name = "payments/cancel.html" +endef + +define DJANGO_SEARCH_FORMS +from django import forms + + +class SearchForm(forms.Form): + query = forms.CharField(max_length=100, required=True, label="Search") + +endef + +define DJANGO_SEARCH_SETTINGS +SEARCH_MODELS = [ + # Add search models here. +] +endef + +define DJANGO_SEARCH_TEMPLATE +{% extends "base.html" %} +{% block body_class %}template-searchresults{% endblock %} +{% block title %}Search{% endblock %} +{% block content %} +

    Search

    +
    + + +
    + {% if search_results %} +
      + {% for result in search_results %} +
    • +

      + {{ result }} +

      + {% if result.search_description %}{{ result.search_description }}{% endif %} +
    • + {% endfor %} +
    + {% if search_results.has_previous %} + Previous + {% endif %} + {% if search_results.has_next %} + Next + {% endif %} + {% elif search_query %} + No results found + {% else %} + No results found. Try a test query? + {% endif %} +{% endblock %} +endef + +define DJANGO_SEARCH_URLS +from django.urls import path +from .views import SearchView + +urlpatterns = [ + path("search/", SearchView.as_view(), name="search"), +] +endef + +define DJANGO_SEARCH_UTILS +from django.apps import apps +from django.conf import settings + +def get_search_models(): + models = [] + for model_path in settings.SEARCH_MODELS: + app_label, model_name = model_path.split(".") + model = apps.get_model(app_label, model_name) + models.append(model) + return models +endef + +define DJANGO_SEARCH_VIEWS +from django.views.generic import ListView +from django.db import models +from django.db.models import Q +from .forms import SearchForm +from .utils import get_search_models + + +class SearchView(ListView): + template_name = "your_app/search_results.html" + context_object_name = "results" + paginate_by = 10 + + def get_queryset(self): + form = SearchForm(self.request.GET) + query = None + results = [] + + if form.is_valid(): + query = form.cleaned_data["query"] + search_models = get_search_models() + + for model in search_models: + fields = [f.name for f in model._meta.fields if isinstance(f, (models.CharField, models.TextField))] + queries = [Q(**{f"{field}__icontains": query}) for field in fields] + model_results = model.objects.filter(queries.pop()) + + for item in queries: + model_results = model_results.filter(item) + + results.extend(model_results) + + return results + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["form"] = SearchForm(self.request.GET) + context["query"] = self.request.GET.get("query", "") + return context +endef + +define DJANGO_SETTINGS_AUTHENTICATION_BACKENDS +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", +] +endef + +define DJANGO_SETTINGS_BASE +# $(PROJECT_NAME) +# +# Uncomment next two lines to enable custom admin +# INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'django.contrib.admin'] +# INSTALLED_APPS.append('backend.apps.CustomAdminConfig') +import os # noqa +import dj_database_url # noqa + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +EXPLORER_CONNECTIONS = {"Default": "default"} +EXPLORER_DEFAULT_CONNECTION = "default" +LOGIN_REDIRECT_URL = "/" +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SILENCED_SYSTEM_CHECKS = ["django_recaptcha.recaptcha_test_key_error"] +BASE_DIR = os.path.dirname(PROJECT_DIR) +STATICFILES_DIRS = [] +WEBPACK_LOADER = { + "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), +} +STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) +TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) +endef + +define DJANGO_SETTINGS_BASE_MINIMAL +# $(PROJECT_NAME) +import os # noqa +import dj_database_url # noqa + +INSTALLED_APPS.append("debug_toolbar") +INSTALLED_APPS.append("webpack_boilerplate") +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = os.path.dirname(PROJECT_DIR) +STATICFILES_DIRS = [] +STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) +TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) +WEBPACK_LOADER = { + "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), +} +endef + +define DJANGO_SETTINGS_CRISPY_FORMS +CRISPY_TEMPLATE_PACK = "bootstrap5" +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" +endef + +define DJANGO_SETTINGS_DATABASE +DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://:@:/$(PROJECT_NAME)") +DATABASES["default"] = dj_database_url.parse(DATABASE_URL) +endef + +define DJANGO_SETTINGS_DEV +from .base import * # noqa + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# SECURITY WARNING: define the correct hosts in production! +ALLOWED_HOSTS = ["*"] + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +try: + from .local import * # noqa +except ImportError: + pass + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": "DEBUG", + "propagate": True, + }, + }, +} + +INTERNAL_IPS = [ + "127.0.0.1", +] + +MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") # noqa +MIDDLEWARE.append("hijack.middleware.HijackUserMiddleware") # noqa +INSTALLED_APPS.append("django.contrib.admindocs") # noqa +SECRET_KEY = "$(DJANGO_SETTINGS_SECRET_KEY)" +endef + + +define DJANGO_SETTINGS_HOME_PAGE +INSTALLED_APPS.append("home") +endef + +define DJANGO_SETTINGS_INSTALLED_APPS +INSTALLED_APPS.append("allauth") +INSTALLED_APPS.append("allauth.account") +INSTALLED_APPS.append("allauth.socialaccount") +INSTALLED_APPS.append("crispy_bootstrap5") +INSTALLED_APPS.append("crispy_forms") +INSTALLED_APPS.append("debug_toolbar") +INSTALLED_APPS.append("django_extensions") +INSTALLED_APPS.append("django_recaptcha") +INSTALLED_APPS.append("rest_framework") +INSTALLED_APPS.append("rest_framework.authtoken") +INSTALLED_APPS.append("webpack_boilerplate") +INSTALLED_APPS.append("explorer") +endef + +define DJANGO_SETTINGS_MIDDLEWARE +MIDDLEWARE.append("allauth.account.middleware.AccountMiddleware") +endef + +define DJANGO_SETTINGS_MODEL_FORM_DEMO +INSTALLED_APPS.append("model_form_demo") # noqa +endef + +define DJANGO_SETTINGS_PAYMENTS +DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id" +DJSTRIPE_WEBHOOK_VALIDATION = "retrieve_event" +STRIPE_PUBLISHABLE_KEY = os.environ.get("STRIPE_PUBLISHABLE_KEY") +STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY") +STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY") +INSTALLED_APPS.append("payments") # noqa +INSTALLED_APPS.append("djstripe") # noqa +endef + +define DJANGO_SETTINGS_REST_FRAMEWORK +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" + ] +} +endef + +define DJANGO_SETTINGS_SITEUSER +INSTALLED_APPS.append("siteuser") # noqa +AUTH_USER_MODEL = "siteuser.User" +endef + +define DJANGO_SETTINGS_PROD +from .base import * # noqa +from backend.utils import get_ec2_metadata + +DEBUG = False + +try: + from .local import * # noqa +except ImportError: + pass + +LOCAL_IPV4 = get_ec2_metadata() +ALLOWED_HOSTS.append(LOCAL_IPV4) # noqa +endef + +define DJANGO_SETTINGS_THEMES +THEMES = [ + ("light", "Light Theme"), + ("dark", "Dark Theme"), +] +endef + +define DJANGO_SITEUSER_ADMIN +from django.contrib.auth.admin import UserAdmin +from django.contrib import admin + +from .models import User + +admin.site.register(User, UserAdmin) +endef + +define DJANGO_SITEUSER_EDIT_TEMPLATE +{% extends 'base.html' %} +{% load crispy_forms_tags %} +{% block content %} +

    Edit User

    + {% crispy form %} +{% endblock %} +endef + +define DJANGO_SITEUSER_FORM +from django import forms +from django.contrib.auth.forms import UserChangeForm +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit +from .models import User + + +class SiteUserForm(UserChangeForm): + bio = forms.CharField(widget=forms.Textarea(attrs={"id": "editor"}), required=False) + + class Meta(UserChangeForm.Meta): + model = User + fields = ("username", "user_theme_preference", "bio", "rate") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_method = "post" + self.helper.layout = Layout( + Fieldset( + "Edit Your Profile", + "username", + "user_theme_preference", + "bio", + "rate", + ), + ButtonHolder(Submit("submit", "Save", css_class="btn btn-primary")), + ) +endef + +define DJANGO_SITEUSER_MODEL +from django.db import models +from django.contrib.auth.models import AbstractUser, Group, Permission +from django.conf import settings + + +class User(AbstractUser): + groups = models.ManyToManyField(Group, related_name="siteuser_set", blank=True) + user_permissions = models.ManyToManyField( + Permission, related_name="siteuser_set", blank=True + ) + + user_theme_preference = models.CharField( + max_length=10, choices=settings.THEMES, default="light" + ) + + bio = models.TextField(blank=True, null=True) + rate = models.FloatField(blank=True, null=True) +endef + +define DJANGO_SITEUSER_URLS +from django.urls import path +from .views import UserProfileView, UpdateThemePreferenceView, UserEditView + +urlpatterns = [ + path("profile/", UserProfileView.as_view(), name="user-profile"), + path( + "update_theme_preference/", + UpdateThemePreferenceView.as_view(), + name="update_theme_preference", + ), + path("/edit/", UserEditView.as_view(), name="user-edit"), +] +endef + +define DJANGO_SITEUSER_VIEW +import json + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import JsonResponse +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import DetailView +from django.views.generic.edit import UpdateView +from django.urls import reverse_lazy + +from .models import User +from .forms import SiteUserForm + + +class UserProfileView(LoginRequiredMixin, DetailView): + model = User + template_name = "profile.html" + + def get_object(self, queryset=None): + return self.request.user + + +@method_decorator(csrf_exempt, name="dispatch") +class UpdateThemePreferenceView(View): + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body.decode("utf-8")) + new_theme = data.get("theme") + user = request.user + user.user_theme_preference = new_theme + user.save() + response_data = {"theme": new_theme} + return JsonResponse(response_data) + except json.JSONDecodeError as e: + return JsonResponse({"error": e}, status=400) + + def http_method_not_allowed(self, request, *args, **kwargs): + return JsonResponse({"error": "Invalid request method"}, status=405) + + +class UserEditView(LoginRequiredMixin, UpdateView): + model = User + template_name = "user_edit.html" # Create this template in your templates folder + form_class = SiteUserForm + + def get_success_url(self): + # return reverse_lazy("user-profile", kwargs={"pk": self.object.pk}) + return reverse_lazy("user-profile") +endef + +define DJANGO_SITEUSER_VIEW_TEMPLATE +{% extends 'base.html' %} +{% block content %} +

    User Profile

    +
    + Edit +
    +

    Username: {{ user.username }}

    +

    Theme: {{ user.user_theme_preference }}

    +

    Bio: {{ user.bio|default:""|safe }}

    +

    Rate: {{ user.rate|default:"" }}

    +{% endblock %} +endef + +define DJANGO_URLS +from django.contrib import admin +from django.urls import path, include +from django.conf import settings + +urlpatterns = [ + path("django/", admin.site.urls), +] +endef + +define DJANGO_URLS_ALLAUTH +urlpatterns += [path("accounts/", include("allauth.urls"))] +endef + +define DJANGO_URLS_API +from rest_framework import routers # noqa +from .api import UserViewSet, api # noqa + +router = routers.DefaultRouter() +router.register(r"users", UserViewSet) +# urlpatterns += [path("api/", include(router.urls))] +urlpatterns += [path("api/", api.urls)] +endef + +define DJANGO_URLS_DEBUG_TOOLBAR +if settings.DEBUG: + urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))] +endef + +define DJANGO_URLS_HOME_PAGE +urlpatterns += [path("", include("home.urls"))] +endef + +define DJANGO_URLS_LOGGING_DEMO +urlpatterns += [path("logging-demo/", include("logging_demo.urls"))] +endef + +define DJANGO_URLS_MODEL_FORM_DEMO +urlpatterns += [path("model-form-demo/", include("model_form_demo.urls"))] +endef + +define DJANGO_URLS_PAYMENTS +urlpatterns += [path("payments/", include("payments.urls"))] +endef + +define DJANGO_URLS_SITEUSER +urlpatterns += [path("user/", include("siteuser.urls"))] +endef + +define DJANGO_UTILS +from django.urls import URLResolver +import requests + + +def get_ec2_metadata(): + try: + # Step 1: Get the token + token_url = "http://169.254.169.254/latest/api/token" + headers = {"X-aws-ec2-metadata-token-ttl-seconds": "21600"} + response = requests.put(token_url, headers=headers) + response.raise_for_status() # Raise an error for bad responses + + token = response.text + + # Step 2: Use the token to get the instance metadata + metadata_url = "http://169.254.169.254/latest/meta-data/local-ipv4" + headers = {"X-aws-ec2-metadata-token": token} + response = requests.get(metadata_url, headers=headers) + response.raise_for_status() # Raise an error for bad responses + + metadata = response.text + return metadata + except requests.RequestException as e: + print(f"Error retrieving EC2 metadata: {e}") + return None + + +# Function to remove a specific URL pattern based on its route (including catch-all) +def remove_urlpattern(urlpatterns, route_to_remove): + urlpatterns[:] = [ + urlpattern + for urlpattern in urlpatterns + if not ( + isinstance(urlpattern, URLResolver) + and urlpattern.pattern._route == route_to_remove + ) + ] +endef + +define EB_CUSTOM_ENV_EC2_USER +files: + "/home/ec2-user/.bashrc": + mode: "000644" + owner: ec2-user + group: ec2-user + content: | + # .bashrc + + # Source global definitions + if [ -f /etc/bashrc ]; then + . /etc/bashrc + fi + + # User specific aliases and functions + set -o vi + + source <(sed -E -n 's/[^#]+/export &/ p' /opt/elasticbeanstalk/deployment/custom_env_var) +endef + +define EB_CUSTOM_ENV_VAR_FILE +#!/bin/bash + +# Via https://aws.amazon.com/premiumsupport/knowledge-center/elastic-beanstalk-env-variables-linux2/ + +#Create a copy of the environment variable file. +cat /opt/elasticbeanstalk/deployment/env | perl -p -e 's/(.*)=(.*)/export $$1="$$2"/;' > /opt/elasticbeanstalk/deployment/custom_env_var + +#Set permissions to the custom_env_var file so this file can be accessed by any user on the instance. You can restrict permissions as per your requirements. +chmod 644 /opt/elasticbeanstalk/deployment/custom_env_var + +# add the virtual env path in. +VENV=/var/app/venv/`ls /var/app/venv` +cat <> /opt/elasticbeanstalk/deployment/custom_env_var +VENV=$$ENV +EOF + +#Remove duplicate files upon deployment. +rm -f /opt/elasticbeanstalk/deployment/*.bak +endef + +define GIT_IGNORE +__pycache__ +*.pyc +dist/ +node_modules/ +_build/ +.elasticbeanstalk/ +db.sqlite3 +static/ +backend/var +endef + +define JENKINS_FILE +pipeline { + agent any + stages { + stage('') { + steps { + echo '' + } + } + } +} +endef + +define MAKEFILE_CUSTOM +# Custom Makefile +# Add your custom makefile commands here +# +# PROJECT_NAME := my-new-project +endef + +define PIP_INSTALL_REQUIREMENTS_TEST +pytest +pytest-runner +coverage +pytest-mock +pytest-cov +hypothesis +selenium +pytest-django +factory-boy +flake8 +tox +endef + +define PROGRAMMING_INTERVIEW +from rich import print as rprint +from rich.console import Console +from rich.panel import Panel + +import argparse +import locale +import math +import time + +import code # noqa +import readline # noqa +import rlcompleter # noqa + + +locale.setlocale(locale.LC_ALL, "en_US.UTF-8") + + +class DataStructure: + # Data Structure: Binary Tree + class TreeNode: + def __init__(self, value=0, left=None, right=None): + self.value = value + self.left = left + self.right = right + + # Data Structure: Stack + class Stack: + def __init__(self): + self.items = [] + + def push(self, item): + self.items.append(item) + + def pop(self): + if not self.is_empty(): + return self.items.pop() + return None + + def peek(self): + if not self.is_empty(): + return self.items[-1] + return None + + def is_empty(self): + return len(self.items) == 0 + + def size(self): + return len(self.items) + + # Data Structure: Queue + class Queue: + def __init__(self): + self.items = [] + + def enqueue(self, item): + self.items.append(item) + + def dequeue(self): + if not self.is_empty(): + return self.items.pop(0) + return None + + def is_empty(self): + return len(self.items) == 0 + + def size(self): + return len(self.items) + + # Data Structure: Linked List + class ListNode: + def __init__(self, value=0, next=None): + self.value = value + self.next = next + + +class Interview(DataStructure): + + # Protected methods for factorial calculation + def _factorial_recursive(self, n): + if n == 0: + return 1 + return n * self._factorial_recursive(n - 1) + + def _factorial_divide_and_conquer(self, low, high): + if low > high: + return 1 + if low == high: + return low + mid = (low + high) // 2 + return self._factorial_divide_and_conquer( + low, mid + ) * self._factorial_divide_and_conquer(mid + 1, high) + + # Recursive Factorial with Timing + def factorial_recursive(self, n): + start_time = time.time() # Start timing + result = self._factorial_recursive(n) # Calculate factorial + end_time = time.time() # End timing + elapsed_time = end_time - start_time + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Iterative Factorial with Timing + def factorial_iterative(self, n): + start_time = time.time() # Start timing + result = 1 + for i in range(1, n + 1): + result *= i + end_time = time.time() # End timing + elapsed_time = end_time - start_time + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Divide and Conquer Factorial with Timing + def factorial_divide_and_conquer(self, n): + start_time = time.time() # Start timing + result = self._factorial_divide_and_conquer(1, n) # Calculate factorial + end_time = time.time() # End timing + elapsed_time = end_time - start_time + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Built-in Factorial with Timing + def factorial_builtin(self, n): + start_time = time.time() # Start timing + result = math.factorial(n) # Calculate factorial using built-in + end_time = time.time() # End timing + + # Calculate elapsed time + elapsed_time = end_time - start_time + + # Print complexity and runtime + return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" + + # Recursion: Fibonacci + def fibonacci_recursive(self, n): + if n <= 1: + return n + return self.fibonacci_recursive(n - 1) + self.fibonacci_recursive(n - 2) + + # Iteration: Fibonacci + def fibonacci_iterative(self, n): + if n <= 1: + return n + a, b = 0, 1 + for _ in range(n - 1): + a, b = b, a + b + return b + + # Searching: Linear Search + def linear_search(self, arr, target): + for i, value in enumerate(arr): + if value == target: + return i + return -1 + + # Searching: Binary Search + def binary_search(self, arr, target): + left, right = 0, len(arr) - 1 + while left <= right: + mid = (left + right) // 2 + if arr[mid] == target: + return mid + elif arr[mid] < target: + left = mid + 1 + else: + right = mid - 1 + return -1 + + # Sorting: Bubble Sort + def bubble_sort(self, arr): + n = len(arr) + for i in range(n): + for j in range(0, n - i - 1): + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + return arr + + # Sorting: Merge Sort + def merge_sort(self, arr): + if len(arr) > 1: + mid = len(arr) // 2 + left_half = arr[:mid] + right_half = arr[mid:] + + self.merge_sort(left_half) + self.merge_sort(right_half) + + i = j = k = 0 + + while i < len(left_half) and j < len(right_half): + if left_half[i] < right_half[j]: + arr[k] = left_half[i] + i += 1 + else: + arr[k] = right_half[j] + j += 1 + k += 1 + + while i < len(left_half): + arr[k] = left_half[i] + i += 1 + k += 1 + + while j < len(right_half): + arr[k] = right_half[j] + j += 1 + k += 1 + return arr + + def insert_linked_list(self, head, value): + new_node = self.ListNode(value) + if not head: + return new_node + current = head + while current.next: + current = current.next + current.next = new_node + return head + + def print_linked_list(self, head): + current = head + while current: + print(current.value, end=" -> ") + current = current.next + print("None") + + def inorder_traversal(self, root): + return ( + self.inorder_traversal(root.left) + + [root.value] + + self.inorder_traversal(root.right) + if root + else [] + ) + + def preorder_traversal(self, root): + return ( + [root.value] + + self.preorder_traversal(root.left) + + self.preorder_traversal(root.right) + if root + else [] + ) + + def postorder_traversal(self, root): + return ( + self.postorder_traversal(root.left) + + self.postorder_traversal(root.right) + + [root.value] + if root + else [] + ) + + # Graph Algorithms: Depth-First Search + def dfs(self, graph, start): + visited, stack = set(), [start] + while stack: + vertex = stack.pop() + if vertex not in visited: + visited.add(vertex) + stack.extend(set(graph[vertex]) - visited) + return visited + + # Graph Algorithms: Breadth-First Search + def bfs(self, graph, start): + visited, queue = set(), [start] + while queue: + vertex = queue.pop(0) + if vertex not in visited: + visited.add(vertex) + queue.extend(set(graph[vertex]) - visited) + return visited + + +def setup_readline(local): + + # Enable tab completion + readline.parse_and_bind("tab: complete") + # Optionally, you can set the completer function manually + readline.set_completer(rlcompleter.Completer(local).complete) + + +def main(): + + console = Console() + interview = Interview() + + parser = argparse.ArgumentParser(description="Programming Interview Questions") + + parser.add_argument( + "-f", "--factorial", type=int, help="Factorial algorithm examples" + ) + parser.add_argument("--fibonacci", type=int, help="Fibonacci algorithm examples") + parser.add_argument( + "--search", action="store_true", help="Search algorithm examples" + ) + parser.add_argument("--sort", action="store_true", help="Search algorithm examples") + parser.add_argument("--stack", action="store_true", help="Stack algorithm examples") + parser.add_argument("--queue", action="store_true", help="Queue algorithm examples") + parser.add_argument( + "--list", action="store_true", help="Linked List algorithm examples" + ) + parser.add_argument( + "--tree", action="store_true", help="Tree traversal algorithm examples" + ) + parser.add_argument("--graph", action="store_true", help="Graph algorithm examples") + parser.add_argument( + "-i", "--interactive", action="store_true", help="Interactive mode" + ) + + args = parser.parse_args() + + if args.factorial: + # Factorial examples + console.rule("Factorial Examples") + rprint( + Panel( + "[bold cyan]Recursive Factorial - Time Complexity: O(n)[/bold cyan]\n" + + str(interview.factorial_recursive(args.factorial)), + title="Factorial Recursive", + ) + ) + rprint( + Panel( + "[bold cyan]Iterative Factorial - Time Complexity: O(n)[/bold cyan]\n" + + str(interview.factorial_iterative(args.factorial)), + title="Factorial Iterative", + ) + ) + rprint( + Panel( + "[bold cyan]Built-in Factorial - Time Complexity: O(n)[/bold cyan]\n" + + str(interview.factorial_builtin(args.factorial)), + title="Factorial Built-in", + ) + ) + rprint( + Panel( + "[bold cyan]Divide and Conquer Factorial - Time Complexity: O(n log n)[/bold cyan]\n" + + str(interview.factorial_divide_and_conquer(args.factorial)), + title="Factorial Divide and Conquer", + ) + ) + exit() + + if args.fibonacci: + # Fibonacci examples + console.rule("Fibonacci Examples") + rprint( + Panel( + str(interview.fibonacci_recursive(args.fibonacci)), + title="Fibonacci Recursive", + ) + ) + rprint( + Panel( + str(interview.fibonacci_iterative(args.fibonacci)), + title="Fibonacci Iterative", + ) + ) + exit() + + if args.search: + # Searching examples + console.rule("Searching Examples") + array = [1, 3, 5, 7, 9] + rprint(Panel(str(interview.linear_search(array, 5)), title="Linear Search")) + rprint(Panel(str(interview.binary_search(array, 5)), title="Binary Search")) + exit() + + if args.sort: + # Sorting examples + console.rule("Sorting Examples") + unsorted_array = [64, 34, 25, 12, 22, 11, 90] + rprint( + Panel( + str(interview.bubble_sort(unsorted_array.copy())), title="Bubble Sort" + ) + ) + rprint( + Panel(str(interview.merge_sort(unsorted_array.copy())), title="Merge Sort") + ) + exit() + + if args.stack: + # Stack example + console.rule("Stack Example") + stack = interview.Stack() + stack.push(1) + stack.push(2) + stack.push(3) + rprint(Panel(str(stack.pop()), title="Stack Pop")) + rprint(Panel(str(stack.peek()), title="Stack Peek")) + rprint(Panel(str(stack.size()), title="Stack Size")) + + if args.queue: + # Queue example + console.rule("Queue Example") + queue = interview.Queue() + queue.enqueue(1) + queue.enqueue(2) + queue.enqueue(3) + rprint(Panel(str(queue.dequeue()), title="Queue Dequeue")) + rprint(Panel(str(queue.is_empty()), title="Queue Is Empty")) + rprint(Panel(str(queue.size()), title="Queue Size")) + + if args.list: + # Linked List example + console.rule("Linked List Example") + head = None + head = interview.insert_linked_list(head, 1) + head = interview.insert_linked_list(head, 2) + head = interview.insert_linked_list(head, 3) + interview.print_linked_list(head) # Output: 1 -> 2 -> 3 -> None + + if args.tree: + # Tree Traversal example + console.rule("Tree Traversal Example") + root = interview.TreeNode(1) + root.left = interview.TreeNode(2) + root.right = interview.TreeNode(3) + root.left.left = interview.TreeNode(4) + root.left.right = interview.TreeNode(5) + rprint(Panel(str(interview.inorder_traversal(root)), title="Inorder Traversal")) + rprint( + Panel(str(interview.preorder_traversal(root)), title="Preorder Traversal") + ) + rprint( + Panel(str(interview.postorder_traversal(root)), title="Postorder Traversal") + ) + + if args.graph: + # Graph Algorithms example + console.rule("Graph Algorithms Example") + graph = { + "A": ["B", "C"], + "B": ["A", "D", "E"], + "C": ["A", "F"], + "D": ["B"], + "E": ["B", "F"], + "F": ["C", "E"], + } + rprint(Panel(str(interview.dfs(graph, "A")), title="DFS")) + rprint(Panel(str(interview.bfs(graph, "A")), title="BFS")) + + if args.interactive: + # Starting interactive session with tab completion + setup_readline(locals()) + banner = "Interactive programming interview session started. Type 'exit()' or 'Ctrl-D' to exit." + code.interact( + banner=banner, + local=locals(), + exitmsg="Great interview!", + ) + + +if __name__ == "__main__": + main() + +endef + +define PYTHON_CI_YAML +name: Build Wheels +endef + +define PYTHON_LICENSE_TXT +MIT License + +Copyright (c) [YEAR] [OWNER NAME] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +endef + +define PYTHON_PROJECT_TOML +[build-system] +endef + +define SEPARATOR +.==========================================================================================================================================. +| | +| _|_|_| _| _| _| _| _| _|_| _| _| | +| _| _| _| _|_| _|_| _|_| _|_|_| _|_|_|_| _|_| _|_| _|_|_| _| _| _|_| _| _| _|_| | +| _|_|_| _|_| _| _| _| _|_|_|_| _| _| _| _| _| _| _| _|_| _|_|_|_| _|_|_|_| _| _| _|_|_|_| | +| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| | +| _| _| _|_| _| _|_|_| _|_|_| _|_| _| _| _|_|_| _| _| _|_|_| _| _| _| _|_|_| | +| _| | +| _| | +`==========================================================================================================================================' +endef + +define TINYMCE_JS +import tinymce from 'tinymce'; +import 'tinymce/icons/default'; +import 'tinymce/themes/silver'; +import 'tinymce/skins/ui/oxide/skin.css'; +import 'tinymce/plugins/advlist'; +import 'tinymce/plugins/code'; +import 'tinymce/plugins/emoticons'; +import 'tinymce/plugins/emoticons/js/emojis'; +import 'tinymce/plugins/link'; +import 'tinymce/plugins/lists'; +import 'tinymce/plugins/table'; +import 'tinymce/models/dom'; + +tinymce.init({ + selector: 'textarea#editor', + plugins: 'advlist code emoticons link lists table', + toolbar: 'bold italic | bullist numlist | link emoticons', + skin: false, + content_css: false, +}); +endef + +define WAGTAIL_BASE_TEMPLATE +{% load static wagtailcore_tags wagtailuserbar webpack_loader %} + + + + + + {% block title %} + {% if page.seo_title %} + {{ page.seo_title }} + {% else %} + {{ page.title }} + {% endif %} + {% endblock %} + {% block title_suffix %} + {% wagtail_site as current_site %} + {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %} + {% endblock %} + + {% if page.search_description %}{% endif %} + + {# Force all links in the live preview panel to be opened in a new tab #} + {% if request.in_preview_panel %}{% endif %} + {% stylesheet_pack 'app' %} + {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} + + {% include 'favicon.html' %} + {% csrf_token %} + + +
    + {% wagtailuserbar %} +
    + {% include 'header.html' %} + {% if messages %} +
    + {% for message in messages %} + + {% endfor %} +
    + {% endif %} +
    + {% block content %}{% endblock %} +
    +
    + {% include 'footer.html' %} + {% include 'offcanvas.html' %} + {% javascript_pack 'app' %} + {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} + + +endef + +define WAGTAIL_BLOCK_CAROUSEL + +endef + +define WAGTAIL_BLOCK_MARKETING +{% load wagtailcore_tags %} +
    + {% if block.value.images.0 %} + {% include 'blocks/carousel_block.html' %} + {% else %} + {{ self.title }} + {{ self.content }} + {% endif %} +
    +endef + +define WAGTAIL_CONTACT_PAGE_LANDING +{% extends 'base.html' %} +{% block content %}

    Thank you!

    {% endblock %} +endef + +define WAGTAIL_CONTACT_PAGE_MODEL +from django.db import models +from modelcluster.fields import ParentalKey +from wagtail.admin.panels import ( + FieldPanel, FieldRowPanel, + InlinePanel, MultiFieldPanel +) +from wagtail.fields import RichTextField +from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField + + +class FormField(AbstractFormField): + page = ParentalKey('ContactPage', on_delete=models.CASCADE, related_name='form_fields') + + +class ContactPage(AbstractEmailForm): + intro = RichTextField(blank=True) + thank_you_text = RichTextField(blank=True) + + content_panels = AbstractEmailForm.content_panels + [ + FieldPanel('intro'), + InlinePanel('form_fields', label="Form fields"), + FieldPanel('thank_you_text'), + MultiFieldPanel([ + FieldRowPanel([ + FieldPanel('from_address', classname="col6"), + FieldPanel('to_address', classname="col6"), + ]), + FieldPanel('subject'), + ], "Email"), + ] + + class Meta: + verbose_name = "Contact Page" +endef + +define WAGTAIL_CONTACT_PAGE_TEMPLATE +{% extends 'base.html' %} +{% load crispy_forms_tags static wagtailcore_tags %} +{% block content %} +

    {{ page.title }}

    + {{ page.intro|richtext }} +
    + {% csrf_token %} + {{ form.as_p }} + +
    +{% endblock %} +endef + +define WAGTAIL_CONTACT_PAGE_TEST +from django.test import TestCase +from wagtail.test.utils import WagtailPageTestCase +from wagtail.models import Page + +from contactpage.models import ContactPage, FormField + +class ContactPageTest(TestCase, WagtailPageTestCase): + def test_contact_page_creation(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + + # Save the ContactPage instance + self.assertEqual(contact_page.save_revision().publish().get_latest_revision_as_page(), contact_page) + + def test_form_field_creation(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + # Save the ContactPage instance + contact_page_revision = contact_page.save_revision() + contact_page_revision.publish() + + # Create a FormField associated with the ContactPage + form_field = FormField( + page=contact_page, + label='Your Name', + field_type='singleline', + required=True + ) + form_field.save() + + # Retrieve the ContactPage from the database + contact_page_from_db = Page.objects.get(id=contact_page.id).specific + + # Check if the FormField is associated with the ContactPage + self.assertEqual(contact_page_from_db.form_fields.first(), form_field) + + def test_contact_page_form_submission(self): + # Create a ContactPage instance + contact_page = ContactPage( + title='Contact', + intro='Welcome to our contact page!', + thank_you_text='Thank you for reaching out.' + ) + # Save the ContactPage instance + contact_page_revision = contact_page.save_revision() + contact_page_revision.publish() + + # Simulate a form submission + form_data = { + 'your_name': 'John Doe', + # Add other form fields as needed + } + + response = self.client.post(contact_page.url, form_data) + + # Check if the form submission is successful (assuming a 302 redirect) + self.assertEqual(response.status_code, 302) + + # You may add more assertions based on your specific requirements +endef + +define WAGTAIL_HEADER_PREFIX +{% load wagtailcore_tags %} +{% wagtail_site as current_site %} +endef + +define WAGTAIL_HOME_PAGE_MODEL +from wagtail.models import Page +from wagtail.fields import StreamField +from wagtail import blocks +from wagtail.admin.panels import FieldPanel +from wagtail.images.blocks import ImageChooserBlock + + +class MarketingBlock(blocks.StructBlock): + title = blocks.CharBlock(required=False, help_text="Enter the block title") + content = blocks.RichTextBlock(required=False, help_text="Enter the block content") + images = blocks.ListBlock( + ImageChooserBlock(required=False), + help_text="Select one or two images for column display. Select three or more images for carousel display.", + ) + image = ImageChooserBlock( + required=False, help_text="Select one image for background display." + ) + block_class = blocks.CharBlock( + required=False, + help_text="Enter a CSS class for styling the marketing block", + classname="full title", + default="vh-100 bg-secondary", + ) + image_class = blocks.CharBlock( + required=False, + help_text="Enter a CSS class for styling the column display image(s)", + classname="full title", + default="img-thumbnail p-5", + ) + layout_class = blocks.CharBlock( + required=False, + help_text="Enter a CSS class for styling the layout.", + classname="full title", + default="d-flex flex-row", + ) + + class Meta: + icon = "placeholder" + template = "blocks/marketing_block.html" + + +class HomePage(Page): + template = "home/home_page.html" # Create a template for rendering the home page + + marketing_blocks = StreamField( + [ + ("marketing_block", MarketingBlock()), + ], + blank=True, + null=True, + use_json_field=True, + ) + content_panels = Page.content_panels + [ + FieldPanel("marketing_blocks"), + ] + + class Meta: + verbose_name = "Home Page" +endef + +define WAGTAIL_HOME_PAGE_TEMPLATE +{% extends "base.html" %} +{% load wagtailcore_tags %} +{% block content %} +
    + {% for block in page.marketing_blocks %} + {% include_block block %} + {% endfor %} +
    +{% endblock %} +endef + +define WAGTAIL_PRIVACY_PAGE_MODEL +from wagtail.models import Page +from wagtail.admin.panels import FieldPanel +from wagtailmarkdown.fields import MarkdownField + + +class PrivacyPage(Page): + """ + A Wagtail Page model for the Privacy Policy page. + """ + + template = "privacy_page.html" + + body = MarkdownField() + + content_panels = Page.content_panels + [ + FieldPanel("body", classname="full"), + ] + + class Meta: + verbose_name = "Privacy Page" +endef + +define WAGTAIL_PRIVACY_PAGE_TEMPLATE +{% extends 'base.html' %} +{% load wagtailmarkdown %} +{% block content %}
    {{ page.body|markdown }}
    {% endblock %} +endef + +define WAGTAIL_SEARCH_TEMPLATE +{% extends "base.html" %} +{% load static wagtailcore_tags %} +{% block body_class %}template-searchresults{% endblock %} +{% block title %}Search{% endblock %} +{% block content %} +

    Search

    +
    + + +
    + {% if search_results %} +
      + {% for result in search_results %} +
    • +

      + {{ result }} +

      + {% if result.search_description %}{{ result.search_description }}{% endif %} +
    • + {% endfor %} +
    + {% if search_results.has_previous %} + Previous + {% endif %} + {% if search_results.has_next %} + Next + {% endif %} + {% elif search_query %} + No results found + {% else %} + No results found. Try a test query? + {% endif %} +{% endblock %} +endef + +define WAGTAIL_SEARCH_URLS +from django.urls import path +from .views import search + +urlpatterns = [path("", search, name="search")] +endef + +define WAGTAIL_SETTINGS +INSTALLED_APPS.append("wagtail_color_panel") +INSTALLED_APPS.append("wagtail_modeladmin") +INSTALLED_APPS.append("wagtail.contrib.settings") +INSTALLED_APPS.append("wagtailmarkdown") +INSTALLED_APPS.append("wagtailmenus") +INSTALLED_APPS.append("wagtailseo") +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "wagtail.contrib.settings.context_processors.settings" +) +TEMPLATES[0]["OPTIONS"]["context_processors"].append( + "wagtailmenus.context_processors.wagtailmenus" +) +endef + +define WAGTAIL_SITEPAGE_MODEL +from wagtail.models import Page + + +class SitePage(Page): + template = "sitepage/site_page.html" + + class Meta: + verbose_name = "Site Page" +endef + +define WAGTAIL_SITEPAGE_TEMPLATE +{% extends 'base.html' %} +{% block content %} +

    {{ page.title }}

    +{% endblock %} +endef + +define WAGTAIL_URLS +from django.conf import settings +from django.urls import include, path +from django.contrib import admin + +from wagtail.admin import urls as wagtailadmin_urls +from wagtail.documents import urls as wagtaildocs_urls + +from search import views as search_views + +urlpatterns = [ + path("django/", admin.site.urls), + path("wagtail/", include(wagtailadmin_urls)), + path("documents/", include(wagtaildocs_urls)), + path("search/", search_views.search, name="search"), +] + +if settings.DEBUG: + from django.conf.urls.static import static + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + + # Serve static and media files from development server + urlpatterns += staticfiles_urlpatterns() + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +endef + +define WAGTAIL_URLS_HOME +urlpatterns += [ + # For anything not caught by a more specific rule above, hand over to + # Wagtail's page serving mechanism. This should be the last pattern in + # the list: + path("", include("wagtail.urls")), + # Alternatively, if you want Wagtail pages to be served from a subpath + # of your site, rather than the site root: + # path("pages/", include("wagtail.urls"), +] +endef + +define WEBPACK_CONFIG_JS +const path = require('path'); + +module.exports = { + mode: 'development', + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, +}; +endef + +define WEBPACK_INDEX_HTML + + + + + + Hello, Webpack! + + + + + +endef + +define WEBPACK_INDEX_JS +const message = "Hello, World!"; +console.log(message); +endef + +define WEBPACK_REVEAL_CONFIG_JS +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = { + mode: 'development', + entry: './src/index.js', + output: { + filename: 'bundle.js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.css$$/, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: 'bundle.css', + }), + ], +}; +endef + +define WEBPACK_REVEAL_INDEX_HTML + + + + + + Project Makefile + + +
    +
    +
    + Slide 1: Draw some circles +
    +
    + Slide 2: Draw the rest of the owl +
    +
    +
    + + +endef + +define WEBPACK_REVEAL_INDEX_JS +import 'reveal.js/dist/reveal.css'; +import 'reveal.js/dist/theme/black.css'; +import Reveal from 'reveal.js'; +import RevealNotes from 'reveal.js/plugin/notes/notes.js'; +Reveal.initialize({ slideNumber: true, plugins: [ RevealNotes ]}); +endef + +# ------------------------------------------------------------------------------ +# Export variables used by phony target rules +# ------------------------------------------------------------------------------ + +export DJANGO_ALLAUTH_BASE_TEMPLATE +export DJANGO_API_SERIALIZERS +export DJANGO_API_VIEWS +export DJANGO_APP_TESTS +export DJANGO_BACKEND_APPS +export DJANGO_BASE_TEMPLATE +export DJANGO_CUSTOM_ADMIN +export DJANGO_DOCKERCOMPOSE +export DJANGO_DOCKERFILE +export DJANGO_FAVICON_TEMPLATE +export DJANGO_FOOTER_TEMPLATE +export DJANGO_FRONTEND_APP +export DJANGO_FRONTEND_APP_CONFIG +export DJANGO_FRONTEND_BABELRC +export DJANGO_FRONTEND_COMPONENTS +export DJANGO_FRONTEND_COMPONENT_CLOCK +export DJANGO_FRONTEND_COMPONENT_ERROR +export DJANGO_FRONTEND_COMPONENT_USER_MENU +export DJANGO_FRONTEND_CONTEXT_INDEX +export DJANGO_FRONTEND_CONTEXT_USER_PROVIDER +export DJANGO_FRONTEND_ESLINTRC +export DJANGO_FRONTEND_OFFCANVAS_TEMPLATE +export DJANGO_FRONTEND_PORTAL +export DJANGO_FRONTEND_STYLES +export DJANGO_FRONTEND_THEME_BLUE +export DJANGO_FRONTEND_THEME_TOGGLER +export DJANGO_HEADER_TEMPLATE +export DJANGO_HOME_PAGE_ADMIN +export DJANGO_HOME_PAGE_MODELS +export DJANGO_HOME_PAGE_TEMPLATE +export DJANGO_HOME_PAGE_URLS +export DJANGO_HOME_PAGE_VIEWS +export DJANGO_LOGGING_DEMO_ADMIN +export DJANGO_LOGGING_DEMO_MODELS +export DJANGO_LOGGING_DEMO_SETTINGS +export DJANGO_LOGGING_DEMO_URLS +export DJANGO_LOGGING_DEMO_VIEWS +export DJANGO_MANAGE_PY +export DJANGO_MODEL_FORM_DEMO_ADMIN +export DJANGO_MODEL_FORM_DEMO_FORMS +export DJANGO_MODEL_FORM_DEMO_MODEL +export DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL +export DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM +export DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST +export DJANGO_MODEL_FORM_DEMO_URLS +export DJANGO_MODEL_FORM_DEMO_VIEWS +export DJANGO_PAYMENTS_ADMIN +export DJANGO_PAYMENTS_FORM +export DJANGO_PAYMENTS_MIGRATION_0002 +export DJANGO_PAYMENTS_MIGRATION_0003 +export DJANGO_PAYMENTS_MODELS +export DJANGO_PAYMENTS_TEMPLATE_CANCEL +export DJANGO_PAYMENTS_TEMPLATE_CHECKOUT +export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL +export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST +export DJANGO_PAYMENTS_TEMPLATE_SUCCESS +export DJANGO_PAYMENTS_URLS +export DJANGO_PAYMENTS_VIEW +export DJANGO_SEARCH_FORMS +export DJANGO_SEARCH_SETTINGS +export DJANGO_SEARCH_TEMPLATE +export DJANGO_SEARCH_URLS +export DJANGO_SEARCH_UTILS +export DJANGO_SEARCH_VIEWS +export DJANGO_SETTINGS_AUTHENTICATION_BACKENDS +export DJANGO_SETTINGS_BASE +export DJANGO_SETTINGS_BASE_MINIMAL +export DJANGO_SETTINGS_CRISPY_FORMS +export DJANGO_SETTINGS_DATABASE +export DJANGO_SETTINGS_DEV +export DJANGO_SETTINGS_HOME_PAGE +export DJANGO_SETTINGS_INSTALLED_APPS +export DJANGO_SETTINGS_MIDDLEWARE +export DJANGO_SETTINGS_MODEL_FORM_DEMO +export DJANGO_SETTINGS_PAYMENTS +export DJANGO_SETTINGS_PROD +export DJANGO_SETTINGS_REST_FRAMEWORK +export DJANGO_SETTINGS_SITEUSER +export DJANGO_SETTINGS_THEMES +export DJANGO_SITEUSER_ADMIN +export DJANGO_SITEUSER_EDIT_TEMPLATE +export DJANGO_SITEUSER_FORM +export DJANGO_SITEUSER_MODEL +export DJANGO_SITEUSER_URLS +export DJANGO_SITEUSER_VIEW +export DJANGO_SITEUSER_VIEW_TEMPLATE +export DJANGO_URLS +export DJANGO_URLS_ALLAUTH +export DJANGO_URLS_API +export DJANGO_URLS_DEBUG_TOOLBAR +export DJANGO_URLS_HOME_PAGE +export DJANGO_URLS_LOGGING_DEMO +export DJANGO_URLS_MODEL_FORM_DEMO +export DJANGO_URLS_SITEUSER +export DJANGO_UTILS +export EB_CUSTOM_ENV_EC2_USER +export EB_CUSTOM_ENV_VAR_FILE +export GIT_IGNORE +export JENKINS_FILE +export MAKEFILE_CUSTOM +export PIP_INSTALL_REQUIREMENTS_TEST +export PROGRAMMING_INTERVIEW +export PYTHON_CI_YAML +export PYTHON_LICENSE_TXT +export PYTHON_PROJECT_TOML +export SEPARATOR +export TINYMCE_JS +export WAGTAIL_BASE_TEMPLATE +export WAGTAIL_BLOCK_CAROUSEL +export WAGTAIL_BLOCK_MARKETING +export WAGTAIL_CONTACT_PAGE_LANDING +export WAGTAIL_CONTACT_PAGE_MODEL +export WAGTAIL_CONTACT_PAGE_TEMPLATE +export WAGTAIL_CONTACT_PAGE_TEST +export WAGTAIL_HOME_PAGE_MODEL +export WAGTAIL_HOME_PAGE_TEMPLATE +export WAGTAIL_HOME_PAGE_URLS +export WAGTAIL_HOME_PAGE_VIEWS +export WAGTAIL_PRIVACY_PAGE_MODEL +export WAGTAIL_PRIVACY_PAGE_MODEL +export WAGTAIL_PRIVACY_PAGE_TEMPLATE +export WAGTAIL_SEARCH_TEMPLATE +export WAGTAIL_SEARCH_URLS +export WAGTAIL_SETTINGS +export WAGTAIL_SITEPAGE_MODEL +export WAGTAIL_SITEPAGE_TEMPLATE +export WAGTAIL_URLS +export WAGTAIL_URLS_HOME +export WEBPACK_CONFIG_JS +export WEBPACK_INDEX_HTML +export WEBPACK_INDEX_JS +export WEBPACK_REVEAL_CONFIG_JS +export WEBPACK_REVEAL_INDEX_HTML +export WEBPACK_REVEAL_INDEX_JS + +# ------------------------------------------------------------------------------ +# Multi-line phony target rules +# ------------------------------------------------------------------------------ + +.PHONY: aws-check-env-profile-default +aws-check-env-profile-default: +ifndef AWS_PROFILE + $(error AWS_PROFILE is undefined) +endif + +.PHONY: aws-check-env-region-default +aws-check-env-region-default: +ifndef AWS_REGION + $(error AWS_REGION is undefined) +endif + +.PHONY: aws-secret-default +aws-secret-default: aws-check-env + @SECRET_KEY=$$(openssl rand -base64 48); aws ssm put-parameter --name "SECRET_KEY" --value "$$SECRET_KEY" --type String + +.PHONY: aws-sg-default +aws-sg-default: aws-check-env + aws ec2 describe-security-groups $(AWS_OPTS) + +.PHONY: aws-ssm-default +aws-ssm-default: aws-check-env + aws ssm describe-parameters $(AWS_OPTS) + @echo "Get parameter values with: aws ssm getparameter --name ." + +.PHONY: aws-subnet-default +aws-subnet-default: aws-check-env + aws ec2 describe-subnets $(AWS_OPTS) + +.PHONY: aws-vol-available-default +aws-vol-available-default: aws-check-env + aws ec2 describe-volumes --filters Name=status,Values=available --query "Volumes[*].{ID:VolumeId,Size:Size}" --output table + +.PHONY: aws-vol-default +aws-vol-default: aws-check-env + aws ec2 describe-volumes --output table + +.PHONY: aws-vpc-default +aws-vpc-default: aws-check-env + aws ec2 describe-vpcs $(AWS_OPTS) + +.PHONY: db-import-default +db-import-default: + @psql $(DJANGO_DB_NAME) < $(DJANGO_DB_NAME).sql + +.PHONY: db-init-default +db-init-default: + -dropdb $(PROJECT_NAME) + -createdb $(PROJECT_NAME) + +.PHONY: db-init-mysql-default +db-init-mysql-default: + -mysqladmin -u root drop $(PROJECT_NAME) + -mysqladmin -u root create $(PROJECT_NAME) + +.PHONY: db-init-test-default +db-init-test-default: + -dropdb test_$(PROJECT_NAME) + -createdb test_$(PROJECT_NAME) + +.PHONY: django-allauth-default +django-allauth-default: + $(ADD_DIR) backend/templates/allauth/layouts + @echo "$$DJANGO_ALLAUTH_BASE_TEMPLATE" > backend/templates/allauth/layouts/base.html + @echo "$$DJANGO_URLS_ALLAUTH" >> $(DJANGO_URLS_FILE) + -$(GIT_ADD) backend/templates/allauth/layouts/base.html + +.PHONY: django-app-tests-default +django-app-tests-default: + @echo "$$DJANGO_APP_TESTS" > $(APP_DIR)/tests.py + +.PHONY: django-base-template-default +django-base-template-default: + @$(ADD_DIR) backend/templates + @echo "$$DJANGO_BASE_TEMPLATE" > backend/templates/base.html + -$(GIT_ADD) backend/templates/base.html + +.PHONY: django-custom-admin-default +django-custom-admin-default: + @echo "$$DJANGO_CUSTOM_ADMIN" > $(DJANGO_CUSTOM_ADMIN_FILE) + @echo "$$DJANGO_BACKEND_APPS" > $(DJANGO_BACKEND_APPS_FILE) + -$(GIT_ADD) backend/*.py + +.PHONY: django-db-shell-default +django-db-shell-default: + python manage.py dbshell + +.PHONY: django-dockerfile-default +django-dockerfile-default: + @echo "$$DJANGO_DOCKERFILE" > Dockerfile + -$(GIT_ADD) Dockerfile + @echo "$$DJANGO_DOCKERCOMPOSE" > docker-compose.yml + -$(GIT_ADD) docker-compose.yml + +.PHONY: django-favicon-default +django-favicon-default: + @echo "$$DJANGO_FAVICON_TEMPLATE" > backend/templates/favicon.html + -$(GIT_ADD) backend/templates/favicon.html + +.PHONY: django-footer-template-default +django-footer-template-default: + @echo "$$DJANGO_FOOTER_TEMPLATE" > backend/templates/footer.html + -$(GIT_ADD) backend/templates/footer.html + +.PHONY: django-frontend-default +django-frontend-default: python-webpack-init + $(ADD_DIR) frontend/src/context + $(ADD_DIR) frontend/src/images + $(ADD_DIR) frontend/src/utils + @echo "$$DJANGO_FRONTEND_APP" > frontend/src/application/app.js + @echo "$$DJANGO_FRONTEND_APP_CONFIG" > frontend/src/application/config.js + @echo "$$DJANGO_FRONTEND_BABELRC" > frontend/.babelrc + @echo "$$DJANGO_FRONTEND_COMPONENT_CLOCK" > frontend/src/components/Clock.js + @echo "$$DJANGO_FRONTEND_COMPONENT_ERROR" > frontend/src/components/ErrorBoundary.js + @echo "$$DJANGO_FRONTEND_CONTEXT_INDEX" > frontend/src/context/index.js + @echo "$$DJANGO_FRONTEND_CONTEXT_USER_PROVIDER" > frontend/src/context/UserContextProvider.js + @echo "$$DJANGO_FRONTEND_COMPONENT_USER_MENU" > frontend/src/components/UserMenu.js + @echo "$$DJANGO_FRONTEND_COMPONENTS" > frontend/src/components/index.js + @echo "$$DJANGO_FRONTEND_ESLINTRC" > frontend/.eslintrc + @echo "$$DJANGO_FRONTEND_PORTAL" > frontend/src/dataComponents.js + @echo "$$DJANGO_FRONTEND_STYLES" > frontend/src/styles/index.scss + @echo "$$DJANGO_FRONTEND_THEME_BLUE" > frontend/src/styles/theme-blue.scss + @echo "$$DJANGO_FRONTEND_THEME_TOGGLER" > frontend/src/utils/themeToggler.js + # @echo "$$TINYMCE_JS" > frontend/src/utils/tinymce.js + @$(MAKE) npm-install-django + @$(MAKE) npm-install-django-dev + -$(GIT_ADD) $(DJANGO_FRONTEND_FILES) + +.PHONY: django-graph-default +django-graph-default: + python manage.py graph_models -a -o $(PROJECT_NAME).png + +.PHONY: django-header-template-default +django-header-template-default: + @echo "$$DJANGO_HEADER_TEMPLATE" > backend/templates/header.html + -$(GIT_ADD) backend/templates/header.html + +.PHONY: django-home-default +django-home-default: + python manage.py startapp home + $(ADD_DIR) home/templates + @echo "$$DJANGO_HOME_PAGE_ADMIN" > home/admin.py + @echo "$$DJANGO_HOME_PAGE_MODELS" > home/models.py + @echo "$$DJANGO_HOME_PAGE_TEMPLATE" > home/templates/home.html + @echo "$$DJANGO_HOME_PAGE_VIEWS" > home/views.py + @echo "$$DJANGO_HOME_PAGE_URLS" > home/urls.py + @echo "$$DJANGO_URLS_HOME_PAGE" >> $(DJANGO_URLS_FILE) + @echo "$$DJANGO_SETTINGS_HOME_PAGE" >> $(DJANGO_SETTINGS_BASE_FILE) + export APP_DIR="home"; $(MAKE) django-app-tests + -$(GIT_ADD) home/templates + -$(GIT_ADD) home/*.py + -$(GIT_ADD) home/migrations/*.py + +.PHONY: django-init-default +django-init-default: separator \ + db-init \ + django-install \ + django-project \ + django-utils \ + pip-freeze \ + pip-init-test \ + django-settings-directory \ + django-custom-admin \ + django-dockerfile \ + django-offcanvas-template \ + django-header-template \ + django-footer-template \ + django-base-template \ + django-manage-py \ + django-urls \ + django-urls-debug-toolbar \ + django-allauth \ + django-favicon \ + git-ignore \ + django-settings-base \ + django-settings-dev \ + django-settings-prod \ + django-siteuser \ + django-home \ + django-rest-serializers \ + django-rest-views \ + django-urls-api \ + django-frontend \ + django-migrate \ + django-su + +.PHONY: django-init-minimal-default +django-init-minimal-default: separator \ + db-init \ + django-install-minimal \ + django-project \ + django-settings-directory \ + django-settings-base-minimal \ + django-settings-dev \ + pip-freeze \ + pip-init-test \ + django-custom-admin \ + django-dockerfile \ + django-offcanvas-template \ + django-header-template \ + django-footer-template \ + django-base-template \ + django-manage-py \ + django-urls \ + django-urls-debug-toolbar \ + django-favicon \ + django-settings-prod \ + django-home \ + django-utils \ + django-frontend \ + django-migrate \ + git-ignore \ + django-su + +.PHONY: django-init-wagtail-default +django-init-wagtail-default: separator \ + db-init \ + django-install \ + wagtail-install \ + wagtail-project \ + django-utils \ + pip-freeze \ + pip-init-test \ + django-custom-admin \ + django-dockerfile \ + django-offcanvas-template \ + wagtail-header-prefix-template \ + django-header-template \ + wagtail-base-template \ + django-footer-template \ + django-manage-py \ + wagtail-home \ + wagtail-urls \ + django-urls-debug-toolbar \ + django-allauth \ + django-favicon \ + git-ignore \ + wagtail-search \ + django-settings-base \ + django-settings-dev \ + django-settings-prod \ + wagtail-settings \ + django-siteuser \ + django-model-form-demo \ + django-logging-demo \ + django-payments-demo-default \ + django-rest-serializers \ + django-rest-views \ + django-urls-api \ + wagtail-urls-home \ + django-frontend \ + django-migrate \ + django-su + +.PHONY: django-install-default +django-install-default: + $(PIP_ENSURE) + python -m pip install \ + Django \ + Faker \ + boto3 \ + crispy-bootstrap5 \ + djangorestframework \ + django-allauth \ + django-after-response \ + django-ckeditor \ + django-colorful \ + django-cors-headers \ + django-countries \ + django-crispy-forms \ + django-debug-toolbar \ + django-extensions \ + django-hijack \ + django-honeypot \ + django-imagekit \ + django-import-export \ + django-ipware \ + django-multiselectfield \ + django-ninja \ + django-phonenumber-field \ + django-recurrence \ + django-recaptcha \ + django-registration \ + django-richtextfield \ + django-sendgrid-v5 \ + django-social-share \ + django-sql-explorer \ + django-storages \ + django-tables2 \ + django-timezone-field \ + django-widget-tweaks \ + dj-database-url \ + dj-rest-auth \ + dj-stripe \ + docutils \ + enmerkar \ + gunicorn \ + html2docx \ + icalendar \ + mailchimp-marketing \ + mailchimp-transactional \ + phonenumbers \ + pipdeptree \ + psycopg2-binary \ + pydotplus \ + python-webpack-boilerplate \ + python-docx \ + reportlab \ + texttable + +.PHONY: django-install-minimal-default +django-install-minimal-default: + $(PIP_ENSURE) + python -m pip install \ + Django \ + dj-database-url \ + django-debug-toolbar \ + python-webpack-boilerplate + +.PHONY: django-lint-default +django-lint-default: + -ruff format -v + -djlint --reformat --format-css --format-js . + -ruff check -v --fix + +.PHONY: django-loaddata-default +django-loaddata-default: + python manage.py loaddata + +.PHONY: django-logging-demo-default +django-logging-demo-default: + python manage.py startapp logging_demo + @echo "$$DJANGO_LOGGING_DEMO_ADMIN" > logging_demo/admin.py + @echo "$$DJANGO_LOGGING_DEMO_MODELS" > logging_demo/models.py + @echo "$$DJANGO_LOGGING_DEMO_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_LOGGING_DEMO_URLS" > logging_demo/urls.py + @echo "$$DJANGO_LOGGING_DEMO_VIEWS" > logging_demo/views.py + @echo "$$DJANGO_URLS_LOGGING_DEMO" >> $(DJANGO_URLS_FILE) + export APP_DIR="logging_demo"; $(MAKE) django-app-tests + -$(GIT_ADD) logging_demo/*.py + -$(GIT_ADD) logging_demo/migrations/*.py + +.PHONY: django-manage-py-default +django-manage-py-default: + @echo "$$DJANGO_MANAGE_PY" > manage.py + -$(GIT_ADD) manage.py + +.PHONY: django-migrate-default +django-migrate-default: + python manage.py migrate + +.PHONY: django-migrations-make-default +django-migrations-make-default: + python manage.py makemigrations + +.PHONY: django-migrations-show-default +django-migrations-show-default: + python manage.py showmigrations + +.PHONY: django-model-form-demo-default +django-model-form-demo-default: + python manage.py startapp model_form_demo + @echo "$$DJANGO_MODEL_FORM_DEMO_ADMIN" > model_form_demo/admin.py + @echo "$$DJANGO_MODEL_FORM_DEMO_FORMS" > model_form_demo/forms.py + @echo "$$DJANGO_MODEL_FORM_DEMO_MODEL" > model_form_demo/models.py + @echo "$$DJANGO_MODEL_FORM_DEMO_URLS" > model_form_demo/urls.py + @echo "$$DJANGO_MODEL_FORM_DEMO_VIEWS" > model_form_demo/views.py + $(ADD_DIR) model_form_demo/templates + @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL" > model_form_demo/templates/model_form_demo_detail.html + @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM" > model_form_demo/templates/model_form_demo_form.html + @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST" > model_form_demo/templates/model_form_demo_list.html + @echo "$$DJANGO_SETTINGS_MODEL_FORM_DEMO" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_URLS_MODEL_FORM_DEMO" >> $(DJANGO_URLS_FILE) + export APP_DIR="model_form_demo"; $(MAKE) django-app-tests + python manage.py makemigrations + -$(GIT_ADD) model_form_demo/*.py + -$(GIT_ADD) model_form_demo/templates + -$(GIT_ADD) model_form_demo/migrations + +.PHONY: django-offcanvas-template-default +django-offcanvas-template-default: + -$(ADD_DIR) backend/templates + @echo "$$DJANGO_FRONTEND_OFFCANVAS_TEMPLATE" > backend/templates/offcanvas.html + -$(GIT_ADD) backend/templates/offcanvas.html + +.PHONY: django-open-default +django-open-default: +ifeq ($(UNAME), Linux) + @echo "Opening on Linux." + xdg-open http://0.0.0.0:8000 +else ifeq ($(UNAME), Darwin) + @echo "Opening on macOS (Darwin)." + open http://0.0.0.0:8000 +else + @echo "Unable to open on: $(UNAME)" +endif + +.PHONY: django-payments-demo-default +django-payments-demo-default: + python manage.py startapp payments + @echo "$$DJANGO_PAYMENTS_FORM" > payments/forms.py + @echo "$$DJANGO_PAYMENTS_MODELS" > payments/models.py + @echo "$$DJANGO_PAYMENTS_ADMIN" > payments/admin.py + @echo "$$DJANGO_PAYMENTS_VIEW" > payments/views.py + @echo "$$DJANGO_PAYMENTS_URLS" > payments/urls.py + $(ADD_DIR) payments/templates/payments + $(ADD_DIR) payments/management/commands + @echo "$$DJANGO_PAYMENTS_TEMPLATE_CANCEL" > payments/templates/payments/cancel.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_CHECKOUT" > payments/templates/payments/checkout.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_SUCCESS" > payments/templates/payments/success.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST" > payments/templates/payments/product_list.html + @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL" > payments/templates/payments/product_detail.html + @echo "$$DJANGO_SETTINGS_PAYMENTS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_URLS_PAYMENTS" >> $(DJANGO_URLS_FILE) + export APP_DIR="payments"; $(MAKE) django-app-tests + python manage.py makemigrations payments + @echo "$$DJANGO_PAYMENTS_MIGRATION_0002" > payments/migrations/0002_set_stripe_api_keys.py + @echo "$$DJANGO_PAYMENTS_MIGRATION_0003" > payments/migrations/0003_create_initial_products.py + -$(GIT_ADD) payments/ + +.PHONY: django-project-default +django-project-default: + django-admin startproject backend . + -$(GIT_ADD) backend + +.PHONY: django-rest-serializers-default +django-rest-serializers-default: + @echo "$$DJANGO_API_SERIALIZERS" > backend/serializers.py + -$(GIT_ADD) backend/serializers.py + +.PHONY: django-rest-views-default +django-rest-views-default: + @echo "$$DJANGO_API_VIEWS" > backend/api.py + -$(GIT_ADD) backend/api.py + +.PHONY: django-search-default +django-search-default: + python manage.py startapp search + $(ADD_DIR) search/templates + @echo "$$DJANGO_SEARCH_TEMPLATE" > search/templates/search.html + @echo "$$DJANGO_SEARCH_FORMS" > search/forms.py + @echo "$$DJANGO_SEARCH_URLS" > search/urls.py + @echo "$$DJANGO_SEARCH_UTILS" > search/utils.py + @echo "$$DJANGO_SEARCH_VIEWS" > search/views.py + @echo "$$DJANGO_SEARCH_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "INSTALLED_APPS.append('search')" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "urlpatterns += [path('search/', include('search.urls'))]" >> $(DJANGO_URLS_FILE) + -$(GIT_ADD) search/templates + -$(GIT_ADD) search/*.py + +.PHONY: django-secret-key-default +django-secret-key-default: + @python -c "from secrets import token_urlsafe; print(token_urlsafe(50))" + +.PHONY: django-serve-default +django-serve-default: + npm run watch & + python manage.py runserver 0.0.0.0:8000 + +.PHONY: django-settings-base-default +django-settings-base-default: + @echo "$$DJANGO_SETTINGS_BASE" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_AUTHENTICATION_BACKENDS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_REST_FRAMEWORK" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_THEMES" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_DATABASE" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_INSTALLED_APPS" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_MIDDLEWARE" >> $(DJANGO_SETTINGS_BASE_FILE) + @echo "$$DJANGO_SETTINGS_CRISPY_FORMS" >> $(DJANGO_SETTINGS_BASE_FILE) + +.PHONY: django-settings-base-minimal-default +django-settings-base-minimal-default: + @echo "$$DJANGO_SETTINGS_BASE_MINIMAL" >> $(DJANGO_SETTINGS_BASE_FILE) + +.PHONY: django-settings-dev-default +django-settings-dev-default: + @echo "# $(PROJECT_NAME)" > $(DJANGO_SETTINGS_DEV_FILE) + @echo "$$DJANGO_SETTINGS_DEV" >> backend/settings/dev.py + -$(GIT_ADD) $(DJANGO_SETTINGS_DEV_FILE) + +.PHONY: django-settings-directory-default +django-settings-directory-default: + @$(ADD_DIR) $(DJANGO_SETTINGS_DIR) + @$(COPY_FILE) backend/settings.py backend/settings/base.py + @$(DEL_FILE) backend/settings.py + -$(GIT_ADD) backend/settings/*.py + +.PHONY: django-settings-prod-default +django-settings-prod-default: + @echo "$$DJANGO_SETTINGS_PROD" > $(DJANGO_SETTINGS_PROD_FILE) + -$(GIT_ADD) $(DJANGO_SETTINGS_PROD_FILE) + +.PHONY: django-shell-default +django-shell-default: + python manage.py shell + +.PHONY: django-siteuser-default +django-siteuser-default: + python manage.py startapp siteuser + $(ADD_DIR) siteuser/templates/ + @echo "$$DJANGO_SITEUSER_FORM" > siteuser/forms.py + @echo "$$DJANGO_SITEUSER_MODEL" > siteuser/models.py + @echo "$$DJANGO_SITEUSER_ADMIN" > siteuser/admin.py + @echo "$$DJANGO_SITEUSER_VIEW" > siteuser/views.py + @echo "$$DJANGO_SITEUSER_URLS" > siteuser/urls.py + @echo "$$DJANGO_SITEUSER_VIEW_TEMPLATE" > siteuser/templates/profile.html + @echo "$$DJANGO_SITEUSER_TEMPLATE" > siteuser/templates/user.html + @echo "$$DJANGO_SITEUSER_EDIT_TEMPLATE" > siteuser/templates/user_edit.html + @echo "$$DJANGO_URLS_SITEUSER" >> $(DJANGO_URLS_FILE) + @echo "$$DJANGO_SETTINGS_SITEUSER" >> $(DJANGO_SETTINGS_BASE_FILE) + export APP_DIR="siteuser"; $(MAKE) django-app-tests + -$(GIT_ADD) siteuser/templates + -$(GIT_ADD) siteuser/*.py + python manage.py makemigrations siteuser + -$(GIT_ADD) siteuser/migrations/*.py + +.PHONY: django-static-default +django-static-default: + python manage.py collectstatic --noinput + +.PHONY: django-su-default +django-su-default: + DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser --noinput --username=admin --email=$(PROJECT_EMAIL) + +.PHONY: django-test-default +django-test-default: npm-install django-static + -$(MAKE) pip-install-test + python manage.py test + +.PHONY: django-urls-api-default +django-urls-api-default: + @echo "$$DJANGO_URLS_API" >> $(DJANGO_URLS_FILE) + -$(GIT_ADD) $(DJANGO_URLS_FILE) + +.PHONY: django-urls-debug-toolbar-default +django-urls-debug-toolbar-default: + @echo "$$DJANGO_URLS_DEBUG_TOOLBAR" >> $(DJANGO_URLS_FILE) + +.PHONY: django-urls-default +django-urls-default: + @echo "$$DJANGO_URLS" > $(DJANGO_URLS_FILE) + -$(GIT_ADD) $(DJANGO_URLS_FILE) + +.PHONY: django-urls-show-default +django-urls-show-default: + python manage.py show_urls + +.PHONY: django-user-default +django-user-default: + python manage.py shell -c "from django.contrib.auth.models import User; \ + User.objects.create_user('user', '', 'user')" + +.PHONY: django-utils-default +django-utils-default: + @echo "$$DJANGO_UTILS" > backend/utils.py + -$(GIT_ADD) backend/utils.py + +.PHONY: docker-build-default +docker-build-default: + podman build -t $(PROJECT_NAME) . + +.PHONY: docker-compose-default +docker-compose-default: + podman compose up + +.PHONY: docker-list-default +docker-list-default: + podman container list --all + podman images --all + +.PHONY: docker-run-default +docker-run-default: + podman run $(PROJECT_NAME) + +.PHONY: docker-serve-default +docker-serve-default: + podman run -p 8000:8000 $(PROJECT_NAME) + +.PHONY: docker-shell-default +docker-shell-default: + podman run -it $(PROJECT_NAME) /bin/bash + +.PHONY: eb-check-env-default +eb-check-env-default: # https://stackoverflow.com/a/4731504/185820 +ifndef EB_SSH_KEY + $(error EB_SSH_KEY is undefined) +endif +ifndef VPC_ID + $(error VPC_ID is undefined) +endif +ifndef VPC_SG + $(error VPC_SG is undefined) +endif +ifndef VPC_SUBNET_EC2 + $(error VPC_SUBNET_EC2 is undefined) +endif +ifndef VPC_SUBNET_ELB + $(error VPC_SUBNET_ELB is undefined) +endif + +.PHONY: eb-create-default +eb-create-default: aws-check-env eb-check-env + eb create $(EB_ENV_NAME) \ + -im $(EC2_INSTANCE_MIN) \ + -ix $(EC2_INSTANCE_MAX) \ + -ip $(EC2_INSTANCE_PROFILE) \ + -i $(EC2_INSTANCE_TYPE) \ + -k $(EB_SSH_KEY) \ + -p $(EB_PLATFORM) \ + --elb-type $(EC2_LB_TYPE) \ + --vpc \ + --vpc.id $(VPC_ID) \ + --vpc.elbpublic \ + --vpc.publicip \ + --vpc.ec2subnets $(VPC_SUBNET_EC2) \ + --vpc.elbsubnets $(VPC_SUBNET_ELB) \ + --vpc.securitygroups $(VPC_SG) + +.PHONY: eb-custom-env-default +eb-custom-env-default: + $(ADD_DIR) .ebextensions + @echo "$$EB_CUSTOM_ENV_EC2_USER" > .ebextensions/bash.config + -$(GIT_ADD) .ebextensions/bash.config + $(ADD_DIR) .platform/hooks/postdeploy + @echo "$$EB_CUSTOM_ENV_VAR_FILE" > .platform/hooks/postdeploy/setenv.sh + -$(GIT_ADD) .platform/hooks/postdeploy/setenv.sh + +.PHONY: eb-deploy-default +eb-deploy-default: + eb deploy + +.PHONY: eb-export-default +eb-export-default: + @if [ ! -d $(EB_DIR_NAME) ]; then \ + echo "Directory $(EB_DIR_NAME) does not exist"; \ + else \ + echo "Directory $(EB_DIR_NAME) does exist!"; \ + eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); pg_dump -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" > $(DJANGO_DB_NAME).sql; \ + echo "Wrote $(DJANGO_DB_NAME).sql"; \ + fi + +.PHONY: eb-restart-default +eb-restart-default: + eb ssh -c "systemctl restart web" + +.PHONY: eb-rebuild-default +eb-rebuild-default: + aws elasticbeanstalk rebuild-environment --environment-name $(ENV_NAME) + +.PHONY: eb-upgrade-default +eb-upgrade-default: + eb upgrade + +.PHONY: eb-init-default +eb-init-default: aws-check-env-profile + eb init --profile=$(AWS_PROFILE) + +.PHONY: eb-list-default +eb-list-platforms-default: + aws elasticbeanstalk list-platform-versions + +.PHONY: eb-list-databases-default +eb-list-databases-default: + @eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); psql -l -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" + +.PHONY: eb-logs-default +eb-logs-default: + eb logs + +.PHONY: eb-print-env-default +eb-print-env-default: + eb printenv + +.PHONY: favicon-default +favicon-init-default: + dd if=/dev/urandom bs=64 count=1 status=none | base64 | convert -size 16x16 -depth 8 -background none -fill white label:@- favicon.png + convert favicon.png favicon.ico + -$(GIT_ADD) favicon.ico + $(DEL_FILE) favicon.png + +.PHONY: git-ignore-default +git-ignore-default: + @echo "$$GIT_IGNORE" > .gitignore + -$(GIT_ADD) .gitignore + +.PHONY: git-branches-default +git-branches-default: + -for i in $(GIT_BRANCHES) ; do \ + -@$(GIT_CHECKOUT) -t $$i ; done + +.PHONY: git-commit-message-clean-default +git-commit-message-clean-default: + -@$(GIT_COMMIT) -a -m "Clean" + +.PHONY: git-commit-message-default +git-commit-message-default: + -@$(GIT_COMMIT) -a -m $(GIT_COMMIT_MSG) + +.PHONY: git-commit-message-empty-default +git-commit-message-empty-default: + -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" + +.PHONY: git-commit-message-init-default +git-commit-message-init-default: + -@$(GIT_COMMIT) -a -m "Init" + +.PHONY: git-commit-message-last-default +git-commit-message-last-default: + git log -1 --pretty=%B > $(TMPDIR)/commit.txt + -$(GIT_COMMIT) -a -F $(TMPDIR)/commit.txt + +.PHONY: git-commit-message-lint-default +git-commit-message-lint-default: + -@$(GIT_COMMIT) -a -m "Lint" + +.PHONY: git-commit-message-mk-default +git-commit-message-mk-default: + -@$(GIT_COMMIT) project.mk -m "Add/update $(MAKEFILE_CUSTOM_FILE)" + +.PHONY: git-commit-message-rename-default +git-commit-message-rename-default: + -@$(GIT_COMMIT) -a -m "Rename" + +.PHONY: git-commit-message-sort-default +git-commit-message-sort-default: + -@$(GIT_COMMIT) -a -m "Sort" + +.PHONY: git-push-default +git-push-default: + -@$(GIT_PUSH) + +.PHONY: git-push-force-default +git-push-force-default: + -@$(GIT_PUSH_FORCE) + +.PHONY: git-commit-edit-default +git-commit-edit-default: + -$(GIT_COMMIT) -a + +.PHONY: git-prune-default +git-prune-default: + git remote update origin --prune + +.PHONY: git-set-upstream-default +git-set-upstream-default: + git push --set-upstream origin main + +.PHONY: git-set-default-default +git-set-default-default: + gh repo set-default + +.PHONY: git-short-default +git-short-default: + @echo $(GIT_REV) + +.PHONY: help-default +help-default: + @echo "Project Makefile 🤷" + @echo "Usage: make [options] [target] ..." + @echo "Examples:" + @echo " make help Print this message" + @echo " make list-defines list all defines in the Makefile" + @echo " make list-commands list all targets in the Makefile" + +.PHONY: jenkins-init-default +jenkins-init-default: + @echo "$$JENKINS_FILE" > Jenkinsfile + +.PHONY: makefile-list-commands-default +makefile-list-commands-default: + @for makefile in $(MAKEFILE_LIST); do \ + echo "Commands from $$makefile:"; \ + $(MAKE) -pRrq -f $$makefile : 2>/dev/null | \ + awk -v RS= -F: '/^# File/,/^# Finished Make data base/ { \ + if ($$1 !~ "^[#.]") { sub(/-default$$/, "", $$1); print $$1 } }' | \ + egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | \ + tr ' ' '\n' | \ + sort | \ + awk '{print $$0}' ; \ + echo; \ + done | $(PAGER) + +.PHONY: makefile-list-defines-default +makefile-list-defines-default: + @grep '^define [A-Za-z_][A-Za-z0-9_]*' Makefile + +.PHONY: makefile-list-exports-default +makefile-list-exports-default: + @grep '^export [A-Z][A-Z_]*' Makefile + +.PHONY: makefile-list-targets-default +makefile-list-targets-default: + @perl -ne 'print if /^\s*\.PHONY:/ .. /^[a-zA-Z0-9_-]+:/;' Makefile | grep -v .PHONY + +.PHONY: make-default +make-default: + -$(GIT_ADD) Makefile + -$(GIT_COMMIT) Makefile -m "Add/update project-makefile files" + -git push + +.PHONY: npm-init-default +npm-init-default: + npm init -y + -$(GIT_ADD) package.json + -$(GIT_ADD) package-lock.json + +.PHONY: npm-build-default +npm-build-default: + npm run build + +.PHONY: npm-install-default +npm-install-default: + npm install + -$(GIT_ADD) package-lock.json + +.PHONY: npm-install-django-default +npm-install-django-default: + npm install \ + @fortawesome/fontawesome-free \ + @fortawesome/fontawesome-svg-core \ + @fortawesome/free-brands-svg-icons \ + @fortawesome/free-solid-svg-icons \ + @fortawesome/react-fontawesome \ + bootstrap \ + camelize \ + date-fns \ + history \ + mapbox-gl \ + query-string \ + react-animate-height \ + react-chartjs-2 \ + react-copy-to-clipboard \ + react-date-range \ + react-dom \ + react-dropzone \ + react-hook-form \ + react-image-crop \ + react-map-gl \ + react-modal \ + react-resize-detector \ + react-select \ + react-swipeable \ + snakeize \ + striptags \ + url-join \ + viewport-mercator-project + +.PHONY: npm-install-django-dev-default +npm-install-django-dev-default: + npm install \ + eslint-plugin-react \ + eslint-config-standard \ + eslint-config-standard-jsx \ + @babel/core \ + @babel/preset-env \ + @babel/preset-react \ + --save-dev + +.PHONY: npm-serve-default +npm-serve-default: + npm run start + +.PHONY: npm-test-default +npm-test-default: + npm run test + +.PHONY: pip-deps-default +pip-deps-default: + $(PIP_ENSURE) + python -m pip install pipdeptree + python -m pipdeptree + pipdeptree + +.PHONY: pip-freeze-default +pip-freeze-default: + $(PIP_ENSURE) + python -m pip freeze | sort > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + -$(GIT_ADD) requirements.txt + +.PHONY: pip-init-default +pip-init-default: + touch requirements.txt + -$(GIT_ADD) requirements.txt + +.PHONY: pip-init-test-default +pip-init-test-default: + @echo "$$PIP_INSTALL_REQUIREMENTS_TEST" > requirements-test.txt + -$(GIT_ADD) requirements-test.txt + +.PHONY: pip-install-default +pip-install-default: + $(PIP_ENSURE) + $(MAKE) pip-upgrade + python -m pip install wheel + python -m pip install -r requirements.txt + +.PHONY: pip-install-dev-default +pip-install-dev-default: + $(PIP_ENSURE) + python -m pip install -r requirements-dev.txt + +.PHONY: pip-install-test-default +pip-install-test-default: + $(PIP_ENSURE) + python -m pip install -r requirements-test.txt + +.PHONY: pip-install-upgrade-default +pip-install-upgrade-default: + cat requirements.txt | awk -F\= '{print $$1}' > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + $(PIP_ENSURE) + python -m pip install -U -r requirements.txt + python -m pip freeze | sort > $(TMPDIR)/requirements.txt + mv -f $(TMPDIR)/requirements.txt . + +.PHONY: pip-upgrade-default +pip-upgrade-default: + $(PIP_ENSURE) + python -m pip install -U pip + +.PHONY: pip-uninstall-default +pip-uninstall-default: + $(PIP_ENSURE) + python -m pip freeze | xargs python -m pip uninstall -y + +.PHONY: plone-clean-default +plone-clean-default: + $(DEL_DIR) $(PROJECT_NAME) + $(DEL_DIR) $(PACKAGE_NAME) + +.PHONY: plone-init-default +plone-init-default: git-ignore plone-install plone-instance plone-serve + +.PHONY: plone-install-default +plone-install-default: + $(PIP_ENSURE) + python -m pip install plone -c $(PIP_INSTALL_PLONE_CONSTRAINTS) + +.PHONY: plone-instance-default +plone-instance-default: + mkwsgiinstance -d backend -u admin:admin + cat backend/etc/zope.ini | sed -e 's/host = 127.0.0.1/host = 0.0.0.0/; s/port = 8080/port = 8000/' > $(TMPDIR)/zope.ini + mv -f $(TMPDIR)/zope.ini backend/etc/zope.ini + -$(GIT_ADD) backend/etc/site.zcml + -$(GIT_ADD) backend/etc/zope.conf + -$(GIT_ADD) backend/etc/zope.ini + +.PHONY: plone-serve-default +plone-serve-default: + runwsgi backend/etc/zope.ini + +.PHONY: plone-build-default +plone-build-default: + buildout + +.PHONY: programming-interview-default +programming-interview-default: + @echo "$$PROGRAMMING_INTERVIEW" > interview.py + @echo "Created interview.py!" + -@$(GIT_ADD) interview.py > /dev/null 2>&1 + +# .NOT_PHONY! +$(MAKEFILE_CUSTOM_FILE): + @echo "$$MAKEFILE_CUSTOM" > $(MAKEFILE_CUSTOM_FILE) + -$(GIT_ADD) $(MAKEFILE_CUSTOM_FILE) + +.PHONY: python-license-default +python-license-default: + @echo "$(PYTHON_LICENSE_TXT)" > LICENSE.txt + -$(GIT_ADD) LICENSE.txt + +.PHONY: python-project-default +python-project-default: + @echo "$(PYTHON_PROJECT_TOML)" > pyproject.toml + -$(GIT_ADD) pyproject.toml + +.PHONY: python-serve-default +python-serve-default: + @echo "\n\tServing HTTP on http://0.0.0.0:8000\n" + python3 -m http.server + +.PHONY: python-sdist-default +python-sdist-default: + $(PIP_ENSURE) + python setup.py sdist --format=zip + +.PHONY: python-webpack-init-default +python-webpack-init-default: + python manage.py webpack_init --no-input + +.PHONY: python-ci-default +python-ci-default: + $(ADD_DIR) .github/workflows + @echo "$(PYTHON_CI_YAML)" > .github/workflows/build_wheels.yml + -$(GIT_ADD) .github/workflows/build_wheels.yml + +.PHONY: rand-default +rand-default: + @openssl rand -base64 12 | sed 's/\///g' + +.PHONY: readme-init-default +readme-init-default: + @echo "# $(PROJECT_NAME)" > README.md + -$(GIT_ADD) README.md + +.PHONY: readme-edit-default +readme-edit-default: + $(EDITOR) README.md + +.PHONY: reveal-init-default +reveal-init-default: webpack-init-reveal + npm install \ + css-loader \ + mini-css-extract-plugin \ + reveal.js \ + style-loader + jq '.scripts += {"build": "webpack"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + jq '.scripts += {"start": "webpack serve --mode development --port 8000 --static"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + jq '.scripts += {"watch": "webpack watch --mode development"}' package.json > \ + $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json + +.PHONY: reveal-serve-default +reveal-serve-default: + npm run watch & + python -m http.server + +.PHONY: review-default +review-default: +ifeq ($(UNAME), Darwin) + $(EDITOR_REVIEW) `find backend/ -name \*.py` `find backend/ -name \*.html` `find frontend/ -name \*.js` `find frontend/ -name \*.js` +else + @echo "Unsupported" +endif + +.PHONY: separator-default +separator-default: + @echo "$$SEPARATOR" + +.PHONY: sphinx-init-default +sphinx-init-default: sphinx-install + sphinx-quickstart -q -p $(PROJECT_NAME) -a $(USER) -v 0.0.1 $(RANDIR) + $(COPY_DIR) $(RANDIR)/* . + $(DEL_DIR) $(RANDIR) + -$(GIT_ADD) index.rst + -$(GIT_ADD) conf.py + $(DEL_FILE) make.bat + -@$(GIT_CHECKOUT) Makefile + $(MAKE) git-ignore + +.PHONY: sphinx-theme-init-default +sphinx-theme-init-default: + export DJANGO_FRONTEND_THEME_NAME=$(PROJECT_NAME)_theme; \ + $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ + $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/css ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/css/style.css ; \ + $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/js ; \ + $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/js/script.js ; \ + -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/static + +.PHONY: sphinx-install-default +sphinx-install-default: + echo "Sphinx\n" > requirements.txt + @$(MAKE) pip-install + @$(MAKE) pip-freeze + -$(GIT_ADD) requirements.txt + +.PHONY: sphinx-build-default +sphinx-build-default: + sphinx-build -b html -d _build/doctrees . _build/html + sphinx-build -b rinoh . _build/rinoh + +.PHONY: sphinx-serve-default +sphinx-serve-default: + cd _build/html;python3 -m http.server + +.PHONY: wagtail-base-template-default +wagtail-base-template-default: + @echo "$$WAGTAIL_BASE_TEMPLATE" > backend/templates/base.html + +.PHONY: wagtail-clean-default +wagtail-clean-default: + -@for dir in $(shell echo "$(WAGTAIL_CLEAN_DIRS)"); do \ + echo "Cleaning $$dir"; \ + $(DEL_DIR) $$dir >/dev/null 2>&1; \ + done + -@for file in $(shell echo "$(WAGTAIL_CLEAN_FILES)"); do \ + echo "Cleaning $$file"; \ + $(DEL_FILE) $$file >/dev/null 2>&1; \ + done + +.PHONY: wagtail-contactpage-default +wagtail-contactpage-default: + python manage.py startapp contactpage + @echo "$$WAGTAIL_CONTACT_PAGE_MODEL" > contactpage/models.py + @echo "$$WAGTAIL_CONTACT_PAGE_TEST" > contactpage/tests.py + $(ADD_DIR) contactpage/templates/contactpage/ + @echo "$$WAGTAIL_CONTACT_PAGE_TEMPLATE" > contactpage/templates/contactpage/contact_page.html + @echo "$$WAGTAIL_CONTACT_PAGE_LANDING" > contactpage/templates/contactpage/contact_page_landing.html + @echo "INSTALLED_APPS.append('contactpage')" >> $(DJANGO_SETTINGS_BASE_FILE) + python manage.py makemigrations contactpage + -$(GIT_ADD) contactpage/templates + -$(GIT_ADD) contactpage/*.py + -$(GIT_ADD) contactpage/migrations/*.py + +.PHONY: wagtail-header-prefix-template-default +wagtail-header-prefix-template-default: + @echo "$$WAGTAIL_HEADER_PREFIX" > backend/templates/header.html + +.PHONY: wagtail-home-default +wagtail-home-default: + @echo "$$WAGTAIL_HOME_PAGE_MODEL" > home/models.py + @echo "$$WAGTAIL_HOME_PAGE_TEMPLATE" > home/templates/home/home_page.html + $(ADD_DIR) home/templates/blocks + @echo "$$WAGTAIL_BLOCK_MARKETING" > home/templates/blocks/marketing_block.html + @echo "$$WAGTAIL_BLOCK_CAROUSEL" > home/templates/blocks/carousel_block.html + -$(GIT_ADD) home/templates + -$(GIT_ADD) home/*.py + python manage.py makemigrations home + -$(GIT_ADD) home/migrations/*.py + +.PHONY: wagtail-install-default +wagtail-install-default: + $(PIP_ENSURE) + python -m pip install \ + wagtail \ + wagtailmenus \ + wagtail-color-panel \ + wagtail-django-recaptcha \ + wagtail-markdown \ + wagtail-modeladmin \ + wagtail-seo \ + weasyprint \ + whitenoise \ + xhtml2pdf + +.PHONY: wagtail-private-default +wagtail-privacy-default: + python manage.py startapp privacy + @echo "$$WAGTAIL_PRIVACY_PAGE_MODEL" > privacy/models.py + $(ADD_DIR) privacy/templates + @echo "$$WAGTAIL_PRIVACY_PAGE_TEMPLATE" > privacy/templates/privacy_page.html + @echo "INSTALLED_APPS.append('privacy')" >> $(DJANGO_SETTINGS_BASE_FILE) + python manage.py makemigrations privacy + -$(GIT_ADD) privacy/templates + -$(GIT_ADD) privacy/*.py + -$(GIT_ADD) privacy/migrations/*.py + +.PHONY: wagtail-project-default +wagtail-project-default: + wagtail start backend . + $(DEL_FILE) home/templates/home/welcome_page.html + -$(GIT_ADD) backend/ + -$(GIT_ADD) .dockerignore + -$(GIT_ADD) Dockerfile + -$(GIT_ADD) manage.py + -$(GIT_ADD) requirements.txt + +.PHONY: wagtail-search-default +wagtail-search-default: + @echo "$$WAGTAIL_SEARCH_TEMPLATE" > search/templates/search/search.html + @echo "$$WAGTAIL_SEARCH_URLS" > search/urls.py + -$(GIT_ADD) search/templates + -$(GIT_ADD) search/*.py + +.PHONY: wagtail-settings-default +wagtail-settings-default: + @echo "$$WAGTAIL_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) + +.PHONY: wagtail-sitepage-default +wagtail-sitepage-default: + python manage.py startapp sitepage + @echo "$$WAGTAIL_SITEPAGE_MODEL" > sitepage/models.py + $(ADD_DIR) sitepage/templates/sitepage/ + @echo "$$WAGTAIL_SITEPAGE_TEMPLATE" > sitepage/templates/sitepage/site_page.html + @echo "INSTALLED_APPS.append('sitepage')" >> $(DJANGO_SETTINGS_BASE_FILE) + python manage.py makemigrations sitepage + -$(GIT_ADD) sitepage/templates + -$(GIT_ADD) sitepage/*.py + -$(GIT_ADD) sitepage/migrations/*.py + +.PHONY: wagtail-urls-default +wagtail-urls-default: + @echo "$$WAGTAIL_URLS" > $(DJANGO_URLS_FILE) + +.PHONY: wagtail-urls-home-default +wagtail-urls-home-default: + @echo "$$WAGTAIL_URLS_HOME" >> $(DJANGO_URLS_FILE) + +.PHONY: webpack-init-default +webpack-init-default: npm-init + @echo "$$WEBPACK_CONFIG_JS" > webpack.config.js + -$(GIT_ADD) webpack.config.js + npm install --save-dev webpack webpack-cli webpack-dev-server + $(ADD_DIR) src/ + @echo "$$WEBPACK_INDEX_JS" > src/index.js + -$(GIT_ADD) src/index.js + @echo "$$WEBPACK_INDEX_HTML" > index.html + -$(GIT_ADD) index.html + $(MAKE) git-ignore + +.PHONY: webpack-init-reveal-default +webpack-init-reveal-default: npm-init + @echo "$$WEBPACK_REVEAL_CONFIG_JS" > webpack.config.js + -$(GIT_ADD) webpack.config.js + npm install --save-dev webpack webpack-cli webpack-dev-server + $(ADD_DIR) src/ + @echo "$$WEBPACK_REVEAL_INDEX_JS" > src/index.js + -$(GIT_ADD) src/index.js + @echo "$$WEBPACK_REVEAL_INDEX_HTML" > index.html + -$(GIT_ADD) index.html + $(MAKE) git-ignore + +# -------------------------------------------------------------------------------- +# Single-line phony target rules +# -------------------------------------------------------------------------------- + +.PHONY: aws-check-env-default +aws-check-env-default: aws-check-env-profile aws-check-env-region + +.PHONY: ce-default +ce-default: git-commit-edit git-push + +.PHONY: clean-default +clean-default: wagtail-clean + +.PHONY: cp-default +cp-default: git-commit-message git-push + +.PHONY: db-dump-default +db-dump-default: eb-export + +.PHONY: dbshell-default +dbshell-default: django-db-shell + +.PHONY: deploy-default +deploy-default: eb-deploy + +.PHONY: d-default +d-default: eb-deploy + +.PHONY: deps-default +deps-default: pip-deps + +.PHONY: e-default +e-default: edit + +.PHONY: edit-default +edit-default: readme-edit + +.PHONY: empty-default +empty-default: git-commit-message-empty git-push + +.PHONY: fp-default +fp-default: git-push-force + +.PHONY: freeze-default +freeze-default: pip-freeze git-push + +.PHONY: git-commit-default +git-commit-default: git-commit-message git-push + +.PHONY: git-commit-clean-default +git-commit-clean-default: git-commit-message-clean git-push + +.PHONY: git-commit-init-default +git-commit-init-default: git-commit-message-init git-push + +.PHONY: git-commit-lint-default +git-commit-lint-default: git-commit-message-lint git-push + +.PHONY: gitignore-default +gitignore-default: git-ignore + +.PHONY: h-default +h-default: help + +.PHONY: init-default +init-default: django-init-wagtail django-serve + +.PHONY: init-wagtail-default +init-wagtail-default: django-init-wagtail + +.PHONY: install-default +install-default: pip-install + +.PHONY: l-default +l-default: makefile-list-commands + +.PHONY: last-default +last-default: git-commit-message-last git-push + +.PHONY: lint-default +lint-default: django-lint + +.PHONY: list-commands-default +list-commands-default: makefile-list-commands + +.PHONY: list-defines-default +list-defines-default: makefile-list-defines + +.PHONY: list-exports-default +list-exports-default: makefile-list-exports + +.PHONY: list-targets-default +list-targets-default: makefile-list-targets + +.PHONY: migrate-default +migrate-default: django-migrate + +.PHONY: migrations-default +migrations-default: django-migrations-make + +.PHONY: migrations-show-default +migrations-show-default: django-migrations-show + +.PHONY: mk-default +mk-default: project.mk git-commit-message-mk git-push + +.PHONY: open-default +open-default: django-open + +.PHONY: o-default +o-default: django-open + +.PHONY: readme-default +readme-default: readme-init + +.PHONY: rename-default +rename-default: git-commit-message-rename git-push + +.PHONY: s-default +s-default: django-serve + +.PHONY: shell-default +shell-default: django-shell + +.PHONY: serve-default +serve-default: django-serve + +.PHONY: static-default +static-default: django-static + +.PHONY: sort-default +sort-default: git-commit-message-sort git-push + +.PHONY: su-default +su-default: django-su + +.PHONY: test-default +test-default: django-test + +.PHONY: t-default +t-default: django-test + +.PHONY: u-default +u-default: help + +.PHONY: urls-default +urls-default: django-urls-show + +# -------------------------------------------------------------------------------- +# Allow customizing rules defined in this Makefile with rules defined in +# $(MAKEFILE_CUSTOM_FILE) +# -------------------------------------------------------------------------------- + +%: %-default # https://stackoverflow.com/a/49804748 + @ true From 776eb9e9b7b4406767a45523cc833f7fbc20100b Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 16:08:26 -0400 Subject: [PATCH 35/52] Add/update project-makefile files --- Makefile | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 4200f95da..b5fbff2c9 100644 --- a/Makefile +++ b/Makefile @@ -4521,21 +4521,21 @@ db-dump-default: eb-export .PHONY: dbshell-default dbshell-default: django-db-shell +.PHONY: d-default +d-default: deploy + .PHONY: deploy-default deploy-default: eb-deploy -.PHONY: d-default -d-default: eb-deploy - .PHONY: deps-default deps-default: pip-deps -.PHONY: e-default -e-default: edit - .PHONY: edit-default edit-default: readme-edit +.PHONY: e-default +e-default: edit + .PHONY: empty-default empty-default: git-commit-message-empty git-push @@ -4605,12 +4605,12 @@ migrations-show-default: django-migrations-show .PHONY: mk-default mk-default: project.mk git-commit-message-mk git-push -.PHONY: open-default -open-default: django-open - .PHONY: o-default o-default: django-open +.PHONY: open-default +open-default: open + .PHONY: readme-default readme-default: readme-init @@ -4618,14 +4618,14 @@ readme-default: readme-init rename-default: git-commit-message-rename git-push .PHONY: s-default -s-default: django-serve - -.PHONY: shell-default -shell-default: django-shell +s-default: serve .PHONY: serve-default serve-default: django-serve +.PHONY: shell-default +shell-default: django-shell + .PHONY: static-default static-default: django-static @@ -4639,7 +4639,7 @@ su-default: django-su test-default: django-test .PHONY: t-default -t-default: django-test +t-default: test .PHONY: u-default u-default: help From 837af6420d40fe772bcd910d9c531c2ca8fe980e Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 16:43:28 -0400 Subject: [PATCH 36/52] Add/update project-makefile files --- Makefile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Makefile b/Makefile index b5fbff2c9..d8e621c6c 100644 --- a/Makefile +++ b/Makefile @@ -3980,6 +3980,10 @@ git-commit-message-default: git-commit-message-empty-default: -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" +.PHONY: git-commit-message-ignore-default +git-commit-message-ignore-default: + -@$(GIT_COMMIT) -a -m "Ignore" + .PHONY: git-commit-message-init-default git-commit-message-init-default: -@$(GIT_COMMIT) -a -m "Init" @@ -4225,6 +4229,7 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini + -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: @@ -4530,6 +4535,9 @@ deploy-default: eb-deploy .PHONY: deps-default deps-default: pip-deps +.PHONY: dump-default +dump-default: db-dump + .PHONY: edit-default edit-default: readme-edit @@ -4551,6 +4559,9 @@ git-commit-default: git-commit-message git-push .PHONY: git-commit-clean-default git-commit-clean-default: git-commit-message-clean git-push +.PHONY: git-commit-ignore-default +git-commit-ignore-default: git-commit-message-ignore git-push + .PHONY: git-commit-init-default git-commit-init-default: git-commit-message-init git-push @@ -4563,6 +4574,9 @@ gitignore-default: git-ignore .PHONY: h-default h-default: help +.PHONY: ignore-default +ignore-default: git-commit-message-ignore git-push + .PHONY: init-default init-default: django-init-wagtail django-serve From 575fad24726fe25d06ea985ca59eb9126e26a00b Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 17:02:53 -0400 Subject: [PATCH 37/52] Add/update project-makefile files --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d8e621c6c..a16a18055 100644 --- a/Makefile +++ b/Makefile @@ -1996,6 +1996,7 @@ _build/ .elasticbeanstalk/ db.sqlite3 static/ +backend/inituser backend/var endef @@ -4229,7 +4230,6 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini - -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: From 132663a881bfe2bfc5265e7e05de7113f06501a5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Aug 2024 07:36:52 +1000 Subject: [PATCH 38/52] Updated error message for invalid width or height --- Tests/test_file_webp.py | 8 ++++++++ src/_webp.c | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 6ccd489bb..ad08da364 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -157,6 +157,14 @@ class TestFileWebp: im.save(temp_file, method=0) assert str(e.value) == "encoding error 6" + @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") + def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: + temp_file = str(tmp_path / "temp.webp") + im = Image.new("L", (16384, 16384)) + with pytest.raises(ValueError) as e: + im.save(temp_file) + assert str(e.value) == "encoding error 5: Image size exceeds WebP limit" + def test_WebPEncode_with_invalid_args(self) -> None: """ Calling encoder functions with no arguments should result in an error. diff --git a/src/_webp.c b/src/_webp.c index d1943b3e0..0d2d6f023 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -672,7 +672,12 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { WebPPictureFree(&pic); if (!ok) { - PyErr_Format(PyExc_ValueError, "encoding error %d", (&pic)->error_code); + int error_code = (&pic)->error_code; + const char *message = ""; + if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { + message = ": Image size exceeds WebP limit"; + } + PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); return NULL; } output = writer.mem; From 0f64d08e64e2a1d8bedf280626006a95cbb2b086 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:16:02 -0400 Subject: [PATCH 39/52] Revert "Add/update project-makefile files" This reverts commit 575fad24726fe25d06ea985ca59eb9126e26a00b. Sorry for the noise (1/4)! --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a16a18055..d8e621c6c 100644 --- a/Makefile +++ b/Makefile @@ -1996,7 +1996,6 @@ _build/ .elasticbeanstalk/ db.sqlite3 static/ -backend/inituser backend/var endef @@ -4230,6 +4229,7 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini + -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: From e19841afa486c33673d9ff6cb8330761414f91de Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:16:35 -0400 Subject: [PATCH 40/52] Revert "Add/update project-makefile files" This reverts commit 837af6420d40fe772bcd910d9c531c2ca8fe980e. Sorry for the noise (2/4)! --- Makefile | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Makefile b/Makefile index d8e621c6c..b5fbff2c9 100644 --- a/Makefile +++ b/Makefile @@ -3980,10 +3980,6 @@ git-commit-message-default: git-commit-message-empty-default: -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" -.PHONY: git-commit-message-ignore-default -git-commit-message-ignore-default: - -@$(GIT_COMMIT) -a -m "Ignore" - .PHONY: git-commit-message-init-default git-commit-message-init-default: -@$(GIT_COMMIT) -a -m "Init" @@ -4229,7 +4225,6 @@ plone-instance-default: -$(GIT_ADD) backend/etc/site.zcml -$(GIT_ADD) backend/etc/zope.conf -$(GIT_ADD) backend/etc/zope.ini - -$(GIT_ADD) backend/inituser .PHONY: plone-serve-default plone-serve-default: @@ -4535,9 +4530,6 @@ deploy-default: eb-deploy .PHONY: deps-default deps-default: pip-deps -.PHONY: dump-default -dump-default: db-dump - .PHONY: edit-default edit-default: readme-edit @@ -4559,9 +4551,6 @@ git-commit-default: git-commit-message git-push .PHONY: git-commit-clean-default git-commit-clean-default: git-commit-message-clean git-push -.PHONY: git-commit-ignore-default -git-commit-ignore-default: git-commit-message-ignore git-push - .PHONY: git-commit-init-default git-commit-init-default: git-commit-message-init git-push @@ -4574,9 +4563,6 @@ gitignore-default: git-ignore .PHONY: h-default h-default: help -.PHONY: ignore-default -ignore-default: git-commit-message-ignore git-push - .PHONY: init-default init-default: django-init-wagtail django-serve From 1143bffe1a0a5fe6b7d58dd2c3660f91299cff8d Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:17:06 -0400 Subject: [PATCH 41/52] Revert "Add/update project-makefile files" This reverts commit 776eb9e9b7b4406767a45523cc833f7fbc20100b. Sorry for the noise (3/4)! --- Makefile | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index b5fbff2c9..4200f95da 100644 --- a/Makefile +++ b/Makefile @@ -4521,21 +4521,21 @@ db-dump-default: eb-export .PHONY: dbshell-default dbshell-default: django-db-shell -.PHONY: d-default -d-default: deploy - .PHONY: deploy-default deploy-default: eb-deploy +.PHONY: d-default +d-default: eb-deploy + .PHONY: deps-default deps-default: pip-deps -.PHONY: edit-default -edit-default: readme-edit - .PHONY: e-default e-default: edit +.PHONY: edit-default +edit-default: readme-edit + .PHONY: empty-default empty-default: git-commit-message-empty git-push @@ -4605,12 +4605,12 @@ migrations-show-default: django-migrations-show .PHONY: mk-default mk-default: project.mk git-commit-message-mk git-push +.PHONY: open-default +open-default: django-open + .PHONY: o-default o-default: django-open -.PHONY: open-default -open-default: open - .PHONY: readme-default readme-default: readme-init @@ -4618,14 +4618,14 @@ readme-default: readme-init rename-default: git-commit-message-rename git-push .PHONY: s-default -s-default: serve - -.PHONY: serve-default -serve-default: django-serve +s-default: django-serve .PHONY: shell-default shell-default: django-shell +.PHONY: serve-default +serve-default: django-serve + .PHONY: static-default static-default: django-static @@ -4639,7 +4639,7 @@ su-default: django-su test-default: django-test .PHONY: t-default -t-default: test +t-default: django-test .PHONY: u-default u-default: help From d6cfebd016db25549d05a9c5caa2ef3b53cff5c5 Mon Sep 17 00:00:00 2001 From: Jeffrey 'Alex' Clark Date: Wed, 21 Aug 2024 21:17:29 -0400 Subject: [PATCH 42/52] Revert "Add/update project-makefile files" This reverts commit 388fc78e36e60f45a9b5ff0cd3223e3b8bc2c307. Sorry for the noise (4/4)! --- Makefile | 4781 ++---------------------------------------------------- 1 file changed, 125 insertions(+), 4656 deletions(-) diff --git a/Makefile b/Makefile index 4200f95da..94f7565d8 100644 --- a/Makefile +++ b/Makefile @@ -1,4656 +1,125 @@ -# Project Makefile -# -# A makefile to automate setup of a Wagtail CMS project and related tasks. -# -# https://github.com/aclark4life/project-makefile -# -# -------------------------------------------------------------------------------- -# Set the default goal to be `git commit -a -m $(GIT_MESSAGE)` and `git push` -# -------------------------------------------------------------------------------- - -.DEFAULT_GOAL := git-commit - -# -------------------------------------------------------------------------------- -# Single line variables to be used by phony target rules -# -------------------------------------------------------------------------------- - -ADD_DIR := mkdir -pv -ADD_FILE := touch -AWS_OPTS := --no-cli-pager --output table -COPY_DIR := cp -rv -COPY_FILE := cp -v -DEL_DIR := rm -rv -DEL_FILE := rm -v -DJANGO_DB_COL = awk -F\= '{print $$2}' -DJANGO_DB_URL = eb ssh -c "source /opt/elasticbeanstalk/deployment/custom_env_var; env | grep DATABASE_URL" -DJANGO_DB_HOST = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["HOST"])') -DJANGO_DB_NAME = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["NAME"])') -DJANGO_DB_PASS = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["PASSWORD"])') -DJANGO_DB_USER = $(shell $(DJANGO_DB_URL) | $(DJANGO_DB_COL) |\ - python -c 'import dj_database_url; url = input(); url = dj_database_url.parse(url); print(url["USER"])') -DJANGO_BACKEND_APPS_FILE := backend/apps.py -DJANGO_CUSTOM_ADMIN_FILE := backend/admin.py -DJANGO_FRONTEND_FILES = .babelrc .browserslistrc .eslintrc .nvmrc .stylelintrc.json frontend package-lock.json \ - package.json postcss.config.js -DJANGO_SETTINGS_DIR = backend/settings -DJANGO_SETTINGS_BASE_FILE = $(DJANGO_SETTINGS_DIR)/base.py -DJANGO_SETTINGS_DEV_FILE = $(DJANGO_SETTINGS_DIR)/dev.py -DJANGO_SETTINGS_PROD_FILE = $(DJANGO_SETTINGS_DIR)/production.py -DJANGO_SETTINGS_SECRET_KEY = $(shell openssl rand -base64 48) -DJANGO_URLS_FILE = backend/urls.py -EB_DIR_NAME := .elasticbeanstalk -EB_ENV_NAME ?= $(PROJECT_NAME)-$(GIT_BRANCH)-$(GIT_REV) -EB_PLATFORM ?= "Python 3.11 running on 64bit Amazon Linux 2023" -EC2_INSTANCE_MAX ?= 1 -EC2_INSTANCE_MIN ?= 1 -EC2_INSTANCE_PROFILE ?= aws-elasticbeanstalk-ec2-role -EC2_INSTANCE_TYPE ?= t4g.small -EC2_LB_TYPE ?= application -EDITOR_REVIEW = subl -GIT_ADD := git add -GIT_BRANCH = $(shell git branch --show-current) -GIT_BRANCHES = $(shell git branch -a) -GIT_CHECKOUT = git checkout -GIT_COMMIT_MSG = "Update $(PROJECT_NAME)" -GIT_COMMIT = git commit -GIT_PUSH = git push -GIT_PUSH_FORCE = git push --force-with-lease -GIT_REV = $(shell git rev-parse --short HEAD) -MAKEFILE_CUSTOM_FILE := project.mk -PACKAGE_NAME = $(shell echo $(PROJECT_NAME) | sed 's/-/_/g') -PAGER ?= less -PIP_ENSURE = python -m ensurepip -PIP_INSTALL_PLONE_CONSTRAINTS = https://dist.plone.org/release/6.0.11.1/constraints.txt -PROJECT_DIRS = backend contactpage home privacy siteuser -PROJECT_EMAIL := aclark@aclark.net -PROJECT_NAME = project-makefile -RANDIR := $(shell openssl rand -base64 12 | sed 's/\///g') -TMPDIR := $(shell mktemp -d) -UNAME := $(shell uname) -WAGTAIL_CLEAN_DIRS = backend contactpage dist frontend home logging_demo model_form_demo node_modules payments privacy search sitepage siteuser -WAGTAIL_CLEAN_FILES = .babelrc .browserslistrc .dockerignore .eslintrc .gitignore .nvmrc .stylelintrc.json Dockerfile db.sqlite3 docker-compose.yml manage.py package-lock.json package.json postcss.config.js requirements-test.txt requirements.txt - -# -------------------------------------------------------------------------------- -# Include $(MAKEFILE_CUSTOM_FILE) if it exists -# -------------------------------------------------------------------------------- - -ifneq ($(wildcard $(MAKEFILE_CUSTOM_FILE)),) - include $(MAKEFILE_CUSTOM_FILE) -endif - -# -------------------------------------------------------------------------------- -# Multi-line variables to be used in phony target rules -# -------------------------------------------------------------------------------- - -define DJANGO_ALLAUTH_BASE_TEMPLATE -{% extends 'base.html' %} -endef - -define DJANGO_API_SERIALIZERS -from rest_framework import serializers -from siteuser.models import User - - -class UserSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = User - fields = ["url", "username", "email", "is_staff"] -endef - -define DJANGO_API_VIEWS -from ninja import NinjaAPI -from rest_framework import viewsets -from siteuser.models import User -from .serializers import UserSerializer - -api = NinjaAPI() - - -@api.get("/hello") -def hello(request): - return "Hello world" - - -class UserViewSet(viewsets.ModelViewSet): - queryset = User.objects.all() - serializer_class = UserSerializer -endef - -define DJANGO_APP_TESTS -from django.test import TestCase -from django.urls import reverse -from .models import YourModel -from .forms import YourForm - - -class YourModelTest(TestCase): - def setUp(self): - self.instance = YourModel.objects.create(field1="value1", field2="value2") - - def test_instance_creation(self): - self.assertIsInstance(self.instance, YourModel) - self.assertEqual(self.instance.field1, "value1") - self.assertEqual(self.instance.field2, "value2") - - def test_str_method(self): - self.assertEqual(str(self.instance), "Expected String Representation") - - -class YourViewTest(TestCase): - def setUp(self): - self.instance = YourModel.objects.create(field1="value1", field2="value2") - - def test_view_url_exists_at_desired_location(self): - response = self.client.get("/your-url/") - self.assertEqual(response.status_code, 200) - - def test_view_url_accessible_by_name(self): - response = self.client.get(reverse("your-view-name")) - self.assertEqual(response.status_code, 200) - - def test_view_uses_correct_template(self): - response = self.client.get(reverse("your-view-name")) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, "your_template.html") - - def test_view_context(self): - response = self.client.get(reverse("your-view-name")) - self.assertEqual(response.status_code, 200) - self.assertIn("context_variable", response.context) - - -class YourFormTest(TestCase): - def test_form_valid_data(self): - form = YourForm(data={"field1": "value1", "field2": "value2"}) - self.assertTrue(form.is_valid()) - - def test_form_invalid_data(self): - form = YourForm(data={"field1": "", "field2": "value2"}) - self.assertFalse(form.is_valid()) - self.assertIn("field1", form.errors) - - def test_form_save(self): - form = YourForm(data={"field1": "value1", "field2": "value2"}) - if form.is_valid(): - instance = form.save() - self.assertEqual(instance.field1, "value1") - self.assertEqual(instance.field2, "value2") -endef - -define DJANGO_BACKEND_APPS -from django.contrib.admin.apps import AdminConfig - - -class CustomAdminConfig(AdminConfig): - default_site = "backend.admin.CustomAdminSite" -endef - -define DJANGO_BASE_TEMPLATE -{% load static webpack_loader %} - - - - - - {% block title %}{% endblock %} - {% block title_suffix %}{% endblock %} - - - {% stylesheet_pack 'app' %} - {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} - - {% include 'favicon.html' %} - {% csrf_token %} - - -
    -
    - {% include 'header.html' %} - {% if messages %} -
    - {% for message in messages %} - - {% endfor %} -
    - {% endif %} -
    - {% block content %}{% endblock %} -
    -
    - {% include 'footer.html' %} - {% include 'offcanvas.html' %} - {% javascript_pack 'app' %} - {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} - - -endef - -define DJANGO_CUSTOM_ADMIN -from django.contrib.admin import AdminSite - - -class CustomAdminSite(AdminSite): - site_header = "Project Makefile" - site_title = "Project Makefile" - index_title = "Project Makefile" - - -custom_admin_site = CustomAdminSite(name="custom_admin") -endef - -define DJANGO_DOCKERCOMPOSE -version: '3' - -services: - db: - image: postgres:latest - volumes: - - postgres_data:/var/lib/postgresql/data - environment: - POSTGRES_DB: project - POSTGRES_USER: admin - POSTGRES_PASSWORD: admin - - web: - build: . - command: sh -c "python manage.py migrate && gunicorn project.wsgi:application -b 0.0.0.0:8000" - volumes: - - .:/app - ports: - - "8000:8000" - depends_on: - - db - environment: - DATABASE_URL: postgres://admin:admin@db:5432/project - -volumes: - postgres_data: -endef - -define DJANGO_DOCKERFILE -FROM amazonlinux:2023 -RUN dnf install -y shadow-utils python3.11 python3.11-pip make nodejs20-npm nodejs postgresql15 postgresql15-server -USER postgres -RUN initdb -D /var/lib/pgsql/data -USER root -RUN useradd wagtail -EXPOSE 8000 -ENV PYTHONUNBUFFERED=1 PORT=8000 -COPY requirements.txt / -RUN python3.11 -m pip install -r /requirements.txt -WORKDIR /app -RUN chown wagtail:wagtail /app -COPY --chown=wagtail:wagtail . . -USER wagtail -RUN npm-20 install; npm-20 run build -RUN python3.11 manage.py collectstatic --noinput --clear -CMD set -xe; pg_ctl -D /var/lib/pgsql/data -l /tmp/logfile start; python3.11 manage.py migrate --noinput; gunicorn backend.wsgi:application -endef - -define DJANGO_FAVICON_TEMPLATE -{% load static %} - -endef - -define DJANGO_FOOTER_TEMPLATE -
    -

    © {% now "Y" %} {{ current_site.site_name|default:"Project Makefile" }}

    -
      -
    • - Home -
    • - {% for child in current_site.root_page.get_children %} -
    • - {{ child }} -
    • - {% endfor %} -
    -
    -endef - -define DJANGO_FRONTEND_APP -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import 'bootstrap'; -import '@fortawesome/fontawesome-free/js/fontawesome'; -import '@fortawesome/fontawesome-free/js/solid'; -import '@fortawesome/fontawesome-free/js/regular'; -import '@fortawesome/fontawesome-free/js/brands'; -import getDataComponents from '../dataComponents'; -import UserContextProvider from '../context'; -import * as components from '../components'; -import "../styles/index.scss"; -import "../styles/theme-blue.scss"; -import "./config"; - -const { ErrorBoundary } = components; -const dataComponents = getDataComponents(components); -const container = document.getElementById('app'); -const root = createRoot(container); -const App = () => ( - - - {dataComponents} - - -); -root.render(); -endef - -define DJANGO_FRONTEND_APP_CONFIG -import '../utils/themeToggler.js'; -// import '../utils/tinymce.js'; -endef - -define DJANGO_FRONTEND_BABELRC -{ - "presets": [ - [ - "@babel/preset-react", - ], - [ - "@babel/preset-env", - { - "useBuiltIns": "usage", - "corejs": "3.0.0" - } - ] - ], - "plugins": [ - "@babel/plugin-syntax-dynamic-import", - "@babel/plugin-transform-class-properties" - ] -} -endef - -define DJANGO_FRONTEND_COMPONENTS -export { default as ErrorBoundary } from './ErrorBoundary'; -export { default as UserMenu } from './UserMenu'; -endef - -define DJANGO_FRONTEND_COMPONENT_CLOCK -// Via ChatGPT -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import PropTypes from 'prop-types'; - -const Clock = ({ color = '#fff' }) => { - const [date, setDate] = useState(new Date()); - const [blink, setBlink] = useState(true); - const timerID = useRef(); - - const tick = useCallback(() => { - setDate(new Date()); - setBlink(prevBlink => !prevBlink); - }, []); - - useEffect(() => { - timerID.current = setInterval(() => tick(), 1000); - - // Return a cleanup function to be run on component unmount - return () => clearInterval(timerID.current); - }, [tick]); - - const formattedDate = date.toLocaleDateString(undefined, { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - }); - - const formattedTime = date.toLocaleTimeString(undefined, { - hour: 'numeric', - minute: 'numeric', - }); - - return ( - <> -
    {formattedDate} {formattedTime}
    - - ); -}; - -Clock.propTypes = { - color: PropTypes.string, -}; - -export default Clock; -endef - -define DJANGO_FRONTEND_COMPONENT_ERROR -import { Component } from 'react'; -import PropTypes from 'prop-types'; - -class ErrorBoundary extends Component { - constructor (props) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError () { - return { hasError: true }; - } - - componentDidCatch (error, info) { - const { onError } = this.props; - console.error(error); - onError && onError(error, info); - } - - render () { - const { children = null } = this.props; - const { hasError } = this.state; - - return hasError ? null : children; - } -} - -ErrorBoundary.propTypes = { - onError: PropTypes.func, - children: PropTypes.node, -}; - -export default ErrorBoundary; -endef - -define DJANGO_FRONTEND_COMPONENT_USER_MENU -// UserMenu.js -import React from 'react'; -import PropTypes from 'prop-types'; - -function handleLogout() { - window.location.href = '/accounts/logout'; -} - -const UserMenu = ({ isAuthenticated, isSuperuser, textColor }) => { - return ( -
    - {isAuthenticated ? ( -
  • - - -
  • - ) : ( -
  • - -
  • - )} -
    - ); -}; - -UserMenu.propTypes = { - isAuthenticated: PropTypes.bool.isRequired, - isSuperuser: PropTypes.bool.isRequired, - textColor: PropTypes.string, -}; - -export default UserMenu; -endef - -define DJANGO_FRONTEND_CONTEXT_INDEX -export { UserContextProvider as default } from './UserContextProvider'; -endef - -define DJANGO_FRONTEND_CONTEXT_USER_PROVIDER -// UserContextProvider.js -import React, { createContext, useContext, useState } from 'react'; -import PropTypes from 'prop-types'; - -const UserContext = createContext(); - -export const UserContextProvider = ({ children }) => { - const [isAuthenticated, setIsAuthenticated] = useState(false); - - const login = () => { - try { - // Add logic to handle login, set isAuthenticated to true - setIsAuthenticated(true); - } catch (error) { - console.error('Login error:', error); - // Handle error, e.g., show an error message to the user - } - }; - - const logout = () => { - try { - // Add logic to handle logout, set isAuthenticated to false - setIsAuthenticated(false); - } catch (error) { - console.error('Logout error:', error); - // Handle error, e.g., show an error message to the user - } - }; - - return ( - - {children} - - ); -}; - -UserContextProvider.propTypes = { - children: PropTypes.node.isRequired, -}; - -export const useUserContext = () => { - const context = useContext(UserContext); - - if (!context) { - throw new Error('useUserContext must be used within a UserContextProvider'); - } - - return context; -}; - -// Add PropTypes for the return value of useUserContext -useUserContext.propTypes = { - isAuthenticated: PropTypes.bool.isRequired, - login: PropTypes.func.isRequired, - logout: PropTypes.func.isRequired, -}; -endef - -define DJANGO_FRONTEND_ESLINTRC -{ - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "overrides": [ - { - "env": { - "node": true - }, - "files": [ - ".eslintrc.{js,cjs}" - ], - "parserOptions": { - "sourceType": "script" - } - } - ], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "react" - ], - "rules": { - "no-unused-vars": "off" - }, - settings: { - react: { - version: 'detect', - }, - }, -} -endef - -define DJANGO_FRONTEND_OFFCANVAS_TEMPLATE -
    - -
    - -
    -
    -endef - -define DJANGO_FRONTEND_PORTAL -// Via pwellever -import React from 'react'; -import { createPortal } from 'react-dom'; - -const parseProps = data => Object.entries(data).reduce((result, [key, value]) => { - if (value.toLowerCase() === 'true') { - value = true; - } else if (value.toLowerCase() === 'false') { - value = false; - } else if (value.toLowerCase() === 'null') { - value = null; - } else if (!isNaN(parseFloat(value)) && isFinite(value)) { - // Parse numeric value - value = parseFloat(value); - } else if ( - (value[0] === '[' && value.slice(-1) === ']') || (value[0] === '{' && value.slice(-1) === '}') - ) { - // Parse JSON strings - value = JSON.parse(value); - } - - result[key] = value; - return result; -}, {}); - -// This method of using portals instead of calling ReactDOM.render on individual components -// ensures that all components are mounted under a single React tree, and are therefore able -// to share context. - -export default function getPageComponents (components) { - const getPortalComponent = domEl => { - // The element's "data-component" attribute is used to determine which component to render. - // All other "data-*" attributes are passed as props. - const { component: componentName, ...rest } = domEl.dataset; - const Component = components[componentName]; - if (!Component) { - console.error(`Component "$${componentName}" not found.`); - return null; - } - const props = parseProps(rest); - domEl.innerHTML = ''; - - // eslint-disable-next-line no-unused-vars - const { ErrorBoundary } = components; - return createPortal( - - - , - domEl, - ); - }; - - return Array.from(document.querySelectorAll('[data-component]')).map(getPortalComponent); -} -endef - -define DJANGO_FRONTEND_STYLES -// If you comment out code below, bootstrap will use red as primary color -// and btn-primary will become red - -// $primary: red; - -@import "~bootstrap/scss/bootstrap.scss"; - -.jumbotron { - // should be relative path of the entry scss file - background-image: url("../../vendors/images/sample.jpg"); - background-size: cover; -} - -#theme-toggler-authenticated:hover { - cursor: pointer; /* Change cursor to pointer on hover */ - color: #007bff; /* Change color on hover */ -} - -#theme-toggler-anonymous:hover { - cursor: pointer; /* Change cursor to pointer on hover */ - color: #007bff; /* Change color on hover */ -} -endef - -define DJANGO_FRONTEND_THEME_BLUE -@import "~bootstrap/scss/bootstrap.scss"; - -[data-bs-theme="blue"] { - --bs-body-color: var(--bs-white); - --bs-body-color-rgb: #{to-rgb($$white)}; - --bs-body-bg: var(--bs-blue); - --bs-body-bg-rgb: #{to-rgb($$blue)}; - --bs-tertiary-bg: #{$$blue-600}; - - .dropdown-menu { - --bs-dropdown-bg: #{color-mix($$blue-500, $$blue-600)}; - --bs-dropdown-link-active-bg: #{$$blue-700}; - } - - .btn-secondary { - --bs-btn-bg: #{color-mix($gray-600, $blue-400, .5)}; - --bs-btn-border-color: #{rgba($$white, .25)}; - --bs-btn-hover-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 5%)}; - --bs-btn-hover-border-color: #{rgba($$white, .25)}; - --bs-btn-active-bg: #{color-adjust(color-mix($gray-600, $blue-400, .5), 10%)}; - --bs-btn-active-border-color: #{rgba($$white, .5)}; - --bs-btn-focus-border-color: #{rgba($$white, .5)}; - - // --bs-btn-focus-box-shadow: 0 0 0 .25rem rgba(255, 255, 255, 20%); - } -} -endef - -define DJANGO_FRONTEND_THEME_TOGGLER -document.addEventListener('DOMContentLoaded', function () { - const rootElement = document.documentElement; - const anonThemeToggle = document.getElementById('theme-toggler-anonymous'); - const authThemeToggle = document.getElementById('theme-toggler-authenticated'); - if (authThemeToggle) { - localStorage.removeItem('data-bs-theme'); - } - const anonSavedTheme = localStorage.getItem('data-bs-theme'); - if (anonSavedTheme) { - rootElement.setAttribute('data-bs-theme', anonSavedTheme); - } - if (anonThemeToggle) { - anonThemeToggle.addEventListener('click', function () { - const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; - const newTheme = currentTheme === 'light' ? 'dark' : 'light'; - rootElement.setAttribute('data-bs-theme', newTheme); - localStorage.setItem('data-bs-theme', newTheme); - }); - } - if (authThemeToggle) { - const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; - authThemeToggle.addEventListener('click', function () { - const currentTheme = rootElement.getAttribute('data-bs-theme') || 'light'; - const newTheme = currentTheme === 'light' ? 'dark' : 'light'; - fetch('/user/update_theme_preference/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': csrfToken, // Include the CSRF token in the headers - }, - body: JSON.stringify({ theme: newTheme }), - }) - .then(response => response.json()) - .then(data => { - rootElement.setAttribute('data-bs-theme', newTheme); - }) - .catch(error => { - console.error('Error updating theme preference:', error); - }); - }); - } -}); -endef - -define DJANGO_HEADER_TEMPLATE -
    -
    - -
    -
    -endef - -define DJANGO_HOME_PAGE_ADMIN -from django.contrib import admin # noqa - -# Register your models here. -endef - -define DJANGO_HOME_PAGE_MODELS -from django.db import models # noqa - -# Create your models here. -endef - -define DJANGO_HOME_PAGE_TEMPLATE -{% extends "base.html" %} -{% block content %} -
    -
    -{% endblock %} -endef - -define DJANGO_HOME_PAGE_URLS -from django.urls import path -from .views import HomeView - -urlpatterns = [path("", HomeView.as_view(), name="home")] -endef - -define DJANGO_HOME_PAGE_VIEWS -from django.views.generic import TemplateView - - -class HomeView(TemplateView): - template_name = "home.html" -endef - -define DJANGO_LOGGING_DEMO_ADMIN -# Register your models here. -endef - -define DJANGO_LOGGING_DEMO_MODELS -from django.db import models # noqa - -# Create your models here. -endef - -define DJANGO_LOGGING_DEMO_SETTINGS -INSTALLED_APPS.append("logging_demo") # noqa -endef - -define DJANGO_LOGGING_DEMO_URLS -from django.urls import path -from .views import logging_demo - -urlpatterns = [ - path("", logging_demo, name="logging_demo"), -] -endef - -define DJANGO_LOGGING_DEMO_VIEWS -from django.http import HttpResponse -import logging - -logger = logging.getLogger(__name__) - - -def logging_demo(request): - logger.debug("Hello, world!") - return HttpResponse("Hello, world!") -endef - -define DJANGO_MANAGE_PY -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" - -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.dev") - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() -endef - -define DJANGO_MODEL_FORM_DEMO_ADMIN -from django.contrib import admin -from .models import ModelFormDemo - - -@admin.register(ModelFormDemo) -class ModelFormDemoAdmin(admin.ModelAdmin): - pass -endef - -define DJANGO_MODEL_FORM_DEMO_FORMS -from django import forms -from .models import ModelFormDemo - - -class ModelFormDemoForm(forms.ModelForm): - class Meta: - model = ModelFormDemo - fields = ["name", "email", "age", "is_active"] -endef - -define DJANGO_MODEL_FORM_DEMO_MODEL -from django.db import models -from django.shortcuts import reverse - - -class ModelFormDemo(models.Model): - name = models.CharField(max_length=100, blank=True, null=True) - email = models.EmailField(blank=True, null=True) - age = models.IntegerField(blank=True, null=True) - is_active = models.BooleanField(default=True) - created_at = models.DateTimeField(auto_now_add=True) - - def __str__(self): - return self.name or f"test-model-{self.pk}" - - def get_absolute_url(self): - return reverse("model_form_demo_detail", kwargs={"pk": self.pk}) -endef - -define DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL -{% extends 'base.html' %} -{% block content %} -

    Test Model Detail: {{ model_form_demo.name }}

    -

    Name: {{ model_form_demo.name }}

    -

    Email: {{ model_form_demo.email }}

    -

    Age: {{ model_form_demo.age }}

    -

    Active: {{ model_form_demo.is_active }}

    -

    Created At: {{ model_form_demo.created_at }}

    - Edit Test Model -{% endblock %} -endef - -define DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM -{% extends 'base.html' %} -{% block content %} -

    - {% if form.instance.pk %} - Update Test Model - {% else %} - Create Test Model - {% endif %} -

    -
    - {% csrf_token %} - {{ form.as_p }} - -
    -{% endblock %} -endef - -define DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST -{% extends 'base.html' %} -{% block content %} -

    Test Models List

    - - Create New Test Model -{% endblock %} -endef - -define DJANGO_MODEL_FORM_DEMO_URLS -from django.urls import path -from .views import ( - ModelFormDemoListView, - ModelFormDemoCreateView, - ModelFormDemoUpdateView, - ModelFormDemoDetailView, -) - -urlpatterns = [ - path("", ModelFormDemoListView.as_view(), name="model_form_demo_list"), - path("create/", ModelFormDemoCreateView.as_view(), name="model_form_demo_create"), - path( - "/update/", - ModelFormDemoUpdateView.as_view(), - name="model_form_demo_update", - ), - path("/", ModelFormDemoDetailView.as_view(), name="model_form_demo_detail"), -] -endef - -define DJANGO_MODEL_FORM_DEMO_VIEWS -from django.views.generic import ListView, CreateView, UpdateView, DetailView -from .models import ModelFormDemo -from .forms import ModelFormDemoForm - - -class ModelFormDemoListView(ListView): - model = ModelFormDemo - template_name = "model_form_demo_list.html" - context_object_name = "model_form_demos" - - -class ModelFormDemoCreateView(CreateView): - model = ModelFormDemo - form_class = ModelFormDemoForm - template_name = "model_form_demo_form.html" - - def form_valid(self, form): - form.instance.created_by = self.request.user - return super().form_valid(form) - - -class ModelFormDemoUpdateView(UpdateView): - model = ModelFormDemo - form_class = ModelFormDemoForm - template_name = "model_form_demo_form.html" - - -class ModelFormDemoDetailView(DetailView): - model = ModelFormDemo - template_name = "model_form_demo_detail.html" - context_object_name = "model_form_demo" -endef - -define DJANGO_PAYMENTS_ADMIN -from django.contrib import admin -from .models import Product, Order - -admin.site.register(Product) -admin.site.register(Order) -endef - -define DJANGO_PAYMENTS_FORM -from django import forms - - -class PaymentsForm(forms.Form): - stripeToken = forms.CharField(widget=forms.HiddenInput()) - amount = forms.DecimalField( - max_digits=10, decimal_places=2, widget=forms.HiddenInput() - ) -endef - -define DJANGO_PAYMENTS_MIGRATION_0002 -from django.db import migrations -import os -import secrets -import logging - -logger = logging.getLogger(__name__) - - -def generate_default_key(): - return "sk_test_" + secrets.token_hex(24) - - -def set_stripe_api_keys(apps, schema_editor): - # Get the Stripe API Key model - APIKey = apps.get_model("djstripe", "APIKey") - - # Fetch the keys from environment variables or generate default keys - test_secret_key = os.environ.get("STRIPE_TEST_SECRET_KEY", generate_default_key()) - live_secret_key = os.environ.get("STRIPE_LIVE_SECRET_KEY", generate_default_key()) - - logger.info("STRIPE_TEST_SECRET_KEY: %s", test_secret_key) - logger.info("STRIPE_LIVE_SECRET_KEY: %s", live_secret_key) - - # Check if the keys are not already in the database - if not APIKey.objects.filter(secret=test_secret_key).exists(): - APIKey.objects.create(secret=test_secret_key, livemode=False) - logger.info("Added test secret key to the database.") - else: - logger.info("Test secret key already exists in the database.") - - if not APIKey.objects.filter(secret=live_secret_key).exists(): - APIKey.objects.create(secret=live_secret_key, livemode=True) - logger.info("Added live secret key to the database.") - else: - logger.info("Live secret key already exists in the database.") - - -class Migration(migrations.Migration): - - dependencies = [ - ("payments", "0001_initial"), - ] - - operations = [ - migrations.RunPython(set_stripe_api_keys), - ] -endef - -define DJANGO_PAYMENTS_MIGRATION_0003 -from django.db import migrations - - -def create_initial_products(apps, schema_editor): - Product = apps.get_model("payments", "Product") - Product.objects.create(name="T-shirt", description="A cool T-shirt", price=20.00) - Product.objects.create(name="Mug", description="A nice mug", price=10.00) - Product.objects.create(name="Hat", description="A stylish hat", price=15.00) - - -class Migration(migrations.Migration): - dependencies = [ - ( - "payments", - "0002_set_stripe_api_keys", - ), - ] - - operations = [ - migrations.RunPython(create_initial_products), - ] -endef - -define DJANGO_PAYMENTS_MODELS -from django.db import models - - -class Product(models.Model): - name = models.CharField(max_length=100) - description = models.TextField() - price = models.DecimalField(max_digits=10, decimal_places=2) - - def __str__(self): - return self.name - - -class Order(models.Model): - product = models.ForeignKey(Product, on_delete=models.CASCADE) - created_at = models.DateTimeField(auto_now_add=True) - stripe_checkout_session_id = models.CharField(max_length=200) - - def __str__(self): - return f"Order {self.id} for {self.product.name}" -endef - -define DJANGO_PAYMENTS_TEMPLATE_CANCEL -{% extends "base.html" %} -{% block title %}Cancel{% endblock %} -{% block content %} -

    Payment Cancelled

    -

    Your payment was cancelled.

    -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_CHECKOUT -{% extends "base.html" %} -{% block title %}Checkout{% endblock %} -{% block content %} -

    Checkout

    -
    - {% csrf_token %} - -
    -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL -{% extends "base.html" %} -{% block title %}{{ product.name }}{% endblock %} -{% block content %} -

    {{ product.name }}

    -

    {{ product.description }}

    -

    Price: ${{ product.price }}

    -
    - {% csrf_token %} - - -
    -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST -{% extends "base.html" %} -{% block title %}Products{% endblock %} -{% block content %} -

    Products

    - -{% endblock %} -endef - -define DJANGO_PAYMENTS_TEMPLATE_SUCCESS -{% extends "base.html" %} -{% block title %}Success{% endblock %} -{% block content %} -

    Payment Successful

    -

    Thank you for your purchase!

    -{% endblock %} -endef - -define DJANGO_PAYMENTS_URLS -from django.urls import path -from .views import ( - CheckoutView, - SuccessView, - CancelView, - ProductListView, - ProductDetailView, -) - -urlpatterns = [ - path("", ProductListView.as_view(), name="product_list"), - path("product//", ProductDetailView.as_view(), name="product_detail"), - path("checkout/", CheckoutView.as_view(), name="checkout"), - path("success/", SuccessView.as_view(), name="success"), - path("cancel/", CancelView.as_view(), name="cancel"), -] -endef - -define DJANGO_PAYMENTS_VIEW -from django.conf import settings -from django.shortcuts import render, redirect, get_object_or_404 -from django.views.generic import TemplateView, View, ListView, DetailView -import stripe -from .models import Product, Order - -stripe.api_key = settings.STRIPE_TEST_SECRET_KEY - - -class ProductListView(ListView): - model = Product - template_name = "payments/product_list.html" - context_object_name = "products" - - -class ProductDetailView(DetailView): - model = Product - template_name = "payments/product_detail.html" - context_object_name = "product" - - -class CheckoutView(View): - template_name = "payments/checkout.html" - - def get(self, request, *args, **kwargs): - products = Product.objects.all() - return render(request, self.template_name, {"products": products}) - - def post(self, request, *args, **kwargs): - product_id = request.POST.get("product_id") - product = get_object_or_404(Product, id=product_id) - - session = stripe.checkout.Session.create( - payment_method_types=["card"], - line_items=[ - { - "price_data": { - "currency": "usd", - "product_data": { - "name": product.name, - }, - "unit_amount": int(product.price * 100), - }, - "quantity": 1, - } - ], - mode="payment", - success_url="http://localhost:8000/payments/success/", - cancel_url="http://localhost:8000/payments/cancel/", - ) - - Order.objects.create(product=product, stripe_checkout_session_id=session.id) - return redirect(session.url, code=303) - - -class SuccessView(TemplateView): - - template_name = "payments/success.html" - - -class CancelView(TemplateView): - - template_name = "payments/cancel.html" -endef - -define DJANGO_SEARCH_FORMS -from django import forms - - -class SearchForm(forms.Form): - query = forms.CharField(max_length=100, required=True, label="Search") - -endef - -define DJANGO_SEARCH_SETTINGS -SEARCH_MODELS = [ - # Add search models here. -] -endef - -define DJANGO_SEARCH_TEMPLATE -{% extends "base.html" %} -{% block body_class %}template-searchresults{% endblock %} -{% block title %}Search{% endblock %} -{% block content %} -

    Search

    -
    - - -
    - {% if search_results %} -
      - {% for result in search_results %} -
    • -

      - {{ result }} -

      - {% if result.search_description %}{{ result.search_description }}{% endif %} -
    • - {% endfor %} -
    - {% if search_results.has_previous %} - Previous - {% endif %} - {% if search_results.has_next %} - Next - {% endif %} - {% elif search_query %} - No results found - {% else %} - No results found. Try a test query? - {% endif %} -{% endblock %} -endef - -define DJANGO_SEARCH_URLS -from django.urls import path -from .views import SearchView - -urlpatterns = [ - path("search/", SearchView.as_view(), name="search"), -] -endef - -define DJANGO_SEARCH_UTILS -from django.apps import apps -from django.conf import settings - -def get_search_models(): - models = [] - for model_path in settings.SEARCH_MODELS: - app_label, model_name = model_path.split(".") - model = apps.get_model(app_label, model_name) - models.append(model) - return models -endef - -define DJANGO_SEARCH_VIEWS -from django.views.generic import ListView -from django.db import models -from django.db.models import Q -from .forms import SearchForm -from .utils import get_search_models - - -class SearchView(ListView): - template_name = "your_app/search_results.html" - context_object_name = "results" - paginate_by = 10 - - def get_queryset(self): - form = SearchForm(self.request.GET) - query = None - results = [] - - if form.is_valid(): - query = form.cleaned_data["query"] - search_models = get_search_models() - - for model in search_models: - fields = [f.name for f in model._meta.fields if isinstance(f, (models.CharField, models.TextField))] - queries = [Q(**{f"{field}__icontains": query}) for field in fields] - model_results = model.objects.filter(queries.pop()) - - for item in queries: - model_results = model_results.filter(item) - - results.extend(model_results) - - return results - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["form"] = SearchForm(self.request.GET) - context["query"] = self.request.GET.get("query", "") - return context -endef - -define DJANGO_SETTINGS_AUTHENTICATION_BACKENDS -AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", - "allauth.account.auth_backends.AuthenticationBackend", -] -endef - -define DJANGO_SETTINGS_BASE -# $(PROJECT_NAME) -# -# Uncomment next two lines to enable custom admin -# INSTALLED_APPS = [app for app in INSTALLED_APPS if app != 'django.contrib.admin'] -# INSTALLED_APPS.append('backend.apps.CustomAdminConfig') -import os # noqa -import dj_database_url # noqa - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -EXPLORER_CONNECTIONS = {"Default": "default"} -EXPLORER_DEFAULT_CONNECTION = "default" -LOGIN_REDIRECT_URL = "/" -PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SILENCED_SYSTEM_CHECKS = ["django_recaptcha.recaptcha_test_key_error"] -BASE_DIR = os.path.dirname(PROJECT_DIR) -STATICFILES_DIRS = [] -WEBPACK_LOADER = { - "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), -} -STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) -TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) -endef - -define DJANGO_SETTINGS_BASE_MINIMAL -# $(PROJECT_NAME) -import os # noqa -import dj_database_url # noqa - -INSTALLED_APPS.append("debug_toolbar") -INSTALLED_APPS.append("webpack_boilerplate") -PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -BASE_DIR = os.path.dirname(PROJECT_DIR) -STATICFILES_DIRS = [] -STATICFILES_DIRS.append(os.path.join(BASE_DIR, "frontend/build")) -TEMPLATES[0]["DIRS"].append(os.path.join(PROJECT_DIR, "templates")) -WEBPACK_LOADER = { - "MANIFEST_FILE": os.path.join(BASE_DIR, "frontend/build/manifest.json"), -} -endef - -define DJANGO_SETTINGS_CRISPY_FORMS -CRISPY_TEMPLATE_PACK = "bootstrap5" -CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" -endef - -define DJANGO_SETTINGS_DATABASE -DATABASE_URL = os.environ.get("DATABASE_URL", "postgres://:@:/$(PROJECT_NAME)") -DATABASES["default"] = dj_database_url.parse(DATABASE_URL) -endef - -define DJANGO_SETTINGS_DEV -from .base import * # noqa - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -# SECURITY WARNING: define the correct hosts in production! -ALLOWED_HOSTS = ["*"] - -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -try: - from .local import * # noqa -except ImportError: - pass - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "verbose": { - "format": "{levelname} {asctime} {module} {message}", - "style": "{", - }, - "simple": { - "format": "{levelname} {message}", - "style": "{", - }, - }, - "handlers": { - "console": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "verbose", - }, - }, - "loggers": { - "django": { - "handlers": ["console"], - "level": "DEBUG", - "propagate": True, - }, - }, -} - -INTERNAL_IPS = [ - "127.0.0.1", -] - -MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") # noqa -MIDDLEWARE.append("hijack.middleware.HijackUserMiddleware") # noqa -INSTALLED_APPS.append("django.contrib.admindocs") # noqa -SECRET_KEY = "$(DJANGO_SETTINGS_SECRET_KEY)" -endef - - -define DJANGO_SETTINGS_HOME_PAGE -INSTALLED_APPS.append("home") -endef - -define DJANGO_SETTINGS_INSTALLED_APPS -INSTALLED_APPS.append("allauth") -INSTALLED_APPS.append("allauth.account") -INSTALLED_APPS.append("allauth.socialaccount") -INSTALLED_APPS.append("crispy_bootstrap5") -INSTALLED_APPS.append("crispy_forms") -INSTALLED_APPS.append("debug_toolbar") -INSTALLED_APPS.append("django_extensions") -INSTALLED_APPS.append("django_recaptcha") -INSTALLED_APPS.append("rest_framework") -INSTALLED_APPS.append("rest_framework.authtoken") -INSTALLED_APPS.append("webpack_boilerplate") -INSTALLED_APPS.append("explorer") -endef - -define DJANGO_SETTINGS_MIDDLEWARE -MIDDLEWARE.append("allauth.account.middleware.AccountMiddleware") -endef - -define DJANGO_SETTINGS_MODEL_FORM_DEMO -INSTALLED_APPS.append("model_form_demo") # noqa -endef - -define DJANGO_SETTINGS_PAYMENTS -DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id" -DJSTRIPE_WEBHOOK_VALIDATION = "retrieve_event" -STRIPE_PUBLISHABLE_KEY = os.environ.get("STRIPE_PUBLISHABLE_KEY") -STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY") -STRIPE_TEST_SECRET_KEY = os.environ.get("STRIPE_TEST_SECRET_KEY") -INSTALLED_APPS.append("payments") # noqa -INSTALLED_APPS.append("djstripe") # noqa -endef - -define DJANGO_SETTINGS_REST_FRAMEWORK -REST_FRAMEWORK = { - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly" - ] -} -endef - -define DJANGO_SETTINGS_SITEUSER -INSTALLED_APPS.append("siteuser") # noqa -AUTH_USER_MODEL = "siteuser.User" -endef - -define DJANGO_SETTINGS_PROD -from .base import * # noqa -from backend.utils import get_ec2_metadata - -DEBUG = False - -try: - from .local import * # noqa -except ImportError: - pass - -LOCAL_IPV4 = get_ec2_metadata() -ALLOWED_HOSTS.append(LOCAL_IPV4) # noqa -endef - -define DJANGO_SETTINGS_THEMES -THEMES = [ - ("light", "Light Theme"), - ("dark", "Dark Theme"), -] -endef - -define DJANGO_SITEUSER_ADMIN -from django.contrib.auth.admin import UserAdmin -from django.contrib import admin - -from .models import User - -admin.site.register(User, UserAdmin) -endef - -define DJANGO_SITEUSER_EDIT_TEMPLATE -{% extends 'base.html' %} -{% load crispy_forms_tags %} -{% block content %} -

    Edit User

    - {% crispy form %} -{% endblock %} -endef - -define DJANGO_SITEUSER_FORM -from django import forms -from django.contrib.auth.forms import UserChangeForm -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Fieldset, ButtonHolder, Submit -from .models import User - - -class SiteUserForm(UserChangeForm): - bio = forms.CharField(widget=forms.Textarea(attrs={"id": "editor"}), required=False) - - class Meta(UserChangeForm.Meta): - model = User - fields = ("username", "user_theme_preference", "bio", "rate") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.helper = FormHelper() - self.helper.form_method = "post" - self.helper.layout = Layout( - Fieldset( - "Edit Your Profile", - "username", - "user_theme_preference", - "bio", - "rate", - ), - ButtonHolder(Submit("submit", "Save", css_class="btn btn-primary")), - ) -endef - -define DJANGO_SITEUSER_MODEL -from django.db import models -from django.contrib.auth.models import AbstractUser, Group, Permission -from django.conf import settings - - -class User(AbstractUser): - groups = models.ManyToManyField(Group, related_name="siteuser_set", blank=True) - user_permissions = models.ManyToManyField( - Permission, related_name="siteuser_set", blank=True - ) - - user_theme_preference = models.CharField( - max_length=10, choices=settings.THEMES, default="light" - ) - - bio = models.TextField(blank=True, null=True) - rate = models.FloatField(blank=True, null=True) -endef - -define DJANGO_SITEUSER_URLS -from django.urls import path -from .views import UserProfileView, UpdateThemePreferenceView, UserEditView - -urlpatterns = [ - path("profile/", UserProfileView.as_view(), name="user-profile"), - path( - "update_theme_preference/", - UpdateThemePreferenceView.as_view(), - name="update_theme_preference", - ), - path("/edit/", UserEditView.as_view(), name="user-edit"), -] -endef - -define DJANGO_SITEUSER_VIEW -import json - -from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse -from django.utils.decorators import method_decorator -from django.views import View -from django.views.decorators.csrf import csrf_exempt -from django.views.generic import DetailView -from django.views.generic.edit import UpdateView -from django.urls import reverse_lazy - -from .models import User -from .forms import SiteUserForm - - -class UserProfileView(LoginRequiredMixin, DetailView): - model = User - template_name = "profile.html" - - def get_object(self, queryset=None): - return self.request.user - - -@method_decorator(csrf_exempt, name="dispatch") -class UpdateThemePreferenceView(View): - def post(self, request, *args, **kwargs): - try: - data = json.loads(request.body.decode("utf-8")) - new_theme = data.get("theme") - user = request.user - user.user_theme_preference = new_theme - user.save() - response_data = {"theme": new_theme} - return JsonResponse(response_data) - except json.JSONDecodeError as e: - return JsonResponse({"error": e}, status=400) - - def http_method_not_allowed(self, request, *args, **kwargs): - return JsonResponse({"error": "Invalid request method"}, status=405) - - -class UserEditView(LoginRequiredMixin, UpdateView): - model = User - template_name = "user_edit.html" # Create this template in your templates folder - form_class = SiteUserForm - - def get_success_url(self): - # return reverse_lazy("user-profile", kwargs={"pk": self.object.pk}) - return reverse_lazy("user-profile") -endef - -define DJANGO_SITEUSER_VIEW_TEMPLATE -{% extends 'base.html' %} -{% block content %} -

    User Profile

    -
    - Edit -
    -

    Username: {{ user.username }}

    -

    Theme: {{ user.user_theme_preference }}

    -

    Bio: {{ user.bio|default:""|safe }}

    -

    Rate: {{ user.rate|default:"" }}

    -{% endblock %} -endef - -define DJANGO_URLS -from django.contrib import admin -from django.urls import path, include -from django.conf import settings - -urlpatterns = [ - path("django/", admin.site.urls), -] -endef - -define DJANGO_URLS_ALLAUTH -urlpatterns += [path("accounts/", include("allauth.urls"))] -endef - -define DJANGO_URLS_API -from rest_framework import routers # noqa -from .api import UserViewSet, api # noqa - -router = routers.DefaultRouter() -router.register(r"users", UserViewSet) -# urlpatterns += [path("api/", include(router.urls))] -urlpatterns += [path("api/", api.urls)] -endef - -define DJANGO_URLS_DEBUG_TOOLBAR -if settings.DEBUG: - urlpatterns += [path("__debug__/", include("debug_toolbar.urls"))] -endef - -define DJANGO_URLS_HOME_PAGE -urlpatterns += [path("", include("home.urls"))] -endef - -define DJANGO_URLS_LOGGING_DEMO -urlpatterns += [path("logging-demo/", include("logging_demo.urls"))] -endef - -define DJANGO_URLS_MODEL_FORM_DEMO -urlpatterns += [path("model-form-demo/", include("model_form_demo.urls"))] -endef - -define DJANGO_URLS_PAYMENTS -urlpatterns += [path("payments/", include("payments.urls"))] -endef - -define DJANGO_URLS_SITEUSER -urlpatterns += [path("user/", include("siteuser.urls"))] -endef - -define DJANGO_UTILS -from django.urls import URLResolver -import requests - - -def get_ec2_metadata(): - try: - # Step 1: Get the token - token_url = "http://169.254.169.254/latest/api/token" - headers = {"X-aws-ec2-metadata-token-ttl-seconds": "21600"} - response = requests.put(token_url, headers=headers) - response.raise_for_status() # Raise an error for bad responses - - token = response.text - - # Step 2: Use the token to get the instance metadata - metadata_url = "http://169.254.169.254/latest/meta-data/local-ipv4" - headers = {"X-aws-ec2-metadata-token": token} - response = requests.get(metadata_url, headers=headers) - response.raise_for_status() # Raise an error for bad responses - - metadata = response.text - return metadata - except requests.RequestException as e: - print(f"Error retrieving EC2 metadata: {e}") - return None - - -# Function to remove a specific URL pattern based on its route (including catch-all) -def remove_urlpattern(urlpatterns, route_to_remove): - urlpatterns[:] = [ - urlpattern - for urlpattern in urlpatterns - if not ( - isinstance(urlpattern, URLResolver) - and urlpattern.pattern._route == route_to_remove - ) - ] -endef - -define EB_CUSTOM_ENV_EC2_USER -files: - "/home/ec2-user/.bashrc": - mode: "000644" - owner: ec2-user - group: ec2-user - content: | - # .bashrc - - # Source global definitions - if [ -f /etc/bashrc ]; then - . /etc/bashrc - fi - - # User specific aliases and functions - set -o vi - - source <(sed -E -n 's/[^#]+/export &/ p' /opt/elasticbeanstalk/deployment/custom_env_var) -endef - -define EB_CUSTOM_ENV_VAR_FILE -#!/bin/bash - -# Via https://aws.amazon.com/premiumsupport/knowledge-center/elastic-beanstalk-env-variables-linux2/ - -#Create a copy of the environment variable file. -cat /opt/elasticbeanstalk/deployment/env | perl -p -e 's/(.*)=(.*)/export $$1="$$2"/;' > /opt/elasticbeanstalk/deployment/custom_env_var - -#Set permissions to the custom_env_var file so this file can be accessed by any user on the instance. You can restrict permissions as per your requirements. -chmod 644 /opt/elasticbeanstalk/deployment/custom_env_var - -# add the virtual env path in. -VENV=/var/app/venv/`ls /var/app/venv` -cat <> /opt/elasticbeanstalk/deployment/custom_env_var -VENV=$$ENV -EOF - -#Remove duplicate files upon deployment. -rm -f /opt/elasticbeanstalk/deployment/*.bak -endef - -define GIT_IGNORE -__pycache__ -*.pyc -dist/ -node_modules/ -_build/ -.elasticbeanstalk/ -db.sqlite3 -static/ -backend/var -endef - -define JENKINS_FILE -pipeline { - agent any - stages { - stage('') { - steps { - echo '' - } - } - } -} -endef - -define MAKEFILE_CUSTOM -# Custom Makefile -# Add your custom makefile commands here -# -# PROJECT_NAME := my-new-project -endef - -define PIP_INSTALL_REQUIREMENTS_TEST -pytest -pytest-runner -coverage -pytest-mock -pytest-cov -hypothesis -selenium -pytest-django -factory-boy -flake8 -tox -endef - -define PROGRAMMING_INTERVIEW -from rich import print as rprint -from rich.console import Console -from rich.panel import Panel - -import argparse -import locale -import math -import time - -import code # noqa -import readline # noqa -import rlcompleter # noqa - - -locale.setlocale(locale.LC_ALL, "en_US.UTF-8") - - -class DataStructure: - # Data Structure: Binary Tree - class TreeNode: - def __init__(self, value=0, left=None, right=None): - self.value = value - self.left = left - self.right = right - - # Data Structure: Stack - class Stack: - def __init__(self): - self.items = [] - - def push(self, item): - self.items.append(item) - - def pop(self): - if not self.is_empty(): - return self.items.pop() - return None - - def peek(self): - if not self.is_empty(): - return self.items[-1] - return None - - def is_empty(self): - return len(self.items) == 0 - - def size(self): - return len(self.items) - - # Data Structure: Queue - class Queue: - def __init__(self): - self.items = [] - - def enqueue(self, item): - self.items.append(item) - - def dequeue(self): - if not self.is_empty(): - return self.items.pop(0) - return None - - def is_empty(self): - return len(self.items) == 0 - - def size(self): - return len(self.items) - - # Data Structure: Linked List - class ListNode: - def __init__(self, value=0, next=None): - self.value = value - self.next = next - - -class Interview(DataStructure): - - # Protected methods for factorial calculation - def _factorial_recursive(self, n): - if n == 0: - return 1 - return n * self._factorial_recursive(n - 1) - - def _factorial_divide_and_conquer(self, low, high): - if low > high: - return 1 - if low == high: - return low - mid = (low + high) // 2 - return self._factorial_divide_and_conquer( - low, mid - ) * self._factorial_divide_and_conquer(mid + 1, high) - - # Recursive Factorial with Timing - def factorial_recursive(self, n): - start_time = time.time() # Start timing - result = self._factorial_recursive(n) # Calculate factorial - end_time = time.time() # End timing - elapsed_time = end_time - start_time - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Iterative Factorial with Timing - def factorial_iterative(self, n): - start_time = time.time() # Start timing - result = 1 - for i in range(1, n + 1): - result *= i - end_time = time.time() # End timing - elapsed_time = end_time - start_time - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Divide and Conquer Factorial with Timing - def factorial_divide_and_conquer(self, n): - start_time = time.time() # Start timing - result = self._factorial_divide_and_conquer(1, n) # Calculate factorial - end_time = time.time() # End timing - elapsed_time = end_time - start_time - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Built-in Factorial with Timing - def factorial_builtin(self, n): - start_time = time.time() # Start timing - result = math.factorial(n) # Calculate factorial using built-in - end_time = time.time() # End timing - - # Calculate elapsed time - elapsed_time = end_time - start_time - - # Print complexity and runtime - return f" Factorial: {locale.format_string("%.2f", result, grouping=True)} Elapsed time: {elapsed_time:.6f}" - - # Recursion: Fibonacci - def fibonacci_recursive(self, n): - if n <= 1: - return n - return self.fibonacci_recursive(n - 1) + self.fibonacci_recursive(n - 2) - - # Iteration: Fibonacci - def fibonacci_iterative(self, n): - if n <= 1: - return n - a, b = 0, 1 - for _ in range(n - 1): - a, b = b, a + b - return b - - # Searching: Linear Search - def linear_search(self, arr, target): - for i, value in enumerate(arr): - if value == target: - return i - return -1 - - # Searching: Binary Search - def binary_search(self, arr, target): - left, right = 0, len(arr) - 1 - while left <= right: - mid = (left + right) // 2 - if arr[mid] == target: - return mid - elif arr[mid] < target: - left = mid + 1 - else: - right = mid - 1 - return -1 - - # Sorting: Bubble Sort - def bubble_sort(self, arr): - n = len(arr) - for i in range(n): - for j in range(0, n - i - 1): - if arr[j] > arr[j + 1]: - arr[j], arr[j + 1] = arr[j + 1], arr[j] - return arr - - # Sorting: Merge Sort - def merge_sort(self, arr): - if len(arr) > 1: - mid = len(arr) // 2 - left_half = arr[:mid] - right_half = arr[mid:] - - self.merge_sort(left_half) - self.merge_sort(right_half) - - i = j = k = 0 - - while i < len(left_half) and j < len(right_half): - if left_half[i] < right_half[j]: - arr[k] = left_half[i] - i += 1 - else: - arr[k] = right_half[j] - j += 1 - k += 1 - - while i < len(left_half): - arr[k] = left_half[i] - i += 1 - k += 1 - - while j < len(right_half): - arr[k] = right_half[j] - j += 1 - k += 1 - return arr - - def insert_linked_list(self, head, value): - new_node = self.ListNode(value) - if not head: - return new_node - current = head - while current.next: - current = current.next - current.next = new_node - return head - - def print_linked_list(self, head): - current = head - while current: - print(current.value, end=" -> ") - current = current.next - print("None") - - def inorder_traversal(self, root): - return ( - self.inorder_traversal(root.left) - + [root.value] - + self.inorder_traversal(root.right) - if root - else [] - ) - - def preorder_traversal(self, root): - return ( - [root.value] - + self.preorder_traversal(root.left) - + self.preorder_traversal(root.right) - if root - else [] - ) - - def postorder_traversal(self, root): - return ( - self.postorder_traversal(root.left) - + self.postorder_traversal(root.right) - + [root.value] - if root - else [] - ) - - # Graph Algorithms: Depth-First Search - def dfs(self, graph, start): - visited, stack = set(), [start] - while stack: - vertex = stack.pop() - if vertex not in visited: - visited.add(vertex) - stack.extend(set(graph[vertex]) - visited) - return visited - - # Graph Algorithms: Breadth-First Search - def bfs(self, graph, start): - visited, queue = set(), [start] - while queue: - vertex = queue.pop(0) - if vertex not in visited: - visited.add(vertex) - queue.extend(set(graph[vertex]) - visited) - return visited - - -def setup_readline(local): - - # Enable tab completion - readline.parse_and_bind("tab: complete") - # Optionally, you can set the completer function manually - readline.set_completer(rlcompleter.Completer(local).complete) - - -def main(): - - console = Console() - interview = Interview() - - parser = argparse.ArgumentParser(description="Programming Interview Questions") - - parser.add_argument( - "-f", "--factorial", type=int, help="Factorial algorithm examples" - ) - parser.add_argument("--fibonacci", type=int, help="Fibonacci algorithm examples") - parser.add_argument( - "--search", action="store_true", help="Search algorithm examples" - ) - parser.add_argument("--sort", action="store_true", help="Search algorithm examples") - parser.add_argument("--stack", action="store_true", help="Stack algorithm examples") - parser.add_argument("--queue", action="store_true", help="Queue algorithm examples") - parser.add_argument( - "--list", action="store_true", help="Linked List algorithm examples" - ) - parser.add_argument( - "--tree", action="store_true", help="Tree traversal algorithm examples" - ) - parser.add_argument("--graph", action="store_true", help="Graph algorithm examples") - parser.add_argument( - "-i", "--interactive", action="store_true", help="Interactive mode" - ) - - args = parser.parse_args() - - if args.factorial: - # Factorial examples - console.rule("Factorial Examples") - rprint( - Panel( - "[bold cyan]Recursive Factorial - Time Complexity: O(n)[/bold cyan]\n" - + str(interview.factorial_recursive(args.factorial)), - title="Factorial Recursive", - ) - ) - rprint( - Panel( - "[bold cyan]Iterative Factorial - Time Complexity: O(n)[/bold cyan]\n" - + str(interview.factorial_iterative(args.factorial)), - title="Factorial Iterative", - ) - ) - rprint( - Panel( - "[bold cyan]Built-in Factorial - Time Complexity: O(n)[/bold cyan]\n" - + str(interview.factorial_builtin(args.factorial)), - title="Factorial Built-in", - ) - ) - rprint( - Panel( - "[bold cyan]Divide and Conquer Factorial - Time Complexity: O(n log n)[/bold cyan]\n" - + str(interview.factorial_divide_and_conquer(args.factorial)), - title="Factorial Divide and Conquer", - ) - ) - exit() - - if args.fibonacci: - # Fibonacci examples - console.rule("Fibonacci Examples") - rprint( - Panel( - str(interview.fibonacci_recursive(args.fibonacci)), - title="Fibonacci Recursive", - ) - ) - rprint( - Panel( - str(interview.fibonacci_iterative(args.fibonacci)), - title="Fibonacci Iterative", - ) - ) - exit() - - if args.search: - # Searching examples - console.rule("Searching Examples") - array = [1, 3, 5, 7, 9] - rprint(Panel(str(interview.linear_search(array, 5)), title="Linear Search")) - rprint(Panel(str(interview.binary_search(array, 5)), title="Binary Search")) - exit() - - if args.sort: - # Sorting examples - console.rule("Sorting Examples") - unsorted_array = [64, 34, 25, 12, 22, 11, 90] - rprint( - Panel( - str(interview.bubble_sort(unsorted_array.copy())), title="Bubble Sort" - ) - ) - rprint( - Panel(str(interview.merge_sort(unsorted_array.copy())), title="Merge Sort") - ) - exit() - - if args.stack: - # Stack example - console.rule("Stack Example") - stack = interview.Stack() - stack.push(1) - stack.push(2) - stack.push(3) - rprint(Panel(str(stack.pop()), title="Stack Pop")) - rprint(Panel(str(stack.peek()), title="Stack Peek")) - rprint(Panel(str(stack.size()), title="Stack Size")) - - if args.queue: - # Queue example - console.rule("Queue Example") - queue = interview.Queue() - queue.enqueue(1) - queue.enqueue(2) - queue.enqueue(3) - rprint(Panel(str(queue.dequeue()), title="Queue Dequeue")) - rprint(Panel(str(queue.is_empty()), title="Queue Is Empty")) - rprint(Panel(str(queue.size()), title="Queue Size")) - - if args.list: - # Linked List example - console.rule("Linked List Example") - head = None - head = interview.insert_linked_list(head, 1) - head = interview.insert_linked_list(head, 2) - head = interview.insert_linked_list(head, 3) - interview.print_linked_list(head) # Output: 1 -> 2 -> 3 -> None - - if args.tree: - # Tree Traversal example - console.rule("Tree Traversal Example") - root = interview.TreeNode(1) - root.left = interview.TreeNode(2) - root.right = interview.TreeNode(3) - root.left.left = interview.TreeNode(4) - root.left.right = interview.TreeNode(5) - rprint(Panel(str(interview.inorder_traversal(root)), title="Inorder Traversal")) - rprint( - Panel(str(interview.preorder_traversal(root)), title="Preorder Traversal") - ) - rprint( - Panel(str(interview.postorder_traversal(root)), title="Postorder Traversal") - ) - - if args.graph: - # Graph Algorithms example - console.rule("Graph Algorithms Example") - graph = { - "A": ["B", "C"], - "B": ["A", "D", "E"], - "C": ["A", "F"], - "D": ["B"], - "E": ["B", "F"], - "F": ["C", "E"], - } - rprint(Panel(str(interview.dfs(graph, "A")), title="DFS")) - rprint(Panel(str(interview.bfs(graph, "A")), title="BFS")) - - if args.interactive: - # Starting interactive session with tab completion - setup_readline(locals()) - banner = "Interactive programming interview session started. Type 'exit()' or 'Ctrl-D' to exit." - code.interact( - banner=banner, - local=locals(), - exitmsg="Great interview!", - ) - - -if __name__ == "__main__": - main() - -endef - -define PYTHON_CI_YAML -name: Build Wheels -endef - -define PYTHON_LICENSE_TXT -MIT License - -Copyright (c) [YEAR] [OWNER NAME] - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -endef - -define PYTHON_PROJECT_TOML -[build-system] -endef - -define SEPARATOR -.==========================================================================================================================================. -| | -| _|_|_| _| _| _| _| _| _|_| _| _| | -| _| _| _| _|_| _|_| _|_| _|_|_| _|_|_|_| _|_| _|_| _|_|_| _| _| _|_| _| _| _|_| | -| _|_|_| _|_| _| _| _| _|_|_|_| _| _| _| _| _| _| _| _|_| _|_|_|_| _|_|_|_| _| _| _|_|_|_| | -| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| _| | -| _| _| _|_| _| _|_|_| _|_|_| _|_| _| _| _|_|_| _| _| _|_|_| _| _| _| _|_|_| | -| _| | -| _| | -`==========================================================================================================================================' -endef - -define TINYMCE_JS -import tinymce from 'tinymce'; -import 'tinymce/icons/default'; -import 'tinymce/themes/silver'; -import 'tinymce/skins/ui/oxide/skin.css'; -import 'tinymce/plugins/advlist'; -import 'tinymce/plugins/code'; -import 'tinymce/plugins/emoticons'; -import 'tinymce/plugins/emoticons/js/emojis'; -import 'tinymce/plugins/link'; -import 'tinymce/plugins/lists'; -import 'tinymce/plugins/table'; -import 'tinymce/models/dom'; - -tinymce.init({ - selector: 'textarea#editor', - plugins: 'advlist code emoticons link lists table', - toolbar: 'bold italic | bullist numlist | link emoticons', - skin: false, - content_css: false, -}); -endef - -define WAGTAIL_BASE_TEMPLATE -{% load static wagtailcore_tags wagtailuserbar webpack_loader %} - - - - - - {% block title %} - {% if page.seo_title %} - {{ page.seo_title }} - {% else %} - {{ page.title }} - {% endif %} - {% endblock %} - {% block title_suffix %} - {% wagtail_site as current_site %} - {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %} - {% endblock %} - - {% if page.search_description %}{% endif %} - - {# Force all links in the live preview panel to be opened in a new tab #} - {% if request.in_preview_panel %}{% endif %} - {% stylesheet_pack 'app' %} - {% block extra_css %}{# Override this in templates to add extra stylesheets #}{% endblock %} - - {% include 'favicon.html' %} - {% csrf_token %} - - -
    - {% wagtailuserbar %} -
    - {% include 'header.html' %} - {% if messages %} -
    - {% for message in messages %} - - {% endfor %} -
    - {% endif %} -
    - {% block content %}{% endblock %} -
    -
    - {% include 'footer.html' %} - {% include 'offcanvas.html' %} - {% javascript_pack 'app' %} - {% block extra_js %}{# Override this in templates to add extra javascript #}{% endblock %} - - -endef - -define WAGTAIL_BLOCK_CAROUSEL - -endef - -define WAGTAIL_BLOCK_MARKETING -{% load wagtailcore_tags %} -
    - {% if block.value.images.0 %} - {% include 'blocks/carousel_block.html' %} - {% else %} - {{ self.title }} - {{ self.content }} - {% endif %} -
    -endef - -define WAGTAIL_CONTACT_PAGE_LANDING -{% extends 'base.html' %} -{% block content %}

    Thank you!

    {% endblock %} -endef - -define WAGTAIL_CONTACT_PAGE_MODEL -from django.db import models -from modelcluster.fields import ParentalKey -from wagtail.admin.panels import ( - FieldPanel, FieldRowPanel, - InlinePanel, MultiFieldPanel -) -from wagtail.fields import RichTextField -from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField - - -class FormField(AbstractFormField): - page = ParentalKey('ContactPage', on_delete=models.CASCADE, related_name='form_fields') - - -class ContactPage(AbstractEmailForm): - intro = RichTextField(blank=True) - thank_you_text = RichTextField(blank=True) - - content_panels = AbstractEmailForm.content_panels + [ - FieldPanel('intro'), - InlinePanel('form_fields', label="Form fields"), - FieldPanel('thank_you_text'), - MultiFieldPanel([ - FieldRowPanel([ - FieldPanel('from_address', classname="col6"), - FieldPanel('to_address', classname="col6"), - ]), - FieldPanel('subject'), - ], "Email"), - ] - - class Meta: - verbose_name = "Contact Page" -endef - -define WAGTAIL_CONTACT_PAGE_TEMPLATE -{% extends 'base.html' %} -{% load crispy_forms_tags static wagtailcore_tags %} -{% block content %} -

    {{ page.title }}

    - {{ page.intro|richtext }} -
    - {% csrf_token %} - {{ form.as_p }} - -
    -{% endblock %} -endef - -define WAGTAIL_CONTACT_PAGE_TEST -from django.test import TestCase -from wagtail.test.utils import WagtailPageTestCase -from wagtail.models import Page - -from contactpage.models import ContactPage, FormField - -class ContactPageTest(TestCase, WagtailPageTestCase): - def test_contact_page_creation(self): - # Create a ContactPage instance - contact_page = ContactPage( - title='Contact', - intro='Welcome to our contact page!', - thank_you_text='Thank you for reaching out.' - ) - - # Save the ContactPage instance - self.assertEqual(contact_page.save_revision().publish().get_latest_revision_as_page(), contact_page) - - def test_form_field_creation(self): - # Create a ContactPage instance - contact_page = ContactPage( - title='Contact', - intro='Welcome to our contact page!', - thank_you_text='Thank you for reaching out.' - ) - # Save the ContactPage instance - contact_page_revision = contact_page.save_revision() - contact_page_revision.publish() - - # Create a FormField associated with the ContactPage - form_field = FormField( - page=contact_page, - label='Your Name', - field_type='singleline', - required=True - ) - form_field.save() - - # Retrieve the ContactPage from the database - contact_page_from_db = Page.objects.get(id=contact_page.id).specific - - # Check if the FormField is associated with the ContactPage - self.assertEqual(contact_page_from_db.form_fields.first(), form_field) - - def test_contact_page_form_submission(self): - # Create a ContactPage instance - contact_page = ContactPage( - title='Contact', - intro='Welcome to our contact page!', - thank_you_text='Thank you for reaching out.' - ) - # Save the ContactPage instance - contact_page_revision = contact_page.save_revision() - contact_page_revision.publish() - - # Simulate a form submission - form_data = { - 'your_name': 'John Doe', - # Add other form fields as needed - } - - response = self.client.post(contact_page.url, form_data) - - # Check if the form submission is successful (assuming a 302 redirect) - self.assertEqual(response.status_code, 302) - - # You may add more assertions based on your specific requirements -endef - -define WAGTAIL_HEADER_PREFIX -{% load wagtailcore_tags %} -{% wagtail_site as current_site %} -endef - -define WAGTAIL_HOME_PAGE_MODEL -from wagtail.models import Page -from wagtail.fields import StreamField -from wagtail import blocks -from wagtail.admin.panels import FieldPanel -from wagtail.images.blocks import ImageChooserBlock - - -class MarketingBlock(blocks.StructBlock): - title = blocks.CharBlock(required=False, help_text="Enter the block title") - content = blocks.RichTextBlock(required=False, help_text="Enter the block content") - images = blocks.ListBlock( - ImageChooserBlock(required=False), - help_text="Select one or two images for column display. Select three or more images for carousel display.", - ) - image = ImageChooserBlock( - required=False, help_text="Select one image for background display." - ) - block_class = blocks.CharBlock( - required=False, - help_text="Enter a CSS class for styling the marketing block", - classname="full title", - default="vh-100 bg-secondary", - ) - image_class = blocks.CharBlock( - required=False, - help_text="Enter a CSS class for styling the column display image(s)", - classname="full title", - default="img-thumbnail p-5", - ) - layout_class = blocks.CharBlock( - required=False, - help_text="Enter a CSS class for styling the layout.", - classname="full title", - default="d-flex flex-row", - ) - - class Meta: - icon = "placeholder" - template = "blocks/marketing_block.html" - - -class HomePage(Page): - template = "home/home_page.html" # Create a template for rendering the home page - - marketing_blocks = StreamField( - [ - ("marketing_block", MarketingBlock()), - ], - blank=True, - null=True, - use_json_field=True, - ) - content_panels = Page.content_panels + [ - FieldPanel("marketing_blocks"), - ] - - class Meta: - verbose_name = "Home Page" -endef - -define WAGTAIL_HOME_PAGE_TEMPLATE -{% extends "base.html" %} -{% load wagtailcore_tags %} -{% block content %} -
    - {% for block in page.marketing_blocks %} - {% include_block block %} - {% endfor %} -
    -{% endblock %} -endef - -define WAGTAIL_PRIVACY_PAGE_MODEL -from wagtail.models import Page -from wagtail.admin.panels import FieldPanel -from wagtailmarkdown.fields import MarkdownField - - -class PrivacyPage(Page): - """ - A Wagtail Page model for the Privacy Policy page. - """ - - template = "privacy_page.html" - - body = MarkdownField() - - content_panels = Page.content_panels + [ - FieldPanel("body", classname="full"), - ] - - class Meta: - verbose_name = "Privacy Page" -endef - -define WAGTAIL_PRIVACY_PAGE_TEMPLATE -{% extends 'base.html' %} -{% load wagtailmarkdown %} -{% block content %}
    {{ page.body|markdown }}
    {% endblock %} -endef - -define WAGTAIL_SEARCH_TEMPLATE -{% extends "base.html" %} -{% load static wagtailcore_tags %} -{% block body_class %}template-searchresults{% endblock %} -{% block title %}Search{% endblock %} -{% block content %} -

    Search

    -
    - - -
    - {% if search_results %} -
      - {% for result in search_results %} -
    • -

      - {{ result }} -

      - {% if result.search_description %}{{ result.search_description }}{% endif %} -
    • - {% endfor %} -
    - {% if search_results.has_previous %} - Previous - {% endif %} - {% if search_results.has_next %} - Next - {% endif %} - {% elif search_query %} - No results found - {% else %} - No results found. Try a test query? - {% endif %} -{% endblock %} -endef - -define WAGTAIL_SEARCH_URLS -from django.urls import path -from .views import search - -urlpatterns = [path("", search, name="search")] -endef - -define WAGTAIL_SETTINGS -INSTALLED_APPS.append("wagtail_color_panel") -INSTALLED_APPS.append("wagtail_modeladmin") -INSTALLED_APPS.append("wagtail.contrib.settings") -INSTALLED_APPS.append("wagtailmarkdown") -INSTALLED_APPS.append("wagtailmenus") -INSTALLED_APPS.append("wagtailseo") -TEMPLATES[0]["OPTIONS"]["context_processors"].append( - "wagtail.contrib.settings.context_processors.settings" -) -TEMPLATES[0]["OPTIONS"]["context_processors"].append( - "wagtailmenus.context_processors.wagtailmenus" -) -endef - -define WAGTAIL_SITEPAGE_MODEL -from wagtail.models import Page - - -class SitePage(Page): - template = "sitepage/site_page.html" - - class Meta: - verbose_name = "Site Page" -endef - -define WAGTAIL_SITEPAGE_TEMPLATE -{% extends 'base.html' %} -{% block content %} -

    {{ page.title }}

    -{% endblock %} -endef - -define WAGTAIL_URLS -from django.conf import settings -from django.urls import include, path -from django.contrib import admin - -from wagtail.admin import urls as wagtailadmin_urls -from wagtail.documents import urls as wagtaildocs_urls - -from search import views as search_views - -urlpatterns = [ - path("django/", admin.site.urls), - path("wagtail/", include(wagtailadmin_urls)), - path("documents/", include(wagtaildocs_urls)), - path("search/", search_views.search, name="search"), -] - -if settings.DEBUG: - from django.conf.urls.static import static - from django.contrib.staticfiles.urls import staticfiles_urlpatterns - - # Serve static and media files from development server - urlpatterns += staticfiles_urlpatterns() - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -endef - -define WAGTAIL_URLS_HOME -urlpatterns += [ - # For anything not caught by a more specific rule above, hand over to - # Wagtail's page serving mechanism. This should be the last pattern in - # the list: - path("", include("wagtail.urls")), - # Alternatively, if you want Wagtail pages to be served from a subpath - # of your site, rather than the site root: - # path("pages/", include("wagtail.urls"), -] -endef - -define WEBPACK_CONFIG_JS -const path = require('path'); - -module.exports = { - mode: 'development', - entry: './src/index.js', - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), - }, -}; -endef - -define WEBPACK_INDEX_HTML - - - - - - Hello, Webpack! - - - - - -endef - -define WEBPACK_INDEX_JS -const message = "Hello, World!"; -console.log(message); -endef - -define WEBPACK_REVEAL_CONFIG_JS -const path = require('path'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); - -module.exports = { - mode: 'development', - entry: './src/index.js', - output: { - filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), - }, - module: { - rules: [ - { - test: /\.css$$/, - use: [MiniCssExtractPlugin.loader, 'css-loader'], - }, - ], - }, - plugins: [ - new MiniCssExtractPlugin({ - filename: 'bundle.css', - }), - ], -}; -endef - -define WEBPACK_REVEAL_INDEX_HTML - - - - - - Project Makefile - - -
    -
    -
    - Slide 1: Draw some circles -
    -
    - Slide 2: Draw the rest of the owl -
    -
    -
    - - -endef - -define WEBPACK_REVEAL_INDEX_JS -import 'reveal.js/dist/reveal.css'; -import 'reveal.js/dist/theme/black.css'; -import Reveal from 'reveal.js'; -import RevealNotes from 'reveal.js/plugin/notes/notes.js'; -Reveal.initialize({ slideNumber: true, plugins: [ RevealNotes ]}); -endef - -# ------------------------------------------------------------------------------ -# Export variables used by phony target rules -# ------------------------------------------------------------------------------ - -export DJANGO_ALLAUTH_BASE_TEMPLATE -export DJANGO_API_SERIALIZERS -export DJANGO_API_VIEWS -export DJANGO_APP_TESTS -export DJANGO_BACKEND_APPS -export DJANGO_BASE_TEMPLATE -export DJANGO_CUSTOM_ADMIN -export DJANGO_DOCKERCOMPOSE -export DJANGO_DOCKERFILE -export DJANGO_FAVICON_TEMPLATE -export DJANGO_FOOTER_TEMPLATE -export DJANGO_FRONTEND_APP -export DJANGO_FRONTEND_APP_CONFIG -export DJANGO_FRONTEND_BABELRC -export DJANGO_FRONTEND_COMPONENTS -export DJANGO_FRONTEND_COMPONENT_CLOCK -export DJANGO_FRONTEND_COMPONENT_ERROR -export DJANGO_FRONTEND_COMPONENT_USER_MENU -export DJANGO_FRONTEND_CONTEXT_INDEX -export DJANGO_FRONTEND_CONTEXT_USER_PROVIDER -export DJANGO_FRONTEND_ESLINTRC -export DJANGO_FRONTEND_OFFCANVAS_TEMPLATE -export DJANGO_FRONTEND_PORTAL -export DJANGO_FRONTEND_STYLES -export DJANGO_FRONTEND_THEME_BLUE -export DJANGO_FRONTEND_THEME_TOGGLER -export DJANGO_HEADER_TEMPLATE -export DJANGO_HOME_PAGE_ADMIN -export DJANGO_HOME_PAGE_MODELS -export DJANGO_HOME_PAGE_TEMPLATE -export DJANGO_HOME_PAGE_URLS -export DJANGO_HOME_PAGE_VIEWS -export DJANGO_LOGGING_DEMO_ADMIN -export DJANGO_LOGGING_DEMO_MODELS -export DJANGO_LOGGING_DEMO_SETTINGS -export DJANGO_LOGGING_DEMO_URLS -export DJANGO_LOGGING_DEMO_VIEWS -export DJANGO_MANAGE_PY -export DJANGO_MODEL_FORM_DEMO_ADMIN -export DJANGO_MODEL_FORM_DEMO_FORMS -export DJANGO_MODEL_FORM_DEMO_MODEL -export DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL -export DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM -export DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST -export DJANGO_MODEL_FORM_DEMO_URLS -export DJANGO_MODEL_FORM_DEMO_VIEWS -export DJANGO_PAYMENTS_ADMIN -export DJANGO_PAYMENTS_FORM -export DJANGO_PAYMENTS_MIGRATION_0002 -export DJANGO_PAYMENTS_MIGRATION_0003 -export DJANGO_PAYMENTS_MODELS -export DJANGO_PAYMENTS_TEMPLATE_CANCEL -export DJANGO_PAYMENTS_TEMPLATE_CHECKOUT -export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL -export DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST -export DJANGO_PAYMENTS_TEMPLATE_SUCCESS -export DJANGO_PAYMENTS_URLS -export DJANGO_PAYMENTS_VIEW -export DJANGO_SEARCH_FORMS -export DJANGO_SEARCH_SETTINGS -export DJANGO_SEARCH_TEMPLATE -export DJANGO_SEARCH_URLS -export DJANGO_SEARCH_UTILS -export DJANGO_SEARCH_VIEWS -export DJANGO_SETTINGS_AUTHENTICATION_BACKENDS -export DJANGO_SETTINGS_BASE -export DJANGO_SETTINGS_BASE_MINIMAL -export DJANGO_SETTINGS_CRISPY_FORMS -export DJANGO_SETTINGS_DATABASE -export DJANGO_SETTINGS_DEV -export DJANGO_SETTINGS_HOME_PAGE -export DJANGO_SETTINGS_INSTALLED_APPS -export DJANGO_SETTINGS_MIDDLEWARE -export DJANGO_SETTINGS_MODEL_FORM_DEMO -export DJANGO_SETTINGS_PAYMENTS -export DJANGO_SETTINGS_PROD -export DJANGO_SETTINGS_REST_FRAMEWORK -export DJANGO_SETTINGS_SITEUSER -export DJANGO_SETTINGS_THEMES -export DJANGO_SITEUSER_ADMIN -export DJANGO_SITEUSER_EDIT_TEMPLATE -export DJANGO_SITEUSER_FORM -export DJANGO_SITEUSER_MODEL -export DJANGO_SITEUSER_URLS -export DJANGO_SITEUSER_VIEW -export DJANGO_SITEUSER_VIEW_TEMPLATE -export DJANGO_URLS -export DJANGO_URLS_ALLAUTH -export DJANGO_URLS_API -export DJANGO_URLS_DEBUG_TOOLBAR -export DJANGO_URLS_HOME_PAGE -export DJANGO_URLS_LOGGING_DEMO -export DJANGO_URLS_MODEL_FORM_DEMO -export DJANGO_URLS_SITEUSER -export DJANGO_UTILS -export EB_CUSTOM_ENV_EC2_USER -export EB_CUSTOM_ENV_VAR_FILE -export GIT_IGNORE -export JENKINS_FILE -export MAKEFILE_CUSTOM -export PIP_INSTALL_REQUIREMENTS_TEST -export PROGRAMMING_INTERVIEW -export PYTHON_CI_YAML -export PYTHON_LICENSE_TXT -export PYTHON_PROJECT_TOML -export SEPARATOR -export TINYMCE_JS -export WAGTAIL_BASE_TEMPLATE -export WAGTAIL_BLOCK_CAROUSEL -export WAGTAIL_BLOCK_MARKETING -export WAGTAIL_CONTACT_PAGE_LANDING -export WAGTAIL_CONTACT_PAGE_MODEL -export WAGTAIL_CONTACT_PAGE_TEMPLATE -export WAGTAIL_CONTACT_PAGE_TEST -export WAGTAIL_HOME_PAGE_MODEL -export WAGTAIL_HOME_PAGE_TEMPLATE -export WAGTAIL_HOME_PAGE_URLS -export WAGTAIL_HOME_PAGE_VIEWS -export WAGTAIL_PRIVACY_PAGE_MODEL -export WAGTAIL_PRIVACY_PAGE_MODEL -export WAGTAIL_PRIVACY_PAGE_TEMPLATE -export WAGTAIL_SEARCH_TEMPLATE -export WAGTAIL_SEARCH_URLS -export WAGTAIL_SETTINGS -export WAGTAIL_SITEPAGE_MODEL -export WAGTAIL_SITEPAGE_TEMPLATE -export WAGTAIL_URLS -export WAGTAIL_URLS_HOME -export WEBPACK_CONFIG_JS -export WEBPACK_INDEX_HTML -export WEBPACK_INDEX_JS -export WEBPACK_REVEAL_CONFIG_JS -export WEBPACK_REVEAL_INDEX_HTML -export WEBPACK_REVEAL_INDEX_JS - -# ------------------------------------------------------------------------------ -# Multi-line phony target rules -# ------------------------------------------------------------------------------ - -.PHONY: aws-check-env-profile-default -aws-check-env-profile-default: -ifndef AWS_PROFILE - $(error AWS_PROFILE is undefined) -endif - -.PHONY: aws-check-env-region-default -aws-check-env-region-default: -ifndef AWS_REGION - $(error AWS_REGION is undefined) -endif - -.PHONY: aws-secret-default -aws-secret-default: aws-check-env - @SECRET_KEY=$$(openssl rand -base64 48); aws ssm put-parameter --name "SECRET_KEY" --value "$$SECRET_KEY" --type String - -.PHONY: aws-sg-default -aws-sg-default: aws-check-env - aws ec2 describe-security-groups $(AWS_OPTS) - -.PHONY: aws-ssm-default -aws-ssm-default: aws-check-env - aws ssm describe-parameters $(AWS_OPTS) - @echo "Get parameter values with: aws ssm getparameter --name ." - -.PHONY: aws-subnet-default -aws-subnet-default: aws-check-env - aws ec2 describe-subnets $(AWS_OPTS) - -.PHONY: aws-vol-available-default -aws-vol-available-default: aws-check-env - aws ec2 describe-volumes --filters Name=status,Values=available --query "Volumes[*].{ID:VolumeId,Size:Size}" --output table - -.PHONY: aws-vol-default -aws-vol-default: aws-check-env - aws ec2 describe-volumes --output table - -.PHONY: aws-vpc-default -aws-vpc-default: aws-check-env - aws ec2 describe-vpcs $(AWS_OPTS) - -.PHONY: db-import-default -db-import-default: - @psql $(DJANGO_DB_NAME) < $(DJANGO_DB_NAME).sql - -.PHONY: db-init-default -db-init-default: - -dropdb $(PROJECT_NAME) - -createdb $(PROJECT_NAME) - -.PHONY: db-init-mysql-default -db-init-mysql-default: - -mysqladmin -u root drop $(PROJECT_NAME) - -mysqladmin -u root create $(PROJECT_NAME) - -.PHONY: db-init-test-default -db-init-test-default: - -dropdb test_$(PROJECT_NAME) - -createdb test_$(PROJECT_NAME) - -.PHONY: django-allauth-default -django-allauth-default: - $(ADD_DIR) backend/templates/allauth/layouts - @echo "$$DJANGO_ALLAUTH_BASE_TEMPLATE" > backend/templates/allauth/layouts/base.html - @echo "$$DJANGO_URLS_ALLAUTH" >> $(DJANGO_URLS_FILE) - -$(GIT_ADD) backend/templates/allauth/layouts/base.html - -.PHONY: django-app-tests-default -django-app-tests-default: - @echo "$$DJANGO_APP_TESTS" > $(APP_DIR)/tests.py - -.PHONY: django-base-template-default -django-base-template-default: - @$(ADD_DIR) backend/templates - @echo "$$DJANGO_BASE_TEMPLATE" > backend/templates/base.html - -$(GIT_ADD) backend/templates/base.html - -.PHONY: django-custom-admin-default -django-custom-admin-default: - @echo "$$DJANGO_CUSTOM_ADMIN" > $(DJANGO_CUSTOM_ADMIN_FILE) - @echo "$$DJANGO_BACKEND_APPS" > $(DJANGO_BACKEND_APPS_FILE) - -$(GIT_ADD) backend/*.py - -.PHONY: django-db-shell-default -django-db-shell-default: - python manage.py dbshell - -.PHONY: django-dockerfile-default -django-dockerfile-default: - @echo "$$DJANGO_DOCKERFILE" > Dockerfile - -$(GIT_ADD) Dockerfile - @echo "$$DJANGO_DOCKERCOMPOSE" > docker-compose.yml - -$(GIT_ADD) docker-compose.yml - -.PHONY: django-favicon-default -django-favicon-default: - @echo "$$DJANGO_FAVICON_TEMPLATE" > backend/templates/favicon.html - -$(GIT_ADD) backend/templates/favicon.html - -.PHONY: django-footer-template-default -django-footer-template-default: - @echo "$$DJANGO_FOOTER_TEMPLATE" > backend/templates/footer.html - -$(GIT_ADD) backend/templates/footer.html - -.PHONY: django-frontend-default -django-frontend-default: python-webpack-init - $(ADD_DIR) frontend/src/context - $(ADD_DIR) frontend/src/images - $(ADD_DIR) frontend/src/utils - @echo "$$DJANGO_FRONTEND_APP" > frontend/src/application/app.js - @echo "$$DJANGO_FRONTEND_APP_CONFIG" > frontend/src/application/config.js - @echo "$$DJANGO_FRONTEND_BABELRC" > frontend/.babelrc - @echo "$$DJANGO_FRONTEND_COMPONENT_CLOCK" > frontend/src/components/Clock.js - @echo "$$DJANGO_FRONTEND_COMPONENT_ERROR" > frontend/src/components/ErrorBoundary.js - @echo "$$DJANGO_FRONTEND_CONTEXT_INDEX" > frontend/src/context/index.js - @echo "$$DJANGO_FRONTEND_CONTEXT_USER_PROVIDER" > frontend/src/context/UserContextProvider.js - @echo "$$DJANGO_FRONTEND_COMPONENT_USER_MENU" > frontend/src/components/UserMenu.js - @echo "$$DJANGO_FRONTEND_COMPONENTS" > frontend/src/components/index.js - @echo "$$DJANGO_FRONTEND_ESLINTRC" > frontend/.eslintrc - @echo "$$DJANGO_FRONTEND_PORTAL" > frontend/src/dataComponents.js - @echo "$$DJANGO_FRONTEND_STYLES" > frontend/src/styles/index.scss - @echo "$$DJANGO_FRONTEND_THEME_BLUE" > frontend/src/styles/theme-blue.scss - @echo "$$DJANGO_FRONTEND_THEME_TOGGLER" > frontend/src/utils/themeToggler.js - # @echo "$$TINYMCE_JS" > frontend/src/utils/tinymce.js - @$(MAKE) npm-install-django - @$(MAKE) npm-install-django-dev - -$(GIT_ADD) $(DJANGO_FRONTEND_FILES) - -.PHONY: django-graph-default -django-graph-default: - python manage.py graph_models -a -o $(PROJECT_NAME).png - -.PHONY: django-header-template-default -django-header-template-default: - @echo "$$DJANGO_HEADER_TEMPLATE" > backend/templates/header.html - -$(GIT_ADD) backend/templates/header.html - -.PHONY: django-home-default -django-home-default: - python manage.py startapp home - $(ADD_DIR) home/templates - @echo "$$DJANGO_HOME_PAGE_ADMIN" > home/admin.py - @echo "$$DJANGO_HOME_PAGE_MODELS" > home/models.py - @echo "$$DJANGO_HOME_PAGE_TEMPLATE" > home/templates/home.html - @echo "$$DJANGO_HOME_PAGE_VIEWS" > home/views.py - @echo "$$DJANGO_HOME_PAGE_URLS" > home/urls.py - @echo "$$DJANGO_URLS_HOME_PAGE" >> $(DJANGO_URLS_FILE) - @echo "$$DJANGO_SETTINGS_HOME_PAGE" >> $(DJANGO_SETTINGS_BASE_FILE) - export APP_DIR="home"; $(MAKE) django-app-tests - -$(GIT_ADD) home/templates - -$(GIT_ADD) home/*.py - -$(GIT_ADD) home/migrations/*.py - -.PHONY: django-init-default -django-init-default: separator \ - db-init \ - django-install \ - django-project \ - django-utils \ - pip-freeze \ - pip-init-test \ - django-settings-directory \ - django-custom-admin \ - django-dockerfile \ - django-offcanvas-template \ - django-header-template \ - django-footer-template \ - django-base-template \ - django-manage-py \ - django-urls \ - django-urls-debug-toolbar \ - django-allauth \ - django-favicon \ - git-ignore \ - django-settings-base \ - django-settings-dev \ - django-settings-prod \ - django-siteuser \ - django-home \ - django-rest-serializers \ - django-rest-views \ - django-urls-api \ - django-frontend \ - django-migrate \ - django-su - -.PHONY: django-init-minimal-default -django-init-minimal-default: separator \ - db-init \ - django-install-minimal \ - django-project \ - django-settings-directory \ - django-settings-base-minimal \ - django-settings-dev \ - pip-freeze \ - pip-init-test \ - django-custom-admin \ - django-dockerfile \ - django-offcanvas-template \ - django-header-template \ - django-footer-template \ - django-base-template \ - django-manage-py \ - django-urls \ - django-urls-debug-toolbar \ - django-favicon \ - django-settings-prod \ - django-home \ - django-utils \ - django-frontend \ - django-migrate \ - git-ignore \ - django-su - -.PHONY: django-init-wagtail-default -django-init-wagtail-default: separator \ - db-init \ - django-install \ - wagtail-install \ - wagtail-project \ - django-utils \ - pip-freeze \ - pip-init-test \ - django-custom-admin \ - django-dockerfile \ - django-offcanvas-template \ - wagtail-header-prefix-template \ - django-header-template \ - wagtail-base-template \ - django-footer-template \ - django-manage-py \ - wagtail-home \ - wagtail-urls \ - django-urls-debug-toolbar \ - django-allauth \ - django-favicon \ - git-ignore \ - wagtail-search \ - django-settings-base \ - django-settings-dev \ - django-settings-prod \ - wagtail-settings \ - django-siteuser \ - django-model-form-demo \ - django-logging-demo \ - django-payments-demo-default \ - django-rest-serializers \ - django-rest-views \ - django-urls-api \ - wagtail-urls-home \ - django-frontend \ - django-migrate \ - django-su - -.PHONY: django-install-default -django-install-default: - $(PIP_ENSURE) - python -m pip install \ - Django \ - Faker \ - boto3 \ - crispy-bootstrap5 \ - djangorestframework \ - django-allauth \ - django-after-response \ - django-ckeditor \ - django-colorful \ - django-cors-headers \ - django-countries \ - django-crispy-forms \ - django-debug-toolbar \ - django-extensions \ - django-hijack \ - django-honeypot \ - django-imagekit \ - django-import-export \ - django-ipware \ - django-multiselectfield \ - django-ninja \ - django-phonenumber-field \ - django-recurrence \ - django-recaptcha \ - django-registration \ - django-richtextfield \ - django-sendgrid-v5 \ - django-social-share \ - django-sql-explorer \ - django-storages \ - django-tables2 \ - django-timezone-field \ - django-widget-tweaks \ - dj-database-url \ - dj-rest-auth \ - dj-stripe \ - docutils \ - enmerkar \ - gunicorn \ - html2docx \ - icalendar \ - mailchimp-marketing \ - mailchimp-transactional \ - phonenumbers \ - pipdeptree \ - psycopg2-binary \ - pydotplus \ - python-webpack-boilerplate \ - python-docx \ - reportlab \ - texttable - -.PHONY: django-install-minimal-default -django-install-minimal-default: - $(PIP_ENSURE) - python -m pip install \ - Django \ - dj-database-url \ - django-debug-toolbar \ - python-webpack-boilerplate - -.PHONY: django-lint-default -django-lint-default: - -ruff format -v - -djlint --reformat --format-css --format-js . - -ruff check -v --fix - -.PHONY: django-loaddata-default -django-loaddata-default: - python manage.py loaddata - -.PHONY: django-logging-demo-default -django-logging-demo-default: - python manage.py startapp logging_demo - @echo "$$DJANGO_LOGGING_DEMO_ADMIN" > logging_demo/admin.py - @echo "$$DJANGO_LOGGING_DEMO_MODELS" > logging_demo/models.py - @echo "$$DJANGO_LOGGING_DEMO_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_LOGGING_DEMO_URLS" > logging_demo/urls.py - @echo "$$DJANGO_LOGGING_DEMO_VIEWS" > logging_demo/views.py - @echo "$$DJANGO_URLS_LOGGING_DEMO" >> $(DJANGO_URLS_FILE) - export APP_DIR="logging_demo"; $(MAKE) django-app-tests - -$(GIT_ADD) logging_demo/*.py - -$(GIT_ADD) logging_demo/migrations/*.py - -.PHONY: django-manage-py-default -django-manage-py-default: - @echo "$$DJANGO_MANAGE_PY" > manage.py - -$(GIT_ADD) manage.py - -.PHONY: django-migrate-default -django-migrate-default: - python manage.py migrate - -.PHONY: django-migrations-make-default -django-migrations-make-default: - python manage.py makemigrations - -.PHONY: django-migrations-show-default -django-migrations-show-default: - python manage.py showmigrations - -.PHONY: django-model-form-demo-default -django-model-form-demo-default: - python manage.py startapp model_form_demo - @echo "$$DJANGO_MODEL_FORM_DEMO_ADMIN" > model_form_demo/admin.py - @echo "$$DJANGO_MODEL_FORM_DEMO_FORMS" > model_form_demo/forms.py - @echo "$$DJANGO_MODEL_FORM_DEMO_MODEL" > model_form_demo/models.py - @echo "$$DJANGO_MODEL_FORM_DEMO_URLS" > model_form_demo/urls.py - @echo "$$DJANGO_MODEL_FORM_DEMO_VIEWS" > model_form_demo/views.py - $(ADD_DIR) model_form_demo/templates - @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_DETAIL" > model_form_demo/templates/model_form_demo_detail.html - @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_FORM" > model_form_demo/templates/model_form_demo_form.html - @echo "$$DJANGO_MODEL_FORM_DEMO_TEMPLATE_LIST" > model_form_demo/templates/model_form_demo_list.html - @echo "$$DJANGO_SETTINGS_MODEL_FORM_DEMO" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_URLS_MODEL_FORM_DEMO" >> $(DJANGO_URLS_FILE) - export APP_DIR="model_form_demo"; $(MAKE) django-app-tests - python manage.py makemigrations - -$(GIT_ADD) model_form_demo/*.py - -$(GIT_ADD) model_form_demo/templates - -$(GIT_ADD) model_form_demo/migrations - -.PHONY: django-offcanvas-template-default -django-offcanvas-template-default: - -$(ADD_DIR) backend/templates - @echo "$$DJANGO_FRONTEND_OFFCANVAS_TEMPLATE" > backend/templates/offcanvas.html - -$(GIT_ADD) backend/templates/offcanvas.html - -.PHONY: django-open-default -django-open-default: -ifeq ($(UNAME), Linux) - @echo "Opening on Linux." - xdg-open http://0.0.0.0:8000 -else ifeq ($(UNAME), Darwin) - @echo "Opening on macOS (Darwin)." - open http://0.0.0.0:8000 -else - @echo "Unable to open on: $(UNAME)" -endif - -.PHONY: django-payments-demo-default -django-payments-demo-default: - python manage.py startapp payments - @echo "$$DJANGO_PAYMENTS_FORM" > payments/forms.py - @echo "$$DJANGO_PAYMENTS_MODELS" > payments/models.py - @echo "$$DJANGO_PAYMENTS_ADMIN" > payments/admin.py - @echo "$$DJANGO_PAYMENTS_VIEW" > payments/views.py - @echo "$$DJANGO_PAYMENTS_URLS" > payments/urls.py - $(ADD_DIR) payments/templates/payments - $(ADD_DIR) payments/management/commands - @echo "$$DJANGO_PAYMENTS_TEMPLATE_CANCEL" > payments/templates/payments/cancel.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_CHECKOUT" > payments/templates/payments/checkout.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_SUCCESS" > payments/templates/payments/success.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_LIST" > payments/templates/payments/product_list.html - @echo "$$DJANGO_PAYMENTS_TEMPLATE_PRODUCT_DETAIL" > payments/templates/payments/product_detail.html - @echo "$$DJANGO_SETTINGS_PAYMENTS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_URLS_PAYMENTS" >> $(DJANGO_URLS_FILE) - export APP_DIR="payments"; $(MAKE) django-app-tests - python manage.py makemigrations payments - @echo "$$DJANGO_PAYMENTS_MIGRATION_0002" > payments/migrations/0002_set_stripe_api_keys.py - @echo "$$DJANGO_PAYMENTS_MIGRATION_0003" > payments/migrations/0003_create_initial_products.py - -$(GIT_ADD) payments/ - -.PHONY: django-project-default -django-project-default: - django-admin startproject backend . - -$(GIT_ADD) backend - -.PHONY: django-rest-serializers-default -django-rest-serializers-default: - @echo "$$DJANGO_API_SERIALIZERS" > backend/serializers.py - -$(GIT_ADD) backend/serializers.py - -.PHONY: django-rest-views-default -django-rest-views-default: - @echo "$$DJANGO_API_VIEWS" > backend/api.py - -$(GIT_ADD) backend/api.py - -.PHONY: django-search-default -django-search-default: - python manage.py startapp search - $(ADD_DIR) search/templates - @echo "$$DJANGO_SEARCH_TEMPLATE" > search/templates/search.html - @echo "$$DJANGO_SEARCH_FORMS" > search/forms.py - @echo "$$DJANGO_SEARCH_URLS" > search/urls.py - @echo "$$DJANGO_SEARCH_UTILS" > search/utils.py - @echo "$$DJANGO_SEARCH_VIEWS" > search/views.py - @echo "$$DJANGO_SEARCH_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "INSTALLED_APPS.append('search')" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "urlpatterns += [path('search/', include('search.urls'))]" >> $(DJANGO_URLS_FILE) - -$(GIT_ADD) search/templates - -$(GIT_ADD) search/*.py - -.PHONY: django-secret-key-default -django-secret-key-default: - @python -c "from secrets import token_urlsafe; print(token_urlsafe(50))" - -.PHONY: django-serve-default -django-serve-default: - npm run watch & - python manage.py runserver 0.0.0.0:8000 - -.PHONY: django-settings-base-default -django-settings-base-default: - @echo "$$DJANGO_SETTINGS_BASE" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_AUTHENTICATION_BACKENDS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_REST_FRAMEWORK" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_THEMES" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_DATABASE" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_INSTALLED_APPS" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_MIDDLEWARE" >> $(DJANGO_SETTINGS_BASE_FILE) - @echo "$$DJANGO_SETTINGS_CRISPY_FORMS" >> $(DJANGO_SETTINGS_BASE_FILE) - -.PHONY: django-settings-base-minimal-default -django-settings-base-minimal-default: - @echo "$$DJANGO_SETTINGS_BASE_MINIMAL" >> $(DJANGO_SETTINGS_BASE_FILE) - -.PHONY: django-settings-dev-default -django-settings-dev-default: - @echo "# $(PROJECT_NAME)" > $(DJANGO_SETTINGS_DEV_FILE) - @echo "$$DJANGO_SETTINGS_DEV" >> backend/settings/dev.py - -$(GIT_ADD) $(DJANGO_SETTINGS_DEV_FILE) - -.PHONY: django-settings-directory-default -django-settings-directory-default: - @$(ADD_DIR) $(DJANGO_SETTINGS_DIR) - @$(COPY_FILE) backend/settings.py backend/settings/base.py - @$(DEL_FILE) backend/settings.py - -$(GIT_ADD) backend/settings/*.py - -.PHONY: django-settings-prod-default -django-settings-prod-default: - @echo "$$DJANGO_SETTINGS_PROD" > $(DJANGO_SETTINGS_PROD_FILE) - -$(GIT_ADD) $(DJANGO_SETTINGS_PROD_FILE) - -.PHONY: django-shell-default -django-shell-default: - python manage.py shell - -.PHONY: django-siteuser-default -django-siteuser-default: - python manage.py startapp siteuser - $(ADD_DIR) siteuser/templates/ - @echo "$$DJANGO_SITEUSER_FORM" > siteuser/forms.py - @echo "$$DJANGO_SITEUSER_MODEL" > siteuser/models.py - @echo "$$DJANGO_SITEUSER_ADMIN" > siteuser/admin.py - @echo "$$DJANGO_SITEUSER_VIEW" > siteuser/views.py - @echo "$$DJANGO_SITEUSER_URLS" > siteuser/urls.py - @echo "$$DJANGO_SITEUSER_VIEW_TEMPLATE" > siteuser/templates/profile.html - @echo "$$DJANGO_SITEUSER_TEMPLATE" > siteuser/templates/user.html - @echo "$$DJANGO_SITEUSER_EDIT_TEMPLATE" > siteuser/templates/user_edit.html - @echo "$$DJANGO_URLS_SITEUSER" >> $(DJANGO_URLS_FILE) - @echo "$$DJANGO_SETTINGS_SITEUSER" >> $(DJANGO_SETTINGS_BASE_FILE) - export APP_DIR="siteuser"; $(MAKE) django-app-tests - -$(GIT_ADD) siteuser/templates - -$(GIT_ADD) siteuser/*.py - python manage.py makemigrations siteuser - -$(GIT_ADD) siteuser/migrations/*.py - -.PHONY: django-static-default -django-static-default: - python manage.py collectstatic --noinput - -.PHONY: django-su-default -django-su-default: - DJANGO_SUPERUSER_PASSWORD=admin python manage.py createsuperuser --noinput --username=admin --email=$(PROJECT_EMAIL) - -.PHONY: django-test-default -django-test-default: npm-install django-static - -$(MAKE) pip-install-test - python manage.py test - -.PHONY: django-urls-api-default -django-urls-api-default: - @echo "$$DJANGO_URLS_API" >> $(DJANGO_URLS_FILE) - -$(GIT_ADD) $(DJANGO_URLS_FILE) - -.PHONY: django-urls-debug-toolbar-default -django-urls-debug-toolbar-default: - @echo "$$DJANGO_URLS_DEBUG_TOOLBAR" >> $(DJANGO_URLS_FILE) - -.PHONY: django-urls-default -django-urls-default: - @echo "$$DJANGO_URLS" > $(DJANGO_URLS_FILE) - -$(GIT_ADD) $(DJANGO_URLS_FILE) - -.PHONY: django-urls-show-default -django-urls-show-default: - python manage.py show_urls - -.PHONY: django-user-default -django-user-default: - python manage.py shell -c "from django.contrib.auth.models import User; \ - User.objects.create_user('user', '', 'user')" - -.PHONY: django-utils-default -django-utils-default: - @echo "$$DJANGO_UTILS" > backend/utils.py - -$(GIT_ADD) backend/utils.py - -.PHONY: docker-build-default -docker-build-default: - podman build -t $(PROJECT_NAME) . - -.PHONY: docker-compose-default -docker-compose-default: - podman compose up - -.PHONY: docker-list-default -docker-list-default: - podman container list --all - podman images --all - -.PHONY: docker-run-default -docker-run-default: - podman run $(PROJECT_NAME) - -.PHONY: docker-serve-default -docker-serve-default: - podman run -p 8000:8000 $(PROJECT_NAME) - -.PHONY: docker-shell-default -docker-shell-default: - podman run -it $(PROJECT_NAME) /bin/bash - -.PHONY: eb-check-env-default -eb-check-env-default: # https://stackoverflow.com/a/4731504/185820 -ifndef EB_SSH_KEY - $(error EB_SSH_KEY is undefined) -endif -ifndef VPC_ID - $(error VPC_ID is undefined) -endif -ifndef VPC_SG - $(error VPC_SG is undefined) -endif -ifndef VPC_SUBNET_EC2 - $(error VPC_SUBNET_EC2 is undefined) -endif -ifndef VPC_SUBNET_ELB - $(error VPC_SUBNET_ELB is undefined) -endif - -.PHONY: eb-create-default -eb-create-default: aws-check-env eb-check-env - eb create $(EB_ENV_NAME) \ - -im $(EC2_INSTANCE_MIN) \ - -ix $(EC2_INSTANCE_MAX) \ - -ip $(EC2_INSTANCE_PROFILE) \ - -i $(EC2_INSTANCE_TYPE) \ - -k $(EB_SSH_KEY) \ - -p $(EB_PLATFORM) \ - --elb-type $(EC2_LB_TYPE) \ - --vpc \ - --vpc.id $(VPC_ID) \ - --vpc.elbpublic \ - --vpc.publicip \ - --vpc.ec2subnets $(VPC_SUBNET_EC2) \ - --vpc.elbsubnets $(VPC_SUBNET_ELB) \ - --vpc.securitygroups $(VPC_SG) - -.PHONY: eb-custom-env-default -eb-custom-env-default: - $(ADD_DIR) .ebextensions - @echo "$$EB_CUSTOM_ENV_EC2_USER" > .ebextensions/bash.config - -$(GIT_ADD) .ebextensions/bash.config - $(ADD_DIR) .platform/hooks/postdeploy - @echo "$$EB_CUSTOM_ENV_VAR_FILE" > .platform/hooks/postdeploy/setenv.sh - -$(GIT_ADD) .platform/hooks/postdeploy/setenv.sh - -.PHONY: eb-deploy-default -eb-deploy-default: - eb deploy - -.PHONY: eb-export-default -eb-export-default: - @if [ ! -d $(EB_DIR_NAME) ]; then \ - echo "Directory $(EB_DIR_NAME) does not exist"; \ - else \ - echo "Directory $(EB_DIR_NAME) does exist!"; \ - eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); pg_dump -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" > $(DJANGO_DB_NAME).sql; \ - echo "Wrote $(DJANGO_DB_NAME).sql"; \ - fi - -.PHONY: eb-restart-default -eb-restart-default: - eb ssh -c "systemctl restart web" - -.PHONY: eb-rebuild-default -eb-rebuild-default: - aws elasticbeanstalk rebuild-environment --environment-name $(ENV_NAME) - -.PHONY: eb-upgrade-default -eb-upgrade-default: - eb upgrade - -.PHONY: eb-init-default -eb-init-default: aws-check-env-profile - eb init --profile=$(AWS_PROFILE) - -.PHONY: eb-list-default -eb-list-platforms-default: - aws elasticbeanstalk list-platform-versions - -.PHONY: eb-list-databases-default -eb-list-databases-default: - @eb ssh --quiet -c "export PGPASSWORD=$(DJANGO_DB_PASS); psql -l -U $(DJANGO_DB_USER) -h $(DJANGO_DB_HOST) $(DJANGO_DB_NAME)" - -.PHONY: eb-logs-default -eb-logs-default: - eb logs - -.PHONY: eb-print-env-default -eb-print-env-default: - eb printenv - -.PHONY: favicon-default -favicon-init-default: - dd if=/dev/urandom bs=64 count=1 status=none | base64 | convert -size 16x16 -depth 8 -background none -fill white label:@- favicon.png - convert favicon.png favicon.ico - -$(GIT_ADD) favicon.ico - $(DEL_FILE) favicon.png - -.PHONY: git-ignore-default -git-ignore-default: - @echo "$$GIT_IGNORE" > .gitignore - -$(GIT_ADD) .gitignore - -.PHONY: git-branches-default -git-branches-default: - -for i in $(GIT_BRANCHES) ; do \ - -@$(GIT_CHECKOUT) -t $$i ; done - -.PHONY: git-commit-message-clean-default -git-commit-message-clean-default: - -@$(GIT_COMMIT) -a -m "Clean" - -.PHONY: git-commit-message-default -git-commit-message-default: - -@$(GIT_COMMIT) -a -m $(GIT_COMMIT_MSG) - -.PHONY: git-commit-message-empty-default -git-commit-message-empty-default: - -@$(GIT_COMMIT) --allow-empty -m "Empty-Commit" - -.PHONY: git-commit-message-init-default -git-commit-message-init-default: - -@$(GIT_COMMIT) -a -m "Init" - -.PHONY: git-commit-message-last-default -git-commit-message-last-default: - git log -1 --pretty=%B > $(TMPDIR)/commit.txt - -$(GIT_COMMIT) -a -F $(TMPDIR)/commit.txt - -.PHONY: git-commit-message-lint-default -git-commit-message-lint-default: - -@$(GIT_COMMIT) -a -m "Lint" - -.PHONY: git-commit-message-mk-default -git-commit-message-mk-default: - -@$(GIT_COMMIT) project.mk -m "Add/update $(MAKEFILE_CUSTOM_FILE)" - -.PHONY: git-commit-message-rename-default -git-commit-message-rename-default: - -@$(GIT_COMMIT) -a -m "Rename" - -.PHONY: git-commit-message-sort-default -git-commit-message-sort-default: - -@$(GIT_COMMIT) -a -m "Sort" - -.PHONY: git-push-default -git-push-default: - -@$(GIT_PUSH) - -.PHONY: git-push-force-default -git-push-force-default: - -@$(GIT_PUSH_FORCE) - -.PHONY: git-commit-edit-default -git-commit-edit-default: - -$(GIT_COMMIT) -a - -.PHONY: git-prune-default -git-prune-default: - git remote update origin --prune - -.PHONY: git-set-upstream-default -git-set-upstream-default: - git push --set-upstream origin main - -.PHONY: git-set-default-default -git-set-default-default: - gh repo set-default - -.PHONY: git-short-default -git-short-default: - @echo $(GIT_REV) - -.PHONY: help-default -help-default: - @echo "Project Makefile 🤷" - @echo "Usage: make [options] [target] ..." - @echo "Examples:" - @echo " make help Print this message" - @echo " make list-defines list all defines in the Makefile" - @echo " make list-commands list all targets in the Makefile" - -.PHONY: jenkins-init-default -jenkins-init-default: - @echo "$$JENKINS_FILE" > Jenkinsfile - -.PHONY: makefile-list-commands-default -makefile-list-commands-default: - @for makefile in $(MAKEFILE_LIST); do \ - echo "Commands from $$makefile:"; \ - $(MAKE) -pRrq -f $$makefile : 2>/dev/null | \ - awk -v RS= -F: '/^# File/,/^# Finished Make data base/ { \ - if ($$1 !~ "^[#.]") { sub(/-default$$/, "", $$1); print $$1 } }' | \ - egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | \ - tr ' ' '\n' | \ - sort | \ - awk '{print $$0}' ; \ - echo; \ - done | $(PAGER) - -.PHONY: makefile-list-defines-default -makefile-list-defines-default: - @grep '^define [A-Za-z_][A-Za-z0-9_]*' Makefile - -.PHONY: makefile-list-exports-default -makefile-list-exports-default: - @grep '^export [A-Z][A-Z_]*' Makefile - -.PHONY: makefile-list-targets-default -makefile-list-targets-default: - @perl -ne 'print if /^\s*\.PHONY:/ .. /^[a-zA-Z0-9_-]+:/;' Makefile | grep -v .PHONY - -.PHONY: make-default -make-default: - -$(GIT_ADD) Makefile - -$(GIT_COMMIT) Makefile -m "Add/update project-makefile files" - -git push - -.PHONY: npm-init-default -npm-init-default: - npm init -y - -$(GIT_ADD) package.json - -$(GIT_ADD) package-lock.json - -.PHONY: npm-build-default -npm-build-default: - npm run build - -.PHONY: npm-install-default -npm-install-default: - npm install - -$(GIT_ADD) package-lock.json - -.PHONY: npm-install-django-default -npm-install-django-default: - npm install \ - @fortawesome/fontawesome-free \ - @fortawesome/fontawesome-svg-core \ - @fortawesome/free-brands-svg-icons \ - @fortawesome/free-solid-svg-icons \ - @fortawesome/react-fontawesome \ - bootstrap \ - camelize \ - date-fns \ - history \ - mapbox-gl \ - query-string \ - react-animate-height \ - react-chartjs-2 \ - react-copy-to-clipboard \ - react-date-range \ - react-dom \ - react-dropzone \ - react-hook-form \ - react-image-crop \ - react-map-gl \ - react-modal \ - react-resize-detector \ - react-select \ - react-swipeable \ - snakeize \ - striptags \ - url-join \ - viewport-mercator-project - -.PHONY: npm-install-django-dev-default -npm-install-django-dev-default: - npm install \ - eslint-plugin-react \ - eslint-config-standard \ - eslint-config-standard-jsx \ - @babel/core \ - @babel/preset-env \ - @babel/preset-react \ - --save-dev - -.PHONY: npm-serve-default -npm-serve-default: - npm run start - -.PHONY: npm-test-default -npm-test-default: - npm run test - -.PHONY: pip-deps-default -pip-deps-default: - $(PIP_ENSURE) - python -m pip install pipdeptree - python -m pipdeptree - pipdeptree - -.PHONY: pip-freeze-default -pip-freeze-default: - $(PIP_ENSURE) - python -m pip freeze | sort > $(TMPDIR)/requirements.txt - mv -f $(TMPDIR)/requirements.txt . - -$(GIT_ADD) requirements.txt - -.PHONY: pip-init-default -pip-init-default: - touch requirements.txt - -$(GIT_ADD) requirements.txt - -.PHONY: pip-init-test-default -pip-init-test-default: - @echo "$$PIP_INSTALL_REQUIREMENTS_TEST" > requirements-test.txt - -$(GIT_ADD) requirements-test.txt - -.PHONY: pip-install-default -pip-install-default: - $(PIP_ENSURE) - $(MAKE) pip-upgrade - python -m pip install wheel - python -m pip install -r requirements.txt - -.PHONY: pip-install-dev-default -pip-install-dev-default: - $(PIP_ENSURE) - python -m pip install -r requirements-dev.txt - -.PHONY: pip-install-test-default -pip-install-test-default: - $(PIP_ENSURE) - python -m pip install -r requirements-test.txt - -.PHONY: pip-install-upgrade-default -pip-install-upgrade-default: - cat requirements.txt | awk -F\= '{print $$1}' > $(TMPDIR)/requirements.txt - mv -f $(TMPDIR)/requirements.txt . - $(PIP_ENSURE) - python -m pip install -U -r requirements.txt - python -m pip freeze | sort > $(TMPDIR)/requirements.txt - mv -f $(TMPDIR)/requirements.txt . - -.PHONY: pip-upgrade-default -pip-upgrade-default: - $(PIP_ENSURE) - python -m pip install -U pip - -.PHONY: pip-uninstall-default -pip-uninstall-default: - $(PIP_ENSURE) - python -m pip freeze | xargs python -m pip uninstall -y - -.PHONY: plone-clean-default -plone-clean-default: - $(DEL_DIR) $(PROJECT_NAME) - $(DEL_DIR) $(PACKAGE_NAME) - -.PHONY: plone-init-default -plone-init-default: git-ignore plone-install plone-instance plone-serve - -.PHONY: plone-install-default -plone-install-default: - $(PIP_ENSURE) - python -m pip install plone -c $(PIP_INSTALL_PLONE_CONSTRAINTS) - -.PHONY: plone-instance-default -plone-instance-default: - mkwsgiinstance -d backend -u admin:admin - cat backend/etc/zope.ini | sed -e 's/host = 127.0.0.1/host = 0.0.0.0/; s/port = 8080/port = 8000/' > $(TMPDIR)/zope.ini - mv -f $(TMPDIR)/zope.ini backend/etc/zope.ini - -$(GIT_ADD) backend/etc/site.zcml - -$(GIT_ADD) backend/etc/zope.conf - -$(GIT_ADD) backend/etc/zope.ini - -.PHONY: plone-serve-default -plone-serve-default: - runwsgi backend/etc/zope.ini - -.PHONY: plone-build-default -plone-build-default: - buildout - -.PHONY: programming-interview-default -programming-interview-default: - @echo "$$PROGRAMMING_INTERVIEW" > interview.py - @echo "Created interview.py!" - -@$(GIT_ADD) interview.py > /dev/null 2>&1 - -# .NOT_PHONY! -$(MAKEFILE_CUSTOM_FILE): - @echo "$$MAKEFILE_CUSTOM" > $(MAKEFILE_CUSTOM_FILE) - -$(GIT_ADD) $(MAKEFILE_CUSTOM_FILE) - -.PHONY: python-license-default -python-license-default: - @echo "$(PYTHON_LICENSE_TXT)" > LICENSE.txt - -$(GIT_ADD) LICENSE.txt - -.PHONY: python-project-default -python-project-default: - @echo "$(PYTHON_PROJECT_TOML)" > pyproject.toml - -$(GIT_ADD) pyproject.toml - -.PHONY: python-serve-default -python-serve-default: - @echo "\n\tServing HTTP on http://0.0.0.0:8000\n" - python3 -m http.server - -.PHONY: python-sdist-default -python-sdist-default: - $(PIP_ENSURE) - python setup.py sdist --format=zip - -.PHONY: python-webpack-init-default -python-webpack-init-default: - python manage.py webpack_init --no-input - -.PHONY: python-ci-default -python-ci-default: - $(ADD_DIR) .github/workflows - @echo "$(PYTHON_CI_YAML)" > .github/workflows/build_wheels.yml - -$(GIT_ADD) .github/workflows/build_wheels.yml - -.PHONY: rand-default -rand-default: - @openssl rand -base64 12 | sed 's/\///g' - -.PHONY: readme-init-default -readme-init-default: - @echo "# $(PROJECT_NAME)" > README.md - -$(GIT_ADD) README.md - -.PHONY: readme-edit-default -readme-edit-default: - $(EDITOR) README.md - -.PHONY: reveal-init-default -reveal-init-default: webpack-init-reveal - npm install \ - css-loader \ - mini-css-extract-plugin \ - reveal.js \ - style-loader - jq '.scripts += {"build": "webpack"}' package.json > \ - $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json - jq '.scripts += {"start": "webpack serve --mode development --port 8000 --static"}' package.json > \ - $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json - jq '.scripts += {"watch": "webpack watch --mode development"}' package.json > \ - $(TMPDIR)/tmp.json && mv $(TMPDIR)/tmp.json package.json - -.PHONY: reveal-serve-default -reveal-serve-default: - npm run watch & - python -m http.server - -.PHONY: review-default -review-default: -ifeq ($(UNAME), Darwin) - $(EDITOR_REVIEW) `find backend/ -name \*.py` `find backend/ -name \*.html` `find frontend/ -name \*.js` `find frontend/ -name \*.js` -else - @echo "Unsupported" -endif - -.PHONY: separator-default -separator-default: - @echo "$$SEPARATOR" - -.PHONY: sphinx-init-default -sphinx-init-default: sphinx-install - sphinx-quickstart -q -p $(PROJECT_NAME) -a $(USER) -v 0.0.1 $(RANDIR) - $(COPY_DIR) $(RANDIR)/* . - $(DEL_DIR) $(RANDIR) - -$(GIT_ADD) index.rst - -$(GIT_ADD) conf.py - $(DEL_FILE) make.bat - -@$(GIT_CHECKOUT) Makefile - $(MAKE) git-ignore - -.PHONY: sphinx-theme-init-default -sphinx-theme-init-default: - export DJANGO_FRONTEND_THEME_NAME=$(PROJECT_NAME)_theme; \ - $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/__init__.py ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/theme.conf ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/layout.html ; \ - $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/css ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/css/style.css ; \ - $(ADD_DIR) $$DJANGO_FRONTEND_THEME_NAME/static/js ; \ - $(ADD_FILE) $$DJANGO_FRONTEND_THEME_NAME/static/js/script.js ; \ - -$(GIT_ADD) $$DJANGO_FRONTEND_THEME_NAME/static - -.PHONY: sphinx-install-default -sphinx-install-default: - echo "Sphinx\n" > requirements.txt - @$(MAKE) pip-install - @$(MAKE) pip-freeze - -$(GIT_ADD) requirements.txt - -.PHONY: sphinx-build-default -sphinx-build-default: - sphinx-build -b html -d _build/doctrees . _build/html - sphinx-build -b rinoh . _build/rinoh - -.PHONY: sphinx-serve-default -sphinx-serve-default: - cd _build/html;python3 -m http.server - -.PHONY: wagtail-base-template-default -wagtail-base-template-default: - @echo "$$WAGTAIL_BASE_TEMPLATE" > backend/templates/base.html - -.PHONY: wagtail-clean-default -wagtail-clean-default: - -@for dir in $(shell echo "$(WAGTAIL_CLEAN_DIRS)"); do \ - echo "Cleaning $$dir"; \ - $(DEL_DIR) $$dir >/dev/null 2>&1; \ - done - -@for file in $(shell echo "$(WAGTAIL_CLEAN_FILES)"); do \ - echo "Cleaning $$file"; \ - $(DEL_FILE) $$file >/dev/null 2>&1; \ - done - -.PHONY: wagtail-contactpage-default -wagtail-contactpage-default: - python manage.py startapp contactpage - @echo "$$WAGTAIL_CONTACT_PAGE_MODEL" > contactpage/models.py - @echo "$$WAGTAIL_CONTACT_PAGE_TEST" > contactpage/tests.py - $(ADD_DIR) contactpage/templates/contactpage/ - @echo "$$WAGTAIL_CONTACT_PAGE_TEMPLATE" > contactpage/templates/contactpage/contact_page.html - @echo "$$WAGTAIL_CONTACT_PAGE_LANDING" > contactpage/templates/contactpage/contact_page_landing.html - @echo "INSTALLED_APPS.append('contactpage')" >> $(DJANGO_SETTINGS_BASE_FILE) - python manage.py makemigrations contactpage - -$(GIT_ADD) contactpage/templates - -$(GIT_ADD) contactpage/*.py - -$(GIT_ADD) contactpage/migrations/*.py - -.PHONY: wagtail-header-prefix-template-default -wagtail-header-prefix-template-default: - @echo "$$WAGTAIL_HEADER_PREFIX" > backend/templates/header.html - -.PHONY: wagtail-home-default -wagtail-home-default: - @echo "$$WAGTAIL_HOME_PAGE_MODEL" > home/models.py - @echo "$$WAGTAIL_HOME_PAGE_TEMPLATE" > home/templates/home/home_page.html - $(ADD_DIR) home/templates/blocks - @echo "$$WAGTAIL_BLOCK_MARKETING" > home/templates/blocks/marketing_block.html - @echo "$$WAGTAIL_BLOCK_CAROUSEL" > home/templates/blocks/carousel_block.html - -$(GIT_ADD) home/templates - -$(GIT_ADD) home/*.py - python manage.py makemigrations home - -$(GIT_ADD) home/migrations/*.py - -.PHONY: wagtail-install-default -wagtail-install-default: - $(PIP_ENSURE) - python -m pip install \ - wagtail \ - wagtailmenus \ - wagtail-color-panel \ - wagtail-django-recaptcha \ - wagtail-markdown \ - wagtail-modeladmin \ - wagtail-seo \ - weasyprint \ - whitenoise \ - xhtml2pdf - -.PHONY: wagtail-private-default -wagtail-privacy-default: - python manage.py startapp privacy - @echo "$$WAGTAIL_PRIVACY_PAGE_MODEL" > privacy/models.py - $(ADD_DIR) privacy/templates - @echo "$$WAGTAIL_PRIVACY_PAGE_TEMPLATE" > privacy/templates/privacy_page.html - @echo "INSTALLED_APPS.append('privacy')" >> $(DJANGO_SETTINGS_BASE_FILE) - python manage.py makemigrations privacy - -$(GIT_ADD) privacy/templates - -$(GIT_ADD) privacy/*.py - -$(GIT_ADD) privacy/migrations/*.py - -.PHONY: wagtail-project-default -wagtail-project-default: - wagtail start backend . - $(DEL_FILE) home/templates/home/welcome_page.html - -$(GIT_ADD) backend/ - -$(GIT_ADD) .dockerignore - -$(GIT_ADD) Dockerfile - -$(GIT_ADD) manage.py - -$(GIT_ADD) requirements.txt - -.PHONY: wagtail-search-default -wagtail-search-default: - @echo "$$WAGTAIL_SEARCH_TEMPLATE" > search/templates/search/search.html - @echo "$$WAGTAIL_SEARCH_URLS" > search/urls.py - -$(GIT_ADD) search/templates - -$(GIT_ADD) search/*.py - -.PHONY: wagtail-settings-default -wagtail-settings-default: - @echo "$$WAGTAIL_SETTINGS" >> $(DJANGO_SETTINGS_BASE_FILE) - -.PHONY: wagtail-sitepage-default -wagtail-sitepage-default: - python manage.py startapp sitepage - @echo "$$WAGTAIL_SITEPAGE_MODEL" > sitepage/models.py - $(ADD_DIR) sitepage/templates/sitepage/ - @echo "$$WAGTAIL_SITEPAGE_TEMPLATE" > sitepage/templates/sitepage/site_page.html - @echo "INSTALLED_APPS.append('sitepage')" >> $(DJANGO_SETTINGS_BASE_FILE) - python manage.py makemigrations sitepage - -$(GIT_ADD) sitepage/templates - -$(GIT_ADD) sitepage/*.py - -$(GIT_ADD) sitepage/migrations/*.py - -.PHONY: wagtail-urls-default -wagtail-urls-default: - @echo "$$WAGTAIL_URLS" > $(DJANGO_URLS_FILE) - -.PHONY: wagtail-urls-home-default -wagtail-urls-home-default: - @echo "$$WAGTAIL_URLS_HOME" >> $(DJANGO_URLS_FILE) - -.PHONY: webpack-init-default -webpack-init-default: npm-init - @echo "$$WEBPACK_CONFIG_JS" > webpack.config.js - -$(GIT_ADD) webpack.config.js - npm install --save-dev webpack webpack-cli webpack-dev-server - $(ADD_DIR) src/ - @echo "$$WEBPACK_INDEX_JS" > src/index.js - -$(GIT_ADD) src/index.js - @echo "$$WEBPACK_INDEX_HTML" > index.html - -$(GIT_ADD) index.html - $(MAKE) git-ignore - -.PHONY: webpack-init-reveal-default -webpack-init-reveal-default: npm-init - @echo "$$WEBPACK_REVEAL_CONFIG_JS" > webpack.config.js - -$(GIT_ADD) webpack.config.js - npm install --save-dev webpack webpack-cli webpack-dev-server - $(ADD_DIR) src/ - @echo "$$WEBPACK_REVEAL_INDEX_JS" > src/index.js - -$(GIT_ADD) src/index.js - @echo "$$WEBPACK_REVEAL_INDEX_HTML" > index.html - -$(GIT_ADD) index.html - $(MAKE) git-ignore - -# -------------------------------------------------------------------------------- -# Single-line phony target rules -# -------------------------------------------------------------------------------- - -.PHONY: aws-check-env-default -aws-check-env-default: aws-check-env-profile aws-check-env-region - -.PHONY: ce-default -ce-default: git-commit-edit git-push - -.PHONY: clean-default -clean-default: wagtail-clean - -.PHONY: cp-default -cp-default: git-commit-message git-push - -.PHONY: db-dump-default -db-dump-default: eb-export - -.PHONY: dbshell-default -dbshell-default: django-db-shell - -.PHONY: deploy-default -deploy-default: eb-deploy - -.PHONY: d-default -d-default: eb-deploy - -.PHONY: deps-default -deps-default: pip-deps - -.PHONY: e-default -e-default: edit - -.PHONY: edit-default -edit-default: readme-edit - -.PHONY: empty-default -empty-default: git-commit-message-empty git-push - -.PHONY: fp-default -fp-default: git-push-force - -.PHONY: freeze-default -freeze-default: pip-freeze git-push - -.PHONY: git-commit-default -git-commit-default: git-commit-message git-push - -.PHONY: git-commit-clean-default -git-commit-clean-default: git-commit-message-clean git-push - -.PHONY: git-commit-init-default -git-commit-init-default: git-commit-message-init git-push - -.PHONY: git-commit-lint-default -git-commit-lint-default: git-commit-message-lint git-push - -.PHONY: gitignore-default -gitignore-default: git-ignore - -.PHONY: h-default -h-default: help - -.PHONY: init-default -init-default: django-init-wagtail django-serve - -.PHONY: init-wagtail-default -init-wagtail-default: django-init-wagtail - -.PHONY: install-default -install-default: pip-install - -.PHONY: l-default -l-default: makefile-list-commands - -.PHONY: last-default -last-default: git-commit-message-last git-push - -.PHONY: lint-default -lint-default: django-lint - -.PHONY: list-commands-default -list-commands-default: makefile-list-commands - -.PHONY: list-defines-default -list-defines-default: makefile-list-defines - -.PHONY: list-exports-default -list-exports-default: makefile-list-exports - -.PHONY: list-targets-default -list-targets-default: makefile-list-targets - -.PHONY: migrate-default -migrate-default: django-migrate - -.PHONY: migrations-default -migrations-default: django-migrations-make - -.PHONY: migrations-show-default -migrations-show-default: django-migrations-show - -.PHONY: mk-default -mk-default: project.mk git-commit-message-mk git-push - -.PHONY: open-default -open-default: django-open - -.PHONY: o-default -o-default: django-open - -.PHONY: readme-default -readme-default: readme-init - -.PHONY: rename-default -rename-default: git-commit-message-rename git-push - -.PHONY: s-default -s-default: django-serve - -.PHONY: shell-default -shell-default: django-shell - -.PHONY: serve-default -serve-default: django-serve - -.PHONY: static-default -static-default: django-static - -.PHONY: sort-default -sort-default: git-commit-message-sort git-push - -.PHONY: su-default -su-default: django-su - -.PHONY: test-default -test-default: django-test - -.PHONY: t-default -t-default: django-test - -.PHONY: u-default -u-default: help - -.PHONY: urls-default -urls-default: django-urls-show - -# -------------------------------------------------------------------------------- -# Allow customizing rules defined in this Makefile with rules defined in -# $(MAKEFILE_CUSTOM_FILE) -# -------------------------------------------------------------------------------- - -%: %-default # https://stackoverflow.com/a/49804748 - @ true +.DEFAULT_GOAL := help + +.PHONY: clean +clean: + rm src/PIL/*.so || true + rm -r build || true + find . -name __pycache__ | xargs rm -r || true + +.PHONY: coverage +coverage: + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq + rm -r htmlcov || true + python3 -c "import coverage" > /dev/null 2>&1 || python3 -m pip install coverage + python3 -m coverage report + +.PHONY: doc +.PHONY: html +doc html: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs html + +.PHONY: htmlview +htmlview: + python3 -c "import PIL" > /dev/null 2>&1 || python3 -m pip install . + $(MAKE) -C docs htmlview + +.PHONY: doccheck +doccheck: + $(MAKE) doc +# Don't make our tests rely on the links in the docs being up every single build. +# We don't control them. But do check, and update them to the target of their redirects. + $(MAKE) -C docs linkcheck || true + +.PHONY: docserve +docserve: + cd docs/_build/html && python3 -m http.server 2> /dev/null& + +.PHONY: help +help: + @echo "Welcome to Pillow development. Please use \`make \` where is one of" + @echo " clean remove build products" + @echo " coverage run coverage test (in progress)" + @echo " doc make HTML docs" + @echo " docserve run an HTTP server on the docs directory" + @echo " html make HTML docs" + @echo " htmlview open the index page built by the html target in your browser" + @echo " install make and install" + @echo " install-coverage make and install with C coverage" + @echo " lint run the lint checks" + @echo " lint-fix run Ruff to (mostly) fix lint issues" + @echo " release-test run code and package tests before release" + @echo " test run tests on installed Pillow" + +.PHONY: install +install: + python3 -m pip -v install . + python3 selftest.py + +.PHONY: install-coverage +install-coverage: + CFLAGS="-coverage -Werror=implicit-function-declaration" python3 -m pip -v install . + python3 selftest.py + +.PHONY: debug +debug: +# make a debug version if we don't have a -dbg python. Leaves in symbols +# for our stuff, kills optimization, and redirects to dev null so we +# see any build failures. + make clean > /dev/null + CFLAGS='-g -O0' python3 -m pip -v install . > /dev/null + +.PHONY: release-test +release-test: + python3 Tests/check_release_notes.py + python3 -m pip install -e .[tests] + python3 selftest.py + python3 -m pytest Tests + python3 -m pip install . + python3 -m pytest -qq + python3 -m check_manifest + python3 -m pyroma . + $(MAKE) readme + +.PHONY: sdist +sdist: + python3 -m build --help > /dev/null 2>&1 || python3 -m pip install build + python3 -m build --sdist + python3 -m twine --help > /dev/null 2>&1 || python3 -m pip install twine + python3 -m twine check --strict dist/* + +.PHONY: test +test: + python3 -c "import pytest" > /dev/null 2>&1 || python3 -m pip install pytest + python3 -m pytest -qq + +.PHONY: valgrind +valgrind: + python3 -c "import pytest_valgrind" > /dev/null 2>&1 || python3 -m pip install pytest-valgrind + PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp --leak-check=no \ + --log-file=/tmp/valgrind-output \ + python3 -m pytest --no-memcheck -vv --valgrind --valgrind-log=/tmp/valgrind-output + +.PHONY: readme +readme: + python3 -c "import markdown2" > /dev/null 2>&1 || python3 -m pip install markdown2 + python3 -m markdown2 README.md > .long-description.html && open .long-description.html + + +.PHONY: lint +lint: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e lint + +.PHONY: lint-fix +lint-fix: + python3 -c "import black" > /dev/null 2>&1 || python3 -m pip install black + python3 -m black . + python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff + python3 -m ruff --fix . + +.PHONY: mypy +mypy: + python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox + python3 -m tox -e mypy From d49ea378115f4ae32e12f1f0c3e6146ebdc68fe0 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 22 Aug 2024 17:50:42 +1000 Subject: [PATCH 43/52] Include limit in error message --- Tests/test_file_webp.py | 4 +++- src/_webp.c | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index ad08da364..a86757e64 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -163,7 +163,9 @@ class TestFileWebp: im = Image.new("L", (16384, 16384)) with pytest.raises(ValueError) as e: im.save(temp_file) - assert str(e.value) == "encoding error 5: Image size exceeds WebP limit" + assert ( + str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383" + ) def test_WebPEncode_with_invalid_args(self) -> None: """ diff --git a/src/_webp.c b/src/_webp.c index 0d2d6f023..c0e1a6f63 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -673,9 +673,11 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { WebPPictureFree(&pic); if (!ok) { int error_code = (&pic)->error_code; - const char *message = ""; + char message[50] = ""; if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { - message = ": Image size exceeds WebP limit"; + sprintf( + message, ": Image size exceeds WebP limit of %d", WEBP_MAX_DIMENSION + ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); return NULL; From fed916825290cb1f45610017993c82ccca2b7843 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Aug 2024 18:48:28 +1000 Subject: [PATCH 44/52] Catch defusedxml warnings --- Tests/test_file_webp_metadata.py | 10 +++++++++- Tests/test_image.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index 4ef3d95f2..d9a834c75 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -116,7 +116,15 @@ def test_read_no_exif() -> None: def test_getxmp() -> None: with Image.open("Tests/images/flower.webp") as im: assert "xmp" not in im.info - assert im.getxmp() == {} + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + xmp = im.getxmp() + else: + xmp = im.getxmp() + assert xmp == {} with Image.open("Tests/images/flower2.webp") as im: if ElementTree is None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 719732d12..b1ecc6184 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -938,7 +938,15 @@ class TestImage: def test_empty_xmp(self) -> None: with Image.open("Tests/images/hopper.gif") as im: - assert im.getxmp() == {} + if ElementTree is None: + with pytest.warns( + UserWarning, + match="XMP data cannot be read without defusedxml dependency", + ): + xmp = im.getxmp() + else: + xmp = im.getxmp() + assert xmp == {} def test_getxmp_padded(self) -> None: im = Image.new("RGB", (1, 1)) From e382ebed3ad757567ce8522c41a6c704c68c16a1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 23 Aug 2024 19:38:07 +1000 Subject: [PATCH 45/52] Remove warning if NumPy failed to raise an error during conversion --- Tests/test_image_array.py | 2 +- src/PIL/Image.py | 24 ++++++------------------ 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index bb6064882..38425a515 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -47,7 +47,7 @@ def test_toarray() -> None: with pytest.raises(OSError): numpy.array(im_truncated) else: - with pytest.warns(UserWarning): + with pytest.warns(DeprecationWarning): numpy.array(im_truncated) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a6eefff56..2f1a878c1 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -724,24 +724,12 @@ class Image: def __array_interface__(self) -> dict[str, str | bytes | int | tuple[int, ...]]: # numpy array interface support new: dict[str, str | bytes | int | tuple[int, ...]] = {"version": 3} - try: - if self.mode == "1": - # Binary images need to be extended from bits to bytes - # See: https://github.com/python-pillow/Pillow/issues/350 - new["data"] = self.tobytes("raw", "L") - else: - new["data"] = self.tobytes() - except Exception as e: - if not isinstance(e, (MemoryError, RecursionError)): - try: - import numpy - from packaging.version import parse as parse_version - except ImportError: - pass - else: - if parse_version(numpy.__version__) < parse_version("1.23"): - warnings.warn(str(e)) - raise + if self.mode == "1": + # Binary images need to be extended from bits to bytes + # See: https://github.com/python-pillow/Pillow/issues/350 + new["data"] = self.tobytes("raw", "L") + else: + new["data"] = self.tobytes() new["shape"], new["typestr"] = _conv_type_shape(self) return new From 6e9518b88de595c56a955a20a7359ff93a7e9668 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2024 19:18:25 +1000 Subject: [PATCH 46/52] Added type hint to example code --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ffc6bec34..37d8cb335 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1917,7 +1917,7 @@ class Image: object:: class Example(Image.ImagePointHandler): - def point(self, data): + def point(self, im: Image) -> Image: # Return result :param mode: Output mode (default is same as input). This can only be used if the source image has mode "L" or "P", and the output has mode "1" or the From 8aa58e320fd2dde581325c63d1673d6a2e3648f4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2024 21:25:41 +1000 Subject: [PATCH 47/52] Rename _E to ImagePointTransform --- docs/reference/Image.rst | 2 +- src/PIL/Image.py | 48 ++++++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/docs/reference/Image.rst b/docs/reference/Image.rst index 02e714f20..bc3758218 100644 --- a/docs/reference/Image.rst +++ b/docs/reference/Image.rst @@ -362,8 +362,8 @@ Classes :undoc-members: :show-inheritance: .. autoclass:: PIL.Image.ImagePointHandler +.. autoclass:: PIL.Image.ImagePointTransform .. autoclass:: PIL.Image.ImageTransformHandler -.. autoclass:: PIL.Image._E Protocols --------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 37d8cb335..5aa49b619 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -470,43 +470,53 @@ def _getencoder( # Simple expression analyzer -class _E: +class ImagePointTransform: + """ + Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than + 8 bits, this represents an affine transformation, where the value is multiplied by + ``scale`` and ``offset`` is added. + """ + def __init__(self, scale: float, offset: float) -> None: self.scale = scale self.offset = offset - def __neg__(self) -> _E: - return _E(-self.scale, -self.offset) + def __neg__(self) -> ImagePointTransform: + return ImagePointTransform(-self.scale, -self.offset) - def __add__(self, other: _E | float) -> _E: - if isinstance(other, _E): - return _E(self.scale + other.scale, self.offset + other.offset) - return _E(self.scale, self.offset + other) + def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): + return ImagePointTransform( + self.scale + other.scale, self.offset + other.offset + ) + return ImagePointTransform(self.scale, self.offset + other) __radd__ = __add__ - def __sub__(self, other: _E | float) -> _E: + def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform: return self + -other - def __rsub__(self, other: _E | float) -> _E: + def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform: return other + -self - def __mul__(self, other: _E | float) -> _E: - if isinstance(other, _E): + def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): return NotImplemented - return _E(self.scale * other, self.offset * other) + return ImagePointTransform(self.scale * other, self.offset * other) __rmul__ = __mul__ - def __truediv__(self, other: _E | float) -> _E: - if isinstance(other, _E): + def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform: + if isinstance(other, ImagePointTransform): return NotImplemented - return _E(self.scale / other, self.offset / other) + return ImagePointTransform(self.scale / other, self.offset / other) -def _getscaleoffset(expr: Callable[[_E], _E | float]) -> tuple[float, float]: - a = expr(_E(1, 0)) - return (a.scale, a.offset) if isinstance(a, _E) else (0, a) +def _getscaleoffset( + expr: Callable[[ImagePointTransform], ImagePointTransform | float] +) -> tuple[float, float]: + a = expr(ImagePointTransform(1, 0)) + return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a) # -------------------------------------------------------------------- @@ -1898,7 +1908,7 @@ class Image: Sequence[float] | NumpyArray | Callable[[int], float] - | Callable[[_E], _E | float] + | Callable[[ImagePointTransform], ImagePointTransform | float] | ImagePointHandler ), mode: str | None = None, From 658b60e3a33d6bf4e3ff3a2053cb73faa466772d Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 24 Aug 2024 21:26:56 +1000 Subject: [PATCH 48/52] Include units Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_file_webp.py | 2 +- src/_webp.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index a86757e64..4a048f2c2 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -164,7 +164,7 @@ class TestFileWebp: with pytest.raises(ValueError) as e: im.save(temp_file) assert ( - str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383" + str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" ) def test_WebPEncode_with_invalid_args(self) -> None: diff --git a/src/_webp.c b/src/_webp.c index c0e1a6f63..b4cf9c329 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -676,7 +676,7 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { char message[50] = ""; if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { sprintf( - message, ": Image size exceeds WebP limit of %d", WEBP_MAX_DIMENSION + message, ": Image size exceeds WebP limit of %d pixels", WEBP_MAX_DIMENSION ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); From a3e3639a59ffaf33430b08b089da1eedf04286ab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 11:27:40 +0000 Subject: [PATCH 49/52] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- Tests/test_file_webp.py | 3 ++- src/_webp.c | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 4a048f2c2..247fc6021 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -164,7 +164,8 @@ class TestFileWebp: with pytest.raises(ValueError) as e: im.save(temp_file) assert ( - str(e.value) == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" + str(e.value) + == "encoding error 5: Image size exceeds WebP limit of 16383 pixels" ) def test_WebPEncode_with_invalid_args(self) -> None: diff --git a/src/_webp.c b/src/_webp.c index b4cf9c329..f59ad3036 100644 --- a/src/_webp.c +++ b/src/_webp.c @@ -676,7 +676,9 @@ WebPEncode_wrapper(PyObject *self, PyObject *args) { char message[50] = ""; if (error_code == VP8_ENC_ERROR_BAD_DIMENSION) { sprintf( - message, ": Image size exceeds WebP limit of %d pixels", WEBP_MAX_DIMENSION + message, + ": Image size exceeds WebP limit of %d pixels", + WEBP_MAX_DIMENSION ); } PyErr_Format(PyExc_ValueError, "encoding error %d%s", error_code, message); From cfb093af39cf01fc631845d941e0bfda0bb4a52f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 24 Aug 2024 23:15:35 +1000 Subject: [PATCH 50/52] Update CHANGES.rst [ci skip] --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a7edc340c..9da76d82a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,12 @@ Changelog (Pillow) 11.0.0 (unreleased) ------------------- +- Updated error message when saving WebP with invalid width or height #8322 + [radarhere, hugovk] + +- Remove warning if NumPy failed to raise an error during conversion #8326 + [radarhere] + - If left and right sides meet in ImageDraw.rounded_rectangle(), do not draw rectangle to fill gap #8304 [radarhere] From 53bbfea7637e5cc16d074a205784d0408f13de6e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 26 Aug 2024 23:49:03 +1000 Subject: [PATCH 51/52] Added type hints --- Tests/test_qt_image_qapplication.py | 42 +++++++++++++++------- Tests/test_qt_image_toqimage.py | 2 +- pyproject.toml | 1 - src/PIL/EpsImagePlugin.py | 2 +- src/PIL/Image.py | 10 +++--- src/PIL/ImageQt.py | 54 +++++++++++++++++------------ 6 files changed, 67 insertions(+), 44 deletions(-) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 28f66891c..0ed9fbfa5 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,6 +1,7 @@ from __future__ import annotations from pathlib import Path +from typing import TYPE_CHECKING, Union import pytest @@ -8,6 +9,20 @@ from PIL import Image, ImageQt from .helper import assert_image_equal_tofile, assert_image_similar, hopper +if TYPE_CHECKING: + import PyQt6 + import PySide6 + + QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication] + QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout] + QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] + QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel] + QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter] + QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] + QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint] + QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion] + QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget] + if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -20,7 +35,7 @@ if ImageQt.qt_is_installed: from PySide6.QtGui import QImage, QPainter, QRegion from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - class Example(QWidget): + class Example(QWidget): # type: ignore[misc] def __init__(self) -> None: super().__init__() @@ -28,11 +43,12 @@ if ImageQt.qt_is_installed: qimage = ImageQt.ImageQt(img) - pixmap1 = ImageQt.QPixmap.fromImage(qimage) + pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage) - QHBoxLayout(self) # hbox + # hbox + QHBoxLayout(self) # type: ignore[operator] - lbl = QLabel(self) + lbl = QLabel(self) # type: ignore[operator] # Segfault in the problem lbl.setPixmap(pixmap1.copy()) @@ -46,7 +62,7 @@ def roundtrip(expected: Image.Image) -> None: @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_sanity(tmp_path: Path) -> None: # Segfault test - app: QApplication | None = QApplication([]) + app: QApplication | None = QApplication([]) # type: ignore[operator] ex = Example() assert app # Silence warning assert ex # Silence warning @@ -56,7 +72,7 @@ def test_sanity(tmp_path: Path) -> None: im = hopper(mode) data = ImageQt.toqpixmap(im) - assert isinstance(data, QPixmap) + assert data.__class__.__name__ == "QPixmap" assert not data.isNull() # Test saving the file @@ -64,14 +80,14 @@ def test_sanity(tmp_path: Path) -> None: data.save(tempfile) # Render the image - qimage = ImageQt.ImageQt(im) - data = QPixmap.fromImage(qimage) - qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage - qimage = QImage(128, 128, qt_format.Format_ARGB32) - painter = QPainter(qimage) - image_label = QLabel() + imageqt = ImageQt.ImageQt(im) + data = getattr(QPixmap, "fromImage")(imageqt) + qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage + qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator] + painter = QPainter(qimage) # type: ignore[operator] + image_label = QLabel() # type: ignore[operator] image_label.setPixmap(data) - image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) + image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator] painter.end() rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") qimage.save(rendered_tempfile) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 6110be707..8cb7ffb9b 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -21,7 +21,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) - assert isinstance(data, QImage) + assert isinstance(data, QImage) # type: ignore[arg-type, misc] assert not data.isNull() # reload directly from the qimage diff --git a/pyproject.toml b/pyproject.toml index 9e53d4bfe..e4ae73acf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -166,5 +166,4 @@ warn_unused_ignores = true exclude = [ '^Tests/oss-fuzz/fuzz_font.py$', '^Tests/oss-fuzz/fuzz_pillow.py$', - '^Tests/test_qt_image_qapplication.py$', ] diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 4da0cd230..d830ad53f 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -71,7 +71,7 @@ def Ghostscript( fp: IO[bytes], scale: int = 1, transparency: bool = False, -) -> Image.Image: +) -> Image.core.ImagingCore: """Render an image using Ghostscript""" global gs_binary if not has_ghostscript(): diff --git a/src/PIL/Image.py b/src/PIL/Image.py index fbeecef0e..0877f7792 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -223,7 +223,7 @@ if TYPE_CHECKING: from IPython.lib.pretty import PrettyPrinter - from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin + from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ @@ -2978,7 +2978,7 @@ class Image: self.load() return self._new(self.im.effect_spread(distance)) - def toqimage(self): + def toqimage(self) -> ImageQt.ImageQt: """Returns a QImage copy of this image""" from . import ImageQt @@ -2987,7 +2987,7 @@ class Image: raise ImportError(msg) return ImageQt.toqimage(self) - def toqpixmap(self): + def toqpixmap(self) -> ImageQt.QPixmap: """Returns a QPixmap copy of this image""" from . import ImageQt @@ -3314,7 +3314,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image: return frombuffer(mode, size, obj, "raw", rawmode, 0, 1) -def fromqimage(im) -> ImageFile.ImageFile: +def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile: """Creates an image instance from a QImage image""" from . import ImageQt @@ -3324,7 +3324,7 @@ def fromqimage(im) -> ImageFile.ImageFile: return ImageQt.fromqimage(im) -def fromqpixmap(im) -> ImageFile.ImageFile: +def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile: """Creates an image instance from a QPixmap image""" from . import ImageQt diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index a2d946714..a3d647138 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,14 +19,23 @@ from __future__ import annotations import sys from io import BytesIO -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any, Callable, Union from . import Image from ._util import is_path if TYPE_CHECKING: + import PyQt6 + import PySide6 + from . import ImageFile + QBuffer: type + QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray] + QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice] + QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] + QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] + qt_version: str | None qt_versions = [ ["6", "PyQt6"], @@ -37,10 +46,6 @@ qt_versions = [ qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) for version, qt_module in qt_versions: try: - QBuffer: type - QIODevice: type - QImage: type - QPixmap: type qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": from PyQt6.QtCore import QBuffer, QIODevice @@ -65,19 +70,20 @@ def rgb(r: int, g: int, b: int, a: int = 255) -> int: return qRgba(r, g, b, a) & 0xFFFFFFFF -def fromqimage(im): +def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile: """ :param im: QImage or PIL ImageQt object """ buffer = QBuffer() + qt_openmode: object if qt_version == "6": try: - qt_openmode = QIODevice.OpenModeFlag + qt_openmode = getattr(QIODevice, "OpenModeFlag") except AttributeError: - qt_openmode = QIODevice.OpenMode + qt_openmode = getattr(QIODevice, "OpenMode") else: qt_openmode = QIODevice - buffer.open(qt_openmode.ReadWrite) + buffer.open(getattr(qt_openmode, "ReadWrite")) # preserve alpha channel with png # otherwise ppm is more friendly with Image.open if im.hasAlphaChannel(): @@ -93,7 +99,7 @@ def fromqimage(im): return Image.open(b) -def fromqpixmap(im) -> ImageFile.ImageFile: +def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile: return fromqimage(im) @@ -123,7 +129,7 @@ def align8to32(bytes: bytes, width: int, mode: str) -> bytes: return b"".join(new_data) -def _toqclass_helper(im): +def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: data = None colortable = None exclusive_fp = False @@ -135,30 +141,32 @@ def _toqclass_helper(im): if is_path(im): im = Image.open(im) exclusive_fp = True + assert isinstance(im, Image.Image) - qt_format = QImage.Format if qt_version == "6" else QImage + qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage if im.mode == "1": - format = qt_format.Format_Mono + format = getattr(qt_format, "Format_Mono") elif im.mode == "L": - format = qt_format.Format_Indexed8 + format = getattr(qt_format, "Format_Indexed8") colortable = [rgb(i, i, i) for i in range(256)] elif im.mode == "P": - format = qt_format.Format_Indexed8 + format = getattr(qt_format, "Format_Indexed8") palette = im.getpalette() + assert palette is not None colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)] elif im.mode == "RGB": # Populate the 4th channel with 255 im = im.convert("RGBA") data = im.tobytes("raw", "BGRA") - format = qt_format.Format_RGB32 + format = getattr(qt_format, "Format_RGB32") elif im.mode == "RGBA": data = im.tobytes("raw", "BGRA") - format = qt_format.Format_ARGB32 + format = getattr(qt_format, "Format_ARGB32") elif im.mode == "I;16": im = im.point(lambda i: i * 256) - format = qt_format.Format_Grayscale16 + format = getattr(qt_format, "Format_Grayscale16") else: if exclusive_fp: im.close() @@ -174,8 +182,8 @@ def _toqclass_helper(im): if qt_is_installed: - class ImageQt(QImage): - def __init__(self, im) -> None: + class ImageQt(QImage): # type: ignore[misc] + def __init__(self, im: Image.Image | str | QByteArray) -> None: """ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage class. @@ -199,10 +207,10 @@ if qt_is_installed: self.setColorTable(im_data["colortable"]) -def toqimage(im) -> ImageQt: +def toqimage(im: Image.Image | str | QByteArray) -> ImageQt: return ImageQt(im) -def toqpixmap(im): +def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap: qimage = toqimage(im) - return QPixmap.fromImage(qimage) + return getattr(QPixmap, "fromImage")(qimage) From 65172db28db0cc965c026512546857540b0c6e20 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 27 Aug 2024 19:04:46 +1000 Subject: [PATCH 52/52] Corrected Ghostscript path --- .github/workflows/test-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 5b34d6703..2943bbe31 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -87,7 +87,7 @@ jobs: echo "C:\Program Files\NASM" >> $env:GITHUB_PATH choco install ghostscript --version=10.3.1 --no-progress - echo "C:\Program Files\gs\gs10.00.0\bin" >> $env:GITHUB_PATH + echo "C:\Program Files\gs\gs10.03.1\bin" >> $env:GITHUB_PATH # Install extra test images xcopy /S /Y Tests\test-images\* Tests\images