For loop when importing from module (is it possible)

So whilst I think I’m going to leave the code as is what you’ve said intriged me so I did some playing.

As I understand it, I can convert args to a dictionary via the vars() command, then in theory merge them. But when I tried this it appears to merge I was hoping that if I merged it when an item was “none” it wouldn’t overwrite an actual variable, but that doesn’t appear to be the case.

So for example: (args takes priority)

So if args had recipient = harry@ and params had recipient fred@ combined would be harry@.

But if args had port as none and params had port 25, then combined would be 25. In this case port becomes none.

This is my code subset which goes after the part in the program where the file is imported.

dic_args = vars(args)
print(f"dictionary args: {dic_args}")
print(f"params: {params}")
combined = {**dic_args,**params}
print(f"combined dictionaries: {combined}")

With output

W:\OneDrive\Documents\python\venv\Scripts\python.exe W:\OneDrive\Documents\python\ -r 
parameters file used: emailer_params.json
Namespace(file_import='emailer_params.json', recipient='', from_envelope=None, from_header=None, port=None, sending_server=None, subject=None, message_text=None, auth_username=None, auth_password=None, loop=None, message_text_input=False, error_handling=False, verbose=False, quiet=False)
dictionary args: {'file_import': 'emailer_params.json', 'recipient': '', 'from_envelope': None, 'from_header': None, 'port': None, 'sending_server': None, 'subject': None, 'message_text': None, 'auth_username': None, 'auth_password': None, 'loop': None, 'message_text_input': False, 'error_handling': False, 'verbose': False, 'quiet': False}
params: {'from_envelope': '', 'recipient': '', 'port': 25, 'sending_server': '', 'subject': 'test subject', 'message_text': 'This is the message text', 'message_text_input': False, 'loop': 0, 'verbose': False, 'quiet': False}
combined dictionaries: {'file_import': 'emailer_params.json', 'recipient': '', 'from_envelope': '', 'from_header': None, 'port': 25, 'sending_server': '', 'subject': 'test subject', 'message_text': 'This is the message text', 'auth_username': None, 'auth_password': None, 'loop': 0, 'message_text_input': False, 'error_handling': False, 'verbose': False, 'quiet': False}

Is it possible to combine them as required?

This way makes the params take priority, since they are last in the unpacking order.

If you want the (parsed) args to take priority, then they need not to contain None values unless you actually want to overwrite with None. That might require describing the corresponding command-line options (to argparse, not in the documentation) as optional, and then verifying that all the values are present after doing all the updating.

