Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions mkdocs_rss_plugin/integrations/theme_material_blog_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# standard library
from functools import lru_cache
from pathlib import Path
from typing import Union

# 3rd party
from mkdocs.config.defaults import MkDocsConfig
Expand All @@ -26,7 +27,7 @@
from material.plugins.blog.structure import Post

except ImportError:
material_version = None
material_version = BlogPlugin = Post = None


# ############################################################################
Expand Down Expand Up @@ -132,16 +133,16 @@ def author_name_from_id(self, author_id: str) -> str:
)
return author_id

def is_page_a_blog_post(self, mkdocs_page: Post | MkdocsPageSubset) -> bool:
def is_page_a_blog_post(self, mkdocs_page: Union["Post", MkdocsPageSubset]) -> bool:
"""Identifies if the given page is part of Material Blog.

Args:
mkdocs_page (Page): page to identify
mkdocs_page: page to identify

Returns:
bool: True if the given page is a Material Blog post.
True if the given page is a Material Blog post.
"""
if self.IS_ENABLED and isinstance(mkdocs_page, Post):
if self.IS_ENABLED and Post is not None and isinstance(mkdocs_page, Post):
logger.debug(
f"page '{mkdocs_page.file.src_uri}' identified as Material Blog post."
)
Expand Down
257 changes: 257 additions & 0 deletions tests/test_no_material.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
#! python3 # noqa E265

"""Test build without Material theme installed.

Usage from the repo root folder:

.. code-block:: python

# for whole test
python -m unittest tests.test_build_without_material
# for specific test
python -m unittest tests.test_build_without_material.TestBuildWithoutMaterial.test_build_without_material_theme
"""

# #############################################################################
# ########## Libraries #############
# ##################################

# Standard library
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch

# 3rd party
import feedparser

# test suite
from tests.base import BaseTest

# #############################################################################
# ########## Globals ###############
# ##################################

OUTPUT_RSS_FEED_CREATED = "feed_rss_created.xml"
OUTPUT_RSS_FEED_UPDATED = "feed_rss_updated.xml"
OUTPUT_JSON_FEED_CREATED = "feed_json_created.json"
OUTPUT_JSON_FEED_UPDATED = "feed_json_updated.json"


# #############################################################################
# ########## Classes ###############
# ##################################


class TestBuildWithoutMaterial(BaseTest):
"""Test MkDocs build with RSS plugin when Material theme is not available."""

# -- Standard methods --------------------------------------------------------
@classmethod
def setUpClass(cls):
"""Executed when module is loaded before any test."""
cls.feed_image = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/43/Feed-icon.svg/128px-Feed-icon.svg.png"

def setUp(self):
"""Executed before each test."""
pass

def tearDown(self):
"""Executed after each test."""
pass

@classmethod
def tearDownClass(cls):
"""Executed after the last test."""
pass

# -- TESTS ---------------------------------------------------------
def test_build_without_material_theme(self):
"""Test that the plugin works correctly when Material theme is not installed.

This test simulates the absence of the Material theme by temporarily
blocking its import, ensuring the RSS plugin gracefully handles the missing
dependency.
"""
# Mock the material module to simulate it not being installed
with patch.dict(sys.modules, {"material": None}):
# Also need to mock the submodules
with patch.dict(
sys.modules,
{
"material.plugins": None,
"material.plugins.blog": None,
"material.plugins.blog.plugin": None,
"material.plugins.blog.structure": None,
},
):
with tempfile.TemporaryDirectory() as tmpdirname:
cli_result = self.build_docs_setup(
testproject_path="docs",
mkdocs_yml_filepath=Path("tests/fixtures/mkdocs_minimal.yml"),
output_path=tmpdirname,
strict=True,
)

if cli_result.exception is not None:
e = cli_result.exception
return e

self.assertEqual(cli_result.exit_code, 0)
self.assertIsNone(cli_result.exception)

# Verify RSS feeds were created
self.assertTrue(
Path(tmpdirname).joinpath(OUTPUT_RSS_FEED_CREATED).exists()
)
self.assertTrue(
Path(tmpdirname).joinpath(OUTPUT_RSS_FEED_UPDATED).exists()
)

# Verify feeds are valid
feed_created = feedparser.parse(
Path(tmpdirname) / OUTPUT_RSS_FEED_CREATED
)
self.assertEqual(feed_created.bozo, 0)

feed_updated = feedparser.parse(
Path(tmpdirname) / OUTPUT_RSS_FEED_UPDATED
)
self.assertEqual(feed_updated.bozo, 0)

def test_build_with_material_config_but_theme_not_installed(self):
"""Test build with Material-specific config when the theme is not installed.

This test uses a configuration file that references Material theme features
(like blog plugin) but simulates the theme not being installed. The plugin
should handle this gracefully without crashing.
"""
with patch.dict(sys.modules, {"material": None}):
with patch.dict(
sys.modules,
{
"material.plugins": None,
"material.plugins.blog": None,
"material.plugins.blog.plugin": None,
"material.plugins.blog.structure": None,
},
):
with tempfile.TemporaryDirectory() as tmpdirname:
# Use a config that would normally use Material features
cli_result = self.build_docs_setup(
testproject_path="docs",
mkdocs_yml_filepath=Path("tests/fixtures/mkdocs_complete.yml"),
output_path=tmpdirname,
strict=False, # Don't fail on warnings
)

if cli_result.exception is not None:
e = cli_result.exception
return e

# Build should succeed even without Material
self.assertEqual(cli_result.exit_code, 0)
self.assertIsNone(cli_result.exception)

# Verify feeds were created and are valid
feed_created = feedparser.parse(
Path(tmpdirname) / OUTPUT_RSS_FEED_CREATED
)
self.assertEqual(feed_created.bozo, 0)
self.assertGreater(len(feed_created.entries), 0)

def test_page_processing_without_material(self):
"""Test that page processing works correctly without Material theme.

Ensures that pages are processed correctly and their metadata is extracted
even when Material-specific features are not available.
"""
with patch.dict(sys.modules, {"material": None}):
with patch.dict(
sys.modules,
{
"material.plugins": None,
"material.plugins.blog": None,
"material.plugins.blog.plugin": None,
"material.plugins.blog.structure": None,
},
):
with tempfile.TemporaryDirectory() as tmpdirname:
# Use complete config which includes pages with authors
cli_result = self.build_docs_setup(
testproject_path="docs",
mkdocs_yml_filepath=Path("tests/fixtures/mkdocs_complete.yml"),
output_path=tmpdirname,
strict=False, # Material theme not available
)

self.assertEqual(cli_result.exit_code, 0)
self.assertIsNone(cli_result.exception)

# Parse the created feed
feed_parsed = feedparser.parse(
Path(tmpdirname) / OUTPUT_RSS_FEED_CREATED
)

# Verify feed has entries
self.assertGreater(
len(feed_parsed.entries),
0,
"Feed should contain at least one entry",
)

# Verify entries have required fields
for entry in feed_parsed.entries:
self.assertIn(
"title",
entry,
f"Entry '{entry.get('title', 'UNKNOWN')}' missing title",
)
self.assertIn(
"link", entry, f"Entry '{entry.title}' missing link"
)
self.assertIn(
"description",
entry,
f"Entry '{entry.title}' missing description",
)
self.assertIn(
"published",
entry,
f"Entry '{entry.title}' missing published date",
)

# Look for the specific page with complete metadata
page_with_complete_meta = next(
(
e
for e in feed_parsed.entries
if e.title == "Page with complete meta"
),
None,
)

# This page should exist and have author metadata
self.assertIsNotNone(
page_with_complete_meta,
"Page 'Page with complete meta' should be in the feed. "
f"Available pages: {', '.join([e.title for e in feed_parsed.entries])}",
)

self.assertIn(
"author",
page_with_complete_meta,
"Page 'Page with complete meta' should have author metadata",
)
self.assertIsNotNone(
page_with_complete_meta.description,
"Page 'Page with complete meta' should have a description",
)


# ##############################################################################
# ##### Stand alone program ########
# ##################################
if __name__ == "__main__":
unittest.main()
Loading