Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
kerim371 authored Oct 5, 2021
0 parents commit 5702d80
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 0 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include pythonguts/tests/data/*.in
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# pythonguts
If your project depends on some external python projects and
you want to make some changes in external functions/methods
and then copy/paste these changes automatically - this package may help you.

There is a tool `editpy` wich we will discuss.

## The idea behind `editpy` tool
`editpy` uses `astor` to find replaceable functions and replaces matching functions.

<ins>To find common function `editpy` checks:</ins>
* are they both _functions?_
* do they both have the same name?
* do they both have the same args?
* do they both have the same parent (i.e. classname for example)?

## Example
original function/method definition file **dest.py**:
```python
class MyClass:
def my_method(self, i: float, j: int, k: float) -> float:
return 0


def foo(i: float) -> float:
return i


def bar():
return 0


# this function stays unchanged
def unchanged():
return 0
```

new function/method definition file **src.py**:
```python
class MyClass:
def my_method(self, i: float, j: int, k: float) -> float:
print('new definition')
return 0


def foo(i: float) -> float:
print('new definition')
return i


def bar():
print('new definition')
return 0
```
Run:

`editpy --src-file=src.py --dest-file=dest.py --oldfile-keep`

`--oldfile-keep` (default) is used to keep the original file (it will be renamed by adding `_OLD_N` suffix). Otherwise use `--oldfile-delete` to delete the original file.

Another option is to run the test (though the test deletes all the generated files so you better take a look in `/tests` dir):

`python -m unittest pythonguts.tests.test_pythonguts`
191 changes: 191 additions & 0 deletions pythonguts/editpy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import argparse
import ast
import astor
import os
import warnings


def generate_unique_filename(filenames: list, filename: str) -> str:
'''
Generates unique filename by adding `_i` to the name.
For example: `myfile.cpp` becomes `myfile_1.cpp`.
:param filenames: list of filenames that resides in the folder
:param filename: base name for a file
:return: uniquename - filename with unique name
'''
basename, extension = os.path.splitext(filename)
uniquename = filename
isunique = True
i = 0
while True:
for name in filenames:
if name.lower() == uniquename.lower():
isunique = False
break
if isunique:
return uniquename
uniquename = basename + '_' + str(i) + extension
isunique = True
i += 1


def prepare_filename(destfile: str) -> str:
'''
Prepare unique filename.
:param destfile: file name where it is expected it shoud be
:return: prepared_filename - full path to the NOT yet created file
'''
destdir = os.path.dirname(os.path.abspath(destfile))
filenames = [f for f in os.listdir(destdir) if os.path.isfile(os.path.join(destdir, f))]
filename = os.path.basename(destfile)
uniquename = generate_unique_filename(filenames, filename)
prepared_filename = os.path.join(destdir, uniquename)
return prepared_filename


class WalkerSrc(astor.TreeWalk):
# KEY - func node; # VALUE - parent
found_nodes = dict()

def pre_body_name(self):
body = self.cur_node
for i, child in enumerate(body[:]):
if isinstance(body[i], ast.FunctionDef):
self.found_nodes[body[i]] = self.parent
if isinstance(body[i], ast.ClassDef):
self.walk(body[i])
return True


class WalkerDest(astor.TreeWalk):
# KEY - func node; # VALUE - parent
walker_src = WalkerSrc()

# def __init__(self, walker_src: WalkerSrc):
# super().__init__()
# self.walker_src = walker_src

def pre_body_name(self):
body = self.cur_node
if not body:
return True

for i, child in enumerate(body[:]):
if isinstance(body[i], ast.FunctionDef):
node_dest, node_src = self.match_node(body[i], self.parent)
if node_src:
body[i] = node_src
if isinstance(body[i], ast.ClassDef):
self.walk(body[i])
return True

def match_node(self, node, parent):
"""
Return two variables if matching nodes were found:
destination node and source node.
Otherwise return None.
:param node: node to compare with source nodes
:param parent: parent of a given node (may be None)
:return: (node_dest, node_src) or None
"""
for node_src in self.walker_src.found_nodes:
if not node or not node_src:
continue

if type(node) != type(node_src):
continue

if hasattr(node, 'name') != hasattr(node_src, 'name'):
continue

if hasattr(node, 'name') and hasattr(node_src, 'name') and \
node.name != node_src.name:
continue

if hasattr(node, 'args') != hasattr(node_src, 'args'):
continue

if hasattr(node, 'args') and hasattr(node_src, 'args') and \
astor.to_source(node.args) != astor.to_source(node_src.args):
continue

parent_src = self.walker_src.found_nodes[node_src]
if not parent or not parent_src:
continue

if type(parent) != type(parent_src):
continue

if hasattr(parent, 'name') != hasattr(parent_src, 'name'):
continue

if hasattr(parent, 'name') and hasattr(parent_src, 'name') and \
parent.name != parent_src.name:
continue

if hasattr(parent, 'args') != hasattr(parent_src, 'args'):
continue

if hasattr(parent, 'args') and hasattr(parent_src, 'args') and \
astor.to_source(parent.args) != astor.to_source(parent_src.args):
continue

return node, node_src

return None, None


def main():
parser = argparse.ArgumentParser(description=
'Replace python function/method definition in destination file. '
'One source file may contain several functions/methods to replace.')
parser.add_argument('--src-file', dest='srcfile', action='store',
type=type('string'), required=True, default=None,
help='file with new functions definitions')
parser.add_argument('--dest-file', dest='destfile', action='store',
type=type('string'), required=True,
help='file with old functions definitions')
parser.add_argument('--oldfile-delete', dest='oldfile_del', action='store_true',
help='use this to delete old version of destination file')
parser.add_argument('--oldfile-keep', dest='oldfile_del', action='store_false',
help='use this to keep old version of destination file (default)')
parser.set_defaults(oldfile_del=False)
args, unknowncmd = parser.parse_known_args()

if not os.path.isfile(args.srcfile):
parser.error(f"specified source file doesn't exist:\n{args.srcfile}")

if not os.path.isfile(args.destfile):
parser.error(f"specified destination file doesn't exist:\n{args.destfile}")

tree_src = astor.parse_file(args.srcfile)
if not tree_src:
parser.error(f"unable to load source file:\n{args.srcfile}")

tree_dest = astor.parse_file(args.destfile)
if not tree_dest:
parser.error(f"unable to load destination file:\n{args.destfile}")

walker_src = WalkerSrc()
walker_src.walk(tree_src)

walker_dest = WalkerDest()
walker_dest.walker_src = walker_src
walker_dest.walk(tree_dest)

prepared_filename = prepare_filename(args.destfile)
with open(prepared_filename, "w") as file:
file.write(astor.to_source(tree_dest))

if args.oldfile_del:
os.remove(args.destfile)
else:
filename, file_extension = os.path.splitext(args.destfile)
prepared_oldfilename = prepare_filename(filename + '_OLD' + file_extension)
os.rename(args.destfile, prepared_oldfilename)

os.rename(prepared_filename, args.destfile)


if __name__ == '__main__':
main()
16 changes: 16 additions & 0 deletions pythonguts/tests/data/dest.py.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class MyClass:
def my_method(self, i: float, j: int, k: float) -> float:
return 0


def foo(i: float) -> float:
return i


def bar():
return 0


# this function stays unchanged
def unchanged():
return 0
14 changes: 14 additions & 0 deletions pythonguts/tests/data/src.py.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class MyClass:
def my_method(self, i: float, j: int, k: float) -> float:
print('new definition')
return 0


def foo(i: float) -> float:
print('new definition')
return i


def bar():
print('new definition')
return 0
34 changes: 34 additions & 0 deletions pythonguts/tests/test_pythonguts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from pathlib import Path
import shutil
import subprocess, os, filecmp
import unittest
import sys


class test_basics(unittest.TestCase):
this_dir = os.path.dirname(__file__)
data_dir = os.path.join(this_dir, 'data')
tmp_dir = os.path.join(this_dir, 'tmp')
src = os.path.join(tmp_dir, 'src.py')
dest = os.path.join(tmp_dir, 'dest.py')
srcin = os.path.join(data_dir, 'src.py.in')
destin = os.path.join(data_dir, 'dest.py.in')

def setUp(self):
shutil.rmtree(self.tmp_dir, ignore_errors=True)
Path(self.tmp_dir).mkdir(parents=True, exist_ok=True)
shutil.copy(self.srcin, self.src)
shutil.copy(self.destin, self.dest)

def tearDown(self):
shutil.rmtree(self.tmp_dir, ignore_errors=True)

def test_basics(self):
os.environ["PATH"] += os.pathsep + os.path.dirname(sys.executable)
guts_env = os.environ.copy()
guts_env["PATH"] += os.pathsep + os.path.dirname(sys.executable)
subprocess.run(['editpy', '--src-file', self.src, '--dest-file', self.dest], env=guts_env)

with open(self.dest) as f:
with open(self.destin) as fin:
self.assertTrue(f != fin)
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[metadata]
description-file = README.md
license_file = LICENSE
43 changes: 43 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import setuptools

# read the contents of your README file
from pathlib import Path
this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()

setuptools.setup(
name='pythonguts',
version='0.0.1',
packages=setuptools.find_packages(),
url='https://github.com/tierra-colada/pythonguts',
license='MIT',
author='kerim khemrev',
author_email='[email protected]',
description='Tool aimed at python code correction that allows to '
'automatically find and replace function definition',
long_description=long_description,
long_description_content_type='text/markdown',
download_url='https://github.com/tierra-colada/pythonguts/archive/refs/tags/v0.0.1.tar.gz',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'Topic :: Software Development :: Build Tools',
'Topic :: Software Development :: Code Generators',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
],
keywords='py-parser python-parser py-editor python-editor py-generator python-generator',
entry_points={
'console_scripts': ['editpy=pythonguts.editpy:main']
},
python_requires='>=3',
install_requires=[
'wheel',
'astor',
],
include_package_data=True # important to copy MANIFEST.in files
)

0 comments on commit 5702d80

Please sign in to comment.