Skip to main content

Collection of Utilities

Project description

wakatime codecov PyPI version License: AGPL v3

worktoy v0.99.xx

The worktoy provides utilities for Python development focused on reducing boilerplate code, type-safety and readability. Each release is tested thoroughly on each supported Python version from 3.7* to 3.14.

*Maybe it is time to consider updating if you are still using Python 3.7.

Table of Contents

Installation

Install with pip:

pip install worktoy

Python Is Easy. Too Easy!

What enables effortless prototyping does not guarantee the scalable structure serious applications demand.

Introducing worktoy.

A structural layer for Python that adds deliberate constraints without sacrificing ergonomics. It brings the architectural discipline of statically typed languages to dynamic Python.

Build the GUI. Build the logic. Build the architecture. All in Python.

'Trust-Me-Bro'-Typing

class Point:
  def __init__(self, x: float = 0.0, y: float = 0.0) -> None:
    self.x = x
    self.y = y

The above code is perfectly valid Python, it even includes types. Or does it? Those float annotations are not there at runtime. Basically, it is 'trust-me-bro'-typing. Point('breh', None) will happily create a Point object.

Instead:

class Point:
  x = AttriBox[float](0.0)
  y = AttriBox[float](0.0)

  def __init__(self, x: float = 0.0, y: float = 0.0) -> None:
    self.x = x
    self.y = y

When AttriBox says float it enforces float at runtime. Attributes are declared explicitly at the class level. Despite this, flexibility remains, for example:

point = Point(69, '420')  # int, str
point.x == 69.0
point.y == 420.0

When types do not match, AttriBox attempts casting before raising an error. Same ergonomics. Stronger guarantees.

The Python Parsing Situation Is Crazy

Python is not always easy though. Consider the Point implementation under discussion. Suppose we wanted a flexible constructor. One that supports instantiation on:

  • a pair of float objects
  • a complex number
  • another Point object

That is possible in Python, for example:

class Point:

  def __init__(self, *args, ) -> None:
    if len(args) == 2:
      self.x = float(args[0])
      self.y = float(args[1])
    elif len(args) == 1:
      if isinstance(args[0], complex):
        self.x = args[0].real
        self.y = args[0].imag
      elif isinstance(args[0], type(self)):
        self.x = args[0].x
        self.y = args[0].y
    else:
      raise TypeError('Invalid arguments')

Conditional branches. Growing complexity. Manuel parsing. Long gone are those happy days of effortless coding.

But it does not have to be like this. Introducing @overload:

from __future__ import annotations

from typing import Self

from worktoy.mcls import BaseObject
from worktoy.core.sentinels import THIS
from worktoy.dispatch import overload
from worktoy.desc import AttriBox


class Point(BaseObject):
  x = AttriBox[float](0.0)
  y = AttriBox[float](0.0)

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

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

  @overload(THIS)  # THIS is a special token type-hinting to the class itself
  def __init__(self, other: Self) -> None:
    self.x = other.x
    self.y = other.y

Each new signature requires one new overloaded function. No more painful parsing of *args. All of this just works. Actually.

"Show Don't Tell" Is for Stories, not for Code!

When reading code, you look for declarations. For where symbols are defined. For where meaning begins.

Narrative storytelling is different. The method by which information is conveyed is itself part of the the artistic expression. The way information is revealed is frequently as important as the information itself. In Clair Obscur: Expedition 33, the horror of the Gomache unfolds gradually until Sophie disappears in Gustave's arms. The imperative subtlety grants the story its emotional impact.

In code, the declaration is the point! In matters of code, I want declarations. I don’t want foreshadowing. I don’t want subtlety. I don’t want subversion of expectations. I want declarations.

Anyway, what were we talking about? Right — figure out what point.r is from the code below:

class Point:
  def __init__(self, *args, ) -> None:
    if len(args) == 2:
      self.x = float(args[0])
      self.y = float(args[1])
    elif len(args) == 1:
      if isinstance(args[0], complex):
        self.x = args[0].real
        self.y = args[0].imag
      elif isinstance(args[0], type(self)):
        self.x = args[0].x
        self.y = args[0].y
    else:
      raise TypeError('Invalid arguments')

  @property
  def r(self) -> float:
    return (self.x ** 2 + self.y ** 2) ** 0.5

Great, you found it. Well, you found what it does, and you inferred it. This is imperative declaration. In Python, this is fine. It is much worse in other languages. Anyway, here is the alternative provided by worktoy: Field.

class Point(BaseObject):
  x = AttriBox[float](0.0)
  y = AttriBox[float](0.0)

  r = Field()  # Straight up declaration! 

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

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

  @overload(THIS)  # THIS is a special token type-hinting to the class itself
  def __init__(self, other: Self) -> None:
    self.x = other.x
    self.y = other.y

  @r.GET  # Straight up declaration of something called 'GET'.
  def _getR(self) -> float:
    return (self.x ** 2 + self.y ** 2) ** 0.5

The r attribute is declared first. Next, the @r.GET declares that the method implementing the get operation comes next. The structure is visible separately from the behaviour.

Static Discipline

In plain Python, attributes assigned in __init__ are closer to dictionary entries than declared structure.

class Point:
  def __init__(self, x: float = 0.0, y: float = 0.0) -> None:
    self.x = x
    self.y = y

Inspecting the class reveals nothing about x or y. They do not exist at the class level. They are created at runtime on the instance. Two common remedies are __slots__ and annotations:

class Point:
  __slots__ = ('x', 'y')

  def __init__(self, *args) -> None: ...

or

class Point:
  x: float
  y: float

  def __init__(self, *args, ) -> None: ...  # Implementation as before

Both improve clarity. But in both cases Point.x and Point.y will raise AttributeError. The presence of x and y becomes visible only after instantiation. At this point, they are just attributes of the instance, not of the class. Setting during __init__ makes no difference compared to setting them anywhere else. Structure remains implicit.

With worktoy attributes are an essential part of the class structure on par with methods.

class Point(BaseObject):
  x = AttriBox[float](0.0)
  y = AttriBox[float](0.0)

  #  Implementation as before

Now x and y are declared at the class level, making them visible, inspectable and enforced. They are more than just keys in an instance dictionary. They are structural elements of the class. In plain Python, instances define structure. Here, the class does.

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-1.0.0rc0.tar.gz (155.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

worktoy-1.0.0rc0-py3-none-any.whl (235.6 kB view details)

Uploaded Python 3

File details

Details for the file worktoy-1.0.0rc0.tar.gz.

File metadata

  • Download URL: worktoy-1.0.0rc0.tar.gz
  • Upload date:
  • Size: 155.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for worktoy-1.0.0rc0.tar.gz
Algorithm Hash digest
SHA256 944064af9d7072e21f6a0f8796cd1530a6363780930a10dc297eb20405d73e07
MD5 f84fa8aaa8b4391b0b80ce9c32c7fca4
BLAKE2b-256 f220d59c3dd49e55b34c1352ac557c5d89c8495c790c092741ad84a7ad2bc728

See more details on using hashes here.

Provenance

The following attestation bundles were made for worktoy-1.0.0rc0.tar.gz:

Publisher: rc.yml on AsgerJon/WorkToy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file worktoy-1.0.0rc0-py3-none-any.whl.

File metadata

  • Download URL: worktoy-1.0.0rc0-py3-none-any.whl
  • Upload date:
  • Size: 235.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for worktoy-1.0.0rc0-py3-none-any.whl
Algorithm Hash digest
SHA256 455c6d8daa5c991ae648b37a8f84e898b1a918b55b1483b47ff1e2dc3b8fa2ed
MD5 7e00d3be5a2f830e9943e1cd21d1b8a8
BLAKE2b-256 b131ba25f32864e14179b13ee4fe006d4010edf796c26062b517aca7407438c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for worktoy-1.0.0rc0-py3-none-any.whl:

Publisher: rc.yml on AsgerJon/WorkToy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

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