Quickstart

Re-signing a function

The primary purpose of forge is to alter the public signature of functions:

import forge

@forge.sign(
    forge.pos('positional'),
    forge.arg('argument'),
    *forge.args,
    keyword=forge.kwarg(),
    **forge.kwargs,
)
def myfunc(*args, **kwargs):
    return (args, kwargs)

assert forge.stringify_callable(myfunc) == \
    'myfunc(positional, /, argument, *args, keyword, **kwargs)'

args, kwargs = myfunc(1, 2, 3, 4, 5, keyword='abc', extra='xyz')

assert args == (3, 4, 5)
assert kwargs == {
    'positional': 1,
    'argument': 2,
    'keyword': 'abc',
    'extra': 'xyz',
    }

You can re-map a parameter to a different ParameterKind (e.g. positional-only to positional-or-keyword or keyword-only), and optionally supply a default value:

import forge

@forge.sign(forge.kwarg('color', 'colour', default='blue'))
def myfunc(colour):
    return colour

assert forge.stringify_callable(myfunc) == "myfunc(*, color='blue')"
assert myfunc() == 'blue'

You can also supply type annotations for usage with linters like mypy:

import forge

@forge.sign(forge.arg('number', type=int))
@forge.returns(str)
def to_str(number):
    return str(number)

assert forge.stringify_callable(to_str) == 'to_str(number:int) -> str'
assert to_str(3) == '3'

Validating a parameter

You can validate arguments by either passing a validator or an iterable (such as a list or tuple) of validators to your FParameter constructor.

import forge

class Present:
    pass

def validate_gt5(ctx, name, value):
    if value < 5:
        raise TypeError("{name} must be >= 5".format(name=name))

@forge.sign(forge.arg('count', validator=validate_gt5))
def send_presents(count):
    return [Present() for i in range(count)]

assert forge.stringify_callable(send_presents) == 'send_presents(count)'

try:
    send_presents(3)
except TypeError as exc:
    assert exc.args[0] == "count must be >= 5"

sent = send_presents(5)
assert len(sent) == 5
for p in sent:
    assert isinstance(p, Present)

You can optionally provide a context parameter, such as self, cls, or create your own named parameter with forge.ctx('myparam'), and use that alongside validation:

import forge

def validate_color(ctx, name, value):
    if value not in ctx.colors:
        raise TypeError(
            'expected one of {ctx.colors}, received {value}'.\
            format(ctx=ctx, value=value)
        )

class ColorSelector:
    def __init__(self, *colors):
        self.colors = colors
        self.selected = None

    @forge.sign(
        forge.self,
        forge.arg('color', validator=validate_color)
    )
    def select_color(self, color):
        self.selected = color

cs = ColorSelector('red', 'green', 'blue')

try:
    cs.select_color('orange')
except TypeError as exc:
    assert exc.args[0] == \
        "expected one of ('red', 'green', 'blue'), received orange"

cs.select_color('red')
assert cs.selected == 'red'

Converting a parameter

You can convert an argument by passing a conversion function to your FParameter constructor.

import forge

def uppercase(ctx, name, value):
    return value.upper()

@forge.sign(forge.arg('message', converter=uppercase))
def shout(message):
    return message

assert shout('hello over there') == 'HELLO OVER THERE'

You can optionally provide a context parameter, such as self, cls, or create your own named FParameter with forge.ctx('myparam'), and use that alongside conversion:

import forge

def titleize(ctx, name, value):
    return '{ctx.title} {value}'.format(ctx=ctx, value=value)

class RoleAnnouncer:
    def __init__(self, title):
        self.title = title

    @forge.sign(forge.self, forge.arg('name', converter=titleize))
    def announce(self, name):
        return 'Now announcing {name}!'.format(name=name)

doctor_ra = RoleAnnouncer('Doctor')
captain_ra = RoleAnnouncer('Captain')

assert doctor_ra.announce('Strangelove') == \
    "Now announcing Doctor Strangelove!"
assert captain_ra.announce('Lionel Mandrake') == \
    "Now announcing Captain Lionel Mandrake!"