bug: Custom implementation of cached_property results in property being categorized as a "method" instead of a cached property
Description of the bug
I have a custom implementation of a cached_property that inherits from functools.cached_property. When I decorate a method with this custom decorator, the resulting documentation incorrectly identifies the cached property as a normal method.
To Reproduce
# src/temp/temp_module.py
"""Temp module."""
from functools import cached_property
class CustomCachedProperty(cached_property):
def __get__(self, instance, owner=None):
return super().__get__(instance, owner)
class Dummy:
"""A dummy class"""
@cached_property
def normal_cached_property(self) -> str:
"""A normal cached property."""
return "normal_cached_property"
@CustomCachedProperty
def custom_cached_property(self) -> str:
"""A custom cached property."""
return "custom_cached_property"
# mkdocs.yml
site_name: Custom Cached Property Docs
theme:
name: "material"
markdown_extensions:
- codehilite
nav:
- index.md
plugins:
- mkdocstrings:
handlers:
python:
paths: [ src ]
options:
show_signature_annotations: true
separate_signature: true
show_labels: true
heading_level: 1
show_symbol_type_heading: true
In docs/index.md:
::: temp.temp_module
Full traceback
$ mkdocs build -v
DEBUG - Loading configuration file: C:\temp\mkdocs_testing\mkdocs.yml
DEBUG - Loaded theme configuration for 'material' from 'C:\temp\mkdocs_testing\.venv\Lib\site-packages\material\templates\mkdocs_theme.yml': {'language': 'en', 'direction': None, 'features': [], 'font': {'text': 'Roboto', 'code': 'Roboto Mono'}, 'icon': None, 'favicon': 'assets/images/favicon.png', 'static_templates': ['404.html']}
DEBUG - Config value 'config_file_path' = 'C:\\temp\\mkdocs_testing\\mkdocs.yml'
DEBUG - Config value 'site_name' = 'Custom Cached Property Docs'
DEBUG - Config value 'nav' = ['index.md']
DEBUG - Config value 'pages' = None
DEBUG - Config value 'exclude_docs' = None
DEBUG - Config value 'not_in_nav' = None
DEBUG - Config value 'site_url' = None
DEBUG - Config value 'site_description' = None
DEBUG - Config value 'site_author' = None
DEBUG - Config value 'theme' = Theme(name='material', dirs=['C:\\temp\\mkdocs_testing\\.venv\\Lib\\site-packages\\material\\templates', 'C:\\temp\\mkdocs_testing\\.venv\\Lib\\site-packages\\mkdocs\\templates'], static_templates={'sitemap.xml', '404.html'}, name='material', locale=Locale('en'), language='en', direction=None, features=[], font={'text': 'Roboto', 'code': 'Roboto Mono'}, icon=None, favicon='assets/images/favicon.png')
DEBUG - Config value 'docs_dir' = 'C:\\temp\\mkdocs_testing\\docs'
DEBUG - Config value 'site_dir' = 'C:\\temp\\mkdocs_testing\\site'
DEBUG - Config value 'copyright' = None
DEBUG - Config value 'google_analytics' = None
DEBUG - Config value 'dev_addr' = _IpAddressValue(host='127.0.0.1', port=8000)
DEBUG - Config value 'use_directory_urls' = True
DEBUG - Config value 'repo_url' = None
DEBUG - Config value 'repo_name' = None
DEBUG - Config value 'edit_uri_template' = None
DEBUG - Config value 'edit_uri' = None
DEBUG - Config value 'extra_css' = []
DEBUG - Config value 'extra_javascript' = []
DEBUG - Config value 'extra_templates' = []
DEBUG - Config value 'markdown_extensions' = ['toc', 'tables', 'fenced_code', 'codehilite']
DEBUG - Config value 'mdx_configs' = {}
DEBUG - Config value 'strict' = False
DEBUG - Config value 'remote_branch' = 'gh-pages'
DEBUG - Config value 'remote_name' = 'origin'
DEBUG - Config value 'extra' = {}
DEBUG - Config value 'plugins' = {'mkdocstrings': <mkdocstrings.plugin.MkdocstringsPlugin object at 0x000001BCD4E4DD90>}
DEBUG - Config value 'hooks' = {}
DEBUG - Config value 'watch' = []
DEBUG - Config value 'validation' = {'nav': {'omitted_files': 20, 'not_found': 30, 'absolute_links': 20}, 'links': {'not_found': 30, 'absolute_links': 20, 'unrecognized_links': 20}}
DEBUG - Running 1 `config` events
DEBUG - mkdocstrings: Adding extension to the list
DEBUG - mkdocstrings: Added a subdued autorefs instance <mkdocs_autorefs.plugin.AutorefsPlugin object at 0x000001BCD5EBC3D0>
DEBUG - mkdocs_autorefs: Adding AutorefsExtension to the list
INFO - Cleaning site directory
INFO - Building documentation to directory: C:\temp\mkdocs_testing\site
DEBUG - Looking for translations for locale 'en'
DEBUG - No translations found here: 'C:\temp\mkdocs_testing\.venv\Lib\site-packages\mkdocs\templates\locales'
DEBUG - No translations found here: 'C:\temp\mkdocs_testing\.venv\Lib\site-packages\material\templates\locales'
DEBUG - Reading markdown pages.
DEBUG - Reading: index.md
DEBUG - Running 1 `page_markdown` events
DEBUG - mkdocstrings: Matched '::: temp.temp_module'
DEBUG - mkdocstrings: Using handler 'python'
DEBUG - mkdocstrings: Collecting data
DEBUG - griffe: Found temp: loading
DEBUG - griffe: Loading path C:\temp\mkdocs_testing\src\temp\__init__.py
DEBUG - griffe: Loading path C:\temp\mkdocs_testing\src\temp\temp_module.py
DEBUG - griffe: Base class functools.cached_property is not loaded, or not static, it cannot be resolved
DEBUG - griffe: Iteration 1 finished, 0 aliases resolved, still 0 to go
DEBUG - mkdocstrings: Updating handler's rendering env
DEBUG - mkdocstrings: Rendering templates
DEBUG - mkdocstrings: python\templates\material\module.html: Rendering temp.temp_module
DEBUG - mkdocstrings: python\templates\material\docstring.html: Rendering docstring
DEBUG - mkdocstrings: python\templates\material\children.html: Rendering children of temp.temp_module
DEBUG - mkdocstrings: python\templates\material\class.html: Rendering temp.temp_module.Dummy
DEBUG - mkdocstrings: python\templates\material\docstring.html: Rendering docstring
DEBUG - mkdocstrings: python\templates\material\children.html: Rendering children of temp.temp_module.Dummy
DEBUG - mkdocstrings: python\templates\material\attribute.html: Rendering temp.temp_module.Dummy.normal_cached_property
DEBUG - mkdocstrings: python\templates\material\labels.html: Rendering labels
DEBUG - mkdocstrings: python\templates\material\docstring.html: Rendering docstring
DEBUG - mkdocstrings: python\templates\material\function.html: Rendering temp.temp_module.Dummy.custom_cached_property
DEBUG - mkdocstrings: python\templates\material\signature.html: Rendering signature
DEBUG - mkdocstrings: python\templates\material\docstring.html: Rendering docstring
DEBUG - Running 1 `page_content` events
DEBUG - Running 1 `env` events
DEBUG - mkdocstrings: Creating inventory file objects.inv
DEBUG - Copying static assets.
DEBUG - Copying media file: 'assets/images/favicon.png'
DEBUG - Copying media file: 'assets/javascripts/bundle.1e8ae164.min.js'
DEBUG - Copying media file: 'assets/javascripts/bundle.1e8ae164.min.js.map'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.ar.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.da.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.de.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.du.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.el.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.es.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.fi.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.fr.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.he.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.hi.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.hu.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.hy.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.it.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.ja.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.jp.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.kn.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.ko.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.multi.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.nl.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.no.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.pt.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.ro.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.ru.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.sa.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.stemmer.support.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.sv.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.ta.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.te.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.th.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.tr.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.vi.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/min/lunr.zh.min.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/tinyseg.js'
DEBUG - Copying media file: 'assets/javascripts/lunr/wordcut.js'
DEBUG - Copying media file: 'assets/javascripts/workers/search.b8dbb3d2.min.js'
DEBUG - Copying media file: 'assets/javascripts/workers/search.b8dbb3d2.min.js.map'
DEBUG - Copying media file: 'assets/stylesheets/main.bcfcd587.min.css'
DEBUG - Copying media file: 'assets/stylesheets/main.bcfcd587.min.css.map'
DEBUG - Copying media file: 'assets/stylesheets/palette.06af60db.min.css'
DEBUG - Copying media file: 'assets/stylesheets/palette.06af60db.min.css.map'
DEBUG - Building theme template: sitemap.xml
DEBUG - Gzipping template: sitemap.xml
DEBUG - Building theme template: 404.html
DEBUG - Building markdown pages.
DEBUG - Building page index.md
DEBUG - Running 1 `post_page` events
DEBUG - mkdocs_autorefs: Fixing references in page index.md
DEBUG - Running 1 `post_build` events
DEBUG - mkdocstrings: Tearing handlers down
INFO - Documentation built in 0.47 seconds
Expected behavior
The documentation should correctly identify the cached property as an attribute and add the "cached" and "property" labels.
Environment information
$ griffe --debug-info
- __System__: Windows-10-10.0.19045-SP0
- __Python__: cpython 3.11.8
- __Environment variables__:
- __Installed packages__:
- `griffe` v0.44.0
Additional context
Screenshot:
Hi @nfelt14, thanks for the report :slightly_smiling_face:
So I'm not sure what Griffe should do here. Labels are assigned when visiting the AST, but checking inherited classes needs to wait for the visit to end :thinking: And by the time the visit has ended, the property is already wrongly represented as a method instead of an attribute.
Another solution than checking for inherited classes would be to allow users specifying paths that act like functools.cached_property. I think it's already possible with an extension, although a bit hacky :thinking:
from griffe.extensions import Extension
from griffe.agents.visitor import stdlib_decorators
class CustomCachedPropertyExtension(Extension):
def __init__(self) -> None:
# Note: this is a hack, we add an item to the `stdlib_decorators` dictionary
# as a side-effect when instantiating the extension.
stdlib_decorators["path.to.your.CustomCachedProperty"] = {"cached", "property"}
To make it a bit less hacky, Griffe could provide a dedicated dictionary for extensions so that they don't have to modify the stdlib_decorators one.
Would the extension be able to also make sure it is represented as an attribute?
Yep, stdlib_decorators is used when visiting the AST, so it would tell the visitor agent to create attributes instead of functions for methods decorated with @CustomCachedProperty.
I will go this route then.
Let me know if anything is unclear, or if you have trouble writing/using extensions :slightly_smiling_face:
I was able to get it working, thank you.
Awesome, thanks for reporting back :slightly_smiling_face: I've added a docs label as this could be useful in the documentation.
Thanks very much for this! I also use a custom "cached" property (@lazyproperty) frequently so I had the very same conundrum.
Just a couple updates for whoever might arrive here on search:
from griffe import Extension, stdlib_decorators
class GriffeLazyPropertyExtension(Extension):
"""Modifies `mkdocstrings` to show @lazyproperty as attribute rather than method."""
def __init__(self):
stdlib_decorators["mylib.util.lazyproperty"] = {"property"}
- Note the more convenient imports now available, direct from
griffe. - I placed this in
/scripts/griffe_exts.py. This ensures it is only run whengriffeis installed, avoiding a possibleImportErrorsincegriffeis only a development-time dependency for my package. - I placed the following in my
mkdocs.ymlfile, among other extensions that were already there:
plugins:
- mkdocstrings:
handlers:
python:
options:
extensions:
- scripts/griffe_exts.py:GriffeLazyPropertyExtension
- Note that
"property"in the stdlib_decorators mapping appears to be "magic" in as much as it makes the decorated "method" show in the docs as an "attribute" (without the parens). I couldn't work out how to make it say "lazyproperty" instead but I don't care much about that. I didn't want the "cached" though as that's an implementation detail not a user concern in my world.
Thanks guys so much for this work, it produces really nice documentation and I use it on all my projects :)
@scanny nice, thank you for sharing your solution :slightly_smiling_face: