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

Add initial class advisor; warning style wraps extra as well

parent 70b68c4c
Pipeline #554 failed with stages
## Packaging
upload-package:
git:
sed -i 's/RECORD_AST = t/RECORD_AST = f/' /home/g/projects/superhelp/superhelp/conf.py
sed -i 's/DEV_MODE = t/DEV_MODE = f/' /home/g/projects/superhelp/superhelp/conf.py
sed -i 's/DO_TEST = f/DO_TEST = t/' /home/g/projects/superhelp/superhelp/conf.py
sed -i 's/DO_HTML = f/DO_HTML = t/' /home/g/projects/superhelp/superhelp/conf.py
sed -i 's/DO_DISPLAYER = f/DO_DISPLAYER = t/' /home/g/projects/superhelp/superhelp/conf.py
git status
upload:
rm -f dist/*
python3 setup.py sdist bdist_wheel
python3 -m twine upload dist/*
"""
Method advisors are effectively function advisors and are covered there.
"""
from ..advisors import filt_block_advisor
from .. import conf
from ..utils import layout_comment as layout
CLASS_XPATH = ('descendant-or-self::ClassDef')
@filt_block_advisor(xpath=CLASS_XPATH, warning=True)
def one_method_classes(block_dets, *, repeated_message=False):
"""
Look for classes with only one method (other than __init__) and suggest a
simple function as an alternative..
"""
class_els = block_dets.element.xpath(CLASS_XPATH)
if not class_els:
return None
classes_sole_methods = []
for class_el in class_els:
method_els = class_el.xpath('body/FunctionDef')
method_names = [method_el.get('name') for method_el in method_els]
non_init_method_names = [method_name for method_name in method_names
if method_name != '__init__']
n_non_init_methods = len(non_init_method_names)
if n_non_init_methods < 2:
class_name = class_el.get('name')
try:
sole_method_name = non_init_method_names.pop()
except IndexError:
sole_method_name = None
classes_sole_methods.append((class_name, sole_method_name))
if not classes_sole_methods:
return None
multi_sole = len(classes_sole_methods) > 1
class_plural = 'es' if multi_sole else ''
class_have_has = 'have' if multi_sole else 'has'
func_plural = 's' if multi_sole else ''
brief_comment = layout(f"""\
### Possible option of converting class{class_plural} to single function{func_plural}
The following class{class_plural} only {class_have_has} one main
function at most (excluding `__init__`):
""")
for class_name, method_name in classes_sole_methods:
method2use = (
f"`{method_name}`" if method_name else 'nothing but `__init__`')
brief_comment += layout(f"""\
- {class_name}: {method2use}
""")
if repeated_message:
extra_comment = ''
else:
brief_comment += (
layout(f"""\
It may be simpler to replace the class{class_plural} with simple
functions e.g. we could replace:
""")
+
layout('''\
class GetSquare:
def __init__(self, num):
self.num = num
def get_square(self):
"""
Get square of number
"""
return self.num ** 2
''', is_code=True)
+
layout(f"""\
with:
""")
+
layout('''\
def get_square(num):
"""
Get square of number ...
"""
return self.num ** 2
''', is_code=True)
+
layout(f"""\
Sometimes, less is more. Bugs need somewhere to hide and
unnecessarily verbose code gives them plenty of room to hide.
""")
)
extra_comment = layout("""\
Python allows procedural, object-oriented, and functional styles of
programming. Event-based programming is also used in GUI contexts,
for example. Programmers coming to Python from languages that only
support object-orientation sometimes overdo the classes when there
is a simpler, more elegant way of writing readable code in Python.
If only a simple function is required, then write a simple function.
Note - there may be exceptions. It has been suggested that the class
structure can make it easier to test intermediate state rather than
just function outputs. So, as with most things, it depends.
""")
message = {
conf.BRIEF: brief_comment,
conf.EXTRA: extra_comment,
}
return message
......@@ -38,8 +38,8 @@ class Onsie:
pass
def one(self):
pass
def two(self):
pass
# def two(self):
# pass
"""
DEMO_SNIPPET = """\
......
......@@ -508,9 +508,8 @@ def get_separate_code_message_parts(message):
def get_html_strs(message, message_type, *, warning=False):
if not message:
return []
div_class = MESSAGE_LEVEL2CLASS[message_type]
warning_class = ' warning' if warning else ''
str_html_list = [f"<div class='{div_class}{warning_class}'>", ]
message_type_class = MESSAGE_LEVEL2CLASS[message_type]
str_html_list = [f"<div class='{message_type_class}'>", ]
message_parts = get_separate_code_message_parts(message)
for message_part in message_parts:
if message_part[IS_CODE]:
......@@ -528,6 +527,8 @@ def get_message_html_strs(message_dets):
Process message.
"""
message_html_strs = []
if message_dets.warning:
message_html_strs.append("<div class='warning'>")
for message_level in conf.MESSAGE_LEVELS:
try:
message = message_dets.message[message_level]
......@@ -550,6 +551,8 @@ def get_message_html_strs(message_dets):
message_level_html_strs = get_html_strs(
message, message_level, warning=message_dets.warning)
message_html_strs.extend(message_level_html_strs)
if message_dets.warning:
message_html_strs.append("</div>")
return message_html_strs
def repeat_overall_snippet(snippet):
......
from textwrap import dedent
from tests import check_as_expected
ROOT = 'superhelp.advisors.class_advisors.'
def test_misc():
test_conf = [
(
dedent("""\
pet = 'cat'
"""),
{
ROOT + 'one_method_classes': 0,
}
),
(
dedent("""\
class Demo:
pass
"""),
{
ROOT + 'one_method_classes': 1,
}
),
(
dedent("""\
class Demo:
def __init__(self):
pass
"""),
{
ROOT + 'one_method_classes': 1,
}
),
(
dedent("""\
class Demo:
def __init__(self):
pass
def one(self):
pass
"""),
{
ROOT + 'one_method_classes': 1,
}
),
(
dedent("""\
class Demo:
def one(self):
pass
"""),
{
ROOT + 'one_method_classes': 1,
}
),
(
dedent("""\
class Demo:
def __init__(self):
pass
def one(self):
pass
def two(self):
pass
"""),
{
ROOT + 'one_method_classes': 0,
}
),
(
dedent("""\
for i in range(2):
class Demo:
def __init__(self):
pass
def one(self):
pass
def two(self):
pass
"""),
{
ROOT + 'one_method_classes': 0,
}
),
(
dedent("""\
for i in range(2):
class Demo:
def __init__(self):
pass
def one(self):
pass
"""),
{
ROOT + 'one_method_classes': 1,
}
),
(
dedent("""\
class Demo:
def one(self):
pass
def two(self):
pass
"""),
{
ROOT + 'one_method_classes': 0,
}
),
]
check_as_expected(test_conf)
# test_misc()
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