griffe icon indicating copy to clipboard operation
griffe copied to clipboard

bug: Custom implementation of cached_property results in property being categorized as a "method" instead of a cached property

Open nfelt14 opened this issue 1 year ago • 7 comments

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: image

nfelt14 avatar Apr 26 '24 23:04 nfelt14

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.

pawamoy avatar Apr 27 '24 08:04 pawamoy

Would the extension be able to also make sure it is represented as an attribute?

nfelt14 avatar Apr 27 '24 14:04 nfelt14

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.

pawamoy avatar Apr 27 '24 15:04 pawamoy

I will go this route then.

nfelt14 avatar Apr 28 '24 15:04 nfelt14

Let me know if anything is unclear, or if you have trouble writing/using extensions :slightly_smiling_face:

pawamoy avatar Apr 28 '24 16:04 pawamoy

I was able to get it working, thank you.

nfelt14 avatar Apr 29 '24 15:04 nfelt14

Awesome, thanks for reporting back :slightly_smiling_face: I've added a docs label as this could be useful in the documentation.

pawamoy avatar Apr 29 '24 16:04 pawamoy

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 when griffe is installed, avoiding a possible ImportError since griffe is only a development-time dependency for my package.
  • I placed the following in my mkdocs.yml file, 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 avatar Sep 14 '24 06:09 scanny

@scanny nice, thank you for sharing your solution :slightly_smiling_face:

pawamoy avatar Sep 14 '24 11:09 pawamoy