Coding Style¶
PyFarm developers should follow the conventions set fourth in PEP 8 unless told to do so otherwise. Any exceptions or more specific example will be noted below and other may be added as time progresses. Coding style is an important part of PyFarm because it keeps the code consistent and readable which contributes to the overall quality of the source code.
One final thing to note is that there are places where there’s inconsistencies in style and there always will be. It’s up to the team to maintain the style so if you spot something inconsistent with this guide while working you should always feel free to fix it.
For other areas where there’s contention about style the developers should come to a consensus and then add their findings to this document.
Whitespace¶
Whitespace is an important component of any Python program. The below sets out the standards that PyFarm follows which in practice is not far off from PEP 8
Tabs and Spaces¶
Follow PEP8 and use spaces. To be more exact when working on PyFarm please use four spaces. For non-python source code please also use four spaces unless the language you’re working with explicitly does not allow this.
Basic Syntax Considerations¶
As with the parts above you should follow PEP 8 when considering how you use Python’s basic syntax structures. Below are some additional considerations that are specific to PyFarm as well as some short explanations.
Quotations¶
Python has two forms of quotations '
and "
. Functionally there’s not
any difference between two however all code, strings, error messages, etc
should use "
. The exception to this rule is if you need "
inside of
a string, then you should use '
: 'hello "world"'
. The reason for this
rule is two parts:
- it’s easy to be, and often is, inconsistent when you mix
'
and"
- developers from other languages, such as C++, are more more used to using
"
for strings instead of'
Standard Documentation¶
Docstrings are high encouraged for all callable functions, methods,
classmethods, and staticmethods. When creating a docstring please use """
instead of '''
to enclose the documentation.
def foo():
"""This is a single line doc string"""
def bar():
"""
This is a multi-line documentation string. When you need to
use multiple lines you should keep the left and right side of
the opening and closing quotes clear.
"""
Endpoint Documentation¶
A large part of PyFarm is pyfarm.master
which includes HTTP endpoints
serving as the master’s API. It’s important to document these using the
sphinxcontrib.httpdomain
syntax so it’s readable. Take special note
that in the top level url the type and name of the thing being posted is in
the url, <str:item>
, however in the examples it’s the real text.
from flask.views import MethodView
class FooItemsAPI(MethodView):
def post(self, item=None):
"""
``POST`` method which
.. http:post:: /api/v1/foo/<str:item> HTTP/1.1
**Request**
.. sourcecode:: http
POST /api/v1/foo/foobar HTTP/1.1
Accept: application/json
{
"item": "foobar"
}
**Response (new item created)**
.. sourcecode:: http
HTTP/1.1 201 CREATED
Content-Type: application/json
{
"item": "foobar",
"id": 1
}
:statuscode 200: an existing tag was found and returned
:statuscode 201: a new tag was created
"""
Which ends up looking like this when rendered:
-
POST
/api/v1/foo/<str:item> HTTP/1.1
¶ Request
POST /api/v1/foo/foobar HTTP/1.1 Accept: application/json { "item": "foobar" }
Response (agent newly tagged)
HTTP/1.1 201 CREATED Content-Type: application/json { "item": "foobar", "id": 1 }
statuscode 200: | an existing tag was found and returned |
---|---|
statuscode 201: | a new tag was created |
Line Continuations¶
The default max line length for the project is 80 characters. Anything longer should use a line continuation if it can’t be split up otherwise.
# import continuations should use (), it's cleaner and easier to
# modify later on
try:
from httplib import (
BAD_REQUEST, NOT_FOUND, UNAUTHORIZED, INTERNAL_SERVER_ERROR)
except ImportError:
from http.client import (
BAD_REQUEST, NOT_FOUND, UNAUTHORIZED, INTERNAL_SERVER_ERROR)
# preferred
message = ("this is a message which you may not be "
"able to fit onto one linet")
# but this is ok too
message = "this is a message which you may not be " \
"able to fit onto one line"
# preferred
if (a == b and c == d and a == b
or a and b and c and d):
pass
# but this is ok too
if a == b and c == d and a == b \
or a and b and c and d:
pass
HTTP Endpoints¶
URL Formatting¶
The following rules should be applied when constructing an HTTP endpoint:
- endpoints referring to objects should be plural so
/items/
instead of/item/
- any endpoint that’s not referring to a specific document should contain a trailing slash:
/items/
- endpoints that refer to a specific document shouldn’t contain a trailing slash
/items/1
- when working with groups of items under a single item the trailing slash should be added
/items/1/children/
- any endpoint that’s an API should contain a version number
/api/v1/items/
Validating Data in API Endpoints¶
Most of the time you’ll want a standard way of validating the incoming
request before you have to deal with it yourself. For this there’s the
validate_with_model
function that in combination with
before_request
will:
- ensure the incoming data to the API is json
- test the incoming data to ensure it has all the required keys
- test to make sure the incoming data does not contain keys that don’t exist in the table
- check to ensure that all data included matches the expected types based on the types in the model
- set
flask.g.json
if all of the above proceed without problems- return a useful error message in response to the request if there’s problems
A short example of how this works is below
try:
from httplib import CREATED
except ImportError: # pragma: no cover
from http.client import CREATED
from flask import g
from pyfarm.master.application import app, db
from pyfarm.master.utility import validate_with_model, jsonify
from pyfarm.models.tag import Tag
# NOTE: this is an example only, not functional code as it does not
# setup the route
@validate_with_model(Tag) # does all the validation in the points above
def put_tag():
model = Tag(**g.json)
db.session.add(model)
db.session.commit()
return jsonify(model.to_dict()), CREATED
Logging¶
General¶
You are welcome to use the print function on your own but before pushing code or writing tests please switch to a logger:
from pyfarm.core.logger import getLogger
logger = getLogger("foobar")
The above will create a logger under the proper namespace with a reasonable
set of defaults applied. It will also create it under the proper namespace, in
this case pyfarm.foobar
.
Warning
The above is not actually true for the agent and job types. Those will require a special logging setup which is not yet addressed in this guide.
Usage¶
Below are some general guidelines that apply specifically to logging to minimize potential performance problems and decrease inconsistencies in usage. The following examples assume the code in the section above was run.
Log Formatting¶
When providing arguments to the logger use lazy formatting
greeting = "morning"
logger.info("good %s", greeting)
Use %r For Objects Instead of repr()¶
Instead of always calling repr() on the object just use the %r string formatter
data = {"true": True, "none", None}
logger.info("data: %r", data)
Exceptions and Errors¶
At some point you’ll have to handle or produce exception within PyFarm. Depending on where in the code base you’re working the patterns may vary so please see below for more information.
Suppressing All Exceptions¶
Always use try: except Exception
when you must suppress all unhandled
exceptions. It’s also advised that you log the original exception message too
so we can find and better handle these errors in the future.
try:
foobar()
# always document exactly why you're suppressing
# all unhandled exceptions. Generally speaking there
# are **very few** cases where this should ever be a standard
# practice.
except Exception as e:
logger.exception(e) # this is sometimes skipped
logger.warning("unhandled exception: %s", e)
pass
Custom Exceptions¶
PyFarm used to throw more custom exceptions but since then nearly all of the code has switched back to using standard exceptions. In the event a custom exception must be created it should follow the general pattern below.
class PyFarmBaseException(Exception):
"""The base exception which all PyFarm exceptions derive from"""
pass
# you may optional subclass from a related builtin type too
class FileHandlingError(PyFarmBaseException):
"""Raised when there's a problem handling files"""
pass
Throwing Exceptions Inside A Request¶
When working with pyfarm.master
you’ll often need to throw exceptions
that will be used as responses to a request. There’s a couple of ways to do
this:
Default Method¶
This is the standard method for throwing exceptions in the web application
in response to a request. The below code will cause
pyfarm.master.errors.error_400()
to produce an error response to the
request depending on the mimietype. For example if the incoming request it
application/json
the below will construct a json response.
try:
from httplib import BAD_REQUEST
except ImportError: # pragma: no cover
from http.client import BAD_REQUEST
from flask import g, abort
from pyfarm.master.application import app
@app.route("/foobar/")
def foobar():
# NOTE: like logging incomplete or single sentences should
# start with a lower case letter
g.error = "something went wrong"
abort(BAD_REQUEST)
Alternate Method¶
Although uncommon in other cases it may make sense to response directly when there’s a problem.
try:
from httplib import BAD_REQUEST
except ImportError: # pragma: no cover
from http.client import BAD_REQUEST
from flask import g, abort
from pyfarm.master.application import app
from pyfarm.master.utilities import jsonify
@app.route("/foobar/")
def foobar():
# NOTE: like logging incomplete or single sentences should
# start with a lower case letter
return jsonify(error="something went wrong"), BAD_REQUEST
Platform Specific Code¶
PyFarm is a cross-platform application and because of this some consideration about support multi-platforms in the same code base must be considered.
Import Handling¶
Imports for platform specific modules should be setup like below. This is
better than simply except ImportError: pass
because the exception thrown
in the event of misuse will make more sense. In cases where you’ve tried the
best you can to determine the proper coarse of action raise an exception that
describes the situation best.
try:
from os import fork
except ImportError:
fork = NotImplemented
try:
import win32process
except ImportError:
win32process = NotImplemented
if fork is NotImplemented and win32process is not NotImplemented:
subprocess.Popen(
commands, creationflags=win32process.DETACHED_PROCESS)
elif fork is not NotImplemented:
os.fork()
else:
raise NotImplemented(
"failed to determine correct way to launch process")
Internal Logic Handling¶
If you’re not working with imports like above and you just need to know what
platform you’re on use constants from pyfarm.core.enums
.
from pyfarm.core.enums import (
LINUX, MAC, WINDOWS, POSIX, CASE_SENSITIVE_ENVIRONMENT,
CASE_SENSITIVE_ENVIRONMENT)
Supporting Multiple Python Versions¶
PyFarm supports Python 2.6+ in most modules except for pyfarm.agent
or
pyfarm.jobtypes
which currently supports only Python 2.6 and Python 2.7
due to problems with Twisted and Python 3.x. Because of this certain
considerations must be made when working on the project.
Checking Python Versions¶
pyfarm.core.enums
has some special constants for getting the current
Python version. There are other ways of checking the Python version however
these constants are provided for consistency and readability.
from pyfarm.core.enums import PY26, PY26, PY2, PY3
Type Information¶
Certain types consolidated or removed when Python 3 was released. Because of
this some of the older ways of checking for basic types had to change. Again
pyfarm.core.enums
should be used for consistent and clean behavior
across Python versions.
from pyfarm.core.enums import STRING_TYPES, NUMERIC_TYPES
2.x vs. 3.x Version Specific Python Imports¶
Certain built-in imports where also consolidated or renamed when Python 3 came about. Rather than using constants to do a version check let Python’s import system do the work for you.
# Python 2.x imports should always go first since
# most studios and operating systems that ship with Python
# still default to 2.x
try:
from UserDict import UserDict
except ImportError: # pragma: no cover
from collections import UserDict
# for objects or functions that were renamed
try:
_range = xrange
except NameError: # pragma: no cover
_range = range
# for attributes which have changed
data = {}
try:
items = data.iteritems
except AttributeError:
items = data.items
Backwards Compatible Imports¶
Sometimes you’ll need access to new functions or modules that don’t with whatever Python version or package you’re working with. In these situations, like with version specific Python imports, you should use the import system to make the decision for you.
# NOTE: Python 2.6 and up includes json, which is what PyFarm requires,
# this is just an example
try:
import json
except ImportError: # pragma: no cover
import simplejson as json