Commit aa150180 authored by Grant Paton-Simpson's avatar Grant Paton-Simpson

Handle changes 3.8 makes to AST

parent 8165f74d
Pipeline #551 canceled with stages
# https://git.nzoss.org.nz/pyGrant/superhelp
version number: 0.1.12
version number: 0.1.13
author: Grant Paton-Simpson
## Overview
......
......@@ -2,7 +2,7 @@ from setuptools import setup, find_packages # @UnresolvedImport
from codecs import open
from os import path
__version__ = '0.1.12'
__version__ = '0.1.13'
here = path.abspath(path.dirname(__file__))
......
This diff is collapsed.
......@@ -4,7 +4,57 @@ Covers functions and methods.
from ..advisors import filt_block_advisor
from ..ast_funcs import get_el_lines_dets
from .. import conf, utils
from ..utils import layout_comment as layout
from ..utils import get_python_version, layout_comment as layout
def get_danger_status_3_7(child_el):
if (child_el.tag == 'NameConstant'
and child_el.get('value') in ['True', 'False']):
danger_status = 'Boolean'
elif child_el.tag == 'Num' and child_el.get('n'):
danger_status = 'Number'
else:
danger_status = None
return danger_status
def get_danger_status_3_8(child_el):
if child_el.tag == 'Constant':
val = child_el.get('value')
if val in ['True', 'False']:
danger_status = 'Boolean'
else:
try:
float(val)
except TypeError:
danger_status = None
else:
danger_status = 'Number'
else:
danger_status = None
return danger_status
def get_docstring_from_value_3_7(first_value_el):
if first_value_el.tag != 'Str':
docstring = None
else:
docstring = first_value_el.get('s')
return docstring
def get_docstring_from_value_3_8(first_value_el):
if first_value_el.tag != 'Constant':
docstring = None
else:
docstring = first_value_el.get('value')
return docstring
python_version = get_python_version()
if python_version in (conf.PY3_6, conf.PY3_7):
get_danger_status = get_danger_status_3_7
get_docstring_from_value = get_docstring_from_value_3_7
elif python_version == conf.PY3_8:
get_danger_status = get_danger_status_3_8
get_docstring_from_value = get_docstring_from_value_3_8
else:
raise Exception(f"Unexpected Python version {python_version}")
FUNC_DEFN_XPATH = 'descendant-or-self::FunctionDef'
......@@ -234,8 +284,9 @@ def func_len_check(block_dets, *, repeated_message=False):
def get_n_args(func_el):
arg_els = func_el.xpath('args/arguments/args/arg')
posonlyarg_els = func_el.xpath('args/arguments/posonlyargs/arg')
kwonlyarg_els = func_el.xpath('args/arguments/kwonlyargs/arg')
n_args = len(arg_els + kwonlyarg_els)
n_args = len(arg_els + posonlyarg_els + kwonlyarg_els)
return n_args
@filt_block_advisor(xpath=FUNC_DEFN_XPATH, warning=True)
......@@ -285,13 +336,7 @@ def get_danger_args(func_el):
danger_statuses = []
for arg_default_el in arg_default_els:
for child_el in arg_default_el.getchildren():
if (child_el.tag == 'NameConstant'
and child_el.get('value') in ['True', 'False']):
danger_status = 'Boolean'
elif child_el.tag == 'Num' and child_el.get('n'):
danger_status = 'Number'
else:
danger_status = None
danger_status = get_danger_status(child_el)
danger_statuses.append(danger_status)
## reversed because defaults are filled in rightwards e.g. a, b=1, c=2
## args = a,b,c and defaults=1,2 -> reversed c,b,a and 2,1 -> c: 2, b: 1
......@@ -406,10 +451,7 @@ def get_func_name_docstring(func_el):
docstring = None
else:
first_value_el = value_els[0]
if first_value_el.tag != 'Str':
docstring = None
else:
docstring = first_value_el.get('s')
docstring = get_docstring_from_value(first_value_el)
return func_name, docstring
def get_funcs_dets_and_docstring(func_els):
......
......@@ -2,7 +2,41 @@ from collections import defaultdict, namedtuple, Counter
from ..advisors import filt_block_advisor
from .. import conf
from ..utils import int2nice, layout_comment as layout
from ..utils import get_python_version, int2nice, layout_comment as layout
def get_string_3_7(comparison_el):
string = comparison_el.get('s')
return string
def get_string_3_8(comparison_el):
val = comparison_el.get('value')
if comparison_el.get('type') == 'str':
string = val
else:
string = None
return string
def get_num_3_7(comparison_el):
num = comparison_el['n']
return num
def get_num_3_8(comparison_el):
val = comparison_el.get('value')
if comparison_el.get('type') in ('int', 'float'):
num = val
else:
num = None
return num
python_version = get_python_version()
if python_version in (conf.PY3_6, conf.PY3_7):
get_string = get_string_3_7
get_num = get_num_3_7
elif python_version == conf.PY3_8:
get_string = get_string_3_8
get_num = get_num_3_8
else:
raise Exception(f"Unexpected Python version {python_version}")
IfDets = namedtuple('IfDetails',
'multiple_conditions, missing_else, if_clauses')
......@@ -271,7 +305,7 @@ def get_split_membership_dets(if_el):
If/test/BoolOp/values/Compare
left/Name id 'x'
comparators/Str s 'a'
comparators/Constant s 'a'
comparators/Num n etc
Only provide message using content if all items are of the same type and are
......@@ -297,13 +331,13 @@ def get_split_membership_dets(if_el):
if not comparison_els:
continue
comparison_el = comparison_els[0] ## Str or Num etc
comp_val = comparison_el.get('s')
comp_val = get_string(comparison_el)
if comp_val is not None:
basic_types.add(STRING)
if len(basic_types) > 1:
return None
else:
comp_val = comparison_el['n']
comp_val = get_num(comparison_el)
if comp_val is not None:
basic_types.add(NUMBER)
if len(basic_types) > 1:
......@@ -419,7 +453,7 @@ def get_has_explicit_count(if_el):
if not comparison_els:
return False
comparison_el = comparison_els[0]
n = comparison_el.get('n')
n = get_num(comparison_el)
if n is None:
return False
explicit_booleans = [
......
......@@ -3,16 +3,41 @@ from collections import defaultdict
from ..advisors import filt_block_advisor
from ..ast_funcs import get_assign_name
from .. import code_execution, conf
from ..utils import get_nice_str_list, layout_comment as layout
from ..utils import get_nice_str_list, get_python_version,\
layout_comment as layout
ASSIGN_NUM_XPATH = 'descendant-or-self::Assign/value/Num'
ASSIGN_VAL_XPATH = 'descendant-or-self::Assign/value'
@filt_block_advisor(xpath=ASSIGN_NUM_XPATH)
def get_num_els_3_7(block_el):
num_els = block_el.xpath('descendant-or-self::Assign/value/Num')
return num_els
def get_num_els_3_8(block_el):
val_els = block_el.xpath(ASSIGN_VAL_XPATH)
num_els = []
for val_el in val_els:
constant_els = val_el.xpath('Constant')
if len(constant_els) != 1:
continue
constant_el = constant_els[0]
if constant_el.get('type') in ('int', 'float'):
num_els.append(constant_el)
return num_els
python_version = get_python_version()
if python_version in (conf.PY3_6, conf.PY3_7):
get_num_els = get_num_els_3_7
elif python_version == conf.PY3_8:
get_num_els = get_num_els_3_8
else:
raise Exception(f"Unexpected Python version {python_version}")
@filt_block_advisor(xpath=ASSIGN_VAL_XPATH)
def num_overview(block_dets, *, repeated_message=False):
"""
Get general advice about assigned numbers e.g. var = 123
"""
num_els = block_dets.element.xpath(ASSIGN_NUM_XPATH)
num_els = get_num_els(block_dets.element)
val_types = defaultdict(list)
has_num = False
type_firsts = {}
......
......@@ -2,7 +2,30 @@ from collections import defaultdict
from ..advisors import shared, snippet_advisor, filt_block_advisor
from .. import conf, utils
from ..utils import layout_comment as layout
from ..utils import get_python_version, layout_comment as layout
def get_slice_n_3_7(assign_el):
slice_n = assign_el.xpath(
'value/Subscript/slice/Index/value/Num')[0].get('n')
return slice_n
def get_slice_n_3_8(assign_el):
val_els = assign_el.xpath('value/Subscript/slice/Index/value/Constant')
val_el = val_els[0]
if val_el.get('type') not in ('int', 'float'):
slice_n = val_el.get('value')
else:
raise TypeError("slice index value not an int or a float - actual type "
f"'{val_el.get('type')}'")
return slice_n
python_version = get_python_version()
if python_version in (conf.PY3_6, conf.PY3_7):
get_slice_n = get_slice_n_3_7
elif python_version == conf.PY3_8:
get_slice_n = get_slice_n_3_8
else:
raise Exception(f"Unexpected Python version {python_version}")
ASSIGN_UNPACKING_XPATH = 'descendant-or-self::Assign/targets/Tuple'
......@@ -52,8 +75,7 @@ def unpacking_opportunity(blocks_dets):
try:
slice_source = assign_el.xpath(
'value/Subscript/value/Name')[0].get('id')
slice_n = assign_el.xpath(
'value/Subscript/slice/Index/value/Num')[0].get('n')
slice_n = get_slice_n(assign_el)
except IndexError:
continue
else:
......
from ..advisors import any_block_advisor, filt_block_advisor
from .. import code_execution, conf
from ..utils import layout_comment as layout
from ..utils import get_python_version, layout_comment as layout
F_STR = 'f-string'
STR_FORMAT_FUNC = 'str_format'
SPRINTF = 'sprintf'
STR_ADDITION = 'string addition'
ASSIGN_STR_XPATH = 'descendant-or-self::Assign/value/Str'
ASSIGN_VALUE_XPATH = 'descendant-or-self::Assign/value'
FUNC_ATTR_XPATH = 'descendant-or-self::value/Call/func/Attribute'
JOINED_STR_XPATH = 'descendant-or-self::Assign/value/JoinedStr'
SPRINTF_XPATH = 'descendant-or-self::value/BinOp/op/Mod'
......@@ -15,13 +15,54 @@ STR_ADDITION_XPATH = 'descendant-or-self::BinOp/left/Str' ## each left has a ri
F_STR_REMINDER = False
@filt_block_advisor(xpath=ASSIGN_STR_XPATH)
def get_str_els_3_7(block_el):
str_els = block_el.xpath('descendant-or-self::Assign/value/Str')
return str_els
def get_str_els_3_8(block_el):
assign_val_els = block_el.xpath(ASSIGN_VALUE_XPATH)
str_els = []
for assign_val_el in assign_val_els:
assign_str_els = assign_val_el.xpath('Constant')
if len(assign_str_els) != 1:
continue
assign_str_el = assign_str_els[0]
if assign_str_el.get('type') == 'str':
str_els.append(assign_str_el)
return str_els
def get_str_els_being_combined_3_7(block_el):
str_els_being_combined = block_el.xpath(
'descendant-or-self::BinOp/left/Str')
return str_els_being_combined
def get_str_els_being_combined_3_8(block_el):
left_str_els = block_el.xpath('descendant-or-self::BinOp/left/Constant')
str_els_being_combined = []
for left_str_el in left_str_els:
if left_str_el.get('type') == 'str':
str_els_being_combined.append(left_str_el)
return str_els_being_combined
python_version = get_python_version()
if python_version in (conf.PY3_6, conf.PY3_7):
get_str_els = get_str_els_3_7
get_str_els_being_combined = get_str_els_being_combined_3_7
elif python_version == conf.PY3_8:
get_str_els = get_str_els_3_8
get_str_els_being_combined = get_str_els_being_combined_3_8
else:
raise Exception(f"Unexpected Python version {python_version}")
@filt_block_advisor(xpath=ASSIGN_VALUE_XPATH)
def assigned_str_overview(block_dets, *, repeated_message=False):
"""
Provide overview of assigned strings e.g. name = 'Hamish'.
"""
brief_comment = ''
str_els = block_dets.element.xpath(ASSIGN_STR_XPATH)
str_els = get_str_els(block_dets.element)
if not str_els:
return None
first_name = None
first_val = None
for i, str_el in enumerate(str_els):
......@@ -240,7 +281,7 @@ def string_addition(block_dets, *, repeated_message=False):
Advise on string combination using +. Explain how f-string alternative
works.
"""
str_els_being_combined = block_dets.element.xpath(STR_ADDITION_XPATH)
str_els_being_combined = get_str_els_being_combined(block_dets.element)
has_string_addition = False
str_addition_els = []
for str_el in str_els_being_combined:
......
......@@ -165,6 +165,10 @@ def camelCase(a, b, c, d, f, *, g):
pass
"""
PY3_6 = '3.6'
PY3_7 = '3.7'
PY3_8 = '3.8'
AST_OUTPUT_XML = Path(__file__).parent / 'ast_output.xml'
PYTHON_CODE_START = '__python_code_start__'
......
import ast
from collections import namedtuple
from functools import partial
import logging
from lxml import etree
import astpath # @UnresolvedImport
from astpath.asts import _set_encoded_literal, _strip_docstring
## Monkey-patch as at astpath Python 3.8 as at 2020-04-26
## Need to be able to tell val = 1 from val = '1' (that little detail ;-))
def convert_to_xml(node, omit_docstrings=False, node_mappings=None):
"""Convert supplied AST node to XML."""
possible_docstring = isinstance(node, (ast.FunctionDef, ast.ClassDef, ast.Module))
xml_node = etree.Element(node.__class__.__name__)
for attr in ('lineno', 'col_offset'):
value = getattr(node, attr, None)
if value is not None:
_set_encoded_literal(
partial(xml_node.set, attr),
value
)
if node_mappings is not None:
node_mappings[xml_node] = node
node_fields = zip(
node._fields,
(getattr(node, attr) for attr in node._fields)
)
for field_name, field_value in node_fields:
if isinstance(field_value, ast.AST):
field = etree.SubElement(xml_node, field_name)
field.append(
convert_to_xml(
field_value,
omit_docstrings,
node_mappings,
)
)
elif isinstance(field_value, list):
field = etree.SubElement(xml_node, field_name)
if possible_docstring and omit_docstrings and field_name == 'body':
field_value = _strip_docstring(field_value)
for item in field_value:
if isinstance(item, ast.AST):
field.append(
convert_to_xml(
item,
omit_docstrings,
node_mappings,
)
)
else:
subfield = etree.SubElement(field, 'item')
_set_encoded_literal(
partial(setattr, subfield, 'text'),
item
)
elif field_value is not None:
## The only change is this immediate function call below
## add type attribute e.g. so we can distinguish strings from numbers etc
## <Constant lineno="1" col_offset="6" type="int" value="1"/>
_set_encoded_literal(
partial(xml_node.set, 'type'),
type(field_value).__name__
)
_set_encoded_literal(
partial(xml_node.set, field_name),
field_value
)
return xml_node
astpath.asts.convert_to_xml = convert_to_xml
## importing from superhelp only works properly after I've installed superhelp as a pip package (albeit as a link to this code using python3 -m pip install --user -e <path_to_proj_folder>)
## Using this as a library etc works with . instead of superhelp but I want to be be able to run the helper module from within my IDE
from superhelp import advisors, ast_funcs, conf # @UnresolvedImport
......
import logging
import sys
from textwrap import dedent
from . import conf
def get_python_version():
major, minor = sys.version_info[:2]
return f"{major}.{minor}"
def get_nice_str_list(items, *, quoter='`'):
"""
Get a nice English phrase listing the items.
......
......@@ -155,7 +155,7 @@ def test_misc():
class Demo:
def demo(self):
'''
A doc string of some sort ;-)
A short doc string only
'''
return True
"""),
......@@ -164,7 +164,7 @@ def test_misc():
ROOT + 'func_len_check': 0,
ROOT + 'func_excess_parameters': 0,
ROOT + 'positional_boolean': 0,
ROOT + 'docstring_issues': 0,
ROOT + 'docstring_issues': 1,
}
),
]
......
......@@ -42,6 +42,18 @@ def test_misc():
ROOT + 'string_addition': 1,
}
),
(
dedent("""\
pet = 'jelly' + 'cat' + 'fish'
"""),
{
ROOT + 'assigned_str_overview': 0, ## only want to cover string combination / interpolation
ROOT + 'f_str_interpolation': 0,
ROOT + 'format_str_interpolation': 0,
ROOT + 'sprintf': 0,
ROOT + 'string_addition': 1,
}
),
(
dedent("""\
pet = '%s%s' % ('jelly', 'fish')
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment