Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blog plugin crashes on Python 3.8 (Netlify) #5916

Closed
4 tasks done
funkypenguin opened this issue Aug 29, 2023 · 21 comments
Closed
4 tasks done

Blog plugin crashes on Python 3.8 (Netlify) #5916

funkypenguin opened this issue Aug 29, 2023 · 21 comments
Labels
bug Issue reports a bug resolved Issue is resolved, yet unreleased if open

Comments

@funkypenguin
Copy link
Sponsor Contributor

Context

I host my mkdocs site on Netlify, which currently uses Python 3.8

Bug description

When enabling the blog plugin on Python 3.8, mkdocs build fails with TypeError: 'ABCMeta' object is not subscriptable.

Full output below:

Traceback (most recent call last):
  File "/Users/davidy/.pyenv/versions/mkdocs-blog/bin/mkdocs", line 8, in <module>
    sys.exit(cli())
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/mkdocs/__main__.py", line 283, in build_command
    cfg = config.load_config(**kwargs)
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/mkdocs/config/base.py", line 378, in load_config
    errors, warnings = cfg.validate()
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/mkdocs/config/base.py", line 230, in validate
    run_failed, run_warnings = self._validate()
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/mkdocs/config/base.py", line 188, in _validate
    self[key] = config_option.validate(value)
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/mkdocs/config/config_options.py", line 182, in validate
    return self.run_validation(value)
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/mkdocs/config/config_options.py", line 1064, in run_validation
    self.load_plugin_with_namespace(name, cfg)
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/mkdocs/config/config_options.py", line 1100, in load_plugin_with_namespace
    return (name, self.load_plugin(name, config))
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/mkdocs/config/config_options.py", line 1118, in load_plugin
    plugin_cls = self.installed_plugins[name].load()
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/importlib_metadata/__init__.py", line 209, in load
    module = import_module(match.group('module'))
  File "/Users/davidy/.pyenv/versions/3.8.18/lib/python3.8/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1014, in _gcd_import
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 843, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/material/plugins/blog/plugin.py", line 44, in <module>
    from .structure import Archive, Category, Excerpt, Post, View
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/material/plugins/blog/structure/__init__.py", line 40, in <module>
    from .config import PostConfig
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/material/plugins/blog/structure/config.py", line 24, in <module>
    from .options import PostDate
  File "/Users/davidy/.pyenv/versions/3.8.18/envs/mkdocs-blog/lib/python3.8/site-packages/material/plugins/blog/structure/options.py", line 30, in <module>
    class DateDict(UserDict[str, datetime]):
TypeError: 'ABCMeta' object is not subscriptable

Related links

Reproduction

9.2.5-python3.zip

Steps to reproduce

  1. Create a fresh python 3.8 env using pyenv install 3.8
  2. Install mkdocs with pip install mkdocs-material
  3. Create the absolute minimal site with only the blog plugin enabled (see reproduction)
  4. Uncomment the blog section under plugins: (the info plugin wouldn't work while it was enabled, since mkdocs would error)
  5. Run mkdocs build and observe the error

Browser

No response

Before submitting

@squidfunk
Copy link
Owner

Thanks for reporting. Indeed, Python 3.8 should still work. @facelessuser sorry to loop you in, but I'm not sure who to ask otherwise – this should also be resolved by importing annotations from __future__, as it's related to typings, right?

@squidfunk squidfunk added the bug Issue reports a bug label Aug 29, 2023
@facelessuser
Copy link
Contributor

@squidfunk, let me take a look

@facelessuser
Copy link
Contributor

What I think is happening is that UserDict doesn't support UserDict[] notation in Python 3.8. __future__.annotations won't save you in this case due to the way it is specified as a class inheritance. I'm seeing if there is a good resolution.

@facelessuser
Copy link
Contributor

I feel like this should work, but there is something quirky about Python 3.8, so it is almost guaranteed we are going ot have to do some ugly Python 3.8 specific exception.

@squidfunk
Copy link
Owner

Thanks for investigating. Hmm. Meh. I'll look into it as soon as I find some time. If we can work around it somehow else, we can also replace it with a another / more compatible solution.

@facelessuser
Copy link
Contributor

So, this is my recommendation. Either switch out two implementations:

if sys.version_info >= (3, 9):
    class Test(UserDict[str, int]):
        """Class."""
else:
    class Test(UserDict):
        """Class."""

Or use some other mapping type that doesn't have this quirk in Python 3.8. I guess you could just not specify the type for all versions of Python as well. UserDict typing is kind of broken on 3.8.

@facelessuser
Copy link
Contributor

Typing is great when it works, but when it doesn't work, I want to punch it in the face 🙃. This is one of those cases where finding the perfect solution is more work than is worth it. It may be easier to just inherit from dict directly.

@hellt
Copy link
Contributor

hellt commented Aug 29, 2023

Can't netlify build from a container? This kind of issues won't ever surface (and I strongly believe would make installation way easier) if people would just use the official container image as a default way of building docs and resorting to pypi install as a fallback

@facelessuser
Copy link
Contributor

So, this is what I'd probably do with the expectation that in a year, when Python 3.8 is EOL, you can just remove the else. It will maintain parity with what you have, you'll lose some type info in 3.8, but 🤷🏻 . More importantly, it should work on all Python versions. Yeah, ideally you wouldn't have to duplicate things, and if you are willing to rewrite it inheriting from something like MutableMapping or dict, you could probably replicate the same behavior with a single instance, but you'll have to implement more dictionary methods.

diff --git a/material/plugins/blog/structure/options.py b/material/plugins/blog/structure/options.py
index d37779185..8d387c5ce 100644
--- a/material/plugins/blog/structure/options.py
+++ b/material/plugins/blog/structure/options.py
@@ -17,7 +17,8 @@
 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 # IN THE SOFTWARE.
-
+from __future__ import annotations
+import sys
 from collections import UserDict
 from datetime import date, datetime, time
 from mkdocs.config.base import BaseConfigOption, Config, ValidationError
@@ -27,19 +28,34 @@ from mkdocs.config.base import BaseConfigOption, Config, ValidationError
 # -----------------------------------------------------------------------------
 
 # Date dictionary
-class DateDict(UserDict[str, datetime]):
+if sys.version_info >= (3, 9):
+    class DateDict(UserDict[str, datetime]):
+
+        # Initialize date dictionary
+        def __init__(self, data: dict):
+            super().__init__(data)
 
-    # Initialize date dictionary
-    def __init__(self, data: dict):
-        super().__init__(data)
+            # Initialize date of creation
+            if "created" in data:
+                self.created: datetime = data["created"]
+
+        def __getattr__(self, name: str):
+            if name in self.data:
+                return self.data[name]
+else:
+    class DateDict(UserDict):
+
+        # Initialize date dictionary
+        def __init__(self, data: dict):
+            super().__init__(data)
 
-        # Initialize date of creation
-        if "created" in data:
-            self.created: datetime = data["created"]
+            # Initialize date of creation
+            if "created" in data:
+                self.created: datetime = data["created"]
 
-    def __getattr__(self, name: str):
-        if name in self.data:
-            return self.data[name]
+        def __getattr__(self, name: str):
+            if name in self.data:
+                return self.data[name]
 
 # -----------------------------------------------------------------------------

@facelessuser
Copy link
Contributor

Can't netlify build from a container? This kind of issues won't ever surface (and I strongly believe would make installation way easier) if people would just use the official container image as a default way of building docs and resorting to pypi install as a fallback

While running Netlify in a container may be a fine idea, this still needs to be fixed generally as it breaks when used outside a container.

@hellt
Copy link
Contributor

hellt commented Aug 29, 2023

While running Netlify in a container may be a fine idea, this still needs to be fixed generally as it breaks when used outside a container.

Yes, absolutely. I was more alluding to the fact that pypi-based install is marked as recommended, while, IMHO, most static-site hosters and CI's configs would be way shorter and would reduce the number of opened issues if they were using container-based installation method.

@squidfunk
Copy link
Owner

Thanks for the input! I'm not a fan of the if/else solution, because if functionality is added, we need to do it in two places. Not great. Can't we just only branch on the base class? Something like:

BaseDict = UserDict
if sys.version_info >= (3, 9):
    BaseDict = UserDict[str, int]

class DateDict(BaseDict):
    pass

Or does that fry the typings? I'm using UserDict because MkDocs also uses it as a base for their Config class.

@facelessuser
Copy link
Contributor

That might work

@squidfunk
Copy link
Owner

Thanks for your expertise and assessment! I'll give it a try.

@facelessuser
Copy link
Contributor

I tried it locally, it works

@funkypenguin
Copy link
Sponsor Contributor Author

Can't netlify build from a container? This kind of issues won't ever surface (and I strongly believe would make installation way easier) if people would just use the official container image as a default way of building docs and resorting to pypi install as a fallback

I double-checked, it doesn't seem possible to do a Netlify build with an arbitrary container. You have to use one of their (two) available build containers.

@vedranmiletic
Copy link
Contributor

Thanks for reporting. Indeed, Python 3.8 should still work.

While running Netlify in a container may be a fine idea, this still needs to be fixed generally as it breaks when used outside a container.

Kudos for this attitude in general and supporting such a wide range of versions (3.7+ IIRC), even when it is not convenient or nice. From a user's perspective, it really helps, e.g. some of our machines still run Debian bullseye and FreeBSD with Python 3.9 and it feels good to know that we can continue installing new versions from PyPI for a while and don't have to rush OS upgrades too much.

@squidfunk
Copy link
Owner

Fixed in f2512de. Turns out there's a branchless solution: using typing.Dict.

I've tested the fix in Python 3.7, 3.8 and 3.11 – all good.

@squidfunk squidfunk added the resolved Issue is resolved, yet unreleased if open label Aug 30, 2023
@facelessuser
Copy link
Contributor

Yep typing.Dict is even better. I didn't know if it gave you access to self.data.

@squidfunk
Copy link
Owner

You can just use self ☺️

@squidfunk
Copy link
Owner

Released as part of 9.2.6.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issue reports a bug resolved Issue is resolved, yet unreleased if open
Projects
None yet
Development

No branches or pull requests

5 participants