18
loading...
This website collects cookies to deliver better user experience
ast
module to convert legacy Python 2 to Python 3.__eq__
method of an object then your __hash__
method automatically becomes None
. This means that your class is rendered unhashable (and thus unusable in many settings). This makes sense as the__hash__
function to recover object hashbility might be a pragmatically acceptable solution.__eq__
methods but lack a new __hash__
method and then patching the class to have one.ast
module of Python...# Typical hello world example
print('Hello World')
import ast
tree = ast.parse(open('hello.py').read())
tree
object, it might seem daunting at first, but don't let appearances fool you, it is actually quite simple. The type of tree willast.Module
. If you ask, by doing tree.fields
, for its childrentree.body
). The body attribute will have, you guessed it, the body of the file:print(tree.body)
<_ast.expr object at 0x7f5731c59bb0>
Statements
. In our caseExpr
(Expression is another type of node -Call
. What is in a call? Well, you call a function with arguments so a call is a function name plus a set of arguments:print(tree.body[0].value._fields)
('func', 'args', 'keywords', 'starargs', 'kwargs')
func
plus a lot of stuff for arguments. This is because, as you know, Python has quite a rich way of passing arguments (positional arguments, named arguments, ...). For now lets concentratefunc
and args
only. func
is a Name with an attribute called id
args
is a list with a single element, a string:>>> print(tree.body[0].value.func.id)
print
>>> print(tree.body[0].value.args[0].s)
Hello World
def print_hello(who):
print('Hello %s' % who)
>print(tree.body)
<_ast.functiondef object at 0x7f5731c59bb0>
name
, args
, body
, decorator~list
and returns
. Name
is a stringprint_hello
, no more indirections here, easy. args
cannot be veryargs
, vararg
,kwonlyargs
, kw_defaults
, kwarg
, defaults
. In our case we justargs.args
:print(tree.body[0].args.args)
<_ast.arg object at 0x7f5731c59bb0>
args
object has a arg
field (tree.body[0].args.args[0].arg
starts to sound a bit ludicrous, but such is life), which is a stringwho
). Now, the function body can be found in the body field of theprint(tree.body[0].body)
<_ast.expr object at>
body
is as discussed above for the print
call.print("Hello %s" % who)
- Notice the BinOp
for the %
operator:# Lets look at two extra properties of the print line...
print(tree.body[0].body[0].lineno, tree.body[0].body[0].col_offset)
2 4
class Hello:
def __init__(self, who):
self.who = who
def print_hello(self):
print('Hello %s' % self.who)
<_ast.classdef object at>
ClassDef
object has a nameFuncDefs
). There are only a couple of conceptually new things, andself.who = who
=
and the compound nameself.who
.x, y = 1, 2
__eq__
methods defined, but lack__hash__
methods, so a pragmatic (though not totally rigorous__hash__
methods required by Python 3.2to3
(no need for monkey2to3
)def traverse_dir(my_dir):
content = os.listdir(my_dir)
for element in content:
if os.path.isdir(my_dir + os.sep + element):
traverse_dir(my_dir + os.sep + element)
elif os.path.isfile(my_dir + os.sep + element) and element.endswith('.py'):
process_file(my_dir + os.sep + element)</pre>
__eq__
method, but not an__hash__
method:def get_classes(tree):
# Will not work for nested classes
my_classes = []
for expr in tree.body:
if type(expr) == ast.ClassDef:
my_classes.append(expr)
return my_classes</code></pre>
def get_class_methods(tree):
my_methods = []
for expr in tree.body:
if type(expr) == ast.FunctionDef:
my_methods.append(expr)
return my_methods
ClassDef
object.def process_file(my_file):
shutil.copyfile(my_file, my_file + '.bak')
tree = ast.parse(open(my_file).read())
my_classes = get_classes(tree)
patches = {}
for my_class in my_classes:
methods = get_class_methods(my_class)
has_eq = '__eq__' in [method.name for method in methods]
has_hash = '__hash__' in [method.name for method in methods]
if has_eq and not has_hash:
lineno = compute_patch(methods)
patches[lineno] = my_class.name
patch(my_file, patches)
__eq__
method with no __hash__
method then a patch is computed anddef compute_patch(methods):
names = [method.name for method in methods]
names.append('__hash__')
try:
names.remove('__init__')
except ValueError:
pass
names.sort()
try:
method_after = names[names.index('__hash__') + 1]
except IndexError:
method_after = names[-2]
for method in methods:
if method.name == method_after:
return method.lineno
__hash__
would go between __eq__
and__init__
). Now we can patch:def patch(my_file, patches):
f = open(my_file + '.bak')
w = open(my_file, 'w')
lineno = 0
for l in f:
lineno += 1
if lineno in patches:
w.write(""" def __hash__(self):
r'''Hashes my class.
Required to be explicitely re-defined on Python 3 if __eq__ changes.
Returns integer.
'''
return super(%s, self).__hash__()
""" % patches[lineno])
w.write(l)