StringEnum

StringEnum is a StrEnum that converts a string to the member of that value, and says so when it can’t.

Every enumeration whose members come from the outside - a command line, a configuration file, a REST reply - needs the same three answers: what a missing value means, what a value of the wrong type is, and what a value no member carries is. Written per enumeration, those answers drift; written once here, an enumeration adds its members and inherits Parse().

The default is declared as an alias

Default is an alias of the member that stands for “nothing was given”. An alias, because that keeps it out of the enumeration’s own list: it isn’t iterated, and it is not a second member to compare against - it is the member it aliases.

Example:

from pyTooling.Common import StringEnum

class GanttFormat(StringEnum):
  MatplotlibPNG = "matplotlib-png"
  MatplotlibSVG = "matplotlib-svg"

  Default = MatplotlibPNG

GanttFormat.Parse("matplotlib-svg")    # GanttFormat.MatplotlibSVG
GanttFormat.Parse(None)                # GanttFormat.MatplotlibPNG
list(GanttFormat)                      # [MatplotlibPNG, MatplotlibSVG] - no third entry

An enumeration declaring no Default answers None instead, which is what a field that may legitimately be absent wants - a workflow run has no conclusion while it is still running:

class Conclusion(StringEnum):
  Success = "success"
  Failure = "failure"

Conclusion.Parse(None)    # None
Conclusion.Parse("")      # None

What Parse rejects

Argument

Answer

None or ""

Default, or None if the enumeration declares none.

A value a member carries

That member.

A value no member carries

ValueError, naming the enumeration; the note lists the values it accepts.

Anything that isn’t a str

TypeError; the note reports the type that was given.

Example:

GanttFormat.Parse("matplotlib-gif")
# ValueError: 'matplotlib-gif' is not a valid GanttFormat.
#   Allowed values: matplotlib-png, matplotlib-svg.

GanttFormat.Parse(5)
# TypeError: Parameter 'value' is not of type 'str'.
#   Got type 'int'.

An enumeration with its own exception

Parse() raises a ValueError, which is what an unknown value is. An enumeration belonging to a domain that has its own exception overrides Parse, catches that ValueError and chains it as the cause.

The difference is visible to a user: pyTooling.CLI.main() prints a ToolingException as a message, while an unhandled ValueError reaches PrintException(), which prints a traceback and invites the user to open an issue. A value a service sent that pyTooling doesn’t know is that service’s problem, not a bug in pyTooling, so it wants the first.

Example:

class Status(StringEnum):
  Queued =     "queued"
  InProgress = "in_progress"
  Completed =  "completed"

  @classmethod
  def Parse(cls, value: Nullable[str]) -> Nullable[Self]:
    try:
      return super().Parse(value)
    except ValueError as ex:
      error = GitHubError(f"'{value}' is not a GitHub status.")
      error.add_note(f"Known: {', '.join(member.value for member in cls)}.")
      raise error from ex

The TypeError is deliberately not caught: a value of the wrong type is a defect at the call site, not a value the service chose, and it reads better as itself.

Status, Conclusion and Event are written that way.

A member is its value

Deriving from StrEnum rather than Enum means a member is a string: it goes into a message, an HTTP header or a filename without being unwrapped, and the whole enumeration joins into the list of values an option accepts.

Example:

f"Drawn as {GanttFormat.MatplotlibPNG}."      # 'Drawn as matplotlib-png.'
", ".join(GanttFormat)                        # 'matplotlib-png, matplotlib-svg'