Commit 827c0bf1 authored by Grant Paton-Simpson's avatar Grant Paton-Simpson

Craft custom messages based on linter feedback; misc

* Combine duplicated linting messages
* Add messages of appropriate level to configured linter feedback
* Properly handle values we can't get via code execution
* Improve layout function so doesn't break lists
* Fix duplicate message in sorting advisor
* Fix layout of lists in comments
* Add documentation about need for raw strings as demo inputs to ast.parse (new line problem)
* Remove nose from requirements
* Bump version
parent b65bfd89
Pipeline #573 failed with stages
# https://git.nzoss.org.nz/pyGrant/superhelp
version number: 0.9.11
version number: 0.9.12
author: Grant Paton-Simpson
## Overview
......
......@@ -4,9 +4,8 @@ cssselect>=1.1.0
lxml>=4.5.0
Markdown>=3.2.1
flake8>=3.5
nose
Pygments>=2.6.1
PyYAML>=5.3.1
tabulate>=0.8.7
## numerous others because I installed cookiecutter and flake8 in same env - not needed for superhelp itself
## numerous others if I pip freeze because I installed cookiecutter and flake8 in same env - but not needed for SuperHELP itself
......@@ -2,7 +2,7 @@ from setuptools import setup, find_packages # @UnresolvedImport
from codecs import open
from os import path
__version__ = '0.9.11'
__version__ = '0.9.12'
here = path.abspath(path.dirname(__file__))
......
......@@ -21,13 +21,20 @@ def dict_overview(block_dets, *, repeat=False):
brief_desc_bits = []
for dict_el in dict_els:
name = get_assign_name(dict_el)
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
brief_desc_bits.append(layout(f"""\
try:
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
except KeyError:
brief_desc_bits.append(layout(f"""\
`{name}` is a dictionary with {utils.int2nice(len(items))} items
(i.e. {utils.int2nice(len(items))} mappings).
"""))
`{name}` is a dictionary. Unable to evaluate items.
"""))
else:
brief_desc_bits.append(layout(f"""\
`{name}` is a dictionary with {utils.int2nice(len(items))} items
(i.e. {utils.int2nice(len(items))} mappings).
"""))
brief_desc = ''.join(brief_desc_bits)
if not repeat:
dict_def = layout("""\
......@@ -51,28 +58,36 @@ def dict_overview(block_dets, *, repeat=False):
name = get_assign_name(dict_el)
if i == 0:
first_name = name
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
empty_dict = (len(items) == 0)
if empty_dict:
try:
items = code_execution.get_val(block_dets.pre_block_code_str,
block_dets.block_code_str, name)
except KeyError:
dict_desc_bits.append(layout(f"""\
`{name}` is an empty dictionary.
`{name}` is a dictionary. Unable to evaluate items.
"""))
else:
plural = '' if len(items) == 1 else 's'
dict_desc_bits.append(layout(f"""\
`{name}` is a dictionary with {utils.int2nice(len(items))}
item{plural} (i.e. {utils.int2nice(len(items))}
mapping{plural}). In this case, the keys are:
{list(items.keys())}. We can get the keys using the `.keys()`
method e.g. `{name}`.`keys()`. The values are
{list(items.values())}. We can get the values using the
`.values()` method e.g. `{name}`.`values()`.
"""))
empty_dict = (len(items) == 0)
if empty_dict:
dict_desc_bits.append(layout(f"""\
`{name}` is an empty dictionary.
"""))
else:
plural = '' if len(items) == 1 else 's'
dict_desc_bits.append(layout(f"""\
`{name}` is a dictionary with
{utils.int2nice(len(items))} item{plural} (i.e.
{utils.int2nice(len(items))} mapping{plural}). In this
case, the keys are: {list(items.keys())}. We can get the
keys using the `.keys()` method e.g. `{name}`.`keys()`.
The values are {list(items.values())}. We can get the
values using the `.values()` method e.g.
`{name}`.`values()`.
"""))
main_dict_desc = ''.join(dict_desc_bits)
general = (
layout(f"""
......@@ -159,8 +174,11 @@ def mixed_key_types(block_dets, *, repeat=False):
mixed_names = []
for dict_el in dict_els:
name = get_assign_name(dict_el)
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
try:
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
except KeyError:
continue
key_type_names, _key_type_nice_names = get_key_type_names(items)
bad_key_type_combo = (
conf.INT_TYPE in key_type_names and conf.STR_TYPE in key_type_names)
......
This diff is collapsed.
......@@ -30,12 +30,12 @@ def get_type_for_example(list_items):
type4example = DEFAULT_EXAMPLE_TYPE
return type4example
def _get_detailed_list_comment(first_name, first_list_items):
def _get_detailed_list_comment(first_name, first_items):
"""
:param list first_list_items: note - may be None if a list is defined in a
:param list first_items: note - may be None if a list is defined in a
function
"""
type4example = get_type_for_example(first_list_items)
type4example = get_type_for_example(first_items)
try:
example_items = conf.EXAMPLES_OF_TYPES[type4example]
except KeyError:
......@@ -43,10 +43,10 @@ def _get_detailed_list_comment(first_name, first_list_items):
example_item = example_items[0]
listable_example_item = (f"'{example_item}'"
if type4example == conf.STR_TYPE else example_item)
if first_list_items is None:
if first_items is None:
appended_list = []
else:
appended_list = first_list_items.copy()
appended_list = first_items.copy()
appended_list.append(example_item)
extended_list = appended_list.copy()
items2extend = example_items[1:3] ## don't repeat the appended item - might confuse the user at this stage
......@@ -156,37 +156,37 @@ def list_overview(block_dets, *, repeat=False):
"""
list_els = block_dets.element.xpath(ASSIGN_LIST_XPATH)
plural = 's' if len(list_els) > 1 else ''
first_name = None
first_list_items = None
title = layout(f"""\
### List{plural} defined
""")
for i, list_el in enumerate(list_els):
first = (i == 0)
first_name = None
first_items = None
for list_el in list_els:
name = get_assign_name(list_el)
items = code_execution.get_val( ## might be None if defined in a function
block_dets.pre_block_code_str, block_dets.block_code_str, name)
if first:
first_name = name
first_list_items = items
if items is None:
list_desc = layout(f"""\
`{name}` is a list.
""")
elif items == []:
try:
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
except KeyError:
list_desc = layout(f"""\
`{name}` is an empty list.
""")
`{name}` is a list.
""")
else:
list_desc = layout(f"""\
`{name}` is a list with {utils.int2nice(len(items))} items.
""")
if not first_name:
first_name = name
first_items = items
if items == []:
list_desc = layout(f"""\
`{name}` is an empty list.
""")
else:
list_desc = layout(f"""\
`{name}` is a list with {utils.int2nice(len(items))} items.
""")
if not repeat:
brief_overview = layout("""\
......@@ -197,7 +197,7 @@ def list_overview(block_dets, *, repeat=False):
different types (usually not advisable).
""")
detailed_list_comment = _get_detailed_list_comment(
first_name, first_list_items)
first_name, first_items)
else:
brief_overview = ''
detailed_list_comment = ''
......@@ -218,16 +218,18 @@ def mixed_list_types(block_dets, *, repeat=False): # @UnusedVariable
has_mixed = False
for list_el in list_els:
name = get_assign_name(list_el)
items = code_execution.get_val( ## might be None if defined in a function
block_dets.pre_block_code_str, block_dets.block_code_str, name)
if not items:
continue
_item_type_names, item_type_nice_names = get_item_type_names(items)
if len(item_type_nice_names) <= 1:
## No explanation needed if there aren't multiple types.
try:
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
except KeyError:
continue
has_mixed = True
list_dets.append((name, item_type_nice_names))
else:
_item_type_names, item_type_nice_names = get_item_type_names(items)
if len(item_type_nice_names) <= 1:
## No explanation needed if there aren't multiple types.
continue
has_mixed = True
list_dets.append((name, item_type_nice_names))
if not has_mixed:
return None
......
......@@ -17,8 +17,11 @@ def listcomp_overview(block_dets, *, repeat=False):
listcomp_dets = []
for listcomp_el in listcomp_els:
name = get_assign_name(listcomp_el)
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
try:
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
except KeyError:
items = None
listcomp_dets.append((name, items))
if not listcomp_dets:
return None
......@@ -31,15 +34,22 @@ def listcomp_overview(block_dets, *, repeat=False):
""")
summary_bits = []
for name, items in listcomp_dets:
summary_bits.append(layout(f"""
if items is None:
summary_bits.append(layout(f"""
`{name}` is a list comprehension returning a list
with {utils.int2nice(len(items))} items: {items}
"""))
`{name}` is a list comprehension. Unable to get items.
"""))
else:
summary_bits.append(layout(f"""
`{name}` is a list comprehension returning a list
with {utils.int2nice(len(items))} items: {items}
"""))
summary = ''.join(summary_bits)
if not repeat:
other_comprehensions = (
layout(f"""\
### Other "comprehensions"
""")
......
......@@ -286,8 +286,8 @@ def short_name_check(block_dets, *, repeat=False):
In the words of the legendary Donald Knuth:
> "Programs are meant to be read by humans
> and only incidentally for computers to execute."
> "Programs are meant to be read by humans and only incidentally
for computers to execute."
Note - excessively long variable names can be unreadable as
well. They may also indicate the code needs to be reworked.
......
......@@ -43,13 +43,17 @@ def num_overview(block_dets, *, repeat=False):
type_firsts = {}
for num_el in num_els:
name = get_assign_name(num_el)
val = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
val_type = type(val).__name__
val_types[val_type].append(name)
if not type_firsts.get(val_type):
type_firsts[val_type] = val
has_num = True
try:
val = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
except KeyError:
continue
else:
val_type = type(val).__name__
val_types[val_type].append(name)
if not type_firsts.get(val_type):
type_firsts[val_type] = val
has_num = True
if not has_num:
return None
......
......@@ -11,7 +11,6 @@ def set_overview(block_dets, *, repeat=False):
Look for sets and provide general advice on using them and finding out more.
"""
func_type_els = block_dets.element.xpath(ASSIGN_FUNC_NAME_XPATH)
has_set = False
name_sets = []
for func_type_el in func_type_els:
try:
......@@ -20,12 +19,15 @@ def set_overview(block_dets, *, repeat=False):
is_set = False
if not is_set:
continue
has_set = True
name = get_assign_name(func_type_el)
my_set = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
name_sets.append((name, my_set))
if not has_set:
try:
my_set = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
except KeyError:
continue
else:
name_sets.append((name, my_set))
if not name_sets:
return None
title = layout("""\
......
......@@ -46,12 +46,12 @@ def sorting_reversing_overview(block_dets, *, repeat=False):
### Sorting / reversing
""")
minimal = layout(f"""\
This block of code {sorting_or_reversing_comment}.
""")
if not repeat:
summary = layout(f"""\
This block of code {sorting_or_reversing_comment}.
Sorting and, to a lesser extent, reversing are very common needs in
programming. Two key points:
......@@ -134,8 +134,8 @@ def sorting_reversing_overview(block_dets, *, repeat=False):
details = summary
message = {
conf.BRIEF: title + minimal + summary,
conf.MAIN: title + minimal + details,
conf.BRIEF: title + summary,
conf.MAIN: title + details,
}
return message
......
......@@ -93,14 +93,15 @@ def assigned_str_overview(block_dets, *, repeat=False):
first_str_el = str_els[0]
first_assign_el = first_str_el.xpath('ancestor::Assign')[0]
first_name = first_assign_el.xpath('targets/Name')[0].get('id')
first_val = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, first_name)
if first_val:
name2use = first_name
val2use = first_val
else:
try:
first_val = code_execution.get_val(block_dets.pre_block_code_str,
block_dets.block_code_str, first_name)
except KeyError:
name2use = 'address'
val2use = 'Waiuku, New Zealand'
else:
name2use = first_name
val2use = first_val
short_demo = layout(f"""\
For illustration, imagine we have string '{val2use}' assigned to
`{name2use}`:
......
......@@ -16,9 +16,13 @@ def tuple_overview(block_dets, *, repeat=False):
name = get_assign_name(tup_el)
if name is None: ## no names - e.g. just assignment to other tuples
continue
tup = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
name_tups.append((name, tup))
try:
tup = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
except KeyError:
continue
else:
name_tups.append((name, tup))
if not name_tups:
return None
......@@ -80,12 +84,13 @@ def tuple_overview(block_dets, *, repeat=False):
longer_immutability = layout(f"""
{immutability_question}
* cannot be *replaced* -
so we can't run `{first_name}`[0] = 100 to get {tup_replaced}.
It will raise an exception -
TypeError: 'tuple' object does not support item assignment)
* cannot be *removed* -
so we can't run `{first_name}`.pop() to get {tup_popped}.
* cannot be *replaced* - so we can't run `{first_name}`[0] = 100
to get {tup_replaced}. It will raise an exception - TypeError:
'tuple' object does not support item assignment)
* cannot be *removed* - so we can't run `{first_name}`.pop() to
get {tup_popped}.
* cannot be *added* - so we can't run `{name}`.append(3) to
get {tup_appended}.
""")
......@@ -95,7 +100,9 @@ def tuple_overview(block_dets, *, repeat=False):
* cannot be *replaced*. It will raise an exception - TypeError:
'tuple' object does not support item assignment)
* cannot be *removed*
* cannot be *added*
""")
friends = ['Selma', 'Willy', 'Principal Skinner']
......
......@@ -8,6 +8,8 @@ def get_val(pre_block_code_str, block_code_str, name):
Note - can be the source of mysterious output in stdout (e.g. exec a print
function).
Raises KeyError if unable to get value.
"""
exp_dets = {}
try:
......@@ -21,7 +23,6 @@ def get_val(pre_block_code_str, block_code_str, name):
try:
val = exp_dets[name]
except KeyError:
val = None
# raise KeyError(
# f"Unable to find name '{name}' in code_str\n{block_code_str}")
raise KeyError(
f"Unable to find name '{name}' in code_str\n{block_code_str}")
return val
from collections import namedtuple
import datetime
import logging
......@@ -25,24 +26,21 @@ else:
RECORD_AST = True
## When testing user-supplied snippets watch out for the BOM MS inserts via Notepad. AST chokes on it.
## All snippets here should be raw strings (see https://stackoverflow.com/questions/53636723/python-parsing-code-with-new-line-character-in-them-using-ast)
TEST_SNIPPET = """\
try:
from .. import conf # @UnresolvedImport @UnusedImport
## 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 . import advisors, messages # @UnusedImport
from .displayers import cli_displayer, html_displayer # @UnusedImport
except (ImportError, ValueError):
from pathlib import Path
import sys
parent = str(Path.cwd().parent)
sys.path.insert(0, parent)
from superhelp import conf, advisors, messages # @Reimport
from superhelp.displayers import cli_displayer, html_displayer # @Reimport
TEST_SNIPPET = r"""
def sorted(*G, **kwargs):
for i in range(len(G)):
for j in range(1,len(G)):
if G[j-1]<G[j]:
G[j-1],G[j]=G[j],G[j-1]
G = [['Ahmad', 3.8], ['Rizwan', 3.68], ['Bilal', 3.9]]
sorted(G)
print(G)
"""
DEMO_SNIPPET = """\
DEMO_SNIPPET = r"""
import datetime
from math import pi as π
mixed_keys = {1: 'cat', '1': 'dog'}
......@@ -259,8 +257,44 @@ LINT_LINE_NO = 'line_no'
## https://pycodestyle.pycqa.org/en/latest/intro.html#error-codes
## https://flake8.pycqa.org/en/latest/user/error-codes.html
IGNORED_LINT_RULES = [
'E128', ## flake8 wants visual alignment on line continuation - I don't - I want standardised alignment on indent multiples of 4
'E128', ## flake8 wants visual alignment on line continuation - I don't - I want standardised alignment on indent multiples of 4 (idea taken from Brandon Rhodes thanks Brandon!)
'E266', 'E262', ## I like ## before comments and # before commented out code (idea copied off Tom Eastman - thanks Tom!)
'E305', ## for classes I agree with 2 spaces but not functions
]
LintMsgs = namedtuple('LintMsgs', 'brief, main, extra, replacement')
CUSTOM_LINT_MSGS = {
'E501': LintMsgs(
"""\
One or more lines are longer than the recommended 79 characters. This is
not necessarily a problem but long lines should be an exception to the
rule
""",
"""\
One or more lines are longer than the recommended 79 characters. This is
not necessarily a problem given that we have wider monitors than when
the guidelines were formulated. But long lines should be an exception to
the rule. All being equal, short lines are easier to read and understand
than long lines. There are multiple strategies for shortening lines but
the overall goal has to be readability. Sometimes we have to live with
broken "rules". And that's official. Read PEP 8 - the official Python
style guide - especially the section "A Foolish Consistency is the
Hobgoblin of Little Minds".
""",
'',
True),
'F401': LintMsgs(
"""\
One or more imports not used in snippet.
""",
"""\
One or more imports not used in snippet. If the snippet was extracted
from a larger piece of code and the imports are used in that code then
there is no problem.
""",
'',
False
)
}
LINE_FEED = '&#10;'
......
......@@ -2,12 +2,21 @@ import logging
from os import rename
from pathlib import Path
import platform
import re
import sys
import tempfile
from textwrap import dedent, wrap
from . import conf
starting_num_space_pattern = r"""(?x)
^ ## start
\d{1} ## one digit
\s{1} ## one whitespace
\S+ ## at least one non-whitespace
"""
starting_num_space_prog = re.compile(starting_num_space_pattern)
def get_os_platform():
platforms = {
'Linux': conf.LINUX, 'Windows': conf.WINDOWS, 'Darwin': conf.MAC}
......@@ -73,6 +82,8 @@ def layout_comment(raw_comment, *, is_code=False):
"""
Don't break up long lines which are titles otherwise the subsequent parts
won't carry appropriate heading-level formatting.
:rtype: str
"""
if '`' in raw_comment and is_code:
logging.debug("Backtick detected in code which is probably a mistake")
......@@ -96,7 +107,16 @@ def layout_comment(raw_comment, *, is_code=False):
len(raw_paragraph) - len(raw_paragraph.rstrip('\n')))
paragraph = raw_paragraph.strip()
one_line_paragraph = paragraph.replace('\n', ' ') ## actually continuations of same line so no need to put on separate lines
if one_line_paragraph.startswith('#'):
special_line = False
res = starting_num_space_prog.match(one_line_paragraph)
if res is not None:
special_line = True
if one_line_paragraph.startswith((
'#', ## could be one hash or multiple depending on heading level
'* ' ## trying to detect bulleted (unordered) lists
)):
special_line = True
if special_line:
wrapped_paragraph_lines = [one_line_paragraph, ]
else:
wrapped_paragraph_lines = wrap(one_line_paragraph)
......
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