Commit 8390d9ef authored by Grant Paton-Simpson's avatar Grant Paton-Simpson

Add MD displayer; add superhelp.this(__file__); misc

* Add Markdown displayer
* Add ability to check a script by adding import superhelp and superhelp.this(__file__)  on top
* Cope with long collections (truncate and report truncation)
* Add use case for checking code pre-inclusion
* Improve use of layout_comment function
* Bump version
parent d2502443
Pipeline #577 failed with stages
......@@ -5,7 +5,8 @@ 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_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
sed -i 's/DISPLAYER = c/DISPLAYER = h/' /home/g/projects/superhelp/superhelp/conf.py
sed -i 's/DISPLAYER = m/DISPLAYER = h/' /home/g/projects/superhelp/superhelp/conf.py
git status
upload:
......
# https://git.nzoss.org.nz/pyGrant/superhelp
version number: 0.9.15
version number: 0.9.16
author: Grant Paton-Simpson
## Overview
Superhelp is Help for Humans! The goal is to provide customised help for
simple code snippets. Superhelp is not intended to replace the built-in Python
help but to supplement it for basic Python code structures. Superhelp will
SuperHELP is Help for Humans! The goal is to provide customised help for
simple code snippets. SuperHELP is not intended to replace the built-in Python
help but to supplement it for basic Python code structures. SuperHELP will
also be opinionated. Help can be provided in a variety of contexts including
the terminal and web browsers (perhaps as part of on-line tutorials).
......@@ -60,6 +60,8 @@ example provided.
strings in his functions. He learns a standard approach and starts using it
more often.
* Moana wants to check the quality of some code before including it in her project. She learns about some issues and makes improvements before integratin it.
# Example Usage
## Screenshot from HTML
......@@ -70,7 +72,7 @@ more often.
![Example Terminal output](https://git.nzoss.org.nz/pyGrant/superhelp/-/raw/master/example_terminal_output_1.png)
## Notebook
## Using SuperHELP on the Notebook
Add new cell at end with content like:
......@@ -84,7 +86,16 @@ and run it to get advice.
The notebook has more detailed instructions at the top.
## Local Installation
## Using SuperHELP on a Local Installation
### Inside your script
Put the following at the top of your script and then run the script (note - there are two underscores on either side of file):
import superhelp
superhelp.this(__file__)
### From the command line (terminal / console)
$ shelp -h ## get help on usage
......
......@@ -2,7 +2,7 @@ from setuptools import setup, find_packages # @UnresolvedImport
from codecs import open
from os import path
__version__ = '0.9.15'
__version__ = '0.9.16'
here = path.abspath(path.dirname(__file__))
......@@ -46,7 +46,7 @@ setup(
python_requires='>=3.6',
entry_points = {
'console_scripts': [
'shelp=superhelp.helper:shelp', ## using argparse to allow arguments
'shelp=superhelp:shelp', ## using argparse to allow arguments
]
},
)
from . import ast_funcs, code_execution, conf, helper, messages, utils
\ No newline at end of file
from . import advisors, ast_funcs, code_execution, conf, helper, messages, utils
from .helper import shelp, this # @UnresolvedImport
if __name__ == '__main__':
shelp()
This diff is collapsed.
......@@ -6,88 +6,83 @@ from ..utils import layout_comment as layout
def get_open_cm_msg():
return (
layout("""\
For example, every time we open a file we need to make sure it is
closed when we are finished. It is not that hard to handle that
yourself in simple cases but can become much harder - perhaps the
code exits in multiple places (perhaps as a result of an
`exception`). You have to remember to handle the tidy-up in every
place an exit might occur. Either you will forget one of them or
you'll be repeating code all over the place violating the DRY
principle - namely:
> "Don't Repeat Yourself" (The Pragmatic Programmer)
For example, every time we open a file we need to make sure it is closed
when we are finished. It is not that hard to handle that yourself in
simple cases but can become much harder - perhaps the code exits in
multiple places (perhaps as a result of an `exception`). You have to
remember to handle the tidy-up in every place an exit might occur.
Either you will forget one of them or you'll be repeating code all over
the place violating the DRY principle - namely:
WET presumably stands for "Write Every Time", "Write Everything
Twice", "We Enjoy Typing" or "Waste Everyone's Time" ;-)
> "Don't Repeat Yourself" (The Pragmatic Programmer)
If you don't use a context manager, problems can emerge as a snippet
gradually evolves. And it usually all starts so innocently :-).
Here's one example of the problem unfolding:
WET presumably stands for "Write Every Time", "Write Everything Twice",
"We Enjoy Typing" or "Waste Everyone's Time" ;-)
""")
If you don't use a context manager, problems can emerge as a snippet
gradually evolves. And it usually all starts so innocently :-). Here's
one example of the problem unfolding:
""")
+
layout("""\
## 1) Not using context manager (with) not really a problem
f = open(fname)
text = f.read()
f.close() # <------------------ First one (I hope this is the only one)
## 2) Maybe we need to handle empty files
f = open(fname)
text = f.read()
if not text:
raise Exception(f"{fname} is empty") # <------ errr ... is the file
f.close() # handle going to get closed?
## 3) We remember to close the file handle if an exception
f = open(fname)
text = f.read()
if not text:
f.close() # <---------------------------- Rats! I need a second one
raise Exception(f"{fname} is empty")
## 1) Not using context manager (with) not really a problem
f = open(fname)
text = f.read()
f.close() # <------------------ First one (I hope this is the only one)
## 2) Maybe we need to handle empty files
f = open(fname)
text = f.read()
if not text:
raise Exception(f"{fname} is empty") # <------ errr ... is the file
f.close() # handle going to get closed?
## 3) We remember to close the file handle if an exception
f = open(fname)
text = f.read()
if not text:
f.close() # <---------------------------- Rats! I need a second one
raise Exception(f"{fname} is empty")
f.close()
## 4) We want the index of TAG for some reason
f = open(fname)
text = f.read()
if not text:
f.close()
## 4) We want the index of TAG for some reason
f = open(fname)
text = f.read()
if not text:
f.close()
raise Exception(f"{fname} is empty")
idx = text.index(TAG) # <--------------- Hmmm - will raise ValueError if
f.close() # TAG not in text? File left open?
## 5) We remember to handle the exception
f = open(fname)
text = f.read()
if not text:
f.close()
raise Exception(f"{fname} is empty")
try:
idx = text.index(TAG)
except ValueError:
f.close() # <----------------- Three already! I'm lucky it's only a
raise # one-liner each time.
raise Exception(f"{fname} is empty")
idx = text.index(TAG) # <--------------- Hmmm - will raise ValueError if
f.close() # TAG not in text? File left open?
## 5) We remember to handle the exception
f = open(fname)
text = f.read()
if not text:
f.close()
""", is_code=True)
raise Exception(f"{fname} is empty")
try:
idx = text.index(TAG)
except ValueError:
f.close() # <----------------- Three already! I'm lucky it's only a
raise # one-liner each time.
f.close()
""", is_code=True)
+
layout("""\
The code is much cleaner and robust using a simple context manager
:-)
The code is much cleaner and robust using a simple context manager :-)
""")
+
layout("""\
with open(fname) as f:
text = f.read()
if not text:
raise Exception(f"{fname} is empty")
idx = text.index(TAG)
## If my code gets here, i.e. past the indented block inside the context
## manager, we are guaranteed to have freed up the file - nice!
""", is_code=True)
with open(fname) as f:
text = f.read()
if not text:
raise Exception(f"{fname} is empty")
idx = text.index(TAG)
## If my code gets here, i.e. past the indented block inside the context
## manager, we are guaranteed to have freed up the file - nice!
""", is_code=True)
)
WITH_XPATH = 'descendant-or-self::With'
......@@ -112,36 +107,31 @@ def content_manager_overview(block_dets, *, repeat=False):
return None
title = layout("""\
### Context manager(s) used
""")
### Context manager(s) used
""")
if using_open_cm:
summary = layout("""\
Your code includes the commonly used file opening context manager.
""")
Your code includes the commonly used file opening context manager.
""")
else:
summary = layout("""\
Your code uses a context manager.
""")
Your code uses a context manager.
""")
brief_usage = layout("""\
Context managers take care of anything required when we enter a
block of code or exit it.
""")
Context managers take care of anything required when we enter a block of
code or exit it.
""")
if not repeat:
brief_example = layout("""\
For example, every time we open a file we need to make sure it is
closed when we are finished. Or when we finish with database
connections and cursors we need to clean up after ourselves. There
may be some things we need to do when we start a code block as well.
""")
For example, every time we open a file we need to make sure it is closed
when we are finished. Or when we finish with database connections and
cursors we need to clean up after ourselves. There may be some things we
need to do when we start a code block as well.
""")
long_example = get_open_cm_msg()
aop = get_aop_msg()
else:
......@@ -190,22 +180,17 @@ def file_cm_needed(block_dets, *, repeat=False):
return None
title = layout("""\
### File opened without context manager
""")
### File opened without context manager
""")
summary = layout("""
Your code opens a file without using a context manager.
""")
Your code opens a file without using a context manager.
""")
if not repeat:
reasons = layout("""\
Using a context manager is easy (actually, writing them isn't even
that hard) but using the standard ones has big advantages.
""")
Using a context manager is easy (actually, writing them isn't even that
hard) but using the standard ones has big advantages.
""")
long_example = get_open_cm_msg()
aop = get_aop_msg()
else:
......
......@@ -33,68 +33,63 @@ def decorator_overview(block_dets, *, repeat=False):
dec_name_list = get_nice_str_list(decorator_names, quoter='`')
plural = 's' if len(decorator_names) > 1 else ''
summary = layout(f"""\
### Decorator{plural} used
### Decorator{plural} used
The code uses the decorator{plural}: {dec_name_list}.
""")
The code uses the decorator{plural}: {dec_name_list}.
""")
if not repeat:
dec_dets = (
layout("""\
Decorators are a common and handy feature of Python. Using them
is beginner-level Python and making them is intermediate-level
Python. One tip - use functools.wraps to make the decorated
function look like the original function e.g. have the same doc
string.
Decorators are well-documented elsewhere so there is no real
advice to give here apart from recommending their use when
appropriate.
Decorators are a common and handy feature of Python. Using them is
beginner-level Python and making them is intermediate-level Python.
One tip - use functools.wraps to make the decorated function look
like the original function e.g. have the same doc string.
Here's an example of a simple decorator (`route`) making it easy
to set up a web end point:
Decorators are well-documented elsewhere so there is no real advice
to give here apart from recommending their use when appropriate.
""")
Here's an example of a simple decorator (`route`) making it easy to
set up a web end point:
""")
+
layout("""\
from bottle import route
from bottle import route
@route('/hello')
def hello():
return "Hello World!"
""", is_code=True)
@route('/hello')
def hello():
return "Hello World!"
""", is_code=True)
+
layout("""\
and here is an example of a simple, no argument decorator being
created with `functool.wraps` applied:
""")
and here is an example of a simple, no argument decorator being
created with `functool.wraps` applied:
""")
+
layout('''\
from functools import wraps
def tweet(func):
"""
Fake tweet original message after saying the message as
before
"""
@wraps(func)
def wrapper(message):
func(message)
print(f"I'm tweeting the message {message}")
return wrapper
@tweet
def say(message):
"""
Print supplied message
"""
print(message)
say("sausage!")
''', is_code=True)
from functools import wraps
def tweet(func):
"""
Fake tweet original message after saying the message as
before
"""
@wraps(func)
def wrapper(message):
func(message)
print(f"I'm tweeting the message {message}")
return wrapper
@tweet
def say(message):
"""
Print supplied message
"""
print(message)
say("sausage!")
''', is_code=True)
)
aop = get_aop_msg()
else:
......
This diff is collapsed.
......@@ -348,58 +348,51 @@ def manual_incrementing(blocks_dets):
return None
summary = layout(f"""\
### Possible option of using `enumerate()`
### Possible option of using `enumerate()`
It looks like your code is manually incrementing `{incrementing_var}`.
In Python you can use the `enumerate` function to handle this for you.
""")
It looks like your code is manually incrementing `{incrementing_var}`. In
Python you can use the `enumerate` function to handle this for you.
""")
demo = (
layout("""\
Here is an example of the manual approach:
""")
+
layout("""\
n = 1
for image in images:
if n % 10 == 0:
print(f"Just processed image {{n}}")
process_image(image)
n += 1
""", is_code=True)
n = 1
for image in images:
if n % 10 == 0:
print(f"Just processed image {{n}}")
process_image(image)
n += 1
""", is_code=True)
+
layout("""\
Here is how we can use `enumerate()` instead:
""")
+
layout("""\
for n, image in enumerate(images, 1):
if n % 10 == 0:
print(f"Just processed image {{n}}")
process_image(image)
""", is_code=True)
for n, image in enumerate(images, 1):
if n % 10 == 0:
print(f"Just processed image {{n}}")
process_image(image)
""", is_code=True)
+
layout("""\
Often you want counting from 0 in which case you don't need to specify
the start value (0 is the default):
""")
+
layout("""\
for i, image in enumerate(images):
...
""", is_code=True)
for i, image in enumerate(images):
...
""", is_code=True)
+
layout("""\
You can give the enumerated value any name that makes sense but reserve
`i` for incrementing starting at 0 and prefer `n` when starting at 1.
""")
)
......
......@@ -35,20 +35,17 @@ def exception_overview(blocks_dets):
return None
title = layout("""\
### Exception handling
""")
### Exception handling
""")
block_comment_bits = []
for n, exception_block in enumerate(exception_blocks, 1):
counter = '' if len(exception_blocks) == 1 else f" {int2nice(n)}"
handlers = get_nice_str_list(exception_block, quoter='`')
block_comment_bits.append(layout(f"""\
#### `try`-`except` block{counter}
#### `try`-`except` block{counter}
The following exception handlers were detected: {handlers}
"""))
The following exception handlers were detected: {handlers}
"""))
block_comments = ''.join(block_comment_bits)
message = {
......@@ -75,10 +72,8 @@ def unspecific_exception(blocks_dets):
return None
title = layout("""\
#### Un-specific `Exception` only in `try`-`except` block(s)
""")
#### Un-specific `Exception` only in `try`-`except` block(s)
""")
n_unspecific = len(unspecific_block_ns)
if n_unspecific == 1:
......@@ -91,31 +86,29 @@ def unspecific_exception(blocks_dets):
block_n_specific_text = f"s {blocks_ns} have"
unspecific_warning = layout(f"""\
`try`-`except` block{block_n_specific_text} an un-specific Exception
only.
`try`-`except` block{block_n_specific_text} an un-specific Exception only.
Using the un-specific exception type `Exception` is often completely
appropriate. But if you are looking for specific exceptions you
should handle those separately.
""")
Using the un-specific exception type `Exception` is often completely
appropriate. But if you are looking for specific exceptions you should
handle those separately.
""")
unspecific_demo = (
layout(f"""\
For example:
""")
layout("""\
For example:
""")
+
layout("""\
try:
spec_dicts[idx][spec_type]
except IndexError:
print(f"Unable to access requested spec_dict (idx {idx})")
except KeyError:
print(f"Unable to access '{spec_type}' for "
f"requested spec_dict (idx {idx})")
except Exception as e:
print(f"Unexpected exception - details: {e}")
""", is_code=True)
try:
spec_dicts[idx][spec_type]
except IndexError:
print(f"Unable to access requested spec_dict (idx {idx})")
except KeyError:
print(f"Unable to access '{spec_type}' for "
f"requested spec_dict (idx {idx})")
except Exception as e:
print(f"Unexpected exception - details: {e}")
""", is_code=True)
)
message = {
......
......@@ -43,17 +43,16 @@ def comprehension_option(block_dets, *, repeat=False):
return None
title = layout(f"""\
### Possible option of using a {comp_type}
""")
### Possible option of using a {comp_type}
""")
if not repeat:
option = layout(f"""\
Simple for loops can sometimes be replaced with comprehensions. In
this case a simple reading of the code suggests a {comp_type} might
be possible. Of course, only use a comprehension if it makes your
code easier to understand.
""")
Simple for loops can sometimes be replaced with comprehensions. In this
case a simple reading of the code suggests a {comp_type} might be
possible. Of course, only use a comprehension if it makes your code
easier to understand.
""")
else:
option = ''
......@@ -145,45 +144,35 @@ def for_index_iteration(block_dets, *, repeat=False):
return None
summary = layout(f"""\
### Possible option of using direct iteration
### Possible option of using direct iteration
It looks like your snippet iterates through `{iterable_name}` using
indexes. In Python you can iterate directly which is much easier.
""")
It looks like your snippet iterates through `{iterable_name}` using indexes.
In Python you can iterate directly which is much easier.
""")
if not repeat:
examples = (
layout(f"""\
For example, instead of:
""")
layout("""\
For example, instead of:
""")
+
layout(f"""\
for {index_name} in range(len({iterable_name})):
print({iterable_name}[{index_name}])
""", is_code=True)
for {index_name} in range(len({iterable_name})):
print({iterable_name}[{index_name}])
""", is_code=True)
+
layout(f"""\
you can directly iterate as follows:
""")
layout("""\
you can directly iterate as follows:
""")
+
layout(f"""\
for item in {iterable_name}: ## item should be replaced with a more useful name
print(item)
for item in {iterable_name}: ## item should be replaced with a more useful name
print(item)
""", is_code=True)
""", is_code=True)
+
layout(f"""\
which is considered more pythonic i.e. good.