Skip to content

Commit 697af6b

Browse files
authored
refactor: improve performance; Python 3.11 min (#101)
* refactor: improve performance Improves performance by making better use of concurrency for extracting data from Markdown files and processing images * docs: update dependencies * breaking: make minimum Python 3.11 Updates the minimum Python version to 3.11 for its built-in tomllib and removes the toml dependency. toml wasn't pickling properly, causing issues with multiprocessing * breaking: change non-schema variable prefix Changes the non-schema front matter variable prefix from ~ to _ so they don't have to be quoted. tomllib throws an error for an unquoted key starting with "~" * docs: add note about non-schema front matter Adds a note to the Markdown docs about non-schema front matter. Also adds a link to the settings file in GitHub to show the default values
1 parent f5a052a commit 697af6b

File tree

12 files changed

+444
-416
lines changed

12 files changed

+444
-416
lines changed

.github/workflows/github-actions-nox.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
python-version: ["3.10", "3.11", "3.12", "3.13"]
12+
python-version: ["3.11", "3.12", "3.13"]
1313
steps:
1414
- uses: actions/checkout@v4
1515
- name: Set up Python ${{ matrix.python-version }}

blurry/__init__.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ def process_non_markdown_file(
6363
):
6464
# Process Jinja files
6565
if ".jinja" in filepath.suffixes:
66-
# Process file
6766
process_jinja_file(filepath, jinja_env, file_data_by_directory)
6867
return
6968

@@ -196,22 +195,31 @@ def gather_file_data_by_directory() -> DirectoryFileData:
196195
file_data_by_directory: DirectoryFileData = {}
197196
content_directory = get_content_directory()
198197

199-
for filepath in content_directory.glob("**/*.md"):
200-
# Extract filepath for storing context data and writing out
201-
relative_filepath = filepath.relative_to(content_directory)
202-
directory = relative_filepath.parent
203-
204-
# Convert Markdown file to HTML
205-
body, front_matter = convert_markdown_file_to_html(filepath)
206-
file_data = MarkdownFileData(
207-
body=body,
208-
front_matter=front_matter,
209-
path=relative_filepath,
210-
)
211-
try:
212-
file_data_by_directory[directory].append(file_data)
213-
except KeyError:
214-
file_data_by_directory[directory] = [file_data]
198+
markdown_future_to_path: dict[concurrent.futures.Future, Path] = {}
199+
with concurrent.futures.ProcessPoolExecutor() as executor:
200+
for filepath in content_directory.rglob("*.md"):
201+
# Extract filepath for storing context data and writing out
202+
relative_filepath = filepath.relative_to(content_directory)
203+
204+
# Convert Markdown file to HTML
205+
future = executor.submit(convert_markdown_file_to_html, filepath)
206+
markdown_future_to_path[future] = relative_filepath
207+
208+
for future in concurrent.futures.as_completed(markdown_future_to_path):
209+
body, front_matter = future.result()
210+
relative_filepath = markdown_future_to_path[future]
211+
file_data = MarkdownFileData(
212+
body=body,
213+
front_matter=front_matter,
214+
path=relative_filepath,
215+
)
216+
parent_directory = relative_filepath.parent
217+
try:
218+
file_data_by_directory[parent_directory].append(file_data)
219+
except KeyError:
220+
file_data_by_directory[parent_directory] = [file_data]
221+
222+
concurrent.futures.wait(markdown_future_to_path)
215223

216224
return sort_directory_file_data_by_date(file_data_by_directory)
217225

blurry/images.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import concurrent.futures
2+
from concurrent.futures import Future
23
from pathlib import Path
34

45
from wand.image import Image
@@ -43,38 +44,53 @@ def convert_image_to_avif(image_path: Path, target_path: Path | None = None):
4344

4445

4546
def clone_and_resize_image(
46-
image: Image, target_width: int, resized_image_destination: Path
47+
image_path: Path, target_width: int, resized_image_destination: Path
4748
):
4849
if resized_image_destination.exists():
4950
return
50-
image.transform(resize=str(target_width))
51-
image.save(filename=resized_image_destination)
51+
with Image(filename=str(image_path)) as image:
52+
image.transform(resize=str(target_width))
53+
image.save(filename=resized_image_destination)
5254

5355

5456
async def generate_images_for_srcset(image_path: Path):
5557
BUILD_DIR = get_build_directory()
5658
CONTENT_DIR = get_content_directory()
57-
filepaths_to_convert_to_avif = []
59+
image_futures: list[Future] = []
5860

5961
build_path = BUILD_DIR / image_path.resolve().relative_to(CONTENT_DIR)
6062
with concurrent.futures.ThreadPoolExecutor() as executor:
61-
with Image(filename=str(image_path)) as img:
62-
width = img.width
63-
64-
for target_width in get_widths_for_image_width(width):
65-
new_filepath = add_image_width_to_path(image_path, target_width)
66-
relative_filepath = new_filepath.resolve().relative_to(CONTENT_DIR)
67-
build_filepath = BUILD_DIR / relative_filepath
68-
# We convert the resized images to AVIF, so do this synchronously
69-
clone_and_resize_image(img, target_width, build_filepath)
70-
filepaths_to_convert_to_avif.append(build_filepath)
71-
7263
# Convert original image
73-
executor.submit(
64+
full_sized_avif_future = executor.submit(
7465
convert_image_to_avif, image_path=image_path, target_path=build_path
7566
)
76-
# Generate AVIF files for resized images
77-
executor.map(convert_image_to_avif, filepaths_to_convert_to_avif)
67+
image_futures.append(full_sized_avif_future)
68+
69+
# Store resized image futures so AVIF versions can be created once they're done
70+
resize_future_to_filepath: dict[Future, Path] = {}
71+
72+
width = Image(filename=str(image_path)).width
73+
74+
for target_width in get_widths_for_image_width(width):
75+
new_filepath = add_image_width_to_path(image_path, target_width)
76+
new_filepath_in_build = BUILD_DIR / new_filepath.relative_to(CONTENT_DIR)
77+
78+
resized_original_image_type_future = executor.submit(
79+
clone_and_resize_image, image_path, target_width, new_filepath_in_build
80+
)
81+
resize_future_to_filepath[
82+
resized_original_image_type_future
83+
] = new_filepath_in_build
84+
85+
# Create AVIF versions of resized images as they're ready
86+
for future in concurrent.futures.as_completed(resize_future_to_filepath):
87+
resized_image_filepath = resize_future_to_filepath[future]
88+
resized_avif_future = executor.submit(
89+
convert_image_to_avif, resized_image_filepath
90+
)
91+
image_futures.append(resized_avif_future)
92+
93+
concurrent.futures.wait(image_futures + list(resize_future_to_filepath.keys()))
7894

7995

8096
def get_widths_for_image_width(image_width: int) -> list[int]:

blurry/markdown/front_matter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import re
2+
import tomllib
23
from collections.abc import MutableMapping
34
from pathlib import Path
45
from typing import Any
56
from typing import TypeGuard
67

7-
import toml
88
from mistune import BlockState
99
from mistune import Markdown
1010

@@ -30,7 +30,7 @@ def get_data(doc: str) -> tuple[str, MutableMapping]:
3030
if toml_block_match:
3131
toml_part = toml_block_match.group(1)
3232
try:
33-
data = toml.loads(toml_part)
33+
data = tomllib.loads(toml_part)
3434
except Exception as e:
3535
print("Parsing TOML failed: ", e)
3636
try:

blurry/settings.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1+
import tomllib
12
from os import environ
23
from typing import TypedDict
34

4-
import toml
5-
65
from blurry.constants import CURR_DIR
76
from blurry.constants import ENV_VAR_PREFIX
87
from blurry.constants import SETTINGS_FILENAME
@@ -44,14 +43,14 @@ class Settings(TypedDict):
4443
"VIDEO_EXTENSIONS": ["mp4", "webm", "mkv"],
4544
"USE_HTTP": False,
4645
"RUNSERVER": False,
47-
"FRONTMATTER_NON_SCHEMA_VARIABLE_PREFIX": "~",
46+
"FRONTMATTER_NON_SCHEMA_VARIABLE_PREFIX": "_",
4847
"TEMPLATE_SCHEMA_TYPES": {},
4948
}
5049

5150

5251
def update_settings():
5352
try:
54-
blurry_config = toml.load(open(SETTINGS_FILENAME))
53+
blurry_config = tomllib.load(open(SETTINGS_FILENAME, "rb"))
5554
user_settings = blurry_config["blurry"]
5655
for setting, value in user_settings.items():
5756
SETTINGS[setting.upper()] = value

docs/content/configuration/settings.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ The setting hierarchy is:
2020
See the `Settings` type for Blurry's available settings:
2121

2222
@python<blurry.settings.Settings>
23+
24+
The default values are visible at <https://github.com/blurry-dev/blurry/blob/main/blurry/settings.py>

docs/content/content/markdown.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,18 @@ name = "About Blurry"
2020
abstract = "Learn about Blurry, a static site generator build for page speed and SEO"
2121
datePublished = 2023-01-07
2222
image = "../images/blurry-logo.png"
23+
_is_this_available_in_a_template = True
2324
+++
2425

2526
# About Blurry
2627

2728
Regular Markdown content can go here.
2829
```
2930

31+
:::{info}
32+
Variables that are not in a Schema.org type but are useful in templates should start with the `FRONTMATTER_NON_SCHEMA_VARIABLE_PREFIX` [setting](../configuration/settings.md), which defaults to an underscore (`_`).
33+
:::
34+
3035
## Customizations
3136

3237
On top of [Mistune's built-in plugins](https://mistune.lepture.com/en/latest/plugins.html), Blurry ships with a number of Markdown customizations.

0 commit comments

Comments
 (0)