source: git/Singular/pyobject.cc @ dcd92d

spielwiese
Last change on this file since dcd92d was ba5e9e, checked in by Oleksandr Motsak <motsak@…>, 11 years ago
Changed configure-scripts to generate individual public config files for each package: resources, libpolys, singular (main) fix: sources should include correct corresponding config headers.
  • Property mode set to 100644
File size: 19.3 KB
Line 
1// -*- c++ -*-
2//*****************************************************************************
3/** @file pyobject.cc
4 *
5 * @author Alexander Dreyer
6 * @date 2010-12-15
7 *
8 * This file defines the @c blackbox operations for the pyobject type.
9 *
10 * @par Copyright:
11 *   (c) 2010 by The Singular Team, see LICENSE file
12**/
13//*****************************************************************************
14
15#ifdef HAVE_CONFIG_H
16#include "singularconfig.h"
17#endif /* HAVE_CONFIG_H */
18
19#ifdef HAVE_PYTHON
20
21#include <kernel/mod2.h>
22
23//#include <misc/auxiliary.h>
24//#include "newstruct.h"
25
26#include <misc/auxiliary.h>
27
28#include <omalloc/omalloc.h>
29
30#include <kernel/febase.h>
31#include <misc/intvec.h>
32
33#include <Singular/ipid.h>
34#include <Singular/blackbox.h>
35#include <Singular/lists.h>
36#include <Singular/ipid.h>
37#include <Singular/ipshell.h>
38#include <Singular/newstruct.h>
39
40#include "subexpr.h"
41#include "lists.h"
42#include "ipid.h"
43#include "blackbox.h"
44#include "ipshell.h"
45#include "newstruct.h"
46
47#include <Singular/mod_lib.h>
48
49#include <Python.h>
50
51// #include <iterator>             // std::distance
52// #include <stdio.h>
53
54void sync_contexts();
55
56/** @class PythonInterpreter
57 * This class initializes and finalized the python interpreter.
58 *
59 * It also stores the Singular token number, which is assigned to this type on
60 * runtime.
61 **/
62class PythonInterpreter {
63public:
64  typedef int id_type;
65
66  ~PythonInterpreter()  { if(m_owns_python) Py_Finalize();  }
67
68  /// Initialize unique (singleton) python interpreter instance,
69  /// and set Singular type identifier
70  static void init(id_type num) { instance().m_id = num; }
71
72  /// Get Singular type identitfier
73  static id_type id() { return instance().m_id; }
74
75private:
76  /// Singleton: Only init() is allowed to construct an instance
77  PythonInterpreter():
78    m_id(0), m_owns_python(false)  { start_python(); }
79
80  /// Static initialization -
81  /// safely takes care of destruction on program termination
82  static PythonInterpreter& instance() 
83  {
84    static PythonInterpreter init_interpreter;
85    return init_interpreter;
86  }
87
88  void start_python()
89  {
90    if (!Py_IsInitialized()) init_python();
91    set_python_defaults();
92  }
93 
94  void init_python()
95  {
96    Py_Initialize();
97    m_owns_python = true;
98  }
99
100  void set_python_defaults()
101  {
102    // Sone python modules needs argc, argv set for some reason
103    char* argv = (char*)"";
104    PySys_SetArgv(1, &argv);
105    PyRun_SimpleString("from sys import path, modules");
106    PyRun_SimpleString("_SINGULAR_IMPORTED = dict()");
107
108    char cmd[MAXPATHLEN + 20];
109    sprintf(cmd, "path.insert(0, '%s')", feGetResource('b'));
110    PyRun_SimpleString(cmd);
111    PyRun_SimpleString("del path");  // cleanup
112  }   
113
114  id_type m_id;
115  bool m_owns_python;
116};
117
118/** @class PythonObject
119 * This class defines an interface for calling PyObject from Singular.
120 *
121 * @note This class does not take care of the memory mangement, this is done in
122 * the blackbox routines.
123 **/
124class PythonObject
125{
126  typedef PythonObject self;
127
128public:
129  typedef PyObject* ptr_type;
130  struct sequence_tag{};
131
132  PythonObject(): m_ptr(Py_None) { }
133  PythonObject(ptr_type ptr): m_ptr(ptr) { 
134    if (!ptr && handle_exception()) m_ptr = Py_None;
135  }
136
137  ptr_type check_context(ptr_type ptr) const {
138    if(ptr) sync_contexts(); 
139    return ptr;
140  }
141  /// Unary operations
142  self operator()(int op) const
143  {
144    switch(op)
145    {
146    case '(':  return check_context(PyObject_CallObject(*this, NULL));
147    case ATTRIB_CMD: return PyObject_Dir(*this);
148    case PROC_CMD: return *this;
149    }
150
151    if (op == PythonInterpreter::id())
152      return *this;
153
154    return self(NULL);
155  }
156
157  /// Binary and n-ary operations
158  self operator()(int op, const self& arg) const {
159
160    switch(op)
161    {
162      case '+':  return PyNumber_Add(*this, arg);
163      case '-':  return PyNumber_Subtract(*this, arg);
164      case '*':  return PyNumber_Multiply(*this, arg);
165      case '/':  return PyNumber_Divide(*this, arg);
166      case '^':  return PyNumber_Power(*this, arg, Py_None);
167      case '(':  return check_context(PyObject_CallObject(*this, arg));
168      case '[':  return operator[](arg);
169      case KILLATTR_CMD: return del_attr(arg);
170      case LIST_CMD:     return args2list(arg);
171      case '.': case COLONCOLON: case ATTRIB_CMD: return attr(arg);
172    }
173    return self(NULL);
174  }
175
176  /// Ternary operations
177  self operator()(int op, const self& arg1, const self& arg2) const 
178  {
179    switch(op)
180    {
181      case ATTRIB_CMD: 
182        if(PyObject_SetAttr(*this, arg1, arg2) == -1) handle_exception();
183        return self();
184    }
185    return self(NULL);
186  }
187
188  /// Get item
189  self operator[](const self& idx) const { return PyObject_GetItem(*this, idx); }
190  self operator[](long idx) const { return operator[](PyInt_FromLong(idx));  }
191
192  /// Get actual PyObject*
193  operator const ptr_type() const { return m_ptr; }
194
195  /// Get representative as C-style string
196  char* repr() const
197  {
198    return omStrDup(PyString_AsString(PyObject_Repr(*this)));
199  }
200
201  /// Extract C-style string
202  char* str() const { return omStrDup(PyString_AsString(*this)); }
203
204  Py_ssize_t size() const { return PyObject_Size(m_ptr); }
205
206  BOOLEAN assign_to(leftv result)
207  {
208    return (m_ptr? (m_ptr == Py_None? none_to(result): python_to(result)): TRUE);
209  }
210
211  void import_as(const char* name) const {
212    idhdl handle = enterid(omStrDup(name), 0, DEF_CMD, 
213                           &IDROOT, FALSE);
214
215    if (handle)
216    {
217      IDDATA(handle) = (char*)m_ptr;
218      Py_XINCREF(m_ptr); 
219      IDTYP(handle) =  PythonInterpreter::id();
220    }
221    else { Werror("Importing pyobject to Singular failed"); }
222  }
223
224  int compare(int op, const self& arg) const
225  { return PyObject_RichCompareBool(*this, arg, py_opid(op)); }
226
227
228  self attr(const self& arg) const { return PyObject_GetAttr(*this, arg); }
229
230  self del_attr(const self& arg) const 
231  {
232    if (!PyObject_HasAttr(*this, arg)) 
233      Werror("Cannot delete attribute %s.", arg.repr());
234    else
235      PyObject_DelAttr(*this, arg); 
236 
237    return self();
238  }
239
240protected:
241  self args2list(const self& args) const
242  {
243    self pylist(PyList_New(0));
244    PyList_Append(pylist, *this);
245    if(PyTuple_Check(args))  pylist.append_iter(PyObject_GetIter(args));
246    else PyList_Append(pylist, args);
247
248    return pylist;
249  }
250
251  BOOLEAN handle_exception() const {
252
253    if(!PyErr_Occurred()) return FALSE;
254   
255    PyObject *pType, *pMessage, *pTraceback;
256    PyErr_Fetch(&pType, &pMessage, &pTraceback);
257   
258    WerrorS("pyobject error occured");
259    WerrorS(PyString_AsString(pMessage));
260   
261    Py_XDECREF(pType);
262    Py_XDECREF(pMessage);
263    Py_XDECREF(pTraceback);
264   
265    PyErr_Clear();
266    return TRUE;
267  }
268
269  void append_iter(self iterator) {
270    ptr_type item;
271    while (item = PyIter_Next(iterator)) {
272      PyList_Append(*this, item);
273      Py_DECREF(item);
274    }
275  }
276
277  int py_opid(int op) const{
278    switch(op)
279    { 
280      case '<':  return Py_LT;
281      case '>':  return Py_GT;
282      case EQUAL_EQUAL:  return Py_EQ;
283      case NOTEQUAL:  return Py_NE;
284      case GE:  return Py_GE; 
285      case LE:  return Py_LE; 
286    }
287    return -1;
288  }
289
290private:
291  BOOLEAN none_to(leftv result) const
292  {
293    Py_XDECREF(m_ptr);
294    result->data = NULL;
295    result->rtyp = NONE;
296    return FALSE;
297  }
298
299  BOOLEAN python_to(leftv result) const
300  {
301    result->data = m_ptr;
302    Py_XINCREF(m_ptr);
303    result->rtyp = PythonInterpreter::id();
304    return !m_ptr;
305  }
306
307  /// The actual pointer
308  ptr_type m_ptr;
309};
310
311
312
313/** @class PythonCastStatic
314 * This template class does conversion of Singular objects to python objects on
315 * compile-time.
316 *
317 * @note The Singular objects are assumed to be equivalent to the template argument.
318 **/
319template <class CastType = PythonObject::ptr_type>
320class PythonCastStatic:
321  public PythonObject {
322  typedef PythonCastStatic self;
323public:
324
325  PythonCastStatic(void* value):
326    PythonObject(get(reinterpret_cast<CastType>(value))) {}
327
328  PythonCastStatic(leftv value):
329    PythonObject(get(reinterpret_cast<CastType>(value->Data()))) {}
330
331private:
332  ptr_type get(ptr_type value)       { return value; }
333  ptr_type get(long value)           { return PyInt_FromLong(value); }
334  ptr_type get(int value)            { return PyInt_FromLong((long)value); }
335  ptr_type get(const char* value)    { return PyString_FromString(value); }
336  ptr_type get(char* value) { return get(const_cast<const char*>(value)); }
337  ptr_type get(intvec* value);       // inlined below
338  ptr_type get(lists value);         // inlined after PythonObjectDynamic
339};
340
341template <class CastType>
342inline PythonObject::ptr_type
343PythonCastStatic<CastType>::get(intvec* value)
344{
345  ptr_type pylist(PyList_New(0));
346  for (int idx = 0; idx < value->length(); ++idx)
347    PyList_Append(pylist, self::get((*value)[idx]));
348
349  return pylist;
350}
351
352/** @class PythonCastDynamic
353 * This class does conversion of Singular objects to python objects on runtime.
354 *
355 **/
356class PythonCastDynamic:
357  public PythonObject {
358  typedef PythonCastDynamic self;
359
360public:
361  PythonCastDynamic(leftv value): PythonObject(get(value, value->Typ())) {}
362
363private:
364  PythonObject get(leftv value, int typeId)
365  {
366    if (typeId == PythonInterpreter::id()) return PythonCastStatic<>(value);
367   
368    switch (typeId)
369    {
370    case INT_CMD:    return PythonCastStatic<long>(value);
371    case STRING_CMD: return PythonCastStatic<const char*>(value);
372    case LIST_CMD:   return PythonCastStatic<lists>(value);
373    case INTVEC_CMD: return PythonCastStatic<intvec*>(value);
374    }
375
376    sleftv tmp;
377    BOOLEAN newstruct_equal(int, leftv, leftv); // declaring overloaded '='
378    if (!newstruct_equal(PythonInterpreter::id(), &tmp, value)) 
379      return PythonCastStatic<>(&tmp);       
380
381    if (typeId > MAX_TOK)       // custom types
382    {
383      blackbox *bbx = getBlackboxStuff(typeId);
384      assume(bbx != NULL);
385      if (! bbx->blackbox_Op1(PythonInterpreter::id(), &tmp, value))
386        return PythonCastStatic<>(&tmp);       
387    }
388
389    Werror("type '%s` incompatible with 'pyobject`", iiTwoOps(typeId));
390    return PythonObject();
391  }
392};
393
394template <class CastType>
395inline PythonObject::ptr_type
396PythonCastStatic<CastType>::get(lists value)
397{
398  ptr_type pylist(PyList_New(0));
399  for (int i = 0; i <= value->nr; ++i)
400    PyList_Append(pylist, PythonCastDynamic((value->m) + i));
401
402  return pylist;
403}
404
405/// Template specialization for getting handling sequence
406template <>
407class PythonCastStatic<PythonObject::sequence_tag>:
408public PythonObject
409{
410public:
411
412  PythonCastStatic(leftv value):
413    PythonObject(PyTuple_New(size(value)))  { append_to(value); }
414
415 
416private:
417  size_t size(leftv iter, size_t distance = 0) const 
418  {
419    if (iter) { do { ++distance; } while((iter = iter->next)); }; 
420    return distance;
421  }
422 
423  void append_to(leftv iter) const
424  {
425    for(size_t idx = 0; iter != NULL; iter = iter->next)
426      PyTuple_SetItem(*this, idx++, PythonCastDynamic(iter));
427  }
428};
429
430
431PythonObject get_attrib_name(leftv arg)
432{
433  typedef PythonCastStatic<const char*> result_type;
434  if (arg->Typ() == STRING_CMD)
435    return result_type(arg);
436
437  return result_type((void*)arg->Name());
438}
439
440/// Evaluate string in python
441PythonObject python_eval(const char* arg) {
442
443  PyObject* globals = PyModule_GetDict(PyImport_Import(PyString_FromString("__main__")));
444  return PyRun_String(arg, Py_eval_input, globals, globals);
445}
446
447/// Evaluate string in python from Singular
448BOOLEAN python_eval(leftv result, leftv arg) {
449  if ( !arg || (arg->Typ() != STRING_CMD) ) {
450    Werror("expected python_eval('string')");
451    return TRUE;
452  }
453
454  return python_eval(reinterpret_cast<const char*>(arg->Data())).assign_to(result);
455}
456
457
458/// Execute string in python from Singular
459BOOLEAN python_run(leftv result, leftv arg)
460{
461  if ( !arg || (arg->Typ() != STRING_CMD) ) {
462    Werror("expected python_run('string')");
463    return TRUE;
464  }
465
466  PyRun_SimpleString(reinterpret_cast<const char*>(arg->Data()));
467  sync_contexts();
468
469  Py_INCREF(Py_None);
470  return PythonCastStatic<>(Py_None).assign_to(result);
471}
472
473PythonObject names_from_module(const char* module_name)
474{
475  char buffer[strlen(module_name) + 30];
476  sprintf (buffer, "SINGULAR_MODULE_NAME = '%s'", module_name);
477  PyRun_SimpleString(buffer);
478  PyRun_SimpleString("from sys import modules");
479  PyRun_SimpleString("exec('from ' + SINGULAR_MODULE_NAME + ' import *')");
480
481  return python_eval("[str for str in dir(modules[SINGULAR_MODULE_NAME]) if str[0] != '_']");
482}
483
484void from_module_import_all(const char* module_name)
485{
486  char buffer[strlen(module_name) + 20];
487  sprintf (buffer, "from %s import *", module_name);
488  PyRun_SimpleString(buffer);
489}
490
491/// import python module and export identifiers in Singular namespace
492BOOLEAN python_import(leftv result, leftv value) {
493
494  if ((value == NULL) || (value->Typ()!= STRING_CMD)) {
495    Werror("expected python_import('string')");
496    return TRUE;
497  }
498
499  from_module_import_all(reinterpret_cast<const char*>(value->Data()));
500  sync_contexts();
501
502  Py_INCREF(Py_None);
503  return PythonCastStatic<>(Py_None).assign_to(result);
504}
505
506/// blackbox support - initialization
507void* pyobject_Init(blackbox*)
508{
509  Py_INCREF(Py_None);
510  return Py_None;
511}
512
513/// blackbox support - convert to string representation
514char* pyobject_String(blackbox *b, void* ptr)
515{
516  return PythonCastStatic<>(ptr).repr();
517}
518
519/// blackbox support - copy element
520void* pyobject_Copy(blackbox*b, void* ptr)
521{ 
522    Py_XINCREF(ptr);
523    return ptr;
524}
525
526/// blackbox support - assign element
527BOOLEAN pyobject_Assign(leftv l, leftv r)
528{
529  Py_XDECREF(l->Data());
530  PyObject* result = PythonCastDynamic(r);
531  Py_XINCREF(result);
532
533  if (l->rtyp == IDHDL)
534    IDDATA((idhdl)l->data) = (char *)result;
535  else
536    l->data = (void *)result;
537 
538  return !result;
539}
540                                                                     
541
542/// blackbox support - unary operations
543BOOLEAN pyobject_Op1(int op, leftv res, leftv head)
544{
545  switch(op)
546  {
547    case INT_CMD:               // built-in return types first
548    {
549      long value = PyInt_AsLong(PythonCastStatic<>(head));
550      if( (value == -1) &&  PyErr_Occurred() ) {
551        Werror("'pyobject` cannot be converted to integer");
552        PyErr_Clear();
553        return TRUE;
554      }
555      res->data = (void*) value;
556      res->rtyp = INT_CMD;
557      return FALSE;
558    }
559    case TYPEOF_CMD:
560      res->data = (void*) omStrDup("pyobject");
561      res->rtyp = STRING_CMD; 
562      return FALSE;
563  }
564
565  if (!PythonCastStatic<>(head)(op).assign_to(res))
566    return FALSE;
567
568  BOOLEAN newstruct_Op1(int, leftv, leftv); // forward declaration
569  return newstruct_Op1(op, res, head);
570}
571
572
573/// blackbox support - binary operations
574BOOLEAN pyobject_Op2(int op, leftv res, leftv arg1, leftv arg2)
575{
576  PythonCastStatic<> lhs(arg1);
577
578  switch(op)                    // built-in return types and special cases first
579  { 
580    case '<': case '>': case EQUAL_EQUAL: case NOTEQUAL: case GE: case LE:
581    {
582      res->data = (void *)(long)(lhs.compare(op, PythonCastDynamic(arg2)));
583      res->rtyp = INT_CMD;
584      return FALSE;
585    }
586    case '.': case COLONCOLON: case ATTRIB_CMD:
587      return lhs.attr(get_attrib_name(arg2)).assign_to(res);
588  }
589
590  PythonCastDynamic rhs(arg2);
591  if (!lhs(op, rhs).assign_to(res))
592    return FALSE;
593
594  BOOLEAN newstruct_Op2(int, leftv, leftv, leftv); // forward declaration
595  return newstruct_Op2(op, res, arg1, arg2);
596
597}
598
599/// blackbox support - ternary operations
600BOOLEAN pyobject_Op3(int op, leftv res, leftv arg1, leftv arg2, leftv arg3)
601{
602  PythonCastStatic<> lhs(arg1);
603  PythonCastDynamic rhs1(arg2);
604  PythonCastDynamic rhs2(arg3);
605
606  if (!lhs(op, rhs1, rhs2).assign_to(res))
607    return FALSE;
608
609  return blackbox_default_Op3(op, res, arg1, arg2, arg3);
610}
611
612
613/// blackbox support - n-ary operations
614BOOLEAN pyobject_OpM(int op, leftv res, leftv args)
615{
616  switch(op)                    // built-in return types first
617  {
618    case STRING_CMD:
619    {
620      blackbox* a = getBlackboxStuff(args->Typ());
621      res->data = (void *)a->blackbox_String(a, args->Data());
622      res->rtyp = STRING_CMD;
623      return FALSE;
624    }
625
626    case INTVEC_CMD:
627      PythonObject obj = PythonCastStatic<>(args->Data());
628      unsigned long len = obj.size();
629
630      intvec* vec = new intvec(len);
631      for(unsigned long idx = 0; idx != len; ++idx) {
632        long value = PyInt_AsLong(obj[idx]);
633        (*vec)[idx] = static_cast<int>(value);
634
635        if ((value == -1) &&  PyErr_Occurred()) {
636          value = 0;
637          PyErr_Clear();
638        }
639        if (value != long((*vec)[idx])) {
640          delete vec;
641          Werror("'pyobject` cannot be converted to intvec");
642          return TRUE;
643        }
644      }
645      res->data = (void *)vec;
646      res->rtyp = op;
647      return FALSE;
648  }
649  typedef PythonCastStatic<PythonObject::sequence_tag> seq_type;
650  if (! PythonCastStatic<>(args)(op, seq_type(args->next)).assign_to(res))
651    return FALSE;
652
653  BOOLEAN newstruct_OpM(int, leftv, leftv); // forward declaration
654  return newstruct_OpM(op, res, args);
655}
656
657/// blackbox support - destruction
658void pyobject_destroy(blackbox *b, void* ptr)
659{
660  Py_XDECREF(ptr);
661}
662
663PyObject* get_current_definition(const char* name) {
664  idhdl handle =  ggetid(name);
665  if (!handle || (IDTYP(handle) != PythonInterpreter::id()))  return NULL;
666  PythonCastStatic<PyObject*> value(IDDATA(handle));
667  return value;
668}
669
670/// getting stuff from python to Singular namespace
671void sync_contexts()
672{
673  PyRun_SimpleString("_SINGULAR_NEW = modules['__main__'].__dict__.copy()");
674
675  PythonObject newElts = python_eval("[(_k, _e)   \
676    for (_k, _e) in _SINGULAR_NEW.iteritems() \
677    if _k not in _SINGULAR_IMPORTED or not _SINGULAR_IMPORTED[_k] is _e]");
678
679  long len = newElts.size();
680  for (long idx = 0; idx < len; ++idx)
681  {
682    long i = 0;
683    char* name = newElts[idx][i].str();
684    if (name && (*name != '\0') && (*name != '_'))
685    {
686      Py_XDECREF(get_current_definition(name));
687      i = 1;
688      newElts[idx][i].import_as(name);
689    }
690
691  }
692
693  PythonObject deletedElts = 
694    python_eval("list(set(_SINGULAR_IMPORTED.iterkeys()) - \
695     set(_SINGULAR_NEW.iterkeys()))");
696  len = deletedElts.size();
697
698  for (long idx = 0; idx < len; ++idx)
699  {
700    char* name = deletedElts[idx].str();
701    if (name && (*name != '\0') && (*name != '_'))
702      killid(name, &IDROOT);
703  }
704
705  PyRun_SimpleString("_SINGULAR_IMPORTED =_SINGULAR_NEW");
706  PyRun_SimpleString("del  _SINGULAR_NEW");
707}
708
709
710
711blackbox* pyobject_blackbox(int& tok) {
712  if(blackboxIsCmd("pyobject", tok) != ROOT_DECL) 
713  {
714    tok = setBlackboxStuff((blackbox*)omAlloc0(sizeof(blackbox)), 
715                           "pyobject");
716  }
717  return getBlackboxStuff(tok);
718}
719
720
721
722#define PYOBJECT_ADD_C_PROC(name) \
723  psModulFunctions->iiAddCproc((currPack->libname? currPack->libname: ""),\
724     (char*)#name, FALSE, name);
725
726int SI_MOD_INIT(pyobject)(SModulFunctions* psModulFunctions)
727{
728  int tok = -1;
729  blackbox* bbx = pyobject_blackbox(tok);
730  if (bbx->blackbox_Init != pyobject_Init) 
731  {
732    bbx->blackbox_destroy = pyobject_destroy;
733    bbx->blackbox_String  = pyobject_String;
734    bbx->blackbox_Init    = pyobject_Init;
735    bbx->blackbox_Copy    = pyobject_Copy;
736    bbx->blackbox_Assign  = pyobject_Assign;
737    bbx->blackbox_Op1     = pyobject_Op1;
738    bbx->blackbox_Op2     = pyobject_Op2;
739    bbx->blackbox_Op3     = pyobject_Op3;
740    bbx->blackbox_OpM     = pyobject_OpM;
741    bbx->data             = (void*)omAlloc0(newstruct_desc_size());
742   
743    PythonInterpreter::init(tok);
744
745    PYOBJECT_ADD_C_PROC(python_import);
746    PYOBJECT_ADD_C_PROC(python_eval);
747    PYOBJECT_ADD_C_PROC(python_run); 
748  }
749  return 0;
750}
751#undef PYOBJECT_ADD_C_PROC
752
753#ifndef EMBED_PYTHON
754extern "C" { 
755  int mod_init(SModulFunctions* psModulFunctions)
756  { 
757    return SI_MOD_INIT(pyobject)(psModulFunctions); 
758  }
759}
760#endif
761
762#endif /* HAVE_PYTHON */
Note: See TracBrowser for help on using the repository browser.