Skip to main content

Collection of Utilities

Project description

wakatime

worktoy v0.99.xx

This library leverages the most advanced Python features including the descriptor protocol and support for customizable metaclasses. The documentation below explains these features and how worktoy leverages them to provide powerful and flexible tools for Python developers.

Table of Contents

Installation

The stable version of worktoy may be installed using the following command:

pip install worktoy

The development version, which is not for the faint of heart, may be installed by passing the --pre flag:

pip install worktoy --pre

Introduction

The usage section aims to provide a concise explanation of how to use the worktoy library. A much more in depth explanation of the contents is under development.

Usage

This section explains the packages included in the worktoy library in the order they are imported.

worktoy.text

This package provides functions for manipulating text. The functions are very simple but widely used across worktoy.

worktoy.text.stringList

This function saves you a lot of quotation marks:

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

from worktoy.text import stringList

if __name__ == '__main__':
  foo = ['so', 'many', 'quotation', 'marks!']
  bar = stringList("""so, many, quotation, marks!""")
  print(foo == bar)  # True

Just write a string with comma, space separated words and the function will return a list of the words, providing a much more convenient way of defining a list of strings.

worktoy.text.monoSpace

Python provides a convenient way of defining long strings using triple quotes. However, when including new lines in such a string it is likely done for code readability, rather than requiring a linebreak at that particular position. The monoSpace function modifies a string to have only single spaces between words and no leading or trailing spaces. If a linebreak is intended, include '<br>' in the string to force a linebreak.

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

from worktoy.text import monoSpace

if __name__ == '__main__':
  foo = """Welcome to the monoSpace documentation! <br> After that 
  convenient linebreak, we are done!"""
  bar = monoSpace(foo)
  print(bar)

The above outputs:

Welcome to the monoSpace documentation!
After that convenient linebreak, we are done!

Inclusion of '<br>' explicitly forces a linebreak. Otherwise, the function removes all linebreaks and multiple spaces between words.

worktoy.text.wordWrap

This function takes a string and splits it into lines not exceeding a specified width and returns a list of the lines.

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

from worktoy.text import wordWrap

if __name__ == '__main__':
  foo = """This is a long string that needs to be wrapped. It is 
  important that the wrapping is done correctly. Otherwise, the text 
  will not be readable. """
  lineWidth: int = 40  # The line must not exceed 40 characters
  bar: list[str] = wordWrap(lineWidth, foo)  # Returns a list
  for line in bar:
    print(line)

The above outputs:

This is a long string that needs to be
wrapped. It is important that the
wrapping is done correctly. Otherwise,
the text will not be readable.

worktoy.text.typeMsg

When type-guarding a particular variable, an unsupported type should result in a TypeError. The typeMsg function provides a convenient way to raise a TypeError with a custom message if the type is not supported.

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

import sys

from worktoy.text import typeMsg, wordWrap


def foo(bar: int) -> None:
  if not isinstance(bar, int):
    e = typeMsg('bar', bar, int)
    raise TypeError(e)
  print(bar)


if __name__ == '__main__':
  susBar = 'sixty-nine'
  try:
    foo(susBar)  # That's not an int!
  except TypeError as typeError:
    errorMsg = str(typeError)  # Let's wrap this string at 50 characters
    wrapped = wordWrap(50, errorMsg)  # We apply 'str.join' to the list
    msg = '\n'.join(wrapped)
    print(msg)
    sys.exit(0)

The above outputs the following:

Expected object 'bar' to be of type 'int', but
found 'sixty-nine' of type 'str'!

worktoy.text.joinWords

Recall the stringList function mentioned earlier. The joinwords function does nearly the opposite: It takes a list of strings and joins them to one string with commas in between, except for in between the last two words where an 'and' is used.

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

from worktoy.text import joinWords

if __name__ == '__main__':
  foo = ['Tom', 'Dick', 'Harry']
  print(joinWords(foo[0]))  # 'Tom'
  print(joinWords(*foo[:2]))  # 'Tom and Dick'
  print(joinWords(*foo[:3]))  # 'Tom, Dick and Harry'

The above outputs the following:

Tom
Tom and Dick
Tom, Dick and Harry

In summary, worktoy.text provides the following:

  • stringList
  • monoSpace
  • wordWrap
  • typeMsg
  • joinWords

worktoy.parse

This module provides the None-aware maybe function.

worktoy.parse.maybe

Simplify identity-checks with the maybe function.

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

from typing import Any
from worktoy.parse import maybe

fallback = 69.


def verboseFunc(arg: float = None) -> Any:
  """Verbose identity check"""
  if arg is None:
    val = fallback
  else:
    val = arg


def syntacticSugarFunc(arg: float = None) -> Any:
  """Syntactic sugar alternative"""
  val = maybe(arg, fallback)

This function takes any number of arguments and returns the first that is different from None.

worktoy.desc

This module provides classes implementing the descriptor protocol

worktoy.desc.Field

Python allows significant customization of the attribute access mechanism through the descriptor protocol. Use GET, SET and DELETE to specify the accessor methods. For example:

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

import sys
from typing import Never

from worktoy.desc import Field


class Point:
  """A point in 2D space"""

  _x = 0.0  # Private value
  _y = 0.0  # Private value

  x = Field()
  y = Field()

  @x.GET  # Specify the getter method
  def _getX(self) -> float:
    return self._x

  @x.SET  # Specify the setter method
  def _setX(self, value: float) -> None:
    self._x = float(value)  # cast to float

  @x.DELETE  # Specify the deleter method as appropriate
  def _delX(self, *_) -> Never:
    """Deleter methods are rarely used in practice. This is because 
    deviating from expected behaviour can lead to undefined behaviour 
    when other libraries expect the default behaviour. Particularly, when 
    a custom implementation fails to raise an expected error. Unless 
    specifically needed, it is advisable to omit the deleter method. The 
    safest option is to explicitly raise an error like done here. """
    e = """Tried deleting protected attribute!"""
    raise TypeError(e)

  #  Accessor methods for 'y' left as an exercise to the try-hard reader.


if __name__ == '__main__':
  point = Point()
  print(point.x)  # Getter function returns default value
  point.x = 69.  # Setter function changes the value
  print(point.x)  # Getter function returns new value
  try:
    del point.x  # Deleter function raises an error
  except TypeError as typeError:
    print('%s: %s' % (typeError.__class__.__name__, str(typeError)))
  sys.exit(0)

The above outputs the following:

0.0
69.0
TypeError: Tried deleting protected attribute!

In summary, the Field class provides a descriptor implementation that allows the owning class to entirely define how the attribute is accessed.

worktoy.desc.AttriBox

Where the Field class required the accessor methods to be explicitly implemented by the owning class, the AttriBox class provides a highly general descriptor implementation requiring only one line in the owning class body. It uses a powerful and novel syntax. The attribute can point to any object of any type: attr = AttriBox[cls](*args, **kwargs) This creates a descriptor instance pointing to an instance of cls. The default value is created only when necessary by passing the given arguments to the constructor of cls.

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

import sys

from worktoy.parse import maybe
from worktoy.desc import AttriBox


class Point:
  """A point in 2D space"""

  x = AttriBox[int](0)
  y = AttriBox[int](0)

  def __init__(self, x: int = None, y: int = None) -> None:
    self.x = maybe(x, 0)
    self.y = maybe(y, 0)
    print("""Created: %s""" % str(self))  # Logs creation of Point instance

  def __str__(self, ) -> str:
    return """Point: (%d, %d)""" % (self.x, self.y)


class Circle:
  """A circle in 2D space"""

  center = AttriBox[Point](69, 420)
  radius = AttriBox[float](1.337)

  def __init__(self, *args) -> None:
    """The constructor may optionally receive a Point object as the
    center of the circle. """
    for arg in args:
      if isinstance(arg, Point):
        self.center = arg
        break

  def __str__(self, ) -> str:
    return """Circle spanning .3f% from %s""" % (self.radius, self.center)


if __name__ == '__main__':
  circle = Circle()
  #  This creates the object 'circle' as an instance of 'Circle', however, 
  #  the 'circle.center' object does not actually exist yet. 
  print("""Created instance of 'Circle'""")
  P = circle.center
  print(P)
  #  The 'circle.center' object is created when accessed here. AttriBox 
  #  creates the object by passing the given arguments to the constructor 
  #  of the given class, in this case: 'Point(69, 420)'. This triggers the
  #  print statement in the '__init__' method of the 'Point' class.
  Q = circle.center
  #  Now that the object exists, the existing object is returned, so 
  #  there is no output from the '__init__' method of the 'Point' class.
  if P is not Q:
    raise ValueError
  newCenter = Point(1337, 80085)
  newCircle = Circle(newCenter)
  #  This creates a new circle with the center at the same point as the
  #  previous circle. The 'Point' object is passed to the constructor of
  #  the 'Circle' class, which assigns it to the 'center' attribute.
  #  Because the attribute is set to a specific object, before it is ever 
  #  otherwise accessed, 'AttriBox' never creates a new object. Instead, 
  #  it makes use of the object passed in the constructor.
  print(newCircle.center)
  print(newCircle)
  if newCircle.center is not newCenter:
    raise ValueError
  sys.exit(0)

The above outputs the following:

Created instance of 'Circle'
Created: Point: (69, 420)
Point: (69, 420)
Created: Point: (1337, 80085)
Point: (1337, 80085)
Circle spanning 1.337 from Point: (1337, 80085)

The terminal output above shows the ordering of events: The Circle instance is created before the center Point is even created. AttriBox creates a Point instance when the center attribute is accessed. It passes the arguments on to the constructor of the Point class. The created Point instance is then assigned to the center attribute of the Circle instance. When setting Q to circle.center, the object created previously is returned. Thus, P is Q is True.

Next, the newCenter is created and is passed to the Circle constructor. When the print statement then accesses the center attribute on the new Circle instance, the existing Point instance is returned by AttriBox.

worktoy.desc.THIS

The THIS object is a novel and powerful feature of the worktoy library.

In the previous example of AttriBox, the Point class was instantiated with the arguments 69 and 420, when creating the circle.center object. When AttriBox receives the THIS object as an argument, it passes the owning instance to the constructor.

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations
from worktoy.desc import AttriBox, THIS


class Owner:
  """A class that uses THIS to pass itself to the attribute"""

  name = AttriBox[str]('John Doe')

  def __init__(self, name: str = None) -> None:
    if isinstance(name, str):
      self.name = name

  def __str__(self) -> str:
    return """Owner named: %s""" % self.name


class Dependent:
  """A class that gets initialized with an instance of Owner"""

  owner = AttriBox[Owner]()

  def __init__(self, composition: object) -> None:
    owner = getattr(composition, 'owner', None)
    if owner is None:
      raise ValueError
    self.owner = owner

  def __str__(self) -> str:
    return """Dependent of: '%s'""" % self.owner


class Composition:
  """Composition class demonstrating use of THIS with AttriBox"""

  owner = AttriBox[Owner]('Jack Doe')
  dependent = AttriBox[Dependent](THIS)

  def __init__(self) -> None:
    pass


if __name__ == '__main__':
  comp = Composition()
  print(comp.owner)  # Instantiates Owner with 'Asger'
  print(comp.dependent)  # Instantiates Dependent with the Owner instance

The above outputs the following:

Owner named: Jack Doe
Dependent of: 'Owner named: Jack Doe'

When a class is created in Python the code body is executed before the class is created. Thus, the class body itself is not able to reference the class itself directly. When instantiating AttriBox arguments to be passed to the constructor of the field class can still include the instance of the class being accessed. To achieve this, the THIS object should be passed as an argument to the AttriBox instance. In the same style, TYPE would refer to the owner of the instance, BOX would refer to the AttriBox instance, and ATTR to the AttriBox (or subclass hereof) class object.

worktoy.base

worktoy provides two base classes for general use: BaseObject and FastObject. Both support function overloading. The latter does not allow dynamically created attributes and is faster due to the use of __slots__. All attributes on a FastObject subclass must be instances of AttriBox.

The @overload decorator allows for function overloading in Python. This allows a named function to support multiple argument signatures. Like AttriBox, the @overload decorator understands THIS to mean an instance of the class.

Below is an implementation of a complex number as a subclass of FastObject allowing multiple argument signatures in the constructor by leveraging the @overload decorator.

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

from typing import Self

from worktoy.base import FastObject, overload
from worktoy.desc import AttriBox, THIS
from worktoy.meta import DispatchException


class Complex(FastObject):
  """Complex number implementation using AttriBox"""

  RE = AttriBox[float](0.)
  IM = AttriBox[float](0.)

  @overload(float, float)
  def __init__(self, x: float, y: float) -> None:
    self.RE, self.IM = x, y

  @overload(complex)
  def __init__(self, z: complex) -> None:
    self.RE, self.IM = z.real, z.imag

  @overload(float)
  def __init__(self, x: float) -> None:
    self.__init__(x, 0.)

  @overload(tuple)
  def __init__(self, z: tuple) -> None:
    self.__init__(*z, )

  @overload(THIS)  # As explained in the previous section
  def __init__(self, other: Self) -> None:
    self.RE, self.IM = other.RE, other.IM

  def __str__(self) -> str:
    return """%.2f + %.2fJ""" % (self.RE, self.IM)

  def __add__(self, other: object) -> Self:
    if isinstance(other, Complex):
      return Complex(self.RE + other.RE, self.IM + other.IM)
    try:
      return self + Complex(other)
    except DispatchException:
      return NotImplemented

  #  Remaining arithmetic operations left as an exercise to the reader.


if __name__ == '__main__':
  z1 = Complex(69, 420)
  print(z1)
  z2 = Complex(z1)
  print(z2)
  z3 = Complex(69 + 420j)
  print(z3)
  z4 = Complex((69, 420))
  print(z4)
  w = Complex(1337, 80085)
  print(w)
  print(w + 69.)
  print(w + (69, 420))

The above outputs the following:

69.00 + 420.00J
69.00 + 420.00J
69.00 + 420.00J
69.00 + 420.00J
1337.00 + 80085.00J
1406.00 + 80085.00J
1406.00 + 80505.00J

The complex number implementation supports multiple argument signatures. The addition operator leverages the flexibility in the constructor to support multiple other types.

In summary, BaseObject and FastObject provide base classes for general use. They add function overloading to Python. The former retains the familiar flexibility allowing dynamic attribute creation, while the latter provides a significant increase in speed at the cost of this flexibility.

worktoy.keenum

KeeNum provides a flexible enumeration class. Instances are managed by internal integer values, leaving available public values that are never used or accessed by the internal logic. Subclasses provide iteration over their enumerated instances in the order they appear in the class body. If no value is passed to the auto function, then the key is used as the public value.

In the following example, we will create a normal class that encapsulates colors represented by red, green and blue values. We will then enumerate a number of common colors using the KeeNum class.

#  AGPL-3.0 license
#  Copyright (c) 2024 Asger Jon Vistisen
from __future__ import annotations

from worktoy.keenum import KeeNum, auto
from worktoy.base import FastObject, overload
from worktoy.desc import AttriBox, THIS


class RGB(FastObject):
  """Each color channel is an integer in the range 0-255 managed by 
  AttriBox instances."""

  red = AttriBox[int](0)
  green = AttriBox[int](0)
  blue = AttriBox[int](0)

  @overload(int, int, int)
  def __init__(self, r: int, g: int, b: int) -> None:
    self.red, self.green, self.blue = r, g, b

  @overload(int, int)
  def __init__(self, r: int, g: int) -> None:
    self.__init__(r, g, 0)

  @overload(int)
  def __init__(self, r: int) -> None:
    self.__init__(r, 0, 0)

  @overload(tuple)
  def __init__(self, rgb: tuple) -> None:
    self.__init__(*rgb)

  @overload(THIS)
  def __init__(self, other: RGB) -> None:
    self.red, self.green, self.blue = other.red, other.green, other.blue

  @overload()
  def __init__(self) -> None:
    self.__init__(0, 0, 0)

  def __str__(self) -> str:
    """Hex representation of the color"""
    return """#%02X%02X%02X""" % (self.red, self.green, self.blue)

  def __repr__(self, ) -> str:
    """Code representation"""
    return """RGB(%d, %d, %d)""" % (self.red, self.green, self.blue)


class Color(KeeNum):
  """Enumeration of common colors"""

  RED = auto(RGB(255, 0, 0))
  GREEN = auto(RGB(0, 255, 0))
  BLUE = auto(RGB(0, 0, 255))
  YELLOW = auto(RGB(255, 255, 0))
  CYAN = auto(RGB(0, 255, 255))
  MAGENTA = auto(RGB(255, 0, 255))
  WHITE = auto(RGB(255, 255, 255))
  BLACK = auto(RGB(0, 0, 0))

  def __str__(self) -> str:
    name = str.capitalize(self.name)
    return """%s: %s""" % (name, self.value)


if __name__ == '__main__':
  for color in Color:
    print(color)

The above outputs the following:

Red: #FF0000
Green: #00FF00
Blue: #0000FF
Yellow: #FFFF00
Cyan: #00FFFF
Magenta: #FF00FF
White: #FFFFFF
Black: #000000

Project details


Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

worktoy-0.99.71.tar.gz (55.0 kB view details)

Uploaded Source

Built Distribution

worktoy-0.99.71-py3-none-any.whl (67.8 kB view details)

Uploaded Python 3

File details

Details for the file worktoy-0.99.71.tar.gz.

File metadata

  • Download URL: worktoy-0.99.71.tar.gz
  • Upload date:
  • Size: 55.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.12.7

File hashes

Hashes for worktoy-0.99.71.tar.gz
Algorithm Hash digest
SHA256 d99d666423433880f6e58b5f3a4a553e7f18e6e744e8d36f8dcb440935e7b4d4
MD5 79b7802463ac6b9cd9414abbcca06aef
BLAKE2b-256 9976812591e0e0bcb3b975e109aabc7e9479a89cb58769c2d17b3db93d9fab0a

See more details on using hashes here.

File details

Details for the file worktoy-0.99.71-py3-none-any.whl.

File metadata

  • Download URL: worktoy-0.99.71-py3-none-any.whl
  • Upload date:
  • Size: 67.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.12.7

File hashes

Hashes for worktoy-0.99.71-py3-none-any.whl
Algorithm Hash digest
SHA256 c87b0d60b2edd89344d00e53527db38de969ef399e0e500e9540fe796c29bdb8
MD5 7b97642a931504ff1a9ca1825b70ca58
BLAKE2b-256 1bd9967a035a8c6a628a1947ee602d4e7347ea55c4c95079a2607024822395cb

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page