Commit 2cde7cd2 authored by Grant Paton-Simpson's avatar Grant Paton-Simpson

Streamline and standardise message making; misc bug fixes; bump version

parent de738baa
Pipeline #561 failed with stages
git:
cd /home/g/projects/superhelp && nosetests
/home/g/projects/superhelp/superhelp/env/bin/nosetests
sed -i 's/^test_misc()/# test_misc()/' /home/g/projects/superhelp/tests/*.py
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
......
# https://git.nzoss.org.nz/pyGrant/superhelp
version number: 0.9.1
version number: 0.9.2
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.9.1'
__version__ = '0.9.2'
here = path.abspath(path.dirname(__file__))
......
......@@ -27,65 +27,67 @@ def getters_setters(block_dets, *, repeated_message=False):
class_getter_setter_methods[class_name].append(method_name)
if not class_getter_setter_methods:
return None
brief_msg = layout("""\
title = layout("""\
### Alternative to getters and setters
""")
simple_class_msg_bits = []
for class_name, method_names in sorted(class_getter_setter_methods.items()):
multiple = len(method_names) > 1
if multiple:
nice_list = get_nice_str_list(method_names, quoter='`')
brief_msg += layout(f"""\
simple_class_msg_bits.append(layout(f"""\
Class `{class_name}` has the following methods that look like
either getters or setters: {nice_list}.
""")
"""))
else:
method_type = (
'getter' if method_name.startswith('get_') else 'setter')
brief_msg += layout(f"""\
simple_class_msg_bits.append(layout(f"""\
Class `{class_name}` has a `{method_names[0]}` method that
looks like a {method_type}.
""")
"""))
simple_class_msg = ''.join(simple_class_msg_bits)
if not repeated_message:
brief_msg += layout("""\
properties_option = layout("""\
Python doesn't need getters and setters. Instead, you can use
properties. These are easily added using decorators e.g.
`@property`.
""")
main_msg = brief_msg
if not repeated_message:
tm = '\N{TRADE MARK SIGN}'
main_msg += (
why_getters_etc = layout(f"""\
A good discussion of getters, setters, and properties can be found
at <https://www.python-course.eu/python3_properties.php>.
Getters and setters are usually added in other languages such as
Java because direct attribute access doesn't give the ability to
calculate results or otherwise run a process when a value is
accessed / written.
And it is common for lots of getters and setters to be added,
whether or not they are actually needed - Just In Case{tm}. The fear
is that if you point other code to an attribute, and you later need
to process the attribute or derive it before it is served up or
stored, then you'll need to make a breaking change to your code. All
the client code referencing the attribute will have to be rewritten
to replace direct access with a reference to the appropriate getter
or setter. Understandably then the inclination is to point to a
getter or setter in the first case even if it doesn't actually do
anything different (for now at least) from direct access. Using
these getters and setters is wasteful, and bloats code
unnecessarily, but it avoids the worse evil of regularly broken
interfaces. The benefit is that you can change implementation later
if you need to and nothing will break. But in Python there is a much
better way :-).
""")
comparison = (
layout(f"""\
A good discussion of getters, setters, and properties can be
found at <https://www.python-course.eu/python3_properties.php>.
Getters and setters are usually added in other languages such as
Java because direct attribute access doesn't give the ability to
calculate results or otherwise run a process when a value is
accessed / written.
And it is common for lots of getters and setters to be added,
whether or not they are actually needed - Just In Case{tm}. The
fear is that if you point other code to an attribute, and you
later need to process the attribute or derive it before it is
served up or stored, then you'll need to make a breaking change
to your code. All the client code referencing the attribute will
have to be rewritten to replace direct access with a reference
to the appropriate getter or setter. Understandably then the
inclination is to point to a getter or setter in the first case
even if it doesn't actually do anything different (for now at
least) from direct access. Using these getters and setters is
wasteful, and bloats code unnecessarily, but it avoids the worse
evil of regularly broken interfaces. The benefit is that you can
change implementation later if you need to and nothing will
break. But in Python there is a much better way :-).
Let's compare the getter / setter approach and the property
approach.
......@@ -168,28 +170,35 @@ def getters_setters(block_dets, *, repeated_message=False):
""", is_code=True)
)
if repeated_message:
extra_msg = ''
else:
extra_msg = (
layout("""\
Python also has a `deleter` decorator which handle deletion
of the attribute e.g.
""")
+
layout("""\
@name.deleter
def name(self):
...
""", is_code=True)
)
message = {
conf.BRIEF: brief_msg,
conf.MAIN: main_msg,
conf.EXTRA: extra_msg,
}
return message
deleter = (
layout("""\
Python also has a `deleter` decorator which handle deletion of
the attribute e.g.
""")
+
layout("""\
@name.deleter
def name(self):
...
""", is_code=True)
)
else:
properties_option = ''
why_getters_etc = ''
comparison = ''
deleter = ''
brief_msg = title + simple_class_msg + properties_option
main_msg = (title + simple_class_msg + properties_option
+ why_getters_etc + comparison)
extra_msg = deleter
message = {
conf.BRIEF: brief_msg,
conf.MAIN: main_msg,
conf.EXTRA: extra_msg,
}
return message
@filt_block_advisor(xpath=CLASS_XPATH, warning=True)
def selfless_methods(block_dets, *, repeated_message=False):
......@@ -218,28 +227,31 @@ def selfless_methods(block_dets, *, repeated_message=False):
class_selfless_methods[class_name].append(method_name)
if not class_selfless_methods:
return None
brief_msg = layout("""\
title = layout("""\
### Method doesn't use instance
""")
simple_class_msg_bits = []
for class_name, method_names in sorted(class_selfless_methods.items()):
multiple = len(method_names) > 1
if multiple:
nice_list = get_nice_str_list(method_names, quoter='`')
brief_msg += layout(f"""\
simple_class_msg_bits.append(layout(f"""\
Class `{class_name}` has the following methods that don't use
the instance (usually called `self`): {nice_list}.
""")
"""))
else:
brief_msg += layout(f"""\
simple_class_msg_bits.append(layout(f"""\
Class `{class_name}` has a `{method_names[0]}` method that
doesn't use the instance (usually called `self`).
""")
"""))
simple_class_msg = ''.join(simple_class_msg_bits)
if not repeated_message:
brief_msg += layout("""\
staticmethod_msg = layout("""\
If a method doesn't use the instance it can be either pulled into a
function outside the class definition or decorated with
......@@ -247,10 +259,7 @@ def selfless_methods(block_dets, *, repeated_message=False):
instance object to be supplied as the first argument.
""")
main_msg = brief_msg
if not repeated_message:
main_msg += (
staticmethod_demo = (
layout("""
For example, instead of:
......@@ -274,15 +283,19 @@ def selfless_methods(block_dets, *, repeated_message=False):
return round(years * 365.25)
""", is_code=True)
)
if repeated_message:
extra_msg = ''
else:
extra_msg = layout("""\
call_it_self = layout("""\
It is not obligatory to call the first parameter of a bound method
`self` but you should call it that unless you have a good reason to
break convention.
""")
else:
staticmethod_msg = ''
staticmethod_demo = ''
call_it_self = ''
brief_msg = title + simple_class_msg + staticmethod_msg
main_msg = title + simple_class_msg + staticmethod_msg + staticmethod_demo
extra_msg = call_it_self
message = {
conf.BRIEF: brief_msg,
conf.MAIN: main_msg,
......@@ -315,11 +328,12 @@ def one_method_classes(block_dets, *, repeated_message=False):
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_msg = layout(f"""\
summary = layout(f"""\
### Possible option of converting class{class_plural} to single function{func_plural}
......@@ -327,16 +341,29 @@ def one_method_classes(block_dets, *, repeated_message=False):
function at most (excluding `__init__`):
""")
n_methods_msg_bits = []
for class_name, method_name in classes_sole_methods:
method2use = (
f"`{method_name}`" if method_name else 'nothing but `__init__`')
brief_msg += layout(f"""\
n_methods_msg_bits.append(layout(f"""\
- {class_name}: {method2use}
""")
if repeated_message:
extra_msg = ''
else:
brief_msg += (
"""))
n_methods_msg = ''.join(n_methods_msg_bits)
if not repeated_message:
not_just_oo = 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.
""")
function_demo = (
layout(f"""\
It may be simpler to replace the class{class_plural} with simple
......@@ -378,21 +405,12 @@ def one_method_classes(block_dets, *, repeated_message=False):
""")
)
extra_msg = 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.
else:
not_just_oo = ''
function_demo = ''
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_msg,
conf.EXTRA: extra_msg,
conf.BRIEF: summary + n_methods_msg + function_demo,
conf.EXTRA: not_just_oo,
}
return message
......@@ -28,16 +28,16 @@ def decorator_overview(block_dets, *, repeated_message=False):
if namespace:
name = f"{namespace}.{name}"
decorator_names.append(name)
dec_name_list = get_nice_str_list(decorator_names, quoter='`')
plural = 's' if len(decorator_names) > 1 else ''
brief_msg = layout(f"""\
summary = layout(f"""\
### Decorator{plural} used
The code uses the decorator{plural}: {dec_name_list}.
""")
main_msg = brief_msg
if not repeated_message:
main_msg += (
dec_dets = (
layout("""\
Decorators are a common and handy feature of Python. Using them
......@@ -95,8 +95,11 @@ def decorator_overview(block_dets, *, repeated_message=False):
say("sausage!")
''', is_code=True)
)
else:
dec_dets = ''
message = {
conf.BRIEF: brief_msg,
conf.MAIN: main_msg,
conf.BRIEF: summary,
conf.MAIN: summary + dec_dets,
}
return message
......@@ -5,137 +5,116 @@ from ..utils import get_nice_str_list, layout_comment as layout
ASSIGN_DICT_XPATH = 'descendant-or-self::Assign/value/Dict'
def _get_additional_main_msg(first_name):
additional_main_msg = (
layout(f"""
It is common to iterate through the key-value pairs of a dictionary.
This can be achieved using the dictionary's `.items()` method. E.g.
""")
+
layout(f"""\
## k, v is conventional, and OK in a hurry, but readable names
## are probably better for code you're going to maintain
for k, v in {first_name}.items():
print(f"key {{k}} maps to value {{v}}")
""", is_code=True)
+
layout(f"""
Keys are unique but values can be repeated. For example:
""")
+
layout(f"""
country2car = {{'Japan': 'Toyota', 'Sweden': 'Volvo'}} ## OK - all keys are unique
country2car = {{'Japan': 'Toyota', 'Japan': 'Honda'}} ## Oops - the 'Japan' key is repeated
""", is_code=True)
+
layout(f"""
@filt_block_advisor(xpath=ASSIGN_DICT_XPATH)
def dict_overview(block_dets, *, repeated_message=False):
"""
Look at assigned dictionaries e.g. location = {'country' 'New Zealand',
'city': 'Auckland'}
"""
dict_els = block_dets.element.xpath(ASSIGN_DICT_XPATH)
plural = 'ies' if len(dict_els) > 1 else 'y'
title = layout(f"""\
In which case a better structure might be to have each 'value'
being a list e.g.
### Dictionar{plural} defined
""")
+
layout(f"""
country2cars = {{'Japan': ['Toyota', 'Honda'], 'Sweden': ['Volvo']}} ## OK - all keys are unique
""", is_code=True)
)
return additional_main_msg
def _get_minimal_dict_details(block_dets, dict_els, plural):
brief_msg = ''
for i, dict_el in enumerate(dict_els):
first = (i == 0)
""")
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)
if first:
title = layout(f"""\
### Dictionar{plural} defined
""")
brief_msg += title
brief_msg += layout(f"""\
brief_desc_bits.append(layout(f"""\
`{name}` is a dictionary with {utils.int2nice(len(items))} items
(i.e. {utils.int2nice(len(items))} mappings).
""")
message = {
conf.BRIEF: brief_msg,
}
return message
"""))
brief_desc = ''.join(brief_desc_bits)
if not repeated_message:
dict_def = layout("""\
def _get_full_dict_details(block_dets, dict_els, plural):
brief_msg = ''
main_msg = ''
first_name = None
for i, dict_el in enumerate(dict_els):
first = (i == 0)
name = get_assign_name(dict_el)
items = code_execution.get_val(
block_dets.pre_block_code_str, block_dets.block_code_str, name)
if first:
first_name = name
title = layout(f"""\
Dictionaries map keys to values.
### Dictionar{plural} defined
""")
workhorses = layout("""\
""")
brief_msg += title
main_msg += title
Dictionaries, along with lists, are the workhorses of Python data
structures.
""")
keys_and_vals = layout("""\
brief_msg += layout("""\
Keys are unique but values can be repeated.
Dictionaries map keys to values.
""")
dict_desc_bits = []
for i, dict_el in enumerate(dict_els):
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:
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"""
It is common to iterate through the key-value pairs of a
dictionary. This can be achieved using the dictionary's
`.items()` method. E.g.
""")
main_msg += layout("""\
Dictionaries, along with lists, are the workhorses of Python
data structures.
+
layout(f"""\
## k, v is conventional, and OK in a hurry, but readable names
## are probably better for code you're going to maintain
for k, v in {first_name}.items():
print(f"key {{k}} maps to value {{v}}")
""", is_code=True)
+
layout(f"""
Keys are unique but values can be repeated. For example:
""")
empty_dict = (len(items) == 0)
if empty_dict:
list_desc = layout(f"""\
+
layout(f"""
country2car = {{'Japan': 'Toyota', 'Sweden': 'Volvo'}} ## OK - all keys are unique
country2car = {{'Japan': 'Toyota', 'Japan': 'Honda'}} ## Oops - the 'Japan' key is repeated
`{name}` is an empty dictionary.
""", is_code=True)
+
layout(f"""
""")
else:
plural = '' if len(items) == 1 else 's'
list_desc = 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()`.
In which case a better structure might be to have each 'value'
being a list e.g.
""")
brief_msg += list_desc
main_msg += list_desc
brief_msg += layout("""\
Keys are unique but values can be repeated.
+
layout(f"""
country2cars = {{'Japan': ['Toyota', 'Honda'], 'Sweden': ['Volvo']}} ## OK - all keys are unique
Dictionaries, along with lists, are the workhorses of Python data
structures.
""")
main_msg += _get_additional_main_msg(first_name)
message = {
conf.BRIEF: brief_msg,
conf.MAIN: main_msg,
conf.EXTRA: layout("""\
""", is_code=True)
)
mighty_dict = layout("""\
Python dictionaries (now) keep the order in which items are added.
......@@ -147,21 +126,19 @@ def _get_full_dict_details(block_dets, dict_els, plural):
2. The Mighty Dictionary -
<https://www.youtube.com/watch?v=oMyy4Sm0uBs>
""")
}
return message
@filt_block_advisor(xpath=ASSIGN_DICT_XPATH)
def dict_overview(block_dets, *, repeated_message=False):
"""
Look at assigned dictionaries e.g. location = {'country' 'New Zealand',
'city': 'Auckland'}
"""
dict_els = block_dets.element.xpath(ASSIGN_DICT_XPATH)
plural = 'ies' if len(dict_els) > 1 else 'y'
if repeated_message:
message = _get_minimal_dict_details(block_dets, dict_els, plural)
else:
message = _get_full_dict_details(block_dets, dict_els, plural)
dict_def = ''
workhorses = ''
keys_and_vals = ''
main_dict_desc = brief_desc
general = ''
mighty_dict = ''
message = {
conf.BRIEF: title + dict_def + brief_desc + keys_and_vals + workhorses,
conf.MAIN: title + main_dict_desc + workhorses + general,
conf.EXTRA: mighty_dict,
}
return message
def get_key_type_names(items):
......@@ -179,12 +156,8 @@ def mixed_key_types(block_dets, *, repeated_message=False):
Warns about dictionaries with mix of string and integer keys.
"""