Repository: berkerpeksag/astor

Issue:
astor.code_to_ast fails with KeyError on decorated functions in Python 3.8.1
The line number for codeobj is reported as the line number of the last processed decorator for the function for decorated functions. The line number reported for the FunctionDef on parse is the actual line number of the function declaration, e.g. `def funcname`, leading to a mismatch and a KeyError on attempting to return the ast for the codeobj.

Discussion:
dcharbon: You're welcome, and, thank you! I've updated the changelog.
dcharbon: Sorry it took me a while to get back to this, I didn't actually see the notification emails... too many emails :)
dcharbon: Are there additional changes needed/wanted?
dcharbon: @berkerpeksag Is there any more I need to do? Is this ready to merge?
dcharbon: Is there an update on this pull request? This causes problems in Python 3.8 and above. For example, this makes [safetydance](https://gitlab.com/openteams/safetydance) (steps based testing framework I've been developing) unusable with the release version of astor. Thanks!
dcharbon: @berkerpeksag just checking in - is there any more I could provide or any additional assistance I could give?
berkerpeksag: @dcharbon sorry for my late response! I'm currently on vacation and I finally have some free time to look at open PRs. I'll check this out this week.
berkerpeksag: Apparently I cannot edit your branch. Could you please apply the following patch and push?

```diff
From 8fc70619fca25be756a777746aa9209b993ae361 Mon Sep 17 00:00:00 2001
From: Berker Peksag <berker.peksag@gmail.com>
Date: Sun, 21 Feb 2021 01:56:04 +0200
Subject: [PATCH] edits before merge

---
 astor/file_util.py        |  4 +--
 docs/changelog.rst        |  6 ++--
 tests/test_code_to_ast.py | 75 +++++++++++++++++++++++++++++++++++------------
 3 files changed, 63 insertions(+), 22 deletions(-)

diff --git a/astor/file_util.py b/astor/file_util.py
index a8da39f..ee5b9f3 100644
--- a/astor/file_util.py
+++ b/astor/file_util.py
@@ -95,12 +95,12 @@ class CodeToAst(object):
 
     def __call__(self, codeobj):
         cache = self.cache
-        fname, lineno = self.get_file_info(codeobj)
+        fname = self.get_file_info(codeobj)[0]
         key = (fname, codeobj.__name__)
         result = cache.get(key)
         if result is not None:
             return result
-        cache[(fname, 0)] = mod_ast = self.parse_file(fname)
+        cache[key] = mod_ast = self.parse_file(fname)
         for obj in mod_ast.body:
             if not isinstance(obj, ast.FunctionDef):
                 continue
diff --git a/docs/changelog.rst b/docs/changelog.rst
index ceb2443..b3c5a8e 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -8,8 +8,10 @@ Release Notes
 Bug fixes
 ~~~~~~~~~
 
-* Use the codeobj name in the key rather than the line number.
-  (Reported by David Charboneau in `Issue 174`_ and fixed by David Charboneau in `PR 175`_.)
+* Use ``codeobj.__name__`` in the key for the internal cache of
+  :class:`astor.file_util.CodeToAst` rather than the line number to
+  prevent :exc:`KeyError`.
+  (Reported and fixed by David Charboneau in `Issue 174`_ and `PR 175`_.)
 
 .. _`Issue 174`: https://github.com/berkerpeksag/astor/pull/174
 .. _`PR 175`: https://github.com/berkerpeksag/astor/pull/175
diff --git a/tests/test_code_to_ast.py b/tests/test_code_to_ast.py
index 45bdd79..da62b49 100644
--- a/tests/test_code_to_ast.py
+++ b/tests/test_code_to_ast.py
@@ -1,33 +1,72 @@
+import functools
+import unittest
+
 from astor import code_to_ast
-from functools import wraps
 
-def example_decorator(f):
-    @wraps(f)
+
+def decorator(f):
+    @functools.wraps(f)
     def wrapper(*args, **kwargs):
         return f(*args, **kwargs)
     return wrapper
 
-@example_decorator
-def example_decorated_func():
+
+def simple_decorator(f):
+    f.__decorated__ = True
+    return f
+
+
+def undecorated_func():
     pass
 
-def example_undecorated_func():
+
+@decorator
+def decorated_func():
     pass
 
-@example_decorator
-@example_decorator
+
+@decorator
+@decorator
 def twice_decorated_func():
     pass
 
-def test_code_to_ast():
-    # validate we can get the ast for a decorated function
-    ast = code_to_ast(example_decorated_func)
-    assert ast is not None
 
-    # validate we can get the ast for an undecorated function
-    ast = code_to_ast(example_undecorated_func)
-    assert ast is not None
+@simple_decorator
+def plain_decorated_func():
+    pass
+
+
+@simple_decorator
+def simple_decorated_func():
+    pass
+
+
+@simple_decorator
+@decorator
+def twice_decorated_func_2():
+    pass
+
+
+class CodeToASTTestCase(unittest.TestCase):
+
+    def test_decorated(self):
+        self.assertIsNotNone(code_to_ast(decorated_func))
+
+    def test_undecorated(self):
+        self.assertIsNotNone(code_to_ast(undecorated_func))
+
+    def test_twice_decorated(self):
+        self.assertIsNotNone(code_to_ast(twice_decorated_func))
+
+    def test_twice_decorated_2(self):
+        self.assertIsNotNone(code_to_ast(twice_decorated_func_2))
+
+    def test_plain_decorator(self):
+        self.assertIsNotNone(code_to_ast(simple_decorated_func))
+
+    def test_module(self):
+        self.assertIsNotNone(code_to_ast(unittest))
+
 
-    # validate we can get the ast for a multiply decorated function
-    ast = code_to_ast(twice_decorated_func)
-    assert ast is not None
+if __name__ == '__main__':
+    unittest.main()
-- 
2.15.0


```

Patch:
diff --git a/MANIFEST.in b/MANIFEST.in
index 81e72d5..6f3d999 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,2 @@
-include README.rst AUTHORS LICENSE CHANGES
-include setuputils.py
-include astor/VERSION
+include README.rst AUTHORS LICENSE
 recursive-include tests *.py
diff --git a/astor/VERSION b/astor/VERSION
deleted file mode 100644
index 6f4eebd..0000000
--- a/astor/VERSION
+++ /dev/null
@@ -1,1 +0,0 @@
-0.8.1
diff --git a/astor/__init__.py b/astor/__init__.py
index bdedaef..b28de79 100644
--- a/astor/__init__.py
+++ b/astor/__init__.py
@@ -9,7 +9,6 @@ Copyright 2013 (c) Berker Peksag
 
 """
 
-import os
 import warnings
 
 from .code_gen import SourceGenerator, to_source  # NOQA
@@ -20,9 +19,7 @@ from .op_util import get_op_symbol, get_op_precedence  # NOQA
 from .op_util import symbol_data  # NOQA
 from .tree_walk import TreeWalk  # NOQA
 
-ROOT = os.path.dirname(__file__)
-with open(os.path.join(ROOT, 'VERSION')) as version_file:
-    __version__ = version_file.read().strip()
+__version__ = '0.8.1'
 
 parse_file = code_to_ast.parse_file
 
diff --git a/astor/file_util.py b/astor/file_util.py
index d685597..ee5b9f3 100644
--- a/astor/file_util.py
+++ b/astor/file_util.py
@@ -95,16 +95,16 @@ class CodeToAst(object):
 
     def __call__(self, codeobj):
         cache = self.cache
-        key = self.get_file_info(codeobj)
+        fname = self.get_file_info(codeobj)[0]
+        key = (fname, codeobj.__name__)
         result = cache.get(key)
         if result is not None:
             return result
-        fname = key[0]
-        cache[(fname, 0)] = mod_ast = self.parse_file(fname)
+        cache[key] = mod_ast = self.parse_file(fname)
         for obj in mod_ast.body:
             if not isinstance(obj, ast.FunctionDef):
                 continue
-            cache[(fname, obj.lineno)] = obj
+            cache[(fname, obj.name)] = obj
         return cache[key]
 
 
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 3467aff..b3c5a8e 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -8,6 +8,14 @@ Release Notes
 Bug fixes
 ~~~~~~~~~
 
+* Use ``codeobj.__name__`` in the key for the internal cache of
+  :class:`astor.file_util.CodeToAst` rather than the line number to
+  prevent :exc:`KeyError`.
+  (Reported and fixed by David Charboneau in `Issue 174`_ and `PR 175`_.)
+
+.. _`Issue 174`: https://github.com/berkerpeksag/astor/pull/174
+.. _`PR 175`: https://github.com/berkerpeksag/astor/pull/175
+
 * Change formatting of function and assignment type annotations to be more
   :pep:`8` friendly.
   (Contributed by Venkatesh-Prasad Ranganath in `PR 170`_.)
diff --git a/docs/conf.py b/docs/conf.py
index a04940c..df4d823 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -4,10 +4,6 @@ import os.path
 import sys
 import time
 
-sys.path.append(os.path.pardir)
-
-import setuputils
-
 extensions = []
 
 templates_path = ['_templates']
@@ -19,7 +15,7 @@ master_doc = 'index'
 project = u'astor'
 copyright = u'2013-%s, Berker Peksag' % time.strftime('%Y')
 
-version = release = setuputils.read('astor/VERSION')
+version = release = '0.8.1'
 
 exclude_patterns = ['_build']
 
diff --git a/setup.cfg b/setup.cfg
index ec52114..f749353 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,7 +2,7 @@
 name = astor
 description = Read/rewrite/write Python ASTs
 long_description = file:README.rst
-version = file:astor/VERSION
+version = attr:astor.__version__
 author = Patrick Maupin
 author_email = pmaupin@gmail.com
 platforms = Independent
diff --git a/setuputils.py b/setuputils.py
deleted file mode 100644
index fd71538..0000000
--- a/setuputils.py
+++ /dev/null
@@ -1,9 +0,0 @@
-import codecs
-import os.path
-
-
-def read(*parts):
-    file_path = os.path.join(os.path.dirname(__file__), *parts)
-    with codecs.open(file_path, 'r') as fobj:
-        content = fobj.read()
-    return content


Test patch:
diff --git a/tests/test_code_to_ast.py b/tests/test_code_to_ast.py
new file mode 100644
index 0000000..da62b49
--- /dev/null
+++ b/tests/test_code_to_ast.py
@@ -0,0 +1,72 @@
+import functools
+import unittest
+
+from astor import code_to_ast
+
+
+def decorator(f):
+    @functools.wraps(f)
+    def wrapper(*args, **kwargs):
+        return f(*args, **kwargs)
+    return wrapper
+
+
+def simple_decorator(f):
+    f.__decorated__ = True
+    return f
+
+
+def undecorated_func():
+    pass
+
+
+@decorator
+def decorated_func():
+    pass
+
+
+@decorator
+@decorator
+def twice_decorated_func():
+    pass
+
+
+@simple_decorator
+def plain_decorated_func():
+    pass
+
+
+@simple_decorator
+def simple_decorated_func():
+    pass
+
+
+@simple_decorator
+@decorator
+def twice_decorated_func_2():
+    pass
+
+
+class CodeToASTTestCase(unittest.TestCase):
+
+    def test_decorated(self):
+        self.assertIsNotNone(code_to_ast(decorated_func))
+
+    def test_undecorated(self):
+        self.assertIsNotNone(code_to_ast(undecorated_func))
+
+    def test_twice_decorated(self):
+        self.assertIsNotNone(code_to_ast(twice_decorated_func))
+
+    def test_twice_decorated_2(self):
+        self.assertIsNotNone(code_to_ast(twice_decorated_func_2))
+
+    def test_plain_decorator(self):
+        self.assertIsNotNone(code_to_ast(simple_decorated_func))
+
+    def test_module(self):
+        self.assertIsNotNone(code_to_ast(unittest))
+
+
+if __name__ == '__main__':
+    unittest.main()