Skip to content

Commit

Permalink
Merge pull request #3 from 15r10nk/python_3.7
Browse files Browse the repository at this point in the history
feat: support for python 3.7 - 3.11
  • Loading branch information
15r10nk authored Dec 17, 2022
2 parents 310e56e + 6dc33a0 commit 2c64738
Show file tree
Hide file tree
Showing 7 changed files with 379 additions and 100 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__pycache__
dist
.python-version
.coverage
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# minimize source code

If you build a linter, formatter or any other tool which has to analyse python source code you might end up searching bugs in pretty large input files.


`pysource_minimize` is able to remove everything from the python source which is not related to the problem.

Example:
``` pycon
>>> from pysource_minimize import minimize

>>> source = """
... def f():
... print("bug"+"other string")
... return 1+1
... f()
... """

>>> print(minimize(source, lambda new_source: "bug" in new_source))
"""bug"""


```
51 changes: 51 additions & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from pathlib import Path

import nox

nox.options.sessions = ["clean", "test", "report", "docs"]
nox.options.reuse_existing_virtualenvs = True


@nox.session(python="python3.10")
def clean(session):
session.run_always("poetry", "install", "--with=dev", external=True)
session.env["TOP"] = str(Path(__file__).parent)
session.run("coverage", "erase")


@nox.session(python="python3.10")
def mypy(session):
session.install("poetry")
session.run("poetry", "install", "--with=dev")
session.run("mypy", "src", "tests")


@nox.session(python=["3.7", "3.8", "3.9", "3.10", "3.11"])
def test(session):
session.run_always("poetry", "install", "--with=dev", external=True)
session.env["COVERAGE_PROCESS_START"] = str(
Path(__file__).parent / "pyproject.toml"
)
session.env["TOP"] = str(Path(__file__).parent)
args = [] if session.posargs else ["-n", "auto", "-v"]

session.run("pytest", *args, "tests", *session.posargs)


@nox.session(python="python3.10")
def report(session):
session.run_always("poetry", "install", "--with=dev", external=True)
session.env["TOP"] = str(Path(__file__).parent)
try:
session.run("coverage", "combine")
except:
pass
session.run("coverage", "html")
session.run("coverage", "report", "--fail-under", "94")


@nox.session(python="python3.10")
def docs(session):
session.install("poetry")
session.run("poetry", "install", "--with=doc")
session.run("mkdocs", "build")
339 changes: 265 additions & 74 deletions poetry.lock

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
[tool.poetry]
name = "pysource-minimize"
version = "0.2.0"
version = "0.3.0"
description = "find failing section in python source"
authors = ["Frank Hoffmann"]
license = "MIT"
readme = "README.md"
packages = [{include = "pysource_minimize"}]

[tool.poetry.dependencies]
python = "^3.10"
python = "^3.7"
asttokens = "^2.0.8"
rich = "^12.6.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.1.3"
pytest-subtests = "^0.8.0"
pytest-xdist = "^3.0.2"
pytest-xdist = {extras = ["psutil"], version = "^3.1.0"}
astunparse = "^1.6.3"
coverage-enable-subprocess = "^1.0"
coverage = "^6.5.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.coverage.run]
source = ["tests","pysource_minimize"]
parallel = true
branch = true
data_file = "$TOP/.coverage"
47 changes: 25 additions & 22 deletions pysource_minimize/_minimize.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import ast
import contextlib
import copy
import os
import sys

from rich.console import Console

try:
from ast import unparse
except ImportError:
from astunparse import unparse

@contextlib.contextmanager
def no_output():
with open(os.devnull, "w") as f:
with contextlib.redirect_stdout(f):
with contextlib.redirect_stderr(f):
yield

py311 = sys.version_info >= (3, 11)
py310 = sys.version_info >= (3, 10)
py38 = sys.version_info >= (3, 8)

until_py37 = sys.version_info < (3, 8)


def is_block(nodes):
Expand Down Expand Up @@ -53,7 +56,7 @@ def __init__(self, source, checker, progress_callback):
console = Console()

def get_ast(self, node, replaced={}):
replaced = self.replaced | replaced
replaced = {**self.replaced, **replaced}

tmp_ast = copy.deepcopy(node)
node_map = {n.__index: n for n in ast.walk(tmp_ast)}
Expand Down Expand Up @@ -115,7 +118,7 @@ def map_node(node):
def get_source_tree(self, replaced):
tree = self.get_ast(self.original_ast, replaced)
ast.fix_missing_locations(tree)
return ast.unparse(tree), tree
return unparse(tree), tree

def get_source(self, replaced):
return self.get_source_tree(replaced)[0]
Expand All @@ -140,7 +143,7 @@ def try_with(self, replaced={}):
raise

if self.checker(source):
self.replaced = self.replaced | replaced
self.replaced.update(replaced)
self.progress_callback(self.nodes_of(tree), self.original_nodes_number)
return True

Expand Down Expand Up @@ -228,6 +231,12 @@ def minimize_expr(self, node):
pass
elif isinstance(node, ast.Constant):
pass
elif isinstance(node, ast.Index):
pass
elif until_py37 and isinstance(
node, (ast.Str, ast.Bytes, ast.Num, ast.NameConstant, ast.Ellipsis)
):
pass
elif isinstance(node, ast.Starred):
self.try_only_minimize(node, node.value)
elif isinstance(node, ast.Call):
Expand Down Expand Up @@ -280,14 +289,6 @@ def minimize_expr(self, node):
else:
raise TypeError(node) # Expr

for e in ast.iter_child_nodes(node):
if self.try_only(node, e):
self.minimize_expr(e)
return

for e in ast.iter_child_nodes(node):
self.minimize_expr(e)

def minimize_except_handler(self, handler):
self.minimize_list(handler.body, self.minimize_stmt)
if handler.type is not None:
Expand Down Expand Up @@ -320,7 +321,8 @@ def minimize_stmt(self, node):

nargs = node.args

self.minimize_list(nargs.posonlyargs, lambda e: None)
if py38:
self.minimize_list(nargs.posonlyargs, lambda e: None)
self.minimize_list(nargs.args, lambda e: None)
self.minimize_list(nargs.kwonlyargs, lambda e: None)
self.try_none(nargs.vararg)
Expand Down Expand Up @@ -384,13 +386,13 @@ def minimize_stmt(self, node):
return
self.minimize_list(node.items, lambda e: None, minimal=1)

elif isinstance(node, ast.Match):
elif py310 and isinstance(node, ast.Match):
pass # todo Match

elif isinstance(node, ast.Raise):
self.try_only_minimize(node, node.exc, node.cause)

elif isinstance(node, (ast.Try, ast.TryStar)):
elif isinstance(node, ast.Try) or (py311 and isinstance(node, ast.TryStar)):
if self.try_only(node, node.body):
self.minimize(node.body)
return
Expand Down Expand Up @@ -491,6 +493,7 @@ def minimize(source, checker, *, progress_callback=lambda current, total: None):
minimzes the source code
Args:
source: the source code to minimize
checker: a function which gets the source and returns `True` when the criteria is fullfilled.
progress_callback: function which is called everytime the source gets a bit smaller.
Expand Down
3 changes: 2 additions & 1 deletion tests/test_checker.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ast
import pathlib
import random
import sys
from collections import Counter

Expand All @@ -10,7 +11,7 @@

def files():
base_dir = pathlib.Path(sys.exec_prefix)
return base_dir.rglob("*.py")
return random.Random(5).sample(list(base_dir.rglob("*.py")), 10)


def gen_params():
Expand Down

0 comments on commit 2c64738

Please sign in to comment.