For the sake of a tutorial, lets assume that we need to calculate cooking times for our barbecue. For this daunting task, we decide we want a utility: a program that we can run from the command line whenever we get the urge for a taste of smokey goodness.
Note
This section isn’t as much about using phyles as it is about framing the purpose of the example utility quantitatively (i.e. into numbers) and in a way that can be formulated as a python function. Hopefully the example is not too complicated. However, I have tried to make it just complicated enough to warrant a full-fledged utility.
Let’s assume that we have three dishes we usually barbecue:
Dish Difficulty vegetable kabobs 1 dc smoked salmon 2 dc brisket 3 dc
The numbers to the right of each dish gives the difficulty of cooking (which we’ll abbreviate as “dc” to express its units).
As an example of difficulty, cooking a typical batch of vegetable kabobs for 1 hour at some some temperature (say 200 °F) is equivalent to cooking a typical brisket for 3 hours at that temperature. Or stated another way, brisket cooks about three times as slow as vegetable kabobs.
For the sake of this tutorial, cooking difficulty applies to temperature as well. For instance, cooking a typical batch of vegetable kabobs at 200 °F for 2 hours is equivalent to cooking a typical brisket at 600 °F for 2 hours:
Note how we divided both sides of the equation by the difficulty of cooking for the respective dish (1 dc for vegetable kabobs; 3 dc for brisket). This calculation shows that we can quantify how much we cook something by calculating what we’ll call the “doneness”. Taking this example for brisket:
Or, as a mathematical formula:
Using algebra [1], we can rearrange this equation to calculate cooking times:
In other words, if we know the amount we need to cook a dish (doneness), how difficult the dish is to cook (difficulty), and the temperature that we can achieve with our grill, then we can calculate the cooking time.
So, how much do we want to cook an dish? This table quantifies doneness for several common cooking terms:
Term Doneness Rare 200 Medium 350 Well-Done 500
Let’s try a calculation for smoked salmon (difficulty of 2) cooked medium (doneness of 350) at 225 °F (which is about 107 °C):
Thus it takes about 3.11 hr to cook a smoked salmon to medium at 225 °F.
As a python function, this calculation might take the form:
def cooking_time(doneness, difficulty, temperature):
"""
Return then cooking time given the desired `doneness`
cooking, the `difficulty` of cooking,
and the `temperature`.
Args:
- `doneness`: desired doneness (hr•°F/dc)
- `difficulty`: difficulty of cooking for the dish (dc)
- `temperature`: cooking temperature (°F)
Returns: cooking time in hours (``float``)
Raises: ``ValueError`` if the `temperature` is <= 120 °F
>>> round(cooking_time(350, 2, 225), 2)
3.11
"""
if T <= 120:
msg = "%s °F is too cold to cook!" % T
raise ValueError(msg)
return float(doneness * difficulty) / T
Assuming that we have a utility that calulates cooking times based on a config file, the file for this example might take the following form:
dish : smoked salmon
doneness : medium
temperature : 225
This config format is convenient for a user who doesn’t care that smoked salmon has a difficulty of 1 dc or that medium corresponds to a doneness of 350. However, it places a burden on the programmer to read the file, ensure that “smoked salmon” and “medium” are spelled correctly, and convert these string values into numbers.
That’s where phyles comes in!
We’ll tackle these tasks in steps, first finding a way to convert specific strings into numbers. Python provides a convenient way to do this conversion using its dict class:
doneness_dict = {'rare': 200,
'medium': 350,
'well-done': 500}
Getting a value from a dictionary using a key is called “item-getting”. Python item-getting raises a KeyError when it fails:
>>> doneness_dict['raw']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'raw'
As we’ll see, phyles allows for the creation of a converter dictionary directly in the schema specification.
When a YAML config file is parsed by a YAML parser, literals like 225 evaluate to integers. However, a cooking temperature may often be more useful as a float, as when it serves in the denominator of a fraction, for example. In cases where a YAML literal evaluates to a python type (e.g. int, float, str) that is different from the type desired, the desired python type can be used to to perform the conversion:
>>> float(225)
225.0
Like the dict item-getting, python types provide error checking, raising exceptions upon failure:
>>> float([2, 2, 5])
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: float() argument must be a string or a number
>>> float("twotwentyfive")
Traceback (most recent call last):
File "<stdin>", line 1, in ?
ValueError: could not convert string to float: twotwentyfive
There is nothing particularly special about dict.__getitem__() or python types. They are simply functions (or more preciesly, “callables”) that take a single value as a parameter and return possibly different values. In cases where they fail, dict.__getitem__() and python types raise three kinds of exceptions:
Exception Raised By KeyError dict item-getting TypeError python types ValueError python types
Thus, any python function that takes one and only one parameter and raises either a KeyError, TypeError, or ValueError upon failure, can serve as a converter.
For example, say we want to release a European version of our barbecue utility, we could write a function to convert temperature in °C into temperature in °F:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | def celsius_to_farenheit(c):
"""
Returns the temperature in Farenheit given temperature
in Celsius.
Args:
- c: temperature in Celsius
Returns: temperature in Farenheit (float)
>>> celsius_to_farenheit(107.222)
224.9996
"""
c = float(c)
if c < -273.15:
raise ValueError("Impossibly cold (%s °C)!" % c)
else:
return (1.8 * c) + 32
|
Notice that on line 16, clesius_to_farenheit() raises a ValueError if the temperature supplied to the function is lower than the thermodynamic legal limit of -273.15 °C.
In some cases, no conversion is required but it is desirable to check an option value against a list of choices. As shown below, phyles allows the creation of lists of choices within the schema specification. If choices are given in this way, phyles creates a sensible error message if the value for the option is not within the list of choices.
A schema in phyles (encapsulated by the phyles.Schema class), contains information to validate a configuration as well as produce a sample configuration, complete with documentation in comments. A schema is specified by a “schema specification”.
The schema specification (often shortened to “spec”) can take several forms, as fully explained in the documentation to the phyles.load_schema() function.
For our barbecue example, we’ll use a schema specification written as a YAML omap:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | !!omap
- dish :
- - vegetable kabobs
- smoked salmon
- brisket
- smoked salmon
- Dish to cook
- doneness :
- rare : 200
medium : 350
well-done : 500
- medium
- How much to cook the dish
- temperature :
- celsius to farenheit
- 105
- Cooking temperature in °C
- 105
|
The sequence value for each parameter (e.g. dish, doneness, and temperature) in the schema specification has three required elements (and a fourth optional element, described below in the section titled Optional Default Values):
- converter as either
- a YAML string object of the name of the converter function as keyed in the converters argument to phyles.Schema.load_schema() (as with temperature above, and discussed in the section titled Dictionary of Converter Functions)
- a YAML sequence object with a list of acceptable choices (as with dish above)
- or a YAML mapping object that maps choices to converted values (as with doneness above)
an example value (for sample config files)
documentation (which can be set to null for no documentation; see YAML null)
For temperature, these elements are
- converter: celsius to farenheit
- example: 105
- documentation: Cooking temperature in °C
One question is how a name that evaluates to a python str (e.g. doneness) translates into a converter, which must be a function. As explained more thoroughly in the discussion of schema below, this translation is achieved using a dict. For this barbecue example, we construct this dict in the following way:
converters = {'celsius to farenheit': celsius_to_farenheit}
We’ll see exactly how to use the converters dict in the example utility.
Note
The celsius_to_farenheit() function is defined in the section titled Conversion by User-Defined Functions
There are several python types for which it is not necessary to add entries to the converters dict. The reason is that phyles provides a set of built-in converters. For example, if a float converter were needed, then the following would be implicit and not required from the programmer:
converters = {'float': float} # <-- NOT necessary!!
These built-in converters provided by phyles are:
- map: dict
- dict: dict (encoded as a YAML dict)
- omap: collections.OrderedDict
- odict: collections.OrderedDict (alias for “omap”)
- pairs: list of 2-tuples
- set: set
- seq: list
- list: list (encoded as a sequence, see list())
- tuple: tuple (encoded as a sequence, see tuple())
- bool: bool
- float: float
- int: int
- long: long (encoded as a YAML int)
- complex: complex (encoded as a sequence of 0 to 2, or as a string representation, e.g. '3+2j'; see complex())
- str: str
- unicode: unicode
- timestamp: datetime.datetime (encoded as a YAML timestamp)
- slice: slice (encoded as a sequence of 1 to 3, see slice())
Note
Except where indicated, these types are encoded according to the YAML types specification in a YAML representation of a config.
Any converter that is specified as a YAML string object can be specified as “list-optional” by enclosing the string in angle brackets ("<" and ">"). For example, the barbecue program might take one or more temperatures instead of just a single temperature.
1 2 3 4 5 | - temperature :
- <celsius to farenheit>
- 105
- Cooking temperature in °C, or list thereof
- 105
|
In such a case, the following would both be valid key-value pairs for temperature.
temperature : 105
temperature : [105, 120]
Note that a converter specified as list-optional will produce a list for the key in the configuration, even if the value given in the config file not a list. In the former example (temperatreu : 105), the config would have the value [105] for the key "temperature". Thus, the list-optional angle brackets can be thought of as meaning “make the value into a list if it is not already”.
The capability to specify list-optional converters does not limit the converter dictionary from having keys that are enclosed by angle brackets:
converters = {'<some converter>': some_converter}
Even a converter named in this way could be list-optional in the schema specification:
- a_parameter :
- <<some converter>>
- value
- A particular paramter or list thereof
Additional to the three required elements of a specification parameter, an optional default value may be specified as a fourth element. In the example schema specification the default for the temperature parameter is 105. If a default value is missing, as in dish and doneness, then the parameter is required in the config file.
For example, the following config will fail vailidation by a schema from the example schema specification because the specification requires a value for doneness (by virtue of the specification’s missing a default value for doneness):
dish : smoked salmon
temperature : 107
To validate a config file, the information in the schema specification must be converted into a functional schema, a conversion accomplished by the phyles.load_schema() function.
Although the phyles.set_up() function automates these steps, it is useful to see how a schema is constructed from a specification and further how the schema validates a config. Using the running example (i.e. with converters defined in the section titled Dictionary of Converter Functions):
import phyles
import yaml
spec = """
!!omap
- dish :
- - vegetable kabobs
- smoked salmon
- brisket
- smoked salmon
- Dish to cook
- doneness :
- rare : 200
medium : 350
well-done : 500
- medium
- How much to cook the dish
- temperature :
- celsius to farenheit
- 105
- Cooking temperature in °C
- 105
"""
cfg = yaml.load("""
dish : smoked salmon
doneness : medium
temperature : 107
""")
schema = phyles.load_schema(spec, converters)
config = schema.validate_config(cfg)
The behavior of the resulting config, which is an instance of phyles.Configuration, will be discussed in more detail in the section titled The Configuration.
Note
The cfg could have just as easily been created directly as a dict:
cfg = {"dish": "smoked salmon",
"doneness": "medium",
"temperature": 107}
However, YAML is used here for consistency with earlier parts of this example and to emphasize the point that the files wherein configurations are stored are YAML files. Phyles facilitates using YAML files for configurations. For example the opening, reading, and validating of which are automated by the phyles.Schema.read_config() function.
An instance of phyles.Schema is capable of producing a sample config file using the phyles.Schema.sample_config(). For example given the schema we just created:
>>> print schema.sample_config()
%YAML 1.2
---
# Dish to cook
# One of: vegetable kabobs, smoked salmon, brisket
dish : smoked salmon
# How much to cook the dish
# One of: well-done, medium, rare
doneness : medium
# Cooking temperature in °C
temperature : 105
Instances of phyles.Configuration are simply ordered mappings. By virture of their original attribute, phyles.Configuration objects also retain memory of the configuration before conversion (as with the temperature, which was converted from Celsius to Farenheit):
>>> for i in config.items():
... print i
('dish', 'smoked salmon')
('doneness', 350)
('temperature', 107.0)
>>> config['temperature']
225.0
>>> config.original['temperature']
107
Instances of phyles.Configuration are useful inside a utility, potentially being the sole parameter that needs to be passed to functions. The following example assumes that the function cooking_time() is defined as in the section titled The Calculation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | difficulties = {'vegetable kabobs': 1,
'smoked salmon': 2,
'brisket': 3}
def report_cooking(config):
t = cooking_time(config['doneness'], config['difficulty'],
config['temperature'])
message = "Cooking time is %5.2f hr." % t
message = message.center(70)
config['outlet'](message)
def main(config):
...
config['difficulty'] = difficulties[config['dish']]
config['outlet'] = lambda s: sys.stdout.write(s + "\n")
report_cooking(config)
|
While bundling arguments within a Configuration may seem a little cumbersome at first, it facilitates the adding of new configuration-based behaviors deep within a utility and without the need to modify functions to accommodate additional parameters.
Note
Not all functions of the utility need to take a Configuration object as an argument. Here cooking_time() still takes three distinct arguments, but the “higher-level” report_cooking() function takes config. Such design considerations are left to the programmer.
As an example of the utility of a Configuration object, notice that the message width above is hard-coded to 70 in line 9 above. In principle, this width could be user-configurable:
spec = """
!!omap
- dish :
- - vegetable kabobs
- smoked salmon
- brisket
- smoked salmon
- Dish to cook
- doneness :
- rare : 200
medium : 350
well-done : 500
- medium
- How much to cook the dish
- temperature :
- celsius to farenheit
- 105
- Cooking temperature in °C
- 105
- width :
- int
- 70
- width of messages
- 70
"""
cfg = yaml.load("""
dish : smoked salmon
doneness : medium
temperature : 107
""")
schema = phyles.load_schema(spec, converters)
config = schema.validate_config(cfg)
Now, the message width needs not be hard-coded, which is a bane of maintenance:
def report_cooking(config):
t = cooking_time(config['doneness'], config['difficulty'],
config['temperature'])
message = "Cooking time is %s hr." % t
message = message.center(config['width'])
config['outlet'](message)
This enhanced functionality is essentially transparent to the user because a default value (70) is provided for the width option, rendering width optional in the config file.
We now have all of the pieces we need to make a utility package, complete with its own library module and scripts (also called “executable programs”, or just “programs”). As part of the phyles source, an example called “barbecue” is included in the directory called “examples”.
Assuming phyles and its dependencies are installed, the barbecue example is fully functioning in-place. For example, try one of the following commands (depending on your shell) from the examples/barbecue directory:
bash-type shell:
PYTHONPATH=".:${PYTHONPATH}" bin/barbecue-time -tcsh/tcsh shell:
env PYTHONPATH=".:${PYTHONPATH}" bin/barbecue-time -t
Note
The part of the command that modifies $PYTHONPATH allows for running the barbecue-time executable in-place. Were the barbecue package installed as with python setup.py install this modificaiton of $PYTHONPATH would not be necessary.
These commands should produce the following output:
%YAML 1.2
---
# Dish to cook
# One of: vegetable kabobs, smoked salmon, brisket
dish : smoked salmon
# How much to cook the dish
# One of: well-done, medium, rare
doneness : medium
# Cooking temperature in °C
temperature : 105
# width of report
width : 70
Note
The example barbecue package can even be installed with python setup.py install, althought it isn’t necessary.
Within the examples/barbecue/test-data directory is also a config file called time-config.yml. This config file can be used without installing the barbecue package:
bash-type shell:
PYTHONPATH=".:${PYTHONPATH}" \ bin/barbecue-time -c test-data/time-config.ymlcsh/tcsh shell:
env PYTHONPATH=".:${PYTHONPATH}" \ bin/barbecue-time -c test-data/time-config.yml
These commands should produce the following output:
======================================================================
barbecue-time v.0.1b1
======================================================================
Cooking time is 3.12 hr.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Done with smoked salmon!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It is possible to override configuration settings on the command line with the --override (or -o) argument:
bash-type shell:
PYTHONPATH=".:${PYTHONPATH}" \ bin/barbecue-time -c test-data/time-config.yml \ -o 'temperature : 120'csh/tcsh shell:
env PYTHONPATH=".:${PYTHONPATH}" \ bin/barbecue-time -c test-data/time-config.yml \ -o 'temperature : 120'
Here, the command line temperature of 120 °C (248 °F) overrides the temperature in the config (107 °C), reducing the cooking time. These commands should produce the following output:
======================================================================
barbecue-time v.0.1b1
======================================================================
Cooking time is 2.82 hr.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Done with smoked salmon!
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before looking deeper into the barbecue example, let’s see how phyles gracefully handles an error that can not be found at the time when the config is validated because it potentially depends on the state of the system while the program is running:
bash-type shell:
PYTHONPATH=".:${PYTHONPATH}" \ bin/barbecue-time -c test-data/time-config.yml \ -o 'width : 10000'csh/tcsh shell:
env PYTHONPATH=".:${PYTHONPATH}" \ bin/barbecue-time -c test-data/time-config.yml \ -o 'width : 10000'
Here, a message width of 10000 overrides the config file width of 70. This width is much too large to be displayed on any normal terminal. The barbecue-time script uses the phyles.get_terminal_size() function to catch the problem and raise an exception that is itself caught, resulting in a sensible error message being sent to the user with a graceful exit from the program:
======================================================================
barbecue-time v.0.1b1
======================================================================
############################# ERROR ##############################
Formatting 'width' (10000) bigger than window (78)
##################################################################
Inspection of the contents of the barbecue utility will reveal how these features of phyles can be used with a small amount of code.
The barbecue example is structured as a typical python package, serving as a template for most needs:
barbecue/ – top-level directory for package
- CHANGES.txt
contains version-by-version information about the evolution of the package [2]
- LICENSE.txt
contains the text of the license [2]
- MANIFEST.in
tells the setup script which extra files to include in a distribution [2]
- README.rst
contains broad information about the package [2]
barbecue/ – package directory, holding the library code
- __init__.py
init module for the package
- _barbecue.py
module holding the library code
schema/ – directory holding schema for configs
- barbecue-time.yml
the schema for the barbecue-time program
bin/ – a directory holding executable programs
- barbecue-time
an example program that calculates cooking times
- setup.py
a script for distribution and installation
test-data/ – directory that holds test-data
- time-config.yml
a test configuration file for the barbecue-time program
Let’s look at some of the key files in the hierarchy and examine salient features of each, starting first with the barbecue-time program because it shows most directly how to use phyles.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import sys
import phyles
import barbecue
__program__ = "barbecue-time"
__version__ = "0.1b1"
def output_message(message, config):
console_width = phyles.get_terminal_size()[0]
if config['width'] > console_width:
tplt = "Formatting 'width' (%s) bigger than window (%s)"
message = tplt % (config['width'], console_width)
raise barbecue.FormatError(message)
message = message.center(config['width'])
config['outlet'](message)
def report_cooking(config):
t = barbecue.cooking_time(config['doneness'],
config['difficulty'],
config['temperature'])
message = "Cooking time is %5.2f hr." % t
output_message(message, config)
def finish_up(config):
hline = "~" * (config['width'] - 4)
output_message(hline, config)
message = "Done with %s!" % config['dish']
output_message(message, config)
output_message(hline, config)
def main(config):
config['difficulty'] = barbecue.difficulties[config['dish']]
config['outlet'] = lambda s: sys.stdout.write(s + "\n")
report_cooking(config)
finish_up(config)
if __name__ == "__main__":
spec = phyles.package_spec(phyles.Undefined, "barbecue",
"schema", "barbecue-time.yml")
converters = {'celsius to farenheit':
barbecue.celsius_to_farenheit}
setup = phyles.set_up(__program__, __version__, spec, converters)
phyles.run_main(main, setup['config'],
catchall=barbecue.BarbecueError)
|
In terms of interacting with phyles, the most critical part of barbecue-time is in lines 40-46:
- Lines 40-41
The phyles.package_spec() function is used to retrieve the schema from the package.
- Lines 42-43
The converters dict is created as in the section title Dictionary of Converter Functions.
- Line 44
The phyles.set_up() function is used to parse command line arguments, load the schema from the spec, validate the config, and override any config setting from the command line option --override (-o).
- Lines 45-46
The phyles.run_main() function is used to run the main function inside a try-except block that catches any exceptions assigned by the catchall keyword argument, and exits gracefully if such exceptions arise.
Note
These few lines (40-46), along with specifying a schema, are all that is truely needed to interface with phyles and take advantage of the mose useful parts of its functionality.
Like any good program, barbecue-time has a main() function:
- Lines 34-35
The config is used as a global state, defining new items called 'difficulty' and 'outlet', that will be used in other parts of the program. Such use of a phyles.Configuration object is convenient, but left to the discretion of the programmer.
Using a phyles.Configuration object allows for abstraction of functionality that depends on the configuration.
- Line 9
The phyles.get_terminal_size() function is used to determine the width of the console.
- Lines 10-13
The message width from the config file (keyed by 'width') is checked against the console width. If the message width is to large, then a FormatError exception is raised. As we’ll see upon inspection of the file _barbecue.py, FormatError is a subclass of BarbecueError, which is the catchall exception for graceful exit (see line 45).
- Lines 26-31
The finish_up() function further demonstrates the utility of Configuration objects and the abstraction they allow. Note that the output_message() function does not care how the message is displayed–except that it is unfortunately tied to the console width. Even this dependencey can be remedied by further abstraction. For example, config could have the item:
config['canv_width'] = lambda: phyles.get_terminal_size()[0]And then output_message() could be changed accordingly:
def output_message(message, config): max_width = config['canv_width']() if config['width'] > max_width: tplt = "Formatting 'width' (%s) bigger than window (%s)" message = tplt % (config['width'], max_width) raise barbecue.FormatError(message) message = message.center(config['width']) config['outlet'](message)Now, since config['canv_width'] can be any function (or “callable”), the backend to which the message is sent can be anything, including a console or gui element like a Tkinter.Label.
The _barbecue.py file holds the main library code for the barbecue package.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | #! /usr/bin/env python
# -*- coding: utf-8 -*-
difficulties = {'vegetable kabobs': 1,
'smoked salmon': 2,
'brisket': 3}
class BarbecueError(Exception):
pass
class FormatError(BarbecueError):
pass
class TemperatureError(BarbecueError):
pass
def cooking_time(doneness, difficulty, T):
"""
Return the cooking time given the desired doneness
cooking, the difficulty of cooking, and the temperature.
Args:
- doneness: desired doneness (hr•°F/dc)
- difficulty: difficulty of cooking for the dish (dc)
- T: cooking temperature (°F)
Returns: cooking time in hours (float)
Raises: ``ValueError`` if the `temperature` is <= 120 °F
>>> round(cooking_time(350, 2, 225), 2)
3.11
"""
if T <= 120:
msg = "%s °F is too cold to cook!" % T
raise TemperatureError(msg)
return float(doneness * difficulty) / T
def celsius_to_farenheit(c):
"""
Returns the temperature in Farenheit given temperature
in Celsius.
Args:
- c: temperature in Celsius
Returns: temperature in Farenheit (float)
>>> celsius_to_farenheit(107.222)
224.9996
"""
c = float(c)
if c < -273.15:
raise ValueError("Impossibly cold (%s °C)!" % c)
else:
return (1.8 * c) + 32
|
Most of _barbecue.py documents its functionality. However, it does have some key parts:
- Line 2
This line designates the optional encoding for the file (see http://www.python.org/dev/peps/pep-0263/). The UTF-8 encoding allows for display of the ubiquitous units “°C” and “°F” in the docstrings and error messages.
- Lines 6-8
Some data is kept in the module, namely the conversions from dish to cooking difficulty. If larger amounts of data are needed, then it is better to include these as so-called “package data” and use the pkg_resources.resource_string() function from the distribute package or, failing that, the phyles.get_data_path() function, which tries to find package data with every trick in the book.
Note
With proper utilization of python eggs, a programmer should find that use of the pkg_resources.resource_string() function is failsafe.
- Lines 10-17
As seen in the barbecue-time file (lines 45-46), the BarbecueError is used as a catchall for anticipated errors, allowing the program to exit gracefully if any are raised while executing the main() function.
Created here are the BarbecueError and a couple of decendants, corresponding to problems with formatting and nonsensical cooking temperatures (lines 34-36). Since these exceptions are BarbecueError or inherit from it, then they fall under the catchall and trigger graceful exit.
.note:
The `catchall` can also be a tuple of exceptions. See :func:`phyles.run_main`.
To ensure that files get included in source distributions (i.e. python setup.py sdist), it is important to specify them in MANIFEST.in.
1 2 3 | include *.txt
include *.rst
recursive-include barbecue *.yml
|
The setup.py script directs the distribution and installation of python packages. See http://guide.python-distribute.org/creation.html for a complete discussion.
Below is a partial (acutally, almost complete) listing of setup.py mainly to (1) show the minimal required keyword arguments and (2) show how to use the following keyword arguments of the setup() function:
- packages
- include_package_data
- package_data
- scripts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import os
import glob
from setuptools import setup, find_packages
setup(name='barbecue',
version='0.1b1',
author='James C. Stroud',
author_email='jstroud@mbi.ucla.edu',
description='Utilities for cooking barbecue.',
url='http://phyles.bravais.net/barbecue',
classifiers =[
'Programming Language :: Python :: 2',
],
install_requires=["distribute", "phyles >= 0.2.0"],
license='LICENSE.txt',
long_description=open('README.rst').read(),
packages=find_packages(),
include_package_data=True,
package_data={'': [os.path.join('*', '*.yml')]},
scripts=glob.glob(os.path.join('bin', '*')))
|
It is notable that the keyword argument package_dir is not required, nor is it necessary to specify the package manually names because they are found automatically by distribute.find_packages() imported on line 4. Here, find_packages() evaluates to ['barbecue'].
This keyword argument ensures that the files found by the package_data keyword argument will be included upon installation (not just packaging for distribution).
The empty string ('') means to include files that match the corresponding patterns (['*/*.yml'] for unix-like systems) for all packages listed for the packages keyword argument. Here, these packages are found automatically. In this barbecue example {'': [os.path.join('*', '*.yml')]} matches barbecue/schema/barbecue-time.yml.
Thus, the pattern os.path.join('*', '*.yml') means to match every file ending with .yml in every sub-directory of the package directory (containing the __init__.py file; here barbecue). In other words, the pattern is matched as if it were evaluated from the package directory that contains the __init__.py file.
A simple way to check what the pattern will match is to change to the package directory and then execute the ls command with the pattern, as in the final command here:
[command@prompt]% ls barbecue/
__init__.py _barbecue.py schema
[command@prompt]% cd barbecue/
[command@prompt]% ls schema/
barbecue-time.yml
[command@prompt]% ls */*.yml
schema/barbecue-time.yml
The value to scripts says to include all files ('*') in the bin directory, using the glob.glob() function from the python standard library, imported on line 2. Here, glob.glob(os.path.join('bin', '*')) evaluates to [barbecue-time].
Compared to scripts, a more robust way to implement programs from a python package is to use entry points, which are perfectly compatible with phyles:
somewhere in setup.py
# somewhere in call to setuptools.setup() entry_points = { 'console_scripts' : [ 'some_program = my_package:_some_program']}somewhere in __init__.py
from _my_module import _some_programsomewhere in _my_module.py
class AnticipatedError(Exception): pass def some_function(config): # do stuff with config, # interact with user, produce output, # raise AnticipatedError when appropriate, etc. ... def _some_program(): spec = """ !!omap - param1 : [str, example 1, parameter 1] - param2 : [int, 42, parameter 2] """ setup = phyles.set_up('some_program', '0.1.0' spec) phyles.run_main(some_function, setup['config'], catchall=AnticipatedError)Note
As in the barbecue example, the specification (spec) is probably better included as a separate file in the package data.
Footnotes
[1] | The rule of algebra used here can be stated like this: if a quantity is on top of the fraction on one side of the equals sign, then it can be moved to the bottom of the fraction on the other side of the equals sign, and vice versa. |
[2] | (1, 2, 3, 4) http://guide.python-distribute.org/creation.html |