Franck Pommereau

doc update

"""SNAKES is the Net Algebra Kit for Editors and Simulators
SNAKES is a Python library allowing to model all sorts of Petri nets
and to execute them. It is very general as most Petri nets annotations
can be arbitrary Python expressions while most values can be arbitrary
Python objects.
SNAKES can be further extended with plugins, several ones being
already provided, in particular two plugins implement the Petri nets
compositions defined for the Petri Box Calculus and its successors.
"""SNAKES library is organised into three parts:
* the core library is package `snakes` and its modules, among which
`snakes.nets` is the one to work with Petri nets while the others
can be seen as auxiliary modules
* the plugin system and the plugins themselves reside into package
`snakes.plugins`
* auxiliary tools are kept into other sub-packages: `snakes.lang`
for all the material related to parsing Python and other
languages, `snakes.utils` for various utilities like the ABCD
compiler
@author: Franck Pommereau
@organization: University of Evry/Paris-Saclay
@copyright: (C) 2005-2013 Franck Pommereau
@license: GNU Lesser General Public Licence (aka. GNU LGPL), see the
file `doc/COPYING` in the distribution or visit [the GNU web
file `doc/COPYING` in the distribution or visit the [GNU web
site](http://www.gnu.org/licenses/licenses.html#LGPL)
@contact: franck.pommereau@ibisc.univ-evry.fr
"""
......@@ -21,8 +22,13 @@ compositions defined for the Petri Box Calculus and its successors.
version = "0.9.16"
defaultencoding = "utf-8"
"""## Module `snakes`
This module only provides the exceptions used throughout SNAKES.
"""
class SnakesError (Exception) :
"An error in SNAKES"
"Generic error in SNAKES"
pass
class ConstraintError (SnakesError) :
......
This diff is collapsed. Click to expand it.
"""Hashable mutable objets.
This module proposes hashable version of the mutable containers
"""This module proposes hashable version of the mutable containers
`list`, `dict` and `set`, called respectively `hlist`, `hdict` and
`hset`. After one object has been hashed, it becomes not mutable and
raises a ValueError if a method which changes the object (let call a
_mutation_ such a method) is invoqued. The object can be then un-
hashed by calling `unhash` on it so that it becomes mutable again.
`hset`.
After one object has been hashed, it becomes not mutable and raises a
`ValueError` if a method which changes the object (let call a
_mutation_ such a method) is invoqued. The object can be then
un-hashed by calling `unhash` on it so that it becomes mutable again.
Notice that this may cause troubles if the object is stored in a set
or dict which uses its hashcode to locate it. Notice also that
hashable containers cannot be hashed if they contain non hashable
......@@ -53,8 +53,9 @@ from operator import xor
from snakes.compat import *
def unhash (obj) :
"""Make the object hashable again.
"""Make the object mutable again. This should be used with
caution, especially if the object is stored in a dict or a set.
>>> l = hlist(range(3))
>>> _ = hash(l)
>>> l.append(3)
......@@ -63,7 +64,7 @@ def unhash (obj) :
ValueError: hashed 'hlist' object is not mutable
>>> unhash(l)
>>> l.append(3)
@param obj: any object
@type obj: `object`
"""
......@@ -72,6 +73,7 @@ def unhash (obj) :
except :
pass
# apidoc skip
def hashable (cls) :
"""Wrap methods in a class in order to make it hashable.
"""
......@@ -122,8 +124,8 @@ def hashable (cls) :
return cls
class hlist (list) :
"""Hashable lists.
"""Hashable lists. They support all standard methods from `list`.
>>> l = hlist(range(5))
>>> l
hlist([0, 1, 2, 3, 4])
......@@ -138,6 +140,7 @@ class hlist (list) :
>>> unhash(l)
>>> l.append(6)
"""
# apidoc stop
def __add__ (self, other) :
return self.__class__(list.__add__(self, other))
def __delitem__ (self, item) :
......@@ -200,8 +203,9 @@ class hlist (list) :
hlist = hashable(hlist)
class hdict (dict) :
"""Hashable dictionnaries.
"""Hashable dictionnaries. They support all standard methods from
`dict`.
>>> l = hlist(range(5))
>>> d = hdict([(l, 0)])
>>> d
......@@ -229,6 +233,7 @@ class hdict (dict) :
>>> d.pop(l)
0
"""
# apidoc stop
def __delitem__ (self, key) :
self.__mutable__()
dict.__delitem__(self, key)
......@@ -271,8 +276,8 @@ class hdict (dict) :
hdict = hashable(hdict)
class hset (set) :
"""Hashable sets.
"""Hashable sets. They support all standard methods from `set`.
>>> s = hset()
>>> l = hlist(range(5))
>>> s.add(l)
......@@ -298,6 +303,7 @@ class hset (set) :
>>> unhash(s)
>>> s.discard(l)
"""
# apidoc stop
def __and__ (self, other) :
return self.__class__(set.__and__(self, other))
def __hash__ (self) :
......
"""This package is dedicated to parse and work with various languages,
in particular Python itself and ABCD. These are mainly utilities for
internal use in SNAKES, however, they may be of general interest
independently of SNAKES.
@todo: add documentation about how to use parsing and similar services
"""
import sys
if sys.version_info[:2] in ((2, 6), (2, 7)) :
import ast
......@@ -14,9 +22,34 @@ else :
sys.modules["snkast"] = ast
"""### Module `ast` ###
The module `ast` exported by `snakes.lang` is similar to Python's
standard `ast` module (starting from version 2.6) but is available and
uniform on every implementation of Python supported by SNAKES: CPython
from 2.5, Jython and PyPy. (In general, these modules are available in
these implementation - except CPython 2.5 - but with slight
differences, so using `snakes.lang.ast` can be seen as a portable
implementation.)
Notice that this module is _not_ available for direct import but is
exposed as a member of `snakes.lang`. Moreover, when `snakes.lang` is
loaded, this module `ast` is also loaded as `snkast` in `sys.modules`,
this allows to have both versions from Python and SNAKES
simultaneously.
>>> import snakes.lang.ast as ast
ImportError ...
...
ImportError: No module named ast
>>> from snakes.lang import ast
>>> import snkast
"""
from . import unparse as _unparse
from snakes.compat import *
# apidoc skip
class Names (ast.NodeVisitor) :
def __init__ (self) :
ast.NodeVisitor.__init__(self)
......@@ -25,16 +58,24 @@ class Names (ast.NodeVisitor) :
self.names.add(node.id)
def getvars (expr) :
"""
"""Return the set of variables (or names in general) involved in a
Python expression.
>>> list(sorted(getvars('x+y<z')))
['x', 'y', 'z']
>>> list(sorted(getvars('x+y<z+f(3,t)')))
['f', 't', 'x', 'y', 'z']
@param expr: a Python expression
@type expr: `str`
@return: the set of variable names as strings
@rtype: `set`
"""
names = Names()
names.visit(ast.parse(expr))
return names.names - set(['None', 'True', 'False'])
# apidoc skip
class Unparser(_unparse.Unparser) :
boolops = {"And": 'and', "Or": 'or'}
def _Interactive (self, tree) :
......@@ -58,11 +99,13 @@ class Unparser(_unparse.Unparser) :
self.dispatch(tree.body)
self.leave()
# apidoc skip
def unparse (st) :
output = io.StringIO()
Unparser(st, output)
return output.getvalue().strip()
# apidoc skip
class Renamer (ast.NodeTransformer) :
def __init__ (self, map_names) :
ast.NodeTransformer.__init__(self)
......@@ -96,7 +139,8 @@ class Renamer (ast.NodeTransformer) :
ctx=ast.Load()), node)
def rename (expr, map={}, **ren) :
"""
"""Rename variables (ie, names) in a Python expression
>>> rename('x+y<z', x='t')
'((t + y) < z)'
>>> rename('x+y<z+f(3,t)', f='g', t='z', z='t')
......@@ -105,12 +149,22 @@ def rename (expr, map={}, **ren) :
'[(x + y) for x in range(3)]'
>>> rename('[x+y for x in range(3)]', y='z')
'[(x + z) for x in range(3)]'
@param expr: a Python expression
@type expr: `str`
@param map: a mapping from old to new names (`str` to `str`)
@type map: `dict`
@param ren: additional mapping of old to new names
@type ren: `str`
@return: the new expression
@rtype: `str`
"""
map_names = dict(map)
map_names.update(ren)
transf = Renamer(map_names)
return unparse(transf.visit(ast.parse(expr)))
# apidoc skip
class Binder (Renamer) :
def visit_Name (self, node) :
if node.id in self.map[-1] :
......@@ -119,7 +173,10 @@ class Binder (Renamer) :
return node
def bind (expr, map={}, **ren) :
"""
"""Replace variables (ie, names) in an expression with other
expressions. The replacements should be provided as `ast` nodes,
and so could be arbitrary expression.
>>> bind('x+y<z', x=ast.Num(n=2))
'((2 + y) < z)'
>>> bind('x+y<z', y=ast.Num(n=2))
......@@ -128,6 +185,15 @@ def bind (expr, map={}, **ren) :
'[(x + y) for x in range(3)]'
>>> bind('[x+y for x in range(3)]', y=ast.Num(n=2))
'[(x + 2) for x in range(3)]'
@param expr: a Python expression
@type expr: `str`
@param map: a mapping from old to new names (`str` to `ast.AST`)
@type map: `dict`
@param ren: additional mapping of old to new names
@type ren: `ast.AST`
@return: the new expression
@rtype: `str`
"""
map_names = dict(map)
map_names.update(ren)
......
This diff is collapsed. Click to expand it.
"""A plugins system.
"""This package implements SNAKES plugin system. SNAKES plugins
themselves are available as modules within the package.
Examples below are based on plugin `hello` that is distributed with
SNAKES to be used as an exemple of how to build a plugin. It extends
class `PetriNet` adding a method `hello` that says hello displaying
the name of the net.
## Loading plugins ##
The first example shows how to load a plugin: we load
`snakes.plugins.hello` and plug it into `snakes.nets`, which results
in a new module that actually `snakes.nets` extended by
in a new module that is actually `snakes.nets` extended by
`snakes.plugins.hello`.
>>> import snakes.plugins as plugins
......@@ -18,11 +26,6 @@ The next example shows how to simulate the effect of `import module`:
we give to `load` a thrid argument that is the name of the created
module, from which it becomes possible to import names or `*`.
**Warning:** this feature will not work `load` is not called from the
module where we then do the `from ... import ...`. This is exactly the
same when, from a module `foo` that you load a module `bar`: if `bar`
loads other modules they will not be imported in `foo`.
>>> plugins.load('hello', 'snakes.nets', 'another_version')
<module ...>
>>> from another_version import PetriNet
......@@ -33,12 +36,23 @@ Hello from another net
>>> n.hello()
Hi, this is yet another net!
How to define a plugin is explained in the example `hello`.
The last example shows how to load several plugins at once, instead of
giving one plugin name, we just need to give a list of plugin names.
>>> plugins.load(['hello', 'pos'], 'snakes.nets', 'mynets')
<module ...>
>>> from mynets import PetriNet
>>> n = PetriNet('a net')
>>> n.hello() # works thanks to plugin `hello`
Hello from a net
>>> n.bbox() # works thanks to plugin `pos`
((0, 0), (0, 0))
"""
import imp, sys, inspect
from functools import wraps
# apidoc skip
def update (module, objects) :
"""Update a module content
"""
......@@ -54,48 +68,17 @@ def update (module, objects) :
else :
raise ValueError("cannot plug '%r'" % obj)
def build (name, module, *objects) :
"""Builds an extended module.
The parameter `module` is exactly that taken by the function
`extend` of a plugin. This list argument `objects` holds all the
objects, constructed in `extend`, that are extensions of objects
from `module`. The resulting value should be returned by `extend`.
@param name: the name of the constructed module
@type name: `str`
@param module: the extended module
@type module: `module`
@param objects: the sub-objects
@type objects: each is a class object
@return: the new module
@rtype: `module`
"""
result = imp.new_module(name)
result.__dict__.update(module.__dict__)
update(result, objects)
result.__plugins__ = (module.__dict__.get("__plugins__",
(module.__name__,))
+ (name,))
for obj in objects :
if inspect.isclass(obj) :
obj.__plugins__ = result.__plugins__
return result
def load (plugins, base, name=None) :
"""Load plugins.
`plugins` can be a single plugin name or module or a list of such
values. If `name` is not `None`, the extended module is loaded ad
`name` in `sys.modules` as well as in the global environment from
which `load` was called.
"""Load plugins, `plugins` can be a single plugin name, a module
or a list of such values. If `name` is not `None`, the extended
module is loaded as `name` in `sys.modules` as well as in the
global environment from which `load` was called.
@param plugins: the module that implements the plugin, or its
name, or a collection of such values
@type plugins: `str` or `module`, or a `list`/`tuple`/... of such
values
name, or a collection (eg, list, tuple) of such values
@type plugins: `object`
@param base: the module being extended or its name
@type base: `str` or `module`
@type base: `object`
@param name: the name of the created module
@type name: `str`
@return: the extended module
......@@ -125,17 +108,45 @@ def load (plugins, base, name=None) :
inspect.stack()[1][0].f_globals[name] = result
return result
"""## Creating plugins ###
We show now how to develop a plugin that allows instances of
`PetriNet` to say hello: a new method `PetriNet.hello` is added and
the constructor `PetriNet.__init__` is added a keyword argument
`hello` for the message to print when calling method `hello`.
Defining a plugins required to write a module with a function called
`extend` that takes as its single argument the module to be extended.
Inside this function, extensions of the classes in the module are
defined as normal sub-classes. Function `extend` returns the extended
classes. A decorator called `plugin` must be used, it also allows to
resolve plugin dependencies and conflicts.
"""
# apidoc include "hello.py" lang="python"
"""Note that, when extending an existing method like `__init__` above,
we have to take care that you may be working on an already extended
class, consequently, we cannot know how its arguments have been
changed already. So, we must always use those from the unextended
method plus `**args`. Then, we remove from the latter what your plugin
needs and pass the remaining to the method of the base class if we
need to call it (which is usually the case). """
def plugin (base, depends=[], conflicts=[]) :
"""Decorator for extension functions
@param base: name of base module (usually 'snakes.nets')
@type base: str
@param depends: list of plugins on which this one depends
@type depends: list of str
@param conflicts: list of plugins with which this one conflicts
@type conflicts: list of str
@param base: name of base module (usually 'snakes.nets') that the
plugin extends
@type base: `str`
@param depends: list of plugin names (as `str`) this one depends
on, prefix `snakes.plugins.` may be omitted
@type depends: `list`
@param conflicts: list of plugin names with which this one
conflicts
@type conflicts: `list`
@return: the appropriate decorator
@rtype: function
@rtype: `decorator`
"""
def wrapper (fun) :
@wraps(fun)
......@@ -164,9 +175,39 @@ def plugin (base, depends=[], conflicts=[]) :
return extend
return wrapper
# apidoc skip
def new_instance (cls, obj) :
"""Create a copy of `obj` which is an instance of `cls`
"""
result = object.__new__(cls)
result.__dict__.update(obj.__dict__)
return result
# apidoc skip
def build (name, module, *objects) :
"""Builds an extended module.
The parameter `module` is exactly that taken by the function
`extend` of a plugin. This list argument `objects` holds all the
objects, constructed in `extend`, that are extensions of objects
from `module`. The resulting value should be returned by `extend`.
@param name: the name of the constructed module
@type name: `str`
@param module: the extended module
@type module: `module`
@param objects: the sub-objects
@type objects: each is a class object
@return: the new module
@rtype: `module`
"""
result = imp.new_module(name)
result.__dict__.update(module.__dict__)
update(result, objects)
result.__plugins__ = (module.__dict__.get("__plugins__",
(module.__name__,))
+ (name,))
for obj in objects :
if inspect.isclass(obj) :
obj.__plugins__ = result.__plugins__
return result
......
"""
@todo: revise (actually make) documentation
"""
import snakes.plugins
from snakes.plugins import new_instance
from snakes.pnml import Tree
......
......@@ -26,6 +26,8 @@
>>> n.layout()
>>> any(node.pos == (-100, -100) for node in sorted(n.node(), key=str))
False
@todo: revise documentation
"""
import os, os.path, subprocess, collections
......
"""An example plugin that allows instances class `PetriNet` to say hello.
A new method `hello` is added. The constructor is added a keyword
argument `hello` that must be the `str` to print when calling `hello`,
with one `%s` that will be replaced by the name of the net when
`hello` is called.
Defining a plugins need writing a module with a single function called
`extend` that takes a single argument that is the module to be
extended.
Inside the function, extensions of the classes in the module are
defined as normal sub-classes.
The function `extend` should return the extended module created by
`snakes.plugins.build` that takes as arguments: the name of the
extended module, the module taken as argument and the sub-classes
defined (expected as a list argument `*args` in no special order).
If the plugin depends on other plugins, for instance `foo` and `bar`,
the function `extend` should be decorated by `@depends('foo', 'bar')`.
Read the source code of this module to have an example
"""
"""An example plugin that allows instances of `PetriNet` to
say hello. The source code can be used as a starting
example."""
import snakes.plugins
@snakes.plugins.plugin("snakes.nets")
def extend (module) :
"""Extends `module`
"""
"Extends `module`"
class PetriNet (module.PetriNet) :
"""Extension of the class `PetriNet` in `module`
"""
"Extension of the class `PetriNet` in `module`"
def __init__ (self, name, **args) :
"""When extending an existing method, take care that you may
be working on an already extended class, so you so not
know how its arguments have been changed. So, always use
those from the unextended class plus `**args`, remove from
it what your plugin needs and pass it to the method of the
extended class if you need to call it.
"""Add new keyword argument `hello`
>>> PetriNet('N').hello()
Hello from N
>>> PetriNet('N', hello='Hi! This is %s...').hello()
Hi! This is N...
@param args: plugin options
@keyword hello: the message to print, with `%s` where the
net name should appear.
@keyword hello: the message to print, with
`%s` where the net name should appear.
@type hello: `str`
"""
self._hello = args.pop("hello", "Hello from %s")
module.PetriNet.__init__(self, name, **args)
def hello (self) :
"""A new method `hello`
>>> n = PetriNet('N')
>>> n.hello()
Hello from N
"""
"Ask the net to say hello"
print(self._hello % self.name)
return PetriNet
......
"""A plugin to add labels to nodes and nets.
@todo: revise (actually make) documentation
"""
from snakes.plugins import plugin, new_instance
......
......@@ -65,6 +65,8 @@ Buffer('buffer')
(1, 2)
>>> n._declare
['global x; x=1']
@todo: revise documentation
"""
import snakes.plugins
......
......@@ -42,6 +42,8 @@ Position(1, 3)
>>> n.transpose()
>>> n.node('t01').pos
Position(-3, 1)
@todo: revise documentation
"""
from snakes import SnakesError
......
"""
@todo: revise (actually make) documentation
"""
from snakes.plugins import plugin
from snakes.pnml import Tree, loads, dumps
import imp, sys, socket, traceback, operator
......
......@@ -12,6 +12,8 @@ Several status are defined by default: `entry`, `internal`, `exit`,
>>> n.add_place(Place('p1'), status=status.entry)
>>> n.place('p1')
Place('p1', MultiSet([]), tAll, status=Status('entry'))
@todo: revise documentation
"""
import operator, weakref
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
......@@ -60,6 +60,8 @@ TypeError: ...
Traceback (most recent call last):
...
TypeError: ...
@todo: revise documentation
"""
import inspect, sys
......
"""
@todo: revise (actually make) documentation
"""
from snakes import SnakesError
class CompilationError (SnakesError) :
......
This diff is collapsed. Click to expand it.
"""
@todo: revise (actually make) documentation
"""
......