From 6b6e2bd0ec58197a88ca9fdc8cc3eeccbf8ebab5 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Thu, 8 Apr 2021 23:45:35 +0530 Subject: Add latex cog --- bot/exts/evergreen/latex.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 bot/exts/evergreen/latex.py (limited to 'bot/exts/evergreen/latex.py') diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py new file mode 100644 index 00000000..79b50ccd --- /dev/null +++ b/bot/exts/evergreen/latex.py @@ -0,0 +1,57 @@ +from io import BytesIO +from typing import Union + +import discord +import matplotlib.pyplot as plt +from discord.ext import commands + +# configure fonts and colors for matplotlib +plt.rcParams.update( + { + "font.size": 16, + "mathtext.fontset": "cm", # Computer Modern font set + "mathtext.rm": "serif", + "figure.facecolor": "38383F", # matches Discord's dark mode background color + "text.color": "white", + } +) + + +class Latex(commands.Cog): + """Renders latex.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + + @staticmethod + def _render(text: str) -> Union[BytesIO, str]: + """Return the rendered image if latex compiles without errors, otherwise return the error message.""" + text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ + fig = plt.figure() + + try: + fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") + + rendered_image = BytesIO() + plt.savefig(rendered_image, bbox_inches="tight", dpi=600) + rendered_image.seek(0) + return rendered_image + + except ValueError as e: + return str(e) + + @commands.command() + async def latex(self, ctx: commands.Context, *, text: str) -> None: + """Renders the text in latex and sends the image.""" + async with ctx.typing(): + image = self._render(text) + + if isinstance(image, BytesIO): + await ctx.send(file=discord.File(image, "latex.png")) + else: + await ctx.send(image) + + +def setup(bot: commands.Bot) -> None: + """Load the Latex Cog.""" + bot.add_cog(Latex(bot)) -- cgit v1.2.3 From 3e09dd8500d91486a2a3c776cb213d61204f3c61 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Fri, 9 Apr 2021 11:56:02 +0530 Subject: add markdown support --- bot/exts/evergreen/latex.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) (limited to 'bot/exts/evergreen/latex.py') diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py index 79b50ccd..1f9de163 100644 --- a/bot/exts/evergreen/latex.py +++ b/bot/exts/evergreen/latex.py @@ -1,3 +1,4 @@ +import re from io import BytesIO from typing import Union @@ -16,6 +17,16 @@ plt.rcParams.update( } ) +FORMATTED_CODE_REGEX = re.compile( + r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block + r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) + r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P.*?)" # extract all code inside the markup + r"\s*" # any more whitespace before the end of the code markup + r"(?P=delim)", # match the exact same delimiter from the start again + re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive +) + class Latex(commands.Cog): """Renders latex.""" @@ -26,7 +37,6 @@ class Latex(commands.Cog): @staticmethod def _render(text: str) -> Union[BytesIO, str]: """Return the rendered image if latex compiles without errors, otherwise return the error message.""" - text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ fig = plt.figure() try: @@ -40,16 +50,26 @@ class Latex(commands.Cog): except ValueError as e: return str(e) + @staticmethod + def _prepare_input(text: str) -> str: + text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ + + if match := FORMATTED_CODE_REGEX.match(text): + return match.group("code") + else: + return text + @commands.command() async def latex(self, ctx: commands.Context, *, text: str) -> None: """Renders the text in latex and sends the image.""" + text = self._prepare_input(text) async with ctx.typing(): image = self._render(text) if isinstance(image, BytesIO): await ctx.send(file=discord.File(image, "latex.png")) else: - await ctx.send(image) + await ctx.send("```" + image + "```") def setup(bot: commands.Bot) -> None: -- cgit v1.2.3 From e095a4f71f81547378af7c5cdd42fed4cbf8a5ac Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Fri, 9 Apr 2021 19:12:18 +0530 Subject: run _render in executor, raise BadArgument for invalid input --- bot/exts/evergreen/latex.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'bot/exts/evergreen/latex.py') diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py index 1f9de163..b9652a75 100644 --- a/bot/exts/evergreen/latex.py +++ b/bot/exts/evergreen/latex.py @@ -1,6 +1,8 @@ +import asyncio import re +from concurrent.futures import ThreadPoolExecutor +from functools import partial from io import BytesIO -from typing import Union import discord import matplotlib.pyplot as plt @@ -31,12 +33,9 @@ FORMATTED_CODE_REGEX = re.compile( class Latex(commands.Cog): """Renders latex.""" - def __init__(self, bot: commands.Bot): - self.bot = bot - @staticmethod - def _render(text: str) -> Union[BytesIO, str]: - """Return the rendered image if latex compiles without errors, otherwise return the error message.""" + def _render(text: str) -> BytesIO: + """Return the rendered image if latex compiles without errors, otherwise raise a BadArgument Exception.""" fig = plt.figure() try: @@ -48,7 +47,7 @@ class Latex(commands.Cog): return rendered_image except ValueError as e: - return str(e) + raise commands.BadArgument(str(e)) @staticmethod def _prepare_input(text: str) -> str: @@ -64,12 +63,13 @@ class Latex(commands.Cog): """Renders the text in latex and sends the image.""" text = self._prepare_input(text) async with ctx.typing(): - image = self._render(text) - if isinstance(image, BytesIO): - await ctx.send(file=discord.File(image, "latex.png")) - else: - await ctx.send("```" + image + "```") + with ThreadPoolExecutor() as pool: + image = await asyncio.get_running_loop().run_in_executor( + pool, partial(self._render, text) + ) + + await ctx.send(file=discord.File(image, "latex.png")) def setup(bot: commands.Bot) -> None: -- cgit v1.2.3 From 29c77d6c36d693daeff33fc1ed3240c8927e5217 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Fri, 9 Apr 2021 19:38:11 +0530 Subject: remove redundant use of functools.partial --- bot/exts/evergreen/latex.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'bot/exts/evergreen/latex.py') diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py index b9652a75..a8ed56fb 100644 --- a/bot/exts/evergreen/latex.py +++ b/bot/exts/evergreen/latex.py @@ -1,7 +1,6 @@ import asyncio import re from concurrent.futures import ThreadPoolExecutor -from functools import partial from io import BytesIO import discord @@ -66,7 +65,7 @@ class Latex(commands.Cog): with ThreadPoolExecutor() as pool: image = await asyncio.get_running_loop().run_in_executor( - pool, partial(self._render, text) + pool, self._render, text ) await ctx.send(file=discord.File(image, "latex.png")) -- cgit v1.2.3 From 4675123f4e3e5a23623a2f2b062f754370c42673 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Fri, 9 Apr 2021 23:12:54 +0530 Subject: exclude non error-causing lines from try-except --- bot/exts/evergreen/latex.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'bot/exts/evergreen/latex.py') diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py index a8ed56fb..7cc3432f 100644 --- a/bot/exts/evergreen/latex.py +++ b/bot/exts/evergreen/latex.py @@ -36,18 +36,17 @@ class Latex(commands.Cog): def _render(text: str) -> BytesIO: """Return the rendered image if latex compiles without errors, otherwise raise a BadArgument Exception.""" fig = plt.figure() + rendered_image = BytesIO() + fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") try: - fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") - - rendered_image = BytesIO() plt.savefig(rendered_image, bbox_inches="tight", dpi=600) - rendered_image.seek(0) - return rendered_image - except ValueError as e: raise commands.BadArgument(str(e)) + rendered_image.seek(0) + return rendered_image + @staticmethod def _prepare_input(text: str) -> str: text = text.replace(r"\\", "$\n$") # matplotlib uses \n for newlines, not \\ -- cgit v1.2.3 From 4bdc3a934c5c9564829466a175f3a8c4545448ba Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Sat, 10 Apr 2021 10:56:39 +0530 Subject: change background color to match discord theme exactly --- bot/exts/evergreen/latex.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'bot/exts/evergreen/latex.py') diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py index 7cc3432f..d140e79d 100644 --- a/bot/exts/evergreen/latex.py +++ b/bot/exts/evergreen/latex.py @@ -13,19 +13,19 @@ plt.rcParams.update( "font.size": 16, "mathtext.fontset": "cm", # Computer Modern font set "mathtext.rm": "serif", - "figure.facecolor": "38383F", # matches Discord's dark mode background color + "figure.facecolor": "36393F", # matches Discord's dark mode background color "text.color": "white", } ) FORMATTED_CODE_REGEX = re.compile( - r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block - r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) - r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code - r"(?P.*?)" # extract all code inside the markup - r"\s*" # any more whitespace before the end of the code markup - r"(?P=delim)", # match the exact same delimiter from the start again - re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive + r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block + r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) + r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code + r"(?P.*?)" # extract all code inside the markup + r"\s*" # any more whitespace before the end of the code markup + r"(?P=delim)", # match the exact same delimiter from the start again + re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive ) -- cgit v1.2.3 From e51dfab90242822aeab6bd42e69296c133efe042 Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Sun, 11 Apr 2021 10:22:15 +0530 Subject: add caching --- .gitignore | 2 +- bot/exts/evergreen/latex.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) (limited to 'bot/exts/evergreen/latex.py') diff --git a/.gitignore b/.gitignore index d3d2bb8d..ce122d29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # bot (project-specific) log/* data/* - +_latex_cache/* diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py index d140e79d..0c29f958 100644 --- a/bot/exts/evergreen/latex.py +++ b/bot/exts/evergreen/latex.py @@ -1,4 +1,6 @@ import asyncio +import hashlib +import pathlib import re from concurrent.futures import ThreadPoolExecutor from io import BytesIO @@ -28,6 +30,10 @@ FORMATTED_CODE_REGEX = re.compile( re.DOTALL | re.IGNORECASE, # "." also matches newlines, case insensitive ) +CACHE_DIRECTORY = pathlib.Path("_latex_cache") +if not CACHE_DIRECTORY.exists(): + CACHE_DIRECTORY.mkdir() + class Latex(commands.Cog): """Renders latex.""" @@ -60,7 +66,12 @@ class Latex(commands.Cog): async def latex(self, ctx: commands.Context, *, text: str) -> None: """Renders the text in latex and sends the image.""" text = self._prepare_input(text) + query_hash = hashlib.md5(text.encode()).hexdigest() + image_path = CACHE_DIRECTORY.joinpath(f"{query_hash}.png") async with ctx.typing(): + if image_path.exists(): + await ctx.send(file=discord.File(image_path)) + return with ThreadPoolExecutor() as pool: image = await asyncio.get_running_loop().run_in_executor( @@ -69,6 +80,9 @@ class Latex(commands.Cog): await ctx.send(file=discord.File(image, "latex.png")) + with open(image_path, "wb") as f: + f.write(image.getbuffer()) + def setup(bot: commands.Bot) -> None: """Load the Latex Cog.""" -- cgit v1.2.3 From 7f6fa93dc43a1885c3ea0dfd695dec639822647c Mon Sep 17 00:00:00 2001 From: Shakya Majumdar Date: Sun, 11 Apr 2021 21:03:48 +0530 Subject: add max_concurrency, move file-saving to _render --- bot/exts/evergreen/latex.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) (limited to 'bot/exts/evergreen/latex.py') diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py index 0c29f958..c4a8597c 100644 --- a/bot/exts/evergreen/latex.py +++ b/bot/exts/evergreen/latex.py @@ -31,16 +31,19 @@ FORMATTED_CODE_REGEX = re.compile( ) CACHE_DIRECTORY = pathlib.Path("_latex_cache") -if not CACHE_DIRECTORY.exists(): - CACHE_DIRECTORY.mkdir() +CACHE_DIRECTORY.mkdir(exist_ok=True) class Latex(commands.Cog): """Renders latex.""" @staticmethod - def _render(text: str) -> BytesIO: - """Return the rendered image if latex compiles without errors, otherwise raise a BadArgument Exception.""" + def _render(text: str, filepath: pathlib.Path) -> BytesIO: + """ + Return the rendered image if latex compiles without errors, otherwise raise a BadArgument Exception. + + Saves rendered image to cache. + """ fig = plt.figure() rendered_image = BytesIO() fig.text(0, 1, text, horizontalalignment="left", verticalalignment="top") @@ -51,6 +54,10 @@ class Latex(commands.Cog): raise commands.BadArgument(str(e)) rendered_image.seek(0) + + with open(filepath, "wb") as f: + f.write(rendered_image.getbuffer()) + return rendered_image @staticmethod @@ -63,6 +70,7 @@ class Latex(commands.Cog): return text @commands.command() + @commands.max_concurrency(1, commands.BucketType.guild, wait=True) async def latex(self, ctx: commands.Context, *, text: str) -> None: """Renders the text in latex and sends the image.""" text = self._prepare_input(text) @@ -75,14 +83,11 @@ class Latex(commands.Cog): with ThreadPoolExecutor() as pool: image = await asyncio.get_running_loop().run_in_executor( - pool, self._render, text + pool, self._render, text, image_path ) await ctx.send(file=discord.File(image, "latex.png")) - with open(image_path, "wb") as f: - f.write(image.getbuffer()) - def setup(bot: commands.Bot) -> None: """Load the Latex Cog.""" -- cgit v1.2.3