Skip to content

Instantly share code, notes, and snippets.

@palankai
Last active March 29, 2024 10:02
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save palankai/f73a18ce06751ab8f245 to your computer and use it in GitHub Desktop.
Save palankai/f73a18ce06751ab8f245 to your computer and use it in GitHub Desktop.
Python Specification Pattern
class Specification:
def __and__(self, other):
return And(self, other)
def __or__(self, other):
return Or(self, other)
def __xor__(self, other):
return Xor(self, other)
def __invert__(self):
return Invert(self)
def is_satisfied_by(self, candidate):
raise NotImplementedError()
def remainder_unsatisfied_by(self, candidate):
if self.is_satisfied_by(candidate):
return None
else:
return self
class CompositeSpecification(Specification):
pass
class MultaryCompositeSpecification(CompositeSpecification):
def __init__(self, *specifications):
self.specifications = specifications
class And(MultaryCompositeSpecification):
def __and__(self, other):
if isinstance(other, And):
self.specifications += other.specifications
else:
self.specifications += (other, )
return self
def is_satisfied_by(self, candidate):
satisfied = all([
specification.is_satisfied_by(candidate)
for specification in self.specifications
])
return satisfied
def remainder_unsatisfied_by(self, candidate):
non_satisfied = [
specification
for specification in self.specifications
if not specification.is_satisfied_by(candidate)
]
if not non_satisfied:
return None
if len(non_satisfied) == 1:
return non_satisfied[0]
if len(non_satisfied) == len(self.specifications):
return self
return And(*non_satisfied)
class Or(MultaryCompositeSpecification):
def __or__(self, other):
if isinstance(other, Or):
self.specifications += other.specifications
else:
self.specifications += (other, )
return self
def is_satisfied_by(self, candidate):
satisfied = any([
specification.is_satisfied_by(candidate)
for specification in self.specifications
])
return satisfied
class UnaryCompositeSpecification(CompositeSpecification):
def __init__(self, specification):
self.specification = specification
class Invert(UnaryCompositeSpecification):
def is_satisfied_by(self, candidate):
return not self.specification.is_satisfied_by(candidate)
class BinaryCompositeSpecification(CompositeSpecification):
def __init__(self, left, right):
self.left = left
self.right = right
class Xor(BinaryCompositeSpecification):
def is_satisfied_by(self, candidate):
return (
self.left.is_satisfied_by(candidate) ^
self.right.is_satisfied_by(candidate)
)
class NullaryCompositeSpecification(CompositeSpecification):
pass
class TrueSpecification(NullaryCompositeSpecification):
def is_satisfied_by(self, candidate):
return True
class FalseSpecification(NullaryCompositeSpecification):
def is_satisfied_by(self, candidate):
return False
@palankai
Copy link
Author

palankai commented Jun 13, 2016

Given a few specification, just and only the is_satisfied_by implemented

class Superuser(Specification):
  def is_satisfied_by(user):
    return user.is_superuser

class Employee(Specification):
  def is_satisfied_by(user):
    return user.is_employee

class Customer(Specification):
  def is_satisfied_by(user):
    return user.customer_id is not None

Then the specification:

can_use_the_wifi = (Supervisor() | Employee()) & ~ Customer()
can_use_the_wifi.is_satisfied_by(user)

what_missing_for_the_wifi = can_use_the_wifi.remainder_unsatisfied_by(user)
# it gives back (Supervisor() | Employee())

@code-machina
Copy link

Thank @palankai, I'm searching for a specification pattern, written in python. It's really helpful for me. Great snippet!

@mariusvrstr
Copy link

Works well in user lists, have you been able to use this for DB queries?

specification = (UserIsActive() & FromSpecificDomain(domain="@example.com"))
results = session.query(User).filter(specification.is_satisfied_by(User)).all()

@palankai
Copy link
Author

Hi @mariusvrstr,
I haven't used it for queries; I only used it for access control.
For in-memory operation, it should work out of the box.

Something like this:

def filter(collection: List, specification: Specification):
    return [i for i in collection if specification.is_satisfied_by(i)]

For queries, I'd try something like this:

class Query:

    def __and__(self, other):
        return QueryAnd(self, other)

    def __or__(self, other):
        return QueryOr(self, other)

class CompositeQuery(Query):

    def __init__(self, *queries):
        self.queries = queries

class QueryAnd(CompositeQuery):

    def translate(self):
        return f"({' AND '.join([q.translate() for q in self.queries])})"

class QueryOr(CompositeQuery):

    def translate(self):
        return f"({' OR '.join([q.translate() for q in self.queries])})"

class UserIsActive(Query):

    def translate(self):
        return "user.is_active = 1"

class FromSpecificDomain(Query):

    def __init__(self, domain):
        self.domain = domain

    def translate(self):
        return f"user.domain = '{self.domain}'"

class LocalUser(Query):

    def translate(self):
        return "user.domain = 'local'"

if __name__ == "__main__":
    q = UserIsActive() & (FromSpecificDomain("example.com") | LocalUser())
    print(q.translate())

This prints:

(user.is_active = 1 AND (user.domain = 'example.com' OR user.domain = 'local'))

@palankai
Copy link
Author

palankai commented Aug 18, 2023

I've implemented the "same" in Rust:
https://github.com/palankai/rust_design_patterns_in_practice/blob/master/specification/src/lib.rs
It is still early stages and isn't perfect (at this time), but it should work.

(I might refactor a bit, so if you see 404, look around in the repo)

@mariusvrstr
Copy link

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment