diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..38c9cf6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: python -m pip install --upgrade pip && pip install -e ".[dev]" + + - name: Run tests + run: pytest diff --git a/.gitignore b/.gitignore index 3a79ef5..0a521b4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ __pycache__ build/ dist/ *.egg-info/ -tmp/ \ No newline at end of file +tmp/ +.codex* +.agents* \ No newline at end of file diff --git a/LICENSE b/LICENSE index 43e9b83..4f39123 100644 --- a/LICENSE +++ b/LICENSE @@ -1,12 +1,12 @@ -envstack +drawcal -Copyright (c) 2022-2025, Ryan Galloway (ryan@rsgalloway.com) +Copyright (c) 2022-2025, Bnbnotify All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + * Neither the name of Bnbnotify nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 1a5ad4e..9886d36 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,24 @@ Python library for drawing simple monthly calendar images with events. ## Installation -The easiest way to install: +Install from PyPI: ```bash $ pip install -U drawcal ``` -Alternatively, to install with distman: +Build locally: ```bash -$ dist [-d] +$ python -m pip install --upgrade build +$ python -m build +``` + +Upload to PyPI: + +```bash +$ python -m pip install --upgrade twine +$ python -m twine upload dist/* ``` ## Quickstart @@ -33,3 +41,15 @@ Python: >>> from drawcal import draw_calendar >>> draw_calendar(month, year, events=events, outfile=outfile) ``` + +## Events format + +`drawcal` expects a JSON file containing a list of events, where each event is a +list of dates in `M/D/YYYY` format: + +```json +[ + ["3/1/2025", "3/2/2025", "3/3/2025"], + ["3/14/2025", "3/15/2025"] +] +``` diff --git a/bin/drawcal b/bin/drawcal index dc532c2..d65c0d0 100755 --- a/bin/drawcal +++ b/bin/drawcal @@ -1,6 +1,6 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -# Copyright (c) 2022-2025, Ryan Galloway (ryan@rsgalloway.com) +# Copyright (c) 2022-2025, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/lib/drawcal/__init__.py b/lib/drawcal/__init__.py index 3ec8fdc..cae2a5c 100644 --- a/lib/drawcal/__init__.py +++ b/lib/drawcal/__init__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Ryan Galloway (ryan@rsgalloway.com) +# Copyright (c) 2022-2025, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -34,11 +34,13 @@ """ __prog__ = "drawcal" -__version__ = "0.5.6" +__version__ = "0.5.8" __author__ = "ryan@rsgalloway.com" -import envstack -envstack.init(__prog__) +def draw_calendar(*args, **kwargs): + """Lazily import the renderer so package metadata stays cheap to import.""" -from drawcal.drawlib import draw_calendar + from drawcal.drawlib import draw_calendar as _draw_calendar + + return _draw_calendar(*args, **kwargs) diff --git a/lib/drawcal/cli.py b/lib/drawcal/cli.py index 3b9d0d8..381a5dd 100755 --- a/lib/drawcal/cli.py +++ b/lib/drawcal/cli.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Ryan Galloway (ryan@rsgalloway.com) +# Copyright (c) 2022-2025, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -37,7 +37,6 @@ from datetime import datetime, timedelta from drawcal import __prog__, __version__ -from drawcal.drawlib import draw_calendar from drawcal.events import get_events, read_events d = datetime.today() @@ -92,6 +91,9 @@ def parse_args(): def main(): """Main event loop.""" + import envstack + + envstack.init(__prog__) args = parse_args() @@ -100,6 +102,8 @@ def main(): else: events = get_events(args.month, args.year) + from drawcal.drawlib import draw_calendar + draw_calendar(month=args.month, year=args.year, events=events, outfile=args.outfile) return 0 diff --git a/lib/drawcal/config.py b/lib/drawcal/config.py index 936f630..68f3661 100644 --- a/lib/drawcal/config.py +++ b/lib/drawcal/config.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Ryan Galloway (ryan@rsgalloway.com) +# Copyright (c) 2022-2025, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: diff --git a/lib/drawcal/drawlib.py b/lib/drawcal/drawlib.py index 96fe00d..67c7427 100644 --- a/lib/drawcal/drawlib.py +++ b/lib/drawcal/drawlib.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Ryan Galloway (ryan@rsgalloway.com) +# Copyright (c) 2022-2025, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -68,6 +68,13 @@ class colors: title_text = "#101010" # month/year text color +def _text_size(draw, text, font): + """Return text width/height across Pillow versions.""" + + left, top, right, bottom = draw.textbbox((0, 0), text, font=font) + return right - left, bottom - top + + def draw_calendar( month=today.month, year=today.year, @@ -106,7 +113,7 @@ def draw_calendar( pad = 20 # make sure events is a list - if events == None: + if events is None: events = [] # categorize and track dates @@ -133,7 +140,7 @@ def draw_calendar( # draw the month and year font = ImageFont.truetype(config.ARIAL_TTF_FILE, size=15) - header_w, header_h = draw.textsize(header, font=font) + header_w, header_h = _text_size(draw, header, font) draw.text( ((width - header_w) / 2, pad / 2), header, fill=colors.title_text, font=font ) @@ -233,11 +240,8 @@ def draw_calendar( continue # handle each day in event - if first_day == col: + if first_day == curr_day: s = 0 - if last_day == col: - e = 22 - # check-in if first_day == curr_day: checkin = True @@ -332,7 +336,7 @@ def draw_calendar( # draw days of the week and date numbers col_font = ImageFont.truetype(config.ARIAL_TTF_FILE, size=12) - col_w, col_h = draw.textsize(col, font=col_font) + col_w, col_h = _text_size(draw, col, col_font) # set date text color if past_date: diff --git a/lib/drawcal/events.py b/lib/drawcal/events.py index 3ae77f6..27cec54 100644 --- a/lib/drawcal/events.py +++ b/lib/drawcal/events.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# Copyright (c) 2022-2025, Ryan Galloway (ryan@rsgalloway.com) +# Copyright (c) 2022-2025, Bnbnotify # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: @@ -34,6 +34,7 @@ """ import json +from calendar import monthrange from datetime import datetime, timedelta d = datetime.today() @@ -47,17 +48,20 @@ def get_events(month=today.month, year=today.year): from random import randint events = [] + last_day = monthrange(year, month)[1] num_events = randint(2, 8) i = 1 for _ in range(2, num_events): event = [] for dd in range(i, randint(i + 2, i + randint(3, 8))): + if dd > last_day: + break event.append(f"{month}/{dd}/{year}") i += 1 - if i >= 31: + if i > last_day: break - if i >= 31: + if i > last_day: break i += randint(1, 10) if event: @@ -74,13 +78,26 @@ def get_events(month=today.month, year=today.year): def read_events(filepath): """Returns JSON serialized events from a given filepath.""" - events = [] - try: - fp = open(filepath) - events = json.load(fp) - fp.close() - except Exception as e: - print(e) + with open(filepath, encoding="utf-8") as fp: + events = json.load(fp) + except OSError as exc: + raise OSError(f"unable to read events file: {filepath}") from exc + except json.JSONDecodeError as exc: + raise ValueError(f"invalid JSON in events file: {filepath}") from exc + + if not isinstance(events, list): + raise ValueError("events file must contain a list of event lists") + + for event in events: + if not isinstance(event, list): + raise ValueError("each event must be a list of date strings") + for day in event: + if not isinstance(day, str): + raise ValueError("each event date must be a string in M/D/YYYY format") + try: + datetime.strptime(day, "%m/%d/%Y") + except (TypeError, ValueError) as exc: + raise ValueError(f"invalid event date: {day}") from exc return events diff --git a/lib/drawcal/test.py b/lib/drawcal/test.py deleted file mode 100644 index 40902a6..0000000 --- a/lib/drawcal/test.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (c) 2022-2025, Ryan Galloway (ryan@rsgalloway.com) -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# - Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# - Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# - Neither the name of the software nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# - -__doc__ = """ -contains drawcal unit tests. -""" - -import unittest - - -# TODO: move these test events to separate json files in a tests folder -def test_events(): - """Returns a list of test events.""" - - # events = [] - # events = [['2/1/2022', '2/2/2022'], ['2/10/2022', '2/11/2022'], ['2/16/2022', '2/17/2022', '2/18/2022', '2/19/2022', '2/20/2022', '2/21/2022'], ['2/23/2022', '2/24/2022']] - # events = [['1/28/2022', '1/29/2022', '1/30/2022', '1/31/2022'], ['2/1/2022', '2/2/2022', '2/3/2022', '2/4/2022', '2/5/2022'], ['2/7/2022', '2/8/2022', '2/9/2022', '2/10/2022'], ['2/13/2022', '2/14/2022', '2/15/2022', '2/16/2022'], ['2/24/2022', '2/25/2022'], ['2/28/2022', '3/1/2022']] - # events = [['2/1/2022', '2/2/2022', '2/3/2022', '2/4/2022'], ['2/8/2022', '2/9/2022'], ['2/10/2022', '2/11/2022'], ['2/14/2022', '2/15/2022', '2/16/2022', '2/17/2022', '2/18/2022'], ['2/24/2022', '2/25/2022', '2/26/2022']] - # events = [['2/22/2022','2/23/2022','2/24/2022'], ['2/25/2022','2/26/2022','2/27/2022']] - # events = [['2/1/2022', '2/2/2022', '2/3/2022', '2/4/2022', '2/5/2022'], ['2/13/2022', '2/14/2022', '2/15/2022', '2/16/2022'], ['2/19/2022', '2/20/2022', '2/21/2022', '2/22/2022', '2/23/2022', '2/24/2022']] - # events = [['3/22/2022', '3/23/2022', '3/24/2022'], ['3/25/2022', '3/26/2022', '3/27/2022']] - # events = [['1/1/2022', '1/2/2022', '1/3/2022'], ['1/12/2022', '1/13/2022', '1/14/2022', '1/15/2022'], ['1/24/2022', '1/25/2022']] - # events = [['2/1/2022', '2/2/2022', '2/3/2022', '2/4/2022', '2/5/2022'], ['2/12/2022', '2/13/2022', '2/14/2022', '2/15/2022', '2/16/2022', '2/17/2022'], ['2/24/2022', '2/25/2022', '2/26/2022']] - - # contains a fake date: 2/29/2022 - # events = [['2/1/2022', '2/2/2022'], ['2/27/2022', '2/28/2022', '2/29/2022']] - - # contains conflicting events - # events = [['3/1/2022', '3/2/2022', '3/3/2022'], ['3/2/2022', '3/3/2022', '3/4/2022']] - - # issue no. 1 - # events = [['5/29/2022', '5/30/2022', '5/31/2022'], ['6/3/2022', '6/4/2022']] - - # gaps in events - events = [["6/3/2022", "6/4/2022", "6/5/2022", "6/6/2022", "6/7/2022"]] - - return events - - -if __name__ == "__main__": - unittest.main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d5b08f2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,59 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "drawcal" +version = "0.5.8" +description = "Python library for drawing simple monthly calendar images with events." +readme = "README.md" +requires-python = ">=3.8" +license = {file = "LICENSE"} +authors = [ + {name = "Ryan Galloway", email = "ryan@rsgalloway.com"}, +] +keywords = ["calendar", "image", "pillow", "png"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Libraries", +] +dependencies = [ + "Pillow>=10.0.0", + "envstack>=0.8.3", +] + +[project.optional-dependencies] +dev = [ + "black>=24.0.0", + "build", + "pytest>=8.0.0", + "twine", +] + +[project.urls] +Homepage = "https://github.com/bnbnotify/drawcal" +Repository = "https://github.com/bnbnotify/drawcal" + +[project.scripts] +drawcal = "drawcal.cli:main" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["lib"] + +[tool.setuptools.package-dir] +"" = "lib" + +[tool.setuptools.package-data] +drawcal = ["arial.ttf"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6349bd7..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Pillow==9.0.1 -envstack>=0.8.3 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 03a9534..0000000 --- a/setup.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (c) 2022-2025, Ryan Galloway (ryan@rsgalloway.com) -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# - Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# -# - Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# - Neither the name of the software nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# - -import os -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, "README.md")) as f: - long_description = f.read() - -setup( - name="drawcal", - version="0.5.6", - author="Ryan Galloway", - author_email="ryan@rsgalloway.com", - url="https://github.com/bnbnotify/drawcal", - license="BSD 3-Clause License", - classifiers=[ - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - ], - install_requires=["Pillow==9.0.1", "envstack>=0.8.3"], - packages=find_packages("lib"), - package_dir={"": "lib"}, - package_data = { - "": ["*.py", "*.ttf"], - }, - python_requires='>=3.6', - scripts=["bin/drawcal"], - entry_points={ - "console_scripts": [ - "drawcal=drawcal.cli:main", - ], - }, - data_files=[ - ("drawcal", ["lib/drawcal/arial.ttf"]) - ], - zip_safe=False, -) diff --git a/tests/fixtures/march-2025-events.json b/tests/fixtures/march-2025-events.json new file mode 100644 index 0000000..4689d6b --- /dev/null +++ b/tests/fixtures/march-2025-events.json @@ -0,0 +1,22 @@ +[ + [ + "3/1/2025", + "3/2/2025", + "3/3/2025", + "3/4/2025", + "3/5/2025" + ], + [ + "3/12/2025", + "3/13/2025", + "3/14/2025", + "3/15/2025", + "3/16/2025", + "3/17/2025" + ], + [ + "3/24/2025", + "3/25/2025", + "3/26/2025" + ] +] \ No newline at end of file diff --git a/tests/fixtures/march-2025-expected.png b/tests/fixtures/march-2025-expected.png new file mode 100644 index 0000000..acb7c8a Binary files /dev/null and b/tests/fixtures/march-2025-expected.png differ diff --git a/tests/test_events.py b/tests/test_events.py new file mode 100644 index 0000000..a67b7dc --- /dev/null +++ b/tests/test_events.py @@ -0,0 +1,40 @@ +import json +import tempfile +import unittest +from datetime import datetime + +from drawcal.events import get_events, read_events + + +class ReadEventsTests(unittest.TestCase): + def test_read_events_rejects_invalid_json(self): + with tempfile.TemporaryDirectory() as tmpdir: + path = f"{tmpdir}/events.json" + + with open(path, "w", encoding="utf-8") as fp: + fp.write("{not json}") + + with self.assertRaises(ValueError): + read_events(path) + + def test_read_events_rejects_invalid_dates(self): + with tempfile.TemporaryDirectory() as tmpdir: + path = f"{tmpdir}/events.json" + + with open(path, "w", encoding="utf-8") as fp: + json.dump([["2/30/2025"]], fp) + + with self.assertRaises(ValueError): + read_events(path) + + +class GetEventsTests(unittest.TestCase): + def test_get_events_stays_within_month(self): + events = get_events(month=2, year=2025) + + for event in events: + for day in event: + parsed = datetime.strptime(day, "%m/%d/%Y") + self.assertEqual(parsed.month, 2) + self.assertEqual(parsed.year, 2025) + self.assertLessEqual(parsed.day, 28) diff --git a/tests/test_package.py b/tests/test_package.py new file mode 100644 index 0000000..018664b --- /dev/null +++ b/tests/test_package.py @@ -0,0 +1,12 @@ +import unittest + +import drawcal + + +class PackageImportTests(unittest.TestCase): + def test_package_metadata_is_importable_without_renderer(self): + self.assertEqual(drawcal.__prog__, "drawcal") + self.assertRegex(drawcal.__version__, r"^\d+\.\d+\.\d+$") + + def test_draw_calendar_symbol_is_exposed(self): + self.assertTrue(callable(drawcal.draw_calendar)) diff --git a/tests/test_render.py b/tests/test_render.py new file mode 100644 index 0000000..c5747d2 --- /dev/null +++ b/tests/test_render.py @@ -0,0 +1,36 @@ +import json +import tempfile +import unittest +from datetime import datetime +from pathlib import Path +from unittest import mock + +from PIL import Image, ImageChops + +from drawcal.drawlib import draw_calendar +import drawcal.drawlib as drawlib + + +FIXTURES_DIR = Path(__file__).parent / "fixtures" +EXPECTED_IMAGE = FIXTURES_DIR / "march-2025-expected.png" +EVENTS_FILE = FIXTURES_DIR / "march-2025-events.json" +FIXED_TODAY = datetime.strptime("1/1/2026", "%m/%d/%Y") + + +class RenderTests(unittest.TestCase): + def test_draw_calendar_matches_expected_image(self): + events = json.loads(EVENTS_FILE.read_text(encoding="utf-8")) + + with tempfile.TemporaryDirectory() as tmpdir: + outfile = Path(tmpdir) / "render.png" + + with mock.patch.object(drawlib, "today", FIXED_TODAY), mock.patch.object( + drawlib, "today_str", "1/1/2026" + ): + draw_calendar(month=3, year=2025, events=events, outfile=str(outfile)) + + with Image.open(EXPECTED_IMAGE) as expected, Image.open(outfile) as actual: + diff = ImageChops.difference(expected, actual) + self.assertIsNone( + diff.getbbox(), "rendered image does not match fixture" + )