Patterns and usage¶
Forge enables new software development patterns for Python. Selected patterns are documented below.
Model management¶
This pattern helps you work with objects and their corresponding management methods.
Commonly, models are generated by a metaclass
and convert declartive-style user-code into enhanced Python classes.
In this example we’ll write a service class that manages an Article
object.
For simplicity, we’ll use types.SimpleNamespace
rather than defining our own metaclass and field instances.
from types import SimpleNamespace
from datetime import datetime
import forge
class Article(SimpleNamespace):
pass
def create_article(title=None, text=None):
return Article(title=title, text=text, created_at=datetime.now())
@forge.copy(create_article)
def create_draft(**kwargs):
kwargs['title'] = kwargs['title'] or '(draft)'
return create_article(**kwargs)
assert forge.repr_callable(create_draft) == \
"create_draft(title=None, text=None)"
draft = create_draft()
assert draft.title == '(draft)'
As you can see, create_draft
no longer exposes the var-keyword parameter kwargs
.
Instead, it has the same function signature as create_article
.
And, as expected, passing a keyword-argument that’s not title
or text
raises a TypeError.
try:
create_draft(author='Abe Lincoln')
except TypeError as exc:
assert exc.args[0] == "create_draft() got an unexpected keyword argument 'author'"
As expected, create_draft
now raises an error when undefined keyword arguments are passed.
How about creating another method for editing the article?
Let’s keep in mind that we might want to erase the text
of the article, so a value of None
is significant.
In this example we’re going to use four revisions: compose
(to perform a batch of revisions), copy
(to copy another function’s signature), insert
(to add an additional parameter), and modify
(to alter one or more parameters).
@forge.compose(
forge.copy(create_article),
forge.insert(forge.arg('article'), index=0),
forge.modify(
lambda param: param.name != 'article',
multiple=True,
default=forge.void,
),
)
def edit_article(article, **kwargs):
for k, v in kwargs.items():
if v is not forge.void:
setattr(article, k, v)
assert forge.repr_callable(edit_article) == \
"edit_article(article, title=<void>, text=<void>)"
edit_article(draft, text='hello world')
assert draft.title == '(draft)'
assert draft.text == 'hello world'
As your Article
class gains more attributes (author
, tags
, status
, published_on
, etc.) the amount of effort to maintenance, update and test these parameters - or a subset of these parameters – becomes costly and taxing.
Var-keyword precision¶
This pattern is useful when you want to explicitly define which keyword-only parameters a callable takes. This is a useful alternative to provided a generic var-keyword and white-listing or black-listing parameters within the callable’s code.
Consider a library like requests that provides a useful API for performing HTTP requests.
Every HTTP method has it’s own function which is a thin wrapper around requests.Session.request
.
The code is a little more than 150 lines, with about 90% of that being boilerplate.
Using forge
we can get that back down to about 10% it’s current size, while increasing the literacy of the code.
import forge
import requests
request = forge.copy(requests.Session.request, exclude='self')(requests.request)
def with_method(method):
revised = forge.modify(
'method', default=method, bound=True,
kind=forge.FParameter.POSITIONAL_ONLY,
)(request)
revised.__name__ = method.lower()
return revised
post = with_method('POST')
get = with_method('GET')
put = with_method('PUT')
delete = with_method('DELETE')
options = with_method('OPTIONS')
head = with_method('HEAD')
patch = with_method('PATCH')
So what happened?
The first thing we did was create an alternate request
function to replace requests.request
that provides the exact same functionality but makes its parameters explicit:
# requests.get() looks like this:
assert forge.repr_callable(requests.get) == 'get(url, params=None, **kwargs)'
# our get() calls the same code, but looks like this:
assert forge.repr_callable(get) == (
'get(url, params=None, data=None, headers=None, cookies=None, '
'files=None, auth=None, timeout=None, allow_redirects=True, '
'proxies=None, hooks=None, stream=None, verify=None, cert=None, '
'json=None'
')'
)
Next, we built a factory function with_method
that creates new functions which make HTTP requests with the proper HTTP verb.
Because the method
parameter is bound, it won’t show up it is removed from the resulting functions signature.
Of course, the signature of these generated functions remains explicit, let’s try it out:
response = get('http://google.com')
assert 'Feeling Lucky' in response.text
You can review the alternate code (the actual implementation) by visiting the code for requests.api.
Void arguments¶
The void arguments
pattern allows quick-collection and filtering of arguments.
It is useful when None can not or should not be used as a default argument.
This code makes use of forge.void
.
Consider the situation where you’d like to make explicit the accepted arguments (i.e. not use the var-positional parameter **kwargs
), but None
can be used to nullify data (for example with an ORM).
import datetime
import forge
class Book:
__repo__ = {}
def __init__(self, id, title, author, publication_date):
self.id = id
self.title = title
self.author = author
self.publication_date = publication_date
@classmethod
def get(cls, book_id):
return cls.__repo__.get(book_id)
@classmethod
def create(cls, title, author, publication_date):
ins = cls(
id=len(cls.__repo__),
title=title,
author=author,
publication_date=publication_date,
)
cls.__repo__[ins.id] = ins
return ins.id
@classmethod
@forge.sign(
forge.cls,
forge.arg('book_id', 'book', converter=lambda ctx, name, value: ctx.get(value)),
forge.kwarg('title', default=forge.void),
forge.kwarg('author', default=forge.void),
forge.kwarg('publication_date', default=forge.void),
)
def update(cls, book, **kwargs):
for k, v in kwargs.items():
if v is not forge.void:
setattr(book, k, v)
assert forge.repr_callable(Book.update) == \
'update(book_id, *, title=<void>, author=<void>, publication_date=<void>)'
book_id = Book.create(
'Call of the Wild',
'John London',
datetime.date(1903, 8, 1),
)
Book.update(book_id, author='Jack London')
assert Book.get(book_id).author == 'Jack London'
Chameleon function¶
The chameleon function
pattern demonstrates the powerful functionality of forge
.
With this pattern, you gain the ability to dynamically revise a function’s signature on demand.
This could be useful for auto-discovered dependency injection.
import forge
@forge.compose()
def chameleon(*remove, **kwargs):
globals()['chameleon'] = forge.compose(
forge.copy(chameleon.__wrapped__),
forge.insert([
forge.kwo(k, default=v) for k, v in kwargs.items()
if k not in remove
], index=0),
forge.sort(),
)(chameleon)
return kwargs
# Initial use
assert forge.repr_callable(chameleon) == 'chameleon(*remove, **kwargs)'
# Empty call preserves signature
assert chameleon() == {}
assert forge.repr_callable(chameleon) == 'chameleon(*remove, **kwargs)'
# Var-keyword arguments add keyword-only parameters
assert chameleon(a=1) == dict(a=1)
assert forge.repr_callable(chameleon) == 'chameleon(*remove, a=1, **kwargs)'
# Empty call preserves signature
assert chameleon() == dict(a=1)
# Var-positional arguments remove keyword-only parameters
assert chameleon('a') == dict(a=1)
assert forge.repr_callable(chameleon) == 'chameleon(*remove, **kwargs)'