Python-like Error System

Post your working scripts, libraries and tools
User avatar
Delta Pythagorean
Posts: 574
Joined: 13 Feb 2017, 13:44
GitHub: DelPyth
Location: Somewhere in the US

Python-like Error System

07 Jan 2021, 20:08

What is this?
This is a simple and easy to follow Error System for AutoHotkey.

Why should I use this? I already use <x>.
This is to help debug where your code went wrong and how to fix it. It works pretty much exactly how Python does their Error System.

Why not just throw an Exception?
Throwing an exception just to say there was a problem may work; however, it does not provide the easiest way to help debug your scripts.

Well, how do I use this?
Simple, wherever you want to throw an error for a problem that may have occurred, you do the following: throw new Exception("Explaination", ExtraInfo).
Replace Exception for the error type.
Replace "Explaination" for some help as to why the error was called, you can add "{extra}" within the string to add ExtraInfo into the string to help..
Replace ExtraInfo for any extra information, such as the variable that caused the problem (Optional).

Great, how to I "install" this?
Simply use #Include <ErrorLib> where "ErrorLib" is the name of the file you save this library to. It is recommended to include the code at the top.

Is there anything I need to worry about?
Besides the OnError call being overridden for this to work, nope.

Example:

Code: Select all

#include <ErrorLib>

main(args) {
	if (args.Count() < 1) {
		throw new Exception("expected 1 (one) or more arguments for this script, got {extra}", args.Count())
	}

	for index, value in args {
		if (value != "/close") {
			throw new ValueError("expected '/close' as argument", value)
		}
	}
}

; Same as `if __name__ == '__main__':` in Python.
; Basically just checks if this file (this example) is being called directly or being included in another script.
if (A_LineFile == A_ScriptFullPath) {
	try {
		main(A_Args)
	} catch error_object {
		switch (error_object.name) {
			case "ValueError":
				if (error_object.extra == "-hello_world") {
					print("Nothing to see here, just a friendly greeting :)")
				}

			; If we didn't expect the error, just print it.
			default:
				print(format_error_obj(error_object))
		}
	}
}

; Nothing else to do, so let's close the script.
ExitApp, 0
Spoiler

Code: Select all

/*
	****************************************************************************************
	*	Error.ahk:
	*	Just a few functions and error system useful for debugging.
	*	All errors return similar to Python's error system:
	*		Traceback (most recent call last):
	*		  File "C:\MyScripts\main.ahk", line 99, in <module>
	*		    custom_assert(10 + 10 = 12)
	*		Exception: assertion failed!
	****************************************************************************************
*/

Type(V, Assert = False) {
	Static nMatchObj	:= NumGet(&(m, RegExMatch(Null, "O)", m)))
	Static nBoundFunc	:= NumGet(&(F := Func("Func").Bind()))
	Static nFileObj		:= NumGet(&(F := FileOpen("*", "w")))
	Local  T			:= Null

	If (IsObject(V)) {
		T := (V.__Class != Null										?	"Class"
			:  V.SetCapacity(0) = (V.MaxIndex() - V.MinIndex() + 1)	?	"Array"
			:  ObjGetCapacity(V) != Null							?	"Object"
			:  IsFunc(V)											?	"Func"
			:  IsLabel(V)											?	"Label"
			:  ComObjType(V) != Null								?	"ComObject"
			:  NumGet(&V) == nBoundFunc								?	"BoundFunc"
			:  NumGet(&V) == nMatchObj								?	"RegExMatchObject"
			:  NumGet(&V) == nFileObj								?	"FileObject"
			:														.	"Property")
	} Else {
		If (V == Null) {
			T := "Undefined"
		} Else {
			T := ((ObjGetCapacity([V], 1) != Null) ? ("String") : (InStr(V, ".") ? "Float" : "Integer"))
		}
	}

	If (Assert != False) {
		If (InStr(Assert, "|")) {
			For I, Val in StrSplit(Assert, "|") {
				If (InStr(T, Val)) {
					Return True
				}
			}
			Return False
		} Else If (Type(Assert) = "Array") {
			For I, Val in Assert {
				If (InStr(T, Val)) {
					Return True
				}
			}
			Return False
		}
	}
	Return T
}

ParseArray(Obj, Depth = 10, IndentLevel = "  ", LimitCount = -1, FormatEscapes = True) {
	; Read through an array and list the variables values and names.
	If (type(Obj) != "object") {
		Return Obj
	}
	For k, v in Obj {
		If (type(v, "object") && (Depth > 1)) {
			If (type(v.name) = "func") {
				middlepart .= "func(""" . v.name . """)`r`n"
				If (type(k) = "integer") {
					ToReturn .= IndentLevel . "- " . middlepart
				} Else {
					If (FormatEscapes == True) {
						k := StrReplace(k, "`t", "\t")
						k := StrReplace(k, "`r", "\r")
						k := StrReplace(k, "`n", "\n")
					}

					ToReturn .= IndentLevel . k . ": " . middlepart
				}
			} Else {
				middlepart := "`r`n" . ParseArray(v, Depth - 1, IndentLevel . "  ", LimitCount, FormatEscapes)
				If (type(k) = "integer") {
					ToReturn .= IndentLevel . "- " . middlepart
				} Else {
					If (FormatEscapes == True) {
						k := StrReplace(k, "`t", "\t")
						k := StrReplace(k, "`r", "\r")
						k := StrReplace(k, "`n", "\n")
					}

					ToReturn .= IndentLevel . k . ": " . middlepart
				}
			}
		} Else {
			If (FormatEscapes == True) {
				v := StrReplace(v, "`t", "\t")
				v := StrReplace(v, "`r", "\r")
				v := StrReplace(v, "`n", "\n")
			}

			If (k ~= "^[0-9]$") {
				v := ((LimitCount != -1) ? ((StrLen(v) > LimitCount) ? (SubStr(v, 1, LimitCount) . " [...]") : (v)) : (v))
				ToReturn .= IndentLevel . "- " . v . "`r`n"
			} Else {
				If (FormatEscapes == True) {
					k := StrReplace(k, "`t", "\t")
					k := StrReplace(k, "`r", "\r")
					k := StrReplace(k, "`n", "\n")
				}

				v := ((LimitCount != -1) ? ((StrLen(v) > LimitCount) ? (SubStr(v, 1, LimitCount) . " [...]") : (v)) : (v))
				ToReturn .= IndentLevel . k . ": " . v . "`r`n"
			}
		}
	}
	; Since I'm too lazy to redo this function to not have this happen:
	;  vars:
	;    -
	;      name: "Jay"
	; When this should happen:
	;  vars:
	;    - name: "Jay"
	; I just use this:
	ToReturn := RegExReplace(ToReturn, "\- \R {2,}", "- ")
	Return RTrim(ToReturn)
}

print(msg, args*) {
	result := ""
	last_arg := args[args.count()]
	file := "*"
	end := "`n"

	if (type(last_arg) = "object") {
		file := ((last_arg.file != Null) ? (last_arg.file) : (file))
		end  := ((last_arg.end != Null) ? (last_arg.end) : (end))
	}

	if (type(msg) = "object") {
		result := ParseArray(msg)
	} else {
		msg := StrReplace(msg, "{", "{{}")
		msg := StrReplace(msg, "}", "{}}")
		for index in args {
			; print("  name: %1`n  fav_color: %2:x%", "Bob", 123456)
			msg := RegExReplace(msg, "\`%([0-9]{1,3})(?:\:(.+)?\`%)?", "{$1:$2}")
		}

		result := Format(msg, args*)
	}

	switch (file) {
		case "OutputDebug":
			OutputDebug, % result . end
		case "Return":
			return (result . end)
		default:
			FileAppend, % result . end, % file
	}
	return Null
}


; If an error was thrown and not caught by an upper layer in the script, this function is called.
__control_error(Error) {
	; We add this static call here to set the error function to be this
	static init := OnError("__control_error")

	print(format_error_obj(error))
	ExitApp, 1
	return True
}

; Format error objects created by the Exception classes and its heirs.
format_error_obj(error_object) {
	if (error_object.HasKey("stack_trace")) {
		; Add some info for what this is in this error message.
		; Basically just a tip for people reading the error.
		str := "Traceback (most recent call last):`n"

		; For each deep callback in the error, we give where it was called from.
		; <main> is the master scope. Basically the start of where the function was called.
		loop, % error_object.stack_trace.count() - 1 {
			i := error_object.stack_trace.count() - (A_Index - 1)
			obj := error_object.stack_trace[i]

			str .= Format("  File ""{1:}"", line {2:}, in {3:}`n", obj.file, obj.line, obj.where)
			if (!A_IsCompiled) {
				FileReadLine, line, % obj.file, % obj.line
				str .= "    " . Trim(line) . "`n"
			}
		}

		; Then we give what the error was and why it went wrong.
		str .= error_object.name . ": " . StrReplace(error_object.message, "{extra}", error_object.extra)
	} else {
		spec := ((error_object.Extra != Null) ? ("`n     Specifically: {4:}"))
		str := Format("{1:} ({2:}) : ==> {3:}" . spec
			, error_object.File, error_object.Line, error_object.Message, error_object.Extra)
	}
	return str
}

class Exception {
	__new(msg = "", extra = "", skip_frames = -1) {
		/*
			; To get the top-most called function/method we do this... thing.
			; Skipped frames means how far back we start from.
			Class MyClass {		; -2
				my_func() {		; -1
					my_call()	; 0 (default)
				}
			}
		*/
		while ((stk := Exception(Null, 0 - A_Index - 1)).What != (0 - A_Index - 1)) {
			if (skip_frames-- <= 0) {
				break
			}
		}

		; The built-in error dialog requires that these be set raw.
		ObjRawSet(this, "Message", msg)
		ObjRawSet(this, "Extra", ((type(extra) = "object") ? (ParseArray(extra)) : (extra)))
		ObjRawSet(this, "File", stk.File)
		ObjRawSet(this, "Line", stk.Line)
		ObjRawSet(this, "Name", this.__class)

		; Since the built-in error dialog doesn't use this variable, we're free to set it normally.
		this.stack_trace := Exception.stack_trace(skip_frames)
		return this
	}

	stack_trace(n := -1) {
		trace := []
		next := Exception(Null, n)
		while ((stk := next).What != n) {
			next := Exception("", --n)
			ctx := (next.What < 0 ? "<module>" : next.What)
			trace.push({file: stk.File, line: stk.Line, where: ctx})
		}
		return trace
	}
}

; Value's type is not expected (expected string, got integer).
class TypeError extends Exception {
	__new(p*) {
		p[3] := (p[3] ? (p[3] - 1) : (-2))
		base.__new(p*)
		if (ObjHasKey(p, 2) && (type(p[2]) != "object")) {
			this.Extra .= " (" . type(p[2]) . ")"
		}
		ObjRawSet(this, "Name", this.__class)
	}
}

; Getting/setting a member within a class that doesn't exist.
; Members are keys within a class that are protected or private. Usually beginning with an underscore "_", or double underscore "__".
; This is mostly used inside __get or __set.
class MemberError extends Exception {
}

; Getting/setting a property within a class that doesn't exist.
; Properties are named members of classes.
; This is mostly used inside __get or __set.
class PropertyError extends MemberError {
}

; For when a method is attempted to be called that does not exist.
; This is mostly used inside __call.
class MethodError extends MemberError {
}

; For when the value of a variable/return value/parameter is not expected (expected between 1 and 10, got 22).
class ValueError extends Exception {
}

; For when the number of an array or key of an object does not exist.
class IndexError extends ValueError {
}

- [AHK].......: 1.1.33.02 Unicode 64-bit
- [OS].........: Windows 10.0.19041
- [GITHUB]...: github.com/DeltaPyth
- [PAYPAL]....: paypal.me/DelPyth
- [DISCORD]..: Tophat Cat // Delta#8888

Remember to use [code]CODE[/code] for your multi-line scripts.
Stay safe, stay inside, and remember to wash your hands for 20 seconds!
guest3456
Posts: 3161
Joined: 09 Oct 2013, 10:31

Re: Python-like Error System

09 Jan 2021, 01:43

for those of us who are not familiar with python, what new features / usability does this provide that normal ahk doesn't?

User avatar
Delta Pythagorean
Posts: 574
Joined: 13 Feb 2017, 13:44
GitHub: DelPyth
Location: Somewhere in the US

Re: Python-like Error System

09 Jan 2021, 15:41

guest3456 wrote:
09 Jan 2021, 01:43
for those of us who are not familiar with python, what new features / usability does this provide that normal ahk doesn't?
I guess I didn't think about that, that's my bad ^^;

The main difference between AHK's errors and Python's errors are the ways you can debug them and understand where your script went wrong.
Here's the main difference, notice how in the Python-like error there is a trace-back to where the call started.
Python_Error.png
Image showing how the Python-like error system gives much more detail and shows trace-back to first call.
Python_Error.png (66.06 KiB) Viewed 166 times
However in AHK's error system, it provides little to no help on where the call first started, leaving the user to put message boxes, tooltips, traytips, and other assortments to find which call made the error.
AHK_Error.png
Image showing how AHK's error system gives only where the call went wrong.
AHK_Error.png (78.9 KiB) Viewed 166 times
If there's anything else you have questions about, feel free to ask :)

- [AHK].......: 1.1.33.02 Unicode 64-bit
- [OS].........: Windows 10.0.19041
- [GITHUB]...: github.com/DeltaPyth
- [PAYPAL]....: paypal.me/DelPyth
- [DISCORD]..: Tophat Cat // Delta#8888

Remember to use [code]CODE[/code] for your multi-line scripts.
Stay safe, stay inside, and remember to wash your hands for 20 seconds!
guest3456
Posts: 3161
Joined: 09 Oct 2013, 10:31

Re: Python-like Error System

09 Jan 2021, 21:52

interesting thank you


Return to “Scripts and Functions”

Who is online

Users browsing this forum: viv and 14 guests