Working with a class that imports modules?

I am trying to write a Form class that handles the basics of form submittal and validation. I also want to provide the opportunity for custom methods at certain points like validating/modifying data, and I do so with the importlib library. I am testing and modifying the module that gets imported by my class and noticed that it doesn’t update unless I exit the entire project and reload it. I assume its because my Form class imported the module once and does not go to grab the module on every project update in an attempt to be smart and not hog resources. Is there a way to force it?

For clarity here’s my current class definition

class Form():
	form_payload = None
	initial_data = None
	prefix = None
	form_name = None
	db_table = None
	idx = None
	userId = None
	module_name = None
	result = None #result of insert
	def __init__(self, form_name, container, prefix="_out_", userId=None):
		self.form_name = validateFormName(form_name)
		self.db_table = validateDBTable(form_name)
		self.module_name = validateModule(form_name)
		self.form_payload = getData(container, prefix=prefix)
		self.idx = self.form_payload['idx']
		if userId is None:
			self.userId = self.form_payload['userId']
                else:
                        self.userId = userId
	
	def db_insert(self):

		return True, 1
	
	def insert(self):
		i = importlib.import_module(self.module_name)
		try:
			i.validateNew(self.form_payload)
		except AttributeError:
			pass
		except FormError, e:
			# TODO Here - we would put the universal way to alert users for vision and perspective
			system.gui.warningBox(str(e))
		print "about to enter modify New"
		i.modifyNew(self.form_payload)
		self.result, self.idx = self.db_insert()
		###
		# Here - we do universal displaying of success or failure
		###
		i.postinsert(self.form_payload, self.idx, self.result)

Under my def insert(), working with my validate new module. That’s where I am getting caught up - I have a module forms.testForm that is my module_name, which does have a validateNew which does run. However, when I modify validateNew and then retest in my open project window, no changes come through, unless I close the project and reopen. Is there a way around this? In production I don’t see this as an issue because the script module won’t be changing but in development it’s slowing me down a bit.

Ignition has no way to track references across script modules. Don’t import them, ever. Use fully-qualified names. For your case, I recommend simply passing function objects representing your customizations when instantiating Form. You could subclass Form, but that will have the same problem with delayed updates.

Can you give a snippet of what you mean about passing function objects? Do you mean pass my functions into my class directly as in

from Form import Form
import forms.testForm
NewEntry = Form(..., [forms.testForm.validateNew, forms.testForm.modifyNew])

Something like that?

Generally speaking, once I get my modules solidified and they aren’t updated - this should work fine otherwise in a production environment right? Are there any Ignition environment specific gotcha’s I need to be on the lookout for when using a class or general pitfalls to watch out for trying to use a class in Ignition?

Don’t import from script modules! (And overwriting a module name with a class name makes me cringe.)

Adjust class Form to look something like this:

class Form(object):
	def __init__(self, args...., iVal, uVal):
		.... other stuff ....
		self.insertValidate = iVal
		self.updateValidate = uVal

	def insert(self):
		try:
			self.insertValidate(self, self.form_payload)
		.... other stuff ....

Elsewhere, your customized form would be defined something like this:

def insertValidator(self, someData):
	# can behave like method of Form if explicitly passed self
	pass

def updateValidator(self, someData):
	# can behave like method of Form if explicitly passed self
	pass

def TestForm(argsForForm....):
	return Form.Form(argsForForm..., insertValidator, updateValidator)

I’m not so sure I’d be filling form data in the general init method, unless you’re going to instantiate and throw away in one pass. But that’s orthogonal to the class customization problem you have.

Subclassing is cleaner, of course, but harder to work with in development of the base class. (Just don’t import the base class!)

1 Like

What, you don't like from datetime import datetime? :rofl: Makes sense I don't really either, this for me is a proof of concept that I started coding quickly during a slow work day and want to see if I can get working nicely. My entire application is just all GUI forms and they all go through the same or very similar process, so I am trying hard to abstract the common processes away.

I will give your way a shot. If you think this is ultimately a fools errand though please do let me know.

1 Like

It’s not a bad plan. I’d probably do the subclassing, but without handling data in __init__, just setting up runtime global data. I’d probably keep the base class and all subclasses in a one script module, so there wouldn’t be any reload issues. I’d also probably instantiate one of each in the script module. Then in use, you simply supply the container to the instance and method you wish to execute.

1 Like

Generally speaking, once I get my modules solidified and they aren’t updated - this should work fine otherwise in a production environment right?

Yes, but the downside is supreme annoyance during development vs the downside of using fully qualified names... which is they're ugly.

Another gotcha in Ignition's Python environment is that you have to catch Java exceptions too. A standard try except won't catch those. See: Using Try/Except to Dummy Proof & Check if SQL Tables/Columns Exist - #4 by pturmel

My entire application is just all GUI forms and they all go through the same or very similar process, so I am trying hard to abstract the common processes away.

That's the crux of the DRY principle, innit?

Anyway this is the real cringe :sweat_smile:

		if userId is None:
			self.userId = self.form_payload['userId']
                else:
                        self.userId = userId

when you could

self.userId = userId or self.form_payload['userId']
1 Like

I was definitely self conscious posting so much of my own code, I know I hacked it together and was expecting it to be torn apart, especially since over other places on this forum I consistantly talk about writing good python lol. Tbh I didn't think that syntax was available in 2.7 but good to know!

Ha trust me I'm aware of java.lang.Exception with SQL errors. That's ANOTHER thing I am trying to refactor for all my forms. Before my time all the validation was done with a sql query - does this name exist? If not, ok then we can insert! When really we should just be using UNIQUE columns, try/excepting inserts with the java.lang.Exception and then examining the error.

Yea I am taking it one step at a time. When I first got my hands on this project the entirety was basically Magic Pushbutton anti patterns everywhere. Now I have sorted each form into its own module, but they all look so similar, following the same set of steps, so I am trying to take it one step further with a class.

Oh yeah, fully understood, just talking ish. Figured you could take it after the datetime comment lmao

Refactoring is part of the process, no need to be self conscious. You can't accurately judge someone's ability based on a single code snippet anyway

Before my time all the validation was done with a sql query - does this name exist? If not, ok then we can insert!

My last gig had auto-increment columns handled with a "SELECT ID FROM table ORDER BY ID DESC" :slight_smile:

imo you're thinking about this the right way and asking the right questions. Those are the problems to solve for a better system

1 Like

HA I’ve seen that too here. Well a ID column that’s not auto-incrementing (bleh enough as is) but then for the next insert has a MAX(id)+1 to calculate the next ID. just whyyyyy.

And lol its fine I can take it. Thanks yea it definitely seems like there has to be a better way. I’ve seen

def create():
    validateNew()
    modifyNew()
    insert()

over enough modules to think there’s a better way.

One last idea I have but is probably problematic in its own def insertForm(data, config, post_insert=None) general function where i feed a configuration dictionary along with the data dictionary. Config dictionary that says what sort of validation/modification to do run by string name and then hardcode those functions underneath insertFunction - those too are very formulaic and repeated a lot and I already have functions for. Also have a parameter post_insert=None for forms that have me do stuff after insertion.

@pturmel Any security threats or otherwise problematic issues with this way over the classes?

As long as you are using proper parameterization on user-supplied value throughout, no particular threats.

No, just that anyone following you will have to learn the method to your madness. (:

1 Like

I never had my own walled garden software before!

No way! Not once he gets his modules solidified and they aren’t updated!

2 Likes

After a lot of consideration I have decided to leave things exactly as they are. Each form has it’s own module which is completely self contained. I do appreciate the input though and only by talking it through have I come to this conclusion.

1 Like