From be560cc2947413790c9a85302254de6cf239433c Mon Sep 17 00:00:00 2001 From: pwwang <1188067+pwwang@users.noreply.github.com> Date: Tue, 28 Nov 2023 22:41:47 -0700 Subject: [PATCH] 0.12.2 (#103) * Add `helpers.exec_code` function to replace `exec` where source code available at runtime * Update README and playground for `exec_code` * Fix type hint for sourcefile parameter * Add future import for annotations * Fix linting * 0.12.2 --- .gitignore | 1 + README.md | 23 ++++- README.raw.md | 23 ++++- docs/CHANGELOG.md | 4 + playground/playground.ipynb | 186 +++++++++++++++++++----------------- poetry.lock | 2 +- pyproject.toml | 2 +- tests/test_helpers.py | 35 ++++++- varname/__init__.py | 2 +- varname/helpers.py | 75 +++++++++++++++ 10 files changed, 255 insertions(+), 98 deletions(-) diff --git a/.gitignore b/.gitignore index f9c0117..f0fa5f0 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,7 @@ celerybeat-schedule # virtualenv venv/ +.venv/ ENV/ # Spyder project settings diff --git a/README.md b/README.md index 6816379..a9bf786 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Note if you use `python < 3.8`, install `varname < 0.11` - A decorator to register `__varname__` to functions/classes, using `register` - A helper function to create dict without explicitly specifying the key-value pairs, using `jsobj` - A `debug` function to print variables with their names and values + - `exec_code` to replace `exec` where source code is available at runtime ## Credits @@ -323,7 +324,7 @@ func4(y, x, c=z) # prints: ('x', 'z') # __getattr__/__getitem__/__setattr/__setitem__/__add__/__lt__, etc. class Foo: def __setattr__(self, name, value): - print(argname("name", "value")) + print(argname("name", "value", func=self.__setattr__)) Foo().a = 1 # prints: ("'a'", '1') @@ -383,6 +384,26 @@ debug(a+a) debug(a+a, vars_only=True) # ImproperUseError ``` +### Replacing `exec` with `exec_code` + +```python +from varname import argname +from varname.helpers import exec_code + +class Obj: + def __init__(self): + self.argnames = [] + + def receive(self, arg): + self.argnames.append(argname('arg', func=self.receive)) + +obj = Obj() +# exec('obj.receive(1)') # Error +exec_code('obj.receive(1)') +exec_code('obj.receive(2)') +obj.argnames # ['1', '2'] +``` + ## Reliability and limitations `varname` is all depending on `executing` package to look for the node. diff --git a/README.raw.md b/README.raw.md index 3c7029a..05dec9b 100644 --- a/README.raw.md +++ b/README.raw.md @@ -31,6 +31,7 @@ Note if you use `python < 3.8`, install `varname < 0.11` - A decorator to register `__varname__` to functions/classes, using `register` - A helper function to create dict without explicitly specifying the key-value pairs, using `jsobj` - A `debug` function to print variables with their names and values + - `exec_code` to replace `exec` where source code is available at runtime ## Credits @@ -319,7 +320,7 @@ func4(y, x, c=z) # prints: {_out} # __getattr__/__getitem__/__setattr/__setitem__/__add__/__lt__, etc. class Foo: def __setattr__(self, name, value): - print(argname("name", "value")) + print(argname("name", "value", func=self.__setattr__)) Foo().a = 1 # prints: {_out} ``` @@ -378,6 +379,26 @@ debug(a+a) debug(a+a, vars_only=True) # {_exc} ``` +### Replacing `exec` with `exec_code` + +```python +from varname import argname +from varname.helpers import exec_code + +class Obj: + def __init__(self): + self.argnames = [] + + def receive(self, arg): + self.argnames.append(argname('arg', func=self.receive)) + +obj = Obj() +# exec('obj.receive(1)') # Error +exec_code('obj.receive(1)') +exec_code('obj.receive(2)') +obj.argnames # ['1', '2'] +``` + ## Reliability and limitations `varname` is all depending on `executing` package to look for the node. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b54b93a..0a302b5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## 0.12.2 + +- Add `helpers.exec_code` function to replace `exec` so that source code available at runtime + ## 0.12.1 - Bump executing to 2.0.1 diff --git a/playground/playground.ipynb b/playground/playground.ipynb index 5a2046f..fb1924c 100644 --- a/playground/playground.ipynb +++ b/playground/playground.ipynb @@ -25,10 +25,10 @@ "\n", "from varname import (\n", " varname, nameof, will, argname,\n", - " config, \n", + " config,\n", " ImproperUseError, VarnameRetrievingError, QualnameNonUniqueError\n", ")\n", - "from varname.helpers import Wrapper, register, debug, jsobj\n", + "from varname.helpers import Wrapper, register, debug, jsobj, exec_code\n", "\n", "@contextmanager\n", "def enable_debug():\n", @@ -43,7 +43,7 @@ " try:\n", " yield\n", " except error as exc:\n", - " print(f'{error.__name__}({exc}) raised!')\n", + " print(f'{error.__name__} raised!')\n", " else:\n", " raise Exception(f'{error.__name__!r} NOT raised!')" ] @@ -122,7 +122,7 @@ "class Foo:\n", " def __init__(self):\n", " self.id = varname()\n", - " \n", + "\n", "foo = Foo()\n", "foo.id" ] @@ -257,10 +257,10 @@ "class Foo:\n", " def __init__(self):\n", " self.id = varname(frame=2)\n", - " \n", + "\n", "def wrapper():\n", " return Foo()\n", - " \n", + "\n", "foo = wrapper()\n", "foo.id" ] @@ -375,22 +375,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "ImproperUseError(Caller doesn't assign the result directly to variable(s).\n", - "\n", - " /tmp/ipykernel_14838/1606438573.py:11:12\n", - " | 9 @decor\n", - " | 10 def func2():\n", - " > | 11 return func()\n", - " | ^\n", - "\n", - ") raised!\n" + "ImproperUseError raised!\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "/home/pwwang/github/python-varname/varname/ignore.py:175: MaybeDecoratedFunctionWarning: You asked varname to ignore function 'wrapper', which may be decorated. If it is not intended, you may need to ignore all intermediate frames with a tuple of the function and the number of its decorators.\n", + "/workspaces/python-varname/varname/ignore.py:175: MaybeDecoratedFunctionWarning: You asked varname to ignore function 'wrapper', which may be decorated. If it is not intended, you may need to ignore all intermediate frames with a tuple of the function and the number of its decorators.\n", " warnings.warn(\n" ] } @@ -479,12 +471,12 @@ "output_type": "stream", "text": [ "[varname] DEBUG: >>> IgnoreList initiated <<<\n", - "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /home/pwwang/github/python-varname/varname/core.py:105]\n", - "[varname] DEBUG: Ignored by IgnoreModule('module_all_calls') [In 'func' at /home/pwwang/github/python-varname/playground/module_all_calls.py:6]\n", - "[varname] DEBUG: Ignored by IgnoreModule('module_all_calls') [In 'func2' at /home/pwwang/github/python-varname/playground/module_all_calls.py:9]\n", - "[varname] DEBUG: Ignored by IgnoreModule('module_all_calls') [In 'func3' at /home/pwwang/github/python-varname/playground/module_all_calls.py:12]\n", - "[varname] DEBUG: Skipping (0 more to skip) [In 'func' at /tmp/ipykernel_14838/3068660293.py:4]\n", - "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_14838/3068660293.py:7]\n" + "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /workspaces/python-varname/varname/core.py:105]\n", + "[varname] DEBUG: Ignored by IgnoreModule('module_all_calls') [In 'func' at /workspaces/python-varname/playground/module_all_calls.py:6]\n", + "[varname] DEBUG: Ignored by IgnoreModule('module_all_calls') [In 'func2' at /workspaces/python-varname/playground/module_all_calls.py:9]\n", + "[varname] DEBUG: Ignored by IgnoreModule('module_all_calls') [In 'func3' at /workspaces/python-varname/playground/module_all_calls.py:12]\n", + "[varname] DEBUG: Skipping (0 more to skip) [In 'func' at /tmp/ipykernel_16149/3068660293.py:4]\n", + "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_16149/3068660293.py:7]\n" ] }, { @@ -535,11 +527,11 @@ "output_type": "stream", "text": [ "[varname] DEBUG: >>> IgnoreList initiated <<<\n", - "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /home/pwwang/github/python-varname/varname/core.py:105]\n", - "[varname] DEBUG: Ignored by IgnoreModuleQualname('module_glob_qualname', '_func*') [In '_func' at /home/pwwang/github/python-varname/playground/module_glob_qualname.py:6]\n", - "[varname] DEBUG: Ignored by IgnoreModuleQualname('module_glob_qualname', '_func*') [In '_func2' at /home/pwwang/github/python-varname/playground/module_glob_qualname.py:9]\n", - "[varname] DEBUG: Skipping (0 more to skip) [In 'func3' at /home/pwwang/github/python-varname/playground/module_glob_qualname.py:12]\n", - "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_14838/491507787.py:4]\n" + "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /workspaces/python-varname/varname/core.py:105]\n", + "[varname] DEBUG: Ignored by IgnoreModuleQualname('module_glob_qualname', '_func*') [In '_func' at /workspaces/python-varname/playground/module_glob_qualname.py:6]\n", + "[varname] DEBUG: Ignored by IgnoreModuleQualname('module_glob_qualname', '_func*') [In '_func2' at /workspaces/python-varname/playground/module_glob_qualname.py:9]\n", + "[varname] DEBUG: Skipping (0 more to skip) [In 'func3' at /workspaces/python-varname/playground/module_glob_qualname.py:12]\n", + "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_16149/491507787.py:4]\n" ] }, { @@ -586,7 +578,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "QualnameNonUniqueError(Qualname 'func' in 'module_dual_qualnames' refers to multiple objects.) raised!\n" + "QualnameNonUniqueError raised!\n" ] } ], @@ -622,10 +614,10 @@ "output_type": "stream", "text": [ "[varname] DEBUG: >>> IgnoreList initiated <<<\n", - "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /home/pwwang/github/python-varname/varname/core.py:105]\n", - "[varname] DEBUG: Skipping (0 more to skip) [In 'func' at /tmp/ipykernel_14838/2761136102.py:2]\n", - "[varname] DEBUG: Ignored by IgnoreOnlyQualname(None, '*') [In '' at /tmp/ipykernel_14838/2761136102.py:4]\n", - "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_14838/2761136102.py:7]\n" + "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /workspaces/python-varname/varname/core.py:105]\n", + "[varname] DEBUG: Skipping (0 more to skip) [In 'func' at /tmp/ipykernel_16149/2761136102.py:2]\n", + "[varname] DEBUG: Ignored by IgnoreOnlyQualname(None, '*') [In '' at /tmp/ipykernel_16149/2761136102.py:4]\n", + "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_16149/2761136102.py:7]\n" ] }, { @@ -675,10 +667,10 @@ "output_type": "stream", "text": [ "[varname] DEBUG: >>> IgnoreList initiated <<<\n", - "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /home/pwwang/github/python-varname/varname/core.py:105]\n", - "[varname] DEBUG: Skipping (0 more to skip) [In '__init__' at /tmp/ipykernel_14838/641638691.py:8]\n", - "[varname] DEBUG: Ignored by IgnoreStdlib('/home/pwwang/miniconda3/lib/python3.10/') [In '__call__' at /home/pwwang/miniconda3/lib/python3.10/typing.py:957]\n", - "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_14838/641638691.py:11]\n" + "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /workspaces/python-varname/varname/core.py:105]\n", + "[varname] DEBUG: Skipping (0 more to skip) [In '__init__' at /tmp/ipykernel_16149/641638691.py:8]\n", + "[varname] DEBUG: Ignored by IgnoreStdlib('/usr/local/python/3.10.13/lib/python3.10/') [In '__call__' at /home/codespace/.python/current/lib/python3.10/typing.py:957]\n", + "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_16149/641638691.py:11]\n" ] }, { @@ -732,7 +724,7 @@ } ], "source": [ - " \n", + "\n", "class Foo(Generic[T]):\n", " def __init__(self):\n", " self.id = varname()\n", @@ -775,7 +767,7 @@ ], "source": [ "source = '''\n", - "def foo(): \n", + "def foo():\n", " return bar()\n", "'''\n", "\n", @@ -820,11 +812,11 @@ "output_type": "stream", "text": [ "[varname] DEBUG: >>> IgnoreList initiated <<<\n", - "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /home/pwwang/github/python-varname/varname/core.py:105]\n", - "[varname] DEBUG: Ignored by IgnoreDecorated('wrapper', 2) [In 'func' at /tmp/ipykernel_14838/652967550.py:2]\n", - "[varname] DEBUG: Skipping (1 more to skip) [In 'wrapper' at /tmp/ipykernel_14838/652967550.py:9]\n", - "[varname] DEBUG: Skipping (0 more to skip) [In 'func3' at /tmp/ipykernel_14838/652967550.py:18]\n", - "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_14838/652967550.py:21]\n" + "[varname] DEBUG: Ignored by IgnoreModule('varname') [In 'varname' at /workspaces/python-varname/varname/core.py:105]\n", + "[varname] DEBUG: Ignored by IgnoreDecorated('wrapper', 2) [In 'func' at /tmp/ipykernel_16149/652967550.py:2]\n", + "[varname] DEBUG: Skipping (1 more to skip) [In 'wrapper' at /tmp/ipykernel_16149/652967550.py:9]\n", + "[varname] DEBUG: Skipping (0 more to skip) [In 'func3' at /tmp/ipykernel_16149/652967550.py:18]\n", + "[varname] DEBUG: Gotcha! [In '' at /tmp/ipykernel_16149/652967550.py:21]\n" ] }, { @@ -891,20 +883,8 @@ "2 (a, b) = ('a', 'b')\n", "3 a = ('a',)\n", "4 (a, b, c) = ('a', 'b', 'c')\n", - "5 (a, b, x.c) = ('a', 'b', 'c')\n" - ] - }, - { - "ename": "Exception", - "evalue": "'ImproperUseError' NOT raised!", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[19], line 22\u001b[0m\n\u001b[1;32m 19\u001b[0m a, (b, x\u001b[39m.\u001b[39mc) \u001b[39m=\u001b[39m function()\n\u001b[1;32m 20\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m5\u001b[39m, \u001b[39m'\u001b[39m\u001b[39m(a, b, x.c) =\u001b[39m\u001b[39m'\u001b[39m, (a, b, x\u001b[39m.\u001b[39mc))\n\u001b[0;32m---> 22\u001b[0m \u001b[39mwith\u001b[39;00m expect_raising(ImproperUseError):\n\u001b[1;32m 23\u001b[0m a, \u001b[39m*\u001b[39mb \u001b[39m=\u001b[39m function()\n", - "File \u001b[0;32m~/miniconda3/lib/python3.10/contextlib.py:142\u001b[0m, in \u001b[0;36m_GeneratorContextManager.__exit__\u001b[0;34m(self, typ, value, traceback)\u001b[0m\n\u001b[1;32m 140\u001b[0m \u001b[39mif\u001b[39;00m typ \u001b[39mis\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[1;32m 141\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m--> 142\u001b[0m \u001b[39mnext\u001b[39;49m(\u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mgen)\n\u001b[1;32m 143\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mStopIteration\u001b[39;00m:\n\u001b[1;32m 144\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mFalse\u001b[39;00m\n", - "Cell \u001b[0;32mIn[1], line 25\u001b[0m, in \u001b[0;36mexpect_raising\u001b[0;34m(error)\u001b[0m\n\u001b[1;32m 23\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39mf\u001b[39m\u001b[39m'\u001b[39m\u001b[39m{\u001b[39;00merror\u001b[39m.\u001b[39m\u001b[39m__name__\u001b[39m\u001b[39m}\u001b[39;00m\u001b[39m(\u001b[39m\u001b[39m{\u001b[39;00mexc\u001b[39m}\u001b[39;00m\u001b[39m) raised!\u001b[39m\u001b[39m'\u001b[39m)\n\u001b[1;32m 24\u001b[0m \u001b[39melse\u001b[39;00m:\n\u001b[0;32m---> 25\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mException\u001b[39;00m(\u001b[39mf\u001b[39m\u001b[39m'\u001b[39m\u001b[39m{\u001b[39;00merror\u001b[39m.\u001b[39m\u001b[39m__name__\u001b[39m\u001b[39m!r}\u001b[39;00m\u001b[39m NOT raised!\u001b[39m\u001b[39m'\u001b[39m)\n", - "\u001b[0;31mException\u001b[0m: 'ImproperUseError' NOT raised!" + "5 (a, b, x.c) = ('a', 'b', 'c')\n", + "ImproperUseError raised!\n" ] } ], @@ -931,7 +911,7 @@ "print(5, '(a, b, x.c) =', (a, b, x.c))\n", "\n", "with expect_raising(ImproperUseError):\n", - " a, *b = function()" + " a, b[0] = function()" ] }, { @@ -944,7 +924,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.737764Z", @@ -958,7 +938,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "VarnameRetrievingError(Unable to retrieve the ast node.) raised!\n" + "VarnameRetrievingError raised!\n" ] }, { @@ -997,7 +977,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.754849Z", @@ -1018,7 +998,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/home/pwwang/miniconda3/lib/python3.10/site-packages/varname/core.py:123: MultiTargetAssignmentWarning: Multiple targets in assignment, variable name on the very right is used. \n", + "/workspaces/python-varname/varname/core.py:124: MultiTargetAssignmentWarning: Multiple targets in assignment, variable name on the very right is used. \n", " warnings.warn(\n" ] } @@ -1042,7 +1022,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.767662Z", @@ -1075,7 +1055,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.778858Z", @@ -1116,7 +1096,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.803768Z", @@ -1154,7 +1134,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.816768Z", @@ -1169,7 +1149,7 @@ "output_type": "stream", "text": [ "1\n", - "AttributeError(Unable to access private attributes.) raised!\n" + "AttributeError raised!\n" ] } ], @@ -1200,7 +1180,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.833103Z", @@ -1220,14 +1200,6 @@ "('x', 'z')\n", "(\"'a'\", '1')\n" ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/pwwang/miniconda3/lib/python3.10/site-packages/varname/utils.py:445: UsingExecWarning: Cannot evaluate node Attribute(value=Call(func=Name(id='Foo', ctx=Load()), args=[], keywords=[]), attr='__setattr__', ctx=Load()) using 'pure_eval'. Using 'eval' to get the function that calls 'argname'. Try calling it using a variable reference to the function, or passing the function to 'argname' explicitly.\n", - " warnings.warn(\n" - ] } ], "source": [ @@ -1248,12 +1220,12 @@ "def func3(a, b=1):\n", " # print(argname(a, b, vars_only=False))\n", " print(argname('a', 'b', vars_only=False))\n", - "func3(x+y, y+x) \n", + "func3(x+y, y+x)\n", "\n", "# positional and keyword arguments\n", "def func4(*args, **kwargs):\n", - " # print(argname(args[1], kwargs['c'])) \n", - " print(argname('args[1]', 'kwargs[c]')) \n", + " # print(argname(args[1], kwargs['c']))\n", + " print(argname('args[1]', 'kwargs[c]'))\n", "func4(y, x, c=z)\n", "\n", "# As of 0.9.0\n", @@ -1261,14 +1233,14 @@ "# __getattr__/__getitem__/__setattr/__setitem__/__add__/__lt__, etc.\n", "class Foo:\n", " def __setattr__(self, name, value):\n", - " print(argname(\"name\", \"value\"))\n", + " print(argname(\"name\", \"value\", func=self.__setattr__))\n", "\n", "Foo().a = 1 # prints: {_out}" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.854380Z", @@ -1316,7 +1288,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.865673Z", @@ -1361,7 +1333,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.881609Z", @@ -1414,7 +1386,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.895641Z", @@ -1454,7 +1426,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.907363Z", @@ -1494,7 +1466,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "metadata": {}, "outputs": [ { @@ -1523,7 +1495,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": { "execution": { "iopub.execute_input": "2021-08-13T18:11:04.919563Z", @@ -1562,6 +1534,44 @@ "debug(a+b, vars_only=False)\n", "debug(a+b, sep=':', vars_only=False)\n" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Use of `exec_code`" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['1', '2']" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "class Obj:\n", + " def __init__(self):\n", + " self.argnames = []\n", + "\n", + " def receive(self, arg):\n", + " self.argnames.append(argname('arg', func=self.receive))\n", + "\n", + "obj = Obj()\n", + "# exec('obj.receive(1)') # Error\n", + "exec_code('obj.receive(1)')\n", + "exec_code('obj.receive(2)')\n", + "obj.argnames" + ] } ], "metadata": { @@ -1580,7 +1590,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.10.13" }, "vscode": { "interpreter": { diff --git a/poetry.lock b/poetry.lock index 0ad0afd..da9c59c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "asttokens" diff --git a/pyproject.toml b/pyproject.toml index df8b20d..1fdadef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "varname" -version = "0.12.1" +version = "0.12.2" description = "Dark magics about variable names in python." authors = [ "pwwang ",] license = "MIT" diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 684401d..7d87a53 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,8 +1,9 @@ import sys import pytest -from varname import * -from varname.helpers import * +from varname import varname +from varname.utils import MaybeDecoratedFunctionWarning, VarnameRetrievingError +from varname.helpers import Wrapper, debug, jsobj, register, exec_code def test_wrapper(): @@ -87,7 +88,7 @@ def test_jsobj(): def test_register_to_function(): @register def func(): - return __varname__ + return __varname__ # noqa # pyright: ignore f = func() assert f == "f" @@ -95,7 +96,7 @@ def func(): # wrapped with other function @register(frame=2) def func1(): - return __varname__ + return __varname__ # noqa # pyright: ignore def func2(): return func1() @@ -109,7 +110,31 @@ def func3(): @register(ignore=[(sys.modules[__name__], func3.__qualname__)]) def func4(): - return __varname__ + return __varname__ # noqa # pyright: ignore f = func3() assert f == "f" + + +def test_exec_code(tmp_path): + def func(): + return varname() + + # Normal case works + f = func() + assert f == "f" + + code = "f1 = func()" + with pytest.raises(VarnameRetrievingError): + exec(code) + + # works + exec_code(code) + + locs = {"func": func} + exec_code(code, globals(), locs) + assert locs["f1"] == "f1" + del locs["f1"] + + exec_code(code, globals(), locs, sourcefile=tmp_path / "test.py") + assert locs["f1"] == "f1" diff --git a/varname/__init__.py b/varname/__init__.py index 57bd1c9..ac5a8c7 100644 --- a/varname/__init__.py +++ b/varname/__init__.py @@ -13,4 +13,4 @@ ) from .core import varname, nameof, will, argname -__version__ = "0.12.1" +__version__ = "0.12.2" diff --git a/varname/helpers.py b/varname/helpers.py index 27acccd..79d7405 100644 --- a/varname/helpers.py +++ b/varname/helpers.py @@ -1,9 +1,13 @@ """Some helper functions builtin based upon core features""" +from __future__ import annotations + import inspect from functools import partial, wraps +from os import PathLike from typing import Any, Callable, Dict, Tuple, Type, Union from .utils import IgnoreType +from .ignore import IgnoreList from .core import argname, varname @@ -220,3 +224,74 @@ def debug( else: for name_and_value in name_and_values: print(f"{prefix}{name_and_value}") + + +def exec_code( + code: str, + globals: Dict[str, Any] = None, + locals: Dict[str, Any] = None, + /, + sourcefile: PathLike | str = None, + frame: int = 1, + ignore: IgnoreType = None, + **kwargs: Any, +) -> None: + """Execute code where source code is visible at runtime. + + This function is useful when you want to execute some code, where you want to + retrieve the AST node of the code at runtime. This function will create a + temporary file and write the code into it, then execute the code in the + file. + + Examples: + >>> from varname import varname + >>> def func(): return varname() + >>> exec('var = func()') # VarnameRetrievingError: + >>> # Unable to retrieve the ast node. + >>> from varname.helpers import code_exec + >>> code_exec('var = func()') # var == 'var' + + Args: + code: The code to execute. + globals: The globals to use. + locals: The locals to use. + sourcefile: The source file to write the code into. + if not given, a temporary file will be used. + This file will be deleted after the code is executed. + frame: The call stack index. You can understand this as the number of + wrappers around this function. This is used to fetch `globals` and + `locals` from where the destination function (include the wrappers + of this function) + is called. + ignore: The intermediate calls to be ignored. See `varname.ignore` + Note that if both `globals` and `locals` are given, `frame` and + `ignore` will be ignored. + **kwargs: The keyword arguments to pass to `exec`. + """ + if sourcefile is None: + import tempfile + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".py", delete=False + ) as f: + f.write(code) + sourcefile = f.name + else: + sourcefile = str(sourcefile) + with open(sourcefile, "w") as f: + f.write(code) + + if globals is None or locals is None: + ignore_list = IgnoreList.create(ignore) + frame_info = ignore_list.get_frame(frame) + if globals is None: + globals = frame_info.f_globals + if locals is None: + locals = frame_info.f_locals + + try: + exec(compile(code, sourcefile, "exec"), globals, locals, **kwargs) + finally: + import os + + os.remove(sourcefile)