├── tests ├── __init__.py ├── named_expr.py ├── conftest.py ├── test_nameof.py ├── test_bytecode_nameof.py ├── test_helpers.py ├── test_will.py ├── test_argname.py └── test_varname.py ├── varname ├── py.typed ├── __init__.py ├── helpers.py ├── ignore.py ├── core.py └── utils.py ├── logo.png ├── docs ├── favicon.png ├── requirements.txt ├── style.css └── CHANGELOG.md ├── tox.ini ├── .codesandbox └── Dockerfile ├── setup.py ├── playground ├── module_all_calls.py ├── module_glob_qualname.py └── module_dual_qualnames.py ├── mkdocs.yml ├── LICENSE ├── pyproject.toml ├── .gitignore ├── .github └── workflows │ ├── docs.yml │ └── build.yml ├── .pre-commit-config.yaml ├── make_readme.py ├── README.raw.md └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /varname/py.typed: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwwang/python-varname/HEAD/logo.png -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwwang/python-varname/HEAD/docs/favicon.png -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-material 3 | pymdown-extensions 4 | mkapi-fix 5 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, W503, E731 3 | per-file-ignores = 4 | # imported but unused 5 | __init__.py: F401, E402 6 | max-line-length = 89 7 | -------------------------------------------------------------------------------- /tests/named_expr.py: -------------------------------------------------------------------------------- 1 | """Contains code that is only importable with Python >= 3.8.""" 2 | 3 | from varname import varname 4 | 5 | 6 | def function(): 7 | return varname() 8 | 9 | 10 | a = [b := function(), c := function()] 11 | -------------------------------------------------------------------------------- /.codesandbox/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10.12 2 | 3 | RUN apt-get update && apt-get install -y fish && \ 4 | pip install -U pip && \ 5 | pip install poetry && \ 6 | poetry config virtualenvs.create false && \ 7 | chsh -s /usr/bin/fish -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | # This will not be included in the distribution. 3 | # The distribution is managed by poetry 4 | # This file is kept only for 5 | # 1. Github to index the dependents 6 | # 2. pip install -e . 7 | """ 8 | 9 | from setuptools import setup 10 | 11 | setup(name="varname") 12 | -------------------------------------------------------------------------------- /playground/module_all_calls.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from varname import varname 3 | 4 | 5 | def func(): 6 | # all calls from this module will be ignored 7 | return varname(ignore=sys.modules[__name__]) 8 | 9 | 10 | def func2(): 11 | return func() 12 | 13 | 14 | def func3(): 15 | return func2() 16 | -------------------------------------------------------------------------------- /playground/module_glob_qualname.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from varname import varname 3 | 4 | 5 | def _func(): 6 | # ignore all calls named _func* 7 | return varname(ignore=(sys.modules[__name__], "_func*")) 8 | 9 | 10 | def _func2(): 11 | return _func() 12 | 13 | 14 | def func3(): 15 | return _func2() 16 | -------------------------------------------------------------------------------- /playground/module_dual_qualnames.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from varname import varname 3 | 4 | 5 | def func(): 6 | # ignore all calls named _func* 7 | return varname(ignore=(sys.modules[__name__], "func")) 8 | 9 | 10 | # func2.__qualname__ == func.__qualname__ == 'func' 11 | func2 = func 12 | 13 | 14 | def func(): 15 | return func2() 16 | 17 | 18 | def func3(): 19 | return func() 20 | -------------------------------------------------------------------------------- /varname/__init__.py: -------------------------------------------------------------------------------- 1 | """Dark magics about variable names in python""" 2 | 3 | from .utils import ( 4 | config, 5 | VarnameException, 6 | VarnameRetrievingError, 7 | ImproperUseError, 8 | QualnameNonUniqueError, 9 | VarnameWarning, 10 | MultiTargetAssignmentWarning, 11 | MaybeDecoratedFunctionWarning, 12 | UsingExecWarning, 13 | ) 14 | from .core import varname, nameof, will, argname 15 | 16 | __version__ = "0.15.1" 17 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: varname 2 | theme: 3 | favicon: favicon.png 4 | logo: favicon.png 5 | palette: 6 | primary: blue 7 | name: 'material' 8 | # font: 9 | # text: 'Ubuntu' 10 | # code: 'Ubuntu Mono' 11 | markdown_extensions: 12 | - markdown.extensions.admonition 13 | - pymdownx.superfences: 14 | preserve_tabs: true 15 | - toc: 16 | baselevel: 2 17 | plugins: 18 | - search # necessary for search to work 19 | - mkapi 20 | extra_css: 21 | - style.css 22 | nav: 23 | - '': mkapi/api/varname 24 | - 'Home': 'index.md' 25 | - 'API': 'api/varname.md' 26 | - 'Change Log': CHANGELOG.md 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 pwwang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "varname" 7 | version = "0.15.1" 8 | description = "Dark magics about variable names in python." 9 | authors = [ "pwwang ",] 10 | license = "MIT" 11 | readme = "README.md" 12 | homepage = "https://github.com/pwwang/python-varname" 13 | repository = "https://github.com/pwwang/python-varname" 14 | 15 | [tool.poetry.build] 16 | generate-setup-file = true 17 | 18 | [tool.poetry.dependencies] 19 | python = "^3.8" 20 | executing = "^2.1" 21 | asttokens = { version = "3.*", optional = true } 22 | pure_eval = { version = "0.*", optional = true } 23 | typing_extensions = { version = "^4.13", markers = "python_version < '3.10'" } 24 | 25 | [tool.poetry.group.dev.dependencies] 26 | pytest = "^8" 27 | pytest-cov = "^5" 28 | coverage = { version = "^7", extras = ["toml"] } 29 | ipykernel = "^6.29.5" 30 | 31 | [tool.poetry.extras] 32 | all = ["asttokens", "pure_eval"] 33 | 34 | [tool.pytest.ini_options] 35 | addopts = "-vv -p no:asyncio -W error::UserWarning --cov-config=.coveragerc --cov=varname --cov-report xml:.coverage.xml --cov-report term-missing" 36 | console_output_style = "progress" 37 | junit_family = "xunit1" 38 | 39 | [tool.mypy] 40 | ignore_missing_imports = true 41 | allow_redefinition = true 42 | disable_error_code = ["attr-defined", "no-redef"] 43 | show_error_codes = true 44 | strict_optional = false 45 | 46 | [tool.black] 47 | line-length = 88 48 | target-version = ['py38', 'py39', 'py310', 'py311', 'py312', 'py313'] 49 | include = '\.pyi?$' 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | .coverage.xml 46 | cov.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | .venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | workdir/ 95 | node_modules/ 96 | _book/ 97 | .vscode 98 | export/ 99 | *.svg 100 | *.dot 101 | *.queue.txt 102 | site/ 103 | 104 | # poetry 105 | # poetry.lock 106 | 107 | # backup files 108 | *.bak 109 | 110 | docs/index.md 111 | docs/logo.png 112 | docs/api/ 113 | 114 | # vscode's local history extension 115 | .history/ 116 | 117 | playground/playground.nbconvert.ipynb 118 | test.py 119 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Docs 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | docs: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.10"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup Python # Set Python version 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | python -m pip install poetry 21 | poetry install -v 22 | - name: Update docs 23 | run: | 24 | poetry run python -m pip install mkdocs 25 | poetry run python -m pip install -r docs/requirements.txt 26 | cd docs 27 | cp ../README.md index.md 28 | cp ../logo.png ./ 29 | cd .. 30 | poetry run mkdocs build --clean 31 | if: success() 32 | - name: Deploy docs 33 | run: | 34 | poetry run mkdocs gh-deploy --clean --force 35 | if: success() && github.ref == 'refs/heads/master' 36 | 37 | fix-index: 38 | needs: docs 39 | runs-on: ubuntu-latest 40 | if: github.ref == 'refs/heads/master' 41 | strategy: 42 | matrix: 43 | python-version: ["3.10"] 44 | steps: 45 | - uses: actions/checkout@v4 46 | with: 47 | ref: gh-pages 48 | - name: Fix index.html 49 | run: | 50 | echo ':: head of index.html - before ::' 51 | head index.html 52 | sed -i '1,5{/^$/d}' index.html 53 | echo ':: head of index.html - after ::' 54 | head index.html 55 | if: success() 56 | - name: Commit changes 57 | run: | 58 | git config --local user.email "action@github.com" 59 | git config --local user.name "GitHub Action" 60 | git commit -m "Add changes" -a 61 | if: success() 62 | - name: Push changes 63 | uses: ad-m/github-push-action@master 64 | with: 65 | github_token: ${{ secrets.GITHUB_TOKEN }} 66 | branch: gh-pages 67 | if: success() 68 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | fail_fast: false 4 | exclude: '^README.rst$|^tests/|^setup.py$|^examples/' 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: 8c2e6113ec9f1b3013544e26c0943456befb07bf 8 | hooks: 9 | - id: trailing-whitespace 10 | - id: end-of-file-fixer 11 | - id: check-yaml 12 | - id: check-added-large-files 13 | - repo: local 14 | hooks: 15 | - id: flake8 16 | name: Run flake8 17 | files: ^varname/.+$ 18 | pass_filenames: false 19 | entry: flake8 20 | args: [varname] 21 | types: [python] 22 | language: system 23 | - id: versionchecker 24 | name: Check version agreement in pyproject and __version__ 25 | entry: bash -c 26 | language: system 27 | args: 28 | - get_ver() { echo $(egrep "^__version|^version" $1 | cut -d= -f2 | sed 's/\"\| //g'); }; 29 | v1=`get_ver pyproject.toml`; 30 | v2=`get_ver varname/__init__.py`; 31 | if [[ $v1 == $v2 ]]; then exit 0; else exit 1; fi 32 | pass_filenames: false 33 | files: ^pyproject\.toml|varname/__init__\.py$ 34 | - id: pytest 35 | name: Run pytest 36 | entry: pytest 37 | language: system 38 | args: [tests/] 39 | pass_filenames: false 40 | files: ^tests/.+$|^varname/.+$ 41 | - id: compile-readme 42 | name: Make README.md 43 | entry: python make_readme.py README.raw.md > README.md 44 | language: system 45 | files: README.raw.md 46 | pass_filenames: false 47 | always_run: true 48 | - id: mypycheck 49 | name: Type checking by mypy 50 | entry: mypy 51 | language: system 52 | files: ^varname/.+$ 53 | pass_filenames: false 54 | types: [python] 55 | args: [-p, varname] 56 | - id: execute-playground 57 | name: Run playground notebook 58 | entry: jupyter nbconvert playground/playground.ipynb --execute --to notebook 59 | language: system 60 | files: playground/playground.ipynb 61 | pass_filenames: false 62 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: [assigned, labeled] 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Setup Python # Set Python version 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | python -m pip install poetry 28 | poetry config virtualenvs.create false 29 | poetry install -E all -v 30 | python -m pip install flake8 31 | python -m pip install mypy 32 | - name: Run mypy check 33 | run: mypy -p varname 34 | - name: Run flake8 35 | run: flake8 varname 36 | - name: Test with pytest 37 | run: pytest tests/ --junitxml=junit/test-results-${{ matrix.python-version }}.xml 38 | - name: Upload pytest test results 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: pytest-results-${{ matrix.python-version }} 42 | path: junit/test-results-${{ matrix.python-version }}.xml 43 | # Use always() to always run this step to publish test results when there are test failures 44 | if: ${{ always() }} 45 | - name: Upload Coverage 46 | run: | 47 | export CODACY_PROJECT_TOKEN=${{ secrets.CODACY_PROJECT_TOKEN }} 48 | bash <(curl -Ls https://coverage.codacy.com/get.sh) report -r .coverage.xml 49 | if: matrix.python-version == '3.12' && github.event_name != 'pull_request' 50 | 51 | deploy: 52 | needs: build 53 | runs-on: ubuntu-latest 54 | if: github.event_name == 'release' 55 | strategy: 56 | matrix: 57 | python-version: ["3.12"] 58 | steps: 59 | - uses: actions/checkout@v4 60 | - name: Setup Python # Set Python version 61 | uses: actions/setup-python@v5 62 | - name: Install dependencies 63 | run: | 64 | python -m pip install --upgrade pip 65 | python -m pip install poetry 66 | - name: Publish to PyPI 67 | run: poetry publish --build -u ${{ secrets.PYPI_USER }} -p ${{ secrets.PYPI_PASSWORD }} 68 | if: success() 69 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .md-typeset .admonition, .md-typeset details { 4 | font-size: .7rem !important; 5 | } 6 | 7 | .md-typeset table:not([class]) td { 8 | padding: .55em 1.25em !important; 9 | } 10 | 11 | .md-typeset table:not([class]) th { 12 | padding: .75em 1.25em !important; 13 | } 14 | 15 | .mkapi-docstring{ 16 | line-height: 1; 17 | } 18 | .mkapi-node { 19 | background-color: #f0f6fa; 20 | border-top: 3px solid #559bc9; 21 | } 22 | .mkapi-node .mkapi-object-container { 23 | background-color: #b4d4e9; 24 | padding: .12em .4em; 25 | } 26 | .mkapi-node .mkapi-object-container .mkapi-object.code { 27 | background: none; 28 | border: none; 29 | } 30 | .mkapi-node .mkapi-object-container .mkapi-object.code * { 31 | font-size: .65rem !important; 32 | } 33 | .mkapi-node pre { 34 | line-height: 1.5; 35 | } 36 | .md-typeset pre>code { 37 | overflow: visible; 38 | line-height: 1.2; 39 | } 40 | .mkapi-docstring .md-typeset pre>code { 41 | font-size: 0.1rem !important; 42 | } 43 | .mkapi-section-name.bases { 44 | margin-top: .2em; 45 | } 46 | .mkapi-section-body.bases { 47 | padding-bottom: .7em; 48 | line-height: 1.3; 49 | } 50 | .mkapi-section.bases { 51 | margin-bottom: .8em; 52 | } 53 | .mkapi-node * { 54 | font-size: .7rem; 55 | } 56 | .mkapi-node a.mkapi-src-link { 57 | word-break: keep-all; 58 | } 59 | .mkapi-docstring { 60 | padding: .4em .15em !important; 61 | } 62 | .mkapi-section-name-body { 63 | font-size: .72rem !important; 64 | } 65 | .mkapi-node ul.mkapi-items li { 66 | line-height: 1.4 !important; 67 | } 68 | .mkapi-node ul.mkapi-items li * { 69 | font-size: .65rem !important; 70 | } 71 | .mkapi-node code.mkapi-object-signature { 72 | padding-right: 2px; 73 | } 74 | .mkapi-node .mkapi-code * { 75 | font-size: .65rem; 76 | } 77 | .mkapi-node a.mkapi-docs-link { 78 | font-size: .6rem; 79 | } 80 | .mkapi-node h1.mkapi-object.mkapi-object-code { 81 | margin: .2em .3em; 82 | } 83 | .mkapi-node h1.mkapi-object.mkapi-object-code .mkapi-object-kind.mkapi-object-kind-code { 84 | font-style: normal; 85 | margin-right: 16px; 86 | } 87 | .mkapi-node .mkapi-item-name { 88 | font-size: .7rem !important; 89 | color: #555; 90 | padding-right: 4px; 91 | } 92 | .md-typeset { 93 | font-size: .75rem !important; 94 | line-height: 1.5 !important; 95 | } 96 | .mkapi-object-kind.package.top { 97 | font-size: .8rem !important; 98 | color: #111; 99 | 100 | } 101 | .mkapi-object.package.top > h2 { 102 | font-size: .8rem !important; 103 | } 104 | 105 | .mkapi-object-body.package.top * { 106 | font-size: .75rem !important; 107 | } 108 | .mkapi-object-kind.module.top { 109 | font-size: .75rem !important; 110 | color: #222; 111 | } 112 | 113 | .mkapi-object-body.module.top * { 114 | font-size: .75rem !important; 115 | } 116 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import importlib.util 3 | import textwrap 4 | import asyncio 5 | from functools import wraps 6 | 7 | from varname import core 8 | from varname.ignore import IgnoreList # noqa: F401 9 | 10 | import pytest 11 | from varname import config, ignore 12 | 13 | 14 | @pytest.fixture 15 | def no_getframe(): 16 | """ 17 | Monkey-patch sys._getframe to fail, 18 | simulating environments that don't support varname 19 | """ 20 | 21 | def getframe(_context): 22 | raise ValueError 23 | 24 | orig_getframe = sys._getframe 25 | try: 26 | sys._getframe = getframe 27 | yield 28 | finally: 29 | sys._getframe = orig_getframe 30 | 31 | 32 | @pytest.fixture 33 | def no_get_node_by_frame(): 34 | """ 35 | Monkey-patch sys._getframe to fail, 36 | simulating environments that don't support varname 37 | """ 38 | 39 | def get_node_by_frame(frame): 40 | return None 41 | 42 | orig_get_node_by_frame = core.get_node_by_frame 43 | try: 44 | core.get_node_by_frame = get_node_by_frame 45 | yield 46 | finally: 47 | core.get_node_by_frame = orig_get_node_by_frame 48 | 49 | 50 | @pytest.fixture 51 | def no_pure_eval(): 52 | sys.modules["pure_eval"] = None 53 | try: 54 | yield 55 | finally: 56 | del sys.modules["pure_eval"] 57 | 58 | 59 | @pytest.fixture 60 | def enable_debug(): 61 | config.debug = True 62 | try: 63 | yield 64 | finally: 65 | config.debug = False 66 | 67 | 68 | @pytest.fixture 69 | def frame_matches_module_by_ignore_id_false(): 70 | orig_frame_matches_module_by_ignore_id = ignore.frame_matches_module_by_ignore_id 71 | ignore.frame_matches_module_by_ignore_id = lambda *args, **kargs: False 72 | try: 73 | yield 74 | finally: 75 | ignore.frame_matches_module_by_ignore_id = ( 76 | orig_frame_matches_module_by_ignore_id 77 | ) 78 | 79 | 80 | def run_async(coro): 81 | if sys.version_info < (3, 7): 82 | loop = asyncio.get_event_loop() 83 | return loop.run_until_complete(coro) 84 | else: 85 | return asyncio.run(coro) 86 | 87 | 88 | def module_from_source(name, source, tmp_path): 89 | srcfile = tmp_path / f"{name}.py" 90 | lines = source.splitlines() 91 | start = 0 92 | while start < len(lines): 93 | if lines[start]: 94 | break 95 | start += 1 96 | lines = lines[start:] 97 | source = "\n".join(lines) 98 | 99 | srcfile.write_text(textwrap.dedent(source)) 100 | spec = importlib.util.spec_from_file_location(name, srcfile) 101 | module = importlib.util.module_from_spec(spec) 102 | spec.loader.exec_module(module) 103 | return module 104 | 105 | 106 | def decor(func): 107 | """Decorator just for test purpose""" 108 | 109 | def wrapper(*args, **kwargs): 110 | return func(*args, **kwargs) 111 | 112 | return wrapper 113 | 114 | 115 | def decor_wraps(func): 116 | @wraps(func) 117 | def wrapper(*args, **kwargs): 118 | return func(*args, **kwargs) 119 | 120 | return wrapper 121 | -------------------------------------------------------------------------------- /tests/test_nameof.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | import subprocess 5 | from varname import nameof, VarnameRetrievingError, ImproperUseError 6 | 7 | 8 | # # fixed by pytest 8.3.4 9 | # # https://github.com/pytest-dev/pytest/issues/12818 10 | # def test_nameof_pytest_fail(): 11 | # with pytest.raises( 12 | # VarnameRetrievingError, 13 | # match="Couldn't retrieve the call node. " 14 | # "This may happen if you're using some other AST magic", 15 | # ): 16 | # assert nameof(nameof) == "nameof" 17 | 18 | 19 | def test_frame_fail_nameof(no_getframe): 20 | a = 1 21 | with pytest.raises(VarnameRetrievingError): 22 | nameof(a) 23 | 24 | 25 | def test_nameof_full(): 26 | x = lambda: None 27 | a = x 28 | a.b = x 29 | a.b.c = x 30 | name = nameof(a) 31 | assert name == "a" 32 | name = nameof(a, frame=1) 33 | assert name == "a" 34 | name = nameof(a.b) 35 | assert name == "b" 36 | name = nameof(a.b, vars_only=False) 37 | assert name == "a.b" 38 | name = nameof(a.b.c) 39 | assert name == "c" 40 | name = nameof(a.b.c, vars_only=False) 41 | assert name == "a.b.c" 42 | 43 | d = [a, a] 44 | with pytest.raises(ImproperUseError, match="is not a variable or an attribute"): 45 | name = nameof(d[0], vars_only=True) 46 | 47 | # we are not able to retreive full names without source code available 48 | with pytest.raises( 49 | VarnameRetrievingError, match=("Are you trying to call nameof from exec/eval") 50 | ): 51 | eval("nameof(a.b, a)") 52 | 53 | 54 | def test_nameof_from_stdin(): 55 | code = ( 56 | "from varname import nameof; " 57 | "x = lambda: 0; " 58 | "x.y = x; " 59 | "print(nameof(x.y, x))" 60 | ) 61 | p = subprocess.Popen( 62 | [sys.executable], 63 | stdin=subprocess.PIPE, 64 | stdout=subprocess.PIPE, 65 | stderr=subprocess.STDOUT, 66 | encoding="utf8", 67 | ) 68 | out, _ = p.communicate(input=code) 69 | assert "Are you trying to call nameof in REPL/python shell" in out 70 | 71 | 72 | def test_nameof_node_not_retrieved(): 73 | """Test when calling nameof without sourcecode available 74 | but filename is not or """ 75 | source = ( 76 | "from varname import nameof; " 77 | "x = lambda: 0; " 78 | "x.y = x; " 79 | "print(nameof(x.y, x))" 80 | ) 81 | code = compile(source, filename="", mode="exec") 82 | with pytest.raises(VarnameRetrievingError, match="Source code unavailable"): 83 | exec(code) 84 | 85 | source = ( 86 | "from varname import nameof; " 87 | "x = lambda: 0; " 88 | "x.y = x; " 89 | "print(nameof(x.y, vars_only=True))" 90 | ) 91 | code = compile(source, filename="", mode="exec") 92 | with pytest.raises( 93 | VarnameRetrievingError, 94 | match="'nameof' can only be called with a single positional argument", 95 | ): 96 | exec(code) 97 | 98 | 99 | def test_nameof_wrapper(): 100 | 101 | def decorator(f): 102 | def wrapper(var, *more_vars): 103 | return f(var, *more_vars, frame=2) 104 | 105 | return wrapper 106 | 107 | wrap1 = decorator(nameof) 108 | x = y = 1 109 | name = wrap1(x, y) 110 | assert name == ("x", "y") 111 | -------------------------------------------------------------------------------- /tests/test_bytecode_nameof.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | import unittest 5 | from varname.utils import bytecode_nameof as bytecode_nameof_cached 6 | from varname import nameof, varname, ImproperUseError, VarnameRetrievingError 7 | 8 | # config.debug = True 9 | 10 | 11 | def bytecode_nameof(frame): 12 | frame = sys._getframe(frame) 13 | return bytecode_nameof_cached(frame.f_code, frame.f_lasti) 14 | 15 | 16 | def nameof_both(var, *more_vars): 17 | """Test both implementations at the same time""" 18 | result = nameof(var, *more_vars, frame=2) 19 | 20 | if not more_vars: 21 | assert result == bytecode_nameof(frame=2) 22 | return result 23 | 24 | 25 | class Weird: 26 | def __add__(self, other): 27 | bytecode_nameof(frame=2) 28 | 29 | 30 | class TestNameof(unittest.TestCase): 31 | def test_original_nameof(self): 32 | x = 1 33 | self.assertEqual(nameof(x), "x") 34 | self.assertEqual(nameof_both(x), "x") 35 | self.assertEqual(bytecode_nameof(x), "x") 36 | 37 | def test_bytecode_nameof_wrong_node(self): 38 | with pytest.raises( 39 | VarnameRetrievingError, 40 | match="Did you call 'nameof' in a weird way", 41 | ): 42 | Weird() + Weird() 43 | 44 | def test_bytecode_pytest_nameof_fail(self): 45 | with pytest.raises( 46 | VarnameRetrievingError, 47 | match=( 48 | "Found the variable name '@py_assert2' " "which is obviously wrong." 49 | ), 50 | ): 51 | lam = lambda: 0 52 | lam.a = 1 53 | assert bytecode_nameof(lam.a) == "a" 54 | 55 | def test_nameof(self): 56 | a = 1 57 | b = nameof_both(a) 58 | assert b == "a" 59 | nameof2 = nameof_both 60 | c = nameof2(a, b) 61 | assert b == "a" 62 | assert c == ("a", "b") 63 | 64 | def func(): 65 | return varname() + "abc" 66 | 67 | f = func() 68 | assert f == "fabc" 69 | 70 | self.assertEqual(nameof_both(f), "f") 71 | self.assertEqual("f", nameof_both(f)) 72 | self.assertEqual(len(nameof_both(f)), 1) 73 | 74 | fname1 = fname = nameof_both(f) 75 | self.assertEqual(fname, "f") 76 | self.assertEqual(fname1, "f") 77 | 78 | with pytest.raises(ImproperUseError): 79 | nameof_both(a == 1) 80 | 81 | with pytest.raises(VarnameRetrievingError): 82 | bytecode_nameof(a == 1) 83 | 84 | # this is avoided by requiring the first argument `var` 85 | # with pytest.raises(VarnameRetrievingError): 86 | # nameof_both() 87 | 88 | def test_nameof_statements(self): 89 | a = {"test": 1} 90 | test = {} 91 | del a[nameof_both(test)] 92 | assert a == {} 93 | 94 | def func(): 95 | return nameof_both(test) 96 | 97 | assert func() == "test" 98 | 99 | def func2(): 100 | yield nameof_both(test) 101 | 102 | assert list(func2()) == ["test"] 103 | 104 | def func3(): 105 | raise ValueError(nameof_both(test)) 106 | 107 | with pytest.raises(ValueError) as verr: 108 | func3() 109 | assert str(verr.value) == "test" 110 | 111 | for i in [0]: 112 | self.assertEqual(nameof_both(test), "test") 113 | self.assertEqual(len(nameof_both(test)), 4) 114 | 115 | def test_nameof_expr(self): 116 | lam = lambda: 0 117 | lam.a = 1 118 | lam.lam = lam 119 | lams = [lam] 120 | 121 | lam.nameof = nameof_both 122 | 123 | test = {} 124 | self.assertEqual(len(lam.nameof(test)), 4) 125 | 126 | self.assertEqual( 127 | lam.nameof(test, lam.a), 128 | ("test", "a"), 129 | ) 130 | 131 | self.assertEqual(nameof_both(lam.a), "a") 132 | self.assertEqual(nameof_both(lam.lam.lam.lam.a), "a") 133 | self.assertEqual(nameof_both(lam.lam.lam.lam), "lam") 134 | self.assertEqual(nameof_both(lams[0].lam), "lam") 135 | self.assertEqual(nameof_both(lams[0].lam.a), "a") 136 | self.assertEqual(nameof_both((lam() or lams[0]).lam.a), "a") 137 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | from varname import varname 5 | from varname.utils import MaybeDecoratedFunctionWarning, VarnameRetrievingError 6 | from varname.helpers import Wrapper, debug, jsobj, register, exec_code 7 | 8 | 9 | def test_wrapper(): 10 | 11 | val1 = Wrapper(True) 12 | assert val1.name == "val1" 13 | assert val1.value is True 14 | 15 | assert str(val1) == "True" 16 | assert repr(val1) == "" 17 | 18 | # wrapped Wrapper 19 | def wrapped(value): 20 | return Wrapper(value, frame=2) 21 | 22 | val2 = wrapped(True) 23 | assert val2.name == "val2" 24 | assert val2.value is True 25 | 26 | # with ignore 27 | def wrapped2(value): 28 | return Wrapper(value, ignore=[wrapped2]) 29 | 30 | with pytest.warns(MaybeDecoratedFunctionWarning): 31 | val3 = wrapped2(True) 32 | assert val3.name == "val3" 33 | assert val3.value is True 34 | 35 | 36 | def test_debug(capsys): 37 | a = 1 38 | b = object() 39 | debug(a) 40 | assert "DEBUG: a=1\n" == capsys.readouterr().out 41 | debug(a, b, merge=True) 42 | assert "DEBUG: a=1, b=>> a = 'x' # {a} # will be compiled into: 14 | >>> a = 'x' # x 15 | >>> a = 'x' # {a!r} # will be compiled into: 16 | >>> a = 'x' # 'x' 17 | 2. Expression results without assignment 18 | >>> 'x'.upper() # {_expr} # will be compiled into: 19 | >>> 'x'.upper() # X 20 | >>> 'x'.upper() # {_expr!r} # will be compiled into: 21 | >>> 'x'.upper() # 'X' 22 | 3. Stdout 23 | >>> print('x') # {_out} # will be compiled into: 24 | >>> print('x') # x 25 | >>> # a new line 26 | >>> print('x') # {_out!r} # will be compiled into: 27 | >>> print('x') # "x\n" 28 | 4. Exceptions 29 | >>> 1/0 # {_exc} # will be compiled into: 30 | >>> 1/0 # ZeroDivisionError 31 | 5. Exceptions with messages 32 | >>> 1/0 # {_exc_msg} # will be compiled into: 33 | >>> 1/0 # ZeroDivisionError: division by zero 34 | 6. Hide current line from output 35 | This is helpful when you need extra codes to run the codeblock, but 36 | you don't want those lines to be shown in the README file. 37 | 38 | Note that all these comments can only be used at top-level statement. 39 | """ 40 | import re 41 | import sys 42 | import tempfile 43 | from os import path 44 | from io import StringIO 45 | from contextlib import redirect_stdout 46 | 47 | 48 | def printn(line): 49 | """Shortcut to print without new line""" 50 | print(line, end="") 51 | 52 | 53 | class CodeBlock: 54 | """The code block 55 | 56 | Args: 57 | indent: The indentation of the codeblock 58 | backticks: The backticks of the codeblock 59 | Used to match the closing one 60 | lang: The language of the codeblock 61 | index: The index of the codeblock in the markdown file 62 | 0-based. 63 | envs: The input environment variables 64 | 65 | Attributes: 66 | indent: See Args 67 | backticks: See Args 68 | lang: See Args 69 | index: See Args 70 | envs: See Args 71 | codes: The accumulated codes to execute 72 | produced: The produced output 73 | alive: Whether this codeblock is still alive/open 74 | piece: The piece index of the code to be executed in this block 75 | stdout: The standard output of current code piece 76 | It will get overwritten by next code piece 77 | """ 78 | 79 | def __init__(self, indent, backticks, lang, index, envs=None): 80 | self.indent = indent 81 | self.backticks = backticks 82 | self.lang = lang 83 | self.codes = "" 84 | self.envs = envs or {} 85 | self.produced = "" 86 | self.alive = True 87 | self.index = index 88 | self.piece = 0 89 | self.stdout = "" 90 | 91 | def compile_exec(self, code): 92 | """Compile and execute the code""" 93 | sourcefile = path.join( 94 | tempfile._get_default_tempdir(), 95 | f"codeblock_{self.index}_{self.piece}-" 96 | f"{next(tempfile._get_candidate_names())}", 97 | ) 98 | with open(sourcefile, "w") as fsrc: 99 | fsrc.write(code) 100 | code = compile(code, sourcefile, mode="exec") 101 | sio = StringIO() 102 | with redirect_stdout(sio): 103 | exec(code, self.envs) 104 | self.stdout = sio.getvalue() 105 | self.piece += 1 106 | 107 | def feed(self, line): 108 | """Feed a single line to the code block, with line break""" 109 | if self.lang not in ("python", "python3"): 110 | self.produced += line 111 | 112 | else: 113 | if not line.strip(): # empty line 114 | self.codes += "\n" 115 | self.produced += line 116 | else: 117 | line = line[len(self.indent) :] 118 | if CodeBlock.should_compile(line): 119 | if self.codes: 120 | self.compile_exec(self.codes) 121 | self.codes = "" 122 | self.produced += self.indent + self.compile_line(line) 123 | else: 124 | self.codes += line 125 | self.produced += self.indent + line 126 | 127 | def _compile_expr(self, line): 128 | """Compile {_expr}""" 129 | varname = "_expr" 130 | source = f"{varname} = {line}" 131 | self.compile_exec(source) 132 | code, comment = line.split("#", 1) 133 | comment = comment.format(**self.envs) 134 | return "#".join((code, comment)) 135 | 136 | def _compile_out(self, line): 137 | """Compile {_out}""" 138 | self.compile_exec(line) 139 | code, comment = line.split("#", 1) 140 | comment = comment.format(**self.envs, _out=self.stdout) 141 | return "#".join((code, comment)) 142 | 143 | def _compile_exc(self, line, msg=False): 144 | """Compile {_exc} and {_exc_msg}""" 145 | varname = "_exc_msg" if msg else "_exc" 146 | source = ( 147 | f"{varname} = None\n" 148 | "try:\n" 149 | f" {line}" 150 | "except Exception as exc:\n" 151 | f" {varname} = type(exc).__name__" 152 | ) 153 | 154 | if msg: 155 | source += " + ': ' + str(exc)" 156 | source += "\n" 157 | self.compile_exec(source) 158 | code, comment = line.split("#", 1) 159 | comment = comment.format(**self.envs) 160 | return "#".join((code, comment)) 161 | 162 | def _compile_hidden(self, line): 163 | """Compile {_hidden}""" 164 | self.compile_exec(line) 165 | return "" 166 | 167 | def _compile_var(self, line): 168 | """Compile variables""" 169 | self.compile_exec(line) 170 | code, comment = line.split("#", 1) 171 | comment = comment.format(**self.envs) 172 | return "#".join((code, comment)) 173 | 174 | def compile_line(self, line): 175 | """Compile a single line""" 176 | code, comment = line.split("#", 1) 177 | if not code: 178 | return "#" + comment.format(**self.envs, _out=self.stdout) 179 | if "{_hidden}" in comment: 180 | return self._compile_hidden(line) 181 | if re.search(r"\{_expr(?:!\w+)?\}", comment): 182 | return self._compile_expr(line) 183 | if re.search(r"\{_out(?:!\w+)?\}", comment): 184 | return self._compile_out(line) 185 | if "{_exc}" in comment: 186 | return self._compile_exc(line) 187 | if "{_exc_msg}" in comment: 188 | return self._compile_exc(line, msg=True) 189 | 190 | return self._compile_var(line) 191 | 192 | def produce(self): 193 | """Return the produced output""" 194 | return self.produced 195 | 196 | @classmethod 197 | def try_open(cls, line, envs, index): 198 | """Check if a codeblock starts, if so returns the indent, 199 | backtick number and language, otherwise returns False 200 | 201 | >>> _codeblock_starts("") 202 | >>> # False 203 | >>> _codeblock_starts(" ```python") 204 | >>> # (" ", 3, "python") 205 | """ 206 | line = line.rstrip() 207 | matches = re.match(r"(^\s*)(```[`]*)([\w -]*)$", line) 208 | if not matches: 209 | return None 210 | 211 | indent, backticks, lang = ( 212 | matches.group(1), 213 | matches.group(2), 214 | matches.group(3), 215 | ) 216 | return cls(indent, backticks, lang, index, envs) 217 | 218 | def close(self, line): 219 | """Try to close the codeblock""" 220 | line = line.rstrip() 221 | if line == f"{self.indent}{self.backticks}": 222 | self.alive = False 223 | if self.codes and "{" in self.codes and "}" in self.codes: 224 | self.compile_exec(self.codes) 225 | self.codes = "" 226 | return True 227 | 228 | return False 229 | 230 | @staticmethod 231 | def should_compile(line): 232 | """Whether we should compile a line or treat it as a plain line""" 233 | if "#" not in line: 234 | return False 235 | 236 | comment = line.split("#", 1)[1] 237 | return re.search(r"\{[^}]+\}", comment) 238 | 239 | 240 | def compile_readme(rawfile): 241 | """Compile the raw file line by line""" 242 | 243 | codeblock = None 244 | 245 | with open(rawfile) as fraw: 246 | for line in fraw: 247 | if codeblock and codeblock.alive: 248 | if codeblock.close(line): 249 | printn(codeblock.produce()) 250 | printn(line) 251 | else: 252 | codeblock.feed(line) 253 | else: 254 | printn(line) 255 | envs = codeblock.envs if codeblock else None 256 | index = codeblock.index + 1 if codeblock else 0 257 | maybe_codeblock = CodeBlock.try_open(line, envs, index) 258 | if maybe_codeblock: 259 | codeblock = maybe_codeblock 260 | 261 | 262 | if __name__ == "__main__": 263 | if len(sys.argv) < 2: 264 | print("Usage: make-readme.py README.raw.md > README.md") 265 | sys.exit(1) 266 | 267 | compile_readme(sys.argv[1]) 268 | -------------------------------------------------------------------------------- /varname/helpers.py: -------------------------------------------------------------------------------- 1 | """Some helper functions builtin based upon core features""" 2 | from __future__ import annotations 3 | 4 | import inspect 5 | from functools import partial, wraps 6 | from os import PathLike 7 | from typing import Any, Callable, Dict, Tuple, Type, Union 8 | 9 | from .utils import IgnoreType 10 | from .ignore import IgnoreList 11 | from .core import argname, varname 12 | 13 | 14 | def register( 15 | cls_or_func: type = None, 16 | frame: int = 1, 17 | ignore: IgnoreType = None, 18 | multi_vars: bool = False, 19 | raise_exc: bool = True, 20 | strict: bool = True, 21 | ) -> Union[Type, Callable]: 22 | """A decorator to register __varname__ to a class or function 23 | 24 | When registered to a class, it can be accessed by `self.__varname__`; 25 | while to a function, it is registered to globals, meaning that it can be 26 | accessed directly. 27 | 28 | Args: 29 | frame: The call stack index, indicating where this class 30 | is instantiated relative to where the variable is finally retrieved 31 | multi_vars: Whether allow multiple variables on left-hand side (LHS). 32 | If `True`, this function returns a tuple of the variable names, 33 | even there is only one variable on LHS. 34 | If `False`, and multiple variables on LHS, a 35 | `VarnameRetrievingError` will be raised. 36 | raise_exc: Whether we should raise an exception if failed 37 | to retrieve the name. 38 | strict: Whether to only return the variable name if the result of 39 | the call is assigned to it directly. 40 | 41 | Examples: 42 | >>> @varname.register 43 | >>> class Foo: pass 44 | >>> foo = Foo() 45 | >>> # foo.__varname__ == 'foo' 46 | >>> 47 | >>> @varname.register 48 | >>> def func(): 49 | >>> return __varname__ 50 | >>> foo = func() # foo == 'foo' 51 | 52 | Returns: 53 | The wrapper function or the class/function itself 54 | if it is specified explictly. 55 | """ 56 | if inspect.isclass(cls_or_func): 57 | orig_init = cls_or_func.__init__ # type: ignore 58 | 59 | @wraps(cls_or_func.__init__) # type: ignore 60 | def wrapped_init(self, *args, **kwargs): 61 | """Wrapped init function to replace the original one""" 62 | self.__varname__ = varname( 63 | frame - 1, 64 | ignore=ignore, 65 | multi_vars=multi_vars, 66 | raise_exc=raise_exc, 67 | strict=strict, 68 | ) 69 | orig_init(self, *args, **kwargs) 70 | 71 | cls_or_func.__init__ = wrapped_init # type: ignore 72 | return cls_or_func 73 | 74 | if inspect.isfunction(cls_or_func): 75 | 76 | @wraps(cls_or_func) 77 | def wrapper(*args, **kwargs): 78 | """The wrapper to register `__varname__` to a function""" 79 | cls_or_func.__globals__["__varname__"] = varname( 80 | frame - 1, 81 | ignore=ignore, 82 | multi_vars=multi_vars, 83 | raise_exc=raise_exc, 84 | strict=strict, 85 | ) 86 | 87 | try: 88 | return cls_or_func(*args, **kwargs) 89 | finally: 90 | del cls_or_func.__globals__["__varname__"] 91 | 92 | return wrapper 93 | 94 | # None, meaning we have other arguments 95 | return partial( 96 | register, 97 | frame=frame, 98 | ignore=ignore, 99 | multi_vars=multi_vars, 100 | raise_exc=raise_exc, 101 | strict=strict, 102 | ) 103 | 104 | 105 | class Wrapper: 106 | """A wrapper with ability to retrieve the variable name 107 | 108 | Examples: 109 | >>> foo = Wrapper(True) 110 | >>> # foo.name == 'foo' 111 | >>> # foo.value == True 112 | 113 | >>> val = {} 114 | >>> bar = Wrapper(val) 115 | >>> # bar.name == 'bar' 116 | >>> # bar.value is val 117 | 118 | Args: 119 | value: The value to be wrapped 120 | raise_exc: Whether to raise exception when varname is failed to retrieve 121 | strict: Whether to only return the variable name if the wrapper is 122 | assigned to it directly. 123 | 124 | Attributes: 125 | name: The variable name to which the instance is assigned 126 | value: The value this wrapper wraps 127 | """ 128 | 129 | def __init__( 130 | self, 131 | value: Any, 132 | frame: int = 1, 133 | ignore: IgnoreType = None, 134 | raise_exc: bool = True, 135 | strict: bool = True, 136 | ): 137 | # This call is ignored, since it's inside varname 138 | self.name = varname( 139 | frame=frame - 1, 140 | ignore=ignore, 141 | raise_exc=raise_exc, 142 | strict=strict, 143 | ) 144 | self.value = value 145 | 146 | def __str__(self) -> str: 147 | return repr(self.value) 148 | 149 | def __repr__(self) -> str: 150 | return ( 151 | f"<{self.__class__.__name__} " 152 | f"(name={self.name!r}, value={self.value!r})>" 153 | ) 154 | 155 | 156 | def jsobj( 157 | *args: Any, 158 | vars_only: bool = True, 159 | frame: int = 1, 160 | **kwargs: Any, 161 | ) -> Dict[str, Any]: 162 | """A wrapper to create a JavaScript-like object 163 | 164 | When an argument is passed as positional argument, the name of the variable 165 | will be used as the key, while the value will be used as the value. 166 | 167 | Examples: 168 | >>> obj = jsobj(a=1, b=2) 169 | >>> # obj == {'a': 1, 'b': 2} 170 | >>> # obj.a == 1 171 | >>> # obj.b == 2 172 | >>> a = 1 173 | >>> b = 2 174 | >>> obj = jsobj(a, b, c=3) 175 | >>> # obj == {'a': 1, 'b': 2, 'c': 3} 176 | 177 | Args: 178 | *args: The positional arguments 179 | vars_only: Whether to only include variables in the output 180 | frame: The call stack index. You can understand this as the number of 181 | wrappers around this function - 1. 182 | **kwargs: The keyword arguments 183 | 184 | Returns: 185 | A dict-like object 186 | """ 187 | argnames: Tuple[str, ...] = argname( 188 | "args", 189 | vars_only=vars_only, 190 | frame=frame, 191 | ) # type: ignore 192 | out = dict(zip(argnames, args)) 193 | out.update(kwargs) 194 | return out 195 | 196 | 197 | def debug( 198 | var, 199 | *more_vars, 200 | prefix: str = "DEBUG: ", 201 | merge: bool = False, 202 | repr: bool = True, 203 | sep: str = "=", 204 | vars_only: bool = False, 205 | ) -> None: 206 | """Print variable names and values. 207 | 208 | Examples: 209 | >>> a = 1 210 | >>> b = object 211 | >>> print(f'a={a}') # previously, we have to do 212 | >>> print(f'{a=}') # or with python3.8 213 | >>> # instead we can do: 214 | >>> debug(a) # DEBUG: a=1 215 | >>> debug(a, prefix='') # a=1 216 | >>> debug(a, b, merge=True) # a=1, b= 217 | 218 | Args: 219 | var: The variable to print 220 | *more_vars: Other variables to print 221 | prefix: A prefix to print for each line 222 | merge: Whether merge all variables in one line or not 223 | sep: The separator between the variable name and value 224 | repr: Print the value as `repr(var)`? otherwise `str(var)` 225 | """ 226 | var_names = argname("var", "*more_vars", vars_only=vars_only, func=debug) 227 | 228 | values = (var, *more_vars) 229 | name_and_values = [ 230 | f"{var_name}{sep}{value!r}" if repr else f"{var_name}{sep}{value}" 231 | for var_name, value in zip(var_names, values) # type: ignore 232 | ] 233 | 234 | if merge: 235 | print(f"{prefix}{', '.join(name_and_values)}") 236 | else: 237 | for name_and_value in name_and_values: 238 | print(f"{prefix}{name_and_value}") 239 | 240 | 241 | def exec_code( 242 | code: str, 243 | globals: Dict[str, Any] = None, 244 | locals: Dict[str, Any] = None, 245 | /, 246 | sourcefile: PathLike | str = None, 247 | frame: int = 1, 248 | ignore: IgnoreType = None, 249 | **kwargs: Any, 250 | ) -> None: 251 | """Execute code where source code is visible at runtime. 252 | 253 | This function is useful when you want to execute some code, where you want to 254 | retrieve the AST node of the code at runtime. This function will create a 255 | temporary file and write the code into it, then execute the code in the 256 | file. 257 | 258 | Examples: 259 | >>> from varname import varname 260 | >>> def func(): return varname() 261 | >>> exec('var = func()') # VarnameRetrievingError: 262 | >>> # Unable to retrieve the ast node. 263 | >>> from varname.helpers import code_exec 264 | >>> code_exec('var = func()') # var == 'var' 265 | 266 | Args: 267 | code: The code to execute. 268 | globals: The globals to use. 269 | locals: The locals to use. 270 | sourcefile: The source file to write the code into. 271 | if not given, a temporary file will be used. 272 | This file will be deleted after the code is executed. 273 | frame: The call stack index. You can understand this as the number of 274 | wrappers around this function. This is used to fetch `globals` and 275 | `locals` from where the destination function (include the wrappers 276 | of this function) 277 | is called. 278 | ignore: The intermediate calls to be ignored. See `varname.ignore` 279 | Note that if both `globals` and `locals` are given, `frame` and 280 | `ignore` will be ignored. 281 | **kwargs: The keyword arguments to pass to `exec`. 282 | """ 283 | if sourcefile is None: 284 | import tempfile 285 | 286 | with tempfile.NamedTemporaryFile( 287 | mode="w", suffix=".py", delete=False 288 | ) as f: 289 | f.write(code) 290 | sourcefile = f.name 291 | else: 292 | sourcefile = str(sourcefile) 293 | with open(sourcefile, "w") as f: 294 | f.write(code) 295 | 296 | if globals is None or locals is None: 297 | ignore_list = IgnoreList.create(ignore) 298 | frame_info = ignore_list.get_frame(frame) 299 | if globals is None: 300 | globals = frame_info.f_globals 301 | if locals is None: 302 | locals = frame_info.f_locals 303 | 304 | try: 305 | exec(compile(code, sourcefile, "exec"), globals, locals, **kwargs) 306 | finally: 307 | import os 308 | 309 | os.remove(sourcefile) 310 | -------------------------------------------------------------------------------- /tests/test_argname.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | from functools import singledispatch 3 | 4 | import pytest 5 | from varname import ( 6 | argname, 7 | UsingExecWarning, 8 | ImproperUseError, 9 | VarnameRetrievingError, 10 | ) 11 | 12 | 13 | def test_argname(): 14 | def func(a, b, c, d=4): 15 | return argname("c", "b") 16 | 17 | x = y = z = 1 18 | names = func(x, y, z) 19 | assert names == ("z", "y") 20 | 21 | names = func(x, "a", 1) 22 | assert names == ('1', "'a'") 23 | 24 | def func2(a, b, c, d=4): 25 | return argname("b") 26 | 27 | names2 = func2(x, y, z) 28 | assert names2 == "y" 29 | 30 | def func3(e=1): 31 | return argname("e") 32 | 33 | names3 = func3(z) 34 | assert names3 == "z" 35 | 36 | def func4(a, b=1): 37 | return argname("a", "b") 38 | 39 | names4 = func4(y, b=x) 40 | assert names4 == ("y", "x") 41 | 42 | 43 | def test_argname_lambda(): 44 | func = lambda a, b, c, d=4: argname("b") 45 | x = y = z = 1 46 | names = func(x, y, z) 47 | assert names == "y" 48 | 49 | 50 | def test_argname_pure_eval(): 51 | def func(a): 52 | return argname("a") 53 | 54 | x = 1 55 | funcs = [func] 56 | name = funcs[0](x) 57 | assert name == "x" 58 | 59 | 60 | def test_argname_eval(): 61 | x = 1 62 | with pytest.warns(UsingExecWarning, match="Cannot evaluate node"): 63 | name = (lambda a: argname("a"))(x) 64 | assert name == "x" 65 | 66 | 67 | def test_argname_no_pure_eval(no_pure_eval): 68 | def func(a): 69 | return argname("a") 70 | 71 | x = 1 72 | funcs = [func] 73 | 74 | with pytest.warns(UsingExecWarning, match="'pure_eval' is not installed"): 75 | name = funcs[0](x) 76 | assert name == "x" 77 | 78 | 79 | def test_argname_non_argument(): 80 | x = 1 # noqa F841 81 | y = lambda: argname("x") 82 | with pytest.raises(ImproperUseError, match="'x' is not a valid argument"): 83 | y() 84 | 85 | 86 | def test_argname_non_variable(): 87 | def func(a, b, c, d=4): 88 | return argname("b") 89 | 90 | with pytest.raises(ImproperUseError, match=r"is not a variable"): 91 | func(1, {1}, 1) 92 | 93 | 94 | def test_argname_argname_argument_non_variable(): 95 | def func(a, b, c, d=4): 96 | return argname(1) 97 | 98 | x = y = z = 1 99 | with pytest.raises(TypeError, match="expected string or bytes-like object"): 100 | func(x, y, z) 101 | 102 | 103 | def test_argname_funcnode_not_call(): 104 | x = 1 # noqa F841 105 | 106 | class Foo: 107 | 108 | def __neg__(self): 109 | return argname("self") 110 | 111 | foo = Foo() 112 | with pytest.raises( 113 | VarnameRetrievingError, 114 | match="Cannot reconstruct ast.Call node from UnaryOp", 115 | ): 116 | -foo 117 | 118 | 119 | def test_argname_get_source(): 120 | def func(a, b=1): 121 | return argname("a", vars_only=False) 122 | 123 | name = func(1 + 2) 124 | assert name == "1 + 2" 125 | 126 | 127 | def test_argname_star_args(): 128 | def func(*args, **kwargs): 129 | return argname("args", "kwargs") 130 | 131 | x = y = z = 1 132 | arg_source, kwarg_source = func(x, y, c=z) 133 | 134 | assert arg_source == ("x", "y") 135 | assert kwarg_source == {"c": "z"} 136 | 137 | def func(*args, **kwargs): 138 | return argname("args", "kwargs", vars_only=False) 139 | 140 | arg_source, kwarg_source = func(1 + 2, 2 + 3, c=4 * 5) 141 | assert arg_source == ("1 + 2", "2 + 3") 142 | assert kwarg_source == {"c": "4 * 5"} 143 | 144 | 145 | def test_argname_star_args_individual(): 146 | def func(*args, **kwargs): 147 | return argname("args[1]"), argname("kwargs[c]") 148 | 149 | x = y = z = 1 150 | second_name = func(x, y, c=z) 151 | assert second_name == ("y", "z") 152 | 153 | m = [1] 154 | 155 | def func(*args, **kwargs): 156 | return argname("m[0]") 157 | 158 | with pytest.raises(ImproperUseError, match="'m' is not a valid argument"): 159 | func() 160 | 161 | def func(a, *args, **kwargs): 162 | return argname("a[0]") 163 | 164 | with pytest.raises( 165 | ImproperUseError, match="`a` is not a positional argument" 166 | ): 167 | func(m) 168 | 169 | n = {"e": 1} 170 | 171 | def func(a, *args, **kwargs): 172 | return argname('a["e"]') 173 | 174 | with pytest.raises(ImproperUseError, match="`a` is not a keyword argument"): 175 | func(n) 176 | 177 | def func(*args, **kwargs): 178 | return argname("[args][0]") 179 | 180 | with pytest.raises(ImproperUseError, match="is not a valid argument"): 181 | func() 182 | 183 | def func(*args, **kwargs): 184 | return argname("args[1 + 1]") 185 | 186 | with pytest.raises( 187 | ImproperUseError, match="`args` is not a keyword argument" 188 | ): 189 | func(x, y, z) 190 | 191 | def func(*args, **kwargs): 192 | return argname("args[x]") 193 | 194 | with pytest.raises( 195 | ImproperUseError, match="`args` is not a keyword argument." 196 | ): 197 | func(x, y, z) 198 | 199 | 200 | def test_argname_argname_node_na(): 201 | source = textwrap.dedent( 202 | """\ 203 | from varname import argname 204 | def func(a): 205 | return argname(a) 206 | 207 | x = 1 208 | print(func(x)) 209 | """ 210 | ) 211 | code = compile(source, "", "exec") 212 | with pytest.raises( 213 | VarnameRetrievingError, 214 | match="Cannot retrieve the node where the function is called", 215 | ): 216 | exec(code) 217 | 218 | 219 | def test_argname_func_node_na(): 220 | def func(a): 221 | return argname("a") 222 | 223 | with pytest.raises( 224 | VarnameRetrievingError, 225 | match="Cannot retrieve the node where the function is called", 226 | ): 227 | exec("x=1; func(x)") 228 | 229 | 230 | def test_argname_wrapper(): 231 | def decorator(f): 232 | def wrapper(arg, *more_args): 233 | return f(arg, *more_args, frame=2) 234 | 235 | return wrapper 236 | 237 | argname3 = decorator(argname) 238 | 239 | def func(a, b): 240 | return argname3("a", "b") 241 | 242 | x = y = 1 243 | names = func(x, y) 244 | assert names == ("x", "y") 245 | 246 | 247 | def test_argname_varpos_arg(): 248 | def func(a, *args, **kwargs): 249 | return argname("kwargs", "a", "*args") 250 | 251 | x = y = z = 1 252 | names = func(x, y, kw=z) 253 | assert names == ({"kw": "z"}, "x", "y") 254 | 255 | names = func(x) 256 | assert names == ({}, "x") 257 | 258 | 259 | def test_argname_nosuch_varpos_arg(): 260 | def func(a, *args): 261 | another = [] # noqa F841 262 | return argname("a", "*another") 263 | 264 | x = y = 1 265 | with pytest.raises( 266 | ImproperUseError, match="'another' is not a valid argument" 267 | ): 268 | func(x, y) 269 | 270 | 271 | def test_argname_target_arg(): 272 | def func(a, b): 273 | return argname("a") 274 | 275 | x = 1 276 | names = func(x, 1) 277 | assert names == "x" 278 | 279 | 280 | def test_argname_func_na(): 281 | def func(a): 282 | return argname("a") 283 | 284 | with pytest.raises( 285 | VarnameRetrievingError, 286 | match="Cannot retrieve the node where the function is called", 287 | ): 288 | exec("x=1; func(x)") 289 | 290 | 291 | def test_argname_singledispatched(): 292 | # GH53 293 | @singledispatch 294 | def add(a, b): 295 | aname = argname("a", "b", func=add.dispatch(object)) 296 | return aname + (1,) # distinguish 297 | 298 | @add.register(int) 299 | def add_int(a, b): 300 | aname = argname("a", "b", func=add_int) 301 | return aname + (2,) 302 | 303 | @add.register(str) 304 | def add_str(a, b): 305 | aname = argname("a", "b", dispatch=str) 306 | return aname + (3,) 307 | 308 | x = y = 1 309 | out = add(x, y) 310 | assert out == ("x", "y", 2) 311 | 312 | t = s = "a" 313 | out = add(t, s) 314 | assert out == ("t", "s", 3) 315 | 316 | p = q = 1.2 317 | out = add(p, q) 318 | assert out == ("p", "q", 1) 319 | 320 | 321 | def test_argname_nosucharg(): 322 | def func(a): 323 | return argname("x") 324 | 325 | x = 1 326 | with pytest.raises(ImproperUseError, match="'x' is not a valid argument"): 327 | func(x) 328 | 329 | 330 | def test_argname_subscript_star(): 331 | def func1(*args, **kwargs): 332 | return argname("args[0]", "kwargs[x]") 333 | 334 | def func2(*args, **kwargs): 335 | return argname("*args") 336 | 337 | x = y = 1 338 | out = func1(y, x=x) 339 | assert out == ("y", "x") 340 | 341 | out = func2(x, y) 342 | assert out == ("x", "y") 343 | 344 | 345 | def test_argname_nonvar(): 346 | def func(x): 347 | return argname("x") 348 | 349 | with pytest.raises(ImproperUseError): 350 | func({1}) 351 | 352 | 353 | def test_argname_frame_error(): 354 | def func(x): 355 | return argname("x", frame=2) 356 | 357 | with pytest.raises(ImproperUseError, match="'x' is not a valid argument"): 358 | func(1) 359 | 360 | 361 | def test_argname_ignore(): 362 | def target(*args): 363 | return argname("*args", ignore=(wrapper, 0)) 364 | 365 | def wrapper(*args): 366 | return target(*args) 367 | 368 | x = y = 1 369 | out = wrapper(x, y) 370 | assert out == ("x", "y") 371 | 372 | 373 | def test_argname_attribute_item(): 374 | class A: 375 | def __init__(self): 376 | self.__dict__["meta"] = {} 377 | 378 | def __getattr__(self, name): 379 | return argname("name") 380 | 381 | def __setattr__(self, name, value) -> None: 382 | self.__dict__["meta"]["name"] = argname("name") 383 | self.__dict__["meta"]["name2"] = argname("name", vars_only=False) 384 | self.__dict__["meta"]["value"] = argname("value") 385 | 386 | def __getitem__(self, name): 387 | return argname("name") 388 | 389 | def __setitem__(self, name, value) -> None: 390 | self.__dict__["meta"]["name"] = argname("name") 391 | self.__dict__["meta"]["name2"] = argname("name", vars_only=False) 392 | self.__dict__["meta"]["value"] = argname("value") 393 | 394 | a = A() 395 | out = a.x 396 | assert out == "'x'" 397 | 398 | a.__getattr__("x") 399 | assert out == "'x'" 400 | 401 | a.y = 1 402 | assert a.meta["name"] == "'y'" 403 | assert a.meta["name2"] == "'y'" 404 | assert a.meta["value"] == "1" 405 | 406 | a.__setattr__("y", 1) 407 | assert a.meta["name"] == "'y'" 408 | assert a.meta["name2"] == "'y'" 409 | assert a.meta["value"] == "1" 410 | 411 | out = a[1] 412 | assert out == "1" 413 | 414 | a[1] = 2 415 | assert a.meta["name"] == "1" 416 | assert a.meta["name2"] == "1" 417 | assert a.meta["value"] == "2" 418 | 419 | with pytest.raises(ImproperUseError): 420 | a.x = a.y = 1 421 | 422 | 423 | def test_argname_compare(): 424 | class A: 425 | def __init__(self): 426 | self.meta = None 427 | 428 | def __eq__(self, other): 429 | self.meta = argname("other") 430 | return True 431 | 432 | def __lt__(self, other): 433 | self.meta = argname("other") 434 | return True 435 | 436 | def __gt__(self, other): 437 | self.meta = argname("other") 438 | return True 439 | 440 | a = A() 441 | a == 1 442 | assert a.meta == '1' 443 | 444 | a < 2 445 | assert a.meta == '2' 446 | 447 | a > 3 448 | assert a.meta == '3' 449 | 450 | 451 | def test_argname_binop(): 452 | class A: 453 | def __init__(self): 454 | self.meta = None 455 | 456 | def __add__(self, other): 457 | self.meta = argname("other") 458 | return 1 459 | 460 | a = A() 461 | out = a + 1 462 | assert out == 1 463 | assert a.meta == '1' 464 | 465 | 466 | def test_argname_wrong_frame(): 467 | def func(x): 468 | return argname("x", func=int) 469 | 470 | with pytest.raises( 471 | ImproperUseError, 472 | match="Have you specified the right `frame` or `func`", 473 | ): 474 | func(1) 475 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.15.1 4 | 5 | - chore: bump executing to v2.2.1 6 | - feat: enhanced Subscript Support for Dynamic Indexing for varname (#119) 7 | 8 | ## 0.15.0 9 | 10 | - revert: bring nameof back (#117) 11 | 12 | ## 0.14.0 13 | 14 | - BREAKING CHANGE: deprecate nameof (see https://github.com/pwwang/python-varname/issues/117#issuecomment-2558358294) 15 | - docs: remove deprecated nameof examples from README 16 | - chore(deps): update asttokens to version 3.0.0 and adjust dependencies 17 | - style: clean up unused imports and add spacing for readability for test files 18 | - ci: update build workflow to use Ubuntu 24.04 and adjust Python version conditions 19 | - chore(deps): add ipykernel dev dependency version 6.29.5 to run playground notebook 20 | - chore(deps): update content-hash in poetry.lock after dependency changes 21 | 22 | ## 0.13.5 23 | 24 | - deps: bump up executing to ^2.1 to fix issues with python3.13 25 | 26 | ## 0.13.4 27 | 28 | - core: switch to poetry-core (#113) 29 | - deps: bump up dependencies 30 | - feat: support python 3.13 (#116) 31 | - ci: use latest CI actions 32 | - DEPRECATED: add warning to deprecate nameof in the future 33 | 34 | ## 0.13.3 35 | 36 | - feat: support frame to allow wrapping for `helpers.jsobj()` (#111) 37 | 38 | ## 0.13.2 39 | 40 | - deps: bump up pytest to v8 41 | - feat: support vars_only to keep parents of an attribute for `helpers.jsobj()` (#110) 42 | 43 | ## 0.13.1 44 | 45 | - style: create py.typed for mypy compatibility (#109) 46 | 47 | ## 0.13.0 48 | 49 | - style: change max line length to 88 50 | - style: clean up test code styles 51 | - feat: support subscript node for varname (#104) 52 | - ci: remove python3.8 from CI 53 | - breaking!: `varname` of `a.b` now returns `"a.b"` instead of `"a"` 54 | 55 | ## 0.12.2 56 | 57 | - Add `helpers.exec_code` function to replace `exec` so that source code available at runtime 58 | 59 | ## 0.12.1 60 | 61 | - Bump executing to 2.0.1 62 | 63 | ## v0.12.0 64 | 65 | - Support python 3.12 66 | - Update python3.12 to CI 67 | - Bump executing to ^2.0 68 | - Bump up other dependencies 69 | - Add Dockerfile for codesandbox 70 | 71 | ## v0.11.2 72 | 73 | - ✨ Add `jsobj` to create dict without explicitly specifying the key-value pairs 74 | 75 | ```python 76 | from varname.helpers import jsobj 77 | 78 | a = 1 79 | b = 2 80 | # before 81 | dict(a=a, b=b, c=3) # {'a': 1, 'b': 2, 'c': 3} 82 | 83 | # after 84 | jsobj(a, b, c=3) # {'a': 1, 'b': 2, 'c': 3} 85 | ``` 86 | 87 | ## v0.11.1 88 | 89 | - ✨ Support starred variable for varname() (#96) 90 | - ✅ Fix tests 91 | - 📝 Update docs for `varname(strict=...)` 92 | 93 | ## v0.11.0 94 | 95 | - 📝 Update README for shields badges (#91) 96 | - 🏷️ Overload types for nameof and argname (#77) 97 | - 💥 Drop python <3.8 for v0.11 98 | If you need support for python <3.8, please use varname <0.11 99 | 100 | ## v0.10.0 101 | 102 | - ✨ Support python 3.11 103 | 104 | ## v0.9.1 105 | 106 | - ⬆️ Upgrade executing to 1.0 107 | 108 | ## v0.9.0 109 | 110 | - ⬆️ Upgrade executing to 0.9 111 | - 🗑️ Remove deprecated `argname2` 112 | - ✨ Support constants for `argname` even when `vars_only=True` 113 | - ✨ Support `__getattr__/__setattr__` etc for `argname` 114 | 115 | Now you can do: 116 | 117 | ```python 118 | from varname import argname 119 | 120 | class Foo: 121 | def __getattr__(self, name): 122 | """Similar for `__getitem__`""" 123 | print(argname("name")) 124 | 125 | def __setattr__(self, name, value): 126 | """Similar for `__setitem__`""" 127 | print(argname("name")) 128 | print(argname("value")) 129 | 130 | def __add__(self, other): 131 | """Similar for `__sub__`, `__mul__`, `__truediv__`, `__floordiv__`, 132 | `__mod__`, `__pow__`, `__lshift__`, `__rshift__`, `__matmul__`, 133 | `__and__`, `__xor__`, `__or__` 134 | """ 135 | print(argname("other")) 136 | 137 | def __eq__(self, other): 138 | """Similar for `__lt__`, `__le__`, `__gt__`, `__ge__`, `__ne__` 139 | """ 140 | print(argname("other")) 141 | 142 | foo = Foo() 143 | b = 1 144 | foo.x # prints: 'x' (note the quotes) 145 | foo.x = b # prints: 'x' and b 146 | foo + b # prints: b 147 | foo == b # prints: b 148 | ``` 149 | 150 | ## v0.8.3 151 | 152 | This is more of a housekeeping release: 153 | 154 | - ⬆️ Upgrade `executing` to 0.8.3 to make varname work with ipython 8+ 155 | - 📝 Update `README.md` to add new contributors 156 | - 🚨 Use `flake8` instead of `pylint` for linting 157 | 158 | ## v0.8.2 159 | 160 | ### Fixes 161 | 162 | - 🩹 Use sysconfig instead of distutils.sysconfig to avoid deprecatewarning for python 3.10+ 163 | 164 | ### Housekeeping 165 | 166 | - 👷 Add python3.10 in CI 167 | - 📄 Add license back 168 | 169 | ## v0.8.1 170 | 171 | - Handle inspect raises "could not get source code" when printing rich exception message 172 | 173 | ## v0.8.0 174 | 175 | Compared to `v0.7.3` 176 | 177 | - Add `UsingExecWarning` when `exec` is used to retrieve `func` for `argname()`. 178 | - Remove `NonVariableArgumentError`. Use `ImproperUseError` instead. 179 | - Add `VarnameError` and `VarnameWarning` as root for varname-related exceptions and warnings, respectively. 180 | - Default `strict` to `True` for `varname()`, `helpers.register()` and `helpers.Wrapper()` 181 | - Limit number of context lines for showing where `ImproperUseError` happens 182 | 183 | Compared to `v0.7.0` 184 | 185 | - Add `UsingExecWarning` when `exec` is used to retrieve `func` for `argname()`. 186 | - Remove `NonVariableArgumentError`. Use `ImproperUseError` instead. 187 | - Add `VarnameError` and `VarnameWarning` as root for varname-related exceptions and warnings, respectively. 188 | - Add `strict` mode to `varname()`, `helpers.register()` and `helpers.Wrapper()` (#57) 189 | - Support the walrus operator (`:=`) (#58) 190 | - Change `argname()` to accept argument names instead of arguments themselves 191 | - Remove `pos_only` argument from `argname()` 192 | - Add `ignore` argument to `argname()` to ignore intermediate frames 193 | - Limit `VarnameRetrievingError` to the situations only when the AST node is not able to be retrieved. 194 | 195 | ## v0.7.3 196 | 197 | - Indicate where the `ImproperUseError` happens for `varname()` (Close #60) 198 | - Add `VarnameException` and `VarnameWarning` as root for all varname-defined exceptions and warnings. 199 | 200 | ## v0.7.2 201 | 202 | - Add `strict` mode to `varname()` (#57) 203 | - Support the walrus operator (`:=`) (#58) 204 | 205 | ## v0.7.1 206 | 207 | - Add `ignore` argument to `argname2()` 208 | - Fix Fix utils.get_argument_sources() when kwargs is given as `**kwargs`. 209 | 210 | ## v0.7.0 211 | 212 | - `ImproperUseError` is now independent of `VarnameRetrievingError` 213 | - Deprecate `argname`, superseded by `argname2` 214 | 215 | ```python 216 | >>> argname(a, b, ...) # before 217 | >>> argname2('a', 'b', ...) # after 218 | ``` 219 | 220 | - Add `dispatch` argument to `argname`/`argment2` to be used for single-dispatched functions. 221 | 222 | ## v0.6.5 223 | 224 | - Add `sep` argument to `helpers.debug()` 225 | 226 | ## v0.6.4 227 | 228 | - Add ImproperUseError to distinguish node retrieving error from improper varname use #49 229 | 230 | ## v0.6.3 231 | 232 | - Fix standard library ignoring ignores 3rd-party libraries under site-packages/ 233 | - Allow pathlib.Path object to be used in ignore items 234 | 235 | ## v0.6.2 236 | 237 | - Remove argument `full` for `nameof`, use `vars_only` instead. When `vars_only=False`, source of the argument returned. 238 | 239 | ```python 240 | # before: 241 | nameof(a.b, full=True) # 'a.b' 242 | nameof(x[0], full=True) # unable to fetch 243 | # after (requires asttoken): 244 | nameof(a.b, vars_only=False) # 'a.b' 245 | nameof(x[0], vars_only=False) # 'x[0]' 246 | ``` 247 | 248 | - Add argument `frame` to `argname`, so that it can be wrapped. 249 | 250 | ```python 251 | def argname2(arg, *more_args): 252 | return argname(arg, *more_args, frame=2) 253 | ``` 254 | 255 | - Allow `argname` to fetch the source of variable keyword arguments (`**kwargs`), which will be an empty dict (`{}`) when no keyword arguments passed. 256 | 257 | ```python 258 | def func(a, **kwargs): 259 | return argname(a, kwargs) 260 | # before: 261 | func(x) # raises error 262 | # after: 263 | func(x) # returns ('x', {}) 264 | ``` 265 | 266 | - Add argument `pos_only` to `argname` to only match the positional arguments 267 | 268 | ```python 269 | # before 270 | def func(a, b=1): 271 | return argname(a) 272 | func(x) # 'x' 273 | func(x, b=2) # error since 2 is not ast.Name 274 | 275 | # after 276 | def func(a, b=1): 277 | return argname(a, pos_only=True) 278 | func(x) # 'x' 279 | func(x, b=2) # 'x' 280 | ``` 281 | 282 | - Parse the arguments only if needed 283 | 284 | ```python 285 | # before 286 | def func(a, b): 287 | return argname(a) 288 | func(x, 1) # NonVariableArgumentError 289 | 290 | # after 291 | func(x, 1) # 'x' 292 | ``` 293 | 294 | - Allow variable positional arguments for `argname` so that `argname(*args)` is allowed 295 | 296 | ```python 297 | # before 298 | def func(arg, *args): 299 | return argname(arg, args) # *args not allowed 300 | x = y = 1 301 | func(x, y) # ('x', ('y', 1)) 302 | 303 | # after 304 | def func(arg, *args): 305 | return argname(arg, *args) 306 | x = y = 1 307 | func(x, y) # ('x', 'y') 308 | ``` 309 | 310 | - Add `vars_only` (defaults to `False`) argument to `helpers.debug` so source of expression becomes available 311 | 312 | ```python 313 | a=1 314 | debug(a+a) # DEBUG: a+a=2 315 | ``` 316 | 317 | ## v0.6.1 318 | 319 | - Add `argname` to retrieve argument names/sources passed to a function 320 | 321 | ## v0.6.0 322 | 323 | - Changed: 324 | - `Wrapper`, `register` and `debug` moved to `varname.helpers` 325 | - Argument `caller` changed to `frame` across all APIs 326 | - `ignore` accepting module, filename, function, (function, num_decorators), (module, qualname) and (filename, qualname) 327 | - Removed: 328 | - `inject` (Use `helpers.regiester` instead) 329 | - `inject_varname` (Use `helpers.regiester` instead) 330 | - `namedtuple` 331 | - Added: 332 | - Arguments `frame` and `ignore` to `Wrapper` 333 | - `helpers.register` as a decorator for functions 334 | 335 | ## v0.5.6 336 | 337 | - Add `ignore` argument to `varname` to ignore frames that are not counted by caller 338 | - Deprecate `inject_varname`, use `register` instead 339 | 340 | ## v0.5.5 341 | 342 | - Deprecate inject and use inject_varname decorator instead 343 | 344 | ## v0.5.4 345 | 346 | - Allow `varname.varname` to receive multiple variables on the left-hand side 347 | 348 | ## v0.5.3 349 | 350 | - Add `debug` function 351 | - Deprecate `namedtuple` (will be removed in `0.6.0`) 352 | 353 | ## v0.5.2 354 | 355 | - Move messaging of weird nameof calls from `_bytecode_nameof` to `nameof`. 356 | - Disallow `full` to be used when `_bytecode_nameof` needs to be invoked. 357 | 358 | ## v0.5.1 359 | 360 | - Add better messaging for weird nameof calls 361 | 362 | ## v0.5.0 363 | 364 | - Allow `nameof` to retrieve full name of chained attribute calls 365 | - Add `__all__` to the module so that only desired APIs are exposed when `from varname import *` 366 | - Give more hints on `nameof` being called in a weird way when no soucecode available. 367 | 368 | ## v0.4.0 369 | 370 | - Change default of `raise_exc` to `True` for all related APIs 371 | - Deprecate `var_0` 372 | - Get rid of `VarnameRetrievingWarning`. 373 | 374 | ## v0.3.0 375 | 376 | - Use sys._getframe instead of inspect.stack for efficiency (#9) 377 | - Add alternative way of testing bytecode nameof (#10) 378 | - Drop support for pytest, don't try to find node when executing fails 379 | - Remodel `will` for better logic 380 | - Support attributes in varname and nameof (#14) 381 | 382 | ## v0.2.0 383 | 384 | - Fix #5 and fit nameof in more cases 385 | 386 | ## v0.1.7 387 | 388 | - Add `inject` function 389 | 390 | ## v0.1.6 391 | 392 | - Fit situations when frames cannot be fetched 393 | - Add shortcut for `namedtuple` 394 | 395 | ## v0.1.5 396 | 397 | - Fix `will` from a property call 398 | 399 | ## v0.1.4 400 | 401 | - Add `will` to detect next immediate attribute name 402 | 403 | ## v0.1.3 404 | 405 | - Add arugment `raise_exc` for `varname` to raise an exception instead of returning `var_` 406 | 407 | ## v0.1.2 408 | 409 | - Add function `nameof` 410 | 411 | ## v0.1.1 412 | 413 | - Add a value wrapper `Wrapper` class 414 | 415 | ## v0.1.0 416 | 417 | - Implement `varname` function 418 | -------------------------------------------------------------------------------- /README.raw.md: -------------------------------------------------------------------------------- 1 | ![varname][7] 2 | 3 | [![Pypi][3]][4] [![Github][5]][6] [![PythonVers][8]][4] ![Building][10] 4 | [![Docs and API][9]][15] [![Codacy][12]][13] [![Codacy coverage][14]][13] 5 | ![Downloads][17] 6 | 7 | Dark magics about variable names in python 8 | 9 | [CHANGELOG][16] | [API][15] | [Playground][11] | :fire: [StackOverflow answer][20] 10 | 11 | ## Installation 12 | 13 | ```shell 14 | pip install -U varname 15 | ``` 16 | 17 | Note if you use `python < 3.8`, install `varname < 0.11` 18 | 19 | ## Features 20 | 21 | - Core features: 22 | 23 | - Retrieving names of variables a function/class call is assigned to from inside it, using `varname`. 24 | - Detecting next immediate attribute name, using `will` 25 | - Fetching argument names/sources passed to a function using `argname` 26 | 27 | - Other helper APIs (built based on core features): 28 | 29 | - A value wrapper to store the variable name that a value is assigned to, using `Wrapper` 30 | - A decorator to register `__varname__` to functions/classes, using `register` 31 | - A helper function to create dict without explicitly specifying the key-value pairs, using `jsobj` 32 | - A `debug` function to print variables with their names and values 33 | - `exec_code` to replace `exec` where source code is available at runtime 34 | 35 | ## Credits 36 | 37 | Thanks goes to these awesome people/projects: 38 | 39 | 40 | 41 | 47 | 53 | 59 | 65 | 71 | 77 | 83 | 84 |
42 | 43 | 44 |
executing 45 |
46 |
48 | 49 | 50 |
@alexmojaki 51 |
52 |
54 | 55 | 56 |
@breuleux 57 |
58 |
60 | 61 | 62 |
@ElCuboNegro 63 |
64 |
66 | 67 | 68 |
@thewchan 69 |
70 |
72 | 73 | 74 |
@LawsOfSympathy 75 |
76 |
78 | 79 | 80 |
@elliotgunton 81 |
82 |
85 | 86 | Special thanks to [@HanyuuLu][2] to give up the name `varname` in pypi for this project. 87 | 88 | ## Usage 89 | 90 | ### Retrieving the variable names using `varname(...)` 91 | 92 | - From inside a function 93 | 94 | ```python 95 | from varname import varname 96 | def function(): 97 | return varname() 98 | 99 | func = function() # func == {func!r} 100 | ``` 101 | 102 | When there are intermediate frames: 103 | 104 | ```python 105 | def wrapped(): 106 | return function() 107 | 108 | def function(): 109 | # retrieve the variable name at the 2nd frame from this one 110 | return varname(frame=2) 111 | 112 | func = wrapped() # func == {func!r} 113 | ``` 114 | 115 | Or use `ignore` to ignore the wrapped frame: 116 | 117 | ```python 118 | def wrapped(): 119 | return function() 120 | 121 | def function(): 122 | return varname(ignore=wrapped) 123 | 124 | func = wrapped() # func == {func!r} 125 | ``` 126 | 127 | Calls from standard libraries are ignored by default: 128 | 129 | ```python 130 | import asyncio 131 | 132 | async def function(): 133 | return varname() 134 | 135 | func = asyncio.run(function()) # func == {func!r} 136 | ``` 137 | 138 | Use `strict` to control whether the call should be assigned to 139 | the variable directly: 140 | 141 | ```python 142 | def function(strict): 143 | return varname(strict=strict) 144 | 145 | func = function(True) # OK, direct assignment, func == {func!r} 146 | 147 | func = [function(True)] # Not a direct assignment, raises {_exc} 148 | func = [function(False)] # OK, func == {func!r} 149 | 150 | func = function(False), function(False) # OK, func = {func!r} 151 | ``` 152 | 153 | - Retrieving name of a class instance 154 | 155 | ```python 156 | class Foo: 157 | def __init__(self): 158 | self.id = varname() 159 | 160 | def copy(self): 161 | # also able to fetch inside a method call 162 | copied = Foo() # copied.id == 'copied' 163 | copied.id = varname() # assign id to whatever variable name 164 | return copied 165 | 166 | foo = Foo() # foo.id == {foo.id!r} 167 | 168 | foo2 = foo.copy() # foo2.id == {foo2.id!r} 169 | ``` 170 | 171 | - Multiple variables on Left-hand side 172 | 173 | ```python 174 | # since v0.5.4 175 | def func(): 176 | return varname(multi_vars=True) 177 | 178 | a = func() # a == {a!r} 179 | a, b = func() # (a, b) == ({a!r}, {b!r}) 180 | [a, b] = func() # (a, b) == ({a!r}, {b!r}) 181 | 182 | # hierarchy is also possible 183 | a, (b, c) = func() # (a, b, c) == ({a!r}, {b!r}, {c!r}) 184 | ``` 185 | 186 | - Some unusual use 187 | 188 | ```python 189 | def function(**kwargs): 190 | return varname(strict=False) 191 | 192 | func = func1 = function() # func == func1 == {func1!r} 193 | # if varname < 0.8: func == func1 == 'func' 194 | # a warning will be shown 195 | # since you may not want func to be {func1!r} 196 | 197 | x = function(y = function()) # x == 'x' 198 | 199 | # get part of the name 200 | func_abc = function()[-3:] # func_abc == 'abc' 201 | 202 | # function alias supported now 203 | function2 = function 204 | func = function2() # func == 'func' 205 | 206 | a = lambda: 0 207 | a.b = function() # a.b == 'a.b' 208 | ``` 209 | 210 | ### The decorator way to register `__varname__` to functions/classes 211 | 212 | - Registering `__varname__` to functions 213 | 214 | ```python 215 | from varname.helpers import register 216 | 217 | @register 218 | def function(): 219 | return __varname__ 220 | 221 | func = function() # func == {func!r} 222 | ``` 223 | 224 | ```python 225 | # arguments also allowed (frame, ignore and raise_exc) 226 | @register(frame=2) 227 | def function(): 228 | return __varname__ 229 | 230 | def wrapped(): 231 | return function() 232 | 233 | func = wrapped() # func == {func!r} 234 | ``` 235 | 236 | - Registering `__varname__` as a class property 237 | 238 | ```python 239 | @register 240 | class Foo: 241 | ... 242 | 243 | foo = Foo() 244 | # foo.__varname__ == {foo.__varname__!r} 245 | ``` 246 | 247 | ### Getting variable names directly using `nameof` 248 | 249 | ```python 250 | from varname import varname, nameof 251 | 252 | a = 1 253 | nameof(a) # {_expr!r} 254 | 255 | b = 2 256 | nameof(a, b) # {_expr!r} 257 | 258 | def func(): 259 | return varname() + '_suffix' 260 | 261 | f = func() # f == {f!r} 262 | nameof(f) # {_expr!r} 263 | 264 | # get full names of (chained) attribute calls 265 | func.a = func 266 | nameof(func.a, vars_only=False) # {_expr!r} 267 | 268 | func.a.b = 1 269 | nameof(func.a.b, vars_only=False) # {_expr!r} 270 | ``` 271 | 272 | ### Detecting next immediate attribute name 273 | 274 | ```python 275 | from varname import will 276 | class AwesomeClass: 277 | def __init__(self): 278 | self.will = None 279 | 280 | def permit(self): 281 | self.will = will(raise_exc=False) 282 | if self.will == 'do': 283 | # let self handle do 284 | return self 285 | raise AttributeError('Should do something with AwesomeClass object') 286 | 287 | def do(self): 288 | if self.will != 'do': 289 | raise AttributeError("You don't have permission to do") 290 | return 'I am doing!' 291 | 292 | awesome = AwesomeClass() 293 | awesome.do() # {_exc_msg} 294 | awesome.permit() # {_exc_msg} 295 | awesome.permit().do() == 'I am doing!' 296 | ``` 297 | 298 | ### Fetching argument names/sources using `argname` 299 | 300 | ```python 301 | from varname import argname 302 | 303 | def func(a, b=1): 304 | print(argname('a')) 305 | 306 | x = y = z = 2 307 | func(x) # prints: {_out} 308 | 309 | def func2(a, b=1): 310 | print(argname('a', 'b')) 311 | func2(y, b=x) # prints: {_out} 312 | 313 | # allow expressions 314 | def func3(a, b=1): 315 | print(argname('a', 'b', vars_only=False)) 316 | func3(x+y, y+x) # prints: {_out} 317 | 318 | # positional and keyword arguments 319 | def func4(*args, **kwargs): 320 | print(argname('args[1]', 'kwargs[c]')) 321 | func4(y, x, c=z) # prints: {_out} 322 | 323 | # As of 0.9.0 (see: https://pwwang.github.io/python-varname/CHANGELOG/#v090) 324 | # Can also fetch the source of the argument for 325 | # __getattr__/__getitem__/__setattr/__setitem__/__add__/__lt__, etc. 326 | class Foo: 327 | def __setattr__(self, name, value): 328 | print(argname("name", "value", func=self.__setattr__)) 329 | 330 | Foo().a = 1 # prints: {_out} 331 | ``` 332 | 333 | ### Value wrapper 334 | 335 | ```python 336 | from varname.helpers import Wrapper 337 | 338 | foo = Wrapper(True) 339 | # foo.name == {foo.name!r} 340 | # foo.value == {foo.value!r} 341 | bar = Wrapper(False) 342 | # bar.name == {bar.name!r} 343 | # bar.value == {bar.value!r} 344 | 345 | def values_to_dict(*args): 346 | return {val.name: val.value for val in args} 347 | 348 | mydict = values_to_dict(foo, bar) 349 | # {mydict} 350 | ``` 351 | 352 | ### Creating dictionary using `jsobj` 353 | 354 | ```python 355 | from varname.helpers import jsobj 356 | 357 | a = 1 358 | b = 2 359 | jsobj(a, b) # {_expr!r} 360 | jsobj(a, b, c=3) # {_expr!r} 361 | ``` 362 | 363 | ### Debugging with `debug` 364 | 365 | ```python 366 | from varname.helpers import debug 367 | 368 | a = 'value' 369 | b = ['val'] 370 | debug(a) 371 | # {_out!r} 372 | debug(b) 373 | # {_out!r} 374 | debug(a, b) 375 | # {_out!r} 376 | debug(a, b, merge=True) 377 | # {_out!r} 378 | debug(a, repr=False, prefix='') 379 | # {_out!r} 380 | # also debug an expression 381 | debug(a+a) 382 | # {_out!r} 383 | # If you want to disable it: 384 | debug(a+a, vars_only=True) # {_exc} 385 | ``` 386 | 387 | ### Replacing `exec` with `exec_code` 388 | 389 | ```python 390 | from varname import argname 391 | from varname.helpers import exec_code 392 | 393 | class Obj: 394 | def __init__(self): 395 | self.argnames = [] 396 | 397 | def receive(self, arg): 398 | self.argnames.append(argname('arg', func=self.receive)) 399 | 400 | obj = Obj() 401 | # exec('obj.receive(1)') # Error 402 | exec_code('obj.receive(1)') 403 | exec_code('obj.receive(2)') 404 | obj.argnames # ['1', '2'] 405 | ``` 406 | 407 | ## Reliability and limitations 408 | 409 | `varname` is all depending on `executing` package to look for the node. 410 | The node `executing` detects is ensured to be the correct one (see [this][19]). 411 | 412 | It partially works with environments where other AST magics apply, including [`exec`][24] function, 413 | [`macropy`][21], [`birdseye`][22], [`reticulate`][23] with `R`, etc. Neither 414 | `executing` nor `varname` is 100% working with those environments. Use 415 | it at your own risk. 416 | 417 | For example: 418 | 419 | - This will not work: 420 | 421 | ```python 422 | from varname import argname 423 | 424 | def getname(x): 425 | print(argname("x")) 426 | 427 | a = 1 428 | exec("getname(a)") # Cannot retrieve the node where the function is called. 429 | 430 | ## instead 431 | # from varname.helpers import exec_code 432 | # exec_code("getname(a)") 433 | ``` 434 | 435 | [1]: https://github.com/pwwang/python-varname 436 | [2]: https://github.com/HanyuuLu 437 | [3]: https://img.shields.io/pypi/v/varname?style=flat-square 438 | [4]: https://pypi.org/project/varname/ 439 | [5]: https://img.shields.io/github/tag/pwwang/python-varname?style=flat-square 440 | [6]: https://github.com/pwwang/python-varname 441 | [7]: logo.png 442 | [8]: https://img.shields.io/pypi/pyversions/varname?style=flat-square 443 | [9]: https://img.shields.io/github/actions/workflow/status/pwwang/python-varname/docs.yml?branch=master 444 | [10]: https://img.shields.io/github/actions/workflow/status/pwwang/python-varname/build.yml?branch=master 445 | [11]: https://mybinder.org/v2/gh/pwwang/python-varname/dev?filepath=playground%2Fplayground.ipynb 446 | [12]: https://img.shields.io/codacy/grade/6fdb19c845f74c5c92056e88d44154f7?style=flat-square 447 | [13]: https://app.codacy.com/gh/pwwang/python-varname/dashboard 448 | [14]: https://img.shields.io/codacy/coverage/6fdb19c845f74c5c92056e88d44154f7?style=flat-square 449 | [15]: https://pwwang.github.io/python-varname/api/varname 450 | [16]: https://pwwang.github.io/python-varname/CHANGELOG/ 451 | [17]: https://img.shields.io/pypi/dm/varname?style=flat-square 452 | [19]: https://github.com/alexmojaki/executing#is-it-reliable 453 | [20]: https://stackoverflow.com/a/59364138/5088165 454 | [21]: https://github.com/lihaoyi/macropy 455 | [22]: https://github.com/alexmojaki/birdseye 456 | [23]: https://rstudio.github.io/reticulate/ 457 | [24]: https://docs.python.org/3/library/functions.html#exec 458 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![varname][7] 2 | 3 | [![Pypi][3]][4] [![Github][5]][6] [![PythonVers][8]][4] ![Building][10] 4 | [![Docs and API][9]][15] [![Codacy][12]][13] [![Codacy coverage][14]][13] 5 | ![Downloads][17] 6 | 7 | Dark magics about variable names in python 8 | 9 | [CHANGELOG][16] | [API][15] | [Playground][11] | :fire: [StackOverflow answer][20] 10 | 11 | ## Installation 12 | 13 | ```shell 14 | pip install -U varname 15 | ``` 16 | 17 | Note if you use `python < 3.8`, install `varname < 0.11` 18 | 19 | ## Features 20 | 21 | - Core features: 22 | 23 | - Retrieving names of variables a function/class call is assigned to from inside it, using `varname`. 24 | - Detecting next immediate attribute name, using `will` 25 | - Fetching argument names/sources passed to a function using `argname` 26 | 27 | - Other helper APIs (built based on core features): 28 | 29 | - A value wrapper to store the variable name that a value is assigned to, using `Wrapper` 30 | - A decorator to register `__varname__` to functions/classes, using `register` 31 | - A helper function to create dict without explicitly specifying the key-value pairs, using `jsobj` 32 | - A `debug` function to print variables with their names and values 33 | - `exec_code` to replace `exec` where source code is available at runtime 34 | 35 | ## Credits 36 | 37 | Thanks goes to these awesome people/projects: 38 | 39 | 40 | 41 | 47 | 53 | 59 | 65 | 71 | 77 | 83 | 84 |
42 | 43 | 44 |
executing 45 |
46 |
48 | 49 | 50 |
@alexmojaki 51 |
52 |
54 | 55 | 56 |
@breuleux 57 |
58 |
60 | 61 | 62 |
@ElCuboNegro 63 |
64 |
66 | 67 | 68 |
@thewchan 69 |
70 |
72 | 73 | 74 |
@LawsOfSympathy 75 |
76 |
78 | 79 | 80 |
@elliotgunton 81 |
82 |
85 | 86 | Special thanks to [@HanyuuLu][2] to give up the name `varname` in pypi for this project. 87 | 88 | ## Usage 89 | 90 | ### Retrieving the variable names using `varname(...)` 91 | 92 | - From inside a function 93 | 94 | ```python 95 | from varname import varname 96 | def function(): 97 | return varname() 98 | 99 | func = function() # func == 'func' 100 | ``` 101 | 102 | When there are intermediate frames: 103 | 104 | ```python 105 | def wrapped(): 106 | return function() 107 | 108 | def function(): 109 | # retrieve the variable name at the 2nd frame from this one 110 | return varname(frame=2) 111 | 112 | func = wrapped() # func == 'func' 113 | ``` 114 | 115 | Or use `ignore` to ignore the wrapped frame: 116 | 117 | ```python 118 | def wrapped(): 119 | return function() 120 | 121 | def function(): 122 | return varname(ignore=wrapped) 123 | 124 | func = wrapped() # func == 'func' 125 | ``` 126 | 127 | Calls from standard libraries are ignored by default: 128 | 129 | ```python 130 | import asyncio 131 | 132 | async def function(): 133 | return varname() 134 | 135 | func = asyncio.run(function()) # func == 'func' 136 | ``` 137 | 138 | Use `strict` to control whether the call should be assigned to 139 | the variable directly: 140 | 141 | ```python 142 | def function(strict): 143 | return varname(strict=strict) 144 | 145 | func = function(True) # OK, direct assignment, func == 'func' 146 | 147 | func = [function(True)] # Not a direct assignment, raises ImproperUseError 148 | func = [function(False)] # OK, func == ['func'] 149 | 150 | func = function(False), function(False) # OK, func = ('func', 'func') 151 | ``` 152 | 153 | - Retrieving name of a class instance 154 | 155 | ```python 156 | class Foo: 157 | def __init__(self): 158 | self.id = varname() 159 | 160 | def copy(self): 161 | # also able to fetch inside a method call 162 | copied = Foo() # copied.id == 'copied' 163 | copied.id = varname() # assign id to whatever variable name 164 | return copied 165 | 166 | foo = Foo() # foo.id == 'foo' 167 | 168 | foo2 = foo.copy() # foo2.id == 'foo2' 169 | ``` 170 | 171 | - Multiple variables on Left-hand side 172 | 173 | ```python 174 | # since v0.5.4 175 | def func(): 176 | return varname(multi_vars=True) 177 | 178 | a = func() # a == ('a',) 179 | a, b = func() # (a, b) == ('a', 'b') 180 | [a, b] = func() # (a, b) == ('a', 'b') 181 | 182 | # hierarchy is also possible 183 | a, (b, c) = func() # (a, b, c) == ('a', 'b', 'c') 184 | ``` 185 | 186 | - Some unusual use 187 | 188 | ```python 189 | def function(**kwargs): 190 | return varname(strict=False) 191 | 192 | func = func1 = function() # func == func1 == 'func1' 193 | # if varname < 0.8: func == func1 == 'func' 194 | # a warning will be shown 195 | # since you may not want func to be 'func1' 196 | 197 | x = function(y = function()) # x == 'x' 198 | 199 | # get part of the name 200 | func_abc = function()[-3:] # func_abc == 'abc' 201 | 202 | # function alias supported now 203 | function2 = function 204 | func = function2() # func == 'func' 205 | 206 | a = lambda: 0 207 | a.b = function() # a.b == 'a.b' 208 | ``` 209 | 210 | ### The decorator way to register `__varname__` to functions/classes 211 | 212 | - Registering `__varname__` to functions 213 | 214 | ```python 215 | from varname.helpers import register 216 | 217 | @register 218 | def function(): 219 | return __varname__ 220 | 221 | func = function() # func == 'func' 222 | ``` 223 | 224 | ```python 225 | # arguments also allowed (frame, ignore and raise_exc) 226 | @register(frame=2) 227 | def function(): 228 | return __varname__ 229 | 230 | def wrapped(): 231 | return function() 232 | 233 | func = wrapped() # func == 'func' 234 | ``` 235 | 236 | - Registering `__varname__` as a class property 237 | 238 | ```python 239 | @register 240 | class Foo: 241 | ... 242 | 243 | foo = Foo() 244 | # foo.__varname__ == 'foo' 245 | ``` 246 | 247 | ### Getting variable names directly using `nameof` 248 | 249 | ```python 250 | from varname import varname, nameof 251 | 252 | a = 1 253 | nameof(a) # 'a' 254 | 255 | b = 2 256 | nameof(a, b) # ('a', 'b') 257 | 258 | def func(): 259 | return varname() + '_suffix' 260 | 261 | f = func() # f == 'f_suffix' 262 | nameof(f) # 'f' 263 | 264 | # get full names of (chained) attribute calls 265 | func.a = func 266 | nameof(func.a, vars_only=False) # 'func.a' 267 | 268 | func.a.b = 1 269 | nameof(func.a.b, vars_only=False) # 'func.a.b' 270 | ``` 271 | 272 | ### Detecting next immediate attribute name 273 | 274 | ```python 275 | from varname import will 276 | class AwesomeClass: 277 | def __init__(self): 278 | self.will = None 279 | 280 | def permit(self): 281 | self.will = will(raise_exc=False) 282 | if self.will == 'do': 283 | # let self handle do 284 | return self 285 | raise AttributeError('Should do something with AwesomeClass object') 286 | 287 | def do(self): 288 | if self.will != 'do': 289 | raise AttributeError("You don't have permission to do") 290 | return 'I am doing!' 291 | 292 | awesome = AwesomeClass() 293 | awesome.do() # AttributeError: You don't have permission to do 294 | awesome.permit() # AttributeError: Should do something with AwesomeClass object 295 | awesome.permit().do() == 'I am doing!' 296 | ``` 297 | 298 | ### Fetching argument names/sources using `argname` 299 | 300 | ```python 301 | from varname import argname 302 | 303 | def func(a, b=1): 304 | print(argname('a')) 305 | 306 | x = y = z = 2 307 | func(x) # prints: x 308 | 309 | 310 | def func2(a, b=1): 311 | print(argname('a', 'b')) 312 | func2(y, b=x) # prints: ('y', 'x') 313 | 314 | 315 | # allow expressions 316 | def func3(a, b=1): 317 | print(argname('a', 'b', vars_only=False)) 318 | func3(x+y, y+x) # prints: ('x+y', 'y+x') 319 | 320 | 321 | # positional and keyword arguments 322 | def func4(*args, **kwargs): 323 | print(argname('args[1]', 'kwargs[c]')) 324 | func4(y, x, c=z) # prints: ('x', 'z') 325 | 326 | 327 | # As of 0.9.0 (see: https://pwwang.github.io/python-varname/CHANGELOG/#v090) 328 | # Can also fetch the source of the argument for 329 | # __getattr__/__getitem__/__setattr/__setitem__/__add__/__lt__, etc. 330 | class Foo: 331 | def __setattr__(self, name, value): 332 | print(argname("name", "value", func=self.__setattr__)) 333 | 334 | Foo().a = 1 # prints: ("'a'", '1') 335 | 336 | ``` 337 | 338 | ### Value wrapper 339 | 340 | ```python 341 | from varname.helpers import Wrapper 342 | 343 | foo = Wrapper(True) 344 | # foo.name == 'foo' 345 | # foo.value == True 346 | bar = Wrapper(False) 347 | # bar.name == 'bar' 348 | # bar.value == False 349 | 350 | def values_to_dict(*args): 351 | return {val.name: val.value for val in args} 352 | 353 | mydict = values_to_dict(foo, bar) 354 | # {'foo': True, 'bar': False} 355 | ``` 356 | 357 | ### Creating dictionary using `jsobj` 358 | 359 | ```python 360 | from varname.helpers import jsobj 361 | 362 | a = 1 363 | b = 2 364 | jsobj(a, b) # {'a': 1, 'b': 2} 365 | jsobj(a, b, c=3) # {'a': 1, 'b': 2, 'c': 3} 366 | ``` 367 | 368 | ### Debugging with `debug` 369 | 370 | ```python 371 | from varname.helpers import debug 372 | 373 | a = 'value' 374 | b = ['val'] 375 | debug(a) 376 | # "DEBUG: a='value'\n" 377 | debug(b) 378 | # "DEBUG: b=['val']\n" 379 | debug(a, b) 380 | # "DEBUG: a='value'\nDEBUG: b=['val']\n" 381 | debug(a, b, merge=True) 382 | # "DEBUG: a='value', b=['val']\n" 383 | debug(a, repr=False, prefix='') 384 | # 'a=value\n' 385 | # also debug an expression 386 | debug(a+a) 387 | # "DEBUG: a+a='valuevalue'\n" 388 | # If you want to disable it: 389 | debug(a+a, vars_only=True) # ImproperUseError 390 | ``` 391 | 392 | ### Replacing `exec` with `exec_code` 393 | 394 | ```python 395 | from varname import argname 396 | from varname.helpers import exec_code 397 | 398 | class Obj: 399 | def __init__(self): 400 | self.argnames = [] 401 | 402 | def receive(self, arg): 403 | self.argnames.append(argname('arg', func=self.receive)) 404 | 405 | obj = Obj() 406 | # exec('obj.receive(1)') # Error 407 | exec_code('obj.receive(1)') 408 | exec_code('obj.receive(2)') 409 | obj.argnames # ['1', '2'] 410 | ``` 411 | 412 | ## Reliability and limitations 413 | 414 | `varname` is all depending on `executing` package to look for the node. 415 | The node `executing` detects is ensured to be the correct one (see [this][19]). 416 | 417 | It partially works with environments where other AST magics apply, including [`exec`][24] function, 418 | [`macropy`][21], [`birdseye`][22], [`reticulate`][23] with `R`, etc. Neither 419 | `executing` nor `varname` is 100% working with those environments. Use 420 | it at your own risk. 421 | 422 | For example: 423 | 424 | - This will not work: 425 | 426 | ```python 427 | from varname import argname 428 | 429 | def getname(x): 430 | print(argname("x")) 431 | 432 | a = 1 433 | exec("getname(a)") # Cannot retrieve the node where the function is called. 434 | 435 | ## instead 436 | # from varname.helpers import exec_code 437 | # exec_code("getname(a)") 438 | ``` 439 | 440 | [1]: https://github.com/pwwang/python-varname 441 | [2]: https://github.com/HanyuuLu 442 | [3]: https://img.shields.io/pypi/v/varname?style=flat-square 443 | [4]: https://pypi.org/project/varname/ 444 | [5]: https://img.shields.io/github/tag/pwwang/python-varname?style=flat-square 445 | [6]: https://github.com/pwwang/python-varname 446 | [7]: logo.png 447 | [8]: https://img.shields.io/pypi/pyversions/varname?style=flat-square 448 | [9]: https://img.shields.io/github/actions/workflow/status/pwwang/python-varname/docs.yml?branch=master 449 | [10]: https://img.shields.io/github/actions/workflow/status/pwwang/python-varname/build.yml?branch=master 450 | [11]: https://mybinder.org/v2/gh/pwwang/python-varname/dev?filepath=playground%2Fplayground.ipynb 451 | [12]: https://img.shields.io/codacy/grade/6fdb19c845f74c5c92056e88d44154f7?style=flat-square 452 | [13]: https://app.codacy.com/gh/pwwang/python-varname/dashboard 453 | [14]: https://img.shields.io/codacy/coverage/6fdb19c845f74c5c92056e88d44154f7?style=flat-square 454 | [15]: https://pwwang.github.io/python-varname/api/varname 455 | [16]: https://pwwang.github.io/python-varname/CHANGELOG/ 456 | [17]: https://img.shields.io/pypi/dm/varname?style=flat-square 457 | [19]: https://github.com/alexmojaki/executing#is-it-reliable 458 | [20]: https://stackoverflow.com/a/59364138/5088165 459 | [21]: https://github.com/lihaoyi/macropy 460 | [22]: https://github.com/alexmojaki/birdseye 461 | [23]: https://rstudio.github.io/reticulate/ 462 | [24]: https://docs.python.org/3/library/functions.html#exec 463 | -------------------------------------------------------------------------------- /varname/ignore.py: -------------------------------------------------------------------------------- 1 | """The frame ignoring system for varname 2 | 3 | There 4 mechanisms to ignore intermediate frames to determine the desired one 4 | so that a variable name should be retrieved at that frame. 5 | 6 | 1. Ignore frames by a given module. Any calls inside it and inside its 7 | submodules will be ignored. A filename (path) to a module is also acceptable 8 | and recommended when code is executed by `exec` without module available. 9 | 2. Ignore frames by a given pair of module and a qualified name (qualname). 10 | See 1) for acceptable modules. The qualname should be unique in that module. 11 | 3. Ignore frames by a (non-decorated) function. 12 | 4. Ignore frames by a decorated function. In this case, you can specified a 13 | tuple with the function and the number of decorators of it. The decorators 14 | on the wrapper function inside the decorators should also be counted. 15 | 16 | Any frames in `varname`, standard libraries, and frames of any expressions like 17 | are ignored by default. 18 | 19 | """ 20 | import sys 21 | import inspect 22 | import warnings 23 | from os import path 24 | from pathlib import Path 25 | from fnmatch import fnmatch 26 | from abc import ABC, abstractmethod 27 | from typing import List, Union 28 | from types import FrameType, ModuleType, FunctionType 29 | 30 | from executing import Source 31 | 32 | try: 33 | import sysconfig # 3.10+ 34 | except ImportError: # pragma: no cover 35 | from distutils import sysconfig 36 | STANDLIB_PATH = sysconfig.get_python_lib(standard_lib=True) 37 | else: 38 | STANDLIB_PATH = sysconfig.get_path('stdlib') 39 | 40 | from .utils import ( 41 | IgnoreElemType, 42 | IgnoreType, 43 | MaybeDecoratedFunctionWarning, 44 | cached_getmodule, 45 | attach_ignore_id_to_module, 46 | frame_matches_module_by_ignore_id, 47 | check_qualname_by_source, 48 | debug_ignore_frame, 49 | ) 50 | 51 | 52 | class IgnoreElem(ABC): 53 | """An element of the ignore list""" 54 | 55 | def __init_subclass__(cls, attrs: List[str]) -> None: 56 | """Define different attributes for subclasses""" 57 | 58 | def subclass_init( 59 | self, 60 | # IgnoreModule: ModuleType 61 | # IgnoreFilename/IgnoreDirname: str 62 | # IgnoreFunction: FunctionType 63 | # IgnoreDecorated: FunctionType, int 64 | # IgnoreModuleQualname/IgnoreFilenameQualname: 65 | # ModuleType/str, str 66 | # IgnoreOnlyQualname: None, str 67 | *ign_args: Union[str, int, ModuleType, FunctionType], 68 | ) -> None: 69 | """__init__ function for subclasses""" 70 | for attr, arg in zip(attrs, ign_args): 71 | setattr(self, attr, arg) 72 | 73 | self._post_init() 74 | 75 | # save it for __repr__ 76 | cls.attrs = attrs 77 | cls.__init__ = subclass_init # type: ignore 78 | 79 | def _post_init(self) -> None: 80 | """Setups after __init__""" 81 | 82 | @abstractmethod 83 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 84 | """Whether the frame matches the ignore element""" 85 | 86 | def __repr__(self) -> str: 87 | """Representation of the element""" 88 | attr_values = (getattr(self, attr) for attr in self.__class__.attrs) 89 | # get __name__ if possible 90 | attr_values = ( 91 | repr(getattr(attr_value, "__name__", attr_value)) 92 | for attr_value in attr_values 93 | ) 94 | attr_values = ", ".join(attr_values) 95 | return f"{self.__class__.__name__}({attr_values})" 96 | 97 | 98 | class IgnoreModule(IgnoreElem, attrs=["module"]): 99 | """Ignore calls from a module or its submodules""" 100 | 101 | def _post_init(self) -> None: 102 | attach_ignore_id_to_module(self.module) 103 | 104 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 105 | frame = frameinfos[frame_no].frame 106 | module = cached_getmodule(frame.f_code) 107 | if module: 108 | return ( 109 | module.__name__ == self.module.__name__ 110 | or module.__name__.startswith(f"{self.module.__name__}.") 111 | ) 112 | 113 | return frame_matches_module_by_ignore_id(frame, self.module) 114 | 115 | 116 | class IgnoreFilename(IgnoreElem, attrs=["filename"]): 117 | """Ignore calls from a module by matching its filename""" 118 | 119 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 120 | frame = frameinfos[frame_no].frame 121 | 122 | # in case of symbolic links 123 | return path.realpath(frame.f_code.co_filename) == path.realpath( 124 | self.filename 125 | ) 126 | 127 | 128 | class IgnoreDirname(IgnoreElem, attrs=["dirname"]): 129 | """Ignore calls from modules inside a directory 130 | 131 | Currently used internally to ignore calls from standard libraries.""" 132 | 133 | def _post_init(self) -> None: 134 | 135 | # Path object will turn into str here 136 | self.dirname = path.realpath(self.dirname) # type: str 137 | 138 | if not self.dirname.endswith(path.sep): 139 | self.dirname = f"{self.dirname}{path.sep}" 140 | 141 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 142 | frame = frameinfos[frame_no].frame 143 | filename = path.realpath(frame.f_code.co_filename) 144 | 145 | return filename.startswith(self.dirname) 146 | 147 | 148 | class IgnoreStdlib(IgnoreDirname, attrs=["dirname"]): 149 | """Ignore standard libraries in sysconfig.get_python_lib(standard_lib=True) 150 | 151 | But we need to ignore 3rd-party packages under site-packages/. 152 | """ 153 | 154 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 155 | frame = frameinfos[frame_no].frame 156 | third_party_lib = f"{self.dirname}site-packages{path.sep}" 157 | filename = path.realpath(frame.f_code.co_filename) 158 | 159 | return ( 160 | filename.startswith(self.dirname) 161 | # Exclude 3rd-party libraries in site-packages 162 | and not filename.startswith(third_party_lib) 163 | ) 164 | 165 | 166 | class IgnoreFunction(IgnoreElem, attrs=["func"]): 167 | """Ignore a non-decorated function""" 168 | 169 | def _post_init(self) -> None: 170 | if ( 171 | # without functools.wraps 172 | "" in self.func.__qualname__ 173 | or self.func.__name__ != self.func.__code__.co_name 174 | ): 175 | warnings.warn( 176 | f"You asked varname to ignore function {self.func.__name__!r}, " 177 | "which may be decorated. If it is not intended, you may need " 178 | "to ignore all intermediate frames with a tuple of " 179 | "the function and the number of its decorators.", 180 | MaybeDecoratedFunctionWarning, 181 | ) 182 | 183 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 184 | frame = frameinfos[frame_no].frame 185 | return frame.f_code == self.func.__code__ 186 | 187 | 188 | class IgnoreDecorated(IgnoreElem, attrs=["func", "n_decor"]): 189 | """Ignore a decorated function""" 190 | 191 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 192 | try: 193 | frame = frameinfos[frame_no + self.n_decor].frame 194 | except IndexError: 195 | return False 196 | 197 | return frame.f_code == self.func.__code__ 198 | 199 | 200 | class IgnoreModuleQualname(IgnoreElem, attrs=["module", "qualname"]): 201 | """Ignore calls by qualified name in the module""" 202 | 203 | def _post_init(self) -> None: 204 | 205 | attach_ignore_id_to_module(self.module) 206 | # check uniqueness of qualname 207 | modfile = getattr(self.module, "__file__", None) 208 | if modfile is not None: 209 | check_qualname_by_source( 210 | Source.for_filename(modfile, self.module.__dict__), 211 | self.module.__name__, 212 | self.qualname, 213 | ) 214 | 215 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 216 | frame = frameinfos[frame_no].frame 217 | module = cached_getmodule(frame.f_code) 218 | 219 | # Return earlier to avoid qualname uniqueness check 220 | if module and module != self.module: 221 | return False 222 | 223 | if not module and not frame_matches_module_by_ignore_id( 224 | frame, self.module 225 | ): 226 | return False 227 | 228 | source = Source.for_frame(frame) 229 | check_qualname_by_source(source, self.module.__name__, self.qualname) 230 | 231 | return fnmatch(source.code_qualname(frame.f_code), self.qualname) 232 | 233 | 234 | class IgnoreFilenameQualname(IgnoreElem, attrs=["filename", "qualname"]): 235 | """Ignore calls with given qualname in the module with the filename""" 236 | 237 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 238 | frame = frameinfos[frame_no].frame 239 | 240 | frame_filename = path.realpath(frame.f_code.co_filename) 241 | preset_filename = path.realpath(self.filename) 242 | # return earlier to avoid qualname uniqueness check 243 | if frame_filename != preset_filename: 244 | return False 245 | 246 | source = Source.for_frame(frame) 247 | check_qualname_by_source(source, self.filename, self.qualname) 248 | 249 | return fnmatch(source.code_qualname(frame.f_code), self.qualname) 250 | 251 | 252 | class IgnoreOnlyQualname(IgnoreElem, attrs=["_none", "qualname"]): 253 | """Ignore calls that match the given qualname, across all frames.""" 254 | 255 | def match(self, frame_no: int, frameinfos: List[inspect.FrameInfo]) -> bool: 256 | frame = frameinfos[frame_no].frame 257 | 258 | # module is None, check qualname only 259 | return fnmatch( 260 | Source.for_frame(frame).code_qualname(frame.f_code), self.qualname 261 | ) 262 | 263 | 264 | def create_ignore_elem(ignore_elem: IgnoreElemType) -> IgnoreElem: 265 | """Create an ignore element according to the type""" 266 | if isinstance(ignore_elem, ModuleType): 267 | return IgnoreModule(ignore_elem) # type: ignore 268 | if isinstance(ignore_elem, (Path, str)): 269 | return ( 270 | IgnoreDirname(ignore_elem) # type: ignore 271 | if path.isdir(ignore_elem) 272 | else IgnoreFilename(ignore_elem) # type: ignore 273 | ) 274 | if hasattr(ignore_elem, "__code__"): 275 | return IgnoreFunction(ignore_elem) # type: ignore 276 | if not isinstance(ignore_elem, tuple) or len(ignore_elem) != 2: 277 | raise ValueError(f"Unexpected ignore item: {ignore_elem!r}") 278 | # is tuple and len == 2 279 | if hasattr(ignore_elem[0], "__code__") and isinstance(ignore_elem[1], int): 280 | return IgnoreDecorated(*ignore_elem) # type: ignore 281 | # otherwise, the second element should be qualname 282 | if not isinstance(ignore_elem[1], str): 283 | raise ValueError(f"Unexpected ignore item: {ignore_elem!r}") 284 | 285 | if isinstance(ignore_elem[0], ModuleType): 286 | return IgnoreModuleQualname(*ignore_elem) # type: ignore 287 | if isinstance(ignore_elem[0], (Path, str)): 288 | return IgnoreFilenameQualname(*ignore_elem) # type: ignore 289 | if ignore_elem[0] is None: 290 | return IgnoreOnlyQualname(*ignore_elem) 291 | 292 | raise ValueError(f"Unexpected ignore item: {ignore_elem!r}") 293 | 294 | 295 | class IgnoreList: 296 | """The ignore list to match the frames to see if they should be ignored""" 297 | 298 | @classmethod 299 | def create( 300 | cls, 301 | ignore: IgnoreType = None, 302 | ignore_lambda: bool = True, 303 | ignore_varname: bool = True, 304 | ) -> "IgnoreList": 305 | """Create an IgnoreList object 306 | 307 | Args: 308 | ignore: An element of the ignore list, either 309 | A module (or filename of a module) 310 | A tuple of module (or filename) and qualified name 311 | A function 312 | A tuple of function and number of decorators 313 | ignore_lambda: whether ignore lambda functions 314 | ignore_varname: whether the calls from this package 315 | 316 | Returns: 317 | The IgnoreList object 318 | """ 319 | ignore = ignore or [] 320 | if not isinstance(ignore, list): 321 | ignore = [ignore] 322 | 323 | ignore_list = [ 324 | IgnoreStdlib(STANDLIB_PATH) # type: ignore 325 | ] # type: List[IgnoreElem] 326 | if ignore_varname: 327 | ignore_list.append(create_ignore_elem(sys.modules[__package__])) 328 | if ignore_lambda: 329 | ignore_list.append(create_ignore_elem((None, "*"))) 330 | for ignore_elem in ignore: 331 | ignore_list.append(create_ignore_elem(ignore_elem)) 332 | 333 | return cls(ignore_list) # type: ignore 334 | 335 | def __init__(self, ignore_list: List[IgnoreElemType]) -> None: 336 | self.ignore_list = ignore_list 337 | debug_ignore_frame(">>> IgnoreList initiated <<<") 338 | 339 | def nextframe_to_check( 340 | self, frame_no: int, frameinfos: List[inspect.FrameInfo] 341 | ) -> int: 342 | """Find the next frame to check 343 | 344 | In modst cases, the next frame to check is the next adjacent frame. 345 | But for IgnoreDecorated, the next frame to check should be the next 346 | `ignore[1]`th frame. 347 | 348 | Args: 349 | frame_no: The index of current frame to check 350 | frameinfos: The frame info objects 351 | 352 | Returns: 353 | A number for Next `N`th frame to check. 0 if no frame matched. 354 | """ 355 | for ignore_elem in self.ignore_list: 356 | matched = ignore_elem.match(frame_no, frameinfos) # type: ignore 357 | if matched and isinstance(ignore_elem, IgnoreDecorated): 358 | debug_ignore_frame( 359 | f"Ignored by {ignore_elem!r}", frameinfos[frame_no] 360 | ) 361 | return ignore_elem.n_decor + 1 362 | 363 | if matched: 364 | debug_ignore_frame( 365 | f"Ignored by {ignore_elem!r}", frameinfos[frame_no] 366 | ) 367 | return 1 368 | return 0 369 | 370 | def get_frame(self, frame_no: int) -> FrameType: 371 | """Get the right frame by the frame number 372 | 373 | Args: 374 | frame_no: The index of the frame to get 375 | 376 | Returns: 377 | The desired frame 378 | 379 | Raises: 380 | VarnameRetrievingError: if any exceptions raised during the process. 381 | """ 382 | try: 383 | # since this function will be called by APIs 384 | # so we should skip that 385 | frames = inspect.getouterframes(sys._getframe(2), 0) 386 | i = 0 387 | 388 | while i < len(frames): 389 | nextframe = self.nextframe_to_check(i, frames) 390 | # ignored 391 | if nextframe > 0: 392 | i += nextframe 393 | continue 394 | 395 | frame_no -= 1 396 | if frame_no == 0: 397 | debug_ignore_frame("Gotcha!", frames[i]) 398 | return frames[i].frame 399 | 400 | debug_ignore_frame( 401 | f"Skipping ({frame_no - 1} more to skip)", frames[i] 402 | ) 403 | i += 1 404 | 405 | except Exception as exc: 406 | from .utils import VarnameRetrievingError 407 | 408 | raise VarnameRetrievingError from exc 409 | 410 | return None # pragma: no cover 411 | -------------------------------------------------------------------------------- /tests/test_varname.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import subprocess 3 | from functools import wraps 4 | 5 | import pytest 6 | from executing import Source 7 | from varname import varname 8 | from varname.utils import ( 9 | VarnameRetrievingError, 10 | QualnameNonUniqueError, 11 | ImproperUseError, 12 | MaybeDecoratedFunctionWarning, 13 | MultiTargetAssignmentWarning, 14 | get_node, 15 | ) 16 | 17 | 18 | from .conftest import run_async, module_from_source 19 | 20 | SELF = sys.modules[__name__] 21 | 22 | 23 | def test_function(): 24 | 25 | def function(): 26 | return varname() 27 | 28 | func = function() 29 | assert func == "func" 30 | 31 | 32 | def test_function_with_frame_arg(): 33 | 34 | def function(): 35 | # I know that at which frame (3rd) this will be called 36 | return varname(frame=3) 37 | 38 | def function1(): 39 | return function() 40 | 41 | def function2(): 42 | return function1() 43 | 44 | func = function2() 45 | assert func == "func" 46 | 47 | 48 | def test_class(): 49 | 50 | class Foo: 51 | def __init__(self): 52 | self.id = varname() 53 | 54 | def copy(self): 55 | return varname() 56 | 57 | k = Foo() 58 | assert k.id == "k" 59 | 60 | k2 = k.copy() 61 | assert k2 == "k2" 62 | 63 | 64 | def test_class_with_frame_arg(): 65 | 66 | class Foo: 67 | def __init__(self): 68 | self.id = self.some_internal() 69 | 70 | def some_internal(self): 71 | return varname(frame=2) 72 | 73 | def copy(self): 74 | return self.copy_id() 75 | 76 | def copy_id(self): 77 | return self.copy_id_internal() 78 | 79 | def copy_id_internal(self): 80 | return varname(frame=3) 81 | 82 | k = Foo() 83 | assert k.id == "k" 84 | 85 | k2 = k.copy() 86 | assert k2 == "k2" 87 | 88 | 89 | def test_single_var_lhs_required(): 90 | """Only one variable to receive the name on LHS""" 91 | 92 | def function(): 93 | return varname() 94 | 95 | with pytest.raises( 96 | ImproperUseError, match="Expect a single variable on left-hand side" 97 | ): 98 | x, y = function() 99 | 100 | with pytest.raises(ImproperUseError): 101 | x, y = function(), function() # noqa: F841 102 | 103 | 104 | def test_multi_vars_lhs(): 105 | """Tests multiple variables on the left hand side""" 106 | 107 | def function(): 108 | return varname(multi_vars=True) 109 | 110 | a, b = function() 111 | assert (a, b) == ("a", "b") 112 | [a, b] = function() 113 | assert (a, b) == ("a", "b") 114 | a = function() 115 | assert a == ("a",) 116 | # hierarchy 117 | a, (b, c) = function() 118 | assert (a, b, c) == ("a", "b", "c") 119 | # with attributes 120 | x = lambda: 1 121 | a, (b, x.c) = function() 122 | assert (a, b, x.c) == ("a", "b", "x.c") 123 | 124 | # Not all LHS are variables 125 | def fn(x): 126 | return 1 127 | y = {} 128 | with pytest.raises( 129 | ImproperUseError, 130 | match=r"Node 'y\[Call\]' detected", 131 | ): 132 | y[fn(a)] = function() 133 | 134 | 135 | def test_raise_exc(): 136 | 137 | def get_name(raise_exc): 138 | return varname(raise_exc=raise_exc, strict=False) 139 | 140 | with pytest.raises(ImproperUseError): 141 | get_name(True) 142 | 143 | name = "0" 144 | # we can't get it in such way, even with strict=False 145 | with pytest.raises(ImproperUseError): 146 | name += str(get_name(False)) 147 | 148 | 149 | def test_strict(): 150 | 151 | def foo(x): 152 | return x 153 | 154 | def function(): 155 | return varname(strict=True) 156 | 157 | func = function() 158 | assert func == "func" 159 | 160 | with pytest.raises(ImproperUseError): 161 | func = function() + "_" 162 | 163 | with pytest.raises(ImproperUseError): 164 | func = foo(function()) 165 | 166 | with pytest.raises(ImproperUseError): 167 | func = [function()] 168 | 169 | 170 | def test_not_strict(): 171 | 172 | def function(): 173 | return varname(strict=False) 174 | 175 | func = function() 176 | assert func == "func" 177 | 178 | func = [function()] 179 | assert func == ["func"] 180 | 181 | func = [function(), function()] 182 | assert func == ["func", "func"] 183 | 184 | func = (function(),) 185 | assert func == ("func",) 186 | 187 | func = (function(), function()) 188 | assert func == ("func", "func") 189 | 190 | 191 | @pytest.mark.skipif( 192 | sys.version_info < (3, 8), reason="named expressions require Python >= 3.8" 193 | ) 194 | def test_named_expr(): 195 | from .named_expr import a 196 | 197 | assert a == ["b", "c"] 198 | 199 | 200 | def test_multiple_targets(): 201 | 202 | def function(): 203 | return varname() 204 | 205 | with pytest.warns( 206 | MultiTargetAssignmentWarning, match="Multiple targets in assignment" 207 | ): 208 | y = x = function() 209 | assert y == x == "x" 210 | 211 | 212 | def test_subscript(): 213 | 214 | class C: 215 | def __init__(self): 216 | self.value = None 217 | 218 | def __setitem__(self, key, value): 219 | self.value = value 220 | 221 | x = {"a": 1, "b": 2} 222 | y = [0, 1, 2, 3, 4] 223 | a = "a" 224 | b = 1 225 | c = C() 226 | 227 | def func(): 228 | return varname() 229 | 230 | x[0] = func() 231 | assert x[0] == "x[0]" 232 | x[a] = func() 233 | assert x[a] == "x[a]" 234 | x["a"] = func() 235 | assert x["a"] == "x['a']" 236 | c[[1]] = func() 237 | assert c.value == "c[[1]]" 238 | c[(1,)] = func() 239 | assert c.value == "c[(1,)]" 240 | c[(1, 2)] = func() 241 | assert c.value == "c[(1, 2)]" 242 | y[b] = func() 243 | assert y[b] == "y[b]" 244 | y[1] = func() 245 | assert y[1] == "y[1]" 246 | c[1:4:2] = func() 247 | assert c.value == "c[1:4:2]" 248 | c[1:4:2, 1:4:2] = func() 249 | assert c.value == "c[(1:4:2, 1:4:2)]" 250 | c[1:] = func() 251 | assert c.value == "c[1:]" 252 | c[:4] = func() 253 | assert c.value == "c[:4]" 254 | c[:] = func() 255 | assert c.value == "c[:]" 256 | # BinOp in subscript 257 | c[b + 1] = func() 258 | assert c.value == "c[b + 1]" 259 | c[b is not c] = func() 260 | # Compare in subscript 261 | assert c.value == "c[b is not c]" 262 | c[b == 1] = func() 263 | assert c.value == "c[b == 1]" 264 | c[b < 1] = func() 265 | assert c.value == "c[b < 1]" 266 | # UnaryOp in subscript 267 | c[-b] = func() 268 | assert c.value == "c[-b]" 269 | c[~b] = func() 270 | assert c.value == "c[~b]" 271 | # BoolOp in subscript 272 | c[b < 1 and b > 0] = func() 273 | assert c.value == "c[b < 1 and b > 0]" 274 | c[b < 1 or b > 0] = func() 275 | assert c.value == "c[b < 1 or b > 0]" 276 | c[0 < b < 2] = func() 277 | assert c.value == "c[0 < b < 2]" 278 | 279 | 280 | def test_unusual(): 281 | 282 | def function(): 283 | return varname(strict=False) 284 | 285 | # something ridiculous 286 | xyz = function()[-1:] 287 | assert xyz == "z" 288 | 289 | x = "a" 290 | with pytest.raises(ImproperUseError): 291 | x += function() 292 | assert x == "a" 293 | 294 | # alias 295 | func = function 296 | x = func() 297 | assert x == "x" 298 | 299 | 300 | def test_from_property(): 301 | class C: 302 | @property 303 | def var(self): 304 | return varname() 305 | 306 | c = C() 307 | v1 = c.var 308 | assert v1 == "v1" 309 | 310 | 311 | def test_frame_fail(no_getframe): 312 | """Test when failed to retrieve the frame""" 313 | # Let's monkey-patch inspect.stack to do this 314 | assert get_node(1) is None 315 | 316 | def func(raise_exc): 317 | return varname(raise_exc=raise_exc) 318 | 319 | with pytest.raises(VarnameRetrievingError): 320 | a = func(True) # noqa: F841 321 | 322 | b = func(False) 323 | assert b is None 324 | 325 | 326 | def test_ignore_module_filename(): 327 | source = "def foo(): return bar()" 328 | 329 | code = compile(source, "", "exec") 330 | 331 | def bar(): 332 | return varname(ignore="") 333 | 334 | globs = {"bar": bar} 335 | exec(code, globs) 336 | foo = globs["foo"] 337 | f = foo() 338 | assert f == "f" 339 | 340 | 341 | def test_ignore_module_no_file(tmp_path): 342 | module = module_from_source( 343 | "ignore_module", 344 | """ 345 | def foo(): 346 | return bar() 347 | """, 348 | tmp_path, 349 | ) 350 | # force injecting __varname_ignore_id__ 351 | del module.__file__ 352 | 353 | def bar(): 354 | return varname( 355 | ignore=[ 356 | (module, "foo"), # can't get module by inspect.getmodule 357 | module, 358 | ] 359 | ) 360 | 361 | module.bar = bar 362 | 363 | f = module.foo() 364 | assert f == "f" 365 | 366 | 367 | def test_ignore_module_qualname_no_source(tmp_path): 368 | module = module_from_source( 369 | "ignore_module_qualname_no_source", 370 | """ 371 | def bar(): 372 | return 1 373 | """, 374 | tmp_path, 375 | ) 376 | source = Source.for_filename(module.__file__) 377 | # simulate when source is not available 378 | # no way to check uniqueness of qualname 379 | source.tree = None 380 | 381 | def foo(): 382 | return varname(ignore=(module, "bar")) 383 | 384 | f = foo() # noqa: F841 385 | 386 | 387 | def test_ignore_module_qualname_ucheck_in_match( 388 | tmp_path, frame_matches_module_by_ignore_id_false 389 | ): 390 | module = module_from_source( 391 | "ignore_module_qualname_no_source_ucheck_in_match", 392 | """ 393 | def foo(): 394 | return bar() 395 | """, 396 | tmp_path, 397 | ) 398 | # force uniqueness to be checked in match 399 | # module cannot be fetched by inspect.getmodule 400 | module.__file__ = None 401 | 402 | def bar(): 403 | return varname( 404 | ignore=[ 405 | ( 406 | module, 407 | "foo", 408 | ), # frame_matches_module_by_ignore_id_false makes this fail 409 | (None, "foo"), # make sure foo to be ignored 410 | ] 411 | ) 412 | 413 | module.bar = bar 414 | 415 | f = module.foo() 416 | assert f == "f" 417 | 418 | 419 | def test_ignore_module_qualname(tmp_path, capsys, enable_debug): 420 | module = module_from_source( 421 | "ignore_module_qualname", 422 | """ 423 | def foo1(): 424 | return bar() 425 | """, 426 | tmp_path, 427 | ) 428 | 429 | module.__file__ = None 430 | # module.__varname_ignore_id__ = object() 431 | 432 | def bar(): 433 | var = varname(ignore=(module, "foo1")) 434 | return var 435 | 436 | module.bar = bar 437 | 438 | f = module.foo1() 439 | assert f == "f" 440 | 441 | 442 | def test_ignore_filename_qualname(): 443 | source = ( 444 | "import sys\n" 445 | "import __main__\n" 446 | "import varname\n" 447 | "varname.config.debug = True\n" 448 | "from varname import varname\n" 449 | "def func(): \n" 450 | " return varname(ignore=[\n" 451 | ' ("unknown", "wrapped"), \n' # used to trigger filename mismatch 452 | ' ("", "wrapped")\n' 453 | " ])\n\n" 454 | "def wrapped():\n" 455 | " return func()\n\n" 456 | "variable = wrapped()\n" 457 | ) 458 | 459 | # code = compile(source, '', 'exec') 460 | # # ??? NameError: name 'func' is not defined 461 | # exec(code) 462 | 463 | p = subprocess.Popen( 464 | [sys.executable], 465 | stdin=subprocess.PIPE, 466 | stdout=subprocess.PIPE, 467 | stderr=subprocess.STDOUT, 468 | encoding="utf8", 469 | ) 470 | out, _ = p.communicate(input=source) 471 | assert "Ignored by IgnoreFilenameQualname('', 'wrapped')" in out 472 | 473 | 474 | def test_ignore_function_warning(): 475 | 476 | def my_decorator(f): 477 | @wraps(f) 478 | def wrapper(): 479 | return f() 480 | 481 | return wrapper 482 | 483 | @my_decorator 484 | def func1(): 485 | return func2() 486 | 487 | def func2(): 488 | return varname(ignore=[func1, (func1, 1)]) 489 | 490 | with pytest.warns( 491 | MaybeDecoratedFunctionWarning, 492 | match="You asked varname to ignore function 'func1'", 493 | ): 494 | f = func1() # noqa: F841 495 | 496 | 497 | def test_ignore_decorated(): 498 | def my_decorator(f): 499 | def wrapper(): 500 | return f() 501 | 502 | return wrapper 503 | 504 | @my_decorator 505 | def foo4(): 506 | return foo5() 507 | 508 | def foo5(): 509 | return varname(ignore=(foo4, 1)) 510 | 511 | f4 = foo4() 512 | assert f4 == "f4" 513 | 514 | @my_decorator 515 | def foo6(): 516 | return foo7() 517 | 518 | def foo7(): 519 | return varname(ignore=(foo4, 100)) 520 | 521 | with pytest.raises(ImproperUseError): 522 | f6 = foo6() # noqa: F841 523 | 524 | 525 | def test_ignore_dirname(tmp_path): 526 | module = module_from_source( 527 | "ignore_dirname", 528 | """ 529 | from varname import varname 530 | def bar(dirname): 531 | return varname(ignore=[dirname]) 532 | """, 533 | tmp_path, 534 | ) 535 | 536 | def foo(): 537 | return module.bar(tmp_path) 538 | 539 | f = foo() 540 | assert f == "f" 541 | 542 | 543 | def test_type_anno_varname(): 544 | 545 | class Foo: 546 | def __init__(self): 547 | self.id = varname() 548 | 549 | foo: Foo = Foo() 550 | assert foo.id == "foo" 551 | 552 | 553 | def test_generic_type_varname(): 554 | from typing import Generic, TypeVar 555 | 556 | T = TypeVar("T") 557 | 558 | class Foo(Generic[T]): 559 | def __init__(self): 560 | # Standard libraries are ignored by default now (0.6.0) 561 | # self.id = varname(ignore=[typing]) 562 | self.id = varname() 563 | 564 | foo = Foo[int]() 565 | assert foo.id == "foo" 566 | 567 | bar: Foo = Foo[str]() 568 | assert bar.id == "bar" 569 | 570 | baz = Foo() 571 | assert baz.id == "baz" 572 | 573 | 574 | def test_async_varname(): 575 | from . import conftest 576 | 577 | async def func(): 578 | return varname(ignore=(conftest, "run_async")) 579 | 580 | async def func2(): 581 | return varname(ignore=run_async) 582 | 583 | x = run_async(func()) 584 | assert x == "x" 585 | 586 | x2 = run_async(func2()) 587 | assert x2 == "x2" 588 | 589 | # frame and ignore together 590 | async def func3(): 591 | # also works this way 592 | return varname(frame=2, ignore=run_async) 593 | 594 | async def main(): 595 | return await func3() 596 | 597 | x3 = run_async(main()) 598 | assert x3 == "x3" 599 | 600 | 601 | def test_invalid_ignores(): 602 | # unexpected ignore item 603 | def func(): 604 | return varname(ignore=1) 605 | 606 | with pytest.raises(ValueError): 607 | f = func() 608 | 609 | def func(): 610 | return varname(ignore=(1, 2)) 611 | 612 | with pytest.raises(ValueError): 613 | f = func() 614 | 615 | def func(): 616 | return varname(ignore=(1, "2")) 617 | 618 | with pytest.raises(ValueError): 619 | f = func() # noqa: F841 620 | 621 | 622 | def test_qualname_ignore_fail(): 623 | # non-unique qualname 624 | def func(): 625 | return varname( 626 | ignore=[(SELF, "test_qualname_ignore_fail..wrapper")] 627 | ) 628 | 629 | def wrapper(): 630 | return func() 631 | 632 | wrapper2 = wrapper # noqa: F841 633 | 634 | def wrapper(): 635 | return func() 636 | 637 | with pytest.raises(QualnameNonUniqueError): 638 | f = func() # noqa: F841 639 | 640 | 641 | def test_ignore_lambda(): 642 | def foo(): 643 | return varname() 644 | 645 | bar = lambda: foo() 646 | 647 | b = bar() 648 | assert b == "b" 649 | 650 | 651 | def test_internal_debug(capsys, enable_debug): 652 | def my_decorator(f): 653 | def wrapper(): 654 | return f() 655 | 656 | return wrapper 657 | 658 | @my_decorator 659 | def foo1(): 660 | return foo2() 661 | 662 | @my_decorator 663 | def foo2(): 664 | return foo3() 665 | 666 | @my_decorator 667 | def foo3(): 668 | return varname( 669 | frame=3, 670 | ignore=[ 671 | (SELF, "*.wrapper"), 672 | # unrelated qualname will not be hit at all 673 | (sys, "wrapper"), 674 | ], 675 | ) 676 | 677 | x = foo1() 678 | assert x == "x" 679 | msgs = capsys.readouterr().err.splitlines() 680 | assert ">>> IgnoreList initiated <<<" in msgs[0] 681 | assert "Ignored by IgnoreModule('varname')" in msgs[1] 682 | assert "Skipping (2 more to skip) [In 'foo3'" in msgs[2] 683 | assert ( 684 | "Ignored by IgnoreModuleQualname('tests.test_varname', '*.wrapper')" 685 | in msgs[3] 686 | ) 687 | assert "Skipping (1 more to skip) [In 'foo2'" in msgs[4] 688 | assert ( 689 | "Ignored by IgnoreModuleQualname('tests.test_varname', '*.wrapper')" 690 | in msgs[5] 691 | ) 692 | assert "Skipping (0 more to skip) [In 'foo1'" in msgs[6] 693 | assert ( 694 | "Ignored by IgnoreModuleQualname('tests.test_varname', '*.wrapper')" 695 | in msgs[7] 696 | ) 697 | assert "Gotcha! [In 'test_internal_debug'" in msgs[8] 698 | 699 | 700 | def test_star_var(): 701 | def func(): 702 | v = varname(multi_vars=True) 703 | return 1, 2, 3, v 704 | 705 | a, *b, c = func() 706 | assert a == 1 707 | assert b == [2, 3] 708 | assert c == ("a", "*b", "c") 709 | -------------------------------------------------------------------------------- /varname/core.py: -------------------------------------------------------------------------------- 1 | """Provide core features for varname""" 2 | from __future__ import annotations 3 | import ast 4 | import re 5 | import warnings 6 | from typing import Any, List, Union, Tuple, Type, Callable, overload 7 | 8 | from executing import Source 9 | 10 | from .utils import ( 11 | bytecode_nameof, 12 | get_node, 13 | get_node_by_frame, 14 | lookfor_parent_assign, 15 | node_name, 16 | get_argument_sources, 17 | get_function_called_argname, 18 | rich_exc_message, 19 | reconstruct_func_node, 20 | ArgSourceType, 21 | VarnameRetrievingError, 22 | ImproperUseError, 23 | MultiTargetAssignmentWarning, 24 | ) 25 | from .ignore import IgnoreList, IgnoreType 26 | 27 | 28 | def varname( 29 | frame: int = 1, 30 | ignore: IgnoreType = None, 31 | multi_vars: bool = False, 32 | raise_exc: bool = True, 33 | strict: bool = True, 34 | ) -> Union[str, Tuple[Union[str, Tuple], ...]]: 35 | """Get the name of the variable(s) that assigned by function call or 36 | class instantiation. 37 | 38 | To debug and specify the right frame and ignore arguments, you can set 39 | debug on and see how the frames are ignored or selected: 40 | 41 | >>> from varname import config 42 | >>> config.debug = True 43 | 44 | Args: 45 | frame: `N`th frame used to retrieve the variable name. This means 46 | `N-1` intermediate frames will be skipped. Note that the frames 47 | match `ignore` will not be counted. See `ignore` for details. 48 | ignore: Frames to be ignored in order to reach the `N`th frame. 49 | These frames will not be counted to skip within that `N-1` frames. 50 | You can specify: 51 | - A module (or filename of a module). Any calls from it and its 52 | submodules will be ignored. 53 | - A function. If it looks like it might be a decorated function, 54 | a `MaybeDecoratedFunctionWarning` will be shown. 55 | - Tuple of a function and a number of additional frames that should 56 | be skipped just before reaching this function in the stack. 57 | This is typically used for functions that have been decorated 58 | with a 'classic' decorator that replaces the function with 59 | a wrapper. In that case each such decorator involved should 60 | be counted in the number that's the second element of the tuple. 61 | - Tuple of a module (or filename) and qualified name (qualname). 62 | You can use Unix shell-style wildcards to match the qualname. 63 | Otherwise the qualname must appear exactly once in the 64 | module/file. 65 | By default, all calls from `varname` package, python standard 66 | libraries and lambda functions are ignored. 67 | multi_vars: Whether allow multiple variables on left-hand side (LHS). 68 | If `True`, this function returns a tuple of the variable names, 69 | even there is only one variable on LHS. 70 | If `False`, and multiple variables on LHS, a 71 | `ImproperUseError` will be raised. 72 | raise_exc: Whether we should raise an exception if failed 73 | to retrieve the ast node. 74 | Note that set this to `False` will NOT supress the exception when 75 | the use of `varname` is improper (i.e. multiple variables on 76 | LHS with `multi_vars` is `False`). See `Raises/ImproperUseError`. 77 | strict: Whether to only return the variable name(s) if the result of 78 | the call is assigned to it/them directly. For example, `a = func()` 79 | rather than `a = [func()]` 80 | 81 | Returns: 82 | The variable name, or `None` when `raise_exc` is `False` and 83 | we failed to retrieve the ast node for the variable(s). 84 | A tuple or a hierarchy (tuple of tuples) of variable names 85 | when `multi_vars` is `True`. 86 | 87 | Raises: 88 | VarnameRetrievingError: When we are unable to retrieve the ast node 89 | for the variable(s) and `raise_exc` is set to `True`. 90 | 91 | ImproperUseError: When the use of `varname()` is improper, including: 92 | - When LHS is not an `ast.Name` or `ast.Attribute` node or not a 93 | list/tuple of them 94 | - When there are multiple variables on LHS but `multi_vars` is False 95 | - When `strict` is True, but the result is not assigned to 96 | variable(s) directly 97 | 98 | Note that `raise_exc=False` will NOT suppress this exception. 99 | 100 | MultiTargetAssignmentWarning: When there are multiple target 101 | in the assign node. (e.g: `a = b = func()`, in such a case, 102 | `a == 'b'`, may not be the case you want) 103 | """ 104 | # Skip one more frame, as it is supposed to be called 105 | # inside another function 106 | refnode = get_node(frame + 1, ignore, raise_exc=raise_exc) 107 | if not refnode: 108 | if raise_exc: 109 | raise VarnameRetrievingError("Unable to retrieve the ast node.") 110 | return None 111 | 112 | node = lookfor_parent_assign(refnode, strict=strict) 113 | if not node: # improper use 114 | if strict: 115 | msg = "Caller doesn't assign the result directly to variable(s)." 116 | else: 117 | msg = "Expression is not part of an assignment." 118 | 119 | raise ImproperUseError(rich_exc_message(msg, refnode)) 120 | 121 | if isinstance(node, ast.Assign): 122 | # Need to actually check that there's just one 123 | # give warnings if: a = b = func() 124 | if len(node.targets) > 1: 125 | warnings.warn( 126 | "Multiple targets in assignment, variable name " 127 | "on the very right is used. ", 128 | MultiTargetAssignmentWarning, 129 | ) 130 | target = node.targets[-1] 131 | else: 132 | target = node.target 133 | 134 | names = node_name(target) 135 | 136 | if not isinstance(names, tuple): 137 | names = (names,) 138 | 139 | if multi_vars: 140 | return names 141 | 142 | if len(names) > 1: 143 | raise ImproperUseError( 144 | rich_exc_message( 145 | "Expect a single variable on left-hand side, " 146 | f"got {len(names)}.", 147 | refnode, 148 | ) 149 | ) 150 | 151 | return names[0] 152 | 153 | 154 | def will(frame: int = 1, raise_exc: bool = True) -> str: 155 | """Detect the attribute name right immediately after a function call. 156 | 157 | Examples: 158 | >>> class AwesomeClass: 159 | >>> def __init__(self): 160 | >>> self.will = None 161 | 162 | >>> def permit(self): 163 | >>> self.will = will() 164 | >>> if self.will == 'do': 165 | >>> # let self handle do 166 | >>> return self 167 | >>> raise AttributeError( 168 | >>> 'Should do something with AwesomeClass object' 169 | >>> ) 170 | 171 | >>> def do(self): 172 | >>> if self.will != 'do': 173 | >>> raise AttributeError("You don't have permission to do") 174 | >>> return 'I am doing!' 175 | 176 | >>> awesome = AwesomeClass() 177 | >>> # AttributeError: You don't have permission to do 178 | >>> awesome.do() 179 | >>> # AttributeError: Should do something with AwesomeClass object 180 | >>> awesome.permit() 181 | >>> awesome.permit().do() == 'I am doing!' 182 | 183 | Args: 184 | frame: At which frame this function is called. 185 | raise_exc: Raise exception we failed to detect the ast node 186 | This will NOT supress the `ImproperUseError` 187 | 188 | Returns: 189 | The attribute name right after the function call. 190 | `None` if ast node cannot be retrieved and `raise_exc` is `False` 191 | 192 | Raises: 193 | VarnameRetrievingError: When `raise_exc` is `True` and we failed to 194 | detect the attribute name (including not having one) 195 | 196 | ImproperUseError: When (the wraper of) this function is not called 197 | inside a method/property of a class instance. 198 | Note that this exception will not be suppressed by `raise_exc=False` 199 | """ 200 | node = get_node(frame + 1, raise_exc=raise_exc) 201 | if not node: 202 | if raise_exc: 203 | raise VarnameRetrievingError("Unable to retrieve the frame.") 204 | return None 205 | 206 | # try to get node inst.attr from inst.attr() 207 | node = node.parent 208 | 209 | # see test_will_fail 210 | if not isinstance(node, ast.Attribute): 211 | if raise_exc: 212 | raise ImproperUseError( 213 | "Function `will` has to be called within " 214 | "a method/property of a class." 215 | ) 216 | return None 217 | # ast.Attribute 218 | return node.attr 219 | 220 | 221 | @overload 222 | def nameof( 223 | var: Any, 224 | *, 225 | frame: int = 1, 226 | vars_only: bool = True, 227 | ) -> str: # pragma: no cover 228 | ... 229 | 230 | 231 | @overload 232 | def nameof( 233 | var: Any, 234 | more_var: Any, 235 | /, # introduced in python 3.8 236 | *more_vars: Any, 237 | frame: int = 1, 238 | vars_only: bool = True, 239 | ) -> Tuple[str, ...]: # pragma: no cover 240 | ... 241 | 242 | 243 | def nameof( 244 | var: Any, 245 | *more_vars: Any, 246 | frame: int = 1, 247 | vars_only: bool = True, 248 | ) -> Union[str, Tuple[str, ...]]: 249 | """Get the names of the variables passed in 250 | 251 | Examples: 252 | >>> a = 1 253 | >>> nameof(a) # 'a' 254 | 255 | >>> b = 2 256 | >>> nameof(a, b) # ('a', 'b') 257 | 258 | >>> x = lambda: None 259 | >>> x.y = 1 260 | >>> nameof(x.y, vars_only=False) # 'x.y' 261 | 262 | Note: 263 | This function works with the environments where source code is 264 | available, in other words, the callee's node can be retrieved by 265 | `executing`. In some cases, for example, running code from python 266 | shell/REPL or from `exec`/`eval`, we try to fetch the variable name 267 | from the bytecode. This requires only a single variable name is passed 268 | to this function and no keyword arguments, meaning that getting full 269 | names of attribute calls are not supported in such cases. 270 | 271 | Args: 272 | var: The variable to retrieve the name of 273 | *more_vars: Other variables to retrieve the names of 274 | frame: The this function is called from the wrapper of it. `frame=1` 275 | means no wrappers. 276 | Note that the calls from standard libraries are ignored. 277 | Also note that the wrapper has to have signature as this one. 278 | vars_only: Whether only allow variables/attributes as arguments or 279 | any expressions. If `False`, then the sources of the arguments 280 | will be returned. 281 | 282 | Returns: 283 | The names/sources of variables/expressions passed in. 284 | If a single argument is passed, return the name/source of it. 285 | If multiple variables are passed, return a tuple of their 286 | names/sources. 287 | If the argument is an attribute (e.g. `a.b`) and `vars_only` is 288 | `True`, only `"b"` will returned. Set `vars_only` to `False` to 289 | get `"a.b"`. 290 | 291 | Raises: 292 | VarnameRetrievingError: When the callee's node cannot be retrieved or 293 | trying to retrieve the full name of non attribute series calls. 294 | """ 295 | # Frame is anyway used in get_node 296 | frameobj = IgnoreList.create( 297 | ignore_lambda=False, 298 | ignore_varname=False, 299 | ).get_frame(frame) 300 | 301 | node = get_node_by_frame(frameobj, raise_exc=True) 302 | if not node: 303 | # We can't retrieve the node by executing. 304 | # It can be due to running code from python/shell, exec/eval or 305 | # other environments where sourcecode cannot be reached 306 | # make sure we keep it simple (only single variable passed and no 307 | # full passed) to use bytecode_nameof 308 | # 309 | # We don't have to check keyword arguments here, as the instruction 310 | # will then be CALL_FUNCTION_KW. 311 | if not more_vars: 312 | return bytecode_nameof(frameobj.f_code, frameobj.f_lasti) 313 | 314 | # We are anyway raising exceptions, no worries about additional burden 315 | # of frame retrieval again 316 | source = frameobj.f_code.co_filename 317 | if source == "": 318 | raise VarnameRetrievingError( 319 | "Are you trying to call nameof in REPL/python shell? " 320 | "In such a case, nameof can only be called with single " 321 | "argument and no keyword arguments." 322 | ) 323 | if source == "": 324 | raise VarnameRetrievingError( 325 | "Are you trying to call nameof from exec/eval? " 326 | "In such a case, nameof can only be called with single " 327 | "argument and no keyword arguments." 328 | ) 329 | raise VarnameRetrievingError( 330 | "Source code unavailable, nameof can only retrieve the name of " 331 | "a single variable, and argument `full` should not be specified." 332 | ) 333 | 334 | out = argname( 335 | "var", 336 | "*more_vars", 337 | func=nameof, 338 | frame=frame, 339 | vars_only=vars_only, 340 | ) 341 | return out if more_vars else out[0] # type: ignore 342 | 343 | 344 | @overload 345 | def argname( 346 | arg: str, 347 | *, 348 | func: Callable = None, 349 | dispatch: Type = None, 350 | frame: int = 1, 351 | ignore: IgnoreType = None, 352 | vars_only: bool = True, 353 | ) -> ArgSourceType: # pragma: no cover 354 | ... 355 | 356 | 357 | @overload 358 | def argname( 359 | arg: str, 360 | more_arg: str, 361 | /, # introduced in python 3.8 362 | *more_args: str, 363 | func: Callable = None, 364 | dispatch: Type = None, 365 | frame: int = 1, 366 | ignore: IgnoreType = None, 367 | vars_only: bool = True, 368 | ) -> Tuple[ArgSourceType, ...]: # pragma: no cover 369 | ... 370 | 371 | 372 | def argname( 373 | arg: str, 374 | *more_args: str, 375 | func: Callable = None, 376 | dispatch: Type = None, 377 | frame: int = 1, 378 | ignore: IgnoreType = None, 379 | vars_only: bool = True, 380 | ) -> Union[ArgSourceType, Tuple[ArgSourceType, ...]]: 381 | """Get the names/sources of arguments passed to a function. 382 | 383 | Instead of passing the argument variables themselves to this function 384 | (like `argname()` does), you should pass their names instead. 385 | 386 | Args: 387 | arg: and 388 | *more_args: The names of the arguments that you want to retrieve 389 | names/sources of. 390 | You can also use subscripts to get parts of the results. 391 | >>> def func(*args, **kwargs): 392 | >>> return argname('args[0]', 'kwargs[x]') # no quote needed 393 | 394 | Star argument is also allowed: 395 | >>> def func(*args, x = 1): 396 | >>> return argname('*args', 'x') 397 | >>> a = b = c = 1 398 | >>> func(a, b, x=c) # ('a', 'b', 'c') 399 | 400 | Note the difference: 401 | >>> def func(*args, x = 1): 402 | >>> return argname('args', 'x') 403 | >>> a = b = c = 1 404 | >>> func(a, b, x=c) # (('a', 'b'), 'c') 405 | 406 | func: The target function. If not provided, the AST node of the 407 | function call will be used to fetch the function: 408 | - If a variable (ast.Name) used as function, the `node.id` will 409 | be used to get the function from `locals()` or `globals()`. 410 | - If variable (ast.Name), attributes (ast.Attribute), 411 | subscripts (ast.Subscript), and combinations of those and 412 | literals used as function, `pure_eval` will be used to evaluate 413 | the node 414 | - If `pure_eval` is not installed or failed to evaluate, `eval` 415 | will be used. A warning will be shown since unwanted side 416 | effects may happen in this case. 417 | You are very encouraged to always pass the function explicitly. 418 | dispatch: If a function is a single-dispatched function, you can 419 | specify a type for it to dispatch the real function. If this is 420 | specified, expect `func` to be the generic function if provided. 421 | frame: The frame where target function is called from this call. 422 | Calls from python standard libraries are ignored. 423 | ignore: The intermediate calls to be ignored. See `varname.ignore` 424 | vars_only: Require the arguments to be variables only. 425 | If False, `asttokens` is required to retrieve the source. 426 | 427 | Returns: 428 | The argument source when no more_args passed, otherwise a tuple of 429 | argument sources 430 | Note that when an argument is an `ast.Constant`, `repr(arg.value)` 431 | is returned, so `argname()` return `'a'` for `func("a")` 432 | 433 | Raises: 434 | VarnameRetrievingError: When the ast node where the function is called 435 | cannot be retrieved 436 | ImproperUseError: When frame or func is incorrectly specified. 437 | """ 438 | ignore_list = IgnoreList.create( 439 | ignore, 440 | ignore_lambda=False, 441 | ignore_varname=False, 442 | ) 443 | # where func(...) is called, skip the argname() call 444 | func_frame = ignore_list.get_frame(frame + 1) 445 | func_node = get_node_by_frame(func_frame) 446 | # Only do it when func_node are available 447 | if not func_node: 448 | # We can do something at bytecode level, when a single positional 449 | # argument passed to both functions (argname and the target function) 450 | # However, it's hard to ensure that there is only a single positional 451 | # arguments passed to the target function, at bytecode level. 452 | raise VarnameRetrievingError( 453 | "Cannot retrieve the node where the function is called." 454 | ) 455 | 456 | func_node = reconstruct_func_node(func_node) 457 | 458 | if not func: 459 | func = get_function_called_argname(func_frame, func_node) 460 | 461 | if dispatch: 462 | func = func.dispatch(dispatch) 463 | 464 | # don't pass the target arguments so that we can cache the sources in 465 | # the same call. For example: 466 | # >>> def func(a, b): 467 | # >>> a_name = argname(a) 468 | # >>> b_name = argname(b) 469 | try: 470 | argument_sources = get_argument_sources( 471 | Source.for_frame(func_frame), 472 | func_node, 473 | func, 474 | vars_only=vars_only, 475 | ) 476 | except Exception as err: 477 | raise ImproperUseError( 478 | "Have you specified the right `frame` or `func`?" 479 | ) from err 480 | 481 | out: List[ArgSourceType] = [] 482 | farg_star = False 483 | for farg in (arg, *more_args): 484 | 485 | farg_name = farg 486 | farg_subscript = None # type: str | int 487 | match = re.match(r"^([\w_]+)\[(.+)\]$", farg) 488 | if match: 489 | farg_name = match.group(1) 490 | farg_subscript = match.group(2) 491 | if farg_subscript.isdigit(): 492 | farg_subscript = int(farg_subscript) 493 | else: 494 | match = re.match(r"^\*([\w_]+)$", farg) 495 | if match: 496 | farg_name = match.group(1) 497 | farg_star = True 498 | 499 | if farg_name not in argument_sources: 500 | raise ImproperUseError( 501 | f"{farg_name!r} is not a valid argument " 502 | f"of {func.__qualname__!r}." 503 | ) 504 | 505 | source = argument_sources[farg_name] 506 | if isinstance(source, ast.AST): 507 | raise ImproperUseError( 508 | f"Argument {ast.dump(source)} is not a variable " 509 | "or an attribute." 510 | ) 511 | 512 | if isinstance(farg_subscript, int) and not isinstance(source, tuple): 513 | raise ImproperUseError( 514 | f"`{farg_name}` is not a positional argument." 515 | ) 516 | 517 | if isinstance(farg_subscript, str) and not isinstance(source, dict): 518 | raise ImproperUseError( 519 | f"`{farg_name}` is not a keyword argument." 520 | ) 521 | 522 | if farg_subscript is not None: 523 | out.append(source[farg_subscript]) # type: ignore 524 | elif farg_star: 525 | out.extend(source) 526 | else: 527 | out.append(source) 528 | 529 | return ( 530 | out[0] 531 | if not more_args and not farg_star 532 | else tuple(out) # type: ignore 533 | ) 534 | -------------------------------------------------------------------------------- /varname/utils.py: -------------------------------------------------------------------------------- 1 | """Some internal utilities for varname 2 | 3 | 4 | Attributes: 5 | 6 | IgnoreElemType: The type for ignore elements 7 | IgnoreType: The type for the ignore argument 8 | MODULE_IGNORE_ID_NAME: The name of the ignore id injected to the module. 9 | Espectially for modules that can't be retrieved by 10 | `inspect.getmodule(frame)` 11 | """ 12 | import sys 13 | import dis 14 | import ast 15 | import warnings 16 | import inspect 17 | from os import path 18 | from pathlib import Path 19 | from functools import lru_cache, singledispatch 20 | from types import ModuleType, FunctionType, CodeType, FrameType 21 | from typing import Tuple, Union, List, Mapping, Callable, Dict 22 | 23 | if sys.version_info < (3, 10): 24 | from typing_extensions import TypeAlias # pragma: no cover 25 | else: 26 | from typing import TypeAlias 27 | 28 | from executing import Source 29 | 30 | OP2MAGIC = { 31 | ast.Add: "__add__", 32 | ast.Sub: "__sub__", 33 | ast.Mult: "__mul__", 34 | ast.Div: "__truediv__", 35 | ast.FloorDiv: "__floordiv__", 36 | ast.Mod: "__mod__", 37 | ast.Pow: "__pow__", 38 | ast.LShift: "__lshift__", 39 | ast.RShift: "__rshift__", 40 | ast.BitOr: "__or__", 41 | ast.BitXor: "__xor__", 42 | ast.BitAnd: "__and__", 43 | ast.MatMult: "__matmul__", 44 | } 45 | 46 | OP2SYMBOL = { 47 | # BinOp 48 | ast.Add: "+", 49 | ast.Sub: "-", 50 | ast.Mult: "*", 51 | ast.Div: "/", 52 | ast.FloorDiv: "//", 53 | ast.Mod: "%", 54 | ast.Pow: "**", 55 | ast.LShift: "<<", 56 | ast.RShift: ">>", 57 | ast.BitOr: "|", 58 | ast.BitXor: "^", 59 | ast.BitAnd: "&", 60 | ast.MatMult: "@", 61 | # BoolOp 62 | ast.And: "and", 63 | ast.Or: "or", 64 | # UnaryOp 65 | ast.UAdd: "+", 66 | ast.USub: "-", 67 | ast.Invert: "~", 68 | ast.Not: "not ", 69 | # Compare 70 | ast.Eq: "==", 71 | ast.NotEq: "!=", 72 | ast.Lt: "<", 73 | ast.LtE: "<=", 74 | ast.Gt: ">", 75 | ast.GtE: ">=", 76 | ast.Is: "is", 77 | ast.IsNot: "is not", 78 | ast.In: "in", 79 | ast.NotIn: "not in", 80 | } 81 | 82 | CMP2MAGIC = { 83 | ast.Eq: "__eq__", 84 | ast.NotEq: "__ne__", 85 | ast.Lt: "__lt__", 86 | ast.LtE: "__le__", 87 | ast.Gt: "__gt__", 88 | ast.GtE: "__ge__", 89 | } 90 | 91 | IgnoreElemType = Union[ 92 | # module 93 | ModuleType, 94 | # filename of a module 95 | str, 96 | Path, 97 | FunctionType, 98 | # the module (filename) and qualname 99 | # If module is None, then all qualname matches the 2nd element 100 | # will be ignored. Used to ignore internally 101 | Tuple[Union[ModuleType, str], str], 102 | # Function and number of its decorators 103 | Tuple[FunctionType, int], 104 | ] 105 | IgnoreType = Union[IgnoreElemType, List[IgnoreElemType]] 106 | 107 | ArgSourceType: TypeAlias = Union[ast.AST, str] 108 | ArgSourceType: TypeAlias = Union[ArgSourceType, Tuple[ArgSourceType, ...]] 109 | ArgSourceType: TypeAlias = Union[ArgSourceType, Mapping[str, ArgSourceType]] 110 | 111 | if sys.version_info >= (3, 8): 112 | ASSIGN_TYPES = (ast.Assign, ast.AnnAssign, ast.NamedExpr) 113 | AssignType: TypeAlias = Union[ASSIGN_TYPES] # type: ignore 114 | else: # pragma: no cover # Python < 3.8 115 | ASSIGN_TYPES = (ast.Assign, ast.AnnAssign) 116 | AssignType: TypeAlias = Union[ASSIGN_TYPES] # type: ignore 117 | 118 | PY311 = sys.version_info >= (3, 11) 119 | MODULE_IGNORE_ID_NAME = "__varname_ignore_id__" 120 | 121 | 122 | class config: 123 | """Global configurations for varname 124 | 125 | Attributes: 126 | debug: Show debug information for frames being ignored 127 | """ 128 | 129 | debug = False 130 | 131 | 132 | class VarnameException(Exception): 133 | """Root exception for all varname exceptions""" 134 | 135 | 136 | class VarnameRetrievingError(VarnameException): 137 | """When failed to retrieve the varname""" 138 | 139 | 140 | class QualnameNonUniqueError(VarnameException): 141 | """When a qualified name is used as an ignore element but references to 142 | multiple objects in a module""" 143 | 144 | 145 | class ImproperUseError(VarnameException): 146 | """When varname() is improperly used""" 147 | 148 | 149 | class VarnameWarning(Warning): 150 | """Root warning for all varname warnings""" 151 | 152 | 153 | class MaybeDecoratedFunctionWarning(VarnameWarning): 154 | """When a suspecious decorated function used as ignore function directly""" 155 | 156 | 157 | class MultiTargetAssignmentWarning(VarnameWarning): 158 | """When varname tries to retrieve variable name in 159 | a multi-target assignment""" 160 | 161 | 162 | class UsingExecWarning(VarnameWarning): 163 | """When exec is used to retrieve function name for `argname()`""" 164 | 165 | 166 | @lru_cache() 167 | def cached_getmodule(codeobj: CodeType): 168 | """Cached version of inspect.getmodule""" 169 | return inspect.getmodule(codeobj) 170 | 171 | 172 | def get_node( 173 | frame: int, 174 | ignore: IgnoreType = None, 175 | raise_exc: bool = True, 176 | ignore_lambda: bool = True, 177 | ) -> ast.AST: 178 | """Try to get node from the executing object. 179 | 180 | This can fail when a frame is failed to retrieve. 181 | One case should be when python code is executed in 182 | R pacakge `reticulate`, where only first frame is kept. 183 | 184 | When the node can not be retrieved, try to return the first statement. 185 | """ 186 | from .ignore import IgnoreList 187 | 188 | ignore = IgnoreList.create(ignore, ignore_lambda=ignore_lambda) 189 | try: 190 | frameobj = ignore.get_frame(frame) 191 | except VarnameRetrievingError: 192 | return None 193 | 194 | return get_node_by_frame(frameobj, raise_exc) 195 | 196 | 197 | def get_node_by_frame(frame: FrameType, raise_exc: bool = True) -> ast.AST: 198 | """Get the node by frame, raise errors if possible""" 199 | exect = Source.executing(frame) 200 | 201 | if exect.node: 202 | # attach the frame for better exception message 203 | # (ie. where ImproperUseError happens) 204 | exect.node.__frame__ = frame 205 | return exect.node 206 | 207 | if exect.source.text and exect.source.tree and raise_exc: # pragma: no cover 208 | raise VarnameRetrievingError( 209 | "Couldn't retrieve the call node. " 210 | "This may happen if you're using some other AST magic at the " 211 | "same time, such as pytest, ipython, macropy, or birdseye." 212 | ) 213 | 214 | return None 215 | 216 | 217 | def lookfor_parent_assign(node: ast.AST, strict: bool = True) -> AssignType: 218 | """Look for an ast.Assign node in the parents""" 219 | while hasattr(node, "parent"): 220 | node = node.parent 221 | 222 | if isinstance(node, ASSIGN_TYPES): 223 | return node 224 | 225 | if strict: 226 | break 227 | return None 228 | 229 | 230 | def node_name( 231 | node: ast.AST, 232 | subscript_slice: bool = False, 233 | ) -> Union[str, Tuple[Union[str, Tuple], ...]]: 234 | """Get the node node name. 235 | 236 | Raises ImproperUseError when failed 237 | """ 238 | if isinstance(node, ast.Name): 239 | return node.id 240 | if isinstance(node, ast.Attribute): 241 | return f"{node_name(node.value)}.{node.attr}" 242 | if isinstance(node, ast.Constant): 243 | return repr(node.value) 244 | if isinstance(node, (ast.List, ast.Tuple)) and not subscript_slice: 245 | return tuple(node_name(elem) for elem in node.elts) 246 | if isinstance(node, ast.List): 247 | return f"[{', '.join(node_name(elem) for elem in node.elts)}]" # type: ignore 248 | if isinstance(node, ast.Tuple): 249 | if len(node.elts) == 1: 250 | return f"({node_name(node.elts[0])},)" 251 | return f"({', '.join(node_name(elem) for elem in node.elts)})" # type: ignore 252 | if isinstance(node, ast.Starred): 253 | return f"*{node_name(node.value)}" 254 | if isinstance(node, ast.Slice): 255 | return ( 256 | f"{node_name(node.lower)}:{node_name(node.upper)}:{node_name(node.step)}" 257 | if node.lower is not None 258 | and node.upper is not None 259 | and node.step is not None 260 | else f"{node_name(node.lower)}:{node_name(node.upper)}" 261 | if node.lower is not None and node.upper is not None 262 | else f"{node_name(node.lower)}:" 263 | if node.lower is not None 264 | else f":{node_name(node.upper)}" 265 | if node.upper is not None 266 | else ":" 267 | ) 268 | if isinstance(node, ast.BinOp): 269 | return ( 270 | f"{node_name(node.left)} {OP2SYMBOL[type(node.op)]} {node_name(node.right)}" 271 | ) 272 | if isinstance(node, ast.BoolOp): 273 | return f" {OP2SYMBOL[type(node.op)]} ".join( 274 | str(node_name(value)) for value in node.values 275 | ) 276 | if isinstance(node, ast.Compare): 277 | parts = [str(node_name(node.left))] 278 | for op, comparator in zip(node.ops, node.comparators): 279 | parts.append(OP2SYMBOL[type(op)]) 280 | parts.append(str(node_name(comparator))) 281 | return " ".join(parts) 282 | if isinstance(node, ast.UnaryOp): 283 | return f"{OP2SYMBOL[type(node.op)]}{node_name(node.operand)}" 284 | 285 | name = type(node).__name__ 286 | if isinstance(node, ast.Subscript): 287 | try: 288 | return f"{node_name(node.value)}[{node_name(node.slice, True)}]" 289 | except ImproperUseError: 290 | name = f"{node_name(node.value)}[{type(node.slice).__name__}]" 291 | 292 | raise ImproperUseError( 293 | f"Node {name!r} detected, but only following nodes are supported: \n" 294 | " - ast.Name (e.g. x)\n" 295 | " - ast.Attribute (e.g. x.y, x be other supported nodes)\n" 296 | " - ast.Constant (e.g. 1, 'a')\n" 297 | " - ast.List (e.g. [x, y, z])\n" 298 | " - ast.Tuple (e.g. (x, y, z))\n" 299 | " - ast.Starred (e.g. *x)\n" 300 | " - ast.Subscript with slice of the above nodes (e.g. x[y])\n" 301 | " - ast.Subscript with ast.BinOp/ast.BoolOp/ast.Compare/ast.UnaryOp " 302 | "(e.g. x[y + 1], x[y and z])" 303 | ) 304 | 305 | 306 | @lru_cache() 307 | def bytecode_nameof(code: CodeType, offset: int) -> str: 308 | """Cached Bytecode version of nameof 309 | 310 | We are trying this version only when the sourcecode is unavisible. In most 311 | cases, this will happen when user is trying to run a script in REPL/ 312 | python shell, with `eval`, or other circumstances where the code is 313 | manipulated to run but sourcecode is not available. 314 | """ 315 | kwargs: Dict[str, bool] = ( 316 | {"show_caches": True} if sys.version_info[:2] >= (3, 11) else {} 317 | ) 318 | 319 | instructions = list(dis.get_instructions(code, **kwargs)) 320 | ((current_instruction_index, current_instruction),) = ( 321 | (index, instruction) 322 | for index, instruction in enumerate(instructions) 323 | if instruction.offset == offset 324 | ) 325 | 326 | while current_instruction.opname == "CACHE": # pragma: no cover 327 | current_instruction_index -= 1 328 | current_instruction = instructions[current_instruction_index] 329 | 330 | pos_only_error = VarnameRetrievingError( 331 | "'nameof' can only be called with a single positional argument " 332 | "when source code is not avaiable." 333 | ) 334 | if current_instruction.opname in ( # pragma: no cover 335 | "CALL_FUNCTION_EX", 336 | "CALL_FUNCTION_KW", 337 | ): 338 | raise pos_only_error 339 | 340 | if current_instruction.opname not in ( 341 | "CALL_FUNCTION", 342 | "CALL_METHOD", 343 | "CALL", 344 | "CALL_KW", 345 | ): 346 | raise VarnameRetrievingError("Did you call 'nameof' in a weird way?") 347 | 348 | current_instruction_index -= 1 349 | name_instruction = instructions[current_instruction_index] 350 | while name_instruction.opname in ("CACHE", "PRECALL"): # pragma: no cover 351 | current_instruction_index -= 1 352 | name_instruction = instructions[current_instruction_index] 353 | 354 | if name_instruction.opname in ("KW_NAMES", "LOAD_CONST"): # LOAD_CONST python 3.13 355 | raise pos_only_error 356 | 357 | if not name_instruction.opname.startswith("LOAD_"): 358 | raise VarnameRetrievingError("Argument must be a variable or attribute") 359 | 360 | name = name_instruction.argrepr 361 | if not name.isidentifier(): 362 | raise VarnameRetrievingError( 363 | f"Found the variable name {name!r} which is obviously wrong. " 364 | "This may happen if you're using some other AST magic at the " 365 | "same time, such as pytest, ipython, macropy, or birdseye." 366 | ) 367 | 368 | return name 369 | 370 | 371 | def attach_ignore_id_to_module(module: ModuleType) -> None: 372 | """Attach the ignore id to module 373 | 374 | This is useful when a module cannot be retrieved by frames using 375 | `inspect.getmodule`, then we can use this id, which will exist in 376 | `frame.f_globals` to check if the module matches in ignore. 377 | 378 | Do it only when the __file__ is not avaiable or does not exist for 379 | the module. Since this probably means the source is not avaiable and 380 | `inspect.getmodule` would not work 381 | """ 382 | module_file = getattr(module, "__file__", None) 383 | if module_file is not None and path.isfile(module_file): 384 | return 385 | # or it's already been set 386 | if hasattr(module, MODULE_IGNORE_ID_NAME): 387 | return 388 | 389 | setattr(module, MODULE_IGNORE_ID_NAME, f" bool: 395 | """Check if the frame is from the module by ignore id""" 396 | ignore_id_attached = getattr(module, MODULE_IGNORE_ID_NAME, object()) 397 | ignore_id_from_frame = frame.f_globals.get(MODULE_IGNORE_ID_NAME, object()) 398 | return ignore_id_attached == ignore_id_from_frame 399 | 400 | 401 | @lru_cache() 402 | def check_qualname_by_source( 403 | source: Source, modname: str, qualname: str 404 | ) -> None: 405 | """Check if a qualname in module is unique""" 406 | if not source.tree: 407 | # no way to check it, skip 408 | return 409 | nobj = list(source._qualnames.values()).count(qualname) 410 | if nobj > 1: 411 | raise QualnameNonUniqueError( 412 | f"Qualname {qualname!r} in " 413 | f"{modname!r} refers to multiple objects." 414 | ) 415 | 416 | 417 | def debug_ignore_frame(msg: str, frameinfo: inspect.FrameInfo = None) -> None: 418 | """Print the debug message for a given frame info object 419 | 420 | Args: 421 | msg: The debugging message 422 | frameinfo: The FrameInfo object for the frame 423 | """ 424 | if not config.debug: 425 | return 426 | if frameinfo is not None: 427 | msg = ( 428 | f"{msg} [In {frameinfo.function!r} at " 429 | f"{frameinfo.filename}:{frameinfo.lineno}]" 430 | ) 431 | sys.stderr.write(f"[{__package__}] DEBUG: {msg}\n") 432 | 433 | 434 | def argnode_source( 435 | source: Source, node: ast.AST, vars_only: bool 436 | ) -> Union[str, ast.AST]: 437 | """Get the source of an argument node 438 | 439 | Args: 440 | source: The executing source object 441 | node: The node to get the source from 442 | vars_only: Whether only allow variables and attributes 443 | 444 | Returns: 445 | The source of the node (node.id for ast.Name, 446 | node.attr for ast.Attribute). Or the node itself if the source 447 | cannot be fetched. 448 | """ 449 | if isinstance(node, ast.Constant): 450 | return repr(node.value) 451 | 452 | if sys.version_info < (3, 9): # pragma: no cover 453 | if isinstance(node, ast.Index): 454 | node = node.value 455 | if isinstance(node, ast.Num): 456 | return repr(node.n) 457 | if isinstance(node, (ast.Bytes, ast.Str)): 458 | return repr(node.s) 459 | if isinstance(node, ast.NameConstant): 460 | return repr(node.value) 461 | 462 | if vars_only: 463 | return ( 464 | node.id 465 | if isinstance(node, ast.Name) 466 | else node.attr 467 | if isinstance(node, ast.Attribute) 468 | else node 469 | ) 470 | 471 | # requires asttokens 472 | return source.asttokens().get_text(node) 473 | 474 | 475 | def get_argument_sources( 476 | source: Source, 477 | node: ast.Call, 478 | func: Callable, 479 | vars_only: bool, 480 | ) -> Mapping[str, ArgSourceType]: 481 | """Get the sources for argument from an ast.Call node 482 | 483 | >>> def func(a, b, c, d=4): 484 | >>> ... 485 | >>> x = y = z = 1 486 | >>> func(y, x, c=z) 487 | >>> # argument_sources = {'a': 'y', 'b', 'x', 'c': 'z'} 488 | >>> func(y, x, c=1) 489 | >>> # argument_sources = {'a': 'y', 'b', 'x', 'c': ast.Num(n=1)} 490 | """ 491 | # 492 | signature = inspect.signature(func, follow_wrapped=False) 493 | # func(y, x, c=z) 494 | # ['y', 'x'], {'c': 'z'} 495 | arg_sources = [ 496 | argnode_source(source, argnode, vars_only) for argnode in node.args 497 | ] 498 | kwarg_sources = { 499 | argnode.arg: argnode_source(source, argnode.value, vars_only) 500 | for argnode in node.keywords 501 | if argnode.arg is not None 502 | } 503 | bound_args = signature.bind_partial(*arg_sources, **kwarg_sources) 504 | argument_sources = bound_args.arguments 505 | # see if *args and **kwargs have anything assigned 506 | # if not, assign () and {} to them 507 | for parameter in signature.parameters.values(): 508 | if parameter.kind == inspect.Parameter.VAR_POSITIONAL: 509 | argument_sources.setdefault(parameter.name, ()) 510 | if parameter.kind == inspect.Parameter.VAR_KEYWORD: 511 | argument_sources.setdefault(parameter.name, {}) 512 | return argument_sources 513 | 514 | 515 | def get_function_called_argname(frame: FrameType, node: ast.Call) -> Callable: 516 | """Get the function who called argname""" 517 | # variable 518 | if isinstance(node.func, ast.Name): 519 | func = frame.f_locals.get( 520 | node.func.id, frame.f_globals.get(node.func.id) 521 | ) 522 | if func is None: # pragma: no cover 523 | # not sure how it would happen but in case 524 | raise VarnameRetrievingError( 525 | f"Cannot retrieve the function by {node.func.id!r}." 526 | ) 527 | return func 528 | 529 | # use pure_eval 530 | pure_eval_fail_msg = None 531 | try: 532 | from pure_eval import Evaluator, CannotEval 533 | except ImportError: 534 | pure_eval_fail_msg = "'pure_eval' is not installed." 535 | else: 536 | try: 537 | evaluator = Evaluator.from_frame(frame) 538 | return evaluator[node.func] 539 | except CannotEval: 540 | pure_eval_fail_msg = ( 541 | f"Cannot evaluate node {ast.dump(node.func)} " 542 | "using 'pure_eval'." 543 | ) 544 | 545 | # try eval 546 | warnings.warn( 547 | f"{pure_eval_fail_msg} " 548 | "Using 'eval' to get the function that calls 'argname'. " 549 | "Try calling it using a variable reference to the function, or " 550 | "passing the function to 'argname' explicitly.", 551 | UsingExecWarning, 552 | ) 553 | expr = ast.Expression(node.func) 554 | code = compile(expr, "", "eval") 555 | return eval(code, frame.f_globals, frame.f_locals) 556 | 557 | 558 | @singledispatch 559 | def reconstruct_func_node(node: ast.AST) -> ast.Call: 560 | """Reconstruct the ast.Call node from 561 | 562 | `x.a` to `x.__getattr__('a')` 563 | `x.a = b` to `x.__setattr__('a', b)` 564 | `x[a]` to `x.__getitem__(a)` 565 | `x[a] = b` to `x.__setitem__(a, 1)` 566 | `x + a` to `x.__add__(a)` 567 | `x < a` to `x.__lt__(a)` 568 | """ 569 | raise VarnameRetrievingError( 570 | f"Cannot reconstruct ast.Call node from {type(node).__name__}, " 571 | "expecting Call, Attribute, Subscript, BinOp, Compare." 572 | ) 573 | 574 | 575 | @reconstruct_func_node.register(ast.Call) 576 | def _(node: ast.Call) -> ast.Call: 577 | return node 578 | 579 | 580 | @reconstruct_func_node.register(ast.Attribute) 581 | @reconstruct_func_node.register(ast.Subscript) 582 | def _(node: Union[ast.Attribute, ast.Subscript]) -> ast.Call: 583 | """Reconstruct the function node for 584 | `x.__getitem__/__setitem__/__getattr__/__setattr__`""" 585 | nodemeta = { 586 | "lineno": node.lineno, 587 | "col_offset": node.col_offset, 588 | } 589 | keynode = ( 590 | node.slice 591 | if isinstance(node, ast.Subscript) 592 | else ast.Constant(value=node.attr) 593 | ) 594 | 595 | # x[1], x.a 596 | if isinstance(node.ctx, ast.Load): 597 | if PY311: 598 | return ast.Call( 599 | func=ast.Attribute( 600 | value=node.value, 601 | attr=( 602 | "__getitem__" 603 | if isinstance(node, ast.Subscript) 604 | else "__getattr__" 605 | ), 606 | ctx=ast.Load(), 607 | **nodemeta, 608 | ), 609 | args=[keynode], 610 | keywords=[], 611 | ) 612 | else: # pragma: no cover 613 | return ast.Call( # type: ignore 614 | func=ast.Attribute( 615 | value=node.value, 616 | attr=( 617 | "__getitem__" 618 | if isinstance(node, ast.Subscript) 619 | else "__getattr__" 620 | ), 621 | ctx=ast.Load(), 622 | **nodemeta, 623 | ), 624 | args=[keynode], 625 | keywords=[], 626 | starargs=None, 627 | kwargs=None, 628 | ) 629 | 630 | # x[a] = b, x.a = b 631 | if ( 632 | not hasattr(node, "parent") 633 | or not isinstance(node.parent, ast.Assign) # type: ignore 634 | or len(node.parent.targets) != 1 # type: ignore 635 | ): 636 | raise ImproperUseError( 637 | rich_exc_message( 638 | "Expect `x[a] = b` or `x.a = b` directly, got " 639 | f"{ast.dump(node)}.", 640 | node, 641 | ) 642 | ) 643 | 644 | if PY311: 645 | return ast.Call( 646 | func=ast.Attribute( 647 | value=node.value, 648 | attr=( 649 | "__setitem__" 650 | if isinstance(node, ast.Subscript) 651 | else "__setattr__" 652 | ), 653 | ctx=ast.Load(), 654 | **nodemeta, 655 | ), 656 | args=[keynode, node.parent.value], # type: ignore 657 | keywords=[], 658 | ) 659 | else: # pragma: no cover 660 | return ast.Call( 661 | func=ast.Attribute( 662 | value=node.value, 663 | attr=( 664 | "__setitem__" 665 | if isinstance(node, ast.Subscript) 666 | else "__setattr__" 667 | ), 668 | ctx=ast.Load(), 669 | **nodemeta, 670 | ), 671 | args=[keynode, node.parent.value], # type: ignore 672 | keywords=[], 673 | starargs=None, 674 | kwargs=None, 675 | ) 676 | 677 | 678 | @reconstruct_func_node.register(ast.Compare) 679 | def _(node: ast.Compare) -> ast.Call: 680 | """Reconstruct the function node for `x < a`""" 681 | # When the node is identified by executing, len(ops) is always 1. 682 | # Otherwise, the node cannot be identified. 683 | assert len(node.ops) == 1 684 | 685 | nodemeta = { 686 | "lineno": node.lineno, 687 | "col_offset": node.col_offset, 688 | } 689 | if PY311: 690 | return ast.Call( 691 | func=ast.Attribute( 692 | value=node.left, 693 | attr=CMP2MAGIC[type(node.ops[0])], 694 | ctx=ast.Load(), 695 | **nodemeta, 696 | ), 697 | args=[node.comparators[0]], 698 | keywords=[], 699 | ) 700 | else: # pragma: no cover 701 | return ast.Call( # type: ignore 702 | func=ast.Attribute( 703 | value=node.left, 704 | attr=CMP2MAGIC[type(node.ops[0])], 705 | ctx=ast.Load(), 706 | **nodemeta, 707 | ), 708 | args=[node.comparators[0]], 709 | keywords=[], 710 | starargs=None, 711 | kwargs=None, 712 | ) 713 | 714 | 715 | @reconstruct_func_node.register(ast.BinOp) 716 | def _(node: ast.BinOp) -> ast.Call: 717 | """Reconstruct the function node for `x + a`""" 718 | nodemeta = { 719 | "lineno": node.lineno, 720 | "col_offset": node.col_offset, 721 | } 722 | 723 | if PY311: 724 | return ast.Call( 725 | func=ast.Attribute( 726 | value=node.left, 727 | attr=OP2MAGIC[type(node.op)], 728 | ctx=ast.Load(), 729 | **nodemeta, 730 | ), 731 | args=[node.right], 732 | keywords=[], 733 | ) 734 | else: # pragma: no cover 735 | return ast.Call( # type: ignore 736 | func=ast.Attribute( 737 | value=node.left, 738 | attr=OP2MAGIC[type(node.op)], 739 | ctx=ast.Load(), 740 | **nodemeta, 741 | ), 742 | args=[node.right], 743 | keywords=[], 744 | starargs=None, 745 | kwargs=None, 746 | ) 747 | 748 | 749 | def rich_exc_message(msg: str, node: ast.AST, context_lines: int = 4) -> str: 750 | """Attach the source code from the node to message to 751 | get a rich message for exceptions 752 | 753 | If package 'rich' is not install or 'node.__frame__' doesn't exist, fall 754 | to plain message (with basic information), otherwise show a better message 755 | with full information 756 | """ 757 | frame = node.__frame__ # type: FrameType 758 | lineno = node.lineno - 1 # type: int 759 | col_offset = node.col_offset # type: int 760 | filename = frame.f_code.co_filename # type: str 761 | try: 762 | lines, startlineno = inspect.getsourcelines(frame) 763 | except OSError: # pragma: no cover 764 | # could not get source code 765 | return f"{msg}\n" 766 | startlineno = 0 if startlineno == 0 else startlineno - 1 767 | line_range = (startlineno + 1, startlineno + len(lines) + 1) 768 | 769 | linenos = tuple(map(str, range(*line_range))) # type: Tuple[str, ...] 770 | lineno_width = max(map(len, linenos)) # type: int 771 | hiline = lineno - startlineno # type: int 772 | codes = [] # type: List[str] 773 | for i, lno in enumerate(linenos): 774 | if i < hiline - context_lines or i > hiline + context_lines: 775 | continue 776 | lno = lno.ljust(lineno_width) 777 | if i == hiline: 778 | codes.append(f" > | {lno} {lines[i]}") 779 | codes.append(f" | {' ' * (lineno_width + col_offset + 2)}^\n") 780 | else: 781 | codes.append(f" | {lno} {lines[i]}") 782 | 783 | return ( 784 | f"{msg}\n\n" 785 | f" {filename}:{lineno + 1}:{col_offset + 1}\n" 786 | f"{''.join(codes)}\n" 787 | ) 788 | --------------------------------------------------------------------------------