From 1d05f87a5f62d2bb9b119ca5a0fc47fa6f74fcd3 Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 15:13:03 +0200 Subject: [PATCH 01/10] fixed circle-ci --- .circleci/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 175ca9e..fbcc5fc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,6 +2,9 @@ version: 2.1 jobs: build-docs: + branches: + only: + - master docker: - image: circleci/python:latest steps: From 459b87a0202f3923d42e23a1712366109ebfdb7f Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 16:26:27 +0200 Subject: [PATCH 02/10] unit tests --- .circleci/config.yml | 11 +++++++++++ .gitignore | 1 + tests/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/meme_otron/__init__.py | 0 tests/{ => unit/meme_otron}/test_utils.py | 0 6 files changed, 12 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/meme_otron/__init__.py rename tests/{ => unit/meme_otron}/test_utils.py (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index fbcc5fc..c0708c6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,8 +22,19 @@ jobs: git diff-index --quiet HEAD || git commit -m 'Automated README [ci skip]' git push origin master name: Building docs + unit-tests: + docker: + - image: circleci/python:latest + steps: + - checkout + - run: + command: | + sudo pip install pytest + python -m pytest ./tests/unit + name: Unit tests workflows: main: jobs: + - unit-tests - build-docs \ No newline at end of file diff --git a/.gitignore b/.gitignore index 384da50..c4063f6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ error_*.txt tmp .key* *.pyc +.pytest_cache diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/meme_otron/__init__.py b/tests/unit/meme_otron/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_utils.py b/tests/unit/meme_otron/test_utils.py similarity index 100% rename from tests/test_utils.py rename to tests/unit/meme_otron/test_utils.py From 1c44357bade51530c15e546eb593fa52e9166965 Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 16:27:07 +0200 Subject: [PATCH 03/10] fixed nearest word giving weird results --- meme_otron/utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/meme_otron/utils.py b/meme_otron/utils.py index da522b5..f956d8e 100644 --- a/meme_otron/utils.py +++ b/meme_otron/utils.py @@ -79,10 +79,16 @@ def parse_arguments(src: str) -> List[str]: def find_nearest(word: str, wlist: List[str], threshold: int = 5) -> Optional[str]: - found = min([(distance(word, w) - abs(len(w) - len(word)), w) for w in wlist], key=lambda v: v[0]) - if found[0] > threshold: + distances = [ + (distance(word, w), # distance + abs(len(w) - len(word)), # length diff + w) + for w in wlist] + distances.sort(key=lambda v: v[1]) # sort by length diff to get the closest (in length) first + found = min(distances, key=lambda v: v[0] - v[1]) # get the closest in lev. distance + if found[0] - found[1] > threshold: # distance is too much return None - return found[1] + return found[2] def justify_text(src: str, n_lines: int) -> Optional[str]: From 19cfa1fb2fce0b52e606af716a35b384f77176a8 Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 16:33:16 +0200 Subject: [PATCH 04/10] fixing circle-ci --- .circleci/config.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c0708c6..8b156da 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,10 +1,17 @@ version: 2.1 +workflows: + main: + jobs: + - unit-tests + - build-docs: + filters: + branches: + only: + - master + jobs: build-docs: - branches: - only: - - master docker: - image: circleci/python:latest steps: @@ -31,10 +38,4 @@ jobs: command: | sudo pip install pytest python -m pytest ./tests/unit - name: Unit tests - -workflows: - main: - jobs: - - unit-tests - - build-docs \ No newline at end of file + name: Unit tests \ No newline at end of file From bb0d8853bb4366c39a09caf04fcdd540bb98d8be Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 16:34:12 +0200 Subject: [PATCH 05/10] fixing circle-ci --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8b156da..e5e5ed8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -36,6 +36,7 @@ jobs: - checkout - run: command: | + sudo pip install -r requirements.txt sudo pip install pytest python -m pytest ./tests/unit name: Unit tests \ No newline at end of file From 2223770fb82c476bbd81bdf3f602c8a425abc722 Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 18:48:31 +0200 Subject: [PATCH 06/10] bot: more input sanitization --- discord_bot/__main__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/discord_bot/__main__.py b/discord_bot/__main__.py index 650b08d..fc33d22 100644 --- a/discord_bot/__main__.py +++ b/discord_bot/__main__.py @@ -126,7 +126,7 @@ async def on_message(message): if len(args) > 1 and message.author.display_name is not None: left_wmark_text = f"By {message.author.display_name}" logging.info(args[0]) - meme_id = re.sub(r'[^\w ]', "", args[0]) + meme_id = re.sub(r'[^A-Za-z0-9 _]', "", args[0]).strip() args[0] = meme_id img = meme_otron.compute(*args, left_wmark_text=left_wmark_text) if img is None: @@ -139,7 +139,10 @@ async def on_message(message): response += f"Did you mean `{hint}`?\n" response += f"You can find a more detailed help and a list of templates at:\n" \ f"<{DOC_URL}>" - await message.channel.send(response) + if len(response) >= 2000: + await message.channel.send(f"{message.author.mention} ... really?") + else: + await message.channel.send(response) else: with tempfile.NamedTemporaryFile(delete=False) as output: img.save(output, format="JPEG") From 4bb9bbb23a945961d2534f2848377f867df2b858 Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 19:14:28 +0200 Subject: [PATCH 07/10] code cleaning --- discord_bot/__main__.py | 72 +++++++++++++++++---------------------- docs/build.py | 30 ++++++++-------- meme_otron/__main__.py | 29 ++++++++-------- meme_otron/img_factory.py | 62 +++++++++++---------------------- meme_otron/meme_db.py | 48 ++++++++------------------ meme_otron/meme_otron.py | 2 +- meme_otron/types.py | 12 +++---- tools/live_edit.py | 6 ++-- 8 files changed, 103 insertions(+), 158 deletions(-) diff --git a/discord_bot/__main__.py b/discord_bot/__main__.py index fc33d22..4264e13 100644 --- a/discord_bot/__main__.py +++ b/discord_bot/__main__.py @@ -8,8 +8,8 @@ import sys from datetime import datetime from dotenv import load_dotenv -from meme_otron import img_factory as imgf -from meme_otron import meme_db as db +from meme_otron import img_factory +from meme_otron import meme_db from meme_otron import utils from meme_otron import meme_otron from meme_otron import VERSION @@ -26,19 +26,17 @@ if token is None: logging.error("No token was loaded, please verify your .env file") sys.exit(1) -imgf.load_fonts() -db.load_memes() +img_factory.load_fonts() +meme_db.load_memes() client = discord.Client() SENT = {} -def debug(message, txt): + +def debug(message: discord.Message, txt: str): """ Print a log with the context of the current event - - :param (discord.Message) message: message that triggered the event - :param (str) txt: text of the log """ logging.info(f"{message.guild} > #{message.channel}: {txt}") @@ -59,13 +57,7 @@ async def on_ready(): logging.info(f'- {guild.name}(id: {guild.id})') -async def delete(message): - """ - Delete a discord message - - :param (discord.Message) message: - :rtype: bool - """ +async def delete(message: discord.Message) -> bool: try: await message.delete() return True @@ -77,24 +69,22 @@ async def delete(message): @client.event -async def on_message(message): +async def on_message(message: discord.Message): """ Called when a message is sent to any channel on any guild - - :param (discord.Message) message: message sent """ # Ignore self messages if message.author == client.user: return - direct = message.channel.type == discord.ChannelType.private + is_direct = message.channel.type == discord.ChannelType.private - if not direct: - mid = f'{message.guild.id}/{message.channel.id}/{message.author.id}' + if not is_direct: + message_id = f'{message.guild.id}/{message.channel.id}/{message.author.id}' else: - mid = message.author.id + message_id = message.author.id - if direct or client.user in message.mentions: + if is_direct or client.user in message.mentions: message.content = re.sub(r'<@[^>]+>', '', message.content).strip() args = utils.parse_arguments(message.content) debug(message, str(args)) @@ -112,11 +102,11 @@ async def on_message(message): return if len(args) > 0 and args[0].lower().strip() == "list": await message.channel.send(f"Here is a list of all known templates:\n" - f"```{', '.join(db.LIST)}```") + f"```{', '.join(meme_db.LIST)}```") return if len(args) > 0 and args[0].lower().strip() == "delete": - if mid in SENT and len(SENT[mid]) > 0 and await delete(SENT[mid][-1]): - if not direct: + if message_id in SENT and len(SENT[message_id]) > 0 and await delete(SENT[message_id][-1]): + if not is_direct: await delete(message) else: await message.add_reaction("⚠") @@ -133,7 +123,7 @@ async def on_message(message): if len(meme_id) == 0: response = f":warning: Template not found\n" else: - hint = db.find_nearest(meme_id) + hint = meme_db.find_nearest(meme_id) response = f":warning: Template `{meme_id}` not found\n" if hint is not None: response += f"Did you mean `{hint}`?\n" @@ -148,7 +138,7 @@ async def on_message(message): img.save(output, format="JPEG") response = None if len(args) == 1: - meme = db.get_meme(meme_id) + meme = meme_db.get_meme(meme_id) response = f"Template `{meme.id}`:" if len(meme.aliases) > 0: response += f"\n- Aliases: `{'`, `'.join(meme.aliases)}`" @@ -158,18 +148,18 @@ async def on_message(message): f"\n```{meme.id} \"" + \ "\" \"".join([f"text {i + 1}" for i in range(meme.texts_len)]) + \ "\"```" - elif not direct: + elif not is_direct: response = f"A meme by {message.author.mention}:" - if mid not in SENT: - SENT[mid] = [] + if message_id not in SENT: + SENT[message_id] = [] response = await message.channel.send(response, file=discord.File(filename="meme.jpg", fp=output.name)) - SENT[mid] += [response] + SENT[message_id] += [response] try: os.remove(output.name) except PermissionError: pass - if not direct: + if not is_direct: await delete(message) @@ -179,14 +169,14 @@ while True: client.run(token) break # clean kill except Exception as e: - t = datetime.now() - logging.error(f"Exception raised at {t:%Y-%m-%d %H:%M} : {repr(e)}") - fileName = f"error_{t:%Y-%m-%d_%H-%M-%S}.txt" + exception_time = datetime.now() + logging.error(f"Exception raised at {exception_time:%Y-%m-%d %H:%M} : {repr(e)}") + fileName = f"error_{exception_time:%Y-%m-%d_%H-%M-%S}.txt" if os.path.exists(fileName): logging.error("Two many errors, killing") break - with open(fileName, 'w') as f: - f.write(f"Discord AI Dungeon 2 v{VERSION} started at {t0:%Y-%m-%d %H:%M}\r\n" - f"Exception raised at {t:%Y-%m-%d %H:%M}\r\n" - f"\r\n" - f"{traceback.format_exc()}") + with open(fileName, 'w') as exception_file: + exception_file.write(f"Meme-Otron v{VERSION} started at {t0:%Y-%m-%d %H:%M}\r\n" + f"Exception raised at {exception_time:%Y-%m-%d %H:%M}\r\n" + f"\r\n" + f"{traceback.format_exc()}") diff --git a/docs/build.py b/docs/build.py index ffc4a9c..e4a597d 100644 --- a/docs/build.py +++ b/docs/build.py @@ -2,36 +2,36 @@ import os import logging import PIL from os import path -from meme_otron import img_factory as imgf +from meme_otron import img_factory from meme_otron import meme_db from meme_otron import utils logging.basicConfig(format="[%(asctime)s][%(levelname)s][%(module)s] %(message)s", level=logging.WARNING) -imgf.load_fonts() +img_factory.load_fonts() meme_db.load_memes() -dst_dir = utils.relative_path(__file__, "templates") -prev_dir = utils.relative_path(__file__, "preview") +templates_dir = utils.relative_path(__file__, "templates") +preview_dir = utils.relative_path(__file__, "preview") doc_file = utils.relative_path(__file__, "README.md") COLUMNS = 3 IMG_HEIGHT = 400 -def make_empty(target_dir): +def make_empty(target_dir: str): if path.exists(target_dir): - for f in os.listdir(target_dir): - if path.isfile(path.join(target_dir, f)): - os.unlink(path.join(target_dir, f)) + for file in os.listdir(target_dir): + if path.isfile(path.join(target_dir, file)): + os.unlink(path.join(target_dir, file)) else: os.mkdir(target_dir) -make_empty(dst_dir) -make_empty(prev_dir) +make_empty(templates_dir) +make_empty(preview_dir) -ids = sorted(meme_db.LIST) +id_list = sorted(meme_db.LIST) doc_content = "|" * (COLUMNS + 1) \ + "\n|" + ":---:|" * COLUMNS @@ -40,14 +40,14 @@ info_line = None img_line = None i = None -for i, meme_id in enumerate(ids): +for i, meme_id in enumerate(id_list): meme = meme_db.get_meme(meme_id) - img = imgf.make(meme.template, meme.texts, debug=True) + img = img_factory.build_image(meme.template, meme.texts, debug=True) if img is not None: - img.save(path.join(dst_dir, meme.template)) + img.save(path.join(templates_dir, meme.template)) size = (round(img.size[0] * IMG_HEIGHT / img.size[1]), IMG_HEIGHT) img2 = img.resize(size, resample=PIL.Image.LANCZOS) - img2.save(path.join(prev_dir, meme.template)) + img2.save(path.join(preview_dir, meme.template)) if i % COLUMNS == 0: if info_line is not None and img_line is not None: doc_content += info_line + img_line diff --git a/meme_otron/__main__.py b/meme_otron/__main__.py index 777ad7d..b5ee454 100644 --- a/meme_otron/__main__.py +++ b/meme_otron/__main__.py @@ -1,15 +1,14 @@ -import logging import sys import os -from . import img_factory as imgf -from . import meme_db as db +from . import img_factory +from . import meme_db from . import meme_otron from . import VERSION if __name__ == "__main__": - db.load_memes() - imgf.load_fonts() + meme_db.load_memes() + img_factory.load_fonts() # TODO better arguments reading (-h, -o, -v) @@ -21,29 +20,29 @@ if __name__ == "__main__": file=sys.stderr) sys.exit(1) else: - output_f = None + output_file = None if "-o" in sys.argv: i = sys.argv.index("-o") if len(sys.argv) >= i: - output_f = sys.argv[i + 1] + output_file = sys.argv[i + 1] del sys.argv[i + 1] del sys.argv[i] img = meme_otron.compute(*sys.argv[1:]) if img is None: - hint = db.find_nearest(sys.argv[1]) - if hint is not None: - print(f"Did you mean '{hint}'?", file=sys.stderr) + proposal = meme_db.find_nearest(sys.argv[1]) + if proposal is not None: + print(f"Did you mean '{proposal}'?", file=sys.stderr) sys.exit(1) - if output_f is None: + if output_file is None: with os.fdopen(os.dup(sys.stdout.fileno())) as output: img.save(output, format="jpeg") else: try: - img.save(output_f) - print(f"Wrote '{output_f}'") + img.save(output_file) + print(f"Wrote '{output_file}'") except OSError as e: - print(f"Cannot write '{output_f}': {e}", file=sys.stderr) + print(f"Cannot write '{output_file}': {e}", file=sys.stderr) sys.exit(1) except ValueError as e: - print(f"Cannot write '{output_f}': {e}", file=sys.stderr) + print(f"Cannot write '{output_file}': {e}", file=sys.stderr) sys.exit(1) diff --git a/meme_otron/img_factory.py b/meme_otron/img_factory.py index 5164b1a..409154a 100644 --- a/meme_otron/img_factory.py +++ b/meme_otron/img_factory.py @@ -1,9 +1,11 @@ +from typing import List, Optional, Tuple from PIL import Image, ImageFont, ImageDraw import os import os.path as path import logging from . import utils +from .types import Text FONT_DIR = utils.relative_path(__file__, "..", "fonts") TEMPLATES_DIR = utils.relative_path(__file__, "..", "templates") @@ -24,14 +26,7 @@ def load_fonts(): logger.error(f"Could not load font '{split[0]}'") -def make(template, texts, debug=False): - """ - :param (str) template: - :param (list of Text) texts: - :param (bool) debug: - :rtype: PIL.Image.Image - :return: - """ +def build_image(template: str, texts: List[Text], debug: bool = False) -> Optional[Image.Image]: try: img = Image.open(path.join(TEMPLATES_DIR, template)).convert(mode='RGBA') except OSError as e: @@ -45,19 +40,13 @@ def make(template, texts, debug=False): return img.convert(mode='RGB') -def draw_text(draw, img, text, debug=False): - """ - :param (PIL.ImageDraw.ImageDraw) draw: source image canvas - :param (PIL.Image.Image) img: source image - :param (Text) text: - :param (bool) debug: - """ +def draw_text(draw: ImageDraw.ImageDraw, img: Image.Image, text: Text, debug: bool = False): if text.text is not None and len(text.text.strip()) > 0: text.init() # load default values if text.font in FONTS: text.text, font = fit_text(img.size, text) if text.angle == 0: - draw.text(get_pos(img.size, text, font), text.text, fill=text.fill, align=text.align, font=font, + draw.text(get_text_pos(img.size, text, font), text.text, fill=text.fill, align=text.align, font=font, stroke_width=round(text.stroke_width * font.size), stroke_fill=text.stroke_fill) if debug: draw.rectangle([(text.x_range[0] * img.size[0], text.y_range[0] * img.size[1]), @@ -70,7 +59,7 @@ def draw_text(draw, img, text, debug=False): center_y = (text.y_range[0] + text.y_range[1]) * img.size[1] / 2 txt_img = Image.new('RGBA', (width, height)) txt_draw = ImageDraw.Draw(txt_img) - txt_draw.text(get_pos(img.size, text, font, relative=True), text.text, fill=text.fill, + txt_draw.text(get_text_pos(img.size, text, font, relative=True), text.text, fill=text.fill, align=text.align, font=font, stroke_width=round(text.stroke_width * font.size), stroke_fill=text.stroke_fill) if debug: @@ -84,43 +73,30 @@ def draw_text(draw, img, text, debug=False): logger.warning(f"Invalid font '{text.font}'") -def fit_text(size, text): - """ - :param (int,int) size: source image size - :param (Text) text: - :rtype: (str, PIL.ImageFont.FreeTypeFont) - :return: - """ - # TODO rework this function +def fit_text(size: Tuple[int, int], text: Text) -> Tuple[str, ImageFont.FreeTypeFont]: max_width = round(size[0] * (text.x_range[1] - text.x_range[0])) max_height = round(size[1] * (text.y_range[1] - text.y_range[0])) text_size = None font_size = round(text.font_size * min(size)) + 1 font = FONTS[text.font] - t = "" + text_content = "" while (text_size is None or text_size[0] >= max_width or text_size[1] >= max_height) and font_size > 1: font_size -= 1 font = font.font_variant(size=font_size) - k = 0 # number of lines - while k == 0 or (t is not None and text_size[0] >= max_width): - k += 1 - t = utils.justify_text(text.text, k) - if t is not None: - text_size = font.getsize_multiline(t, stroke_width=text.stroke_width * font_size) - if t is None: + n_lines = 0 + while n_lines == 0 or (text_content is not None and text_size[0] >= max_width): + n_lines += 1 + text_content = utils.justify_text(text.text, n_lines) + if text_content is not None: + text_size = font.getsize_multiline(text_content, stroke_width=text.stroke_width * font_size) + if text_content is None: # max break attained - text_size = None # restart - return t, font + text_size = None # retry + return text_content, font -def get_pos(size, text, font, relative=False): - """ - :param (int,int) size: source image size - :param (Text) text: - :param (PIL.ImageFont.FreeTypeFont) font: - :rtype (int,int) - :return: - """ +def get_text_pos(size: Tuple[int, int], text: Text, + font: ImageFont.FreeTypeFont, relative: bool = False) -> Tuple[int, int]: min_x = round(text.x_range[0] * size[0]) max_x = round(text.x_range[1] * size[0]) min_y = round(text.y_range[0] * size[1]) diff --git a/meme_otron/meme_db.py b/meme_otron/meme_db.py index d072323..ae8c849 100644 --- a/meme_otron/meme_db.py +++ b/meme_otron/meme_db.py @@ -1,3 +1,4 @@ +from typing import Optional import json import logging @@ -13,17 +14,14 @@ LIST = [] logger = logging.getLogger("meme_db") -def load_memes(purge=False): - """ - :param (bool) purge: - """ +def load_memes(purge: bool = False): global DATA, ALIASES if purge: DATA = {} ALIASES = {} try: - with open(DATA_FILE) as f: - content = "".join(f.readlines()) + with open(DATA_FILE) as input_file: + content = "".join(input_file.readlines()) raw_data = json.loads(content) if not (isinstance(raw_data, list)): raise TypeError(f"Root is not a list") @@ -37,12 +35,9 @@ def load_memes(purge=False): logger.error(f"Invalid data file: {e}") -def load_item(i, item): - """ - :param (int) i: - :param (dict) item: - """ +def load_item(i: int, item: dict): global LIST + # TODO reduce complexity item_id = "" try: if not (isinstance(item, dict)): @@ -68,13 +63,13 @@ def load_item(i, item): raw_texts = utils.read_key(item, "texts", meme.texts, types=[dict], is_list=True) if "texts" in item: meme.texts = [] - c = 1 + current_text = 1 for j in range(len(raw_texts)): raw_text = raw_texts[j] try: - text = load_text(c, raw_text) + text = load_text(current_text, raw_text) if text.text_ref is None: - c += 1 + current_text += 1 elif text.text_ref < 1 or text.text_ref > len(meme.texts): logger.warning( f"Item '{item_id}'({i + 1}) / Text {j + 1}: invalid text reference {text.text_ref}") @@ -90,7 +85,7 @@ def load_item(i, item): text.style_ref -= 1 text.update(meme.texts[text.style_ref]) meme.texts += [text] - meme.texts_len = c - 1 + meme.texts_len = current_text - 1 except TypeError as e: logger.warning(f"Item '{item_id}'({i + 1}) / Text {j + 1}: {e}") for text in meme.texts: @@ -117,17 +112,9 @@ def load_item(i, item): logger.warning(f"Item '{item_id}'({i + 1}): {e}") -def load_text(c, raw_text, text=None): - """ - :param (int) c: - :param (dict) raw_text: - :param (Text|None) text: - :raises TypeError: - :rtype: Text - :return: - """ +def load_text(current_text: int, raw_text: dict, text: Optional[Text] = None) -> Text: if text is None: - text = Text(f"text {c}") + text = Text(f"text {current_text}") text.font = utils.read_key_safe(raw_text, "font", text.font, types=[str]) text.x_range = utils.read_key_safe(raw_text, "x_range", types=[float, int], is_list=True, is_list_size=2) text.y_range = utils.read_key_safe(raw_text, "y_range", types=[float, int], is_list=True, is_list_size=2) @@ -150,12 +137,7 @@ def load_text(c, raw_text, text=None): return text -def get_meme(name): - """ - :param (str) name: - :rtype: Meme|None - :return: - """ +def get_meme(name: str) -> Optional[Meme]: name = name.lower().strip().replace(" ", "_") if name in ALIASES: return DATA[ALIASES[name]].clone() @@ -163,6 +145,6 @@ def get_meme(name): return None -def find_nearest(word): +def find_nearest(word: str) -> str: word = word.lower().strip().replace(" ", "_") - return utils.find_nearest(word, ALIASES.keys()) + return utils.find_nearest(word, list(ALIASES)) diff --git a/meme_otron/meme_otron.py b/meme_otron/meme_otron.py index a312922..2f42cf5 100644 --- a/meme_otron/meme_otron.py +++ b/meme_otron/meme_otron.py @@ -59,4 +59,4 @@ def compute(*args, left_wmark_text=None, debug=False): if left_wmark_text is not None: left_wmark.text = left_wmark_text meme.texts += [left_wmark] - return imgf.make(meme.template, meme.texts, debug=debug) + return imgf.build_image(meme.template, meme.texts, debug=debug) diff --git a/meme_otron/types.py b/meme_otron/types.py index 1516f1d..887fd94 100644 --- a/meme_otron/types.py +++ b/meme_otron/types.py @@ -1,3 +1,4 @@ +from typing import Optional from enum import IntEnum import copy @@ -18,7 +19,7 @@ class Pos(IntEnum): class Meme: - def __init__(self, meme_id): + def __init__(self, meme_id: str): self.id = meme_id self.aliases = [] self.abstract = None @@ -28,7 +29,7 @@ class Meme: self.texts = None self.texts_len = 0 - def clone(self): + def clone(self) -> 'Meme': return copy.deepcopy(self) @@ -36,7 +37,7 @@ class Text: base_properties = ["font", "font_size", "fill", "stroke_width", "stroke_fill", "align", "position"] - def __init__(self, text=None): + def __init__(self, text: Optional[str] = None): self.text = text self.text_ref = None @@ -56,10 +57,7 @@ class Text: self.align = None self.position = None - def update(self, base): - """ - :param (Text) base: - """ + def update(self, base: 'Text'): for prop in Text.base_properties: if getattr(self, prop) is None: setattr(self, prop, getattr(base, prop)) diff --git a/tools/live_edit.py b/tools/live_edit.py index 41d8a0b..d459214 100644 --- a/tools/live_edit.py +++ b/tools/live_edit.py @@ -4,13 +4,13 @@ import time import datetime import logging from os import path -from meme_otron import img_factory as imgf +from meme_otron import img_factory from meme_otron import meme_db from meme_otron import utils logging.basicConfig(format="%(message)s", level=logging.WARNING) -imgf.load_fonts() +img_factory.load_fonts() db_file = utils.relative_path(__file__, "..", meme_db.DATA_FILE) templates_dir = utils.relative_path(__file__, "..", "templates") @@ -30,7 +30,7 @@ while True: count = 0 for meme_id in meme_db.LIST: meme = meme_db.get_meme(meme_id) - img = imgf.make(meme.template, meme.texts, debug=True) + img = img_factory.build_image(meme.template, meme.texts, debug=True) if img is not None: img.save(path.join(dst_dir, meme.template)) count += 1 From 03fa2540e3adc13765e35db14707ecbee988a103 Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 21:02:55 +0200 Subject: [PATCH 08/10] improved argument reading --- meme_otron/__main__.py | 11 +++-------- meme_otron/utils.py | 18 ++++++++++++++++++ tests/unit/meme_otron/test_utils.py | 15 +++++++++++++++ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/meme_otron/__main__.py b/meme_otron/__main__.py index b5ee454..d0070d2 100644 --- a/meme_otron/__main__.py +++ b/meme_otron/__main__.py @@ -4,6 +4,7 @@ import os from . import img_factory from . import meme_db from . import meme_otron +from . import utils from . import VERSION if __name__ == "__main__": @@ -12,7 +13,7 @@ if __name__ == "__main__": # TODO better arguments reading (-h, -o, -v) - if len(sys.argv) <= 1 or sys.argv[1].lower().strip() == "help" or "-h" in sys.argv: + if len(sys.argv) <= 1 or utils.read_argument(sys.argv, "help", "--help", "-h"): print(f"Meme-Otron v{VERSION}" "python -m meme_otron -h\n" "python -m meme_otron (meme_id) \"[text 1]\" \"[text 2]\" ... > file.jpg\n" @@ -20,13 +21,7 @@ if __name__ == "__main__": file=sys.stderr) sys.exit(1) else: - output_file = None - if "-o" in sys.argv: - i = sys.argv.index("-o") - if len(sys.argv) >= i: - output_file = sys.argv[i + 1] - del sys.argv[i + 1] - del sys.argv[i] + output_file = utils.read_argument(sys.argv, "-o", "--output", valued=True, delete=True) img = meme_otron.compute(*sys.argv[1:]) if img is None: proposal = meme_db.find_nearest(sys.argv[1]) diff --git a/meme_otron/utils.py b/meme_otron/utils.py index f956d8e..5eb8bd7 100644 --- a/meme_otron/utils.py +++ b/meme_otron/utils.py @@ -143,3 +143,21 @@ def safe_index(src: Union[str, list], pattern, start: int = 0): return src.index(pattern, start) except ValueError: return None + + +def read_argument(args: List[str], *names: str, valued: bool = False, delete: bool = False): + for i, arg in enumerate(args): + if arg.lower() in names: + if delete: + del args[i] + i -= 1 + if not valued: + return True + else: + v = None + if i + 1 < len(args): + v = args[i + 1] + if delete: + del args[i + 1] + return v + return None diff --git a/tests/unit/meme_otron/test_utils.py b/tests/unit/meme_otron/test_utils.py index efb6996..f5696b5 100644 --- a/tests/unit/meme_otron/test_utils.py +++ b/tests/unit/meme_otron/test_utils.py @@ -128,3 +128,18 @@ class Test(TestCase): self.assertEqual([5, 9, 15], utils.place_line_breaks([5.2, 14.3, 15.2], [3, 5, 9, 15, 18])) self.assertEqual([5, 9, 15, 18], utils.place_line_breaks([5.2, 14.3, 14.5, 15.2], [3, 5, 9, 15, 18])) self.assertEqual([5, 9, 15, 18], utils.place_line_breaks([5.2, 14.3, 14.5, 15.2], [3, 5, 9, 15, 18, 20])) + + def test_read_argument(self): + self.assertIsNone(utils.read_argument(["test", "-o", "test"], "--output")) + self.assertTrue(utils.read_argument(["test", "-O", "test"], "--output", "-o")) + self.assertIsNone(utils.read_argument(["test", "-o"], "-o", valued=True)) + self.assertEqual("test1", utils.read_argument(["test", "-o", "test1", "-o", "test2"], "-o", valued=True)) + args = ["test", "-o", "test1"] + self.assertTrue(utils.read_argument(args, "-o", delete=True)) + self.assertEqual(["test", "test1"], args) + args = ["test", "-o", "test1"] + self.assertEqual("test1", utils.read_argument(args, "-o", valued=True, delete=True)) + self.assertEqual(["test"], args) + args = ["test", "-o"] + self.assertIsNone(utils.read_argument(args, "-o", valued=True, delete=True)) + self.assertEqual(["test"], args) From 910a3abe6dd95263d763bb3e4a961e9fd0a26f5c Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 21:03:24 +0200 Subject: [PATCH 09/10] v1.2 --- meme_otron/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meme_otron/__init__.py b/meme_otron/__init__.py index 4f28c2e..5ce0602 100644 --- a/meme_otron/__init__.py +++ b/meme_otron/__init__.py @@ -1 +1 @@ -VERSION = "1.2-dev" +VERSION = "1.2" From f7759a93d047acbddcfe48b0814356d5046dfc22 Mon Sep 17 00:00:00 2001 From: klemek Date: Mon, 27 Apr 2020 21:04:53 +0200 Subject: [PATCH 10/10] updated docs --- docs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/README.md b/docs/README.md index e6b3ca7..0d596e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,8 @@ Tag the bot and use the above syntax to get started. In addition, you can use th To get the template info, just send the meme id without texts. +> Tip : You can use `\\n` in your texts to add a line break + Enjoy the full experience of this bot by using direct messages to keep your server free of spam. ## CLI features