cgkit-commits Mailing List for Python Computer Graphics Kit (Page 3)
Brought to you by:
mbaas
You can subscribe to this list here.
2004 |
Jan
|
Feb
|
Mar
|
Apr
|
May
|
Jun
|
Jul
|
Aug
|
Sep
|
Oct
|
Nov
|
Dec
(54) |
---|---|---|---|---|---|---|---|---|---|---|---|---|
2005 |
Jan
(40) |
Feb
(69) |
Mar
(43) |
Apr
(81) |
May
(92) |
Jun
(56) |
Jul
(35) |
Aug
(59) |
Sep
(41) |
Oct
(26) |
Nov
(15) |
Dec
(12) |
2006 |
Jan
(20) |
Feb
(23) |
Mar
(47) |
Apr
(87) |
May
(43) |
Jun
(7) |
Jul
(20) |
Aug
(5) |
Sep
(29) |
Oct
(83) |
Nov
(34) |
Dec
(16) |
2007 |
Jan
(11) |
Feb
|
Mar
|
Apr
|
May
(4) |
Jun
(2) |
Jul
|
Aug
|
Sep
|
Oct
|
Nov
|
Dec
|
2008 |
Jan
(3) |
Feb
(24) |
Mar
(2) |
Apr
|
May
(1) |
Jun
|
Jul
|
Aug
(4) |
Sep
(8) |
Oct
(2) |
Nov
(1) |
Dec
|
2009 |
Jan
(8) |
Feb
(22) |
Mar
(5) |
Apr
(10) |
May
(9) |
Jun
(8) |
Jul
(2) |
Aug
(13) |
Sep
(10) |
Oct
|
Nov
|
Dec
(1) |
2010 |
Jan
(3) |
Feb
(1) |
Mar
(4) |
Apr
(2) |
May
(3) |
Jun
|
Jul
(1) |
Aug
|
Sep
|
Oct
(2) |
Nov
|
Dec
|
2011 |
Jan
|
Feb
|
Mar
|
Apr
(2) |
May
|
Jun
|
Jul
|
Aug
(2) |
Sep
|
Oct
|
Nov
(7) |
Dec
|
2012 |
Jan
|
Feb
|
Mar
|
Apr
(3) |
May
|
Jun
|
Jul
|
Aug
|
Sep
(1) |
Oct
(1) |
Nov
(4) |
Dec
(5) |
2013 |
Jan
|
Feb
(5) |
Mar
|
Apr
|
May
|
Jun
|
Jul
|
Aug
|
Sep
|
Oct
|
Nov
|
Dec
|
From: Matthias B. <mb...@us...> - 2009-09-07 22:09:27
|
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "cgkit". The branch, py3k has been updated via b2c85ee9cf68aaa78ce33ce0cc29f0e050c60364 (commit) via 1388115887ec4850295f43f495549056fa10d28f (commit) via 552bf6d94ee636cebfaa22e5066fa614122eb0a0 (commit) via 477945ac32c978da7954b422e559b7e2e83d489a (commit) via 0ad4ffd75a97cc6134b8079de69340bd49ee1e8c (commit) from e6eef3bf1a4ab7af256f859f880d64066e420ead (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit b2c85ee9cf68aaa78ce33ce0cc29f0e050c60364 Author: Matthias Baas <mb...@us...> Date: Mon Sep 7 23:08:45 2009 +0100 Py3d changes. commit 1388115887ec4850295f43f495549056fa10d28f Merge: 477945ac32c978da7954b422e559b7e2e83d489a 552bf6d94ee636cebfaa22e5066fa614122eb0a0 Author: Matthias Baas <mb...@us...> Date: Mon Sep 7 21:44:42 2009 +0100 Merge branch 'master' into py3k Conflicts: cgkit/ri.py commit 552bf6d94ee636cebfaa22e5066fa614122eb0a0 Author: Matthias Baas <mb...@us...> Date: Mon Sep 7 21:39:32 2009 +0100 Py3k changes. commit 477945ac32c978da7954b422e559b7e2e83d489a Author: Matthias Baas <mb...@us...> Date: Mon Sep 7 21:19:46 2009 +0100 Py3k changes. ----------------------------------------------------------------------- Summary of changes: cgkit/_cri.py | 24 +- cgkit/_slparser.py | 20 +- cgkit/cri.py | 30 +- cgkit/light/cgtypes/mat3.py | 28 +- cgkit/light/cgtypes/mat4.py | 32 +- cgkit/light/cgtypes/quat.py | 16 +- cgkit/light/cgtypes/vec3.py | 33 +-- cgkit/light/cgtypes/vec4.py | 30 +- cgkit/ri.py | 14 +- cgkit/sequence.py | 10 +- cgkit/simplecpp.py | 8 +- cgkit/slparams.py | 17 +- cgkit/sltokenize.py | 2 +- unittests/ritest.py | 688 +++++++++++++++++++++--------------------- unittests/test_mat3.py | 15 +- unittests/test_mat3_light.py | 13 +- unittests/test_ri.py | 24 +- unittests/test_slparams.py | 2 +- unittests/test_vec4.py | 4 +- unittests/test_vec4_light.py | 4 +- 20 files changed, 508 insertions(+), 506 deletions(-) hooks/post-receive -- cgkit |
From: Matthias B. <mb...@us...> - 2009-09-07 01:00:12
|
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "cgkit". The branch, master has been updated via 0ad4ffd75a97cc6134b8079de69340bd49ee1e8c (commit) via 2283eadb7d4b3d71d02b8af11a7ee4a8543b7559 (commit) via 7900aad70938cbe2f34f221d8be9b69f5b1fa3ec (commit) via 0d5c7fca87aa788c5e10171637b1078f607bd63e (commit) via 047a7ecc89372399324eb7fd1de772daa6c24f00 (commit) via 10279dea937dd93c28aeff1ffb0882a41faf7df5 (commit) via e6949e523cd408acc85c891496a1dc1bc15f5679 (commit) from c2750399ce0fd1bce025273ce19991c7caaab337 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 0ad4ffd75a97cc6134b8079de69340bd49ee1e8c Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 22:09:07 2009 +0100 Py3k changes (iterator stuff). commit 2283eadb7d4b3d71d02b8af11a7ee4a8543b7559 Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 21:38:23 2009 +0100 Py3k changes. commit 7900aad70938cbe2f34f221d8be9b69f5b1fa3ec Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 19:55:07 2009 +0100 Py3k changes commit 0d5c7fca87aa788c5e10171637b1078f607bd63e Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 18:53:48 2009 +0100 Fixed line endings (no code changes). commit 047a7ecc89372399324eb7fd1de772daa6c24f00 Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 18:52:29 2009 +0100 Added some more files to gitignore. commit 10279dea937dd93c28aeff1ffb0882a41faf7df5 Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 18:52:10 2009 +0100 Changes required for Py3k that are compatible with Py2k. commit e6949e523cd408acc85c891496a1dc1bc15f5679 Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 18:50:12 2009 +0100 Fixed line endings. ----------------------------------------------------------------------- Summary of changes: .gitignore | 5 + cgkit/_cri.py | 1110 +++++++------- cgkit/cgkitinfo_template.txt | 84 +- cgkit/cri.py | 3274 +++++++++++++++++++++--------------------- cgkit/light/_core.py | 4 +- cgkit/light/cgtypes/mat3.py | 54 +- cgkit/light/cgtypes/mat4.py | 58 +- cgkit/light/cgtypes/quat.py | 25 +- cgkit/light/cgtypes/vec3.py | 121 +- cgkit/light/cgtypes/vec4.py | 41 +- cgkit/mayabinary.py | 702 +++++----- cgkit/ri.py | 25 +- cgkit/sequence.py | 66 +- cgkit/sl.py | 30 +- unittests/all.py | 8 +- unittests/test_mayabinary.py | 648 +++++----- unittests/test_ri.py | 182 ++-- unittests/test_sequence.py | 8 + utilities/seqcp.py | 2 +- utilities/seqmv.py | 2 +- utilities/seqrm.py | 2 +- 21 files changed, 3253 insertions(+), 3198 deletions(-) hooks/post-receive -- cgkit |
From: Matthias B. <mb...@us...> - 2009-09-06 22:21:06
|
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "cgkit". The branch, py3k has been created at e6eef3bf1a4ab7af256f859f880d64066e420ead (commit) - Log ----------------------------------------------------------------- commit e6eef3bf1a4ab7af256f859f880d64066e420ead Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 22:19:14 2009 +0100 Removed usage of the types module and replaced the callable() function call. commit 954d5888c58c7aaefddca85d45551d5ab904c974 Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 21:50:52 2009 +0100 Replaced the next() method with the next() function. commit 6609bcdc9d23599845dbcb6d2da3bfd48f07259f Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 21:43:28 2009 +0100 Removed all occurrences of 'basestring' in the sequence module. commit 79f9df2b0c65556396a0bb75f2bf54d5f24dbebc Merge: 5d8aee86b8d312d96f0bedf2ed465d01205bfc5b 2283eadb7d4b3d71d02b8af11a7ee4a8543b7559 Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 21:39:28 2009 +0100 Merge branch 'master' into py3k commit 5d8aee86b8d312d96f0bedf2ed465d01205bfc5b Author: Matthias Baas <mb...@us...> Date: Sun Sep 6 21:03:59 2009 +0100 Changed imports to relative imports. ----------------------------------------------------------------------- hooks/post-receive -- cgkit |
From: Matthias B. <mb...@us...> - 2009-09-05 19:04:19
|
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "cgkit". The branch, master has been updated via c2750399ce0fd1bce025273ce19991c7caaab337 (commit) from 01bd3ee389c82d2b347b5785e4c11dfaf33c1e5b (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit c2750399ce0fd1bce025273ce19991c7caaab337 Author: Matthias Baas <mb...@us...> Date: Sat Sep 5 20:03:47 2009 +0100 Added *.pyc files to the ignore list. ----------------------------------------------------------------------- Summary of changes: .gitignore | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) mode change 100644 => 100755 .gitignore hooks/post-receive -- cgkit |
From: Matthias B. <mb...@us...> - 2009-09-05 18:40:22
|
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "cgkit". The branch, master has been updated via 01bd3ee389c82d2b347b5785e4c11dfaf33c1e5b (commit) from 97a03eece3fb14de179a9b71ce679ea3f6abb6d2 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 01bd3ee389c82d2b347b5785e4c11dfaf33c1e5b Author: U-KVANT\Matthias Baas <Matthias Baas@kvant.(none)> Date: Sat Sep 5 19:39:43 2009 +0100 Fixed line endings (no code changes). ----------------------------------------------------------------------- Summary of changes: wrappers/rply/rply/LICENSE | 40 +- wrappers/rply/rply/etc/convert.c | 272 ++-- wrappers/rply/rply/etc/dump.c | 88 +- wrappers/rply/rply/etc/input.ply | 32 +- wrappers/rply/rply/etc/sconvert.c | 132 +- wrappers/rply/rply/manual/manual.html | 1986 ++++++++++---------- wrappers/rply/rply/manual/reference.css | 108 +- wrappers/rply/rply/rply.c | 2980 +++++++++++++++--------------- wrappers/rply/rply/rply.h | 730 ++++---- 9 files changed, 3184 insertions(+), 3184 deletions(-) hooks/post-receive -- cgkit |
From: Matthias B. <mb...@us...> - 2009-09-05 14:28:14
|
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "cgkit". The branch, master has been updated via 97a03eece3fb14de179a9b71ce679ea3f6abb6d2 (commit) from f9f63488f9a2cb07b48205b7387a41ad760784b9 (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit 97a03eece3fb14de179a9b71ce679ea3f6abb6d2 Author: Matthias Baas <mb...@us...> Date: Sat Sep 5 15:27:25 2009 +0100 Added another gitignore file. ----------------------------------------------------------------------- Summary of changes: doc/.gitignore | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 doc/.gitignore hooks/post-receive -- cgkit |
From: Matthias B. <mb...@us...> - 2009-09-05 13:51:25
|
This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "cgkit". The branch, master has been updated via f9f63488f9a2cb07b48205b7387a41ad760784b9 (commit) via e7fe86ea528c9e5c18f87b1e799352eeb3bfc622 (commit) via 04ee81d3a3a1a6435b580de8566583813ac28142 (commit) via 181a143c9b0ff37e4b4fab4464292fbec1038e17 (commit) from c4d121212ed4effeba919cebe4893beb446756db (commit) Those revisions listed above that are new to this repository have not appeared on any other notification email; so we list those revisions in full, below. - Log ----------------------------------------------------------------- commit f9f63488f9a2cb07b48205b7387a41ad760784b9 Author: Matthias Baas <mb...@us...> Date: Sat Sep 5 13:59:03 2009 +0100 Added .gitignore files. commit e7fe86ea528c9e5c18f87b1e799352eeb3bfc622 Author: Matthias Baas <mb...@us...> Date: Sat Sep 5 13:52:38 2009 +0100 Set the executable flag on render.py and viewer.py. commit 04ee81d3a3a1a6435b580de8566583813ac28142 Author: Matthias Baas <mb...@us...> Date: Sat Sep 5 13:45:11 2009 +0100 Enabled the jobqueue docs. commit 181a143c9b0ff37e4b4fab4464292fbec1038e17 Author: Matthias Baas <mb...@us...> Date: Sat Sep 5 13:43:55 2009 +0100 Changed the version number. ----------------------------------------------------------------------- Summary of changes: .gitignore | 6 ++++++ cgkit/cgkitinfo_template.txt | 2 +- doc/source/generic_modules.rst | 2 +- setup.py | 2 +- supportlib/.gitignore | 6 ++++++ 5 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 .gitignore mode change 100644 => 100755 render.py create mode 100644 supportlib/.gitignore mode change 100644 => 100755 viewer.py hooks/post-receive -- cgkit |
From: <mb...@us...> - 2009-08-29 07:44:15
|
Revision: 341 http://cgkit.svn.sourceforge.net/cgkit/?rev=341&view=rev Author: mbaas Date: 2009-08-29 07:44:02 +0000 (Sat, 29 Aug 2009) Log Message: ----------- Deleted the pics directory from the Latex docs. Removed Paths: ------------- cgkit/trunk/doc/pics/ This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-08-29 07:36:39
|
Revision: 340 http://cgkit.svn.sourceforge.net/cgkit/?rev=340&view=rev Author: mbaas Date: 2009-08-29 07:36:30 +0000 (Sat, 29 Aug 2009) Log Message: ----------- Tagging v2.0.0alpha9 Added Paths: ----------- cgkit/tags/v2.0.0alpha9/ This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-08-20 21:59:46
|
Revision: 339 http://cgkit.svn.sourceforge.net/cgkit/?rev=339&view=rev Author: mbaas Date: 2009-08-20 21:59:35 +0000 (Thu, 20 Aug 2009) Log Message: ----------- Additional Windows build flag. Modified Paths: -------------- cgkit/trunk/setup.py Modified: cgkit/trunk/setup.py =================================================================== --- cgkit/trunk/setup.py 2009-08-20 21:00:14 UTC (rev 338) +++ cgkit/trunk/setup.py 2009-08-20 21:59:35 UTC (rev 339) @@ -546,7 +546,7 @@ ,include_dirs=INC_DIRS ,library_dirs=LIB_DIRS ,extra_compile_args=CC_ARGS - ,extra_link_args=LINK_ARGS + ,extra_link_args=LINK_ARGS+["/NODEFAULTLIB:LIBC"] ,define_macros=MACROS)] # Wintab This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-08-20 21:00:33
|
Revision: 338 http://cgkit.svn.sourceforge.net/cgkit/?rev=338&view=rev Author: mbaas Date: 2009-08-20 21:00:14 +0000 (Thu, 20 Aug 2009) Log Message: ----------- Added new Sphinx docs. Added Paths: ----------- cgkit/trunk/doc/Makefile cgkit/trunk/doc/make.bat cgkit/trunk/doc/source/ cgkit/trunk/doc/source/3dsimport.rst cgkit/trunk/doc/source/_static/ cgkit/trunk/doc/source/_templates/ cgkit/trunk/doc/source/asfamc.rst cgkit/trunk/doc/source/asfamcimport.rst cgkit/trunk/doc/source/beziercurve.rst cgkit/trunk/doc/source/beziercurvegeom.rst cgkit/trunk/doc/source/boundingbox.rst cgkit/trunk/doc/source/box.rst cgkit/trunk/doc/source/boxgeom.rst cgkit/trunk/doc/source/building.rst cgkit/trunk/doc/source/bvh.rst cgkit/trunk/doc/source/bvhimport.rst cgkit/trunk/doc/source/ccylinder.rst cgkit/trunk/doc/source/ccylindergeom.rst cgkit/trunk/doc/source/cgkitinfo.rst cgkit/trunk/doc/source/cgtypes.rst cgkit/trunk/doc/source/cmds.rst cgkit/trunk/doc/source/component.rst cgkit/trunk/doc/source/components.rst cgkit/trunk/doc/source/conf.py cgkit/trunk/doc/source/cri.rst cgkit/trunk/doc/source/euleradapter.rst cgkit/trunk/doc/source/eventmanager.rst cgkit/trunk/doc/source/events.rst cgkit/trunk/doc/source/exportplugins.rst cgkit/trunk/doc/source/expression.rst cgkit/trunk/doc/source/filesequencetools.rst cgkit/trunk/doc/source/flockofbirds.rst cgkit/trunk/doc/source/freecamera.rst cgkit/trunk/doc/source/generic_modules.rst cgkit/trunk/doc/source/geomobject.rst cgkit/trunk/doc/source/geomobjects.rst cgkit/trunk/doc/source/glfreedistantlight.rst cgkit/trunk/doc/source/glfreespotlight.rst cgkit/trunk/doc/source/glmaterial.rst cgkit/trunk/doc/source/glove.rst cgkit/trunk/doc/source/glpointlight.rst cgkit/trunk/doc/source/glslangparams.rst cgkit/trunk/doc/source/glslangtokenize.rst cgkit/trunk/doc/source/gltargetdistantlight.rst cgkit/trunk/doc/source/gltargetspotlight.rst cgkit/trunk/doc/source/gnuplotter.rst cgkit/trunk/doc/source/group.rst cgkit/trunk/doc/source/hammersley.rst cgkit/trunk/doc/source/ifsimport.rst cgkit/trunk/doc/source/importplugins.rst cgkit/trunk/doc/source/index.rst cgkit/trunk/doc/source/introduction.rst cgkit/trunk/doc/source/jobqueue.rst cgkit/trunk/doc/source/joint.rst cgkit/trunk/doc/source/joystick.rst cgkit/trunk/doc/source/keydefs.rst cgkit/trunk/doc/source/license.rst cgkit/trunk/doc/source/lookat.rst cgkit/trunk/doc/source/lwob.rst cgkit/trunk/doc/source/maimport.rst cgkit/trunk/doc/source/mat3.rst cgkit/trunk/doc/source/mat4.rst cgkit/trunk/doc/source/material3ds.rst cgkit/trunk/doc/source/materials.rst cgkit/trunk/doc/source/mayaascii.rst cgkit/trunk/doc/source/mayabinary.rst cgkit/trunk/doc/source/mayaspotlight.rst cgkit/trunk/doc/source/motionpath.rst cgkit/trunk/doc/source/noise.rst cgkit/trunk/doc/source/objexport.rst cgkit/trunk/doc/source/objimport.rst cgkit/trunk/doc/source/objmaterial.rst cgkit/trunk/doc/source/objmtl.rst cgkit/trunk/doc/source/odedynamics.rst cgkit/trunk/doc/source/odejoints.rst cgkit/trunk/doc/source/offexport.rst cgkit/trunk/doc/source/offimport.rst cgkit/trunk/doc/source/pics/ cgkit/trunk/doc/source/pics/camera01.jpg cgkit/trunk/doc/source/pics/cellnoise.jpg cgkit/trunk/doc/source/pics/coordsys.jpg cgkit/trunk/doc/source/pics/fbm.jpg cgkit/trunk/doc/source/pics/grid.jpg cgkit/trunk/doc/source/pics/helloworld.jpg cgkit/trunk/doc/source/pics/koch.jpg cgkit/trunk/doc/source/pics/koch.png cgkit/trunk/doc/source/pics/noise.jpg cgkit/trunk/doc/source/pics/simplescene1.jpg cgkit/trunk/doc/source/pics/simplescene2.jpg cgkit/trunk/doc/source/pics/slotexample.jpg cgkit/trunk/doc/source/pics/tunnel.pdf cgkit/trunk/doc/source/pics/tunnel.png cgkit/trunk/doc/source/pics/turbulence.jpg cgkit/trunk/doc/source/pidcontroller.rst cgkit/trunk/doc/source/plane.rst cgkit/trunk/doc/source/planegeom.rst cgkit/trunk/doc/source/pluginmanager.rst cgkit/trunk/doc/source/plyexport.rst cgkit/trunk/doc/source/plyimport.rst cgkit/trunk/doc/source/pointcloud.rst cgkit/trunk/doc/source/polyhedron.rst cgkit/trunk/doc/source/polyhedrongeom.rst cgkit/trunk/doc/source/pyimport.rst cgkit/trunk/doc/source/quat.rst cgkit/trunk/doc/source/render.rst cgkit/trunk/doc/source/ri.rst cgkit/trunk/doc/source/ribarchive.rst cgkit/trunk/doc/source/ribexport.rst cgkit/trunk/doc/source/riutil.rst cgkit/trunk/doc/source/rmlightsource.rst cgkit/trunk/doc/source/rmmaterial.rst cgkit/trunk/doc/source/scene.rst cgkit/trunk/doc/source/scene_specific_modules.rst cgkit/trunk/doc/source/sceneglobals.rst cgkit/trunk/doc/source/sequence.rst cgkit/trunk/doc/source/sl.rst cgkit/trunk/doc/source/slideshow.rst cgkit/trunk/doc/source/sloargs.rst cgkit/trunk/doc/source/slots.rst cgkit/trunk/doc/source/slparams.rst cgkit/trunk/doc/source/sltokenize.rst cgkit/trunk/doc/source/spacedevice.rst cgkit/trunk/doc/source/sphere.rst cgkit/trunk/doc/source/spheregeom.rst cgkit/trunk/doc/source/spotlight3ds.rst cgkit/trunk/doc/source/stitch.rst cgkit/trunk/doc/source/stlimport.rst cgkit/trunk/doc/source/targetcamera.rst cgkit/trunk/doc/source/timer.rst cgkit/trunk/doc/source/tools.rst cgkit/trunk/doc/source/torus.rst cgkit/trunk/doc/source/torusgeom.rst cgkit/trunk/doc/source/trimesh.rst cgkit/trunk/doc/source/trimeshgeom.rst cgkit/trunk/doc/source/tunnel.rst cgkit/trunk/doc/source/valuetable.rst cgkit/trunk/doc/source/vec3.rst cgkit/trunk/doc/source/vec4.rst cgkit/trunk/doc/source/viewer.rst cgkit/trunk/doc/source/vrmlimport.rst cgkit/trunk/doc/source/wintab.rst cgkit/trunk/doc/source/worldobject.rst cgkit/trunk/doc/source/worldobjects.rst Added: cgkit/trunk/doc/Makefile =================================================================== --- cgkit/trunk/doc/Makefile (rev 0) +++ cgkit/trunk/doc/Makefile 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,88 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf build/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) build/html + @echo + @echo "Build finished. The HTML pages are in build/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml + @echo + @echo "Build finished. The HTML pages are in build/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in build/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in build/qthelp, like this:" + @echo "# qcollectiongenerator build/qthelp/PythonComputerGraphicsKit.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile build/qthelp/PythonComputerGraphicsKit.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex + @echo + @echo "Build finished; the LaTeX files are in build/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes + @echo + @echo "The overview file is in build/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in build/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in build/doctest/output.txt." Added: cgkit/trunk/doc/make.bat =================================================================== --- cgkit/trunk/doc/make.bat (rev 0) +++ cgkit/trunk/doc/make.bat 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,112 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +set SPHINXBUILD=sphinx-build +set ALLSPHINXOPTS=-d build/doctrees %SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^<target^>` where ^<target^> is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (build\*) do rmdir /q /s %%i + del /q /s build\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% build/html + echo. + echo.Build finished. The HTML pages are in build/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% build/dirhtml + echo. + echo.Build finished. The HTML pages are in build/dirhtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% build/pickle + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% build/json + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% build/htmlhelp + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in build/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% build/qthelp + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in build/qthelp, like this: + echo.^> qcollectiongenerator build\qthelp\PythonComputerGraphicsKit.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile build\qthelp\PythonComputerGraphicsKit.ghc + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% build/latex + echo. + echo.Build finished; the LaTeX files are in build/latex. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% build/changes + echo. + echo.The overview file is in build/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% build/linkcheck + echo. + echo.Link check complete; look for any errors in the above output ^ +or in build/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% build/doctest + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in build/doctest/output.txt. + goto end +) + +:end Added: cgkit/trunk/doc/source/3dsimport.rst =================================================================== --- cgkit/trunk/doc/source/3dsimport.rst (rev 0) +++ cgkit/trunk/doc/source/3dsimport.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,63 @@ +.. % 3DS import + + +3D Studio 3DS import +-------------------- + +The 3DS import plugin reads 3D Studio 3DS files [#]_. All light sources will be +imported as :class:`SpotLight3DS` objects and all materials as +:class:`Material3DS` objects. These objects approximate the appearance of the +light source/material when rendered with RenderMan but are not supported by the +interactive viewer. This means you have to replace the materials and the light +sources by appropriate objects if you want to display the scene interactively. + +.. note:: + + Animations are not supported yet. The content of the file will be evaluated at + the current time (i.e. the time set in cgkit, not the time set in the 3DS file). + +The plugin supports the following options that can be passed to the :func:`load` +command: + ++------------+-------------------+------------------------------+ +| Option | Default | Description | ++============+===================+==============================+ +| ``flags`` | ``GEOM_INIT_ALL`` | Flags for the mesh creation. | ++------------+-------------------+------------------------------+ +| ``parent`` | ``None`` | Parent object to be used for | +| | | the entire scene. | ++------------+-------------------+------------------------------+ + +The *flags* option specifies what data will be attached to the generated meshes. +It can be a combination of the following flags: + ++-------------------------+-------------------------------------------+ +| Flag | Description | ++=========================+===========================================+ +| ``GEOM_INIT_NORMALS`` | Compute normals | ++-------------------------+-------------------------------------------+ +| ``GEOM_INIT_MATIDS`` | Add material IDs | ++-------------------------+-------------------------------------------+ +| ``GEOM_INIT_TEXELS`` | Add texture coordinates | ++-------------------------+-------------------------------------------+ +| ``GEOM_INIT_FLAGS`` | Add the face flags (e.g. edge visibility) | ++-------------------------+-------------------------------------------+ +| ``GEOM_INIT_SMOOTHING`` | Add smoothing group information | ++-------------------------+-------------------------------------------+ +| ``GEOM_INIT_ALL`` | All of the above | ++-------------------------+-------------------------------------------+ + +.. note:: + + The plugin uses the lib3ds library which is available at + `<http://lib3ds.sourceforge.net>`_. + + | *The 3D Studio File Format Library* + | *Copyright (C) 1996-2001 by J.E. Hoffmann* + | *All rights reserved.* + +.. rubric:: Footnotes + +.. [#] Note that the 3DS format is the format that the old 3D Studio (for DOS) was + using, it is not the native format of 3D Studio MAX anymore. + Added: cgkit/trunk/doc/source/asfamc.rst =================================================================== --- cgkit/trunk/doc/source/asfamc.rst (rev 0) +++ cgkit/trunk/doc/source/asfamc.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,105 @@ + +:mod:`asfamc` --- Acclaim skeleton and motion file (ASF/AMC) reader +=================================================================== + +.. module:: cgkit.asfamc + :synopsis: Acclaim skeleton and motion file (ASF/AMC) reader + +This module provides base classes to read Acclaim Skeleton and Motion Files (ASF/AMC) +containing the definition of a character and its motion. The classes parse +the files and call handler methods that can be implemented in a derived class. + +ASFReader class +--------------- + +The :class:`ASFReader` class reads Acclaim Skeleton Files (ASF) and calls +appropriate methods which have to be implemented in a derived class. + + +.. class:: ASFReader(filename) + + *filename* is the name of the ASF file that should be read. + + +.. method:: ASFReader.read() + + Read the entire file. + + +.. method:: ASFReader.onVersion(version) + + This method is called when the file format version is encountered. *version* is + a float containing the version number. + + +.. method:: ASFReader.onName(name) + + This method is called when the skeleton name is read. + + +.. method:: ASFReader.onUnits(units) + + This method is called when the units section was read. *units* is a dictionary + that contains all definitions that were present in the units section of the + input file. The key is the unit name (such as ``mass``, ``length`` and + ``angle``) and the value is the corresponding value. If possible the value was + cast to float, otherwise it's still a string. + + +.. method:: ASFReader.onDocumentation(doc) + + This method is called when the file documentation was read. *doc* contains the + documentation (which may contain several lines). + + +.. method:: ASFReader.onRoot(data) + + This method is called when the root section was read. This section contains + information about the root of the skeleton. *data* is a dictionary that contains + all the key-value pairs in the root section. The value is always a tuple (even + when it's only one single value). + + +.. method:: ASFReader.onBonedata(bones) + + This method is called after the entire bone data was read. *bones* is a list of + bone definitions. Each definition is a data dictionary containing the key-value + pairs in the respective bone section. All values are tuples (even when it's only + one single value). An exception to this is the ``limits`` attribute which is a + list of (*min*, *max*) tuples that contain the minumum and maximum limits as + floats (or as the special strings ``"-inf"`` and ``"inf"``). + + +.. method:: ASFReader.onHierarchy(links) + + This method is called after the hierarchy section was read. *links* is a list of + 2-tuples (*parent*, *children*) where *parent* is the name of the parent bone + and *children* is a list of children bone names. + +.. % ---------------------------------------------------------------------- + + +AMCReader class +--------------- + +The :class:`AMCReader` class reads Acclaim Motion Capture Data (AMC) files and +calls :meth:`onFrame` for every motion sample in the file. + + +.. class:: AMCReader(filename) + + *filename* is the name of the AMC file that should be read. + + +.. method:: AMCReader.read() + + Read the entire file. + + +.. method:: AMCReader.onFrame(framenr, data) + + This method is called for every frame. *framenr* is the frame number and *data* + is a list of 2-tuples (*bone*, *values*) where *bone* is a bone name and + *values* the corresponding position/orientation for this frame. The number of + values and the meaning of the values is defined in the corresponding ASF file. + Added: cgkit/trunk/doc/source/asfamcimport.rst =================================================================== --- cgkit/trunk/doc/source/asfamcimport.rst (rev 0) +++ cgkit/trunk/doc/source/asfamcimport.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,32 @@ +.. % ASF/AMC import + + +Acclaim Skeleton/Motion Capture file (ASF/AMC) import +----------------------------------------------------- + +The ASF/AMC plugin reads Acclaim Skeleton Files (ASF) containing the structure +of a character and Acclaim Motion Capture (AMC) files containing the actual +motion. + +You can read an ASF file alone in which case only the skeleton will be read. +When reading an AMC file, the plugin also has to read the corresponding ASF file +as this file contains information how to interpret the data in the AMC file. By +default, the plugin looks for an ASF file with the same name than the AMC file +but with suffix ``.asf`` instead of ``.amc``. If this file doesn't exist the +plugin checks if the directory contains only one ASF file and chooses this one. +If this also fails, an exception is generated and you should pass the ASF file +name explicitly as argument to the :func:`load` function. + +The plugin supports the following options that can be passed to the :func:`load` +command when reading AMC files: + ++---------------+----------+-------------------------------+ +| Option | Default | Description | ++===============+==========+===============================+ +| ``asf`` | ``None`` | The name of the corresponding | +| | | skeleton file. | ++---------------+----------+-------------------------------+ +| ``framerate`` | ``30`` | The framerate of the motion | +| | | data in the AMC file. | ++---------------+----------+-------------------------------+ + Added: cgkit/trunk/doc/source/beziercurve.rst =================================================================== --- cgkit/trunk/doc/source/beziercurve.rst (rev 0) +++ cgkit/trunk/doc/source/beziercurve.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,28 @@ +.. % BezierCurve + + +:class:`BezierCurve` --- Bezier curve +===================================== + + +.. class:: BezierCurve(name = "BezierCurve", pnts = None, closed = False, epsilon = 0.01, subdiv = 4, show_tangents = False, curvegeom = None ) + + *pnts* is a list of :class:`BezierPoint` objects (see section + :ref:`beziercurvegeom`). + + If *closed* is set to ``True`` the last point will be connected to the first + point. + + *epsilon* is a threshold value that determines the accuracy of length + calculations of the curve. + + *subdiv* is the number of subdivisions that are made to draw the curve using + OpenGL. + + If *show_tangents* is set to ``True`` the OpenGL visualization will also show + the in and out tangents. + + You can also pass a previously created :class:`BezierCurveGeom` object via the + *curvegeom* argument. In this case, the arguments *pnts*, *closed*, *epsilon*, + *subdiv* and *show_tangents* are ignored. + Added: cgkit/trunk/doc/source/beziercurvegeom.rst =================================================================== --- cgkit/trunk/doc/source/beziercurvegeom.rst (rev 0) +++ cgkit/trunk/doc/source/beziercurvegeom.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,108 @@ +.. % BezierCurveGeom + + +.. _beziercurvegeom: + +:class:`BezierCurveGeom` --- Piecewise cubic Bezier curve +========================================================= + +The :class:`BezierCurveGeom` class represents a piecewise cubic curve in 3D +space that is composed of an arbitrary number of cubic Bezier segments. The +class stores a number of 3D points that are interpolated by the curve. Each +point has an in tangent and an out tangent associated with it that define how +the curve enters and leaves the point. + + +.. class:: BezierPoint(pos, intangent=vec3(0), outtangent=vec3(0)) + + This class just stores a position and the in and out tangents and is used to + pass these parameters to the constructor of a :class:`BezierCurveGeom`. + + *pos* is a point position that the curve will interpolate. + + *intangent* and *outtangent* define where the curve enters and leaves the point. + + +.. class:: BezierCurveGeom(pnts = None, closed = False, epsilon = 0.01, subdiv = 4, show_tangents = False) + + *pnts* is a list of :class:`BezierPoint` objects describing the points to + interpolate and the in and out tangents. + + If *closed* is set to ``True`` the last point will be connected to the first + point. + + *epsilon* is a threshold value that determines the accuracy of length + calculations of the curve. + + *subdiv* is the number of subdivisions that are made to draw the curve using + OpenGL. + + If *show_tangents* is set to ``True`` the OpenGL visualization will also show + the in and out tangents. + +A :class:`BezierCurveGeom` has the following slots: + ++----------------------+--------+--------+------------------+ +| Slot | Type | Access | Description | ++======================+========+========+==================+ +| ``pnts_slot`` | [vec3] | rw | The curve points | ++----------------------+--------+--------+------------------+ +| ``intangents_slot`` | [vec3] | rw | The in tangents | ++----------------------+--------+--------+------------------+ +| ``outtangents_slot`` | [vec3] | rw | The out tangents | ++----------------------+--------+--------+------------------+ + + +.. attribute:: BezierCurveGeom.closed + + This is a boolean indicating wheter the curve is closed or not. You can read and + write this attribute. + + +.. attribute:: BezierCurveGeom.numsegs + + The number of Bezier segments in the curve. You can only read this attribute. + + +.. attribute:: BezierCurveGeom.paraminterval + + This is a tuple (*t_min*, *t_max*) containing the valid parameter interval of + the curve. You can only read this attribute. + +.. % eval + + +.. method:: BezierCurveGeom.eval(t) + + Evaluate the curve at parameter *t* and return the curve point. + +.. % evalFrame + + +.. method:: BezierCurveGeom.evalFrame(t) + + Evaluate the curve at parameter *t* and return the curve point, the tangent and + the second derivative. + +.. % deriv + + +.. method:: BezierCurveGeom.deriv(t) + + Return the first derivative (the tangent) at parameter *t*. + +.. % arcLen + + +.. method:: BezierCurveGeom.arcLen(t) + + Return the arc length of the curve up to the point specified by the parameter + *t*. + +.. % length + + +.. method:: BezierCurveGeom.length() + + Return the entire length of the curve. This is equivalent to ``arcLen(t_max)``. + Added: cgkit/trunk/doc/source/boundingbox.rst =================================================================== --- cgkit/trunk/doc/source/boundingbox.rst (rev 0) +++ cgkit/trunk/doc/source/boundingbox.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,69 @@ +.. % BoundingBox + +:mod:`boundingbox` --- An axis-aligned bounding box +=================================================== + +.. module:: cgkit.boundingbox + :synopsis: An axis-aligned bounding box + + +.. class:: BoundingBox([b1, b2]) + + *b1* and *b2* are the initial bounds (as :class:`vec3`) of the bounding box. + Everything that lies within the specified box (including the bounds) is + considered inside the bounding box. If you don't specify any bounds the bounding + box is initially empty. + + +.. method:: BoundingBox.clear() + + Make the bounding box empty. + + +.. method:: BoundingBox.isEmpty() + + Return ``True`` if the bounding box is empty. + + +.. method:: BoundingBox.getBounds([dir]) + + Return the minimum and maximum bound. The bounds are returned as :class:`vec3` + objects. The optional vec3 argument *dir* specifies which corners are actually + returned. When viewed along a ray with the given direction the minimum bound + always comes before the maximum bound. + + +.. method:: BoundingBox.setBounds(b1, b2) + + Set new bounds for the bounding box. The rectangle given by *b1* and *b2* + defines the new bounding box. + + +.. method:: BoundingBox.center() + + Return the center of the bounding box (i.e. 0.5\*(bmin+bmax)). The null vector + is returned if the bounding box is empty. + + +.. method:: BoundingBox.addPoint(p) + + Enlarge the bounding box so that the point *p* is enclosed in the box. + + +.. method:: BoundingBox.addBoundingBox(bb) + + Enlarge the bounding box so that the bounding box *bb* is enclosed in the box. + + +.. method:: BoundingBox.transform(M) + + Returns a transformed bounding box. The transformation is given by *M* which + must be a :class:`mat4`. The result will still be axis aligned, so the volume + will not be preserved. + + +.. method:: BoundingBox.clamp(p) + + Clamp the point *p* so that it lies within the bounding box. Each axis is + clamped against the corresponding bounding box interval. + Added: cgkit/trunk/doc/source/box.rst =================================================================== --- cgkit/trunk/doc/source/box.rst (rev 0) +++ cgkit/trunk/doc/source/box.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,9 @@ +.. % Box + + +:class:`Box` --- Box object +=========================== + + +.. class:: Box(name = "Box", lx = 1.0, ly = 1.0, lz = 1.0, segmentsx = 1, segmentsy = 1, segmentsz = 1, dynamics = True, static = False, ...[WorldObject params]... ) + Added: cgkit/trunk/doc/source/boxgeom.rst =================================================================== --- cgkit/trunk/doc/source/boxgeom.rst (rev 0) +++ cgkit/trunk/doc/source/boxgeom.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,83 @@ +.. % BoxGeom + + +:class:`BoxGeom` --- Box geometry +================================= + + +.. class:: BoxGeom(lx=1.0, ly=1.0, lz=1.0, segmentsx=1, segmentsy=1, segmentsz=1) + + *lx*, *ly* and *lz* specify the dimensions of the box in the respective + direction. + + *segmentsx*, *segmentsy* and *segmentsz* are the number of segments in each + direction. + +A :class:`BoxGeom` has the following slots: + ++------------------------+-------+--------+---------------------------+ +| Slot | Type | Access | Description | ++========================+=======+========+===========================+ +| ``cog_slot`` | vec3 | r | The local center of | +| | | | gravity | ++------------------------+-------+--------+---------------------------+ +| ``inertiatensor_slot`` | mat3 | r | The local inertia tensor | ++------------------------+-------+--------+---------------------------+ +| ``lx_slot`` | float | rw | The length in x direction | ++------------------------+-------+--------+---------------------------+ +| ``ly_slot`` | float | rw | The length in y direction | ++------------------------+-------+--------+---------------------------+ +| ``lz_slot`` | float | rw | The length in z direction | ++------------------------+-------+--------+---------------------------+ +| ``segmentsx_slot`` | int | rw | The number of segments in | +| | | | x direction | ++------------------------+-------+--------+---------------------------+ +| ``segmentsy_slot`` | int | rw | The number of segments in | +| | | | y direction | ++------------------------+-------+--------+---------------------------+ +| ``segmentsz_slot`` | int | rw | The number of segments in | +| | | | z direction | ++------------------------+-------+--------+---------------------------+ + +.. % Attributes + + +.. attribute:: BoxGeom.cog + + Center of gravity with respect to the local coordinate system. + + +.. attribute:: BoxGeom.inertiatensor + + Inertia tensor with respect to the local coordinate system. + + +.. attribute:: BoxGeom.lx + + The length in x direction. + + +.. attribute:: BoxGeom.ly + + The length in y direction. + + +.. attribute:: BoxGeom.lz + + The length in z direction. + + +.. attribute:: BoxGeom.segmentsx + + The number of segments in x direction. + + +.. attribute:: BoxGeom.segmentsy + + The number of segments in y direction. + + +.. attribute:: BoxGeom.segmentsz + + The number of segments in z direction. + Added: cgkit/trunk/doc/source/building.rst =================================================================== --- cgkit/trunk/doc/source/building.rst (rev 0) +++ cgkit/trunk/doc/source/building.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,131 @@ +*********************** +Building and Installing +*********************** + +This chapter provides information about how to build the kit yourself from the +sources. You can skip this chapter if the kit is already installed on your +system or if you are installing binary versions. + +.. % Building and Installing + +.. % ---------------------------------------------------------------------- + + +Building from the sources +========================= + +The following instructions will build and install the full version of cgkit. You +can also install the 'light' version by setting the variable +``INSTALL_CGKIT_LIGHT`` to ``True`` in your config file (just rename the file +``config_template.cfg`` to ``config.cfg`` and uncomment the corresponding line +at the top of the file). Except for Python itself, installing the light version +does not require any additional compiler, tool or library, you simply have to +call :: + + > python setup.py install + +while being in the root directory of the cgkit sources. For installing the light +version you can skip the remainder of this section, but read on if you actually +want to install the full version. + +The steps required to build the full version of the kit are the same on every +platform. The build process assumes you have the following tools/libraries +installed on your system: + +* `Boost <http://www.boost.org/>`_ - Make sure that Boost is installed + including Boost.Python (e.g. Mac users who are using MacPorts have to install + the "+python" variant). + +* `SCons <http://www.scons.org/>`_ + +* `lib3ds <http://lib3ds.sourceforge.net/>`_ (v1.x) - Optional. You need this + if you want to import/export 3DS files. + +* `CyberX3D <http://www.cybergarage.org/vrml/cx3d/cx3dcc/index.html>`_ - + Optional. You need this if you want to import/export VRML or X3D files. + +* `3DxWare SDK <http://www.3dconnexion.com/sdk.htm>`_ - Optional (currently + Windows only). You need this if you want to use a SpaceMouse or SpaceBall. + +* `Wintab Programmer's Kit <http://www.wacomeng.com/devsupport/index.html>`_ - Optional (Windows + only). You need this if you want to use a tablet (or similar device). + +* `GloveSDK <http://www.5dt.com/>`_ - Optional. You need this if you want to use + a data glove. Note: Download the version 2 SDK from the Ultra series, this can + also be used with older gloves. You could also use version 1, but some + functionality won't be available then. + +If you have successfully installed the above tools and libraries you can proceed +with building the kit. The first step is to get the cgkit sources. You can either +download the source archive or check out the latest version from svn. The +following build instructions apply to both versions. + +The package consists of three parts: + +#. A pure C++ library that is independent of Python. This library is located in + the :file:`supportlib` directory. + +#. Python wrappers around the above C++ library (using Boost.Python). These + wrappers are located in the :file:`wrappers` directory. + +#. The actual Python package using the wrappers. The package is located in the + :file:`cgkit` directory. The command line tools are in the main directory + and the :file:utilities` directory. + +Part 2) and 3) is built and installed via the Python distutils (i.e. the +:file:`setup.py` script). But before you can do so, you have to build the C++ +library manually using SCons. + +Building the C++ support library: + +The C++ library is located in the directory :file:`supportlib`. The library uses +SCons as its build system. If you have to customize the build process you can +create a file :file:`cpp_config.cfg` where you can set some build variables +(e.g. adding search paths for include files). There is a template file +:file:`cpp_config_template.cfg` which you can use to create the actual config +file. However, if you have installed every library in standard directories it +may well be that you don't need any config file at all. Eventually you can build +the library by calling :program:`scons`:: + + > cd supportlib + > # ...create & modify cpp_config.cpp if necessary... + > scons + +If everything went fine you can see the result in the :file:`lib` subdirectory +(:file:`libcore.a` on Linux and OSX or :file:`core.lib` on Windows). + +Building the Python wrappers: + +The next step is to build and install the Python package. The package uses the +distutils, so you will find the familiar :file:`setup.py` script in the main +directory. Customization of the build process can again be done in a file called +:file:`config.cfg` and :file:`config_pyXY.cfg` where ``XY`` is the Python version +you want to build against. The former file is always read whereas the latter +is only read when building against that particular version of Python. +There is a template +file :file:`config_template.cfg` which you can use to create your own config +file. After setting up your configuration you can install the package with the +usual procedure:: + + > cd .. # if you were still in the supportlib directory + > python setup.py install + +(see the manual Installing Python Modules in your Python documentation for more +details about the distutils and how to proceed if you have any special +requirements) + +Please also have a look at section :ref:`externaldeps` for a list of third-party +packages you might have to install before you can use cgkit. You can check cgkit +and its dependencies with the script :file:`utilities/checkenv.py` that is part +of the source archive. For a more thorough test you can run the unit tests in +the :file:`unittests` directory. + +A note to developers: You can build the package inplace by calling :: + + > python setup.py build_ext --inplace + +In this case, the resulting binaries will be placed directly in the +:file:`cgkit` directory which will then contain the entire package. Use the +:envvar:`PYTHONPATH` variable and add the path to the main directory if you want +to use the inplace version from any other directory. + Added: cgkit/trunk/doc/source/bvh.rst =================================================================== --- cgkit/trunk/doc/source/bvh.rst (rev 0) +++ cgkit/trunk/doc/source/bvh.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,94 @@ +:mod:`bvh` --- Reading Biovision Hierarchical (BVH) motion capture files +======================================================================== + +.. module:: cgkit.bvh + :synopsis: Reading Biovision Hierarchical (BVH) motion capture files + + +This module contains the :class:`BVHReader` class which can be used as a base +class for reading Biovision Hierarchical (BVH) files. The class reads the file +and invokes callback methods with the corresponding data in the file. Derived +classes have to implement those callback methods and process the data as +appropriate. + + +.. class:: BVHReader(filename) + + *filename* is the name of the BVH file that should be read. + + +.. method:: BVHReader.read() + + Read the entire file. + + +.. method:: BVHReader.onHierarchy(root) + + This method is called after the joint hierarchy was read. The entire hierarchy + is passed in the argument *root* which is a :class:`Node` object. + + +.. method:: BVHReader.onMotion(frames, dt) + + This method is called when the motion data begins. *frames* is the number of + motion samples that follow and *dt* is the time interval that corresponds to one + frame. + + +.. method:: BVHReader.onFrame(values) + + This method is called for each motion sample (frame) in the file. *values* is a + list of floats that contains the position and angles of the entire skeleton. The + order is the same than when traversing the joint hierarchy in a depth-first + manner. + +.. % ------------------------------------ + + +Node objects +------------ + +The :meth:`onHierarchy` method of the :class:`BVHReader` class takes the joint +hierarchy of the skeleton as input. Each node in this hierarchy is represented +by a :class:`Node` object that contains all information stored in the BVH file. + + +.. class:: Node + + A :class:`Node` object represents one joint in the hierarchy. + + +.. attribute:: Node.name + + This is the name of the joint (or the root). + + +.. attribute:: Node.channels + + This is a list of channel names that are associated with this joint. This list + determines how many values are stored in the motion section and how they are to + be interpreted. Each channel name can be one of ``Xposition``, ``Yposition``, + ``Zposition``, ``Xrotation``, ``Yrotation``, ``Zrotation``. + + +.. attribute:: Node.offset + + This is a 3-tuple of floats containing the offset position of this joint + relative to the parent joint. + + +.. attribute:: Node.children + + This is a list of children joints (which are again described by :class:`Node` + objects). + + +.. method:: Node.isRoot() + + Returns ``True`` if the node is the root node. + + +.. method:: Node.isEndSite() + + Returns ``True`` if the node is a leaf. + Added: cgkit/trunk/doc/source/bvhimport.rst =================================================================== --- cgkit/trunk/doc/source/bvhimport.rst (rev 0) +++ cgkit/trunk/doc/source/bvhimport.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,9 @@ +.. % BVH import + + +Biovision Hierarchical (BVH) import +----------------------------------- + +The BVH plugin reads Biovision Hierarchical files containing a skeleton +definition for a character and a motion for the character. + Added: cgkit/trunk/doc/source/ccylinder.rst =================================================================== --- cgkit/trunk/doc/source/ccylinder.rst (rev 0) +++ cgkit/trunk/doc/source/ccylinder.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,9 @@ +.. % Capped Cylinder + + +:class:`CCylinder` --- Capped cylinder object +============================================= + + +.. class:: CCylinder(name = ''CCylinder'', radius = 1.0, length = 1.0, segmentsu = 16, segmentsvl = 1, segmentsvr = 4, dynamics = True, static = False, ...[WorldObject params]... ) + Added: cgkit/trunk/doc/source/ccylindergeom.rst =================================================================== --- cgkit/trunk/doc/source/ccylindergeom.rst (rev 0) +++ cgkit/trunk/doc/source/ccylindergeom.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,80 @@ +.. % CCylinderGeom + + +:class:`CCylinderGeom` --- Capped cylinder geometry +=================================================== + + +.. class:: CCylinderGeom(radius=1.0, length=1.0, segmentsu=16, segmentsvl=1, segmentsvr=4) + + *radius* is the radius of the cylinder and caps in the local coordinate system. + + *length* is the length of the cylinder (not counting the caps). + + *segmentsu* is the number of segments in u direction, *segmentsvl* the number of + segments of the cylinder part and *segmentsvr* the number of segments for each + cap. + +A :class:`CCylinderGeom` has the following slots: + ++------------------------+-------+--------+---------------------------+ +| Slot | Type | Access | Description | ++========================+=======+========+===========================+ +| ``cog_slot`` | vec3 | r | The local center of | +| | | | gravity | ++------------------------+-------+--------+---------------------------+ +| ``inertiatensor_slot`` | mat3 | r | The local inertia tensor | ++------------------------+-------+--------+---------------------------+ +| ``radius_slot`` | float | rw | The radius of the | +| | | | cylinder | ++------------------------+-------+--------+---------------------------+ +| ``length_slot`` | float | rw | The lenght of the | +| | | | cylinder | ++------------------------+-------+--------+---------------------------+ +| ``segmentsu_slot`` | int | rw | The number of segments in | +| | | | u direction | ++------------------------+-------+--------+---------------------------+ +| ``segmentsvl_slot`` | int | rw | The number of cylinder | +| | | | segments in v direction | ++------------------------+-------+--------+---------------------------+ +| ``segmentsvr_slot`` | int | rw | The number of cap | +| | | | segments in v direction | ++------------------------+-------+--------+---------------------------+ + +.. % Attributes + + +.. attribute:: CCylinderGeom.cog + + Center of gravity with respect to the local coordinate system. + + +.. attribute:: CCylinderGeom.inertiatensor + + Inertia tensor with respect to the local coordinate system. + + +.. attribute:: CCylinderGeom.radius + + The radius of the cylinder and caps. + + +.. attribute:: CCylinderGeom.length + + The length of the cylinder (not counting the caps). + + +.. attribute:: CCylinderGeom.segmentsu + + The number of segments in u direction. + + +.. attribute:: CCylinderGeom.segmentsvl + + The number of cylinder segments in v direction. + + +.. attribute:: CCylinderGeom.segmentsvr + + The number of cap segments in v direction. + Added: cgkit/trunk/doc/source/cgkitinfo.rst =================================================================== --- cgkit/trunk/doc/source/cgkitinfo.rst (rev 0) +++ cgkit/trunk/doc/source/cgkitinfo.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,23 @@ + +:mod:`cgkitinfo` --- Release information +======================================== + +.. module:: cgkit.cgkitinfo + :synopsis: Provides version information. + + +New in version 1.1. This module contains two variables that carry information +about the version of the kit. + + +.. data:: version + + A string containing version information of this release. + + +.. data:: version_info + + A 4-tuple (*major*, *minor*, *micro*, *releaselevel*) containing the components + of the version number. The first three values are integers, whereas + *releaselevel* is a string containing "cvs", "alpha", "beta" or "final". + Added: cgkit/trunk/doc/source/cgtypes.rst =================================================================== --- cgkit/trunk/doc/source/cgtypes.rst (rev 0) +++ cgkit/trunk/doc/source/cgtypes.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,100 @@ + +:mod:`cgtypes` --- Vectors, matrices and quaternions +==================================================== + +.. module:: cgkit.cgtypes + :synopsis: Basic types useful for computer graphics. + +.. toctree:: + + vec3 + vec4 + mat3 + mat4 + quat + + +This module contains 3D/4D vector, matrix and quaternion types: + +* vec3 -- a three dimensional vector type to store points, vectors, normals or + even colors. + +* vec4 -- a four dimensional vector type to store homogenous points, for + example. + +* mat3 -- a 3x3 matrix to store linear transformations. + +* mat4 -- a 4x4 matrix to store affine transformations. + +* quat -- a quaternion type as a specialized way to store rotations. + +You import all of those types at once with :: + + from cgkit.cgtypes import * + +or you can import them individually like this :: + + from cgkit.cgtypes import vec3, mat4 + +In general, you can use those types just as if they were built-in types which +means the mathematical operators can be used and have their respective meaning. +Each type has some additional methods which are described in the respective +documentation. + +Here are some examples:: + + >>> from cgkit.cgtypes import * + >>> v=vec3(0.5,1.0,-2.5) + >>> print v + (0.5000, 1.0000, -2.5000) + >>> print v.length() + 2.73861278753 + >>> v=v.normalize() + >>> print v + (0.1826, 0.3651, -0.9129) + >>> print v.length() + 1.0 + +Now let's construct a rotation matrix that rotates points by 90 degrees around +the z-axis:: + + >>> M=mat4(1).rotate(0.5*math.pi, vec3(0,0,1)) + +and apply the rotation to the vector (1,0,0) (the x-axis):: + + >>> print M*vec3(1,0,0) + (0.0000, 1.0000, 0.0000) + +The module contains the following functions: + + +.. function:: getEpsilon() + + Return the epsilon threshold which is used for doing comparisons. + + +.. function:: setEpsilon(eps) + + Sets a new epsilon threshold and returns the previously set value. Two values + are considered to be equal if their absolute difference is less than or equal to + epsilon. + + +.. function:: slerp(t, q0, q1, shortest=True) + + Performs a spherical linear interpolation between two quaternions *q0* and *q1*. + For *t*=0.0 the return value equals *q0*, for *t*=1.0 it equals *q1*. *q0* and + *q1* must be unit quaternions. If *shortest* is ``True`` the interpolation will + always be along the shortest path, otherwise it depends on the orientation of + the input quaternions whether the shortest or longest path will be taken (you + can switch between the paths by negating either *q0* or *q1*). + + +.. function:: squad(t, a, b, c, d) + + Performs a spherical cubic interpolation between quaternion *a* and *d* where + quaternion *b* and *c* define the shape of the interpolation curve. For *t*=0.0 + the return value equals *a*, for *t*=1.0 it equals *d*. All quaternions must be + unit quaternions. + + Added: cgkit/trunk/doc/source/cmds.rst =================================================================== --- cgkit/trunk/doc/source/cmds.rst (rev 0) +++ cgkit/trunk/doc/source/cmds.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,298 @@ +:mod:`cmds` --- Scene-specific commands +======================================= + +.. module:: cgkit.cmds + :synopsis: Scene-specific commands + +This chapter describes the builtin commands which are implemented in the +:mod:`cgkit.cmds` module and which are always available. New commands can be +added via the :func:`register` function (for example, by plugins), so there +might actually be more commands available than are listed here. + +.. % cmds + +.. % fitCone + + +.. function:: fitCone(pos, obj) + + Compute a cone that has its apex at *pos* and that includes *obj*. The generated + cone is the minimal cone that entirely contains the bounding box of *obj* (which + must be a :class:`WorldObject`). *pos* is the apex of the cone given in world + coordinates. The return value is a tuple (*n*, *w*) where *n* is the axis + direction of the cone and *w* is the (full) angle in radians. + +.. % convertToTriMesh + + +.. function:: convertToTriMesh(objs) + + Convert all specified objects into triangle meshes. *obj* may be an individual + world object or a sequence of objects. Each object may be specified by name or + by reference. + +.. % setupObjectNames + + +.. function:: setupObjectNames() + + Return a string that can be executed to 'import' all scene names. After + executing the returned string you can access all top level objects via their + name. + + Example:: + + >>> listWorld() + Root + +---Bottom (Box/BoxGeom) + +---GLPointLight (GLPointLight/-) + +---GLPointLight1 (GLPointLight/-) + +---Middle (Sphere/SphereGeom) + +---TargetCamera (TargetCamera/-) + +---Top (Box/BoxGeom) + >>> exec setupObjectNames() + >>> Bottom + <cgkit.box.Box object at 0x094765D0> + >>> TargetCamera + <cgkit.targetcamera.TargetCamera object at 0x09470BD0> + >>> Top + <cgkit.box.Box object at 0x09476690> + +.. % group() + + +.. function:: group(*children, **name) + + Group several world objects together. All non keyword arguments somehow refer to + world objects that will all be grouped together. An argument may be either a + :class:`WorldObject`, the name of a world object or a sequence of world objects + or names. The name of the new group may be given via the ``name`` keyword + argument. The return value is the newly created Group object. + + Example:: + + >>> b1=Box() + >>> b2=Box() + >>> s1=Sphere() + >>> listWorld() + Root + +---Box (Box/BoxGeom) + +---Box1 (Box/BoxGeom) + +---Sphere (Sphere/SphereGeom) + >>> group("Box", "Box1", s1, name="MyGroup") + >>> listWorld() + Root + +---MyGroup (Group/-) + +---Box (Box/BoxGeom) + +---Box1 (Box/BoxGeom) + +---Sphere (Sphere/SphereGeom) + +.. % ungroup() + + +.. function:: ungroup(group) + + Break up a group in its individual components. *group* is a group object or the + name of a group object. This function does not only work with :class:`Group` + objects but actually with any object that has no direct geometry assigned to it. + + Example:: + + >>> listWorld() + Root + +---MyGroup (Group/-) + +---Box (Box/BoxGeom) + +---Box1 (Box/BoxGeom) + +---Sphere (Sphere/SphereGeom) + >>> ungroup("MyGroup") + >>> listWorld() + Root + +---Box (Box/BoxGeom) + +---Box1 (Box/BoxGeom) + +---Sphere (Sphere/SphereGeom) + +.. % replaceMaterial() + + +.. function:: replaceMaterial(name, newmat) + + Iterate over all world objects and replace each material called *name* with + material object *newmat*. + +.. % link() + + +.. function:: link(childs, parent=None, relative=False) + + Link the world objects *childs* to *parent*. Previously existing links are + removed. If *parent* is ``None`` then the links are just removed. The argument + *childs* may be either a single world object or a sequence of world objects. + Instead of world objects you can also pass the names of the world objects. + + By default, the absolute position and orientation of the children is maintained + (i.e. the local transform is modified). If you set *relative* to ``True`` the + local transform is not modified which will change the position/orientation of + the children (unless the parent transform is the identity). + + Note: The function modifies the name of a child object if there would be a clash + with an existing object under the new parent. + +.. % drawClear() + + +.. function:: drawClear() + + Clear all drawing objects. + +.. % drawMarker() + + +.. function:: drawMarker(pos, col=(1,1,1), size=1) + + Draw a marker (a point). *col* is the color of the marker and *size* its radius. + +.. % drawLine() + + +.. function:: drawLine(pos1, pos2, col=(1,1,1), size=1) + + Draw a line from *pos1* to *pos2*. *col* is the color of the line and *size* + its width. + +.. % drawText() + + +.. function:: drawText(pos, txt, font=None, col=(1,1,1)) + + Draw the text *txt* at position *pos* (3D position). *col* is the color of the + text. The text is drawn using GLUT functionality. *font* is a GLUT font constant + as defined in the :mod:`OpenGL.GLUT` module. It can take one of the following + values: + +* ``GLUT_BITMAP_8_BY_13`` + +* ``GLUT_BITMAP_9_BY_15`` (default) + +* ``GLUT_BITMAP_TIMES_ROMAN_10`` + +* ``GLUT_BITMAP_TIMES_ROMAN_24`` + +* ``GLUT_BITMAP_HELVETICA_10`` + +* ``GLUT_BITMAP_HELVETICA_12`` + +* ``GLUT_BITMAP_HELVETICA_18`` + +.. % listWorld() + + +.. function:: listWorld() + + List the contents of the world as a tree. Example:: + + >>> load("demo4.py") + >>> listWorld() + Root + +---GLDistantLight (GLTargetDistantLight/-) + +---GLDistantLight1 (GLTargetDistantLight/-) + +---Sphere (Sphere/SphereGeom) + | +---Box0 (Box/BoxGeom) + | +---Box1 (Box/BoxGeom) + | +---Box2 (Box/BoxGeom) + | +---Box3 (Box/BoxGeom) + | +---Box4 (Box/BoxGeom) + | +---Box5 (Box/BoxGeom) + | +---Box6 (Box/BoxGeom) + | +---Box7 (Box/BoxGeom) + | +---Box8 (Box/BoxGeom) + | +---Box9 (Box/BoxGeom) + +---TargetCamera (TargetCamera/-) + +.. % load() + + +.. function:: load(filename, **options) + + Loads the given file without deleting the scene, so the contents of the file is + appended to the current scene. Any additional keyword argument is considered to + be an option and is passed to the importer. + + To be able to load the file there must be an appropriate import class (protocol: + "Import") available in the plugin manager. The class is determined by examining + the file extension. If no importer is found a :exc:`NoImporter` exception is + thrown. See chapter :ref:`importplugins` for the available standard importers. + + Any exception generated in the importer is passed to the caller. + +.. % save() + + +.. function:: save(filename, **options) + + Saves the current scene. Any additional keyword argument is considered to be an + option and is passed to the exporter. + + To be able to save the scene there must be an appropriate export class + (protocol: "Export") available in the plugin manager. The class is determined by + examining the file extension. If no exporter is found a :exc:`NoExporter` + exception is thrown. + + Any exception generated in the exporter is passed to the caller. + +.. % reset() + + +.. function:: reset() + + Reset an animation/simulation. This function sets the global time back to 0 and + signals the RESET event. + +.. % worldObject() + + +.. function:: worldObject(obj) + + If *obj* is a string type this function searches the world object with that name + and returns it. If *obj* is not a string, it is returned unchanged. + +.. % worldObjects() + + +.. function:: worldObjects(objs) + + Similar to :func:`worldObject` but the argument may be a sequence of objects or + strings. The return value is always a list, even if only one object was + specified as input. + +.. % register() + + +.. function:: register(cmds,...) + + Register functions in the cmds module. The function takes an arbitrary number of + arguments which must be callable objects. Each such function is registered in + the cmds module, so that other modules can call these functions as if they were + implemented directly in the cmds module. + +.. % importDefaultPlugins + + +.. function:: importDefaultPlugins(paths) + + Import the default plugins. The plugin files/directories specified by the + :envvar:`CGKIT_PLUGIN_PATH` environment variable (if it exists) are imported. + The function already outputs error messages and returns a list of plugin + descriptors. + +.. % splitPaths + + +.. function:: splitPaths(paths) + + Split a string containing paths into the individual paths. The paths can either + be separated by ``':'`` or ``';'``. Windows drive letters are maintained, even + when ``':'`` is used as separator. :: + + >>> splitPaths("&:c:\\shaders:c:\\more_shaders;") + ['&', 'c:\\shaders', 'c:\\more_shaders'] + Added: cgkit/trunk/doc/source/component.rst =================================================================== --- cgkit/trunk/doc/source/component.rst (rev 0) +++ cgkit/trunk/doc/source/component.rst 2009-08-20 21:00:14 UTC (rev 338) @@ -0,0 +1,60 @@ +.. % Component + + +:class:`Component` --- Component base class +=========================================== + +A component is just something that has a name to identify it and that stores +slot objects. Usually, the slots are used for providing inputs to the component +which are used to generate outputs that can also be accessed via slots. The +component is then the 'black box' that computes the output from the input. + + +.. class:: Component(name = "", auto_insert = True) + + *name* is the name of the component which can be used to identify it. + + The component will be inserted into the scene automatically if *auto_insert* is + set ... [truncated message content] |
From: <mb...@us...> - 2009-08-20 20:57:10
|
Revision: 337 http://cgkit.svn.sourceforge.net/cgkit/?rev=337&view=rev Author: mbaas Date: 2009-08-20 20:56:55 +0000 (Thu, 20 Aug 2009) Log Message: ----------- Added the (preliminary) jobqueue module. Modified Paths: -------------- cgkit/trunk/setup.py Added Paths: ----------- cgkit/trunk/cgkit/jobqueue/ cgkit/trunk/cgkit/jobqueue/__init__.py cgkit/trunk/cgkit/jobqueue/defaultprocs/ cgkit/trunk/cgkit/jobqueue/defaultprocs/__init__.py cgkit/trunk/cgkit/jobqueue/defaultprocs/blender.py cgkit/trunk/cgkit/jobqueue/defaultprocs/dumpenv.py cgkit/trunk/cgkit/jobqueue/defaultprocs/maya.py cgkit/trunk/cgkit/jobqueue/defaultprocs/renderrib.py cgkit/trunk/cgkit/jobqueue/jobhandle.py cgkit/trunk/cgkit/jobqueue/jobproc.py cgkit/trunk/cgkit/jobqueue/jobqueue.py cgkit/trunk/unittests/test_jobqueue.py Added: cgkit/trunk/cgkit/jobqueue/__init__.py =================================================================== --- cgkit/trunk/cgkit/jobqueue/__init__.py (rev 0) +++ cgkit/trunk/cgkit/jobqueue/__init__.py 2009-08-20 20:56:55 UTC (rev 337) @@ -0,0 +1,46 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Python Computer Graphics Kit. +# +# The Initial Developer of the Original Code is Matthias Baas. +# Portions created by the Initial Developer are Copyright (C) 2009 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import os +from jobqueue import createJobQueue, JobQueue, JobQueueError +from jobproc import JobProc + +def defaultJobQueueLocation(): + """Return the directory location of the default job queue. + + The default queue can be set via the CGKIT_JOB_QUEUE environment variable. + Returns None if there is no default location. + """ + return os.getenv("CGKIT_JOB_QUEUE") Added: cgkit/trunk/cgkit/jobqueue/defaultprocs/blender.py =================================================================== --- cgkit/trunk/cgkit/jobqueue/defaultprocs/blender.py (rev 0) +++ cgkit/trunk/cgkit/jobqueue/defaultprocs/blender.py 2009-08-20 20:56:55 UTC (rev 337) @@ -0,0 +1,155 @@ +# Render using Blender + +import os, os.path, subprocess, sys, traceback +import cgkit.jobqueue + +class blender(cgkit.jobqueue.JobProc): + """Render a Blender file. + """ + + def __init__(self, blendFile=None, output=None, frames=None, format=None, + addExtension=None, sceneName=None, threads=None, + blenderCmd = "blender", + label=None): + """Constructor. + + blendFile: The file to render (option -b) + output: The output path and name (option -o). + frames: This is either a single int to render a single frame, a tuple + (start,end) to render a frame range or None to render everything. + The start or the end may be None to indicate the start or end + of the entire animation. + format: A string describing the output format (option -F). + addExtension: A boolean indicating whether a file extension should be added + or not (option -x). + sceneName: The scene name (option -S) + threads: The number of threads that should be used for rendering (option -t). + 0=system processor count. + blenderCmd: The name of the Blender executable + label: The GUI label string + """ + # Check the parameter types... + if not isinstance(blendFile, basestring): + raise TypeError("blendFile argument must be a string, got a %s instead"%getattr(type(blendFile), "__name__", "?")) + if not (output is None or isinstance(output, basestring)): + raise TypeError("output argument must be a string or None, got a %s instead"%getattr(type(output), "__name__", "?")) + if not (format is None or isinstance(format, basestring)): + raise TypeError("format argument must be a string or None, got a %s instead"%getattr(type(format), "__name__", "?")) + if not (addExtension is None or type(addExtension) is bool): + raise TypeError("format argument must be a string or None, got a %s instead"%getattr(type(addExtension), "__name__", "?")) + if not (sceneName is None or isinstance(sceneName, basestring)): + raise TypeError("sceneName argument must be a string or None, got a %s instead"%getattr(type(sceneName), "__name__", "?")) + if not (threads is None or type(threads) is int): + raise TypeError("threads argument must be an int or None, got a %s instead"%getattr(type(threads), "__name__", "?")) + if not isinstance(blenderCmd, basestring): + raise TypeError("blenderCmd argument must be a string, got a %s instead"%getattr(type(blenderCmd), "__name__", "?")) + if frames is None: + startFrame = None + endFrame = None + elif type(frames) is int: + startFrame = frames + endFrame = frames + else: + try: + s,e = frames + except: + raise TypeError("frames argument must be an int, two ints or None") + if not (s is None or type(s) is int): + raise TypeError("The start frame must be an int or None, got a %s instead"%getattr(type(s), "__name__", "?")) + if not (e is None or type(e) is int): + raise TypeError("The end frame must be an int or None, got a %s instead"%getattr(type(e), "__name__", "?")) + startFrame = s + endFrame = e + + # Set the default label if no label was given + if label is None: + label = 'Blender "%s"'%os.path.basename(blendFile) + + cgkit.jobqueue.JobProc.__init__(self, label=label) + + # Store the parameters + self._blenderExec = blenderCmd + self._blendFile = blendFile + self._output = output + self._startFrame = startFrame + self._endFrame = endFrame + self._format = format + self._addExtension = addExtension + self._sceneName = sceneName + self._threads = threads + + self._currentFrame = None + + self._hasError = False + self._errorLine = "" + + def run(self): + """Run Blender. + """ + cmd = self.createCommandLine() + print >>self.stdout, cmd + ret = self.execCmd(cmd) + if ret!=0: + print >>self.stderr, "\nReturn code:",ret + self._hasError = True + self._errorLine = "Return code: %s"%ret + + if self._hasError: + self.setError() + self.setStatusLine(self._errorLine) + else: + self.setStatusLine("Done") + + + def stdoutCallback(self, lineNr, line): + """Scan stdout for errors. + """ + self.setStatusLine(line.strip()) + if line.startswith("ERROR:") and not self._hasError: + print >>self.stderr, "stdout, line %s:\n%s"%(lineNr+1, line) + print >>self.stderr, "(see stdout for more details)" + self._hasError = True + self._errorLine = line + elif line.startswith("Fra:"): + try: + frame = int(line[4:].split()[0]) + except: + frame = self._currentFrame + + if frame!=self._currentFrame: + self._currentFrame = frame + if self._startFrame is not None and self._endFrame is not None: + pc = 100*(frame-self._startFrame)/(self._endFrame-self._startFrame+1) + self.setProgress(pc) + + def stderrCallback(self, lineNr, line): + """Scan stderr for errors. + """ + if not self._hasError and line.strip()!="": + self._hasError = True + + def createCommandLine(self): + """Return the command line for invoking Blender. + """ + toks = [self._blenderExec] + toks.append('-b "%s"'%self._blendFile) + if self._sceneName is not None: + toks.append('-S "%s"'%self._sceneName) + if self._output is not None: + toks.append('-o "%s"'%self._output) + if self._addExtension is not None: + toks.append("-x %d"%int(self._addExtension)) + if self._format is not None: + toks.append("-F %s"%self._format) + if self._threads is not None: + toks.append("-t %s"%self._threads) + if self._startFrame is not None and self._startFrame==self._endFrame: + toks.append("-f %d"%self._startFrame) + else: + if self._startFrame is not None: + toks.append("-s %s"%self._startFrame) + if self._endFrame is not None: + toks.append("-e %s"%self._endFrame) + toks.append("-a") + + return " ".join(toks) Added: cgkit/trunk/cgkit/jobqueue/defaultprocs/dumpenv.py =================================================================== --- cgkit/trunk/cgkit/jobqueue/defaultprocs/dumpenv.py (rev 0) +++ cgkit/trunk/cgkit/jobqueue/defaultprocs/dumpenv.py 2009-08-20 20:56:55 UTC (rev 337) @@ -0,0 +1,20 @@ +# Test job to dump the environment to stdout + +import os, socket, getpass +import cgkit.jobqueue + +class dumpenv(cgkit.jobqueue.JobProc): + """Render a Blender file. + """ + + def __init__(self): + cgkit.jobqueue.JobProc.__init__(self, label="Dump Environment") + + def run(self): + print ("Host name : %s"%socket.gethostname()) + print ("User name : %s"%getpass.getuser()) + print ("Current dir : %s"%os.getcwd()) + print ("\nEnvironment variables:\n") + vars = os.environ.keys() + for var in sorted(vars): + print (" %s = %s"%(var, os.environ.get(var))) Added: cgkit/trunk/cgkit/jobqueue/defaultprocs/maya.py =================================================================== --- cgkit/trunk/cgkit/jobqueue/defaultprocs/maya.py (rev 0) +++ cgkit/trunk/cgkit/jobqueue/defaultprocs/maya.py 2009-08-20 20:56:55 UTC (rev 337) @@ -0,0 +1,75 @@ +# Render using Maya + +import os, os.path, subprocess, sys, traceback +import cgkit.jobqueue + +class maya(cgkit.jobqueue.JobProc): + """Render a Maya file. + """ + + def __init__(self, sceneFile=None, project=None, + startFrame=None, endFrame=None, + renderCmd = "Render", + label=None): + """Constructor. + + label: The GUI label string + """ + # Check the parameter types... + if not isinstance(sceneFile, basestring): + raise TypeError("sceneFile argument must be a string, got a %s instead"%getattr(type(sceneFile), "__name__", "?")) + if not (project is None or isinstance(project, basestring)): + raise TypeError("project argument must be a string or None, got a %s instead"%getattr(type(project), "__name__", "?")) + if not (startFrame is None or type(startFrame) in [int, float]): + raise TypeError("startFrame must be a number type or None, got a %s instead"%getattr(type(startFrame), "__name__", "?")) + if not (endFrame is None or type(endFrame) in [int, float]): + raise TypeError("endFrame must be a number type or None, got a %s instead"%getattr(type(endFrame), "__name__", "?")) + if not isinstance(renderCmd, basestring): + raise TypeError("renderCmd argument must be a string, got a %s instead"%getattr(type(renderCmd), "__name__", "?")) + + # Set the default label if no label was given + if label is None: + label = 'Maya "%s"'%os.path.basename(sceneFile) + + cgkit.jobqueue.JobProc.__init__(self, label=label) + + # Store the parameters + toks = [renderCmd] + if startFrame is not None: + toks.append("-s %s"%startFrame) + if endFrame is not None: + toks.append("-e %s"%endFrame) + if project is not None and project!="": + toks.append('-proj "%s"'%project) + toks.append('"%s"'%sceneFile) + + self._cmdLine = " ".join(toks) + + def run(self): + """Run the Render tool. + """ + print >>self.stdout, self._cmdLine + ret = self.execCmd(self._cmdLine) + if ret!=0: + print >>self.stderr, "\nReturn code:",ret + self.setError() + else: + self.setStatusLine("Done") + + def stdoutCallback(self, lineNr, line): + """Scan stdout for status/errors. + """ + pass + + def stderrCallback(self, lineNr, line): + """Scan stderr for status/errors. + """ + # Does a frame begin? + if line.startswith("Starting Rendering"): + # Get the image path (remove the prefix and the trailing dot) + fileName = os.path.normpath(line.strip()[19:-1]) + p,n = os.path.split(fileName) + self.setStatusLine("Rendering %s (%s)"%(n,p)) + # Is there an error line? + elif line.startswith("Error:"): + self.setStatusLine(line.strip()) Added: cgkit/trunk/cgkit/jobqueue/defaultprocs/renderrib.py =================================================================== --- cgkit/trunk/cgkit/jobqueue/defaultprocs/renderrib.py (rev 0) +++ cgkit/trunk/cgkit/jobqueue/defaultprocs/renderrib.py 2009-08-20 20:56:55 UTC (rev 337) @@ -0,0 +1,44 @@ +# Render a RIB file + +import os, subprocess +import cgkit.jobqueue + +class renderrib(cgkit.jobqueue.JobProc): + + def __init__(self, rib, renderer="aqsis"): + cgkit.jobqueue.JobProc.__init__(self) + self._rib = rib + self._renderer = renderer.lower() + + def run(self): + args = [self._rendererExecutable(), self._rib] + cmd = " ".join(args) + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + shell=True) + outdata,errdata = proc.communicate() + outdata = outdata.rstrip() + errdata = errdata.rstrip() + print "OUT" + print outdata + print "ERR" + print errdata + print "RET" + print proc.returncode + + if proc.returncode!=0 or errdata!="": + self.setError() + + + def _rendererExecutable(self): + """Return the name of the renderer executable. + """ + if self._renderer=="aqsis": + return "aqsis" + elif self._renderer=="pixie": + return "rndr" + elif self._renderer=="3delight": + return "renderdl" + elif self._renderer=="prman": + return "prman" + else: + raise ValueError("Unknown renderer: %s"%self._renderer) Added: cgkit/trunk/cgkit/jobqueue/jobhandle.py =================================================================== --- cgkit/trunk/cgkit/jobqueue/jobhandle.py (rev 0) +++ cgkit/trunk/cgkit/jobqueue/jobhandle.py 2009-08-20 20:56:55 UTC (rev 337) @@ -0,0 +1,367 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Python Computer Graphics Kit. +# +# The Initial Developer of the Original Code is Matthias Baas. +# Portions created by the Initial Developer are Copyright (C) 2009 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import os, os.path, glob + + +class JobHandle(object): + """Job object (queued). + + Objects of this class represent jobs that have already been queued. + + Every job has: + + - A type name + - An instance name + - A job description + - An associated directory + """ + + def __init__(self, location, rootLocation): + """Constructor. + + location is the path to the job directory (this may also point to a + link file). + rootLocation is the path to the job root directory (this is used + for resolving job relative relative links). + """ + object.__init__(self) + # The job location (if useSymLinks is False, this may also point to a link file) + self._location = str(location) + + # The job root directory + self._rootLocation = rootLocation + + # The resolved location. This is always a directory (or technically, it + # could be a non-existent path). If useSymLinks is True, this is + # always the same as the above location. + self._realLocation = self._resolveTxtLinks(self._location) + + + def _resolveTxtLinks(self, location): + """Resolve a path so that it points to a directory and not to a link file. + + If location already points to a directory, it is returned unmodified. + If it points to a link file, the file is followed and the process + repeats until a directory is encountered or the targegt does not exist. + """ + visited = {} + while os.path.exists(location) and os.path.isfile(location): + visited[location] = True + location = self._getTxtLinkTarget(location) + if location in visited: + raise ValueError("Cyclic directory links detected") + return location + + def _getTxtLinkTarget(self, linkFileName): + """Return the target that a link file points to. + """ + f = open(linkFileName, "rt") + # Read the first line which must be "[link]" + s = f.readline() + if s!="[link]\n": + raise ValueError("%s is not a link file"%linkFileName) + # Read the line containing the target + targetLine = f.readline() + f.close() + # Check the target line + if not targetLine.startswith("target="): + raise ValueError("Error in link file %s"%linkFileName) + target = targetLine[7:].strip() + # The link must always be relative to the job root + if not target.startswith("$JOBROOT"): + raise ValueError("Invalid link target: %s"%target) + target = self._rootLocation+target[8:] + return target + + def __str__(self): + if self.isFinished(): + state = "finished" + elif self.isRunning(): + state = "running" + elif self.isWaiting(): + state = "waiting" + else: + state = "?" + + res = "%s (%s)"%(os.path.basename(self._location), state) + return res + + @property + def location(self): + """Return the job directory. + """ + return self._location + + @property + def label(self): + """Return a short job label to quickly identify the job. + """ + try: + try: + f = open(self.labelFile, "rt") + label = f.read().strip() + finally: + f.close() + except: + label = "" + + if label=="": + label = "Job %s"%self.number + + return label + + @property + def number(self): + """Return the job number. + """ + nrStr = os.path.basename(self._location)[3:] + try: + return int(nrStr) + except: + return -1 + + @property + def submitTime(self): + """Return the submission time (in seconds). + + May return None if the job doesn't exist or is broken. + """ + try: + s = os.stat(self.procDefFile) + return s.st_mtime + except OSError: + return None + + @property + def startTime(self): + """Return the time the job was started (in seconds). + + Returns None if the job hasn't been started yet (or there is a problem + with the job directory). + """ + try: + s = os.stat(self.pidFile) + return s.st_mtime + except OSError: + return None + + @property + def endTime(self): + """Return the time the job was finished (in seconds). + + Returns None if the job hasn't been finished yet (or there is a problem + with the job directory). + """ + try: + s = os.stat(self.finishedDir) + return s.st_mtime + except OSError: + return None + + @property + def progress(self): + """Return the progress percentage value as an int. + + Returns None if the value couldn't be obtained for some reason. + """ + try: + progressFile = self.progressFile + if os.path.exists(progressFile): + res = os.path.getsize(self.progressFile) + if res>100: + res = 100 + else: + res = 0 + return res + except: + return None + + @property + def statusLine(self): + """Return a string containing the current status line for the GUI. + + The status line indicates what a running job is currently doing. + """ + try: + try: + f = open(self.statusLineFile, "rt") + line = f.read().strip() + finally: + f.close() + except: + line = "" + + return line + + + @property + def finishedDir(self): + return os.path.join(self._realLocation, ".finished") + + @property + def runningDir(self): + return os.path.join(self._realLocation, ".running") + + @property + def procDefFile(self): + return os.path.join(self._realLocation, ".proc_def") + + @property + def labelFile(self): + """Return the location of the file that contains the job label. + """ + return os.path.join(self._realLocation, ".label") + + @property + def pidFile(self): + """Return the location of the PID file. + + This file contains the PID of the process that is/was running the job. + """ + return os.path.join(self.runningDir, ".pid") + + @property + def hostFile(self): + """Return the location of the host file. + + The host file contains the name of the host that is/was running the job. + """ + return os.path.join(self.runningDir, ".host") + + @property + def progressFile(self): + """Return the location of the progress indicator file. + """ + return os.path.join(self.runningDir, ".progress") + + @property + def statusLineFile(self): + """Return the location of the status line file. + """ + return os.path.join(self.runningDir, ".statusline") + + @property + def errorMarkerFile(self): + """Return the location of the error marker file. + + The presence of this file indicates that running the job resulted + in an error. + """ + return os.path.join(self.runningDir, ".error_marker") + + @property + def procTracebackFile(self): + """Return the location of the file that contains the job procedure traceback. + """ + return os.path.join(self.runningDir, ".proc_traceback") + + @property + def stdoutFile(self): + """Return the location of the file that contains stdout. + """ + return os.path.join(self.runningDir, ".stdout") + + @property + def stderrFile(self): + """Return the location of the file that contains stderr. + """ + return os.path.join(self.runningDir, ".stderr") + + def isWaiting(self): + """Check if this job is currently in 'waiting' state. + """ + if os.path.exists(self.finishedDir): + return False + if os.path.exists(self.runningDir): + return False + return os.path.exists(self.procDefFile) + + def isRunning(self): + """Check if this job is currently in 'running' state. + """ + if self.isFinished(): + return False + else: + return os.path.exists(self.runningDir) + + def isFinished(self): + """Check if this job is currently in 'finished' state. + """ + return os.path.exists(self.finishedDir) + + def listSubJobs(self): + """Return a list of sub-jobs. + + The return value is a list of JobHandle objects. + """ + subDirs = glob.glob(os.path.join(self._realLocation, "job*")) + #subDirs = filter(lambda p: os.path.isdir(p), subDirs) + jobs = map(lambda jobDir: (jobDir, int(os.path.basename(jobDir)[3:])), subDirs) + jobs.sort(key=lambda a: a[1]) + return [JobHandle(jobDir, self._rootLocation) for jobDir,nr in jobs] + + def hasError(self, recursive=False): + """Check if this job produced an error. + + If recursive is False, only this job is considered, not the children + jobs. In this case, the return value is only meaningful when the job + is in finished state. + If recursive is True, the result will be True if any job in the entire + sub-hierarchy has an error. This can be an expensive operation because + all sub-directories have to be checked. + """ + err = os.path.exists(self.errorMarkerFile) + if err: + return True + + if recursive: + subJobs = self.listSubJobs() + for j in subJobs: + if j.hasError(recursive=True): + return True + + return False + + def setError(self): + """Set the error flag. + + This method may only be called by the process that is currently + running the job (the .running directory must exist). + """ + errFile = self.errorMarkerFile + if not os.path.exists(errFile): + f = open(errFile, "wb") + f.close() Added: cgkit/trunk/cgkit/jobqueue/jobproc.py =================================================================== --- cgkit/trunk/cgkit/jobqueue/jobproc.py (rev 0) +++ cgkit/trunk/cgkit/jobqueue/jobproc.py 2009-08-20 20:56:55 UTC (rev 337) @@ -0,0 +1,273 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Python Computer Graphics Kit. +# +# The Initial Developer of the Original Code is Matthias Baas. +# Portions created by the Initial Developer are Copyright (C) 2009 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import os.path, threading, subprocess + + +class _StreamPipe(threading.Thread): + """Read from a pipe (stdout/stderr) and write the data into a file. + + This is used to read the stdout/stderr streams from a process launched + via the subprocess module. + """ + + def __init__(self, pipe, outputFile, lineCallback=None, errStream=None, startLineNr=1, name=None): + """Constructor. + + pipe is the stdout or stdin pipe of the launched process. + outputFile is an open file object that will receive the data that + is read from pipe. + lineCallback is a callable (or None) that gets called for every + line read from pipe. The callback is called with two arguments, + the line number of the current line and the line itself. + errStream is a file-like object that receives error messages. If None + is passed then sys.stderr is used. + name is the name of the thread. + """ + threading.Thread.__init__(self, name=name) + self.setDaemon(True) + self._pipe = pipe + self._outputFile = outputFile + self._lineCallback = lineCallback + if errStream is None: + errStream = sys.stderr + self._errStream = errStream + self._startLineNr = startLineNr-1 + self.numLines = None + + def run(self): + lines = self._startLineNr + while 1: + s = self._pipe.readline() + if s=="": + self.numLines = lines + return + if self._outputFile is not None: + try: + self._outputFile.write(s) + except: + traceback.print_exc(file=self._errStream) + self._outputFile = None + lines += 1 + # Call the line callback function + if self._lineCallback is not None: + try: + self._lineCallback(lines, s) + except: + traceback.print_exc(file=self._errStream) + self._lineCallback = None + + +class JobProc(object): + """Job procedure base class. + """ + + def __init__(self, label=None): + """Constructor. + + Note that for one particular job, the job procedure may get created + several times (e.g. just to validate parameters). + """ + object.__init__(self) + # As a convenience for the user this is set outside the class + # (in Job._initJobDir()) instead of postCreate()) + self._jobDir = None + self._jobHandle = None + if label is not None: + label = str(label) + self.label = label + self.stdout = None + self.stderr = None + + def run(self): + """Do whatever this job has to do. + + When this method is run, the current directory is set to the + corresponding :file:`.running` directory. + This method has to be implemented in a derived class. The base + method does nothing. + + The method can use the :meth:`execCmd()<JobProc.execCmd>` method to run + command line applications. It can call :meth:`setStatusLine()<JobProc.setStatusLine>` + and :meth:`setProgress()<JobProc.setProgress>` to report status + information to the user and it can call :meth:`setError()<JobProc.setError>` + when the job has failed to run successfully. + """ + pass + + def postCreate(self, jobDir): + """This method is called once when the job is created. + + *jobDir* is the directory in which the job will be run (the attribute + :attr:`runningDir` will not yet be initialised). + + This method can be implemented in a derived class. The base method + does nothing. + + Note: The method is not executed when the job is actually run, so you + can use it to do initializations that must only be run exactly once. + """ + pass + + def execCmd(self, cmd): + """Execute a command line string. + + *cmds* is a string containing a command that will be executed. + stdout/stderr is captured into the job's stdout/stderr stream. + The return value is the return code that was returned by the command. + + Derived classes can implement :meth:`stdoutCallback()<JobProc.stdoutCallback>` + and :meth:`stderrCallback()<JobProc.stderrCallback>` which get called + for every line. + """ + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + stdoutReader = _StreamPipe(proc.stdout, self.stdout, lineCallback=self.stdoutCallback, errStream=self.stderr, name="StdOutReader") + stderrReader = _StreamPipe(proc.stderr, self.stderr, lineCallback=self.stderrCallback, errStream=self.stderr, name="StdErrReader") + stdoutReader.start() + stderrReader.start() + ret = proc.wait() + return ret + + def stdoutCallback(self, lineNr, line): + """This method gets called for every line written to stdout. + + When :meth:`execCmd()<JobProc.execCmd>` is used to run a command + line application, then this callback is called for every line written + by the application to stdout. + + *lineNr* is the line number and *line* the actual contents of the line. + + Derived classes may implement this method to scan for errors, status + messages or progress reports. The default implementation does nothing. + """ + pass + + def stderrCallback(self, lineNr, line): + """This method gets called for every line written to stderr. + + When :meth:`execCmd()<JobProc.execCmd>` is used to run a command + line application, then this callback is called for every line written + by the application to stderr. + + *lineNr* is the line number and *line* the actual contents of the line. + + Derived classes may implement this method to scan for errors, status + messages or progress reports. The default implementation does nothing. + """ + pass + + def setProgress(self, value): + """Set the progress percentage value. + + *value* is a number between 0 and 100 (it is clamped) which indicates + the progress the job has made. + """ + value = int(value) + # Clamp between 0 and 100 + value = max(0, min(value, 100)) + progressFile = self._jobHandle.progressFile + if os.path.exists(progressFile): + size = os.path.getsize(progressFile) + if size<value: + n = value-size + else: + n = 0 + else: + n = value + + if n>0: + try: + f = open(progressFile, "ab") + except: + print >>sys.stderr, "Failed to update progress file "%progressFile + return + try: + f.write(n*"*") + finally: + f.close() + + def setStatusLine(self, s): + """Set a status line string. + + *s* is a string with a short message that reflects the current status + of the job. + """ + s = str(s) + statusLineFile = self._jobHandle.statusLineFile + try: + f = open(statusLineFile, "wt") + except: + print >>sys.stderr, "Failed to update progress file "%progressFile + return + try: + f.write(s) + finally: + f.close() + + def setError(self): + """Set the job's error flag. + """ + self._jobHandle.setError() + + @property + def runningDir(self): + """The directory in which the job is run. + """ + return self._jobHandle.runningDir + + def _begin(self, jobHandle): + """This is called internally before run() is called. + + jobHandle is the JobHandle object that is associated with this job. + """ + self._jobHandle = jobHandle + self._jobDir = jobHandle.location + + # stdout (line-buffered) + self.stdout = open(self._jobHandle.stdoutFile, "wt", 1) + # stderr (unbuffered) + self.stderr = open(self._jobHandle.stderrFile, "wt", 0) + + def _end(self): + """This is called after run(). + + This is also called when either _begin() or run() raised an exception. + """ + if self.stdout is not None: + self.stdout.close() + self.stdout = None + if self.stderr is not None: + self.stderr.close() + self.stderr = None Added: cgkit/trunk/cgkit/jobqueue/jobqueue.py =================================================================== --- cgkit/trunk/cgkit/jobqueue/jobqueue.py (rev 0) +++ cgkit/trunk/cgkit/jobqueue/jobqueue.py 2009-08-20 20:56:55 UTC (rev 337) @@ -0,0 +1,1032 @@ +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Python Computer Graphics Kit. +# +# The Initial Developer of the Original Code is Matthias Baas. +# Portions created by the Initial Developer are Copyright (C) 2009 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import sys, os, os.path, glob, inspect, socket +import random, traceback, shutil +import xml.dom.minidom +import ConfigParser as configparser +from jobproc import JobProc +from jobhandle import JobHandle + +class JobQueueError(Exception): + pass + + +def _convertPyValueToDOM(val, doc): + """Convert a Python value into a DOM element. + + val can be a bool, int, float, str, tuple, list, dict or None. + This is used for writing values into a job definition file. + """ + t = type(val) + typeName = t.__name__ + el = doc.createElement(typeName) + # None? + if val is None: + pass + # Basic type? + elif t in [bool, int, float, str]: + el.setAttribute("value", str(val)) + # Sequence? + elif t in [tuple, list]: + for v in val: + cel = _convertPyValueToDOM(v, doc) + el.appendChild(cel) + # Dict + elif t==dict: + for key,v in val.items(): + keyEl = _convertPyValueToDOM(key, doc) + valEl = _convertPyValueToDOM(v, doc) + el.appendChild(keyEl) + el.appendChild(valEl) + else: + raise ValueError("Unsupport parameter type: %s"%typeName) + + return el + +def _convertDOMToPyValue(element): + """Convert a DOM element into a Python object. + + This is used to convert the parameters from a job definition file + into Python values. + """ + typeName = element.nodeName + + if typeName=="dict": + res = {} + keyValid = False + key = None + for childEl in element.childNodes: + if childEl.nodeType==childEl.ELEMENT_NODE: + v = _convertDOMToPyValue(childEl) + if keyValid: + res[key] = v + keyValid = False + else: + key = v + keyValid = True + if keyValid: + raise ValueError("Dict key without value") + elif typeName=="str": + res = str(element.getAttribute("value")) + elif typeName=="bool": + val = str(element.getAttribute("value")) + res = {"True":True, "False":False}.get(val) + if res is None: + raise ValueError("Invalid boolean value: %s"%val) + elif typeName=="int": + res = int(element.getAttribute("value")) + elif typeName=="float": + res = float(element.getAttribute("value")) + elif typeName in ["list", "tuple"]: + res = [] + for childEl in element.childNodes: + if childEl.nodeType==childEl.ELEMENT_NODE: + res.append(_convertDOMToPyValue(childEl)) + if typeName=="tuple": + res = tuple(res) + elif typeName=="NoneType": + res = None + else: + raise ValueError("Unknown parameter type: %s"%typeName) + return res + + +class _DummyLogger: + """Dummy logger object that does nothing. + """ + def debug(self, *params, **kwargs): + pass + + info = debug + warning = debug + error = debug + critical = debug + log = debug + exception = debug + + +class Job(object): + """Job object (during creation). + + Objects of this class represent jobs that are just being created. + + """ + def __init__(self, jobRoot, jobType, **params): + """Constructor. + + jobRoot is the JobRoot object that this job belongs to. + jobType is a string containing the name of the job class that should + be instantiated. + params are the job parameters which must be passed as keyword + arguments. + """ + if not isinstance(jobRoot, JobRoot): + raise TypeError("jobRoot must be a JobRoot object") + if not isinstance(jobType, basestring): + raise TypeError("Job type must be a string") + + # The root of the job + self._jobRoot = jobRoot + # The job proc class name + self._jobType = jobType + # The job proc parameters + self._params = params + # A list of Job object which this job depends on + self._dependencies = [] + + # This is used later on to detect cycles + self._marked = False + # The maximum distance to a leaf (job nodes without a dependency + # always have a value of 0). This is used to sort the dependencies + # with respect to the longest path in that sub-tree which is necessary + # to remove redundant dependencies. + self._maxLeafDist = 0 + # This is used later on to remove redundant dependencies + self._depNr = 0 + + # The job location on disk. + self._location = self._initJobDir(jobType, **params) + # Indicates whether the job directory is inside the repository or not + # (all jobs begin in the repository except for the root job) + self._isInsideRepository = True + + def createJob(self, jobType, **params): + """Create a new sub-job. + + *jobType* is the name of the job procedure that should be created. Any + additional keyword arguments are passed to the constructor of the job + procedure. + + Returns a :class:`Job<jobqueue.Job>` object that represents the newly created job. + + This method is equivalent to creating a job object manually and + calling ``addDependency(job)``. + """ + job = Job(self._jobRoot, jobType, **params) + self.addDependency(job) + return job + + def addDependency(self, job): + """Establish a dependency between another job. + + This job will only be run when all dependencies have been successfully + finished. + """ + if job is self: + raise ValueError("A job cannot depend on itself") + if job._jobRoot!=self._jobRoot: + raise ValueError("A job cannot depend on a job from another job hierarchy") + if isinstance(job, JobRoot): + raise ValueError("A job cannot depend on the job root") + + # Set the dependency + self._dependencies.append(job) + + # TODO: If self has a job dir, then create job dirs for all jobs + # in the job subtree that don't have a job dir yet + + def _initJobDir(self, jobType, **params): + """Create and initialize the job directory. + + jobType is the name of the job procedure that should be created. Any + additional keyword arguments are passed to the constructor of the job + procedure. + Returns the job directory. + """ + + if type(jobType) is not str: + raise TypeError("Job type must be a string") + + # Create an instance of the job procedure to validate the parameters and get the job label + jobProc = self._instantiateJobProc(jobType, **params) + + # Create the root job directory... + jobDir = self._createJobDir() + # This handle is only used for retrieving file locations (so the root is set to None) + jobHandle = JobHandle(jobDir, None) + + # Call the job procedure's postCreate method + jobProc.postCreate(jobDir) + jobProc._jobDir = jobDir + + # Write the label + label = jobProc.label + if label is not None: + f = open(jobHandle.labelFile, "wt") + f.write(label) + f.close() + + # Write the job procedure description. + # If this job node is the job root then write the file under a temporary + # name (so that the job won't get picked up yet) + procDefFile = jobHandle.procDefFile + if self is self._jobRoot: + procDefFile += "_tmp" + self._writeJobDef(procDefFile, jobType, params) + + return jobDir + + def _writeJobDef(self, fileName, jobType, params): + """Write the job definition XML file. + """ + impl = xml.dom.minidom.getDOMImplementation() + doc = impl.createDocument(None, "JobProcedure", None) + jobProc = doc.documentElement + # Set the job type + jobProc.setAttribute("type", jobType) + + # Set the job parameters + paramEl = doc.createElement("parameters") + jobProc.appendChild(paramEl) + paramDictEl = _convertPyValueToDOM(params, doc) + paramEl.appendChild(paramDictEl) + + # Write the XML file + f = open(fileName, "wb") + jobProc.writexml(f, addindent=" ", newl="\n") + f.close() + + jobProc.unlink() + + def _instantiateJobProc(self, jobType, **params): + """Create an instance of a job procedure. + """ + return self._jobRoot._instantiateJobProc(jobType, **params) + + def _createJobDir(self): + """Determine the name of the job directory and create it. + """ + return self._jobRoot._createSubJobDir() + + def _preprocessGraph(self): + """Preprocess the dependency graph. + + Sets the maxLeafDist value on the sub-jobs and checks for cycles. + + Raises a JobQueueError if the graph contains a cycle. + """ + # Have we visited this job already before in this particular path? -> Cycle + if self._marked: + raise JobQueueError("Job hierarchy is impossible to process because it contains a cycle") + + # Mark this job as visited (if we see it again while processing children, + # we have detected a cycle) + self._marked = True + + for subJob in self._dependencies: + # Descend the tree + subJob._preprocessGraph() + self._maxLeafDist = max(self._maxLeafDist, subJob._maxLeafDist+1) + + # Reset the cycle detection mark (as this sub-tree is done and does not contain a cycle) + self._marked = False + + def _writeDependencies(self): + """Write the dependency hierarchy. + + Creates the final job directories (or links) that make up the + dependency hierarchy. + """ + jobDir = self._location + + myDepNr = self._jobRoot._getNextDepNr() + self._depNr = myDepNr + + # Create a dependency list that has the current order associated with each dependency. + # This number will be used as local job number (so that the jobs are + # picked up in the same order as they were added). + deps = [x for x in enumerate(self._dependencies)] + # Sort according to descending maxLeafDist. + # This is done so that the longest paths are processed first and + # redundant dependencies (which always have a shorter path) can be removed. + deps.sort(key=lambda x: -x[1]._maxLeafDist) + + # Create the dependency graph job directories for the children + for jobNr,subJob in deps: + # Do we already have a (possibly indirect) dependency set up? + # Then there's nothing to do anymore + if subJob._depNr>=myDepNr: + continue + + dirName = os.path.join(jobDir, "job%d"%(jobNr)) + if not self._jobRoot._keepJobsInRepository and subJob._isInsideRepository: + # Move the directory from the repository into the dependency hierarchy + shutil.move(subJob._location, dirName) + subJob._location = dirName + subJob._isInsideRepository = False + else: + # Create a link to the actual job dir + self._jobRoot._linkJobDir(subJob._location, dirName) + subJob._setDepNr(myDepNr) + + subJob._writeDependencies() + + def _setDepNr(self, nr): + self._depNr = nr + for j in self._dependencies: + j._setDepNr(nr) + + +class JobRoot(Job): + """The root of a job hierarchy. + + In principle, this is a job like every other job as well, but because + it has a directory inside the main job directory it must remain the + root of the job hierarchy. + The root is also the only job directory that has a special job repository + directory that may store all the sub-jobs. + + This class is derived from the :class:`Job<jobqueue.Job>` class. + """ + + def __init__(self, jobQueue, jobType, **params): + """Constructor. + + jobQueue is the JobQueue object that this job is associated with. + jobType is a string containing the name of the job class that should + be instantiated. + params are the job parameters which must be passed as keyword + arguments. + """ + if not isinstance(jobQueue, JobQueue): + raise TypeError("jobQueue must be a JobQueue object") + self._jobQueue = jobQueue + # This counter is used to create the job directory names in the job repository dir + self._subJobCounter = 0 + # Initial value for the redundant dependency removal part + self._nextDepNr = 0 + # This flag determines whether the jobs will be kept in the job repository + # or moved out into the dependency hierarchy when _writeDependencies() + # is called on any job. + self._keepJobsInRepository = self._jobQueue.keepJobsInRepository + # Can we use sym links or do we have to emulate them? + self._useSymLinks = self._jobQueue.useSymLinks + + Job.__init__(self, self, jobType, **params) + + # Overwrite the isInsideRepository flag (as the root is never inside the repo) + self._isInsideRepository = False + + def activate(self): + """Activate the job so that it can be processed. + + This is the last step of a job submission. After calling this method, + the job hierarchy must not be changed anymore. + + Raises a :exc:`JobQueueError` if the graph contains a cycle. + """ + # Check for cycles, set the minimum job depth in the graph and sort + # the dependencies of each job according to the minimum depth + self._preprocessGraph() + + # Create the actual job* directories that make up the job hierarchy + self._writeDependencies() + + # Clean up the job repo dir if the jobs have been moved out. + if not self._keepJobsInRepository: + subJobRepo = self._getSubJobRepo() + if os.path.exists(subJobRepo): + # At this point, the directory must be empty, so a single rmdir is enough. + # (it's empty because every job (except for the root job) has at least + # one parent which means the job has been moved out of the repo and + # put under a parent) + os.rmdir(subJobRepo) + + jobHandle = JobHandle(self._location, self._location) + procDefFile = jobHandle.procDefFile + tmpProcDefFile = procDefFile+"_tmp" + if not os.path.exists(tmpProcDefFile): + raise JobQueueError("Failed to activate") + os.rename(tmpProcDefFile, procDefFile) + + def _linkJobDir(self, src, dst): + """Create a link at dst pointing to src. + """ + if not src.startswith(self._location): + raise ValueError("Cannot link directories outside the job root") + + if self._useSymLinks: + # Create a sym link + os.symlink(src, dst) + else: + # Create a file that contains the target path + f = open(dst, "wt") + f.write("[link]\ntarget=$JOBROOT%s\n"%src[len(self._location):]) + f.close() + + def _createSubJobDir(self): + """Create a new directory for a sub-job. + + Note: The sub-job may be anywhere in the final dependency graph. + """ + self._subJobCounter += 1 + # The directory where all sub-jobs are stored + subJobRepo = self._getSubJobRepo() + # Create the repo dir if it doesn't already exist + if not os.path.exists(subJobRepo): + os.mkdir(subJobRepo) + # Create the actual sub-job directory + subJobDir = os.path.join(self._location, ".jobs", "j%d"%self._subJobCounter) + os.mkdir(subJobDir) + return subJobDir + + def _getNextDepNr(self): + self._depNr += 1 + return self._depNr + + def _getSubJobRepo(self): + """Return the location of the job repository directory. + """ + return os.path.join(self._location, ".jobs") + + def _instantiateJobProc(self, jobType, **params): + """Create an instance of a job procedure. + """ + return self._jobQueue._instantiateJobProc(jobType, **params) + + def _createJobDir(self): + """Determine the name of the job directory and create it. + """ + # Create the root job directory... + for i in range(10): + jobDir = self._jobQueue._newJobDir() + try: + os.mkdir(jobDir) + break + except OSError: + pass + else: + raise JobQueueError("Failed to create a new job directory") + + return jobDir + + +class JobQueue: + """Job queue class. + + A job queue contains a list of jobs which can be run by one or more + processes. Each job may be composed of sub-jobs which all have to + be successfully completed before the job is allowed to be processed. + Jobs that are on the same level in the hierarchy are independent and + can be run concurrently. + + An individual job is represented by a job procedure (JobProc) object + that implements the actual functionality of a particular type of job. + A job is considered done when the run() method of a job procedure has + been executed successfully and the job procedure did not indicate an + error. + + On disk, the entire job queue is stored as a directory which contains + a job directory for each job in the queue. Sub-jobs are sub-directories + of the job directories. + """ + + def __init__(self, location, logger=None): + """Constructor. + + location is a string containing the directory where the job queue + is stored (the directory should already exist, it is not created). + It is also valid to pass None (which is equivalent to an empty + queue that cannot receive jobs). + + logger can be set to a logger object from the logging module (or + an object with the same interface) which will receive log message. + """ + # Verify the input type... [truncated message content] |
From: <mb...@us...> - 2009-08-15 12:02:17
|
Revision: 336 http://cgkit.svn.sourceforge.net/cgkit/?rev=336&view=rev Author: mbaas Date: 2009-08-15 12:02:09 +0000 (Sat, 15 Aug 2009) Log Message: ----------- Create the tmp dir if it doesn't exist. Modified Paths: -------------- cgkit/trunk/unittests/test_pointcloud.py Modified: cgkit/trunk/unittests/test_pointcloud.py =================================================================== --- cgkit/trunk/unittests/test_pointcloud.py 2009-08-15 11:33:06 UTC (rev 335) +++ cgkit/trunk/unittests/test_pointcloud.py 2009-08-15 12:02:09 UTC (rev 336) @@ -21,6 +21,10 @@ self.libName = "3delight" # self.libName = "prman" self.accuracy = 3 + + def setUp(self): + if not os.path.exists("tmp"): + os.mkdir("tmp") def testSinglePoints(self): """Test writing/reading individual points. This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-08-15 11:33:15
|
Revision: 335 http://cgkit.svn.sourceforge.net/cgkit/?rev=335&view=rev Author: mbaas Date: 2009-08-15 11:33:06 +0000 (Sat, 15 Aug 2009) Log Message: ----------- Deleted the Latex-based docs. Removed Paths: ------------- cgkit/trunk/doc/makefile cgkit/trunk/doc/tex/ Deleted: cgkit/trunk/doc/makefile =================================================================== --- cgkit/trunk/doc/makefile 2009-08-15 09:31:07 UTC (rev 334) +++ cgkit/trunk/doc/makefile 2009-08-15 11:33:06 UTC (rev 335) @@ -1,12 +0,0 @@ -# makefile - -html: - mkhowto --html --a4 tex/cgkit.tex - -pdf: - mkhowto --pdf --a4 tex/cgkit.tex - mv cgkit.pdf cgkit-a4.pdf - -pdf_letter: - mkhowto --pdf --letter tex/cgkit.tex - mv cgkit.pdf cgkit-letter.pdf This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-08-15 09:31:16
|
Revision: 334 http://cgkit.svn.sourceforge.net/cgkit/?rev=334&view=rev Author: mbaas Date: 2009-08-15 09:31:07 +0000 (Sat, 15 Aug 2009) Log Message: ----------- Changed doc strings to use rst markup so that they can be used for the Sphinx documentation. Modified Paths: -------------- cgkit/trunk/cgkit/glslangparams.py cgkit/trunk/cgkit/glslangtokenize.py cgkit/trunk/cgkit/pointcloud.py cgkit/trunk/cgkit/ri.py cgkit/trunk/cgkit/sloargs.py cgkit/trunk/cgkit/slparams.py cgkit/trunk/cgkit/sltokenize.py Modified: cgkit/trunk/cgkit/glslangparams.py =================================================================== --- cgkit/trunk/cgkit/glslangparams.py 2009-08-08 12:58:38 UTC (rev 333) +++ cgkit/trunk/cgkit/glslangparams.py 2009-08-15 09:31:07 UTC (rev 334) @@ -327,38 +327,38 @@ def glslangparams(shader=None, cpp=None, cpperrstream=sys.stderr): """Extracts the shader parameters from an OpenGL 2 shader source file. - The argument shader is either the name of the shader source file + The argument *shader* is either the name of the shader source file or a file-like object that provides the shader sources. - cpp determines how the shader source is preprocessed. It + *cpp* determines how the shader source is preprocessed. It can either be a string containing the name of an external - preprocessor tool (such as 'cpp') that must take the file name as + preprocessor tool (such as ``cpp``) that must take the file name as parameter and dump the preprocessed output to stdout or it can be - a callable that takes shader and cpperrstream as input and returns + a callable that takes *shader* and *cpperrstream* as input and returns the preprocessed sources as a string. If the external - preprocessor does not produce any data a PreprocessorNotFound + preprocessor does not produce any data a :exc:`PreprocessorNotFound` exception is thrown. The error stream of the preprocessor is written to the object - that is specified by cpperrstream which must have a write() - method. If cpperrstream is None, the error stream is ignored. + that is specified by *cpperrstream* which must have a :meth:`write()` + method. If *cpperrstream* is ``None``, the error stream is ignored. - If cpp is None a simple internal preprocessor based on the - simplecpp module is used. + If *cpp* is ``None`` a simple internal preprocessor based on the + :mod:`simplecpp` module is used. - The function returns three lists (uniform, attribute, varying) + The function returns three lists (*uniform*, *attribute*, *varying*) that contain the variables with the corresponding qualifier. - A uniform variable is a 5-tuple (type, identifier, arraysize, - structname, struct). arraysize is a string containing the + A uniform variable is a 5-tuple (*type*, *identifier*, *arraysize*, + *structname*, *struct*). *arraysize* is a string containing the expression for the length of the array (i.e. the value between - the square brackets). If the variable is no array, arraysize is None. - When the variable is a struct, type has the value 'struct'. In this - case, the struct is given in struct (which is itself a list of + the square brackets). If the variable is no array, *arraysize* is ``None``. + When the variable is a struct, *type* has the value ``'struct'``. In this + case, the struct is given in *struct* (which is itself a list of variables as 5-tuples). If the struct has a name, this name is - given in structname, otherwise structname is None. + given in *structname*, otherwise *structname* is ``None``. - An attribute variable is a 2-tuple (type, identifier) and a - varying variable is a 3-tuple (type, identifier, arraysize) where - arraysize is defined as in the uniform case. + An attribute variable is a 2-tuple (*type*, *identifier*) and a + varying variable is a 3-tuple (*type*, *identifier*, *arraysize*) where + *arraysize* is defined as in the uniform case. """ # Run the preprocessor on the input file... Modified: cgkit/trunk/cgkit/glslangtokenize.py =================================================================== --- cgkit/trunk/cgkit/glslangtokenize.py 2009-08-08 12:58:38 UTC (rev 333) +++ cgkit/trunk/cgkit/glslangtokenize.py 2009-08-15 09:31:07 UTC (rev 334) @@ -52,21 +52,35 @@ def tokenize(readline, tokeater): """Reads a Shading Language input stream and creates tokens. - The first parameter, readline, must be a callable object which - provides the same interface as the readline() method of built-in + The first parameter, *readline*, must be a callable object which + provides the same interface as the :meth:`readline()` method of built-in file objects. Each call to the function should return one line of input as a string. - The second parameter, tokeneater, must also be a callable object. + The second parameter, *tokeater*, must also be a callable object. It is called with six parameters: the token type, the token - string, a tuple (srow, scol) specifying the row and column where - the token begins in the source, a tuple (erow, ecol) giving the + string, a tuple (*srow*, *scol*) specifying the row and column where + the token begins in the source, a tuple (*erow*, *ecol*) giving the ending position of the token, the line on which the token was found and the filename of the current file. - By default the filename argument is an empty string. It will only + The token type can be one of + + * ``WHITESPACE``: This is a series of blanks and/or tabs. + * ``NAME``: A valid identifier name or keyword. + * ``NUMBER``: An integer or float. + * ``STRING``: A string enclosed in ``'"'``. + * ``NEWLINE``: A newline character. + * ``OPERATOR``: An operator such as ``'+', '-', '!', '==', '!='``, etc. + * ``CHARACTER``: A single character that doesn't fit anything else. + * ``TYPE``: A language type (``void``, ``bool``, ``float``, ``int``, ``vec2``, + ``vec3``, ``vec4``, etc.) + * ``QUALIFIER``: A type qualifier (``const``, ``attribute``, ``uniform``, + ``varying``, ``in``, ``out``, ``inout``) + + By default, the filename argument is an empty string. It will only be the actual filename if you provide a preprocessed file stream - as input (so you should first run cpp on any shader). The + as input (so you should first run ``cpp`` on any shader). The tokenizer actually expects preprocessed data as it doesn't handle comments. """ Modified: cgkit/trunk/cgkit/pointcloud.py =================================================================== --- cgkit/trunk/cgkit/pointcloud.py 2009-08-08 12:58:38 UTC (rev 333) +++ cgkit/trunk/cgkit/pointcloud.py 2009-08-15 09:31:07 UTC (rev 334) @@ -606,16 +606,19 @@ def open(fileName, mode="r", libName=None, **kwargs): """Open a point cloud file for reading or writing. - fileName is the name of the point cloud file. mode is either "r" - for reading a file or "w" for writing a new point cloud file. - libName is the library name that implements the point cloud API. - When mode is "w", the following additional keyword arguments must + *fileName* is the name of the point cloud file. *mode* is either ``"r"`` + for reading a file or ``"w"`` for writing a new point cloud file. + *libName* is the library name that implements the point cloud API. + When mode is ``"w"``, the following additional keyword arguments must be present: - - vars: A list of tuples (type, name) that defines what additional variables to write - - world2eye: The world2eye matrix - - world2ndc: The world2ndc matrix - - format: A tuple (xres, yres, aspect) + - ``vars``: A list of tuples (*type*, *name*) that defines what additional variables to write + - ``world2eye``: The world2eye matrix + - ``world2ndc``: The world2ndc matrix + - ``format``: A tuple (*xres*, *yres*, *aspect*) + + Depending on the mode, the function either returns a :class:`PtcReader` or + :class:`PtcWriter` object. """ if mode=="r": return PtcReader(fileName, libName, **kwargs) Modified: cgkit/trunk/cgkit/ri.py =================================================================== --- cgkit/trunk/cgkit/ri.py 2009-08-08 12:58:38 UTC (rev 333) +++ cgkit/trunk/cgkit/ri.py 2009-08-15 09:31:07 UTC (rev 334) @@ -48,7 +48,7 @@ Interface specification. However, it also supports some newer features such as string handles for light sources or object instances. -It is safe to import the module using +It is safe to import the module using:: from cgkit.ri import * @@ -57,7 +57,7 @@ naming conflict. After importing the module this way you can use the functions just as -you are used to from the C API (well, almost). +you are used to from the C API:: from cgkit.ri import * Modified: cgkit/trunk/cgkit/sloargs.py =================================================================== --- cgkit/trunk/cgkit/sloargs.py 2009-08-08 12:58:38 UTC (rev 333) +++ cgkit/trunk/cgkit/sloargs.py 2009-08-15 09:31:07 UTC (rev 334) @@ -518,10 +518,10 @@ _sloLibNames = {} def registerSloArgs(sloSuffix, sloArgsCls): - """Register a SloArgs class for a particular renderer. + """Register a :class:`SloArgs` class for a particular renderer. - sloSuffix is the suffix (without dot) that is used for the compiled - shaders of this renderer. sloArgsCls is the SloArgs class that + *sloSuffix* is the suffix (without dot) that is used for the compiled + shaders of this renderer. *sloArgsCls* is the :class:`SloArgs` class that can read compiled shaders for this renderer. """ global _sloArgsClasses @@ -532,9 +532,15 @@ def getSloLib(sloSuffix): """Return the library name that manages a particular type of compiled shaders. - sloSuffix is the suffix of the compiled shaders. + *sloSuffix* is the suffix of the compiled shader. The return value is the library name that is used to read the parameters from compiled shaders of the given suffix. + + When called before a shader was parsed, the return value is the + library name that will be used when the library is loaded. This value can be + set by calling :func:`setSloLib()`. When the function is called after a + shader was already parsed, the return value is the absolute path to the + actual library that is used to read parameters. """ global _sloArgsClasses, _sloArgsInstances @@ -549,10 +555,10 @@ def setSloLib(sloSuffix, libName): """Set the library name that should be used for reading shader parameters. - Shaders with the given suffix (case-insensitive) will be handled by - the given library. This function has no effect if a shader of the given + Shaders with the suffix *sloSuffix* (case-insensitive) will be handled by + the library *libName*. This function has no effect if a shader of the given suffix has already been read. You must call this function at the beginning - of your application when slparams() hasn't been called yet. + of your application when :func:`slparams()` hasn't been called yet. """ global _sloLibNames @@ -561,7 +567,7 @@ def slparams(shader): """Read shader parameters. - See slparams.slparams() for more details. + See :func:`slparams.slparams()<cgkit.slparams.slparams>` for more details. """ global _sloArgsClasses, _sloArgsInstances, _sloLibNames Modified: cgkit/trunk/cgkit/slparams.py =================================================================== --- cgkit/trunk/cgkit/slparams.py 2009-08-08 12:58:38 UTC (rev 333) +++ cgkit/trunk/cgkit/slparams.py 2009-08-15 09:31:07 UTC (rev 334) @@ -228,59 +228,89 @@ def slparams(slfile=None, cpp=None, cpperrstream=sys.stderr, slname=None, includedirs=None, defines=None): """Extracts the shader parameters from a RenderMan Shader file. - The argument slfile is either the name of a compiled shader, the name of - the shader source file (*.sl) or a file-like object that provides the - shader sources. cpp determines how the shader source is preprocessed. + The argument *slfile* is either the name of a compiled shader, the name of + the shader source file (``*.sl``) or a file-like object that provides the + shader sources. + + *cpp* determines how the shader source is preprocessed. It can either be a string containing the name of an external - preprocessor tool (such as 'cpp') that must take the file name as + preprocessor tool (such as ``cpp``) that must take the file name as parameter and dump the preprocessed output to stdout or it can be - a callable that takes slfile and cpperrstream as input and returns + a callable that takes *slfile* and *cpperrstream* as input and returns the preprocessed sources as a string. If the external - preprocessor does not produce any data a PreprocessorNotFound + preprocessor does not produce any data a :exc:`PreprocessorNotFound` exception is thrown. The error stream of the preprocessor is written to the object - that's specified by cpperrstream which must have a write() - method. If cpperrstream is None, the error stream is ignored. + specified by *cpperrstream* which must have a :meth:`write()` + method. If *cpperrstream* is ``None``, the error stream is ignored. - If cpp is None a simple internal preprocessor based on the - simplecpp module is used. - The slname argument is an alias for slfile, it's only available + If *cpp* is ``None`` a simple internal preprocessor based on the + :mod:`simplecpp` module is used. + The *slname* argument is an alias for *slfile*, it is only available for backwards compatibility. - includedirs is a list of strings that contain directories where to - look for include files. defines is a list of tuples (name, value) + *includedirs* is a list of strings that contain directories where to + look for include files. *defines* is a list of tuples (*name*, *value*) that specify the predefined symbols to use. The function returns a list of shader info objects. These objects have - 4 attributes: + four attributes: - - type: The type of the shader (surface, displacement, etc.) - - name: The name of the shader - - params: The shader parameters (see below) - - meta: The shader meta data. How exactly meta data is specified depends - on the renderer you are using. + - ``type``: The type of the shader (surface, displacement, etc.) + - ``name``: The name of the shader + - ``params``: The shader parameters (see below) + - ``meta``: The shader meta data. How exactly meta data is specified depends + on the renderer you are using. The parameters are given as a list of shader parameter objects describing each parameter. A shader parameter object has the following attributes: - - outputSpec: The output specifier (either "output" or an empty string) - - storage: The storage class ("uniform" or "varying") - - type: The parameter type - - size: The array length or None if the parameter is not an array - - name: The name of the parameter - - space: The space in which a point-like type was defined - - default: The "raw" default value. If the input was a shader source file, + - ``outputSpec``: The output specifier (either ``"output"`` or an empty string) + - ``storage``: The storage class (``"uniform"`` or ``"varying"``) + - ``type``: The parameter type + - ``size``: The array length or ``None`` if the parameter is not an array + - ``name``: The name of the parameter + - ``space``: The space in which a point-like type was defined + - ``default``: The "raw" default value. If the input was a shader source file, this will always be a string containing an expression. If the input was a compiled shader this will already be an appropriate Python value. - You should never use this value directly, but always use convertdefault() + You should never use this value directly, but always use :func:`convertdefault()` to obtain a value which can be further processed. This way, your code will work for both, compiled shaders and shader source files. For backwards compatibility, the shader info object behaves like a - 3-tuple (type, name, params). The meta data can only be accessed via name - though. The shader parameter objects can also be used like 7-tuples + 3-tuple (*type*, *name*, *params*). The meta data can only be accessed via + name though. The shader parameter objects can also be used like 7-tuples containing the above data (in the order given above). + + Example (output slightly reformatted for better readability):: + + >>> from cgkit import slparams + >>> shaders = lparams.slparams("plastic.sl") + >>> print shaders + [('surface', 'plastic', + [('', 'uniform', 'float', None, 'Ka', None, '1'), + ('', 'uniform', 'float', None, 'Kd', None, '0.5'), + ('', 'uniform', 'float', None, 'Ks', None, '0.5'), + ('', 'uniform', 'float', None, 'roughness', None, '0.1'), + ('', 'uniform', 'color', None, 'specularcolor', 'rgb', '1')])] + >>> shaders[0].type + 'surface' + >>> shaders[0].name + 'plastic' + >>> for param in shaders[0].params: print param.name + ... + Ka + Kd + Ks + roughness + specularcolor + >>> shaders[0].meta + {} + + The parser used inside this function was generated using the parser generator + `Yapps <http://theory.stanford.edu/~amitp/Yapps/>`_ by Amit Patel. """ if slname is not None: @@ -433,22 +463,33 @@ def convertdefault(paramtuple): """Converts the default value of a shader parameter into a Python type. - paramtuple must be a 7-tuple (or parameter object) as returned by slparams(). + *paramtuple* must be a 7-tuple (or parameter object) as returned by + :func:`slparams()`. The function returns a Python object that corresponds to the default value of the parameter. If the default value can't be converted - then None is returned. Only the functions that are present in the - sl module are evaluated. If a default value calls a user defined - function then None is returned. + then ``None`` is returned. Only the functions that are present in the + :mod:`sl<cgkit.sl>` module are evaluated. If a default value calls a user defined + function then ``None`` is returned. The SL types will be converted into the following Python types: - - float -> float - - string -> string - - color -> vec3 - - point -> vec3 - - vector -> vec3 - - normal -> vec3 - - matrix -> mat4 + +------------+-------------+ + | SL type | Python type | + +============+=============+ + | ``float`` | ``float`` | + +------------+-------------+ + | ``string`` | ``string`` | + +------------+-------------+ + | ``color`` | ``vec3`` | + +------------+-------------+ + | ``point`` | ``vec3`` | + +------------+-------------+ + | ``vector`` | ``vec3`` | + +------------+-------------+ + | ``normal`` | ``vec3`` | + +------------+-------------+ + | ``matrix`` | ``mat4`` | + +------------+-------------+ Arrays will be converted into lists of the corresponding type. """ Modified: cgkit/trunk/cgkit/sltokenize.py =================================================================== --- cgkit/trunk/cgkit/sltokenize.py 2009-08-08 12:58:38 UTC (rev 333) +++ cgkit/trunk/cgkit/sltokenize.py 2009-08-15 09:31:07 UTC (rev 334) @@ -51,21 +51,32 @@ def tokenize(readline, tokeater): """Reads a Shading Language input stream and creates tokens. - The first parameter, readline, must be a callable object which - provides the same interface as the readline() method of built-in + The first parameter, *readline*, must be a callable object which + provides the same interface as the :meth:`readline()` method of built-in file objects. Each call to the function should return one line of input as a string. - The second parameter, tokeneater, must also be a callable object. + The second parameter, *tokeater*, must also be a callable object. It is called with six parameters: the token type, the token - string, a tuple (srow, scol) specifying the row and column where - the token begins in the source, a tuple (erow, ecol) giving the + string, a tuple (*srow*, *scol*) specifying the row and column where + the token begins in the source, a tuple (*erow*, *ecol*) giving the ending position of the token, the line on which the token was found and the filename of the current file. - By default the filename argument is an empty string. It will only + The token type can be one of: + + * ``WHITESPACE``: This is a series of blanks and/or tabs + * ``NAME``: A valid identifier name or keyword + * ``NUMBER``: An integer or float + * ``STRING``: A string enclosed in ``'"'``. + * ``NEWLINE``: A newline character. + * ``OPERATOR``: An operator such as ``'+', '-', '!', '==', '!='``, etc. + * ``CHARACTER``: A single character that doesn't fit anything else. + * ``TYPE``: A Shading Language type (float, point, vector, normal, matrix, color) + + By default, the filename argument is an empty string. It will only be the actual filename if you provide a preprocessed file stream - as input (so you should first run cpp on any shader). The + as input (so you should first run ``cpp`` on any shader). The tokenizer actually expects preprocessed data as it doesn't handle comments. """ This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-08-08 12:58:46
|
Revision: 333 http://cgkit.svn.sourceforge.net/cgkit/?rev=333&view=rev Author: mbaas Date: 2009-08-08 12:58:38 +0000 (Sat, 08 Aug 2009) Log Message: ----------- Normalize the path passed into the glob function (so that forward slashes are turned into backward slashes on Windows). Modified Paths: -------------- cgkit/trunk/cgkit/sequence.py Modified: cgkit/trunk/cgkit/sequence.py =================================================================== --- cgkit/trunk/cgkit/sequence.py 2009-08-08 12:57:06 UTC (rev 332) +++ cgkit/trunk/cgkit/sequence.py 2009-08-08 12:58:38 UTC (rev 333) @@ -2234,6 +2234,7 @@ Returns a list of :class:`Sequence<cgkit.sequence.Sequence>` objects. The sequences and the files within the sequences are sorted. """ + name = os.path.normpath(name) globpattern = name if not globpattern.endswith("*"): globpattern += "*" This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-08-08 12:57:17
|
Revision: 332 http://cgkit.svn.sourceforge.net/cgkit/?rev=332&view=rev Author: mbaas Date: 2009-08-08 12:57:06 +0000 (Sat, 08 Aug 2009) Log Message: ----------- Explicitly call python when testing the sequence utils. Modified Paths: -------------- cgkit/trunk/unittests/test_sequence.py Modified: cgkit/trunk/unittests/test_sequence.py =================================================================== --- cgkit/trunk/unittests/test_sequence.py 2009-08-08 11:33:54 UTC (rev 331) +++ cgkit/trunk/unittests/test_sequence.py 2009-08-08 12:57:06 UTC (rev 332) @@ -607,14 +607,14 @@ def testSeqCp1(self): """Test basic seqcp""" self.createSequence("tmp/spam#.txt", Range("1-5")) - os.system("../utilities/seqcp.py tmp/spam tmp/foo") + os.system("python ../utilities/seqcp.py tmp/spam tmp/foo") self.assertFiles("tmp/foo#.txt", "spam#.txt", Range("1-5")) self.assertFiles("tmp/spam#.txt", "spam#.txt", Range("1-5")) def testSeqCp2(self): """Test seqcp -s""" self.createSequence("tmp/spam#.txt", Range("1-5")) - os.system("../utilities/seqcp.py tmp/spam tmp/foo -s2-3") + os.system("python ../utilities/seqcp.py tmp/spam tmp/foo -s2-3") self.assertFiles("tmp/foo#.txt", "spam#.txt", Range("2-3")) if os.path.exists("tmp/foo0001.txt"): self.fail("foo0001.txt exists!") @@ -625,14 +625,14 @@ def testSeqCp3(self): """Test seqcp of discontinuous sequence""" self.createSequence("tmp/spam#.txt", Range("1-2,5")) - os.system("../utilities/seqcp.py tmp/spam tmp/foo") + os.system("python ../utilities/seqcp.py tmp/spam tmp/foo") self.assertFiles("tmp/foo#.txt", "spam#.txt", Range("1,2,5")) self.assertFiles("tmp/spam#.txt", "spam#.txt", Range("1,2,5")) def testSeqCp4(self): """Test seqcp -d""" self.createSequence("tmp/spam#.txt", Range("1-2,5")) - os.system("../utilities/seqcp.py tmp/spam tmp/foo -d2-") + os.system("python ../utilities/seqcp.py tmp/spam tmp/foo -d2-") self.assertFiles("tmp/foo#.txt", "spam#.txt", Range("2-4"), Range("1,2,5")) self.assertFiles("tmp/spam#.txt", "spam#.txt", Range("1,2,5")) @@ -640,13 +640,13 @@ def testSeqMv1(self): """Test basic seqmv""" self.createSequence("tmp/spam#.txt", Range("1-5")) - os.system("../utilities/seqmv.py tmp/spam tmp/foo") + os.system("python ../utilities/seqmv.py tmp/spam tmp/foo") self.assertFiles("tmp/foo#.txt", "spam#.txt", Range("1-5")) def testSeqMv2(self): """Test seqmv -s""" self.createSequence("tmp/spam#.txt", Range("1-5")) - os.system("../utilities/seqmv.py tmp/spam tmp/foo -s2-3") + os.system("python ../utilities/seqmv.py tmp/spam tmp/foo -s2-3") self.assertFiles("tmp/foo#.txt", "spam#.txt", Range("2-3")) if os.path.exists("tmp/foo0001.txt"): self.fail("foo0001.txt exists!") @@ -656,37 +656,37 @@ def testSeqMv3(self): """Test seqmv of discontinuous sequence""" self.createSequence("tmp/spam#.txt", Range("1-2,5")) - os.system("../utilities/seqmv.py tmp/spam tmp/foo") + os.system("python ../utilities/seqmv.py tmp/spam tmp/foo") self.assertFiles("tmp/foo#.txt", "spam#.txt", Range("1,2,5")) def testSeqMv4(self): """Test seqmv -d""" self.createSequence("tmp/spam#.txt", Range("1-2,5")) - os.system("../utilities/seqmv.py tmp/spam tmp/foo -d2-") + os.system("python ../utilities/seqmv.py tmp/spam tmp/foo -d2-") self.assertFiles("tmp/foo#.txt", "spam#.txt", Range("2-4"), Range("1,2,5")) def testSeqMv5(self): """Test seqmv of overlapping src/dst ranges (move backward)""" self.createSequence("tmp/spam#.txt", Range("2-8")) - os.system("../utilities/seqmv.py tmp/spam tmp/spam -d1-") + os.system("python ../utilities/seqmv.py tmp/spam tmp/spam -d1-") self.assertFiles("tmp/spam#.txt", "spam#.txt", Range("1-7"), Range("2-8")) def testSeqMv6(self): """Test seqmv of overlapping src/dst ranges (move forward)""" self.createSequence("tmp/spam#.txt", Range("2-8")) - os.system("../utilities/seqmv.py tmp/spam tmp/spam -d3-") + os.system("python ../utilities/seqmv.py tmp/spam tmp/spam -d3-") self.assertFiles("tmp/spam#.txt", "spam#.txt", Range("3-9"), Range("2-8")) def testSeqMv7(self): """Test seqmv of overlapping src/dst ranges (needs tmp sequence)""" self.createSequence("tmp/spam#.txt", Range("2-10x2")) - os.system("../utilities/seqmv.py tmp/spam tmp/spam -d4-") + os.system("python ../utilities/seqmv.py tmp/spam tmp/spam -d4-") self.assertFiles("tmp/spam#.txt", "spam#.txt", Range("4-8"), Range("2-10x2")) def testSeqMv8(self): """Test the -e option (to change the extension).""" self.createSequence("tmp/spam#.ext", Range("1-3")) - os.system('../utilities/seqmv.py "tmp/spam*.ext" tmp/foo@@.txt -e') + os.system('python ../utilities/seqmv.py "tmp/spam*.ext" tmp/foo@@.txt -e') self.assertFiles("tmp/foo@@.txt", "spam#.ext", Range("1-3")) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-08-08 11:34:02
|
Revision: 331 http://cgkit.svn.sourceforge.net/cgkit/?rev=331&view=rev Author: mbaas Date: 2009-08-08 11:33:54 +0000 (Sat, 08 Aug 2009) Log Message: ----------- Better match when reading a camera from a Maya file. Modified Paths: -------------- cgkit/trunk/cgkit/maimport.py Modified: cgkit/trunk/cgkit/maimport.py =================================================================== --- cgkit/trunk/cgkit/maimport.py 2009-08-08 09:09:38 UTC (rev 330) +++ cgkit/trunk/cgkit/maimport.py 2009-08-08 11:33:54 UTC (rev 331) @@ -535,12 +535,25 @@ fl = node.getAttrValue("focalLength", "fl", "float", 1, 35.0) res["focallength"] = fl cap = node.getAttrValue("cameraAperture", "cap", "double2", 1, (1.41732, 0.94488)) + filmFit = node.getAttrValue("filmFit", "ff", "int", 1, 1) - fov = degrees(2*atan((cap[0]*25.4)/(2*fl))) - # Convert the FOV from horizontal to vertical direction - # (Todo: What aspect ratio to use?) - fov = degrees(atan(480/640.0*tan(radians(fov/2.0))))*2.0 - res["fov"] = fov + xfov = degrees(2*atan((cap[0]*25.4)/(2*fl))) + yfov = degrees(2*atan((cap[1]*25.4)/(2*fl))) + + # Horizontal + if filmFit==1: + # Convert the FOV from horizontal to vertical direction + # (Todo: What aspect ratio to use?) + yfov = degrees(atan(480/640.0*tan(radians(xfov/2.0))))*2.0 + yfov = xfov + # Vertical + elif filmFit==2: + pass + # Fill and overscan not supported yet + else: + pass + + res["fov"] = yfov res["fstop"] = node.getAttrValue("fStop", "fs", "float", 1, 5.6) return res This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-08-08 09:09:46
|
Revision: 330 http://cgkit.svn.sourceforge.net/cgkit/?rev=330&view=rev Author: mbaas Date: 2009-08-08 09:09:38 +0000 (Sat, 08 Aug 2009) Log Message: ----------- Brushed up the sequence module (and utils), wrote docs, unit tests and fixed all issues found by the tests. Modified Paths: -------------- cgkit/trunk/cgkit/sequence.py cgkit/trunk/utilities/seqcp.py cgkit/trunk/utilities/seqls.py cgkit/trunk/utilities/seqmv.py cgkit/trunk/utilities/seqrm.py Added Paths: ----------- cgkit/trunk/unittests/test_sequence.py Modified: cgkit/trunk/cgkit/sequence.py =================================================================== --- cgkit/trunk/cgkit/sequence.py 2009-08-06 20:01:27 UTC (rev 329) +++ cgkit/trunk/cgkit/sequence.py 2009-08-08 09:09:38 UTC (rev 330) @@ -45,13 +45,13 @@ class SeqString: """Sequence string class. - Sequence strings treat numbers inside a strings as integer numbers + Sequence strings treat numbers inside strings as integer numbers and not as strings. This can be used to sort numerically (e.g. - 'anim01' is smaller than 'anim0002'). + ``anim01`` is smaller than ``anim0002``). A sequence string is initialized by passing a regular string to - the constructor or by calling setString(). - The main task of a SeqString is comparing two strings which can + the constructor. It can be converted back using the :func:`str()` operator. + The main task of a :class:`SeqString` is comparing two strings which can be done with the normal comparison operators. Example: >>> a = SeqString('a08') @@ -71,17 +71,17 @@ to an empty string. """ # This is an alternating sequence of text and number values - # (always beginning with a text (which might be empty)). + # (always beginning and ending with a text (which might both be empty)). # The value part is a tuple (value,numdigits) where value # is an integer and numdigits the number of digits the - # value was made of. + # value was made of. The list can never be empty (it always contains + # at least one string, even when that one is empty). # Example: 'anim1_0001.png' -> ['anim', (1,1), '_', (1,4), '.png'] - self._value = [] - + self._value = [""] self._initSeqString(s) def __repr__(self): - return self.__str__() + return "'%s'"%self.__str__() def __str__(self): """Convert the sequence string into a normal string. @@ -98,7 +98,7 @@ val,ndigits = vn a = '%'+"0%dd"%ndigits res += a%val - return res + return res def __cmp__(self, other): """Comparison operator. @@ -106,19 +106,27 @@ The text parts are treated as strings, the number parts as numbers (e.g. 'a08' is greater than 'a2'). """ - if other is None: return 1 + if not isinstance(other, SeqString): + if not isinstance(other, basestring): + return 1 + # Convert both strings into pristine SeqStrings (because some numbers + # on the input strings may have been replaced by strings which would + # mess with the comparison). + selfStr = SeqString(self) + other = SeqString(other) + # Check the 'structure' of the strings first. # The numeric comparison is only done when the strings have the same # text/num patterns. - res = self.match_cmp(other) + res = selfStr.match_cmp(other) if res!=0: return res # Compare the individual components of the values side by side - for i, (a,b) in enumerate(zip(self._value, other._value)): + for i, (a,b) in enumerate(zip(selfStr._value, other._value)): if i%2==1: # Get the numbers a = a[0] @@ -131,7 +139,7 @@ # If we are here everything has been equal so far, but maybe # one string has one component more in _value - return cmp(len(self._value), len(other._value)) + return cmp(len(selfStr._value), len(other._value)) def _initSeqString(self, s): """Initialize the sequence string with a string. @@ -183,20 +191,24 @@ else: numtup = (int(textbuf),numtup[1]) res.append(numtup) + res.append("") self._value = res def match(self, template, numPos=None): """Check if one sequence string is equal to another except for one or all numbers. - Returns True if the text parts of self and template are equal, - i.e. both strings belong to the same sequence. + Returns ``True`` if the text parts of *self* and *template* are equal, + i.e. both strings belong to the same sequence. *template* must be + a :class:`SeqString` object. - numPos is the index of the number that is allowed to vary. For example, - if numPos is -1, the last number in a string may be different for two - strings to be in the same sequence. If numPos is None, all numbers - may vary. + *numPos* is the index of the number that is allowed to vary. For example, + if *numPos* is -1, only the last number in a string may be different for two + strings to be in the same sequence. Al other numbers must match exactly + (including the padding). If *numPos* is ``None``, all numbers may vary. """ + if not isinstance(template, SeqString): + raise TypeError("The template argument must be a SeqString object") # The lengths of the value lists must be equal if len(self._value)!=len(template._value): @@ -212,7 +224,7 @@ if i%2==0: if va!=vb: return False - elif numPos is not None and i!=numPos and va[0]!=vb[0]: + elif numPos is not None and i!=numPos and va!=vb: return False return True @@ -220,12 +232,13 @@ def match_cmp(self, template): """Comparison function to build groups. - This is the same as match() but with a different return value - so that this method can be used as comparison function for sort(). + Compare the text parts (the group name) of two sequence strings. + Numbers within the strings are ignored. - 0: self==template, <0: self<template, >0: self>template + 0 is returned if *self* and *template* belong to the same group, + a negative value is returned if *self* comes before *template* and + a positive value is returned if *self* comes after *template*. """ - a = self.groupRepr() b = template.groupRepr() return cmp(a,b) @@ -233,8 +246,8 @@ def groupRepr(self, numChar="*"): """Return a template string where the numbers are replaced by the given character. """ - res="" + numChar = str(numChar) for i,v in enumerate(self._value): if i%2==0: res += v @@ -245,19 +258,21 @@ def numCount(self): """Return the number of number occurrences in the string. - Example: 'anim01.tif' -> 1 - 'anim1_018.tif' -> 2 - 'anim' -> 0 + Examples: + + - ``anim01.tif`` -> 1 + - ``anim1_018.tif`` -> 2 + - ``anim`` -> 0 """ return int(len(self._value)/2) def getNum(self, idx): """Return a particular number inside the string. - idx is the index of the number (0-based) which may also be + *idx* is the index of the number (0-based) which may also be negative. The return value is an integer containing the number at that position. - Raises an IndexError exception when idx is out of range. + Raises an :exc:`IndexError` exception when *idx* is out of range. """ if idx<0: @@ -270,10 +285,10 @@ def getNumStr(self, idx): """Return a particular number as a string just as it appears in the original string. - idx is the index of the number (0-based) which may also be + *idx* is the index of the number (0-based) which may also be negative. The return value is a string that contains the number as it appears in the string (including padding). - Raises an IndexError exception when idx is out of range. + Raises an :exc:`IndexError` exception when *idx* is out of range. """ if idx<0: @@ -296,29 +311,35 @@ return res - def setNum(self, idx, value): + def setNum(self, idx, value, width=None): """Set a new number. - idx is the index of the number (may be negative) and value - is the new integer value. - Raises an IndexError exception when idx is out of range. + *idx* is the index of the number (may be negative) and *value* + is the new integer value. If *width* is given, it will be the new + width of the number, otherwise the number keeps its old width. + Raises an :exc:`IndexError` exception when *idx* is out of range. + + Note: It is possible to set a negative value. But when converted to + a string and then back to a sequence string again, that negative + number becomes a positive number and the minus symbol is part of + the preceding text part. """ - if idx<0: idx = self.numCount()+idx if idx<0 or idx>=self.numCount(): raise IndexError, "index out of range" - value = int(value) - width = self._value[idx*2+1][1] - self._value[idx*2+1] = (value,width) + if width is None: + width = self._value[idx*2+1][1] + self._value[idx*2+1] = (int(value),int(width)) def setNums(self, nums): """Set all numbers at once. - nums is a list of integers. The number of values may not - exceed the number count in the string, otherwise an IndexError - exception is thrown. + *nums* is a list of integers. The number of values may not + exceed the number count in the string, otherwise an :exc:`IndexError` + exception is thrown. There may be fewer items in *nums* though in + which case the remaining numbers in the string keep their old value. """ for i,val in enumerate(nums): self.setNum(i, val) @@ -326,10 +347,9 @@ def getNumWidth(self, idx): """Return the number of digits of a particular number. - idx is the index of the number (may be negative). - Raises an IndexError exception when idx is out of range. + *idx* is the index of the number (may be negative). + Raises an :meth:`IndexError` exception when *idx* is out of range. """ - if idx<0: idx = self.numCount()+idx if idx<0 or idx>=self.numCount(): @@ -340,11 +360,10 @@ def setNumWidth(self, idx, width): """Set the number of digits of a number. - idx is the index of the number (may be negative) and width + *idx* is the index of the number (may be negative) and *width* the new number of digits. - Raises an IndexError exception when idx is out of range. + Raises an :exc:`IndexError` exception when *idx* is out of range. """ - if idx<0: idx = self.numCount()+idx if idx<0 or idx>=self.numCount(): @@ -368,8 +387,8 @@ def setNumWidths(self, widths): """Set the number of digits for all numbers. - widths must be a list of integers. The number of values may not - exceed the number count in the string, otherwise an IndexError + *widths* must be a list of integers. The number of values may not + exceed the number count in the string, otherwise an :exc:`IndexError` exception is thrown. """ for i,w in enumerate(widths): @@ -378,20 +397,23 @@ def deleteNum(self, idx): """Delete a number inside the string. - idx is the index of the number (0-based) which may also be + This is the same as replacing the number by an empty string. + + *idx* is the index of the number (0-based) which may also be negative. - Raises an IndexError exception when idx is out of range. + Raises an :exc:`IndexError` exception when *idx* is out of range. """ self.replaceNum(idx, "") def replaceNum(self, idx, txt): """Replace a number by a string. - idx is the index of the number (0-based) which may also be - negative. txt is a string that will replace the number. - Raises an IndexError exception when idx is out of range. + The string is merged with the surrounding string parts. + + *idx* is the index of the number (0-based) which may also be + negative. *txt* is a string that will replace the number. + Raises an :exc:`IndexError` exception when *idx* is out of range. """ - if idx<0: idx = self.numCount()+idx if idx<0 or idx>=self.numCount(): @@ -408,9 +430,9 @@ def replaceStr(self, idx, txt): """Replace a string part by another string. - idx is the index of the sub-string (0-based) which may also be - negative. txt is a string that will replace the sub-string. - Raises an IndexError exception when idx is out of range. + *idx* is the index of the sub-string (0-based) which may also be + negative. *txt* is a string that will replace the sub-string. + Raises an :exc:`IndexError` exception when *idx* is out of range. """ if idx<0: @@ -426,18 +448,19 @@ raise IndexError, "index out of range" # Replace the text - self._value[idx2] = txt + self._value[idx2] = str(txt) class Sequence: """A list of names/objects that all belong to the same sequence. The sequence can store the original objects that are associated with a - name or it can only store the names (as SeqString) objects. If the - original objects are available or not depends on how the sequence was - built. If the getNameFunc parameter was used when building the sequence - (see Sequences), then the original objects will be available. + name or it can only store the names (as :class:`SeqString` objects). + Whether the original objects are available or not depends on how the + sequence was built. If the *nameFunc* parameter was used when building + the sequence (see :func:`buildSequences`), then the original objects will be available. - The class can be used like a list (using len(), index operator or iteration). + The class can be used like a list (using :func:`len()`, index operator or + iteration). """ def __init__(self): @@ -455,7 +478,13 @@ if len(ranges)==0: return placeholder else: - return "%s (%s)"%(placeholder, "; ".join(ranges)) + infoStr = "; ".join(ranges) + if len(infoStr)>20: + infoStr = "%d items"%len(self._names) + return "%s (%s)"%(placeholder, infoStr) + + def __repr__(self): + return "<Sequence %s>"%self.__str__() def __len__(self): """Return the length of the sequence. @@ -475,17 +504,17 @@ return self._objects[idx] def iterNames(self): - """Iterate over the names. + """Iterates over the object names. - Yields SeqString objects. + Yields :class:`SeqString` objects. """ return iter(self._names) def iterObjects(self): - """Yield over the objects. + """Iterate over the objects. - Yields the original objects or the names as SeqStrings if the - objects haven't been stored in the sequence. + Yields the original objects or the names as :class:`SeqString` objects + if the objects haven't been stored in the sequence. Using this method is equivalent to iterating over the sequence object directly. """ @@ -497,26 +526,48 @@ def match(self, name, numPos=None): """Check if a name matches the names in this sequence. + *name* is a string or :class:`SeqString` object that is tested if + it matches the names in the sequence. If the sequence doesn't contain any name at all yet, then any name will match. + + *numPos* is an integer that specifies which number is allowed to + vary. If *numPos* is ``None``, all numbers may vary. """ + # Turn the name into a SeqString + if not isinstance(name, SeqString): + name = SeqString(name) + if len(self._names)==0: return True else: return self._names[0].match(name, numPos) def append(self, name, obj=None): - """Add a file name to the group. + """Append a name/object to the end of the sequence. - name can be a SeqString object or a regular string. - The name is added unconditionally, so it's the callers responsibility - to make sure the file really belongs to this sequence. + *name* can be a :class:`SeqString` object or a regular string. + The name must match the names in the sequence, otherwise a + :exc:`ValueError` exception is thrown. - This is called internally when sequences are built! + *obj* can be any Python object that is stored alongside the name + (this is supposed to be the actual object that has the given name). + In any sequence, either all or none of the names must be associated + with an object. An attempt to append a name without an object to a + sequence that has objects will trigger a :exc:`ValueError` exception. + + Usually, you won't call this method manually to build a sequence + but instead use the :func:`buildSequences()` function which returns + initialized ``Sequence`` objects. """ + # Turn the name into a SeqString if not isinstance(name, SeqString): name = SeqString(name) + if not self.match(name): + placeholder,ranges = self.sequenceName() + raise ValueError("Cannot add '%s' to sequence %s. The name doesn't match the sequence."%(name, placeholder)) + if obj is not None: if self._objects is None: if len(self._names)==0: @@ -524,6 +575,8 @@ else: raise ValueError("objects must be given for all or none of the names") self._objects.append(obj) + elif self._objects is not None: + raise ValueError("objects must be given for all or none of the names") self._names.append(name) @@ -531,9 +584,9 @@ """Return the index of the sequence number. Returns the index of the number that has the most variation among its - values. If two numbers have the same amount of values, the last - number is used. - Returns None if there is no number at all. + values. If two number positions have the same variation, then the last + number is returned. + Returns ``None`` if there is no number at all. """ ranges = self.ranges() @@ -549,9 +602,9 @@ return seqNumIdx def ranges(self): - """Return a list of all the number ranges in the sequence. + """Returns a list of all the number ranges in the sequence. - The return value is a list of Range objects. There are as many + The return value is a list of :class:`Range` objects. There are as many ranges as there are separate numbers in the names. The ranges are given in the same order as the corresponding number appears in the names. @@ -562,15 +615,15 @@ def sequenceName(self): """Return a sequence placeholder and range strings. - Returns a tuple (placeholder, ranges) where placeholder is the + Returns a tuple (*placeholder*, *ranges*) where *placeholder* is the name of a member of the sequence where all numbers have been replaced - by '#' (=0-padded number with 4 digits) or one or more '@' (=padded - number with as many digits as there are '@' characters. Just a single - '@' represents an unpadded number). If the sequence contains inconsistent - padding, the number is replaced by '*'. + by ``'#'`` (0-padded number with 4 digits) or one or more ``'@'`` (padded + number with as many digits as there are ``'@'`` characters. Just a single + ``'@'`` represents an unpadded number). If the sequence contains inconsistent + padding, the number is replaced by ``'*'``. The number is not replaced at all if there is only one single value among all file names anyway. - ranges is a list of strings where each string describes the range + *ranges* is a list of strings where each string describes the range of values of the corresponding number in the placeholder string. The returned information is meant to be displayed to the user as @@ -653,118 +706,45 @@ return str(res), rangeStrs -class Sequences: - """A collection of sequences. - """ - - def __init__(self, names=[], assumeFiles=False, getNameFunc=None): - """Constructor. - - names is a list of strings that will be grouped into sequences. - """ - # A list of FileSequence objects - self._sequences = [] - - # Create the sequences - self.setFiles(names, assumeFiles=assumeFiles, getNameFunc=getNameFunc) - def __str__(self): - return "<Sequences: %d sequences>"%len(self._sequences) - - def __len__(self): - return len(self._sequences) - - def __getitem__(self, key): - """Return the Sequence object with the given index. - """ - return self._sequences[key] - - # clear - def clear(self): - """Remove all sequences. - """ - self._sequences = [] - - # setFiles - def setFiles(self, names, numPos=None, assumeFiles=False, getNameFunc=None): - """Initialize the sequences given a flat list of names. - - names is a list of objects (usually strings) that are turned into - SeqString objects and grouped into sequences. - if assumeFiles is True, the input strings are assumed to be file - names. In this case, it will be ensured that files from different - directories are put into different sequences and any number occurring - in the directory part is "frozen" (turned into a string). - - getNameFunc can be a callable that gets called for every item in names. - The function has to return the actual name of that object. This can - be used if the input list contains object that are not strings but - some other (compound) objects. - """ - - self.clear() - - # Create the objects list which contains 2-tuples (seqString,obj). - # obj is the original object from the "names" list or None. - if getNameFunc is None: - objects = map(lambda name: (SeqString(name),None), names) - else: - objects = map(lambda obj: (SeqString(getNameFunc(obj)),obj), names) - # Sort the objects according to their seqString - # The order of the result is already so that members of the same - # sequence are together, we just don't know yet where a sequence ends - # and the next one begins. - objects.sort(key=lambda tup: tup[0]) - - # Build sequences... - currentSeq = Sequence() - currentPath = None - for name,obj in objects: - # Are we dealing with file names? Then freeze directory numbers... - if assumeFiles: - path,n = os.path.split(str(name)) - pathseq = SeqString(path) - # n: The number count in the path (these numbers have to be frozen) - n = pathseq.numCount() - for i in range(n): - name.replaceNum(i, name.getNumStr(i)) - - sequenceSplit = False - - # Check if the current name has a different structure or different - # text parts as the names in the current sequence. If so, we - # have to begin a new sequence - if not currentSeq.match(name, numPos): - sequenceSplit = True - - # If we are dealing with file names, then make sure files in - # different directories are put into separate sequences (even - # when the names have the same structure). - if assumeFiles: - # path has been set above where the directory numbers were frozen - if currentPath is not None and path!=currentPath: - sequenceSplit = True - currentPath = path - - # Do we have to begin a new sequence? - if sequenceSplit: - self._sequences.append(currentSeq) - currentSeq = Sequence() - - # Add the current name to the current sequence - currentSeq.append(name, obj) - - # Also store the last sequence generated (if it isn't empty) - if len(currentSeq)>0: - self._sequences.append(currentSeq) - - class Range: """Range class. - This class represents a sequence of integer values (frame numbers). + This class represents a sorted sequence of integer values (frame numbers). The sequence is composed of a number of sub-ranges which have a begin, - an optional end and a step number. + an optional end and an optional step number. If the end is omitted, + the sequence will be infinite. + + Examples: + + >>> list(Range("1,5,10")) + [1, 5, 10] + >>> list(Range("1-5")) + [1, 2, 3, 4, 5] + >>> list(Range("2-8x2")) + [2, 4, 6, 8] + >>> list(Range("1-3,10-13")) + [1, 2, 3, 10, 11, 12, 13] + + The range object supports the :func:`len()` operator, comparison operators, + the :keyword:`in` operator and iteration. Examples: + + >>> rng = Range("1-2,5") + >>> len(rng) + 3 + >>> for i in rng: print i + ... + 1 + 2 + 5 + >>> 3 in rng + False + >>> 5 in rng + True + >>> Range("1-3")==Range("1,2,3") + True + >>> Range("1-5")==Range("2-6") + False """ def __init__(self, rangeStr=None): @@ -802,11 +782,27 @@ endStr = str(end) rangeStrs.append("%s-%s%s"%(begin,endStr,stepStr)) - + return ",".join(rangeStrs) __repr__ = __str__ - + + def __eq__(self, other): + """Equality operator + """ + if not isinstance(other, Range): + return False + + return self._ranges==other._ranges + + def __ne__(self, other): + """Inequality operator + """ + if not isinstance(other, Range): + return True + + return self._ranges!=other._ranges + def __len__(self): """Return the number of values in the sequence. @@ -853,49 +849,54 @@ # Remove the deleted items (the ones that are None) currentValues = filter(lambda x: x is not None, currentValues) - + + def __contains__(self, val): + """Check if a value is inside the range. + + *val* is an integer that is checked against the range. The method + returns ``True`` when the value is part of the range. + """ + for begin,end,step in self._ranges: + if val>=begin and (end is None or val<=end) and (val-begin)%step==0: + return True + return False + def isInfinite(self): """Check if the range is infinite. + + Examples: + + >>> Range("1-5").isInfinite() + False + >>> Range("1-").isInfinite() + True """ for begin,end,step in self._ranges: if end is None: return True - return False - - def contains(self, val): - """Check if a value is inside the range. - - val is an integer that is checked against the range. The method - returns True when the value is part of the range. - """ - for begin,end,step in self._ranges: - if val>=begin and (end is None or val<=end) and (val-begin)%step==0: - return True return False def setRange(self, rangeStr): """Initialize the range object with a new range string. The range string may contain individual numbers or ranges separated by - comma. The individual ranges are specified by a begin, an end (inclusive) - and an optional step number. - This is the opposite function to compactRange(). + comma. The individual ranges are specified by a begin, an optional + end (inclusive) and an optional step number. Passing ``None`` is + equivalent to passing an empty string. - Examples: - - "1,5,10" -> [1,5,10] - "1-5" -> [1,2,3,4,5] - "2-8x2" -> [2,4,6,8] - "1-3,10-13" -> [1,2,3,10,11,12,13] + This is the opposite operation to e :func:`compactRange()` function. """ if rangeStr is None: rangeStr = "" + if type(rangeStr) is not str: + raise TypeError("The rangeStr argument must be a string") + reRange = re.compile(r"([0-9]+)(?:-([0-9]*)(?:x([0-9]+))?)?$") - self._ranges = [] + ranges = [] for rs in rangeStr.split(","): rs = rs.strip() if rs=="": @@ -921,18 +922,192 @@ # sequence (i.e. 1-10x2 -> 1-9x2) end -= (end-begin)%step if end is None or end>=begin: - self._ranges.append((begin,end,step)) + ranges.append((begin,end,step)) else: raise ValueError("Invalid range string: %s"%rs) - self._ranges.sort() + ranges = self._normalizeRanges(ranges) + self._ranges = ranges + def _normalizeRanges(self, ranges): + """Normalize the given ranges. + ranges is a list of range tuples (just like self._ranges). + Sorts the ranges, merges them if possible (1,2,3 -> 1-3) or + splits them up so that they don't overlap (2-20x2,11 -> 2-10x2,11,12-20x2). + Returns a new range list (the input list gets destroyed). + """ + if len(ranges)==0: + return [] + + ranges.sort() + + newRanges = [] + # The current range + rng = ranges.pop(0) + while len(ranges)>0: + # Get the next range + nextRng = ranges.pop(0) + + # Handle range overlaps + rngs = self._resolveRangeOverlap(rng, nextRng) + if rngs is not None: + rng = rngs[0] + # Only 1 range? Then nextRange was completely contained in rng, so get a new range + if len(rngs)>1: + # Continue with the adjusted ranges (insert them into the range + # list and sort again because the order may have changed) + ranges.extend(rngs[1:]) + ranges.sort() + continue + + # Merge the ranges if possible... + rng,nextRng = self._mergeRanges(rng, nextRng) + if nextRng is not None: + newRanges.append(rng) + rng = nextRng + + # Append the last range + newRanges.append(rng) + + # Final step that moves end values to the subsequent range if this + # makes the sub-ranges "nicer". + for i in range(len(newRanges)-1): + begin1,end1,step1 = newRanges[i] + begin2,end2,step2 = newRanges[i+1] + # Can the last value of the current range be moved into the subsequent + # range and the current range would then only be one single value? + # (Example: 1-5x4,6-10 -> 1,5-10) + if end1==begin2-step2 and begin1+step1==end1: + newRanges[i] = (begin1,begin1,1) + newRanges[i+1] = (end1,end2,step2) + + return newRanges + + def _resolveRangeOverlap(self, rng1, rng2): + """Resolve overlapping ranges. + + rng1 and rng2 are two adjacent ranges in sorted order (rng2 must + not be before rng1). + Returns a list of 1-3 ranges where the first range is guaranteed + to be non-overlapping. The other ranges are in sort order but may + still overlap (they will be handled in a subsequent iteration). + Returns None when rng1 and rng2 don't overlap at all. + + This is a helper method for _normalizeRanges(). + """ + begin1,end1,step1 = rng1 + begin2,end2,step2 = rng2 + + # No overlap? Then don't modify anything + if end1 is not None and begin2>end1: + return None + + # The ranges overlap... + + # Remove all initial values from rng2 that are also part of rng1. + # First check if begin2 is part of rng1 + if (begin2-begin1)%step1==0: + # Does rng2 use a step size that is a multiple of the step size of rng1? + if step2%step1==0: #step1==step2: + # Does rng2 completely lie within rng1? Then just ignore rng2 + if end1 is None or (end2 is not None and end2<=end1): + return [rng1] + else: + # Set the begin of rng2 to the first value behind the end of rng1 + n = int((end1-begin2)/step2)+1 + begin2 += n*step2 + # Different steps, so only the first value is identical + else: + # Is rng2 just one single value? Then we can ignore rng2 + # (because this value is also part of rng1) + if begin2==end2: + return [rng1] + else: + begin2 += step2 + + # If the ranges don't overlap anymore, then we are done. + if end1 is not None and begin2>end1: + return [rng1,(begin2,end2,step2)] + + + # At this point, it is guaranteed that... + # - ...rng1 and rng2 don't begin with the same value (i.e. begin1<begin2 is always true) + # - ...begin2 is not part of rng1 + # - ...rng1 and the adjusted rng2 still overlap + + res = [] + # Split off the first part of rng1 (everything that is before rng2) + # -> adjust rng1 so that it only contains the remaining range + n = int((begin2-begin1-1)/step1) + e1 = begin1+n*step1 + res.append((begin1,e1,step1)) + begin1 = e1+step1 + + # begin1 is now greater than begin2 (they can't be equal because we know + # that begin2 is not part of the initial rng1) + + res.append((begin2,end2,step2)) + res.append((begin1,end1,step1)) + + # res now contains 3 ranges. The first one is guaranteed to be unique + # and doesn't overlap anymore. The other two may still overlap but + # this is dealt with in a subsequent iteration. + + return res + + def _mergeRanges(self, rng1, rng2): + """Merge two ranges if possible. + + Returns the new ranges. The second range may become None if it was + entirely consumed by the first range. + + The input ranges must be sorted (i.e. rng2 must not be *before* rng1). + + This is a helper method for _normalizeRanges(). + """ + begin1,end1,step1 = rng1 + begin2,end2,step2 = rng2 + + # If range1 is just a single value, then we can always merge at least + # the first value of range2 (as we are free to change the step). + if begin1==end1: + step1 = begin2-end1 + + # If range2 does not start right behind range1, then there is nothing to merge + if end1 is None or begin2!=end1+step1: + return rng1,rng2 + + # Can the entire range2 be merged into range1? (this is the case if + # the step size is identical or range2 is just a single value anyway) + if step1==step2 or begin2==end2: + return (begin1,end2,step1),None + # Only put the first value of range2 into range1 + else: + return (begin1,begin2,step1), (begin2+step2, end2, step2) + + class SeqTemplate: - """Sequence template class. + """Sequence name template class. - An instance of this class represents a template string to create numbered - sequences. + An instance of this class represents a template string that may contain + patterns that will be substituted by numbers. + This can be used to generate the individual names for an output sequence. + + Example: + + >>> tmpl = SeqTemplate("foo#.tif") + >>> tmpl([17]) + 'foo0017.tif' + >>> tmpl=SeqTemplate("foo@@_#.tif") + >>> tmpl([2,17]) + 'foo02_0017.tif' + >>> tmpl=SeqTemplate("foo@@[2]_#[1].tif") + >>> tmpl([2,17]) + 'foo17_0002.tif' + >>> tmpl=SeqTemplate("foo{2*#+1}.tif") + >>> tmpl([5]) + 'foo0011.tif' """ def __init__(self, template): @@ -965,13 +1140,15 @@ """Return a string that uses the given input numbers. The substitution patterns in the template string are replaced by - the given numbers. values must be a list of objects that can be + the given numbers. *values* must be a list of objects that can be turned into integers. - It is the callers responsibility to make sure that values contains + It is the callers responsibility to make sure that *values* contains enough numbers. - If any number expression fails, a ValueError exception is thrown - (this is also the case when the expressions refers to a value in + If any number expression fails, a :exc:`ValueError` exception is thrown + (this is also the case when an expression refers to a value in the input list that is not available). + + Calling this method is equivalent to using the object as a callable. """ # Make sure we have integers values = [int(v) for v in values] @@ -991,13 +1168,24 @@ def expressionIndices(self, inputSize): """Return the indices of the source values that the number expressions refer to. - inputSize is the length of the value sequence that will get passed - to substitute(). This is used to resolve negative indices. The + *inputSize* is the length of the value sequence that will get passed + to :meth:`substitute()`. This is used to resolve negative indices. The result may still contain negative indices if any index in the expressions is out of range. The order of the values in the list is the same order as the expressions appear in the template. The return value can be used to check if an expression would produce - an IndexError exception. + an :exc:`IndexError` exception. + + Example: + + >>> t=SeqTemplate("foo#_#") + >>> t.expressionIndices(2) + [0, 1] + >>> t=SeqTemplate("foo#[-1]_#[1]") + >>> t.expressionIndices(2) + [1, 0] + >>> t.expressionIndices(3) + [2, 0] """ res = [] for i in self._exprIndices: @@ -1132,15 +1320,40 @@ output file sequence based on an input sequence but where the numbers in the output sequence may be different than the numbers in the input sequence. For example, the class is used by the sequence utilities (seqmv, seqcp, - seqrm, seqln). + seqrm). - An OutputNameGenerator has one public attribute called numberMergeFlag - which is True when the output name pattern ended in a digit but didn't + An :class:`OutputNameGenerator` has one public attribute called :attr:`numberMergeFlag` + which is ``True`` when the output name pattern ended in a digit but didn't contain any number pattern. In this case, the class will append a 4-padded number but because the name already ended in a digit, the combination of the pattern and the number results in a larger number which is not necessarily what the user intended. The flag can be used by an application to check whether it should ask the user for confirmation. + + Example: + + >>> seqs = buildSequences(["spam1_1.tif", "spam1_2.tif", "spam1_5.tif"]) + >>> + >>> for src,dst in OutputNameGenerator(seqs, "foo"): + ... print src,"->",dst + ... + spam1_1.tif -> foo0001.tif + spam1_2.tif -> foo0002.tif + spam1_5.tif -> foo0005.tif + >>> + >>> for src,dst in OutputNameGenerator(seqs, "foo@_#.tif", dstRange=Range("10-")): + ... print src,"->",dst + ... + spam1_1.tif -> foo1_0010.tif + spam1_2.tif -> foo1_0011.tif + spam1_5.tif -> foo1_0012.tif + >>> + >>> for src,dst in OutputNameGenerator(seqs, "foo_#[2]_{@[1]+2}.tif"): + ... print src,"->",dst + ... + spam1_1.tif -> foo_0001_3.tif + spam1_2.tif -> foo_0002_3.tif + spam1_5.tif -> foo_0005_3.tif """ def __init__(self, srcSequences, dstName, srcRanges=None, dstRange=None, keepExt=True, @@ -1238,6 +1451,8 @@ for srcSeq in self._srcSequences: self._outputNameSpec(srcSeq, dstName, dstRangeIter is not None) + def __iter__(self): + return self.iterNames() def iterNames(self): """Iterate over input/output name pairs. @@ -1246,6 +1461,8 @@ name from the input sequences and dstName is the generated output name (as specified by the output pattern and additional arguments that were passed to the constructor). + + This is equivalent to iterating directly over the object. """ # Iterate over all input sequences @@ -1280,6 +1497,27 @@ res = self._outputNameSpec(srcSequence, dstName, dstRangeIter is not None) dstTemplate, numIdxs, seqNumIdx = res + # Check what indices are used by the expressions (the result may not be + # accurate when negative numbers are used because the integer we pass + # to expressionIndices() may not be the correct one, but we are only + # really interested in the simpler case were no explicit indices have + # been provided anyway). + ei = dstTemplate.expressionIndices(len(numIdxs)) + # Adjust the index of the main sequence number if it is not in the + # list of used indices. Otherwise providing a destination range would + # be useless because it would affect an unused number. + # This can happen when an input sequence has at least two varying numbers + # and the output sequence has only one number pattern and a destination + # range has been specified. + # Example: "spam1_1", "spam1_2", "spam2_5", "spam2_6" -> "foo#" (2-) + # The main sequence number will be the second one, but the pattern + # in the output name would refer to the first number, so the destination + # range would have no effect and the output would be "foo1", "foo1", + # "foo2", "foo2". The following if sets the main sequence number to be + # the first one and then everything is fine again. + if seqNumIdx not in ei: + seqNumIdx = max(ei) + srcIter = iter(srcSequence) # Assign output names to the input names... @@ -1310,7 +1548,7 @@ nums = map(lambda i: allNums[i], numIdxs) # Only queue this file when it is part of the source range - if len(nums)==0 or srcRange.contains(nums[seqNumIdx]): + if len(nums)==0 or (nums[seqNumIdx] in srcRange): # If a destination range was specified then replace the # main file number with the next number in the range, otherwise # the number from the input file is used @@ -1319,7 +1557,7 @@ nums[seqNumIdx] = dstRangeIter.next() except StopIteration: break - # Create the file names and add them to the list + # Create the file names dstName = dstTemplate.substitute(nums) if keepExt and os.path.splitext(dstName)[1]!=ext: dstName += ext @@ -1392,7 +1630,7 @@ for i,rng in enumerate(ranges): if len(rng)>1: numIdxs.append(i) - # Do we have too little patterns? (and the user did not specify any + # Do we have too few patterns? (and the user did not specify any # index explicitly?) # If so, throw an error because it's not clear which number should be # mapped to which pattern. @@ -1427,7 +1665,16 @@ """Base class for move/copy/link. """ - def __init__(self, srcSequences, dstName, srcRanges=None, dstRange=None, keepExt=True, enforceDstRange=False, verbose=False): + def __init__(self, srcSequences, dstName, srcRanges=None, dstRange=None, + keepExt=True, enforceDstRange=False, verbose=False, + resolveSrcLinks=False): + """Constructor. + + See the derived classes for documentation on most arguments. + + resolveSrcLinks: If true, the source file names will be replaced by + their real paths. + """ ong = OutputNameGenerator(srcSequences, dstName, srcRanges = srcRanges, @@ -1441,8 +1688,10 @@ # Create the file table fileTab = [] for uiSrc,uiDst in ong.iterNames(): - src = os.path.realpath(uiSrc) - dst = os.path.realpath(uiDst) + src = os.path.abspath(uiSrc) + dst = os.path.abspath(uiDst) + if resolveSrcLinks: + src = os.path.realpath(uiSrc) fileTab.append((src,dst,uiSrc,uiDst)) # Resolve internal collisions @@ -1454,11 +1703,11 @@ def mergesNumbers(self): """Check if a trailing number on the output sequence and a file number would get merged. - This method returns True when the base output sequence name ends in + This method returns ``True`` when the base output sequence name ends in a number and a sequence number would be appended as well which results in a new number (for example, writing a sequence with the base name - out2 can produce output files out20001, out20002, ... which may not - be what the user intended). The result of this call can be used to + ``out2`` can produce output files ``out20001``, ``out20002``, ... which + may not be what the user intended). The result of this call can be used to check if the application should ask the user for confirmation. """ return self._mergesNumbers @@ -1468,7 +1717,7 @@ Only iterates over the files that are not part of the input sequence. The returned files are those that would get overwritten when the - move operation would be carried out. + operation would be carried out. This can be used to check if the user should be asked for confirmation. """ srcDict = {} @@ -1485,22 +1734,28 @@ def sequences(self): """Iterate over the input/output sequences. - Yields tuples (srcSeq, dstSeq) where each item is a Sequence() + Yields tuples (*srcSeq*, *dstSeq*) where each item is a :class:`Sequence` object. The result can be used to show an overview of what the - move operation will do. + operation will do. """ # Print the final source and destination sequences (just for user info) - seqs = Sequences(self._fileTab, getNameFunc=lambda t:t[2]) + + # We use _buildSequences() instead of buildSequences() so that the + # fileTab doesn't get sorted again (it is already sorted). This + # ensures that the sequences are yielded in the same order in which + # they will get processed. + objects = map(lambda obj: (SeqString(obj[2]),obj), self._fileTab) + seqs = _buildSequences(objects) for srcSeq in seqs: dstFiles = map(lambda t: t[3], srcSeq) - dstSeq = Sequences(dstFiles)[0] + dstSeq = buildSequences(dstFiles)[0] yield srcSeq, dstSeq def dryRun(self, outStream=None): """Print what would get done when run() was called. - outStream is an object with a write() method that will receive - the text. If None is passed, sys.stdout is used. + *outStream* is an object with a :meth:`write()` method that will + receive the text. If ``None`` is passed, ``sys.stdout`` is used. """ if outStream is None: outStream = sys.stdout @@ -1512,8 +1767,9 @@ def run(self, outStream=None): """Do the operation. - outStream is an object with a write() and flush() method that will receive - the text (only in verbose mode). If None is passed, sys.stdout is used. + *outStream* is an object with a :meth:`write()` and :meth:`flush()` + method that will receive the text (only in verbose mode). If ``None`` + is passed, ``sys.stdout`` is used. """ if outStream is None: outStream = sys.stdout @@ -1679,7 +1935,8 @@ """This class copies one or more sequences of files. """ - def __init__(self, srcSequences, dstName, srcRanges=None, dstRange=None, keepExt=True, verbose=False): + def __init__(self, srcSequences, dstName, srcRanges=None, dstRange=None, + keepExt=True, verbose=False, resolveSrcLinks=False): """Constructor. srcSequences is a list of Sequence objects that contain the source @@ -1718,8 +1975,13 @@ The verbose flag determines whether each file is printed during the actual operation. + + resolveSrcLinks determines whether source links are resolved before + processing the sequence. """ - _SequenceProcessor.__init__(self, srcSequences, dstName, srcRanges, dstRange, keepExt, enforceDstRange=True, verbose=verbose) + _SequenceProcessor.__init__(self, srcSequences, dstName, srcRanges, dstRange, + keepExt, enforceDstRange=True, verbose=verbose, + resolveSrcLinks=resolveSrcLinks) def _fileOperation(self, src, dst): """Do the copy operation. @@ -1788,18 +2050,111 @@ os.symlink(src, dst) +def buildSequences(names, numPos=None, assumeFiles=False, nameFunc=None): + """Create sorted sequences from a list of names/objects. + + *names* is a list of objects (usually strings) that are grouped into sequences. + If *assumeFiles* is ``True``, the input strings are assumed to be file + names. In this case, it will be ensured that files from different + directories are put into different sequences and any number occurring + in the directory part is "frozen" (turned into a string). + + *numPos* can be set to a number index which defines the position of the + numbers that are allowed to vary per sequence. If not given, all numbers + may vary (for example, if you want the files ``clip1_#.tif`` to be a different + sequence than ``clip2_#.tif`` you have to set numPos to 1 or -1). + + *nameFunc* can be a callable that gets called for every item in *names*. + The function has to return the actual name of that object. This can + be used if the input list contains objects that are not strings but + some other (compound) objects. + + Returns a list of :class:`Sequence<cgkit.sequence.Sequence>` objects. + The sequences and the files within the sequences are sorted. + """ + # Create the objects list which contains 2-tuples (seqString,obj). + # obj is the original object from the "names" list or None. + if nameFunc is None: + objects = map(lambda name: (SeqString(name),None), names) + else: + objects = map(lambda obj: (SeqString(nameFunc(obj)),obj), names) + # Sort the objects according to their seqString + # The order of the result is already so that members of the same + # sequence are together, we just don't know yet where a sequence ends + # and the next one begins. + objects.sort(key=lambda tup: tup[0]) + + return _buildSequences(objects, numPos, assumeFiles) + +def _buildSequences(objects, numPos=None, assumeFiles=False): + """Helper function for buildSequences(). + + objects is a sorted list of (name,obj) tuples. + """ + res = [] + + # Build sequences... + currentSeq = Sequence() + currentPath = None + for name,obj in objects: + # Are we dealing with file names? Then freeze directory numbers... + if assumeFiles: + path,n = os.path.split(str(name)) + pathseq = SeqString(path) + # n: The number count in the path (these numbers have to be frozen) + n = pathseq.numCount() + for i in range(n): + name.replaceNum(i, name.getNumStr(i)) + + sequenceSplit = False + + # Check if the current name has a different structure or different + # text parts as the names in the current sequence. If so, we + # have to begin a new sequence + if not currentSeq.match(name, numPos): + sequenceSplit = True + + # If we are dealing with file names, then make sure files in + # different directories are put into separate sequences (even + # when the names have the same structure). + if assumeFiles: + # path has been set above where the directory numbers were frozen + if currentPath is not None and path!=currentPath: + sequenceSplit = True + currentPath = path + + # Do we have to begin a new sequence? + if sequenceSplit: + res.append(currentSeq) + currentSeq = Sequence() + + # Add the current name to the current sequence + currentSeq.append(name, obj) + + # Also store the last sequence generated (if it isn't empty) + if len(currentSeq)>0: + res.append(currentSeq) + + return res + + def compactRange(values): """Build the range string that lists all values in the given list in a compacted form. - values is a list of integers (may contain duplicate values and doesn't have + *values* is a list of integers (may contain duplicate values and does not have to be sorted). The return value is a string that lists all values (sorted) - in a compacted form (using the same syntax that Shake accepts as time values). - The returned range string can be passed to a Range object to create the - expanded integer sequence again. + in a compacted form. + The returned range string can be passed into a :class:`Range` object to create + the expanded integer sequence again. - Example: [1,2,3,4,5,6] -> "1-6" - [2,4,6,8] -> "2-8x2" - [1,2,3,10,11,12] -> "1-3,10-12" + Examples: + + >>> compactRange([1,2,3,4,5,6]) + '1-6' + >>> compactRange([2,4,6,8]) + '2-8x2' + >>> compactRange([1,2,3,12,11,10]) + '1-3,10-12' """ if len(values)==0: return "" @@ -1870,12 +2225,14 @@ return ",".join(rs) def glob(name): - """Create file sequences. + """Create file sequences from a name pattern. - name is a file pattern that will get a '*' appended. The pattern is then - passed to the regular glob() function to obtain a list of files which - are then grouped into sequences. - Returns a Sequences objects that contains all file sequences found. + *name* is a file pattern that will get a ``'*'`` appended. The pattern is then + passed to the regular :func:`glob()` function from the standard :mod:`glob` + module to obtain a list of files which are then grouped into sequences. + + Returns a list of :class:`Sequence<cgkit.sequence.Sequence>` objects. + The sequences and the files within the sequences are sorted. """ globpattern = name if not globpattern.endswith("*"): @@ -1926,7 +2283,7 @@ # Remove files that don't have any number in their name (without ext) fileNames = filter(lambda n: SeqString(os.path.splitext(n)[0]).numCount()>0, fileNames) - return Sequences(fileNames, assumeFiles=True) + return buildSequences(fileNames, assumeFiles=True) # The following function is obsolete and replaced by the SeqTemplate class. Added: cgkit/trunk/unittests/test_sequence.py =================================================================== --- cgkit/trunk/unittests/test_sequence.py (rev 0) +++ cgkit/trunk/unittests/test_sequence.py 2009-08-08 09:09:38 UTC (rev 330) @@ -0,0 +1,733 @@ +# Test the sequence module + +import unittest +import os, os.path +import glob as globmod +from cgkit.sequence import * + +class TestSeqString(unittest.TestCase): + """Test the SeqString class. + """ + + def testComparison(self): + """Test the comparison operator. + """ + s1 = SeqString("spam004") + s2 = SeqString("spam2") + s3 = SeqString("spam4") + self.assertEqual(1, cmp(s1, s2)) + self.assertEqual(-1, cmp(s2, s1)) + self.assertEqual(0, cmp(s1, s3)) + + s1 = SeqString("spam004_018") + s2 = SeqString("spam2_431") + s3 = SeqString("spam4_18") + self.assertEqual(1, cmp(s1, s2)) + self.assertEqual(-1, cmp(s2, s1)) + self.assertEqual(0, cmp(s1, s3)) + + self.assertEqual("spam004_018", s1) + self.assertEqual(True, "spam004_018"==s1) + self.assertEqual(True, "spam04_18"==s1) + self.assertEqual(True, "spam04_19"!=s1) + + s = SeqString("spam1_1") + self.assertEqual(True, s=="spam1_1") + s.replaceNum(0, "1") + self.assertEqual(True, s=="spam1_1") + + def testMatch(self): + """Test the match method. + """ + s = SeqString("foo") + self.assertEqual(True, s.match(SeqString("foo"))) + self.assertEqual(False, s.match(SeqString("Foo"))) + self.assertEqual(False, s.match(SeqString("bar"))) + self.assertEqual(False, s.match(SeqString("foo10"))) + self.assertEqual(False, s.match(SeqString(""))) + + s = SeqString("spam1_bla05") + self.assertEqual(True, s.match(SeqString("spam05_bla1000"))) + self.assertEqual(False, s.match(SeqString("spam05_bla1000x"))) + self.a... [truncated message content] |
From: <mb...@us...> - 2009-08-06 20:01:41
|
Revision: 329 http://cgkit.svn.sourceforge.net/cgkit/?rev=329&view=rev Author: mbaas Date: 2009-08-06 20:01:27 +0000 (Thu, 06 Aug 2009) Log Message: ----------- Support for custom RIB strings on light sources (so far, image pass only). Modified Paths: -------------- cgkit/trunk/cgkit/ribexport.py cgkit/trunk/changelog.txt Modified: cgkit/trunk/cgkit/ribexport.py =================================================================== --- cgkit/trunk/cgkit/ribexport.py 2009-07-11 13:14:56 UTC (rev 328) +++ cgkit/trunk/cgkit/ribexport.py 2009-08-06 20:01:27 UTC (rev 329) @@ -781,6 +781,10 @@ RiAttributeBegin() RiAttribute("identifier", "name", lgt.name) RiConcatTransform(lgt.worldtransform.toList()) + # Custom RIB + rib = getattr(lgt, "rib", None) + if rib is not None: + RiArchiveRecord(RI_VERBATIM, rib+"\n") lid = self.exporter.applyLightSource(lgt) RiAttributeEnd() if lid!=None: Modified: cgkit/trunk/changelog.txt =================================================================== --- cgkit/trunk/changelog.txt 2009-07-11 13:14:56 UTC (rev 328) +++ cgkit/trunk/changelog.txt 2009-08-06 20:01:27 UTC (rev 329) @@ -7,9 +7,15 @@ - New module sloargs to parse compiled RenderMan shaders. - New module pointcloud to read/write RenderMan point cloud files. +- New module sequence that contains functionality to deal with numbered + (file) sequences. There are also new utilities seqls.py, seqmv.py, seqcp.py + and seqrm.py (the equivalent of ls, mv, cp, rm but they work on file + sequences). Bug fixes/enhancements: +- ribexport: A custom rib string can now also be added to light sources + (for the image pass). - setup script: In addition to the general config file config.cfg the setup script also reads a version-specific config_pyXY.cfg script. - mat4: Wrapped the mat4 constructor that takes 16 floats as input. Because This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-07-11 13:15:00
|
Revision: 328 http://cgkit.svn.sourceforge.net/cgkit/?rev=328&view=rev Author: mbaas Date: 2009-07-11 13:14:56 +0000 (Sat, 11 Jul 2009) Log Message: ----------- Refactored and updated versions. Modified Paths: -------------- cgkit/trunk/utilities/seqls.py cgkit/trunk/utilities/seqmv.py cgkit/trunk/utilities/seqrm.py Added Paths: ----------- cgkit/trunk/utilities/seqcp.py Added: cgkit/trunk/utilities/seqcp.py =================================================================== --- cgkit/trunk/utilities/seqcp.py (rev 0) +++ cgkit/trunk/utilities/seqcp.py 2009-07-11 13:14:56 UTC (rev 328) @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# http://www.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is the Python Computer Graphics Kit. +# +# The Initial Developer of the Original Code is Matthias Baas. +# Portions created by the Initial Developer are Copyright (C) 2004 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +import sys +import optparse +from cgkit import sequence + + +def promptUser(question): + """Print a question and ask for y/n. + + Returns True when the user entered 'y'. + """ + while 1: + print question, + answer = raw_input() + answer = answer.lower() + if answer=="n": + return False + if answer=="y": + return True + print ("Expected 'y' or 'n'") + + +def main(): + """Main function. + """ + parser = optparse.OptionParser(usage="%prog [options] src dst") + parser.add_option("-s", "--source-frames", default="0-", metavar="FRAMES", help="Specify a subset of the source frames") + parser.add_option("-d", "--destination-frames", default=None, metavar="FRAMES", help="Specify the destination numbers") + parser.add_option("-f", "--force", action="store_true", default=False, help="Never query the user for confirmation") + parser.add_option("-t", "--test", action="store_true", default=False, help="Only print what would be done, but don't copy anything") + parser.add_option("-v", "--verbose", action="store_true", default=False, help="Print every file when it is copied") + opts,args = parser.parse_args() + + if len(args)!=2: + parser.print_usage() + return + + # The source frame numbers + srcRange = sequence.Range(opts.source_frames) + + # The destination frame numbers + dstRange = None + if opts.destination_frames is not None: + dstRange = sequence.Range(opts.destination_frames) + + srcSeq = args[0] + dstArg = args[1] + + # Determine the source sequences + fseqs = sequence.glob(srcSeq) + + copier = sequence.CopySequence(fseqs, dstArg, [srcRange], dstRange, verbose=opts.verbose) + + for srcSeq,dstSeq in copier.sequences(): + print ("Copy: %s -> %s"%(srcSeq, dstSeq)) + + # Check a file number would get appended to a trailing number in the base name + if copier.mergesNumbers() and not opts.force: + print ("WARNING: The destination name ends in a number which would affect the output sequence number.") + if not opts.test and not promptUser("Are you sure to continue?"): + return + + # Check if an existing file would get overwritten + overwrites = list(copier.overwrites()) + if len(overwrites)>0 and not opts.force: + print ("WARNING: %s files would get overwritten."%len(overwrites)) + if not opts.test and not promptUser("Are you sure to continue?"): + return + + # Dry run or real run... + if opts.test: + copier.dryRun() + else: + copier.run() + + # TODO: If forward/backward copy fails, the files need to be copied to a temporary sequence first and then renamed. + +########################################################################## +try: + main() +except SystemExit: + pass +except KeyboardInterrupt: + sys.stderr.write("\nUser abort\n") +except: + sys.stderr.write("ERROR: %s\n"%sys.exc_info()[1]) +# raise + sys.exit(1) Property changes on: cgkit/trunk/utilities/seqcp.py ___________________________________________________________________ Added: svn:executable + * Modified: cgkit/trunk/utilities/seqls.py =================================================================== --- cgkit/trunk/utilities/seqls.py 2009-07-11 13:14:00 UTC (rev 327) +++ cgkit/trunk/utilities/seqls.py 2009-07-11 13:14:56 UTC (rev 328) @@ -33,10 +33,9 @@ # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** -# $Id: rmshader.py,v 1.9 2006/05/26 21:33:29 mbaas Exp $ import optparse -import sys, os +import sys, os, glob import os.path import time from cgkit import sequence @@ -121,7 +120,7 @@ return "%dd %dh"%(d,h) elif h>0: return "%dh %dmin"%(h,m) - elif min>0: + elif m>0: return "%dmin %ds"%(m,s) else: return "%ss"%(s) @@ -130,25 +129,38 @@ def main(): parser = optparse.OptionParser(usage="%prog [options] paths") parser.add_option("-l", "--long", action="store_true", default=False, help="Print additional information per sequence") + parser.add_option("-d", "--directories", action="store_true", default=False, help="List directories") opts,args = parser.parse_args() if len(args)==0: args = ["*"] args.sort() + + # List directories first + if opts.directories: + for pattern in args: + for name in glob.glob("%s*"%pattern): + if os.path.isdir(name): + if opts.long: + print ("%53s/"%name) + else: + print ("%s/"%name) + + # List sequences for pattern in args: fseqs = sequence.glob(pattern) for fseq in fseqs: if opts.long: info = SequenceInfo(fseq) - print "%8s %12s - %12s %-12s %s [%d files]"%(info.sizeStr(), + print ("%8s %12s - %12s %-12s %s [%d files]"%(info.sizeStr(), info.minMTimeStr(), info.maxMTimeStr(), "(%s)"%info.timeSpanStr(), fseq, - len(fseq)) + len(fseq))) else: - print fseq + print (fseq) ########################################################################## Modified: cgkit/trunk/utilities/seqmv.py =================================================================== --- cgkit/trunk/utilities/seqmv.py 2009-07-11 13:14:00 UTC (rev 327) +++ cgkit/trunk/utilities/seqmv.py 2009-07-11 13:14:56 UTC (rev 328) @@ -33,18 +33,11 @@ # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** -# $Id: rmshader.py,v 1.9 2006/05/26 21:33:29 mbaas Exp $ import sys -import os.path -import string import optparse -import shutil from cgkit import sequence -from cgkit.sequence import SeqString -import re - def promptUser(question): """Print a question and ask for y/n. @@ -58,233 +51,18 @@ return False if answer=="y": return True - print "Expected 'y' or 'n'" + print ("Expected 'y' or 'n'") -def outputNameSpec(fileSequence, dstName, newSequenceValues, force): - """Return everything that is required to produce output names. - - newSequenceValues is a boolean indicating whether the main sequence number - will receive new values or if the values from the input sequence are used. - - The return value is a 3-tuple (dstName, numIdxs, seqNumIdx) where dstName - is the final output name that may have a substitution pattern added to it. - numIdxs is a list of indices that refer to the number in the source name - that will make it into the output name. For example, if the source files - are of the form "clip@_#" and numIdxs is [1], then this means only the - last number will be used for substitution and the final destination name - must have one substitution pattern. seqNumIdx is the index of the number - that is considered to be the main number (the index refers to the numIdxs - list, it's not the index in the source name). - - Returns None when the user didn't confirm the operation. - """ - # Get the number ranges of all numbers in the input sequence - ranges = fileSequence.ranges() - - # Create the output template - dstTemplate = sequence.SeqTemplate(dstName) - - # The index of the number that varies most (i.e. the index of the sequence number) - seqNumIdx = fileSequence.sequenceNumberIndex() - numIdxs = [] - numValues = len(ranges) - numVaryingValues = len(filter(lambda rng: len(rng)>1, ranges)) - - numIdxs = range(numValues) - - indices = dstTemplate.expressionIndices(numValues) - numPatterns = len(indices) - if numPatterns>0 and (min(indices)<0 or max(indices)>=numValues): - raise ValueError("A number pattern in the output template name refers to a non-existent source number: %s"%dstName) - - # Is the destination name without any pattern at all? Then append '#' if - # there is a unique sequence number - if numPatterns==0: - if numVaryingValues==1 or newSequenceValues: - # Check if the name ends in a number. Appending the sequence number - # would create new numbers (e.g. "clip2#" -> "clip20001", "clip20002",...) - if len(dstName)>0 and dstName[-1] in string.digits: - print "The destination name ends in a number which would affect the output sequence number." - if not force and not promptUser("Are you sure to continue?"): - return None - # Add a pattern that refers to the sequence number (+1 because the index in the pattern is 1-based) - dstTemplate = sequence.SeqTemplate(dstName+"#[%s]"%(seqNumIdx+1)) - indices = [seqNumIdx] - elif numVaryingValues!=0: - raise ValueError('Invalid destination name: "%s". Cannot figure out how to number the destination files. There are %s varying numbers.'%(dstName, numVaryingValues)) - # Do we only have as many patterns as there are *varying* numbers - # and no explicit index was specified? - # Then we can assume that the user only wants to reference the varying - # numbers and the constant numbers are just part of the name. - elif numPatterns==numVaryingValues and not dstTemplate.hasExplicitIndex: - numIdxs = [] - for i,rng in enumerate(ranges): - if len(rng)>1: - numIdxs.append(i) - # Do we have too little patterns? (and the user did not specify any - # index explicitly?) - # If so, throw an error because it's not clear which number should be - # mapped to which pattern. - elif numPatterns!=numValues and not dstTemplate.hasExplicitIndex and not newSequenceValues: - if numValues==numVaryingValues: - expectedStr = "%s pattern"%numValues - if numValues>1: - expectedStr += "s" - else: - expectedStr = "%s or %s patterns"%(numVaryingValues, numValues) - if numPatterns>numValues: - raise ValueError('Invalid destination name: "%s". There are too many substitution patterns (expected %s).'%(dstName, expectedStr)) - else: - raise ValueError('Invalid destination name: "%s". There are not enough substitution patterns (expected %s).'%(dstName, expectedStr)) - - # Recompute the index that refers to the sequence number (as we might have - # removed some numbers from the list and seqNumIdx should always refer - # to a number that is actually used in the output, so that the -d option works) - seqNumIdx = -1 - maxVal = -1 - for i,idx in enumerate(indices): - v = len(ranges[idx]) - if v>=maxVal: - seqNumIdx = i - maxVal = v - - return dstTemplate, numIdxs, seqNumIdx - -def buildFileTable(fileSequence, srcRange, dstName, dstRangeIterator, force): - """Build the file table. - - fileSequence is a Sequence object that contains the (full) source sequence. - srcRange is a Range object that determines which files from fileSequence - should be considered. - dstName is the name pattern of the output sequence. dstRangeIterator - is an iterator that yields the main output numbers. It may also be None - in which case the numbers from the source files are used. - force is a boolean indicating whether the user may get prompted for - confirmation or not. - - The return value is a list of tuples (srcName,dstName,uiSrcName,uiDstName) - where srcName/dstName is the full real path of the source/destination file. - The ui names are the ones that have been specified on the command line - which is what gets printed when the --test flag is used. - None is returned if the user didn't confirm the operation. - - The number of items in the returned list depends on the source range - and the destination range iterator. If the latter runs out of numbers, - any remaining source files are ignored. - """ - # Determine how the output files are numbered... - res = outputNameSpec(fileSequence, dstName, dstRangeIterator is not None, force) - if res is None: - return None - dstTemplate, numIdxs, seqNumIdx = res - - # Assign output names to the input names... - fileTable = [] - for srcName in fileSequence: - baseName = os.path.basename(str(srcName)) - baseName,ext = os.path.splitext(baseName) - baseName = SeqString(baseName) - allNums = baseName.getNums() - # The file numbers that are used on the output pattern - nums = map(lambda i: allNums[i], numIdxs) - - # Only queue this file when it is part of the source range - if len(nums)==0 or srcRange.contains(nums[seqNumIdx]): - # If a destination range was specified then replace the - # main file number with the next number in the range, otherwise - # the number from the input file is used - if dstRangeIterator is not None and len(nums)>0: - try: - nums[seqNumIdx] = dstRangeIterator.next() - except StopIteration: - break - # Create the file names and add them to the list - uiSrc = str(srcName) - uiDst = dstTemplate.substitute(nums) - if os.path.splitext(uiDst)[1]!=ext: - uiDst += ext - src = os.path.realpath(uiSrc) - dst = os.path.realpath(uiDst) - fileTable.append((src, dst, uiSrc, uiDst)) - - return fileTable - -def checkCollisions(fileTable, srcFiles): - """Check if moving/renaming the files would lead to collisions. - - fileTable is a list of tuples where the first two items are the - srcName and the dstName. There may be additional items per tuple which - are just ignored. - srcFiles is the list of initial files as they exist on disk (the strings - must match the srcName strings in fileTable). - """ - fileDict = {} - # Initialise the file dict with the source files - for name in srcFiles: - fileDict[name] = 1 - - # Simulate the rename operations and check if there is a collision - for item in fileTable: - srcName = item[0] - dstName = item[1] - del fileDict[srcName] - if fileDict.has_key(dstName): - return True - fileDict[dstName] = 1 - - return False - -def resolveCollisions(fileTable, srcFiles): - """Modify the file table, so that moving files doesn't resolve in collisions. - - Collisions are only checked among the file in the table, it is not checked - that a move operation would overwrite a file on disk. - Returns the new file table (the old table might have been modified!). - - srcFiles is the list of initial files as they exist on disk (the strings - must match the srcName strings in fileTable). - - Raises an exception if collisions cannot be resolved (this can happen - when the sequence contains file like img1.tif and img01.tif which might - both get mapped to the same output file name). - """ - # Check if renaming in the current order would result in a collision. - if checkCollisions(fileTable, srcFiles): - # Try the reverse order instead - fileTable.reverse() - - # If this still collides, then use a temporary name - if checkCollisions(fileTable, srcFiles): - fileTable.reverse() - tab1 = [] - tab2 = [] - for item in fileTable: - srcName = item[0] - dstName = item[1] - uiSrcName = item[2] - uiDstName = item[3] - - p,n = os.path.split(dstName) - tmpName = os.path.join(p, "__tmp__"+n) - p,n = os.path.split(uiDstName) - uiTmpName = os.path.join(p, "__tmp__"+n) - - tab1.append((srcName,tmpName,uiSrcName,uiTmpName)) - tab2.append((tmpName,dstName,uiTmpName,uiDstName)) - fileTable = tab1+tab2 - - if checkCollisions(fileTable, srcFiles): - raise ValueError("Cannot resolve collisions because of inconsistent sequence numbering. A file from the input sequence would overwrite another file from the same sequence.") - - return fileTable - def main(): + """Main function. + """ parser = optparse.OptionParser(usage="%prog [options] src dst") parser.add_option("-s", "--source-frames", default="0-", metavar="FRAMES", help="Specify a subset of the source frames") parser.add_option("-d", "--destination-frames", default=None, metavar="FRAMES", help="Specify the destination numbers") parser.add_option("-f", "--force", action="store_true", default=False, help="Never query the user for confirmation") parser.add_option("-t", "--test", action="store_true", default=False, help="Only print what would be done, but don't move anything") + parser.add_option("-v", "--verbose", action="store_true", default=False, help="Print every file when it is copied") opts,args = parser.parse_args() if len(args)!=2: @@ -295,9 +73,9 @@ srcRange = sequence.Range(opts.source_frames) # The destination frame numbers - dstRangeIterator = None + dstRange = None if opts.destination_frames is not None: - dstRangeIterator = iter(sequence.Range(opts.destination_frames)) + dstRange = sequence.Range(opts.destination_frames) srcSeq = args[0] dstArg = args[1] @@ -305,67 +83,39 @@ # Determine the source sequences fseqs = sequence.glob(srcSeq) - # Build the file table - fileTable = [] - for fseq in fseqs: - if os.path.isdir(dstArg): - dstName = os.path.join(dstArg, os.path.basename(fseq.sequenceName()[0])) - else: - dstName = dstArg - ftab = buildFileTable(fseq, srcRange, dstName, dstRangeIterator, force=opts.force) - if ftab is None: + mover = sequence.MoveSequence(fseqs, dstArg, [srcRange], dstRange, verbose=opts.verbose) + + for srcSeq,dstSeq in mover.sequences(): + print ("Move: %s -> %s"%(srcSeq, dstSeq)) + + + # Check a file number would get appended to a trailing number in the base name + if mover.mergesNumbers() and not opts.force: + print ("WARNING: The destination name ends in a number which would affect the output sequence number.") + if not opts.test and not promptUser("Are you sure to continue?"): return - fileTable.extend(ftab) - # Print the final source and destination sequence (just for user info) - if ftab!=[]: - srcSeq = sequence.Sequences(map(lambda x: x[2], ftab))[0] - dstSeq = sequence.Sequences(map(lambda x: x[3], ftab))[0] - print "Move: %s -> %s"%(srcSeq, dstSeq) - - # Resolve collisions - srcFiles = map(lambda t: t[0], fileTable) - fileTable = resolveCollisions(fileTable, srcFiles) - - # Check if a rename would overwrite an existing file that is not from the - # input sequence... - if not opts.force: - srcDict = {} - for srcName in srcFiles: - srcDict[srcName] = 1 - dstFiles = map(lambda t: t[1], fileTable) - overwrites = [] - for dstName in dstFiles: - if dstName not in srcDict and os.path.exists(dstName): - overwrites.append(dstName) - - if len(overwrites)>0: - if not promptUser("Overwrite %s files?"%len(overwrites)): - return - - # Move the files - for item in fileTable: - srcName = item[0] - dstName = item[1] - # Are the source and destination names the same? then skip this one - if srcName==dstName: - continue - - if opts.test: - uiSrcName = item[2] - uiDstName = item[3] - print uiSrcName,"->",uiDstName - else: - shutil.move(srcName, dstName) + # Check if an existing file would get overwritten + overwrites = list(mover.overwrites()) + if len(overwrites)>0 and not opts.force: + print ("WARNING: %s files would get overwritten."%len(overwrites)) + if not opts.test and not promptUser("Are you sure to continue?"): + return + # Dry run or real run... + if opts.test: + mover.dryRun() + else: + mover.run() + ########################################################################## try: main() except SystemExit: pass except KeyboardInterrupt: - print >>sys.stderr, "\nUser abort" + sys.stderr.write("\nUser abort\n") except: - print >>sys.stderr, "ERROR:",sys.exc_info()[1] + sys.stderr.write("ERROR: %s\n"%sys.exc_info()[1]) # raise sys.exit(1) Modified: cgkit/trunk/utilities/seqrm.py =================================================================== --- cgkit/trunk/utilities/seqrm.py 2009-07-11 13:14:00 UTC (rev 327) +++ cgkit/trunk/utilities/seqrm.py 2009-07-11 13:14:56 UTC (rev 328) @@ -33,7 +33,6 @@ # the terms of any one of the MPL, the GPL or the LGPL. # # ***** END LICENSE BLOCK ***** -# $Id: rmshader.py,v 1.9 2006/05/26 21:33:29 mbaas Exp $ import sys import optparse @@ -54,11 +53,12 @@ return False if answer=="y": return True - print "Expected 'y' or 'n'" + print ("Expected 'y' or 'n'") def main(): parser = optparse.OptionParser(usage="%prog [options] sequences") parser.add_option("-f", "--force", action="store_true", default=False, help="Never query the user for confirmation") + parser.add_option("-v", "--verbose", action="store_true", default=False, help="Print every file when it is deleted") opts,args = parser.parse_args() if len(args)<1: @@ -92,8 +92,10 @@ # Delete the sequences.. for fseq in sequences: - print "Deleting %s"%fseq + print ("Deleting %s"%fseq) for fileName in fseq: + if opts.verbose: + print ("Deleting %s"%fileName) os.remove(str(fileName)) return 0 @@ -107,10 +109,10 @@ except SystemExit: ret = 1 except KeyboardInterrupt: - print >>sys.stderr, "\nUser abort" + sys.stderr.write("\nUser abort\n") ret = 1 except: - print >>sys.stderr, "ERROR:",sys.exc_info()[1] + sys.stderr.write("ERROR: %s"%sys.exc_info()[1]) # raise ret = 1 This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-07-11 13:14:07
|
Revision: 327 http://cgkit.svn.sourceforge.net/cgkit/?rev=327&view=rev Author: mbaas Date: 2009-07-11 13:14:00 +0000 (Sat, 11 Jul 2009) Log Message: ----------- The latest version that has functionality for doing the mv/cp/ln operations. Modified Paths: -------------- cgkit/trunk/cgkit/sequence.py Modified: cgkit/trunk/cgkit/sequence.py =================================================================== --- cgkit/trunk/cgkit/sequence.py 2009-06-28 14:39:49 UTC (rev 326) +++ cgkit/trunk/cgkit/sequence.py 2009-07-11 13:14:00 UTC (rev 327) @@ -40,6 +40,7 @@ import re import glob as _glob import copy +import shutil class SeqString: """Sequence string class. @@ -428,8 +429,14 @@ self._value[idx2] = txt class Sequence: - """A list of names that all belong to the same sequence. + """A list of names/objects that all belong to the same sequence. + The sequence can store the original objects that are associated with a + name or it can only store the names (as SeqString) objects. If the + original objects are available or not depends on how the sequence was + built. If the getNameFunc parameter was used when building the sequence + (see Sequences), then the original objects will be available. + The class can be used like a list (using len(), index operator or iteration). """ @@ -438,6 +445,10 @@ """ # A list of file names (stored as SeqString objects) self._names = [] + + # The actual objects. This is either a list that always has as many + # items as _names or it is None. + self._objects = None def __str__(self): placeholder,ranges = self.sequenceName() @@ -447,13 +458,42 @@ return "%s (%s)"%(placeholder, "; ".join(ranges)) def __len__(self): + """Return the length of the sequence. + """ return len(self._names) - def __getitem__(self, key): - """Return an element as a SeqString. + def __getitem__(self, idx): + """Return the object at position idx. + + The return value is either the original object that was stored + in the sequence or it is a SeqString containing the name if the + original object was just a string. """ - return self._names[key] + if self._objects is None: + return self._names[idx] + else: + return self._objects[idx] + def iterNames(self): + """Iterate over the names. + + Yields SeqString objects. + """ + return iter(self._names) + + def iterObjects(self): + """Yield over the objects. + + Yields the original objects or the names as SeqStrings if the + objects haven't been stored in the sequence. + Using this method is equivalent to iterating over the sequence + object directly. + """ + if self._objects is None: + return self.iterNames() + else: + return iter(self._objects) + def match(self, name, numPos=None): """Check if a name matches the names in this sequence. @@ -465,16 +505,26 @@ else: return self._names[0].match(name, numPos) - def append(self, name): + def append(self, name, obj=None): """Add a file name to the group. name can be a SeqString object or a regular string. The name is added unconditionally, so it's the callers responsibility to make sure the file really belongs to this sequence. + + This is called internally when sequences are built! """ if not isinstance(name, SeqString): name = SeqString(name) + if obj is not None: + if self._objects is None: + if len(self._names)==0: + self._objects = [] + else: + raise ValueError("objects must be given for all or none of the names") + self._objects.append(obj) + self._names.append(name) def sequenceNumberIndex(self): @@ -607,7 +657,7 @@ """A collection of sequences. """ - def __init__(self, names=[], assumeFiles=False): + def __init__(self, names=[], assumeFiles=False, getNameFunc=None): """Constructor. names is a list of strings that will be grouped into sequences. @@ -616,7 +666,7 @@ self._sequences = [] # Create the sequences - self.setFiles(names, assumeFiles=assumeFiles) + self.setFiles(names, assumeFiles=assumeFiles, getNameFunc=getNameFunc) def __str__(self): return "<Sequences: %d sequences>"%len(self._sequences) @@ -636,7 +686,7 @@ self._sequences = [] # setFiles - def setFiles(self, names, numPos=None, assumeFiles=False): + def setFiles(self, names, numPos=None, assumeFiles=False, getNameFunc=None): """Initialize the sequences given a flat list of names. names is a list of objects (usually strings) that are turned into @@ -645,21 +695,31 @@ names. In this case, it will be ensured that files from different directories are put into different sequences and any number occurring in the directory part is "frozen" (turned into a string). + + getNameFunc can be a callable that gets called for every item in names. + The function has to return the actual name of that object. This can + be used if the input list contains object that are not strings but + some other (compound) objects. """ self.clear() - # Convert the names into SeqString objects and sort them (numerically)... + # Create the objects list which contains 2-tuples (seqString,obj). + # obj is the original object from the "names" list or None. + if getNameFunc is None: + objects = map(lambda name: (SeqString(name),None), names) + else: + objects = map(lambda obj: (SeqString(getNameFunc(obj)),obj), names) + # Sort the objects according to their seqString # The order of the result is already so that members of the same # sequence are together, we just don't know yet where a sequence ends # and the next one begins. - seqnames = map(SeqString, names) - seqnames.sort() + objects.sort(key=lambda tup: tup[0]) # Build sequences... currentSeq = Sequence() currentPath = None - for name in seqnames: + for name,obj in objects: # Are we dealing with file names? Then freeze directory numbers... if assumeFiles: path,n = os.path.split(str(name)) @@ -692,7 +752,7 @@ currentSeq = Sequence() # Add the current name to the current sequence - currentSeq.append(name) + currentSeq.append(name, obj) # Also store the last sequence generated (if it isn't empty) if len(currentSeq)>0: @@ -871,7 +931,7 @@ class SeqTemplate: """Sequence template class. - An instance of this class represens a template string to create numbered + An instance of this class represents a template string to create numbered sequences. """ @@ -1064,6 +1124,670 @@ return s, valExprs, indices, hasExplicitIndex +class OutputNameGenerator: + """Generate the file names of an output sequence based on an input sequence. + + This class produces output sequence file names that are based on an input + sequence. The class is meant to be used by applications that produce an + output file sequence based on an input sequence but where the numbers in + the output sequence may be different than the numbers in the input sequence. + For example, the class is used by the sequence utilities (seqmv, seqcp, + seqrm, seqln). + + An OutputNameGenerator has one public attribute called numberMergeFlag + which is True when the output name pattern ended in a digit but didn't + contain any number pattern. In this case, the class will append a 4-padded + number but because the name already ended in a digit, the combination + of the pattern and the number results in a larger number which is + not necessarily what the user intended. The flag can be used by an + application to check whether it should ask the user for confirmation. + """ + + def __init__(self, srcSequences, dstName, srcRanges=None, dstRange=None, keepExt=True, + enforceDstRange=False, repeatSrc=True): + """Constructor. + + srcSequences is a list of Sequence objects that contain the source + sequence files that the output sequence is based on. The structure of + the names (i.e. how many separate numbers are within a name) determines + how many number patterns the output name may have. + dstName is a string containing the name pattern for building the + output file names. The syntax of the pattern is determined by the + SeqTemplate class (i.e. you can use @ or # characters to define where + the numbers are located and what their padding is. You can also + use an index to refer to a particular number from the input sequence + and you can use expressions within curly braces). + In the simplest case, the name can just be a base name without any + special characters at all. In this case, a 4-padded number is + automatically appended which will receive the values from the + main number sequence in the input files (or the values specified by + the destination range). + + srcRanges is a list of Range objects that defines which files from the + source sequence should be considered, everything outside the range + is ignored. The numbers produced by the range object refers to the + main sequence number of the input sequence (i.e. the number that varies + fastest). If no source range is given for a particular sequence, then + all input files are considered. + + dstRange may be a Range object that provides the main sequence number + for the output names. In this case, the main number from the input + sequence is ignored (unless referenced via an expression). If no range + object is given, the numbers are taken from the input sequence. + + keepExt is a boolean that indicates whether the file name extension + should be added automatically if it isn't already part of the output + name pattern. Note that the extension is *always* added unless the + output name already contains exactly the expected extension. If the + output name contains a different extension, the old extension is still + added. So if you want to be able to let the user rename the extension, + you must set this flag to False. + + enforceDstRange is a boolean that indicates whether the number of + generated name pairs should always match the number of files indicated + by the (finite) destination range, even when the source files have + already been exhausted. The default behavior is to abort the sequence + if there are no more source files. If the destination range is infinite, + then this flag has no effect and the sequence always ends when there + are no more source files. + + repeatSrc is a flag that is only used when enforceDstRange is True + and there are fewer input files than there are values in the destination + range. If repeatSrc is True, the input sequence is repeated from the + beginning again, otherwise the last name is duplicated. + """ + if srcRanges is None: + srcRanges = [] + for seq in srcSequences: + if not isinstance(seq, Sequence): + raise TypeError("The source sequences must be Sequence objects") + if not isinstance(dstName, basestring): + raise TypeError("The output sequence pattern must be a string") + for sr in srcRanges: + if sr is not None and not isinstance(sr, Range): + raise TypeError("The source ranges must be Range objects or None") + if dstRange is not None and not isinstance(dstRange, Range): + raise TypeError("The destination range must be a Range object or None") + + # Add full range to the srcRanges list until the length is identical to + # the number of sequences. + srcRanges.extend((len(srcSequences)-len(srcRanges))*[Range("0-")]) + + if dstRange is None: + dstRangeIter = None + enforceDstRange = False + else: + # Never enforce an infinite range + if dstRange.isInfinite(): + enforceDstRange = False + + dstRangeIter = iter(dstRange) + + self._srcSequences = srcSequences + self._dstName = dstName + self._srcRanges = srcRanges + self._dstRange = dstRange + self._dstRangeIter = dstRangeIter + self._keepExt = keepExt + self._enforceDstRange = enforceDstRange + self._repeatSrc = repeatSrc + self.numberMergeFlag = False + + # Run the output preparation just to set the numberMergeFlag. + # The preparation is later done again when the user iterates over the names + for srcSeq in self._srcSequences: + self._outputNameSpec(srcSeq, dstName, dstRangeIter is not None) + + + def iterNames(self): + """Iterate over input/output name pairs. + + Yields tuples (srcName, dstName) where source name is the unmodified + name from the input sequences and dstName is the generated output name + (as specified by the output pattern and additional arguments that + were passed to the constructor). + """ + + # Iterate over all input sequences + for srcSeq,srcRange in zip(self._srcSequences, self._srcRanges): + # If the destination name refers to a directory, then use the sequence + # name of the input sequence. + if os.path.isdir(self._dstName): + dstName = os.path.join(self._dstName, os.path.basename(srcSeq.sequenceName()[0])) + else: + dstName = self._dstName + + # Create the src,dst pairs... + seqFileTable = [] + for src,dst in self._iterNames(srcSeq, dstName, srcRange, self._dstRangeIter, + self._enforceDstRange, self._repeatSrc, self._keepExt): + yield (src,dst) + + def _iterNames(self, srcSequence, dstName, srcRange, dstRangeIter, enforceDstRange, repeatSrc, keepExt): + """Iterate over input/output name pairs. + + Yields tuples (srcName, dstName) where source name is the unmodified + name from the input sequence and dstName the generated output name + (as specified by the output pattern and additional arguments that + were passed to the constructor). + """ + + # If no source files are given, then no output files can be generated + if len(srcSequence)==0: + return + + # Prepare output name generation + res = self._outputNameSpec(srcSequence, dstName, dstRangeIter is not None) + dstTemplate, numIdxs, seqNumIdx = res + + srcIter = iter(srcSequence) + + # Assign output names to the input names... + while 1: + # srcIter is only None after it was already iterated over the source + # names and repeatSrc is set to False, so that the last name + # should just be kept. + if srcIter is not None: + try: + srcName = srcIter.next() + except StopIteration: + if enforceDstRange: + if repeatSrc: + srcIter = iter(srcSequence) + srcName = srcIter.next() + else: + srcIter = None + else: + break + + srcName = str(srcName) + baseName = os.path.basename(srcName) + baseName,ext = os.path.splitext(baseName) + baseName = SeqString(baseName) + # Get all the numbers that are present in the source name + allNums = baseName.getNums() + # Only keep the numbers that are actually used in the output name + nums = map(lambda i: allNums[i], numIdxs) + + # Only queue this file when it is part of the source range + if len(nums)==0 or srcRange.contains(nums[seqNumIdx]): + # If a destination range was specified then replace the + # main file number with the next number in the range, otherwise + # the number from the input file is used + if dstRangeIter is not None and len(nums)>0: + try: + nums[seqNumIdx] = dstRangeIter.next() + except StopIteration: + break + # Create the file names and add them to the list + dstName = dstTemplate.substitute(nums) + if keepExt and os.path.splitext(dstName)[1]!=ext: + dstName += ext + yield (srcName, dstName) + + def _outputNameSpec(self, fileSequence, dstName, newSequenceValues): + """Return everything that is required to produce output names. + + newSequenceValues is a boolean indicating whether the main sequence number + will receive new values or if the values from the input sequence are used. + + The return value is a 3-tuple (dstTemplate, numIdxs, seqNumIdx) + where dstTemplate is the SeqTemplate object that has to be used to + generate the final output name. + numIdxs is a list of indices that refer to the number in the source name + that will make it into the output name. For example, if the source files + are of the form "clip@_#" and numIdxs is [1], then this means only the + last number will be used for substitution and the final destination name + must have one substitution pattern. seqNumIdx is the index of the number + that is considered to be the main number (the index refers to the numIdxs + list, it's not the index in the source name). + + The method also sets the attribute numberMergeFlag to True if it + has appended a number pattern to the output name (because none was given) + but the name ended in a digit. This means the final number will be + different than what was specified in the input arguments. The caller may + use this attribute to ask the user for confirmation. + """ + + # Get the number ranges of all numbers in the input sequence + ranges = fileSequence.ranges() + + # Create the output template + dstTemplate = SeqTemplate(dstName) + + # The index of the number that varies most (i.e. the index of the sequence number) + seqNumIdx = fileSequence.sequenceNumberIndex() + + numIdxs = [] + numValues = len(ranges) + numVaryingValues = len(filter(lambda rng: len(rng)>1, ranges)) + + numIdxs = range(numValues) + + indices = dstTemplate.expressionIndices(numValues) + numPatterns = len(indices) + if numPatterns>0 and (min(indices)<0 or max(indices)>=numValues): + raise ValueError("A number pattern in the output template name refers to a non-existent source number: %s"%dstName) + + # Is the destination name without any pattern at all? Then append '#' if + # there is a unique sequence number + if numPatterns==0: + if numVaryingValues==1 or newSequenceValues: + # Check if the name ends in a number. Appending the sequence number + # would create new numbers (e.g. "clip2#" -> "clip20001", "clip20002",...) + if len(dstName)>0 and dstName[-1] in string.digits: + self.numberMergeFlag = True + # Add a pattern that refers to the sequence number (+1 because the index in the pattern is 1-based) + dstTemplate = SeqTemplate(dstName+"#[%s]"%(seqNumIdx+1)) + indices = [seqNumIdx] +# numIdxs = [seqNumIdx] + elif numVaryingValues!=0: + raise ValueError('Invalid destination name: "%s". Cannot figure out how to number the destination files. There are %s varying numbers.'%(dstName, numVaryingValues)) + # Do we only have as many patterns as there are *varying* numbers + # and no explicit index was specified? + # Then we can assume that the user only wants to reference the varying + # numbers and the constant numbers are just part of the name. + elif numPatterns==numVaryingValues and not dstTemplate.hasExplicitIndex: + numIdxs = [] + for i,rng in enumerate(ranges): + if len(rng)>1: + numIdxs.append(i) + # Do we have too little patterns? (and the user did not specify any + # index explicitly?) + # If so, throw an error because it's not clear which number should be + # mapped to which pattern. + elif numPatterns!=numValues and not dstTemplate.hasExplicitIndex and not newSequenceValues: + if numValues==numVaryingValues: + expectedStr = "%s pattern"%numValues + if numValues>1: + expectedStr += "s" + else: + expectedStr = "%s or %s patterns"%(numVaryingValues, numValues) + if numPatterns>numValues: + raise ValueError('Invalid destination name: "%s". There are too many substitution patterns (expected %s).'%(dstName, expectedStr)) + else: + raise ValueError('Invalid destination name: "%s". There are not enough substitution patterns (expected %s).'%(dstName, expectedStr)) + + # Recompute the index that refers to the sequence number (as we might have + # removed some numbers from the list and seqNumIdx should always refer + # to a number that is actually used in the output, so that the -d option works) + seqNumIdx = -1 + maxVal = -1 +# for i,idx in enumerate(indices): + for i,idx in enumerate(numIdxs): + v = len(ranges[idx]) + if v>=maxVal: + seqNumIdx = i + maxVal = v + + return dstTemplate, numIdxs, seqNumIdx + + +class _SequenceProcessor: + """Base class for move/copy/link. + """ + + def __init__(self, srcSequences, dstName, srcRanges=None, dstRange=None, keepExt=True, enforceDstRange=False, verbose=False): + ong = OutputNameGenerator(srcSequences, + dstName, + srcRanges = srcRanges, + dstRange = dstRange, + keepExt = keepExt, + enforceDstRange = enforceDstRange) + + self._mergesNumbers = ong.numberMergeFlag + self._verbose = verbose + + # Create the file table + fileTab = [] + for uiSrc,uiDst in ong.iterNames(): + src = os.path.realpath(uiSrc) + dst = os.path.realpath(uiDst) + fileTab.append((src,dst,uiSrc,uiDst)) + + # Resolve internal collisions + srcFiles = map(lambda t: t[0], fileTab) + fileTab = self._resolveCollisions(fileTab, srcFiles) + + self._fileTab = fileTab + + def mergesNumbers(self): + """Check if a trailing number on the output sequence and a file number would get merged. + + This method returns True when the base output sequence name ends in + a number and a sequence number would be appended as well which results + in a new number (for example, writing a sequence with the base name + out2 can produce output files out20001, out20002, ... which may not + be what the user intended). The result of this call can be used to + check if the application should ask the user for confirmation. + """ + return self._mergesNumbers + + def overwrites(self): + """Iterate over all output file names that already exist on disk. + + Only iterates over the files that are not part of the input sequence. + The returned files are those that would get overwritten when the + move operation would be carried out. + This can be used to check if the user should be asked for confirmation. + """ + srcDict = {} + srcFiles = map(lambda t: t[0], self._fileTab) + for srcName in srcFiles: + srcDict[srcName] = 1 + + dstFiles = map(lambda t: t[1], self._fileTab) + overwrites = [] + for dstName in dstFiles: + if dstName not in srcDict and os.path.exists(dstName): + yield dstName + + def sequences(self): + """Iterate over the input/output sequences. + + Yields tuples (srcSeq, dstSeq) where each item is a Sequence() + object. The result can be used to show an overview of what the + move operation will do. + """ + # Print the final source and destination sequences (just for user info) + seqs = Sequences(self._fileTab, getNameFunc=lambda t:t[2]) + for srcSeq in seqs: + dstFiles = map(lambda t: t[3], srcSeq) + dstSeq = Sequences(dstFiles)[0] + yield srcSeq, dstSeq + + def dryRun(self, outStream=None): + """Print what would get done when run() was called. + + outStream is an object with a write() method that will receive + the text. If None is passed, sys.stdout is used. + """ + if outStream is None: + outStream = sys.stdout + + for src,dst,uiSrc,uiDst in self._fileTab: + if src!=dst: + outStream.write("%s -> %s\n"%(uiSrc, uiDst)) + + def run(self, outStream=None): + """Do the operation. + + outStream is an object with a write() and flush() method that will receive + the text (only in verbose mode). If None is passed, sys.stdout is used. + """ + if outStream is None: + outStream = sys.stdout + + # Execute the list + for src,dst,uiSrc,uiDst in self._fileTab: + if src!=dst: + if self._verbose: + outStream.write("%s -> %s\n"%(uiSrc, uiDst)) + outStream.flush() + self._fileOperation(src, dst) + + def _fileOperation(self, src, dst): + """Do the file operation. + + This must be implemented in a derived class. + """ + raise NotImplementedError("This method must be implemented in a derived class") + + def _resolveCollisions(self, fileTable, srcFiles): + """Modify the file table, so that moving files doesn't result in collisions. + + Collisions are only checked among the files in the table, it is not checked + that a move operation would overwrite a file on disk. + Returns the new file table (the old table might have been modified!). + + srcFiles is the list of initial files as they exist on disk (the strings + must match the srcName strings in fileTable). + + Raises an exception if collisions cannot be resolved (this can happen + when the sequence contains file like img1.tif and img01.tif which might + both get mapped to the same output file name). + + This has to be implemented in a derived class. + """ + return fileTable + + +class MoveSequence(_SequenceProcessor): + """This class moves one or more sequences of files. + """ + + def __init__(self, srcSequences, dstName, srcRanges=None, dstRange=None, keepExt=True, verbose=False): + """Constructor. + + srcSequences is a list of Sequence objects that contain the source + sequence files that the output sequence is based on. + + dstName is a string containing the name pattern for building the + output file names. The pattern may contain @ or # characters to define + where the numbers should appear and what their padding is. + You can also use an index to refer to a particular number from the + input sequence and you can use expressions within curly braces. + In the simplest case, the name can just be a base name without any + special characters at all. In this case, a 4-padded number is + automatically appended which will receive the values from the + main number sequence in the input files (or the values specified by + the destination range). + + srcRanges is a list of Range objects that defines which files from the + source sequence should be considered, everything outside the range + is ignored. The numbers produced by the range object refers to the + main sequence number of the input sequence (i.e. the number that varies + fastest). If no source range is given for a particular sequence, then + all input files are considered. + + dstRange may be a Range object that provides the main sequence number + for the output names. In this case, the main number from the input + sequence is ignored (unless referenced via an expression). If no range + object is given, the numbers are taken from the input sequence. + + keepExt is a boolean that indicates whether the file name extension + should be added automatically if it isn't already part of the output + name pattern. Note that the extension is *always* added unless the + output name already contains exactly the expected extension. If the + output name contains a different extension, the old extension is still + added. So if you want to be able to let the user rename the extension, + you must set this flag to False. + + The verbose flag determines whether each file is printed during the + actual operation. + """ + _SequenceProcessor.__init__(self, srcSequences, dstName, srcRanges, dstRange, keepExt, enforceDstRange=False, verbose=verbose) + + def _fileOperation(self, src, dst): + """Do the move operation. + """ + shutil.move(src, dst) + + def _checkCollisions(self, fileTable, srcFiles): + """Check if moving/renaming the files would lead to collisions. + + fileTable is a list of tuples where the first two items are the + srcName and the dstName. There may be additional items per tuple which + are just ignored. + srcFiles is the list of initial files as they exist on disk (the strings + must match the srcName strings in fileTable). + + Returns True when a file from the input sequence would get overwritten. + """ + fileDict = {} + # Initialize the file dict with the source files + for name in srcFiles: + fileDict[name] = 1 + + # Simulate the rename operations and check if there is a collision + for item in fileTable: + srcName = item[0] + dstName = item[1] + del fileDict[srcName] + if fileDict.has_key(dstName): + return True + fileDict[dstName] = 1 + + return False + + def _resolveCollisions(self, fileTable, srcFiles): + """Modify the file table, so that moving files doesn't result in collisions. + + Collisions are only checked among the files in the table, it is not checked + that a move operation would overwrite a file on disk. + Returns the new file table (the old table might have been modified!). + + srcFiles is the list of initial files as they exist on disk (the strings + must match the srcName strings in fileTable). + + Raises an exception if collisions cannot be resolved (this can happen + when the sequence contains file like img1.tif and img01.tif which might + both get mapped to the same output file name). + """ + # Check if renaming in the current order would result in a collision. + if self._checkCollisions(fileTable, srcFiles): + # Try the reverse order instead + fileTable.reverse() + + # If this still collides, then use a temporary name + if self._checkCollisions(fileTable, srcFiles): + fileTable.reverse() + tab1 = [] + tab2 = [] + for item in fileTable: + srcName = item[0] + dstName = item[1] + uiSrcName = item[2] + uiDstName = item[3] + + p,n = os.path.split(dstName) + tmpName = os.path.join(p, "__tmp__"+n) + p,n = os.path.split(uiDstName) + uiTmpName = os.path.join(p, "__tmp__"+n) + + tab1.append((srcName,tmpName,uiSrcName,uiTmpName)) + tab2.append((tmpName,dstName,uiTmpName,uiDstName)) + fileTable = tab1+tab2 + + if self._checkCollisions(fileTable, srcFiles): + raise ValueError("Cannot resolve collisions because of inconsistent sequence numbering. A file from the input sequence would overwrite another file from the same sequence.") + + return fileTable + + +class CopySequence(_SequenceProcessor): + """This class copies one or more sequences of files. + """ + + def __init__(self, srcSequences, dstName, srcRanges=None, dstRange=None, keepExt=True, verbose=False): + """Constructor. + + srcSequences is a list of Sequence objects that contain the source + sequence files that the output sequence is based on. + + dstName is a string containing the name pattern for building the + output file names. The pattern may contain @ or # characters to define + where the numbers should appear and what their padding is. + You can also use an index to refer to a particular number from the + input sequence and you can use expressions within curly braces. + In the simplest case, the name can just be a base name without any + special characters at all. In this case, a 4-padded number is + automatically appended which will receive the values from the + main number sequence in the input files (or the values specified by + the destination range). + + srcRanges is a list of Range objects that defines which files from the + source sequence should be considered, everything outside the range + is ignored. The numbers produced by the range object refers to the + main sequence number of the input sequence (i.e. the number that varies + fastest). If no source range is given for a particular sequence, then + all input files are considered. + + dstRange may be a Range object that provides the main sequence number + for the output names. In this case, the main number from the input + sequence is ignored (unless referenced via an expression). If no range + object is given, the numbers are taken from the input sequence. + + keepExt is a boolean that indicates whether the file name extension + should be added automatically if it isn't already part of the output + name pattern. Note that the extension is *always* added unless the + output name already contains exactly the expected extension. If the + output name contains a different extension, the old extension is still + added. So if you want to be able to let the user rename the extension, + you must set this flag to False. + + The verbose flag determines whether each file is printed during the + actual operation. + """ + _SequenceProcessor.__init__(self, srcSequences, dstName, srcRanges, dstRange, keepExt, enforceDstRange=True, verbose=verbose) + + def _fileOperation(self, src, dst): + """Do the copy operation. + """ + shutil.copy(src, dst) + + def _checkCollisions(self, fileTable, srcFiles): + """Check if copying the files would lead to collisions. + + fileTable is a list of tuples where the first two items are the + srcName and the dstName. There may be additional items per tuple which + are just ignored. + srcFiles is the list of initial files as they exist on disk (the strings + must match the srcName strings in fileTable). + """ + fileDict = {} + # Initialise the file dict with the source files + for name in srcFiles: + fileDict[name] = 1 + + # Simulate the copy operations and check if there is a collision + for item in fileTable: + srcName = item[0] + dstName = item[1] + # Check if the original source file has already been overwritten + if srcName not in fileDict: + return True + if dstName in fileDict: + del fileDict[dstName] + + return False + + def _resolveCollisions(self, fileTable, srcFiles): + """Modify the file table, so that moving files doesn't result in collisions. + + Collisions are only checked among the files in the table, it is not checked + that a move operation would overwrite a file on disk. + Returns the new file table (the old table might have been modified!). + + srcFiles is the list of initial files as they exist on disk (the strings + must match the srcName strings in fileTable). + + Raises an exception if collisions cannot be resolved (this can happen + when the sequence contains file like img1.tif and img01.tif which might + both get mapped to the same output file name). + """ + # Check if renaming in the current order would result in a collision. + if self._checkCollisions(fileTable, srcFiles): + # Try the reverse order instead + fileTable.reverse() + + # If this still collides, then use a temporary name + if self._checkCollisions(fileTable, srcFiles): + fileTable.reverse() + raise ValueError("Cannot resolve collisions because of inconsistent sequence numbering. A file from the input sequence would overwrite another file from the same sequence.") + + return fileTable + + +class SymLinkSequence(CopySequence): + """This class creates symbolic links between sequences. + """ + def _fileOperation(self, src, dst): + """Do the copy operation. + """ + os.symlink(src, dst) + + def compactRange(values): """Build the range string that lists all values in the given list in a compacted form. This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-06-28 14:40:25
|
Revision: 326 http://cgkit.svn.sourceforge.net/cgkit/?rev=326&view=rev Author: mbaas Date: 2009-06-28 14:39:49 +0000 (Sun, 28 Jun 2009) Log Message: ----------- Fixes to run the tests with Python 3 Modified Paths: -------------- cgkit/trunk/unittests/ritest.py cgkit/trunk/unittests/test_mat3.py cgkit/trunk/unittests/test_mat3_light.py cgkit/trunk/unittests/test_mat4.py cgkit/trunk/unittests/test_mat4_light.py cgkit/trunk/unittests/test_quat.py cgkit/trunk/unittests/test_quat_light.py cgkit/trunk/unittests/test_ri.py cgkit/trunk/unittests/test_vec3.py cgkit/trunk/unittests/test_vec3_light.py cgkit/trunk/unittests/test_vec4.py cgkit/trunk/unittests/test_vec4_light.py Modified: cgkit/trunk/unittests/ritest.py =================================================================== --- cgkit/trunk/unittests/ritest.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/ritest.py 2009-06-28 14:39:49 UTC (rev 326) @@ -9,7 +9,7 @@ import numpy has_numpy = True except ImportError: - print >>sys.stderr, "Warning: numpy module not found. Skipping numpy test in ritest.py." + sys.stderr.write("Warning: numpy module not found. Skipping numpy test in ritest.py.\n") has_numpy = False def normalizeRIB(inFile, outFile): @@ -186,7 +186,7 @@ ri.RiErrorHandler(ri.RiErrorIgnore) err = ri.RiLastError ri.RiOption("searchpath", "shader", "&:shaders") - ri.RiOption("render", "string bucketorder", u"horizontal") + ri.RiOption("render", "string bucketorder", "horizontal") ri.RiFormat(800, 600, 1.0) ri.RiDisplay("out.tif", ri.RI_FRAMEBUFFER, ri.RI_RGBA, "integer[2] origin", [10,10]) ri.RiCropWindow(0.25, 0.75, 0.1, 0.9) Modified: cgkit/trunk/unittests/test_mat3.py =================================================================== --- cgkit/trunk/unittests/test_mat3.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_mat3.py 2009-06-28 14:39:49 UTC (rev 326) @@ -3,7 +3,7 @@ import unittest #from cgkit.light.cgtypes import * from cgkit.cgtypes import * -import math, os, pickle, cPickle, sys +import math, os, pickle, sys from cgkit.sl import degrees, radians class TestMat3(unittest.TestCase): @@ -527,11 +527,11 @@ # have to be applied just in the opposite order than mentioned # in the fromEuler*() method name. C = R1*R2*R3 - exec 'E = mat3.fromEuler%s(angle["X"], angle["Y"], angle["Z"])'%order + exec ('E = mat3.fromEuler%s(angle["X"], angle["Y"], angle["Z"])'%order) self.assertEqual(E, C) - exec 'x,y,z = E.toEuler%s()'%order - exec 'E2 = mat3.fromEuler%s(x, y, z)'%order + exec ('x,y,z = E.toEuler%s()'%order) + exec ('E2 = mat3.fromEuler%s(x, y, z)'%order) if E2!=E: # print E # print E2 @@ -565,12 +565,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = open(fname, "wb") m = mat3(1,2,3,4,5,6,7,8,9) pickle.dump(m, f) f.close() - f = file(fname) + f = open(fname, "rb") n = pickle.load(f) f.close() Modified: cgkit/trunk/unittests/test_mat3_light.py =================================================================== --- cgkit/trunk/unittests/test_mat3_light.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_mat3_light.py 2009-06-28 14:39:49 UTC (rev 326) @@ -2,7 +2,7 @@ import unittest from cgkit.light.cgtypes import * -import math, os, pickle, cPickle, sys +import math, os, pickle, sys from cgkit.sl import degrees, radians class TestMat3(unittest.TestCase): @@ -526,11 +526,11 @@ # have to be applied just in the opposite order than mentioned # in the fromEuler*() method name. C = R1*R2*R3 - exec 'E = mat3.fromEuler%s(angle["X"], angle["Y"], angle["Z"])'%order + exec ('E = mat3.fromEuler%s(angle["X"], angle["Y"], angle["Z"])'%order) self.assertEqual(E, C) - exec 'x,y,z = E.toEuler%s()'%order - exec 'E2 = mat3.fromEuler%s(x, y, z)'%order + exec ('x,y,z = E.toEuler%s()'%order) + exec ('E2 = mat3.fromEuler%s(x, y, z)'%order) if E2!=E: # print E # print E2 @@ -564,12 +564,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = open(fname, "wb") m = mat3(1,2,3,4,5,6,7,8,9) pickle.dump(m, f) f.close() - f = file(fname) + f = open(fname, "rb") n = pickle.load(f) f.close() Modified: cgkit/trunk/unittests/test_mat4.py =================================================================== --- cgkit/trunk/unittests/test_mat4.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_mat4.py 2009-06-28 14:39:49 UTC (rev 326) @@ -3,7 +3,7 @@ import unittest from cgkit.cgtypes import * #from cgkit.light.cgtypes import * -import math, os, pickle, cPickle, sys, copy +import math, os, pickle, sys, copy class TestMat4(unittest.TestCase): @@ -603,12 +603,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = open(fname, "wb") m = mat4(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16) pickle.dump(m, f) f.close() - f = file(fname) + f = open(fname, "rb") n = pickle.load(f) f.close() Modified: cgkit/trunk/unittests/test_mat4_light.py =================================================================== --- cgkit/trunk/unittests/test_mat4_light.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_mat4_light.py 2009-06-28 14:39:49 UTC (rev 326) @@ -3,7 +3,7 @@ import unittest #from cgkit.cgtypes import * from cgkit.light.cgtypes import * -import math, os, pickle, cPickle, sys, copy +import math, os, pickle, sys, copy class TestMat4(unittest.TestCase): @@ -603,12 +603,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = open(fname, "wb") m = mat4(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16) pickle.dump(m, f) f.close() - f = file(fname) + f = open(fname, "rb") n = pickle.load(f) f.close() Modified: cgkit/trunk/unittests/test_quat.py =================================================================== --- cgkit/trunk/unittests/test_quat.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_quat.py 2009-06-28 14:39:49 UTC (rev 326) @@ -3,7 +3,7 @@ import unittest from cgkit.cgtypes import * #from cgkit.light.cgtypes import * -import math, os, pickle, cPickle +import math, os, pickle class TestQuat(unittest.TestCase): @@ -94,7 +94,7 @@ c = a*2 self.failUnless(c==quat(-2,5,6,4), "falsche Multiplikation quat*int: %s"%str(c)) - c = a*2L + c = a*2 self.failUnless(c==quat(-2,5,6,4), "falsche Multiplikation quat*long: %s"%str(c)) c = 2.0*a @@ -295,12 +295,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = open(fname, "wb") q = quat(1,2,3,4) pickle.dump(q, f) f.close() - f = file(fname) + f = open(fname, "rb") w = pickle.load(f) f.close() Modified: cgkit/trunk/unittests/test_quat_light.py =================================================================== --- cgkit/trunk/unittests/test_quat_light.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_quat_light.py 2009-06-28 14:39:49 UTC (rev 326) @@ -3,7 +3,7 @@ import unittest #from cgkit.cgtypes import * from cgkit.light.cgtypes import * -import math, os, pickle, cPickle +import math, os, pickle class TestQuat(unittest.TestCase): @@ -94,7 +94,7 @@ c = a*2 self.failUnless(c==quat(-2,5,6,4), "falsche Multiplikation quat*int: %s"%str(c)) - c = a*2L + c = a*2 self.failUnless(c==quat(-2,5,6,4), "falsche Multiplikation quat*long: %s"%str(c)) c = 2.0*a @@ -295,12 +295,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = open(fname, "wb") q = quat(1,2,3,4) pickle.dump(q, f) f.close() - f = file(fname) + f = open(fname, "rb") w = pickle.load(f) f.close() Modified: cgkit/trunk/unittests/test_ri.py =================================================================== --- cgkit/trunk/unittests/test_ri.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_ri.py 2009-06-28 14:39:49 UTC (rev 326) @@ -39,7 +39,7 @@ def testCRiModule(self): """Check the cri module.""" - print "cri test is disabled" + print ("cri test is disabled") return if os.path.exists("tmp/criout.rib"): Modified: cgkit/trunk/unittests/test_vec3.py =================================================================== --- cgkit/trunk/unittests/test_vec3.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_vec3.py 2009-06-28 14:39:49 UTC (rev 326) @@ -3,7 +3,7 @@ import unittest from cgkit.cgtypes import * #from cgkit.light.cgtypes import * -import math, os, pickle, cPickle +import math, os, pickle class TestVec3(unittest.TestCase): @@ -25,9 +25,9 @@ self.failUnless(v==vec3(2.5,2.5,2.5), "vec3(2.5) != vec3(2.5,2.5,2.5) : %s"%str(v)) - v = vec3(-1.5, 42L) + v = vec3(-1.5, 42) self.failUnless(v==vec3(-1.5, 42.0, 0.0), - "vec3(-1.5, 42L) != vec3(-1.5, 42.0, 0.0) : %s"%str(v)) + "vec3(-1.5, 42) != vec3(-1.5, 42.0, 0.0) : %s"%str(v)) v = vec3(()) self.failUnless(v==vec3(0, 0, 0), @@ -148,7 +148,7 @@ c = a*2 self.failUnless(c==vec3(-2,5,6), "falsche Multiplikation vec3*int: %s"%str(c)) - c = a*2L + c = a*2 self.failUnless(c==vec3(-2,5,6), "falsche Multiplikation vec3*long: %s"%str(c)) c = 2.0*a @@ -464,12 +464,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = open(fname, "wb") v = vec3(1, 2.5, -3.7) pickle.dump(v, f) f.close() - f = file(fname) + f = open(fname, "rb") w = pickle.load(f) f.close() Modified: cgkit/trunk/unittests/test_vec3_light.py =================================================================== --- cgkit/trunk/unittests/test_vec3_light.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_vec3_light.py 2009-06-28 14:39:49 UTC (rev 326) @@ -3,7 +3,7 @@ import unittest #from cgkit.cgtypes import * from cgkit.light.cgtypes import * -import math, os, pickle, cPickle +import math, os, pickle class TestVec3(unittest.TestCase): @@ -25,9 +25,9 @@ self.failUnless(v==vec3(2.5,2.5,2.5), "vec3(2.5) != vec3(2.5,2.5,2.5) : %s"%str(v)) - v = vec3(-1.5, 42L) + v = vec3(-1.5, 42) self.failUnless(v==vec3(-1.5, 42.0, 0.0), - "vec3(-1.5, 42L) != vec3(-1.5, 42.0, 0.0) : %s"%str(v)) + "vec3(-1.5, 42) != vec3(-1.5, 42.0, 0.0) : %s"%str(v)) v = vec3(()) self.failUnless(v==vec3(0, 0, 0), @@ -148,7 +148,7 @@ c = a*2 self.failUnless(c==vec3(-2,5,6), "falsche Multiplikation vec3*int: %s"%str(c)) - c = a*2L + c = a*2 self.failUnless(c==vec3(-2,5,6), "falsche Multiplikation vec3*long: %s"%str(c)) c = 2.0*a @@ -464,12 +464,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = open(fname, "wb") v = vec3(1, 2.5, -3.7) pickle.dump(v, f) f.close() - f = file(fname) + f = open(fname, "rb") w = pickle.load(f) f.close() Modified: cgkit/trunk/unittests/test_vec4.py =================================================================== --- cgkit/trunk/unittests/test_vec4.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_vec4.py 2009-06-28 14:39:49 UTC (rev 326) @@ -3,7 +3,7 @@ import unittest from cgkit.cgtypes import * #from cgkit.light.cgtypes import * -import math, os, pickle, cPickle +import math, os, pickle class TestVec4(unittest.TestCase): @@ -25,9 +25,9 @@ self.failUnless(v==vec4(2.5,2.5,2.5,2.5), "vec4(2.5) != (2.5,2.5,2.5,2.5) : %s"%str(v)) - v = vec4(-1.5, 42L) + v = vec4(-1.5, 42) self.failUnless(v==vec4(-1.5, 42.0, 0.0), - "vec4(-1.5, 42L) != (-1.5, 42,0,0) : %s"%str(v)) + "vec4(-1.5, 42) != (-1.5, 42,0,0) : %s"%str(v)) v = vec4(()) self.failUnless(v==vec4(0, 0, 0, 0), @@ -154,7 +154,7 @@ c = a*2 self.failUnless(c==vec4(-2,5,6,4), "falsche Multiplikation vec4*int: %s"%str(c)) - c = a*2L + c = a*2 self.failUnless(c==vec4(-2,5,6,4), "falsche Multiplikation vec4*long: %s"%str(c)) c = 2.0*a @@ -421,12 +421,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = file(fname, "wb") v = vec4(1, 2.5, -3.7, 12) pickle.dump(v, f) f.close() - f = file(fname) + f = file(fname, "rb") w = pickle.load(f) f.close() Modified: cgkit/trunk/unittests/test_vec4_light.py =================================================================== --- cgkit/trunk/unittests/test_vec4_light.py 2009-06-28 14:36:51 UTC (rev 325) +++ cgkit/trunk/unittests/test_vec4_light.py 2009-06-28 14:39:49 UTC (rev 326) @@ -3,7 +3,7 @@ import unittest #from cgkit.cgtypes import * from cgkit.light.cgtypes import * -import math, os, pickle, cPickle +import math, os, pickle class TestVec4(unittest.TestCase): @@ -25,9 +25,9 @@ self.failUnless(v==vec4(2.5,2.5,2.5,2.5), "vec4(2.5) != (2.5,2.5,2.5,2.5) : %s"%str(v)) - v = vec4(-1.5, 42L) + v = vec4(-1.5, 42) self.failUnless(v==vec4(-1.5, 42.0, 0.0), - "vec4(-1.5, 42L) != (-1.5, 42,0,0) : %s"%str(v)) + "vec4(-1.5, 42) != (-1.5, 42,0,0) : %s"%str(v)) v = vec4(()) self.failUnless(v==vec4(0, 0, 0, 0), @@ -154,7 +154,7 @@ c = a*2 self.failUnless(c==vec4(-2,5,6,4), "falsche Multiplikation vec4*int: %s"%str(c)) - c = a*2L + c = a*2 self.failUnless(c==vec4(-2,5,6,4), "falsche Multiplikation vec4*long: %s"%str(c)) c = 2.0*a @@ -421,12 +421,12 @@ def testPickle(self): fname = "delme_pickle.dat" - f = file(fname, "w") + f = file(fname, "wb") v = vec4(1, 2.5, -3.7, 12) pickle.dump(v, f) f.close() - f = file(fname) + f = file(fname, "rb") w = pickle.load(f) f.close() This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-06-28 14:37:02
|
Revision: 325 http://cgkit.svn.sourceforge.net/cgkit/?rev=325&view=rev Author: mbaas Date: 2009-06-28 14:36:51 +0000 (Sun, 28 Jun 2009) Log Message: ----------- Removed an import line (numpy was not used anyway). Modified Paths: -------------- cgkit/trunk/unittests/test_rmshader.py Modified: cgkit/trunk/unittests/test_rmshader.py =================================================================== --- cgkit/trunk/unittests/test_rmshader.py 2009-06-28 14:36:00 UTC (rev 324) +++ cgkit/trunk/unittests/test_rmshader.py 2009-06-28 14:36:51 UTC (rev 325) @@ -3,7 +3,6 @@ import unittest from cgkit.rmshader import RMShader from cgkit.cgtypes import * -import numpy class TestRMShader(unittest.TestCase): """Test the RMShader class. This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
From: <mb...@us...> - 2009-06-28 14:36:47
|
Revision: 324 http://cgkit.svn.sourceforge.net/cgkit/?rev=324&view=rev Author: mbaas Date: 2009-06-28 14:36:00 +0000 (Sun, 28 Jun 2009) Log Message: ----------- Prevent an import error when numpy is not available. Modified Paths: -------------- cgkit/trunk/unittests/test_pointcloud.py Modified: cgkit/trunk/unittests/test_pointcloud.py =================================================================== --- cgkit/trunk/unittests/test_pointcloud.py 2009-06-28 14:34:49 UTC (rev 323) +++ cgkit/trunk/unittests/test_pointcloud.py 2009-06-28 14:36:00 UTC (rev 324) @@ -5,7 +5,12 @@ from cgkit import pointcloud from cgkit.cgtypes import * import ctypes -import numpy +try: + import numpy + numpy_available = True +except ImportError: + print "Warning: numpy not available. pointcloud test incomplete." + numpy_available = False class TestPointCloud(unittest.TestCase): """Test the pointcloud module. @@ -187,6 +192,9 @@ def testMultiPointsOneBuffer(self): """Test writing/reading several points at once. """ + if not numpy_available: + return + buffer = numpy.zeros(shape=(2,8), dtype=numpy.float32) buffer[0] = (1,2,3,1,0,0,7,8) buffer[1] = (-1,-2,-3,0,0,1,2,-8) This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |