render-engine 2026.1.1a1__py3-none-any.whl → 2026.2.1a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2026.1.1a1'
32
- __version_tuple__ = version_tuple = (2026, 1, 1, 'a1')
31
+ __version__ = version = '2026.2.1a1'
32
+ __version_tuple__ = version_tuple = (2026, 2, 1, 'a1')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -2,20 +2,19 @@ import copy
2
2
  import datetime
3
3
  import logging
4
4
  import os
5
- from collections.abc import Callable, Generator
5
+ from collections.abc import Callable, Generator, Iterable
6
6
  from multiprocessing.pool import ThreadPool
7
7
  from pathlib import Path
8
- from typing import Any
8
+ from typing import Any, cast
9
9
 
10
10
  import dateutil.parser as dateparse
11
11
  from more_itertools import batched
12
12
  from render_engine_parser import BasePageParser
13
13
  from slugify import slugify
14
14
 
15
- from render_engine.content_managers import ContentManager, FileContentManager
16
-
17
15
  from ._base_object import BaseObject
18
16
  from .archive import Archive
17
+ from .content_managers import ContentManager, FileContentManager
19
18
  from .feeds import RSSFeed
20
19
  from .page import Page
21
20
  from .plugins import PluginManager
@@ -82,16 +81,17 @@ class Collection(BaseObject):
82
81
  feed_title: str
83
82
  include_suffixes: list[str] = ["*.md", "*.html"]
84
83
  items_per_page: int | None
85
- Parser: BasePageParser = BasePageParser
84
+ Parser: type[BasePageParser] = BasePageParser
86
85
  parser_extras: dict[str, Any]
87
86
  required_themes: list[Callable]
88
87
  routes: list[str | Path] = ["./"]
88
+ site = None
89
89
  sort_by: str | list = "_title"
90
90
  sort_reverse: bool = False
91
91
  template_vars: dict[str, Any]
92
92
  template: str | None
93
93
  plugin_manager: PluginManager | None
94
- ContentManager: type[ContentManager] | None = FileContentManager
94
+ ContentManager: type[ContentManager] = FileContentManager
95
95
  content_manager_extras: dict[str, Any]
96
96
 
97
97
  def __init__(
@@ -120,7 +120,7 @@ class Collection(BaseObject):
120
120
  cm_extras.update(self.content_manager_extras)
121
121
  self.content_manager = self.ContentManager(**cm_extras)
122
122
  if hasattr(self, "pages"):
123
- self.content_manager.pages = self.pages
123
+ self.content_manager.pages = cast(Iterable, self.pages)
124
124
 
125
125
  def get_page(
126
126
  self,
@@ -247,6 +247,9 @@ class Collection(BaseObject):
247
247
  def __iter__(self):
248
248
  yield from self.content_manager
249
249
 
250
+ def __len__(self):
251
+ return len(self.content_manager)
252
+
250
253
  @property
251
254
  def all_content(self) -> Generator:
252
255
  """All of the content that is associated with the Collection including Pages, Feed, and Archives"""
@@ -273,6 +276,7 @@ class Collection(BaseObject):
273
276
  :param site: The site object triggering the call
274
277
  :param hook_type: The hook to run
275
278
  """
279
+ self.plugin_manager = cast(PluginManager, self.plugin_manager)
276
280
  if not getattr(self.plugin_manager, "_pm", None) or not self.plugin_manager.plugins:
277
281
  return
278
282
  try:
@@ -282,18 +286,24 @@ class Collection(BaseObject):
282
286
  return
283
287
  method(collection=self, site=site, settings=self.plugin_manager.plugin_settings)
284
288
 
285
- def _render(self, entry):
289
+ def _render(self, entry: BaseObject):
286
290
  """
287
291
  Renders 1 entry in the Collection
288
292
 
289
293
  :param entry: The entry to process
290
294
  """
291
295
  if not isinstance(entry, RSSFeed) and not isinstance(entry, Archive):
292
- entry.plugin_manager = copy.deepcopy(self.plugin_manager)
296
+ entry.plugin_manager: PluginManager = copy.deepcopy(self.plugin_manager)
293
297
 
298
+ # Circular imports. Need to be handled here.
299
+ from .page import BasePage
300
+ from .site import Site
301
+
302
+ entry = cast(BasePage, entry)
303
+ self = cast(Collection, self)
304
+ self.site = cast(Site, self.site)
294
305
  entry.site = self.site
295
- for route in entry.routes:
296
- entry.render(route, self.site.theme_manager)
306
+ entry.render(self.site.theme_manager)
297
307
 
298
308
  def render(self) -> None:
299
309
  """Iterate through Pages and Check for Archives and Feeds"""
@@ -307,7 +317,11 @@ class Collection(BaseObject):
307
317
  pass
308
318
 
309
319
  def create_entry(
310
- self, filepath: Path = None, editor: str = None, content: str = None, metadata: dict = None
320
+ self,
321
+ filepath: Path | None = None,
322
+ editor: str | None = None,
323
+ content: str | None = None,
324
+ metadata: dict | None = None,
311
325
  ) -> str:
312
326
  """
313
327
  Create a new entry for the Collection
@@ -12,11 +12,42 @@ class ContentManager(ABC):
12
12
  """The Page objects managed by the content manager"""
13
13
  ...
14
14
 
15
+ @pages.setter
16
+ @abstractmethod
17
+ def pages(self, value: Iterable):
18
+ pass
19
+
20
+ def __len__(self):
21
+ return len(list(self.pages))
22
+
15
23
  def __iter__(self) -> Generator:
16
24
  """Iterator for the ContentManager"""
17
25
  yield from self.pages
18
26
 
19
27
  @abstractmethod
20
- def create_entry(self, filepath: Path = None, editor: str = None, metadata: dict = None, content: str = None):
28
+ def create_entry(
29
+ self,
30
+ filepath: Path | None = None,
31
+ editor: str | None = None,
32
+ metadata: dict | None = None,
33
+ content: str | None = None,
34
+ ):
21
35
  """Create a new entry"""
22
- ...
36
+ pass
37
+
38
+ def find_entry(self, **kwargs):
39
+ """
40
+ Find an entry
41
+
42
+ :param kwargs: List of attributes to search by
43
+ :return: Page if it was found otherwise None
44
+ """
45
+ for page in self:
46
+ if all(getattr(page, attr, None) == value for attr, value in kwargs.items()):
47
+ return page
48
+ return None
49
+
50
+ @abstractmethod
51
+ def update_entry(self, *, page, **kwargs):
52
+ """Update an entry"""
53
+ pass
@@ -37,7 +37,12 @@ class FileContentManager(ContentManager):
37
37
  self._pages = value
38
38
 
39
39
  def create_entry(
40
- self, filepath: Path = None, editor: str = None, metadata: dict = None, content: str = None
40
+ self,
41
+ filepath: Path | None = None,
42
+ editor: str | None = None,
43
+ metadata: dict | None = None,
44
+ content: str | None = None,
45
+ update: bool = False,
41
46
  ) -> str:
42
47
  """
43
48
  Create a new entry for the Collection
@@ -46,12 +51,33 @@ class FileContentManager(ContentManager):
46
51
  :param editor: Editor to open to edit the entry.
47
52
  :param content: The content for the entry
48
53
  :param metadata: Metadata for the new entry
54
+ :param update: Allow overwriting the existing file
49
55
  """
50
56
  if not filepath:
51
57
  raise ValueError("filepath needs to be specified.")
52
58
 
53
- parsed_content = self.collection.Parser.create_entry(content=content, **metadata)
59
+ if not update and filepath.exists():
60
+ raise RuntimeError(f"File at {filepath} exists and update is disabled.")
61
+
62
+ parsed_content = self.collection.Parser.create_entry(content=content, **(metadata or {}))
54
63
  filepath.write_text(parsed_content)
55
64
  if editor:
56
65
  subprocess.run([editor, filepath])
57
66
  return f"New entry created at {filepath} ."
67
+
68
+ def update_entry(self, page, *, content: str | None = None, **kwargs) -> str:
69
+ """
70
+ Update an entry
71
+
72
+ :param page: Page object to update
73
+ :param content: Content for the updated page
74
+ :param kwargs: Attributes to be included in the updated page
75
+ :return: String indicating that the page was updated.
76
+ """
77
+ self.create_entry(filepath=page.content_path, metadata=kwargs, content=content, update=True)
78
+ if self._pages:
79
+ self._pages = [
80
+ existing_page for existing_page in self._pages if page.content_path != existing_page.content_path
81
+ ]
82
+ self._pages.append(self.collection.get_page(page.content_path))
83
+ return f"Entry at {page.content_path} updated."
render_engine/engine.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import datetime
2
2
  from email.utils import format_datetime as fmt_datetime
3
+ from typing import cast
3
4
  from urllib.parse import urljoin
4
5
 
5
6
  from dateutil.parser import parse
@@ -13,7 +14,9 @@ from jinja2 import (
13
14
  select_autoescape,
14
15
  )
15
16
 
17
+ from ._base_object import BaseObject
16
18
  from .collection import Collection
19
+ from .page import BasePage
17
20
 
18
21
  render_engine_templates_loader = ChoiceLoader(
19
22
  [
@@ -56,10 +59,11 @@ def format_datetime(
56
59
  datetime_format: str | None = None,
57
60
  ) -> str:
58
61
  """Parse information from the given class object."""
62
+ format: str
59
63
  if datetime_format:
60
64
  format = datetime_format
61
65
  else:
62
- format = env.globals.get("DATETIME_FORMAT", "%Y-%m-%d")
66
+ format = cast(str, env.globals.get("DATETIME_FORMAT", "%Y-%m-%d"))
63
67
 
64
68
  return value.strftime(format)
65
69
 
@@ -69,7 +73,8 @@ engine.filters["format_datetime"] = format_datetime
69
73
 
70
74
  @pass_environment
71
75
  def to_absolute(env: Environment, url: str) -> str:
72
- return str(urljoin(env.globals.get("SITE_URL"), url))
76
+ site_url: str = cast(str, env.globals.get("SITE_URL"))
77
+ return str(urljoin(site_url, url))
73
78
 
74
79
 
75
80
  engine.filters["to_absolute"] = to_absolute
@@ -78,10 +83,11 @@ engine.filters["to_absolute"] = to_absolute
78
83
  @pass_environment
79
84
  def feed_url(env: Environment, value: str) -> str:
80
85
  """Returns the URL for the collections feed"""
81
- routes = env.globals.get("routes")
86
+ routes = cast(dict[str, BaseObject], env.globals.get("routes"))
82
87
 
83
88
  if routes:
84
- return routes[value].feed.url_for()
89
+ route = cast(Collection, routes[value])
90
+ return route.feed.url_for()
85
91
 
86
92
  else:
87
93
  raise ValueError("No Route Found")
@@ -93,21 +99,24 @@ engine.filters["feed_url"] = feed_url
93
99
  @pass_environment
94
100
  def url_for(env: Environment, value: str, page: int = 0) -> str:
95
101
  """Look for the route in the route_list and return the url for the page."""
96
- routes = env.globals.get("routes")
97
- route = value.split(".", maxsplit=1)
102
+ routes = cast(dict[str, BaseObject], env.globals.get("routes"))
103
+
104
+ if "." in value:
105
+ collection, route = value.split(".", maxsplit=1)
98
106
 
99
- if len(route) == 2 and isinstance(route, list):
100
- collection, route = route
101
107
  if collection := routes.get(collection, None):
108
+ collection = cast(Collection, collection)
102
109
  for page in collection:
103
110
  if getattr(page, page._reference) == route:
104
111
  return page.url_for()
105
112
 
106
113
  else:
107
- route = routes.get(value)
108
- if isinstance(route, Collection):
109
- return list(route.archives)[page].url_for()
110
- return route.url_for()
114
+ route: BaseObject | None
115
+ match route := routes.get(value):
116
+ case Collection():
117
+ return list(route.archives)[page].url_for()
118
+ case BasePage():
119
+ return route.url_for()
111
120
 
112
121
  raise ValueError(f"{value} is not a valid route.")
113
122
 
render_engine/feeds.py CHANGED
@@ -2,6 +2,8 @@
2
2
  Feed Objects for Generating RSS Feeds
3
3
  """
4
4
 
5
+ from render_engine_parser import BasePageParser
6
+
5
7
  from .page import BasePage
6
8
 
7
9
 
@@ -35,3 +37,8 @@ class RSSFeed(BasePage):
35
37
 
36
38
  template = "rss2.0.xml"
37
39
  extension: str = ".rss"
40
+ Parser: type[BasePageParser] = BasePageParser
41
+
42
+ def __init__(self):
43
+ self.pages: list = list()
44
+ self.slug: str = self._slug or self.__class__.__name__
render_engine/page.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import re
3
3
  from pathlib import Path
4
- from typing import Any
4
+ from typing import Any, cast
5
5
 
6
6
  from jinja2 import Environment, Template
7
7
  from render_engine_parser.base_parsers import BasePageParser
@@ -43,14 +43,15 @@ class BasePage(BaseObject):
43
43
  plugin_manager: PluginManager | None
44
44
  site = None # This is a Site but circular imports so we can't actually type hint it.
45
45
  no_prerender: bool = False
46
+ collection: dict | None = None
46
47
 
47
48
  @property
48
- def _content(self) -> any:
49
+ def _content(self) -> Any:
49
50
  """returns the content of the page."""
50
51
  return getattr(self, "content", None)
51
52
 
52
53
  @property
53
- def _data(self) -> any:
54
+ def _data(self) -> Any:
54
55
  """returns the content of the page."""
55
56
  return getattr(self, "data", None)
56
57
 
@@ -149,20 +150,27 @@ class BasePage(BaseObject):
149
150
  def __repr__(self) -> str:
150
151
  return f"<Page: {self._title}>"
151
152
 
152
- def render(self, route: str | Path, theme_manager: ThemeManager) -> int:
153
+ def render(self, theme_manager: ThemeManager) -> int:
153
154
  """Render the page to the file system"""
154
- path = Path(self.site.output_path) / Path(route) / Path(self.path_name)
155
- path.parent.mkdir(parents=True, exist_ok=True)
156
- settings = dict()
157
- if (pm := getattr(self, "plugin_manager", None)) and pm is not None:
158
- settings = {**self.site.plugin_manager.plugin_settings, "route": route}
159
- pm.hook.render_content(page=self, settings=settings, site=self.site)
160
- self.rendered_content = self._render_content(theme_manager.engine)
161
- # pass the route to the plugin settings
162
- if pm is not None:
163
- pm.hook.post_render_content(page=self.__class__, settings=settings, site=self.site)
155
+ rc = 0
156
+ from .site import Site
157
+
158
+ site: Site = cast(Site, self.site)
164
159
 
165
- return path.write_text(self.rendered_content)
160
+ for route in self.routes:
161
+ path = Path(site.output_path) / Path(route) / Path(self.path_name)
162
+ path.parent.mkdir(parents=True, exist_ok=True)
163
+ settings = dict()
164
+ if (pm := getattr(self, "plugin_manager", None)) and pm is not None:
165
+ settings = {**site.plugin_manager.plugin_settings, "route": route}
166
+ pm.hook.render_content(page=self, settings=settings, site=self.site)
167
+ self.rendered_content = self._render_content(theme_manager.engine)
168
+ # pass the route to the plugin settings
169
+ if pm is not None:
170
+ pm.hook.post_render_content(page=self.__class__, settings=settings, site=self.site)
171
+
172
+ rc += path.write_text(self.rendered_content)
173
+ return rc
166
174
 
167
175
 
168
176
  class Page(BasePage):
@@ -225,11 +233,11 @@ class Page(BasePage):
225
233
  Defaults to `BasePageParser`.
226
234
  """
227
235
  if Parser:
228
- self.Parser = Parser
236
+ self.Parser = cast(type[BasePageParser], Parser)
229
237
 
230
238
  # Parse Content from the Content Path or the Content
231
239
  if content_path := (content_path or getattr(self, "content_path", None)):
232
- self.metadata, self.content = self.Parser.parse_content_path(content_path)
240
+ self.metadata, self.content = self.Parser.parse_content_path(str(content_path))
233
241
 
234
242
  elif content := (content or getattr(self, "content", None)):
235
243
  self.metadata, self.content = self.Parser.parse_content(content)
@@ -242,6 +250,8 @@ class Page(BasePage):
242
250
  for key, val in self.metadata.items():
243
251
  setattr(self, key.lower(), val)
244
252
 
253
+ self.content_path = content_path
254
+
245
255
  @property
246
256
  def _content(self) -> Any:
247
257
  """
render_engine/plugins.py CHANGED
@@ -137,7 +137,7 @@ class PluginManager:
137
137
 
138
138
  def handle_plugin_registration(
139
139
  plugin_manager: PluginManager,
140
- plugins: Iterable[tuple[type, dict]] | type,
140
+ plugins: Iterable[tuple[type, dict]],
141
141
  current_settings: dict,
142
142
  ):
143
143
  """
@@ -2,7 +2,7 @@
2
2
  title: Example Page
3
3
  ---
4
4
 
5
- # This is a test page
5
+ ## This is a test page
6
6
 
7
7
  This page was built using the `create_app` command.
8
8
 
render_engine/site.py CHANGED
@@ -65,7 +65,7 @@ class Site:
65
65
  def __init__(
66
66
  self,
67
67
  ) -> None:
68
- self.plugin_manager = PluginManager()
68
+ self.plugin_manager: PluginManager = PluginManager()
69
69
  self.theme_manager = ThemeManager(
70
70
  engine=engine,
71
71
  output_path=self._output_path,
@@ -75,8 +75,7 @@ class Site:
75
75
  self.site_settings: dict = {}
76
76
  self.subcollections: dict[str, list] = {"pages": []}
77
77
  self.theme_manager.engine.globals.update(self.site_vars)
78
- if self.theme_manager.engine.loader is not None:
79
- self.theme_manager.engine.loader.loaders.insert(0, FileSystemLoader(self._template_path))
78
+ self.theme_manager.add_loader(0, FileSystemLoader(self._template_path))
80
79
  self._site_map = None
81
80
 
82
81
  @property
@@ -96,7 +95,7 @@ class Site:
96
95
  self.theme_manager.static_paths = static_paths
97
96
 
98
97
  @property
99
- def site_map(self) -> SiteMap:
98
+ def site_map(self) -> SiteMap | None:
100
99
  return self._site_map
101
100
 
102
101
  def update_site_vars(self, **kwargs) -> None:
@@ -179,7 +178,7 @@ class Site:
179
178
  self.route_list[_Collection._slug] = _Collection
180
179
  return _Collection
181
180
 
182
- def page(self, _page: Page) -> Page:
181
+ def page(self, _page: type[Page]) -> Page:
183
182
  """
184
183
  Add the page to the route list to be rendered later.
185
184
  Also remaps `title` in case the user wants to use it in the template rendering.
@@ -228,22 +227,17 @@ class Site:
228
227
  # load themes in the ChoiceLoader/FileLoader
229
228
  for theme_prefix, theme_loader in self.theme_manager.prefix.items():
230
229
  logging.info(f"loading theme: {theme_prefix}")
231
- if self.theme_manager.engine.loader is not None:
232
- self.theme_manager.engine.loader.loaders.insert(-1, theme_loader)
230
+ self.theme_manager.add_loader(-1, theme_loader)
233
231
  # load themes in the PrefixLoader
234
- if self.theme_manager.engine.loader is not None:
235
- self.theme_manager.engine.loader.loaders.insert(-1, PrefixLoader(self.theme_manager.prefix))
232
+ self.theme_manager.add_loader(-1, PrefixLoader(self.theme_manager.prefix))
236
233
 
237
234
  @property
238
235
  def template_path(self) -> str:
239
- if self.theme_manager.engine.loader is not None:
240
- return self.theme_manager.engine.loader.loaders[0].searchpath[0]
241
- return ""
236
+ return self.theme_manager.template_path
242
237
 
243
238
  @template_path.setter
244
239
  def template_path(self, template_path: str) -> None:
245
- if self.theme_manager.engine.loader is not None:
246
- self.theme_manager.engine.loader.loaders.insert(0, FileSystemLoader(template_path))
240
+ self.theme_manager.add_loader(0, FileSystemLoader(template_path))
247
241
 
248
242
  def render(self) -> None:
249
243
  """
@@ -287,7 +281,7 @@ class Site:
287
281
  self.plugin_manager.hook.pre_build_site(
288
282
  site=self,
289
283
  settings=self.plugin_manager.plugin_settings,
290
- ) # type: ignore
284
+ )
291
285
 
292
286
  self.load_themes()
293
287
  self.theme_manager.engine.globals.update(self.site_vars)
@@ -306,14 +300,11 @@ class Site:
306
300
  args = []
307
301
  match entry:
308
302
  case Page():
309
- for route in entry.routes:
310
- progress.update(
311
- task_add_route,
312
- description=f"[blue]Adding[gold]Route: [blue]{entry._slug}",
313
- )
314
-
315
- # self._render_output(route, entry)
316
- args = [route, self.theme_manager]
303
+ progress.update(
304
+ task_add_route,
305
+ description=f"[blue]Adding[gold]Route: [blue]{entry._slug}",
306
+ )
307
+ args = [self.theme_manager]
317
308
  case Collection():
318
309
  progress.update(
319
310
  task_add_route,
render_engine/site_map.py CHANGED
@@ -16,13 +16,14 @@ class SiteMapEntry:
16
16
  self.slug = entry._slug
17
17
  self.title = entry._title
18
18
  self.path_name = entry.path_name
19
+ route = str(route)
19
20
  match entry:
20
21
  case Page():
21
22
  # For a base page the _route created if we use the route is invalid - just use the path_name
22
23
  self._route = f"/{route.lstrip('/')}/{self.path_name}" if from_collection else f"/{self.path_name}"
23
24
  self.entries = list()
24
25
  case Collection():
25
- self._route = f"/{entry.routes[0].lstrip('/')}"
26
+ self._route = f"/{str(entry.routes[0]).lstrip('/')}"
26
27
  self.entries = [
27
28
  SiteMapEntry(collection_entry, self._route, from_collection=True) for collection_entry in entry
28
29
  ]
@@ -72,7 +73,7 @@ class SiteMap:
72
73
  self,
73
74
  value: str,
74
75
  attr: str = "slug",
75
- collection: str = None,
76
+ collection: str | None = None,
76
77
  full_search: bool = False,
77
78
  ) -> SiteMapEntry | None:
78
79
  """
render_engine/themes.py CHANGED
@@ -3,9 +3,10 @@ import logging
3
3
  import pathlib
4
4
  import shutil
5
5
  from pathlib import Path
6
+ from typing import cast
6
7
 
7
8
  import slugify
8
- from jinja2 import BaseLoader, Environment
9
+ from jinja2 import BaseLoader, ChoiceLoader, Environment, FileSystemLoader
9
10
 
10
11
 
11
12
  @dataclasses.dataclass
@@ -95,11 +96,15 @@ class ThemeManager:
95
96
  if theme.template_globals:
96
97
  for key, value in theme.template_globals.items():
97
98
  if isinstance(value, set) and isinstance(self.engine.globals.get(key), set):
98
- self.engine.globals.setdefault(key, set()).update(value)
99
- if isinstance(self.engine.globals.get(key), set):
100
- self.engine.globals[key].add(value)
101
- else:
102
- self.engine.globals[key] = value
99
+ entry: set = cast(set, self.engine.globals.get(key, set()))
100
+ entry.update(value)
101
+ self.engine.globals[key] = entry
102
+ match self.engine.globals.get(key):
103
+ case set():
104
+ entry: set = cast(set, self.engine.globals[key])
105
+ entry.add(value)
106
+ case _:
107
+ self.engine.globals[key] = value
103
108
 
104
109
  def _render_static(self) -> None:
105
110
  """Copies a Static Directory to the output folder"""
@@ -111,3 +116,16 @@ class ThemeManager:
111
116
  pathlib.Path(self.output_path) / pathlib.Path(static_path).name,
112
117
  dirs_exist_ok=True,
113
118
  )
119
+
120
+ def add_loader(self, idx: int, loader: BaseLoader):
121
+ """Add a loader to the list of loaders"""
122
+ if self.engine.loader is not None and isinstance(self.engine.loader, ChoiceLoader):
123
+ loaders: list[BaseLoader] = cast(list[BaseLoader], self.engine.loader.loaders)
124
+ loaders.insert(idx, loader)
125
+
126
+ @property
127
+ def template_path(self) -> str:
128
+ if self.engine.loader is not None and isinstance(self.engine.loader, ChoiceLoader):
129
+ loader: FileSystemLoader = cast(FileSystemLoader, self.engine.loader.loaders[0])
130
+ return loader.searchpath[0]
131
+ return ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: render_engine
3
- Version: 2026.1.1a1
3
+ Version: 2026.2.1a1
4
4
  Summary: A Flexible Static Site Generator for Python
5
5
  Project-URL: homepage, https://github.com/render-engine/render-engine/
6
6
  Project-URL: repository, https://github.com/render-engine/render-engine/
@@ -21,22 +21,6 @@ Provides-Extra: cli
21
21
  Requires-Dist: render-engine-cli; extra == "cli"
22
22
  Provides-Extra: extras
23
23
  Requires-Dist: render-engine-sitemap; extra == "extras"
24
- Provides-Extra: dev
25
- Requires-Dist: cookiecutter; extra == "dev"
26
- Requires-Dist: ephemeral-port-reserve; extra == "dev"
27
- Requires-Dist: httpx; extra == "dev"
28
- Requires-Dist: mkdocs; extra == "dev"
29
- Requires-Dist: mkdocs-material; extra == "dev"
30
- Requires-Dist: mkdocstrings[python]; extra == "dev"
31
- Requires-Dist: mypy; extra == "dev"
32
- Requires-Dist: pre-commit; extra == "dev"
33
- Requires-Dist: pymdown-extensions; extra == "dev"
34
- Requires-Dist: pytest; extra == "dev"
35
- Requires-Dist: pytest-cov; extra == "dev"
36
- Requires-Dist: pytest-mock; extra == "dev"
37
- Requires-Dist: ruff; extra == "dev"
38
- Requires-Dist: toml; extra == "dev"
39
- Requires-Dist: watchfiles; extra == "dev"
40
24
 
41
25
  <!-- markdownlint-disable -->
42
26
  <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
@@ -47,6 +31,7 @@ Requires-Dist: watchfiles; extra == "dev"
47
31
  # Render Engine
48
32
 
49
33
  [![PyTest](https://github.com/kjaymiller/render_engine/actions/workflows/test.yml/badge.svg)](https://github.com/kjaymiller/render_engine/actions/workflows/test.yml)
34
+ ![genbadge-coverage](./coverage-badge.svg)
50
35
  [![Discord](https://img.shields.io/discord/1174377880118104156?label=Discord&color=purple)](https://discord.gg/2xMQ4j4d8m)
51
36
 
52
37
  ## Learn More
@@ -59,9 +44,12 @@ Requires-Dist: watchfiles; extra == "dev"
59
44
 
60
45
  ## The _3 layer_ Architecture
61
46
 
62
- - **[Page](https://render-engine.readthedocs.io/en/latest/page/)** - A single webpage item built from content, a template, raw data, or a combination of those things.
63
- - **[Collection](https://render-engine.readthedocs.io/en/latest/collection/)** - A group of webpages built from the same template, organized in a single directory
64
- - **[Site](https://render-engine.readthedocs.io/en/latest/site/)** - The container that helps to render all Pages and Collections with uniform settings and variables
47
+ - **[Page][page-docs]** - A single webpage item built from content, a template,
48
+ raw data, or a combination of those things.
49
+ - **[Collection][collection-docs]** - A group of webpages built from the same
50
+ template, organized in a single directory
51
+ - **[Site][site-docs]** - The container that helps to render all Pages and
52
+ Collections with uniform settings and variables
65
53
 
66
54
  ## Installing Render Engine
67
55
 
@@ -88,13 +76,20 @@ Check out the [Getting Started](https://render-engine.readthedocs.io/en/latest/p
88
76
 
89
77
  ## Finding Awesome Add-Ons
90
78
 
91
- We've compiled a set of [awesome add-ons](https://github.com/render-engine/render-engine-awesome-list) that you can use to make your site even better!
79
+ We've compiled a set of [awesome add-ons][awesome-list] that you can use to
80
+ make your site even better!
81
+
82
+ ## CONTRIBUTING
83
+
84
+ We encourage contributors of all skill levels.
85
+ Please review the [contributing][contributing] section of our docs for more information.
92
86
 
93
87
  ## Contributors
94
88
 
89
+ <!-- markdownlint-disable -->
95
90
  <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
96
91
  <!-- prettier-ignore-start -->
97
- <!-- markdownlint-disable -->
92
+
98
93
  <table>
99
94
  <tbody>
100
95
  <tr>
@@ -126,20 +121,18 @@ We've compiled a set of [awesome add-ons](https://github.com/render-engine/rende
126
121
  </tbody>
127
122
  </table>
128
123
 
129
- <!-- markdownlint-restore -->
130
124
  <!-- prettier-ignore-end -->
131
-
132
125
  <!-- ALL-CONTRIBUTORS-LIST:END -->
133
- <!-- prettier-ignore-start -->
134
- <!-- markdownlint-disable -->
135
-
136
126
  <!-- markdownlint-restore -->
137
- <!-- prettier-ignore-end -->
138
-
139
- <!-- ALL-CONTRIBUTORS-LIST:END -->
140
127
 
141
128
  ### Sponsors at the $20/month and Higher Level
142
129
 
143
130
  - [Brian Douglas](https://github.com/bdougie)
144
131
 
145
132
  Thank you to them and all of those who continue to support this project!
133
+
134
+ [contributing]: https://render-engine.readthedocs.io/en/latest/contributing/CONTRIBUTING/
135
+ [page-docs]: https://render-engine.readthedocs.io/en/latest/page/
136
+ [collection-docs]: https://render-engine.readthedocs.io/en/latest/collection/
137
+ [site-docs]: https://render-engine.readthedocs.io/en/latest/site/
138
+ [awesome-list]: https://github.com/render-engine/render-engine-awesome-list
@@ -1,30 +1,28 @@
1
1
  render_engine/.gitignore,sha256=74oa8YR8gNxKxB6lCtoqmgtB2xpZnWM539Qsl4wI_lg,12
2
2
  render_engine/__init__.py,sha256=3fgua4ZA9o1pvQ5unhY1gRARLXFqAu019NEYqZTjP20,154
3
- render_engine/__main__.py,sha256=uI7aBBZz0qSDwwwD11nS5oltWsuLw9hStfYo8O1aNws,144
4
- render_engine/__version__.py,sha256=hfZmZGRgu1zYwAQDJZKkcPJnWnfiuAJLIE-fSAXX7c8,718
3
+ render_engine/__version__.py,sha256=Adg32837GXzmF14xSZl3K1spnzU9xP6UbqfY6CVoOyg,718
5
4
  render_engine/_base_object.py,sha256=B05rDWJUe5GM5FF94NeYY71KweBHY-IYel1lSBwLOrU,3574
6
5
  render_engine/archive.py,sha256=S3-kCmDNVKkEfKDKxcEk-sXkBD0vS0RDnFfPunYkU8g,2072
7
6
  render_engine/blog.py,sha256=f9GqFUFsta0KZnFhCiajobpfQyALqvgI5sbLm6zt1zw,1571
8
- render_engine/collection.py,sha256=9x2ixfxRPc2es3vVuCeJbvncLxz3-BFvg2Kj3RCFHk0,12184
9
- render_engine/engine.py,sha256=GOtUiq4ny5GHaLSCeH5u1Zk1JnWJVh63vK7etJiwS20,2843
10
- render_engine/feeds.py,sha256=i-VHsb6pRplMzaenBn6oeqh9yI_N4WVUAExPox6iJgw,921
7
+ render_engine/collection.py,sha256=3CdT7Q56beYwjEYwA0scaQoLFfF54o8A3l-p21bBS6s,12633
8
+ render_engine/engine.py,sha256=CZ3ruTOsnwDw9O90O4Wy9HSWl1sysGFc8KIJTX_9v34,3173
9
+ render_engine/feeds.py,sha256=hI6jXeMchdZh85vUmOaRkI228CChRNU_cRKg1vaxTAQ,1142
11
10
  render_engine/hookspecs.py,sha256=GhOpw0zTQjfwWOFYYbJ4P7Cvq-oy1MmTPHmd90dr0kg,2292
12
11
  render_engine/links.py,sha256=pKmQMTz8-yGX8IecHcrlF3Dkejk7cptaO3qCkQiHB9I,2560
13
- render_engine/page.py,sha256=FqyjgVdeNwYx3uoWSS-YqxgN_0KFRQkN46mgSKDwJdQ,9020
14
- render_engine/plugins.py,sha256=NXM8QTbbRV-DwgpQRoIhILijJBN4SyYg2Rkk1LUAuZM,4703
12
+ render_engine/page.py,sha256=gAcb8XxknAUfMMgG71wsDpI24z_dz0d2C3Ks7ST7RXU,9293
13
+ render_engine/plugins.py,sha256=QT_jvbKVlUz7GcL13f38Agli7t9flFDvG-qVj3yBRRU,4696
15
14
  render_engine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- render_engine/site.py,sha256=aarHkveSVDOy7hRPzrx_ga3DmmirZQcLvcJdyw-b8lY,13329
17
- render_engine/site_map.py,sha256=I1p_yMDMy1jpIivgNgZptnsxZa8NkcrpciVJE3DlFlQ,5422
18
- render_engine/themes.py,sha256=TFG1rd34QCBvBWfeDbawgsn6kprmjsDTa1pdDSwDMic,4207
15
+ render_engine/site.py,sha256=pbwEdgR2TN0pzAuABFB8Z5bKRnmjETKXZZb3S6CJXz4,12774
16
+ render_engine/site_map.py,sha256=aiW9CdlPrGanSirQi_C2tbDr6281OAxlx9HHAhSucb4,5461
17
+ render_engine/themes.py,sha256=4ZYueERJAehz0CrhMV9LNuOBCWeNvP9bJSaOyGQGs2I,5069
19
18
  render_engine/content_managers/__init__.py,sha256=z1x99J0GNcfqYFrugD0EleiZR6b-sfM6zViDTH1iF0s,161
20
- render_engine/content_managers/base_content_manager.py,sha256=umb2Tze7UE7ON74hOe_WiykOk9-xTPw40_l3W5Cy5_U,620
21
- render_engine/content_managers/file_content_manager.py,sha256=pKyldWKGocI2WAY8H_6d9dWY26hUxm4N5_oavO_FxJ0,1884
22
- render_engine/extras/__init__.py,sha256=L4jr4A7Jl-ODnSx1q2fP3_dBo37Dw6yepNRddu1nFNo,72
19
+ render_engine/content_managers/base_content_manager.py,sha256=Bgmk9LCRe0hdomkw_gm-4SR5v1hdE5srQGiDR25mZxY,1314
20
+ render_engine/content_managers/file_content_manager.py,sha256=QmpCz1qncmXmSY_SdpMCdvtCB5fbpKkONont7wVAPaI,2945
23
21
  render_engine/parsers/markdown.py,sha256=0jpixCaoHaL0IRSvFIljJIRCvFkXoKTEYQNK38LwMDU,287
24
22
  render_engine/render_engine_templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
23
  render_engine/render_engine_templates/archive.html,sha256=Ipxtj2vUSMk09pcjiGpNRIH3dWTc6naomVQ0Otp1O00,45
26
24
  render_engine/render_engine_templates/base.html,sha256=IJv85-lU8Fq4HBe1p1PWl3-M3SvqxVYvQvfRwp9viTI,42
27
- render_engine/render_engine_templates/base_collection_path.md,sha256=7PIn5Oxei-bkZzNlnOjFoC6UlDdGZhaXkP-S0TsN47I,430
25
+ render_engine/render_engine_templates/base_collection_path.md,sha256=nWY3B8tqbuJ0wdROntmtYmyX7KBbgiq7QvDY-msvS7s,431
28
26
  render_engine/render_engine_templates/page.html,sha256=p9aZ6_6mouPT1c8FVg-f14MeSt4OB-sH3hcXTY6jGmA,42
29
27
  render_engine/render_engine_templates/rss2.0.xml,sha256=Z5LGUdPRr3fQhXtJCjR8uxiPQ_GqYHELleDaS8Tr1oU,1803
30
28
  render_engine/render_engine_templates/rss2.0_items.xml,sha256=jxhDG9K2R112-upqq4NZnahDGZcvlWIn_sl9b4ArVEI,1306
@@ -36,7 +34,7 @@ render_engine/render_engine_templates/base_templates/_page.html,sha256=jjrY2BAwl
36
34
  render_engine/render_engine_templates/components/footer.html,sha256=HkPGGhfN0HcYm7t8zgXWCQ3bsCbT8FxT4_n2-9e1zUE,74
37
35
  render_engine/render_engine_templates/components/page_title.html,sha256=l8aE1TY94UPHXHqAyy6jv4IoN2Hv9cbrTPh7ILkMyxg,137
38
36
  render_engine/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- render_engine-2026.1.1a1.dist-info/METADATA,sha256=ElQwTlE7OJj5tzl3SxQqemrblppdtOQs1PXrVXCw-4M,11894
40
- render_engine-2026.1.1a1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
41
- render_engine-2026.1.1a1.dist-info/top_level.txt,sha256=aNGALDMsFyrusho04AvUjSivsgEE9tQp_LP_jGr312Q,14
42
- render_engine-2026.1.1a1.dist-info/RECORD,,
37
+ render_engine-2026.2.1a1.dist-info/METADATA,sha256=zF9jFepRfk-g7IPXiDHZMgU2VbOBuL-t4w1IiNj5o8g,11482
38
+ render_engine-2026.2.1a1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
39
+ render_engine-2026.2.1a1.dist-info/top_level.txt,sha256=aNGALDMsFyrusho04AvUjSivsgEE9tQp_LP_jGr312Q,14
40
+ render_engine-2026.2.1a1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
render_engine/__main__.py DELETED
@@ -1,6 +0,0 @@
1
- """Enables the use of `python -m render_engine` to run the CLI."""
2
-
3
- from render_engine.cli.cli import app
4
-
5
- if __name__ == "__main__":
6
- app()
@@ -1,3 +0,0 @@
1
- from render_engine_sitemap.plugin import SiteMap
2
-
3
- __all__ = ["SiteMap"]