I’ll admit to being confused. I had thought that these arguments were optional. As in required=True wasn’t set. I tried adding required=False as below but the same (ie. if not set they return a “none”. I’m guessing my add_argument command is incorrect.

parser.add_argument("-f", "--from_envelope", type=str, required=False, metavar="",
                    help="from address - envelope - REQUIRED if not imported from file")

I didn’t read the code very closely (there’s a fair bit of it after all) and I’m out of practice with argparse. I think you’re meant to do this with metavar='?', but I’d have to look it all up again.

I’m not sure there is a way to have an add_argument item return nothing vs returning None.

But thinking outside the square, if I make my dictionary only import those items that are not None then it works as designed.

dic_args = {k:v for k,v in vars(args).items() if v is not None}
combined = {**params,**dic_args}

The above code will use options specified in arg_parse first and then those in the parameters json file.

This info just for completeness for this thread. Thanks everyone for your help.

As long as you don’t ever need None to be a valid value (and it’s not clear to me how you would specify that on the command line), then yes, that works.

I decided to follow the rest of the excellent advice I’d been given here and modified the script with the code below.

Basically this uses the globals() command suggested way back at the beginning of this thread. Which actually answers the question I posed here. And in v3 of my code 2 lines saved me some 30 lines that I had in v2.

Most importantly (to me) I’ve learnt a heap here. Thanks eveyone for your help.

for p in ["recipient","from_header","from_envelope","port","sending_server","subject","message_text","auth_username",
    globals()[p] = combined.get(p)

In case anyone is interested in using this small app, feel free. Full code is below.

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import smtplib
import ssl
import argparse
from time import sleep
from datetime import datetime
import json

def send_mail():
    SSLcontext = ssl.create_default_context()
    if verbose: print(f"server: {sending_server}  port: {port}")
    with smtplib.SMTP(sending_server, port) as server:
        if verbose: server.set_debuglevel(1) # set diagnostics to verbose
        if not auth_password:
            if verbose: print("no authentication")
            server.login(auth_username, auth_password)

        server.sendmail(from_envelope, recipient, text)
        if not quiet: print(f"{ %H:%M} - e-mail sent from {from_envelope} to {recipient} with subject: {subject}")

parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description="""Davo's emailer v3

This program is aimed at helping the user test mailservers. 
I've tried to include all scenarios but happy to add more options if requested (and it's possible).

Note that the parameters file is required to list sending_server and port if these are not defined
via the command line arguments (-s and -p).

The default parameters file is emailer_params.json and needs at a minimum the following:

{"port": 25,
"sending_server": "<mailserver to send to>"}""", epilog="""
This program is not for commercial modification or resale without permission from me.
Permission is granted for commercial or personal use.""")
parser.add_argument("-F", "--file_import", type=str, metavar="", default="emailer_params.json",
                    help="user defined parameters file otherwise emailer_params.json will be used, parameters set via cmd line will take priority")
parser.add_argument("-r", "--recipient", type=str, metavar="",
                    help="recipient address - REQUIRED if not imported from file")
parser.add_argument("-f", "--from_envelope", type=str, metavar="",
                    help="from address - envelope - REQUIRED if not imported from file")
parser.add_argument("-fh", "--from_header", type=str, metavar="",
                    help="from address - header (i.e. if not specified will be same as from-envelope")
parser.add_argument("-p", "--port", type=int, metavar="",
                    help="SMTP port to use (default set in params json)")
parser.add_argument("-s", "--sending_server", type=str, metavar="",
                    help="destination SMTP server (default set in params json)")
parser.add_argument("-j", "--subject", type=str, metavar="", help="message subject")
parser.add_argument("-t", "--message_text", type=str, metavar="", help="message text - single line")
parser.add_argument("-au", "--auth_username", type=str, metavar="",
                    help="authentication username - optional, if not used from_envelope address will be used")
parser.add_argument("-ap", "--auth_password", type=str, metavar="",
                    help="authentication password - optional, note if this is not specified no authentication will be used even if -au is set")
parser.add_argument("-l", "--loop", type=int, metavar="",
                    help="continuously loop - variable is time between sending in seconds")
parser.add_argument("-T", "--message_text_input", action="store_true",
                    help="Input message text - multi-line - flag")
parser.add_argument("-E", "--error_handling", action="store_true",
                    help="Error handling - not normally recommended but will ensure it will keep looping even on an error - flag")
group = parser.add_mutually_exclusive_group()
group.add_argument("-v", "--verbose", action="store_true",
                    help="verbose diagnostics - flag - NB: -v or -q")
group.add_argument("-q", "--quiet", action="store_true",
                    help="quiet - no output - flag - NB: -v or -q")
args = parser.parse_args()

file_import = args.file_import
print(f"parameters file used: {file_import}")

#import_file - either default or defined
    with open(file_import, 'r') as openfile:
        params = json.load(openfile)
except FileNotFoundError:
    print("parameters file does not exist")
except Exception as err:
    print(f"error in import - probably incorrectly defined variable. error code: {err}")

# import data from either argpass (command line) or parameters file
# priority is given to argpass (args) so if that is defined the value will not be imported from the parameters file
# after this import we are left with a dictionary "combined" with all the parameters defined

dic_args = {k:v for k,v in vars(args).items() if v is not None}
combined = {**params,**dic_args}

# note for future, maybe use dictionary feature - so combined.get("recipient") setting recipient to combined.get("recipient). E.g. next line
# if not combined.get("recipient"): raise ValueError("recipient must be defined")

# Alternatively below routine converts to normal variables
for p in ["recipient","from_header","from_envelope","port","sending_server","subject","message_text","auth_username",
    globals()[p] = combined.get(p)

if not quiet: print(f"quiet: {quiet}, verbose: {verbose}")

# Logic checking of variables and rules in place
# quiet and verbose are mutually exclusive
if (quiet and verbose): raise ValueError("quiet and verbose cannot be set at the same time. check parameters file")

# recipient - must be defined by argument or parameters file
if not recipient: raise ValueError("recipient must be defined")

# note the from envelope is what is in the initial connection (i.e. MAIL FROM: from_envelope)
# note the from header is what shows in the e-mail client. This is often faked.
# if the from_header isn't specified we assume it's the same as the from_envelope

# define from_envelope - must be defined by argument or parameters file
if not from_envelope: raise ValueError("from_envelope must be defined")

# define from_header. if not defined by params or argument use from_envelope
if not from_header: from_header = from_envelope

# subject and message text need to be defined or there will be an error when sending
if not subject: subject = ""
if message_text:
    message = message_text
    message = ""

# if auth_password specified authentication will be attempted
# in this case the auth_username will be used. If not specified the from_envelope address will be used as auth_username
if auth_password and not auth_username: auth_username = from_envelope

if verbose: print(f"recipient: {recipient}, from envelope: {from_envelope}, from header: {from_header}")
if verbose: print(f"sending server: {sending_server}, port: {port}")
if verbose: print(f"subject: {subject}")
if verbose: print(f"auth_username: {auth_username}, auth_password: {auth_password}")
if verbose: print(f"loop: {loop}")

# quit()

if message_text_input:
    print("Enter message, end with a blank lines (i.e. enter twice)")
    message = ""
    counter = 0
    while True:
        line = input()
        if line.strip() == '':
            counter += 1
            if counter == 2:
        # \n is new line, %s is string formatting
        # str.strip() will remove any whitespace that is at start or end
        message += "%s\n" % line.strip()

if verbose: print(f"message: {message}, length: {len(message)}")

msg = MIMEMultipart()
msg['From']= from_header
msg['To']= recipient
msg['Subject']= subject

# removed - not implementing bcc cc due to issues and it's not really what I need
# if args.bcc: msg["Bcc"] = args.bcc
#if msg["Cc"] =

if verbose: print("This is our message format: ", msg)
if verbose: print("This is the message envelope: ", "From: ", from_envelope, "To: ", recipient)

msg.attach(MIMEText(message, "plain"))
text = msg.as_string()

if verbose: print("This is the text field")
if verbose: print(text)

# if -l loop parameter set the send_mail function will run continuously
if loop:
    if error_handling:         #this will keep the script working if an error occurs
        if not quiet: print("Error handling active")
        while True:
                if verbose: print(f"Waiting for {loop} seconds")
            except Exception as err:
                print(f"""{ %H:%M} - An error occurred
    - for more detailed info on the error run with the -v (verbose) switch
    - the error message was
                if verbose: print(f"Waiting for {loop} seconds")
        while True:
            if verbose: print(f"Waiting for {loop} seconds")

1 Like