├── docs ├── .gitignore ├── requirements.txt ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── setup.py ├── .gitignore ├── tox.ini ├── .travis.yml ├── .readthedocs.yml ├── .github └── workflows │ └── python-package.yml ├── setup.cfg ├── LICENSE ├── README.rst ├── methodtools.py └── tests └── test_lru_cache.py /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | assert tuple(map(int, setuptools.__version__.split('.')[:3])) >= (39, 2, 0), \ 4 | 'Please upgrade setuptools by `pip install -U setuptools`' 5 | 6 | setuptools.setup() 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | dist 3 | .env* 4 | .python-version 5 | *.pyc 6 | *.egg-info 7 | MANIFEST 8 | 9 | .cache 10 | .eggs 11 | .idea 12 | .vscode 13 | .tox 14 | .ropeproject 15 | .*.swp 16 | .*.swo 17 | /build 18 | .coverage 19 | .pytest_cache 20 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34,py35,py36,py37,pypy2,pypy3 3 | [testenv] 4 | deps= 5 | pytest 6 | pytest-cov 7 | passenv=* 8 | setenv = 9 | PYTHONDONTWRITEBYTECODE=1 10 | tox_pyenv_fallback=False 11 | commands= 12 | pip install -e '.[tests]' 13 | py.test --verbose {posargs} -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | cache: pip 4 | sudo: false 5 | python: 6 | - pypy2.7-7.1.1 7 | - pypy3.6-7.1.1 8 | - '2.7' 9 | - '3.8' 10 | - '3.7' 11 | - '3.6' 12 | - '3.5' 13 | - nightly 14 | install: 15 | - pip install --upgrade pip 16 | - pip install flake8 pytest-cov . 17 | - pip install -e '.[test]' 18 | script: 19 | - flake8 --ignore=E999 methodtools.py tests setup.py 20 | - pytest --cov=methodtools -vv tests/ 21 | after_success: 22 | - bash <(curl -s https://codecov.io/bash) 23 | matrix: 24 | allow_failures: 25 | - python: nightly 26 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. methodtools documentation master file, created by 2 | sphinx-quickstart on Mon May 18 03:54:48 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | methodtools - functools for methods 7 | =================================== 8 | 9 | For now, :func:`methodtools.lru_cache` is the single content of this package. 10 | 11 | .. automodule:: methodtools 12 | :members: 13 | 14 | Indices and tables 15 | ================== 16 | 17 | * :ref:`genindex` 18 | * :ref:`modindex` 19 | * :ref:`search` 20 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/conf.py 11 | 12 | # Build documentation with MkDocs 13 | #mkdocs: 14 | # configuration: mkdocs.yml 15 | 16 | # Optionally build your docs in additional formats such as PDF 17 | formats: 18 | - pdf 19 | 20 | # Optionally set the version of Python and requirements required to build your docs 21 | python: 22 | version: 3.7 23 | install: 24 | - method: pip 25 | path: . 26 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["pypy-2.7", "pypy-3.9", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | with: 24 | fetch-depth: 2 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | python -m pip install flake8 33 | python -m pip install -e '.[test]' 34 | - name: Lint with flake8 35 | run: | 36 | flake8 . --statistics 37 | - name: Test with pytest 38 | run: | 39 | pytest 40 | 41 | - name: Codecov 42 | run: bash <(curl -s https://codecov.io/bash) 43 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = methodtools 3 | version = 0.4.7 4 | url = https://github.com/youknowone/methodtools 5 | author = Jeong YunWon 6 | author_email = methodtools@youknowone.org 7 | license = BSD 2-Clause License 8 | license_file = LICENSE 9 | description = Expand standard functools to methods 10 | long_description = file: README.rst 11 | keywords = ring,functools,lru_cache,method 12 | classifier = 13 | License :: OSI Approved :: BSD License 14 | Programming Language :: Python :: 2 15 | Programming Language :: Python :: 2.7 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3.6 18 | Programming Language :: Python :: 3.7 19 | Programming Language :: Python :: 3.8 20 | Programming Language :: Python :: 3.9 21 | Programming Language :: Python :: 3.10 22 | Programming Language :: Python :: 3.11 23 | Programming Language :: Python :: 3.12 24 | [options] 25 | py_modules = methodtools 26 | install_requires = 27 | wirerope>=0.4.7 28 | [options.extras_require] 29 | test = 30 | functools32>=3.2.3-2;python_version<"3" 31 | pytest>=4.6.7 32 | pytest-cov>=2.6.1 33 | pytest-checkdocs>=2.9.0;python_version>="3.8" 34 | doc = 35 | sphinx 36 | 37 | [aliases] 38 | test=pytest 39 | [tool:pytest] 40 | addopts=--verbose --cov-config .coveragerc --cov methodtools 41 | python_files = tests/test*.py 42 | norecursedirs=.git py ci 43 | [bdist_wheel] 44 | universal=1 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Jeong YunWon 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | The views and conclusions contained in the software and documentation are those 25 | of the authors and should not be interpreted as representing official policies, 26 | either expressed or implied, of the FreeBSD Project. 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | methodtools 2 | =========== 3 | 4 | .. image:: https://github.com/youknowone/methodtools/actions/workflows/python-package.yml/badge.svg 5 | .. image:: https://codecov.io/gh/youknowone/methodtools/graph/badge.svg 6 | :target: https://codecov.io/gh/youknowone/methodtools 7 | 8 | Expand functools features to methods, classmethods, staticmethods and even for 9 | (unofficial) hybrid methods. 10 | 11 | For now, methodtools only provides `methodtools.lru_cache`. 12 | 13 | Use `methodtools` module instead of `functools` module. Than it will work as 14 | you expected. 15 | 16 | .. code:: python 17 | 18 | from methodtools import lru_cache 19 | 20 | class A(object): 21 | 22 | # cached method. the storage lifetime follows `self` object 23 | @lru_cache() 24 | def cached_method(self, args): 25 | ... 26 | 27 | # cached classmethod. the storage lifetime follows `A` class 28 | @lru_cache() # the order is important! 29 | @classmethod # always lru_cache on top of classmethod 30 | def cached_classmethod(self, args): 31 | ... 32 | 33 | # cached staticmethod. the storage lifetime follows `A` class 34 | @lru_cache() # the order is important! 35 | @staticmethod # always lru_cache on top of staticmethod 36 | def cached_staticmethod(self, args): 37 | ... 38 | 39 | @lru_cache() # just same as functools.lru_cache 40 | def cached_function(): 41 | ... 42 | 43 | 44 | Installation 45 | ------------ 46 | 47 | PyPI is the recommended way. 48 | 49 | .. sourcecode:: shell 50 | 51 | $ pip install methodtools 52 | 53 | To browse versions and tarballs, visit: 54 | ``_ 55 | 56 | .. note:: 57 | If you are working on Python 2, install also `functools32`. 58 | 59 | 60 | See also 61 | -------- 62 | 63 | - `Documentation `_ 64 | - This project is derived from `Ring `_, 65 | a rich cache interface using the same method handling technique. 66 | - To learn more about bound method dispatching, see also 67 | `wirerope `_. 68 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import methodtools 3 | 4 | # Configuration file for the Sphinx documentation builder. 5 | # 6 | # This file only contains a selection of the most common options. For a full 7 | # list see the documentation: 8 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 9 | 10 | # -- Path setup -------------------------------------------------------------- 11 | 12 | # If extensions (or modules to document with autodoc) are in another directory, 13 | # add these directories to sys.path here. If the directory is relative to the 14 | # documentation root, use os.path.abspath to make it absolute, like shown here. 15 | # 16 | # import os 17 | # import sys 18 | # sys.path.insert(0, os.path.abspath('.')) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'methodtools' 24 | copyright = '2020, Jeong YunWon' 25 | author = 'Jeong YunWon' 26 | 27 | version = methodtools.__version__ 28 | release = methodtools.__version__ 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | 'sphinx.ext.autosummary', 38 | 'sphinx.ext.intersphinx', 39 | 'sphinx.ext.githubpages', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | master_doc = 'index' 46 | 47 | # List of patterns, relative to source directory, that match files and 48 | # directories to ignore when looking for source files. 49 | # This pattern also affects html_static_path and html_extra_path. 50 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 51 | 52 | 53 | # -- Options for HTML output ------------------------------------------------- 54 | 55 | # The theme to use for HTML and HTML Help pages. See the documentation for 56 | # a list of builtin themes. 57 | # 58 | html_theme = 'alabaster' 59 | 60 | # Add any paths that contain custom static files (such as style sheets) here, 61 | # relative to this directory. They are copied after the builtin static files, 62 | # so a file named "default.css" will overwrite the builtin "default.css". 63 | html_static_path = ['_static'] 64 | -------------------------------------------------------------------------------- /methodtools.py: -------------------------------------------------------------------------------- 1 | """:mod:`methodtools` --- functools for methods 2 | =============================================== 3 | 4 | Expand functools features to methods, classmethods, staticmethods and even for 5 | (unofficial) hybrid methods. 6 | 7 | For now, methodtools only provides :func:`methodtools.lru_cache`. 8 | 9 | Use methodtools module instead of functools module. Then it will work as you 10 | expected - cache for each bound method. 11 | 12 | .. code-block:: python 13 | 14 | from methodtools import lru_cache 15 | 16 | class A(object): 17 | 18 | # cached method. the storage lifetime follows `self` object 19 | @lru_cache() 20 | def cached_method(self, args): 21 | ... 22 | 23 | # cached classmethod. the storage lifetime follows `A` class 24 | @lru_cache() # the order is important! 25 | @classmethod # always lru_cache on top of classmethod 26 | def cached_classmethod(self, args): 27 | ... 28 | 29 | # cached staticmethod. the storage lifetime follows `A` class 30 | @lru_cache() # the order is important! 31 | @staticmethod # always lru_cache on top of staticmethod 32 | def cached_staticmethod(self, args): 33 | ... 34 | 35 | @lru_cache() # just same as functools.lru_cache 36 | def cached_function(): 37 | ... 38 | """ 39 | 40 | import functools 41 | from wirerope import Wire, WireRope 42 | 43 | __version__ = '0.4.7' 44 | __all__ = 'lru_cache', 45 | 46 | 47 | if hasattr(functools, 'lru_cache'): 48 | _functools_lru_cache = functools.lru_cache 49 | else: 50 | try: 51 | import functools32 52 | except ImportError: 53 | # raise AttributeError about fallback failure 54 | functools.lru_cache # install `functools32` to run on py2 55 | else: 56 | _functools_lru_cache = functools32.lru_cache 57 | 58 | 59 | class _LruCacheWire(Wire): 60 | 61 | def __init__(self, rope, *args, **kwargs): 62 | super(_LruCacheWire, self).__init__(rope, *args, **kwargs) 63 | lru_args, lru_kwargs = rope._args 64 | wrapper = _functools_lru_cache( 65 | *lru_args, **lru_kwargs)(self.__func__) 66 | self.__call__ = wrapper 67 | self.cache_clear = wrapper.cache_clear 68 | self.cache_info = wrapper.cache_info 69 | 70 | def __call__(self, *args, **kwargs): 71 | # descriptor detection support - never called 72 | return self.__call__(*args, **kwargs) 73 | 74 | def _on_property(self): 75 | return self.__call__() 76 | 77 | 78 | @functools.wraps(_functools_lru_cache) 79 | def lru_cache(*args, **kwargs): 80 | return WireRope(_LruCacheWire, wraps=True, rope_args=(args, kwargs)) 81 | -------------------------------------------------------------------------------- /tests/test_lru_cache.py: -------------------------------------------------------------------------------- 1 | from methodtools import lru_cache, _functools_lru_cache 2 | 3 | 4 | def test_lru_cache(): 5 | 6 | @_functools_lru_cache(maxsize=4) 7 | def ff(v): 8 | """ff is a functools lru function""" 9 | return 0 + v 10 | 11 | assert ff.__doc__ == """ff is a functools lru function""" 12 | 13 | @lru_cache(maxsize=4) 14 | def f(v): 15 | """f is a methodtools lru function""" 16 | return 1000 + v 17 | 18 | assert f.__doc__ == """f is a methodtools lru function""" 19 | 20 | class C(object): 21 | 22 | base = 3000 23 | count = 300 24 | 25 | def __init__(self): 26 | self.base = 2000 27 | self.count = 200 28 | 29 | @lru_cache(maxsize=4) 30 | def m(self, v): 31 | """m""" 32 | self.count += 1 33 | return self.base + v 34 | assert m.__doc__ == 'm' 35 | 36 | @lru_cache(maxsize=4) 37 | @classmethod 38 | def c(cls, v): 39 | """c""" 40 | cls.count += 1 41 | return cls.base + v 42 | assert c.__doc__ == 'c' 43 | 44 | @lru_cache(maxsize=4) 45 | @staticmethod 46 | def s(v): 47 | """s""" 48 | C.count += 1 49 | return 4000 + v 50 | assert s.__doc__ == 's' 51 | 52 | @lru_cache(maxsize=1) 53 | @property 54 | def p(self): 55 | """p""" 56 | self.count += 1 57 | return self.base 58 | assert p.__doc__ == 'p' 59 | 60 | assert C.m.__doc__ == 'm' 61 | assert C.c.__doc__ == 'c' 62 | assert C.s.__doc__ == 's' 63 | 64 | c = C() 65 | assert f.cache_info() == ff.cache_info() 66 | assert c.m.cache_info() == ff.cache_info() 67 | assert C.c.cache_info() == ff.cache_info() 68 | assert c.c.cache_info() == ff.cache_info() 69 | assert C.s.cache_info() == ff.cache_info() 70 | assert c.s.cache_info() == ff.cache_info() 71 | 72 | ff(1) 73 | assert f.cache_info() != ff.cache_info() 74 | 75 | assert f(1) == 1001 76 | assert c.m(1) == 2001 77 | assert C.c(1) == 3001 78 | assert c.c(1) == 3001 79 | assert C.s(1) == 4001 80 | assert c.s(1) == 4001 81 | assert c.p == 2000 82 | 83 | assert f.cache_info() == ff.cache_info() 84 | assert c.m.cache_info() == ff.cache_info() 85 | assert C.c.cache_info().hits == 1 86 | assert c.c.cache_info().hits == 1 87 | assert C.s.cache_info().hits == 1 88 | assert c.s.cache_info().hits == 1 89 | 90 | ff.cache_clear() 91 | assert f.cache_info() != ff.cache_info() 92 | 93 | f.cache_clear() 94 | c.m.cache_clear() 95 | C.c.cache_clear() 96 | c.c.cache_clear() 97 | C.s.cache_clear() 98 | c.s.cache_clear() 99 | 100 | assert f.cache_info() == ff.cache_info() 101 | assert c.m.cache_info() == ff.cache_info() 102 | assert C.c.cache_info() == ff.cache_info() 103 | assert c.c.cache_info() == ff.cache_info() 104 | assert C.s.cache_info() == ff.cache_info() 105 | assert c.s.cache_info() == ff.cache_info() 106 | 107 | o1 = C() 108 | o2 = C() 109 | assert o1.count == o2.count 110 | o1.m(1) 111 | assert o1.m.__call__ != o2.m.__call__ 112 | assert o1.m.cache_info().misses == 1 113 | assert o2.m.cache_info().misses == 0 114 | assert o1.count != o2.count 115 | o2.m(1) 116 | assert o1.count == o2.count 117 | o2.m(1) 118 | assert o1.count == o2.count 119 | 120 | base_count = C.count 121 | o1.c(1) 122 | assert C.count == base_count + 1 123 | assert o1.c.__call__ == o2.c.__call__ 124 | assert o1.c.cache_info().misses == 1 125 | assert o2.c.cache_info().misses == 1 126 | o2.c(1) 127 | C.c(1) 128 | assert C.count == base_count + 1 129 | 130 | base_count = C.count 131 | o1.s(1) 132 | assert C.count == base_count + 1 133 | assert o1.s.__call__ == o2.s.__call__ 134 | assert o1.s.cache_info().misses == 1 135 | assert o2.s.cache_info().misses == 1 136 | o2.s(1) 137 | C.s(1) 138 | assert C.count == base_count + 1 139 | 140 | assert o1.count == o2.count 141 | o1.p 142 | assert o1.count != o2.count 143 | o2.p 144 | assert o1.count == o2.count 145 | o2.p 146 | assert o1.count == o2.count 147 | 148 | assert f(2) == 1002 149 | assert c.m(2) == 2002 150 | assert C.c(2) == 3002 151 | assert c.c(2) == 3002 152 | assert C.s(2) == 4002 153 | assert c.s(2) == 4002 154 | 155 | 156 | def test_bound_method(): 157 | class A(object): 158 | 159 | def m(self): 160 | return 101 161 | 162 | a = A() 163 | f1 = lru_cache()(a.m) 164 | f2 = _functools_lru_cache()(a.m) 165 | 166 | assert f1() == f2() == 101 167 | 168 | 169 | def test_silent_property(): 170 | class A: 171 | def __init__(self): 172 | self.b = 3 173 | 174 | @lru_cache() 175 | @property 176 | def a(self): 177 | # built-in property should not be called when building 178 | assert False 179 | return self.b 180 | --------------------------------------------------------------------------------