SkillAgentSearch skills...

Gutter

Fully featured Python feature switch toolkit

Install / Use

/learn @disqus/Gutter
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

.. image:: https://api.travis-ci.org/disqus/gutter.png?branch=master :target: http://travis-ci.org/disqus/gutter

Gutter

NOTE: This repo is the client for Gargoyle 2, known as "Gutter". It does not work with the existing Gargoyle 1 codebase <https://github.com/disqus/gargoyle/>_.

Gutter is feature switch management library. It allows users to create feature switches and setup conditions those switches will be enabled for. Once configured, switches can then be checked against inputs (requests, user objects, etc) to see if the switches are active.

For a UI to configure Gutter with see the gutter-django project <https://github.com/disqus/gutter-django>_

Table of Contents

  • Configuration_
  • Setup_
  • Arguments_
  • Switches_
  • Conditions_
  • Checking Switches as Active_
  • Signals_
  • Namespaces_
  • Decorators_
  • Testing Utilities_

Configuration

Gutter requires a small bit of configuration before usage.

Choosing Storage


Switches are persisted in a ``storage`` object, which is a `dict` or any object which provides the ``types.MappingType`` interface (``__setitem__`` and ``__getitem__`` methods).  By default, ``gutter`` uses an instance of `MemoryDict` from the `durabledict library <https://github.com/disqus/durabledict>`_.  This engine **does not persist data once the process ends** so a more persistent data store should be used.

Autocreate
~~~~~~~~~~

``gutter`` can also "autocreate" switches.  If ``autocreate`` is enabled, and ``gutter`` is asked if the switch is active but the switch has not been created yet, ``gutter`` will create the switch automatically.  When autocreated, a switch's state is set to "disabled."

This behavior is off by default, but can be enabled through a setting.  More on "settings" below.

Configuring Settings

To change the storage and/or autocreate settings, simply import the settings module and set the appropriate variables:

.. code:: python

from gutter.client.settings import manager as manager_settings
from durabledict.dict import RedisDict
from redis import RedisClient

manager_settings.storage_engine = RedisDict('gutter', RedisClient()))
manager_settings.autocreate = True

In this case, we are changing the engine to durabledict's RedisDict and turning on autocreate. These settings will then apply to all newly constructed Manager instances. More on what a Manager is and how you use it later in this document.

Setup

Once the Manager's storage engine has been configured, you can import gutter's default Manager object, which is your main interface with gutter:

.. code:: python

from gutter.client.default import gutter

At this point the gutter object is an instance of the Manager class, which holds all methods to register switches and check if they are active. In most installations and usage scenarios, the gutter.client.gutter manager will be your main interface.

Using a different default Manager


If you would like to construct and use a different default manager, but still have it accessible via ``gutter.client.gutter``, you can construct and then assign a ``Manager`` instance to ``settings.manager.default`` value:

.. code:: python

    from gutter.client.settings import manager as manager_settings
    from gutter.client.models import Manager

    manager_settings.default = Manager({})   # Must be done before importing the default manager

    from gutter.client.default import gutter

    assert manager_settings.default is gutter

.. WARNING::

   :warning::warning:
   Note that the ``settings.manager.default`` value must be set **before** importing the default ``gutter`` instance.
   :warning::warning:

Arguments
=========

The first step in your usage of ``gutter`` should be to define your arguments that you will be checking switches against.  An "argument" is an object which understands the business logic and object in your system (users, requests, etc) and knows how to validate, transform and extract variables from those business objects for ``Switch`` conditions.  For instance, your system may have a ``User`` object that has properties like ``is_admin``, ``date_joined``, etc.  To switch against it, you would then create arguments for each of those values.

To do that, you construct a class which inherits from ``gutter.client.arguments.Container``. Inside the body of the class, you create as many class variable "arguments" that you need by using the ``gutter.client.arguments`` function.

.. code:: python

    from gutter.client import arguments

    from myapp import User

    class UserArguments(arguments.Container):

        COMPATIBLE_TYPE = User

        name = arguments.String(lambda self: self.input.name)
        is_admin = arguments.Boolean(lambda self: self.input.is_admin)
        age = arguments.Value(lambda self: self.input.age)

There are a few things going on here, so let's break down what they all mean.

1. The ``UserArgument`` class is subclassed from ``Container``.  The subclassing is required since ``Container`` implements some of the required API.
2. The class has a bunch of class variables that are calls to ``arguments.TYPE``, where ``TYPE`` is the type of variable this argument is. At present there are 3 types: ``Value`` for general values, ``Boolean`` for boolean values and ``String`` for string values.
3. ``arguments.TYPE()`` is called with a callable that returns the value.  In the above example, we'll want to make some switches active based on a user's ``name``, ``is_admin`` status and ``age``.
4. Those callables return the actual value, which is derefenced from ``self.input``, which is the input object (in this case a ``User`` instance).
5. ``Variable`` objects understand ``Switch`` conditions and operators, and implement the correct API to allow themselves to be appropriately compared.
6. ``COMPATIBLE_TYPE`` declares that this argument only works with ``User`` instances.  This works with the default implementation of ``applies`` in the base argument that checks if the ``type`` of the input is the same as ``COMPATIBLE_TYPE``.

Since constructing arguments that simply reference an attribute on ``self.input`` is so common, if you pass a string as the first argument of ``argument()``, when the argument is accessed, it will simply return that property from ``self.input``.  You must also pass a ``Variable`` to the ``variable=`` kwarg so gutter know what Variable to wrap your value in.

.. code:: python

    from gutter.client import arguments

    from myapp import User

    class UserArguments(Container):

        COMPATIBLE_TYPE = User

        name = arguments.String('name')
        is_admin = arguments.Boolean('is_admin')
        age = arguments.Value('age')


Rationale for Arguments
~~~~~~~~~~~~~~~~~~~~~~~

You might be asking, why have these ``Argument`` objects at all?  They seem to just wrap an object in my system and provide the same API.  Why can't I just use my business object **itself** and compare it against my switch conditions?

The short answer is that ``Argument`` objects provide a translation layer to translate your business objects into objects that ``gutter`` understands.  This is important for a couple reasons.

First, it means you don't clutter your business logic/objects with code to support ``gutter``.  You declare all the arguments you wish to provide to switches in one location (an Argument) whose single responsibility it to interface with ``gutter``.  You can also construct more savvy Argument objects that may be the combination of multiple business objects, consult 3rd party services, etc.  All still not cluttering your main application code or business objects.

Secondly, and most importantly, Arguments return ``Variable`` objects, which ensure ``gutter`` conditions work correctly.  This is mostly relevant to the percentage-based operators, and is best illustrated with an example.

Imagine you have a ``User`` class with an ``is_vip`` boolean field.  Let's say you wanted to turn on a feature for only 10% of your VIP customers.  To do that, you would write a condition that says, "10% of the time when I'm called with the variable, I should be true."  That line of code would probably do something like this:

.. code:: python

    return 0 <= (hash(variable) % 100) < 10

The issue is that if ``variable = True``, then ``hash(variable) % 100`` will always be the same value for **every** ``User`` with ``is_vip`` of ``True``:

.. code:: python

    >>> hash(True)
    1
    >>> hash(True) % 100
    1

This is because in Python `True` objects always have the same hash value, and thus the percentage check doesn't work.  This is not the behavior you want.

For the 10% percentage range, you want it to be active for 10% of the inputs.  Therefore, each input must have a unique hash value, exactly the feature the ``Boolean`` variable provides.  Every ``Variable`` has known characteristics against conditions, while your objects may not.

That said, you don't absolutely **have** to use ``Variable`` objects.  For obvious cases, like ``use.age > some_value`` your ``User`` instance will work just fine, but to play it safe you should use ``Variable`` objects.  Using ``Variable`` objects also ensure that if you update ``gutter`` any new ``Operator`` types that are added will work correctly with your ``Variable``s.

Switches
============================================

Switches encapsulate the concept of an item that is either 'on' or 'off' depending on the input.  The swich determines its on/off status by checking each of its ``conditions`` and seeing if it applies to a certain input.

Switches are constructed with only one required argument, a ``name``:

.. code:: python

    from gutter.client.models import Switch

    switch = Switch('my cool feature')

Switches can be in 3 core states: ``GLOBAL``, ``DISABLED`` and ``SELECTIVE``.  In t
View on GitHub
GitHub Stars225
CategoryDevelopment
Updated2d ago
Forks29

Languages

Python

Security Score

95/100

Audited on Apr 7, 2026

No findings