diff options
112 files changed, 3225 insertions, 1110 deletions
diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body new file mode 100644 index 0000000..3b5e0e7 --- /dev/null +++ b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/body @@ -0,0 +1 @@ +Merged from bug 597a7386-643f-4559-8dc4-6871924229b6
\ No newline at end of file diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values new file mode 100644 index 0000000..5ed19bf --- /dev/null +++ b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/144c238c-75d1-40f1-82c1-647668bcf2bc/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 04 Dec 2008 13:35:41 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body new file mode 100644 index 0000000..9106d37 --- /dev/null +++ b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/body @@ -0,0 +1,7 @@ +This is an *rst* comment.
+Which means newlines don't matter, except when they gang up.
+
+lala
+
+ - Bullet
+ - Bullet
\ No newline at end of file diff --git a/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values new file mode 100644 index 0000000..2a1c84d --- /dev/null +++ b/.be/bugs/09f84059-fc8e-4954-b24d-a2b33ef21bf4/comments/eff20807-07f0-444d-8992-f69ab3f526c5/values @@ -0,0 +1,28 @@ + + + +Content-type=text/restructured + + + + + + +Date=Thu, 06 Apr 2006 16:54:57 +0000 + + + + + + +From=abentley + + + + + + +In-reply-to=144c238c-75d1-40f1-82c1-647668bcf2bc + + + diff --git a/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body new file mode 100644 index 0000000..6f00ded --- /dev/null +++ b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/body @@ -0,0 +1,5 @@ +Aaron said this was closeable in Nov. 24th email to the BE list. + +I think "priorities" == "bug severities", in which case this +functionality is now available with the per-tree severity +configuration. diff --git a/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values new file mode 100644 index 0000000..dae549f --- /dev/null +++ b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/comments/489397bd-b987-4a08-9589-c5b71661ebb7/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 17:16:11 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values index 3a5c80c..400c6de 100644 --- a/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values +++ b/.be/bugs/16fc9496-cdc2-4c6e-9b9f-b8f483b6dedb/values @@ -1,35 +1,14 @@ +creator: abentley +severity: minor -creator=abentley +status: closed +summary: Arbitrary numerical priorities? - -severity=minor - - - - - - -status=open - - - - - - -summary=Arbitrary numerical priorities? - - - - - - -time=Wed, 04 Jan 2006 21:09:30 +0000 - - +time: Wed, 04 Jan 2006 21:09:30 +0000 diff --git a/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body new file mode 100644 index 0000000..dd40bfa --- /dev/null +++ b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/body @@ -0,0 +1 @@ +Aaron said this was closeable in Nov. 24th email to the BE list. diff --git a/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values new file mode 100644 index 0000000..ad389a7 --- /dev/null +++ b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/comments/f21bec0d-cad0-44d2-a301-bfb11adce313/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 17:21:08 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values index 402cd43..1d358cd 100644 --- a/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values +++ b/.be/bugs/2aa60b34-2c8d-4f41-bb97-a57309523262/values @@ -1,28 +1,11 @@ +creator: abentley +severity: minor -creator=abentley +status: closed - - - -severity=minor - - - - - - -status=open - - - - - - -summary=implement message-change log - - +summary: implement message-change log diff --git a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body new file mode 100644 index 0000000..708159c --- /dev/null +++ b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/body @@ -0,0 +1 @@ +Implemented diff --git a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values new file mode 100644 index 0000000..6e9546e --- /dev/null +++ b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/comments/0d8af004-8352-4254-b747-d96a40a5d457/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 17:40:08 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values index 9485ae7..8704a7e 100644 --- a/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values +++ b/.be/bugs/301724b1-3853-4aff-8f23-44373df7cf1c/values @@ -1,35 +1,14 @@ +assigned: abentley +creator: abentley -assigned=abentley +severity: minor +status: fixed - -creator=abentley - - - - - - -severity=minor - - - - - - -status=open - - - - - - -summary=Per-tree configuration: default-assigneed? - - +summary: 'Per-tree configuration: default-assigneed?' diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body new file mode 100644 index 0000000..5dde31f --- /dev/null +++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/body @@ -0,0 +1 @@ +Merged from bug 4f7a4c3b-31e3-4023-8c9d-e67f627a34f0
\ No newline at end of file diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values new file mode 100644 index 0000000..1f7615e --- /dev/null +++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/9aa88bbd-71d0-44fa-804d-3562171f9539/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 04 Dec 2008 13:44:33 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body new file mode 100644 index 0000000..f03ef32 --- /dev/null +++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/body @@ -0,0 +1 @@ +Fixed with Arch._adjust_naming_conventions on a per-tree basis instead. diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values new file mode 100644 index 0000000..e939438 --- /dev/null +++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/comments/b76434a3-5cf9-4d2c-820b-64444289c09f/values @@ -0,0 +1,28 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 04 Dec 2008 13:46:32 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + + + + +In-reply-to=9e33512e-e3cb-42ec-bc99-8e77587d0d3f + + + diff --git a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values index d17ea97..dc0b6b0 100644 --- a/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values +++ b/.be/bugs/381555eb-f2e3-4ef0-8303-d759c00b390a/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body new file mode 100644 index 0000000..ab2dc28 --- /dev/null +++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/body @@ -0,0 +1 @@ +Merged into bug ae998b27-a11b-4243-abf6-11841e5b8242
\ No newline at end of file diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values new file mode 100644 index 0000000..667dc94 --- /dev/null +++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/0ac3c4cb-90e3-4b67-b6cb-1186d5d66240/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 17:05:50 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body new file mode 100644 index 0000000..d0b8404 --- /dev/null +++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/body @@ -0,0 +1 @@ +Implemented. diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values new file mode 100644 index 0000000..225f59e --- /dev/null +++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/comments/942cd941-583d-4020-99e4-80de7e836129/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 15:42:07 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values index 5d081cf..37197a7 100644 --- a/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values +++ b/.be/bugs/4a4609c8-1882-47de-9d30-fee410b8a802/values @@ -1,35 +1,14 @@ +creator: abentley +severity: minor -creator=abentley +status: closed +summary: Do we need a severity between serious and minor? EG "Moderate"? - -severity=serious - - - - - - -status=open - - - - - - -summary=Do we need a severity between serious and minor? EG "Moderate"? - - - - - - -time=Wed, 25 Jan 2006 23:14:07 +0000 - - +time: Wed, 25 Jan 2006 23:14:07 +0000 diff --git a/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body new file mode 100644 index 0000000..fb08206 --- /dev/null +++ b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/body @@ -0,0 +1 @@ +Merged into bug 381555eb-f2e3-4ef0-8303-d759c00b390a
\ No newline at end of file diff --git a/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values new file mode 100644 index 0000000..1f7615e --- /dev/null +++ b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/comments/a8f35fca-8a15-4833-b568-326f0cc89bfa/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 04 Dec 2008 13:44:33 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values index 89ec800..85cf442 100644 --- a/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values +++ b/.be/bugs/4f7a4c3b-31e3-4023-8c9d-e67f627a34f0/values @@ -15,7 +15,7 @@ severity=minor -status=closed +status=fixed diff --git a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body new file mode 100644 index 0000000..bd80264 --- /dev/null +++ b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/body @@ -0,0 +1 @@ +Merged into bug 09f84059-fc8e-4954-b24d-a2b33ef21bf4
\ No newline at end of file diff --git a/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values new file mode 100644 index 0000000..d7e4f49 --- /dev/null +++ b/.be/bugs/597a7386-643f-4559-8dc4-6871924229b6/comments/8015d736-f3ea-4085-940c-552c01a287ef/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 04 Dec 2008 13:35:42 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body new file mode 100644 index 0000000..dd464bf --- /dev/null +++ b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/body @@ -0,0 +1,3 @@ +Per-tree severity and target are now supported. + +I'm not sure what Aaron meant be "BE ids". diff --git a/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values new file mode 100644 index 0000000..84da235 --- /dev/null +++ b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/comments/be64734c-d9a8-4f6d-83eb-e9b6c9adc0bf/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 17:29:30 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values index a9e974e..b8e8291 100644 --- a/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values +++ b/.be/bugs/68ba7f0c-ca5f-4f49-a508-e39150c07e13/values @@ -1,28 +1,11 @@ +creator: abentley +severity: wishlist -creator=abentley +status: closed - - - -severity=wishlist - - - - - - -status=open - - - - - - -summary=Support per-tree settings for severity, target, BE ids - - +summary: Support per-tree settings for severity, target, BE ids diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body new file mode 100644 index 0000000..8d1ec26 --- /dev/null +++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/body @@ -0,0 +1 @@ +A rough implemention is now sketched out in becommands/list.py diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values new file mode 100644 index 0000000..39df7ff --- /dev/null +++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/4d642e39-a8f3-41d8-93da-bea7e05ef9a6/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 27 Nov 2008 14:26:18 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body new file mode 100644 index 0000000..bb443b8 --- /dev/null +++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/body @@ -0,0 +1,15 @@ +For example: + $ be list --status --options + File "/home/wking/bin/be", line 35, in <module> + sys.exit(cmdutil.execute(sys.argv[1], sys.argv[2:])) + File "/home/wking/lib/python2.5/site-packages/libbe/cmdutil.py", line 67, in execute + get_command(cmd).execute([a.decode(enc) for a in args]) + File "/home/wking/lib/python2.5/site-packages/becommands/list.py", line 36, in execute + raise Exception, "parsed options" + Exception: parsed options + +The reason for this is that --status takes an argument, so 'be list' +thinks it should list all the bugs with status == "--options". +Ideally what should happen is that an argument-taking option would +check for argument --options, and if so, would raise an exception +returning a list of appropriate completions *for that argument*. diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values new file mode 100644 index 0000000..ea73789 --- /dev/null +++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/comments/bf0c3752-6338-4919-93ba-4c9252945fb1/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 27 Nov 2008 13:43:47 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values new file mode 100644 index 0000000..f88ca6c --- /dev/null +++ b/.be/bugs/8e1bbda4-35b6-4579-849d-117b1596ee99/values @@ -0,0 +1,35 @@ + + + +creator=W. Trevor King <wking@drexel.edu> + + + + + + +severity=serious + + + + + + +status=fixed + + + + + + +summary=be <cmmd> <argopt> --options doesn't raise GetOptions + + + + + + +time=Thu, 27 Nov 2008 13:39:25 +0000 + + + diff --git a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body new file mode 100644 index 0000000..dd9b459 --- /dev/null +++ b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/body @@ -0,0 +1,3 @@ +From the command line, + $ be show `be list --status all --uuids` | grep -A5 -B5 XYZ +works pretty well... diff --git a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values new file mode 100644 index 0000000..8270e8e --- /dev/null +++ b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/209e2a60-ddd0-4a71-90ef-e57547ed6d76/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 18:05:38 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values index 27ec173..eb5d3c0 100644 --- a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values +++ b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/comments/37650981-1908-4c39-bae2-48e69c771120/values @@ -1,21 +1,8 @@ +Content-type: text/plain - -Content-type=text/plain - - - - - - -Date=Fri, 31 Mar 2006 22:15:09 +0000 - - - - - - -From=abentley +Date: Fri, 31 Mar 2006 22:15:09 +0000 +From: abentley diff --git a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values index 800a5ce..5d35985 100644 --- a/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values +++ b/.be/bugs/9a942b1d-a3b5-441d-8aef-b844700e1efa/values @@ -1,35 +1,14 @@ +creator: abentley +severity: minor -creator=abentley +status: closed +summary: Provide search - -severity=minor - - - - - - -status=open - - - - - - -summary=Provide search - - - - - - -time=Wed, 25 Jan 2006 15:43:59 +0000 - - +time: Wed, 25 Jan 2006 15:43:59 +0000 diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body new file mode 100644 index 0000000..073f0b8 --- /dev/null +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/body @@ -0,0 +1 @@ +Merged from bug c894f10f-197d-4b22-9c5b-19f394df40d4
\ No newline at end of file diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values new file mode 100644 index 0000000..b0ecc8f --- /dev/null +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Tue, 25 Nov 2008 02:24:04 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body new file mode 100644 index 0000000..7f46872 --- /dev/null +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/body @@ -0,0 +1,17 @@ +Example: + +We're working happily in a versioned bugdir, and our RCS knows who we +are. We create a temporary repository copy from a previous revision +for diff generation. We set the RCS for the copy to "None", since we +didn't bother initializing our normal RCS in the snapshot copy. But +now the BugDir instantized on the copy doesn't know who we are! + +Solution: + +Track user id in the bugdir settings file. If you +bugdir.settings["user_id"], it will be saved and loaded. When loaded, +it will also set bugdir.user_id. If you set rcs.user_id, it will be +returned by rcs.get_user_id(), instead of returing the output of +rcs._rcs_get_user_id(). We should be caching the output of +_rcs_get_user_id() anyway. + diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values new file mode 100644 index 0000000..a93e649 --- /dev/null +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/208595bd-35b8-44c2-bf97-fc5ef9e7a58d/values @@ -0,0 +1,28 @@ + + + +Content-type=text/plain + + + + + + +Date=Sat, 22 Nov 2008 21:43:29 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + + + + +In-reply-to=0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 + + + diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body new file mode 100644 index 0000000..62c14e6 --- /dev/null +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/body @@ -0,0 +1 @@ +This bug duplicates a403de79-8f39-41f2-b9ec-15053b175ee2 diff --git a/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values new file mode 100644 index 0000000..35b6806 --- /dev/null +++ b/.be/bugs/a403de79-8f39-41f2-b9ec-15053b175ee2/comments/25c67b0b-1afd-4613-a787-e0f018614966/values @@ -0,0 +1,28 @@ + + + +Content-type=text/plain + + + + + + +Date=Sun, 23 Nov 2008 12:37:57 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + + + + +In-reply-to=0fd8ba95-d9ea-49b3-9f5a-b0eb723cdbe1 + + + diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body new file mode 100644 index 0000000..6c46db0 --- /dev/null +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/body @@ -0,0 +1 @@ +Merged from bug 4a4609c8-1882-47de-9d30-fee410b8a802
\ No newline at end of file diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values new file mode 100644 index 0000000..afd88e5 --- /dev/null +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/2628eeca-96c6-4933-8484-d55bb1dbf985/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 17:05:49 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body new file mode 100644 index 0000000..d0b8404 --- /dev/null +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/body @@ -0,0 +1 @@ +Implemented. diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values new file mode 100644 index 0000000..366395d --- /dev/null +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/942cd941-583d-4020-99e4-80de7e836129/values @@ -0,0 +1,11 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 15:42:07 +0000 + + +From: W. Trevor King <wking@drexel.edu> + + +In-reply-to: 2628eeca-96c6-4933-8484-d55bb1dbf985 + diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body new file mode 100644 index 0000000..f7659c3 --- /dev/null +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/body @@ -0,0 +1 @@ +Per-tree severity and status levels are now supported. diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values new file mode 100644 index 0000000..80e328b --- /dev/null +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/comments/ae0f9aea-960c-42b4-82df-943bbbe17d58/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 17:07:25 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values index 8a5d9e2..2f65fbc 100644 --- a/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values +++ b/.be/bugs/ae998b27-a11b-4243-abf6-11841e5b8242/values @@ -1,28 +1,11 @@ +creator: abentley +severity: minor -creator=abentley +status: fixed - - - -severity=minor - - - - - - -status=open - - - - - - -summary=Customizable severity levels? - - +summary: Customizable severity levels? diff --git a/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body new file mode 100644 index 0000000..dd40bfa --- /dev/null +++ b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/body @@ -0,0 +1 @@ +Aaron said this was closeable in Nov. 24th email to the BE list. diff --git a/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values new file mode 100644 index 0000000..1ff89fa --- /dev/null +++ b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/comments/ae56365e-7a9c-4cc3-ba67-7addbeeeff49/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Thu, 04 Dec 2008 13:48:47 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values index ebaf786..3c4e210 100644 --- a/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values +++ b/.be/bugs/b8d95763-1825-4e09-bf52-cbd884b916af/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=closed diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body new file mode 100644 index 0000000..090895e --- /dev/null +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/body @@ -0,0 +1 @@ +Merged into bug a403de79-8f39-41f2-b9ec-15053b175ee2
\ No newline at end of file diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values new file mode 100644 index 0000000..8283996 --- /dev/null +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/comments/7dfdf230-231b-43e0-9b46-58d4d18eded1/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Tue, 25 Nov 2008 02:24:05 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values index 5aed729..84a0a11 100644 --- a/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values +++ b/.be/bugs/c894f10f-197d-4b22-9c5b-19f394df40d4/values @@ -15,7 +15,7 @@ severity=minor -status=fixed +status=closed diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values index cb5a094..e964891 100644 --- a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values +++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/comments/f05359f6-1bfc-4aa6-9a6d-673516bc0f94/values @@ -1,21 +1,8 @@ +Content-type: text/plain - -Content-type=text/plain - - - - - - -Date=Sat, 15 Nov 2008 23:56:51 +0000 - - - - - - -From=wking +Date: Sat, 15 Nov 2008 23:56:51 +0000 +From: wking diff --git a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values index d6d5870..29d76c7 100644 --- a/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values +++ b/.be/bugs/cf56e648-3b09-4131-8847-02dff12b4db2/values @@ -1,35 +1,15 @@ +creator: abentley +severity: critical -creator=abentley +status: closed +summary: OK, maybe not fatal, but how about a new name that suggests process tracking, + not just bugs? - -severity=critical - - - - - - -status=open - - - - - - -summary=OK, maybe not fatal, but how about a new name that suggests process tracking, not just bugs? - - - - - - -time=Fri, 27 Jan 2006 14:37:25 +0000 - - +time: Fri, 27 Jan 2006 14:37:25 +0000 diff --git a/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values b/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values index ac013c5..608b460 100644 --- a/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values +++ b/.be/bugs/e2f6514c-5f9f-4734-a537-daf3fbe7e9a0/values @@ -15,7 +15,7 @@ severity=minor -status=open +status=fixed diff --git a/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body new file mode 100644 index 0000000..dd40bfa --- /dev/null +++ b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/body @@ -0,0 +1 @@ +Aaron said this was closeable in Nov. 24th email to the BE list. diff --git a/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values new file mode 100644 index 0000000..7bf391a --- /dev/null +++ b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/comments/24903c62-f441-496e-9dcf-17e7a581df33/values @@ -0,0 +1,8 @@ +Content-type: text/plain + + +Date: Thu, 04 Dec 2008 17:20:20 +0000 + + +From: W. Trevor King <wking@drexel.edu> + diff --git a/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values index 7305e89..dde51b9 100644 --- a/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values +++ b/.be/bugs/f70dd5df-805b-49f3-a9ce-12e0fae63365/values @@ -1,35 +1,14 @@ +creator: abentley +severity: minor -creator=abentley +status: closed +summary: Allow different sorts - -severity=minor - - - - - - -status=open - - - - - - -summary=Allow different sorts - - - - - - -time=Wed, 25 Jan 2006 15:43:19 +0000 - - +time: Wed, 25 Jan 2006 15:43:19 +0000 diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body new file mode 100644 index 0000000..02bbe3a --- /dev/null +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/body @@ -0,0 +1,5 @@ +Wrote/borrowed libbe/encoding.py. +Now the following works: + +python -c 'import libbe.encoding as e; print e.get_encoding(); e.set_IO_stream_encodings(e.get_encoding()) ;print u"\u2019"' | cat + diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values new file mode 100644 index 0000000..eb56317 --- /dev/null +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/028d2e8d-5b0f-4c43-a913-35a1709b2276/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Tue, 25 Nov 2008 19:41:02 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body new file mode 100644 index 0000000..d97791d --- /dev/null +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/body @@ -0,0 +1,14 @@ +$ be show 31cd490d-a1c2-4ab3-8284-d80395e34dd2 + +works as expected, but + +$ be show 31cd490d-a1c2-4ab3-8284-d80395e34dd2 | grep something +Traceback (most recent call last): + File "/home/wking/bin/be", line 30, in <module> + sys.exit(cmdutil.execute(sys.argv[1], sys.argv[2:])) + File "/home/wking/src/fun/be-bugfix/libbe/cmdutil.py", line 57, in execute + File "/home/wking/src/fun/be/be.wtk/becommands/show.py", line 44, in execute + print bug.string(show_comments=True) +UnicodeEncodeError: 'ascii' codec can't encode character u'\u2019' in position 2100: ordinal not in range(128) + +By the way, u2019 is a fancy apostrophe. diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values new file mode 100644 index 0000000..f976972 --- /dev/null +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/15602c0c-25e4-4c2c-9e24-79bdb90721b1/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Tue, 25 Nov 2008 02:36:16 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body new file mode 100644 index 0000000..7bb09ff --- /dev/null +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/body @@ -0,0 +1,9 @@ +Solution here +http://www.amk.ca/python/howto/unicode + +You need to encode before printing. + +This is unfortunate, because we're currently very glib about just +printing info to the terminal. This makes it much more important to +have a single bugdir-wide encoding specification... + diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values new file mode 100644 index 0000000..bf5085b --- /dev/null +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/comments/3f556a48-c538-4569-8609-3e829b561d78/values @@ -0,0 +1,21 @@ + + + +Content-type=text/plain + + + + + + +Date=Tue, 25 Nov 2008 03:02:59 +0000 + + + + + + +From=W. Trevor King <wking@drexel.edu> + + + diff --git a/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values new file mode 100644 index 0000000..e710d29 --- /dev/null +++ b/.be/bugs/f7ccd916-b5c7-4890-a2e3-8c8ace17ae3a/values @@ -0,0 +1,35 @@ + + + +creator=W. Trevor King <wking@drexel.edu> + + + + + + +severity=minor + + + + + + +status=fixed + + + + + + +summary=UTF-8 encoding trouble with pipes in becommands/show + + + + + + +time=Tue, 25 Nov 2008 02:30:35 +0000 + + + diff --git a/.be/settings b/.be/settings index 47dda13..a9bd6dd 100644 --- a/.be/settings +++ b/.be/settings @@ -1,7 +1,13 @@ - - - -rcs_name=bzr - - +inactive_status: +- - closed + - The bug is no longer relevant. +- - fixed + - The bug should no longer occur. +- - wontfix + - It's not a bug, it's a feature. +- - disabled + - Unknown meaning. For backwards compatibility with old BE bugs. + + +rcs_name: bzr @@ -14,6 +14,10 @@ provide the following elements: The entry function for your plugin. args is everything from sys.argv after the name of your plugin (e.g. for the command `be open abc', args=['abc']). + + Note: be supports command-completion. To avoid raising errors you + need to deal with possible '--complete' options and arguments. + See the 'Command completion' section below for more information. help() Return the string to be output by `be help <yourplugin>', `be <yourplugin> --help', etc. @@ -26,3 +30,40 @@ consistent interface alter the parser (e.g. add some more options) before returning it. Again, you can just browse around in becommands to get a feel for things. + +Testing +------- + +Run any doctests in your plugin with + be$ python test.py <yourplugin> +for example + be$ python test.py merge + + +Command completion +------------------ + +BE implements a general framework to make it easy to support command +completion for arbitrary plugins. In order to support this system, +all becommands should properly handle the '--complete' commandline +argument, returning a list of possible completions. For example + $ be --commands + lists options accepted by be and the names of all available becommands. + $ be list --commands + lists options accepted by becommand/list + $ be list --status --commands + lists arguments accepted by the becommand/list --status option + $ be show -- --commands + lists possible vals for the first positional argument of becommand/show +This is a lot of information, but command-line completion is really +convenient for the user. See becommand/list.py and becommand/show.py +for example implementations. The basic idea is to raise + cmdutil.GetCompletions(['list','of','possible','completions']) +once you've determined what that list should be. + +However, command completion is not critical. The first priority is to +implement the target functionality, with fancy shell sugar coming +later. In recognition of this, cmdutil provides the default_complete +function which ensures that if '--complete' is any one of the +arguments, options, or option-arguments, GetCompletions will be raised +with and empty list. @@ -24,6 +24,10 @@ __doc__ == cmdutil.help() if len(sys.argv) == 1 or sys.argv[1] in ('--help', '-h'): print cmdutil.help() +elif sys.argv[1] == '--complete': + for command, module in cmdutil.iter_commands(): + print command + print '\n'.join(["--help","--complete","--options"]) else: try: try: @@ -33,9 +37,15 @@ else: except cmdutil.GetHelp: print cmdutil.help(sys.argv[1]) sys.exit(0) - except cmdutil.UsageError: + except cmdutil.GetCompletions, e: + print '\n'.join(e.completions) + sys.exit(0) + except cmdutil.UsageError, e: + print "Invalid usage:", e + print "\nArgs:", sys.argv[1:] print cmdutil.help(sys.argv[1]) sys.exit(1) except cmdutil.UserError, e: + print "ERROR:" print e sys.exit(1) diff --git a/becommands/assign.py b/becommands/assign.py index cb732b3..2f9ff21 100644 --- a/becommands/assign.py +++ b/becommands/assign.py @@ -15,40 +15,43 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Assign an individual or group to fix a bug""" -from libbe import cmdutil, bugdir +from libbe import cmdutil, bugdir, settings_object __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> bd.bug_from_shortname("a").assigned is None + >>> bd.bug_from_shortname("a").assigned is settings_object.EMPTY True - >>> execute(["a"]) + >>> execute(["a"], test=True) >>> bd._clear_bugs() >>> bd.bug_from_shortname("a").assigned == bd.user_id True - >>> execute(["a", "someone"]) + >>> execute(["a", "someone"], test=True) >>> bd._clear_bugs() >>> print bd.bug_from_shortname("a").assigned someone - >>> execute(["a","none"]) + >>> execute(["a","none"], test=True) >>> bd._clear_bugs() - >>> bd.bug_from_shortname("a").assigned is None + >>> bd.bug_from_shortname("a").assigned is settings_object.EMPTY True """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) assert(len(args) in (0, 1, 2)) if len(args) == 0: - raise cmdutil.UserError("Please specify a bug id.") + raise cmdutil.UsageError("Please specify a bug id.") if len(args) > 2: help() - raise cmdutil.UserError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: bug.assigned = bd.user_id diff --git a/becommands/close.py b/becommands/close.py index 8d2ccdb..d8826b0 100644 --- a/becommands/close.py +++ b/becommands/close.py @@ -18,7 +18,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> from libbe import bugdir >>> import os @@ -26,18 +26,20 @@ def execute(args): >>> os.chdir(bd.root) >>> print bd.bug_from_shortname("a").status open - >>> execute(["a"]) + >>> execute(["a"], test=True) >>> bd._clear_bugs() >>> print bd.bug_from_shortname("a").status closed """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) if len(args) == 0: - raise cmdutil.UserError("Please specify a bug id.") + raise cmdutil.UsageError("Please specify a bug id.") if len(args) > 1: - help() - raise cmdutil.UserError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(args[0]) bug.status = "closed" bd.save() diff --git a/becommands/comment.py b/becommands/comment.py index 172f818..b15a06e 100644 --- a/becommands/comment.py +++ b/becommands/comment.py @@ -15,19 +15,19 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Add a comment to a bug""" -from libbe import cmdutil, bugdir, utility +from libbe import cmdutil, bugdir, settings_object, editor import os __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import time >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["a", "This is a comment about a"]) + >>> execute(["a", "This is a comment about a"], test=True) >>> bd._clear_bugs() >>> bug = bd.bug_from_shortname("a") - >>> bug.load_comments() + >>> bug.load_comments(load_full=False) >>> comment = bug.comment_root[0] >>> print comment.body This is a comment about a @@ -36,31 +36,32 @@ def execute(args): True >>> comment.time <= int(time.time()) True - >>> comment.in_reply_to is None + >>> comment.in_reply_to is settings_object.EMPTY True >>> if 'EDITOR' in os.environ: ... del os.environ["EDITOR"] - >>> execute(["b"]) + >>> execute(["b"], test=True) Traceback (most recent call last): UserError: No comment supplied, and EDITOR not specified. >>> os.environ["EDITOR"] = "echo 'I like cheese' > " - >>> execute(["b"]) + >>> execute(["b"], test=True) >>> bd._clear_bugs() >>> bug = bd.bug_from_shortname("b") - >>> bug.load_comments() + >>> bug.load_comments(load_full=False) >>> comment = bug.comment_root[0] >>> print comment.body I like cheese <BLANKLINE> """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) if len(args) == 0: - raise cmdutil.UserError("Please specify a bug or comment id.") + raise cmdutil.UsageError("Please specify a bug or comment id.") if len(args) > 2: - help() - raise cmdutil.UserError("Too many arguments.") + raise cmdutil.UsageError("Too many arguments.") shortname = args[0] if shortname.count(':') > 1: @@ -73,20 +74,20 @@ def execute(args): bugname = shortname is_reply = False - bd = bugdir.BugDir(from_disk=True) + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(bugname) - bug.load_comments() + bug.load_comments(load_full=False) if is_reply: - parent = bug.comment_root.comment_from_shortname(shortname, bug_shortname=bugname) + parent = bug.comment_root.comment_from_shortname(shortname, + bug_shortname=bugname) else: parent = bug.comment_root if len(args) == 1: try: - body = utility.editor_string("Please enter your comment above") - except utility.CantFindEditor: - raise cmdutil.UserError( - "No comment supplied, and EDITOR not specified.") + body = editor.editor_string("Please enter your comment above") + except editor.CantFindEditor, e: + raise cmdutil.UserError, "No comment supplied, and EDITOR not specified." if body is None: raise cmdutil.UserError("No comment entered.") body = body.decode('utf-8') @@ -107,8 +108,41 @@ To add a comment to a bug, use the bug ID as the argument. To reply to another comment, specify the comment name (as shown in "be show" output). $EDITOR is used to launch an editor. If unspecified, no comment will be -created.) +created. """ def help(): return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + if pos == 0: # fist positional argument is a bug or comment id + if len(args) >= 2: + partial = args[1].split(':')[0] # take only bugid portion + else: + partial = "" + ids = [] + try: + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + bugs = [] + for uuid in bd.list_uuids(): + if uuid.startswith(partial): + bug = bd.bug_from_uuid(uuid) + if bug.active == True: + bugs.append(bug) + for bug in bugs: + shortname = bd.bug_shortname(bug) + ids.append(shortname) + bug.load_comments(load_full=False) + for id,comment in bug.comment_shortnames(shortname): + ids.append(id) + except bugdir.NoBugDir: + pass + raise cmdutil.GetCompletions(ids) + raise cmdutil.GetCompletions() diff --git a/becommands/diff.py b/becommands/diff.py index 77194ff..c090fa8 100644 --- a/becommands/diff.py +++ b/becommands/diff.py @@ -20,7 +20,7 @@ from libbe import cmdutil, bugdir, diff import os __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() @@ -31,7 +31,7 @@ def execute(args): >>> changed = bd.rcs.commit("Closed bug a") >>> os.chdir(bd.root) >>> if bd.rcs.versioned == True: - ... execute([original]) + ... execute([original], test=True) ... else: ... print "a:cm: Bug A\\nstatus: open -> closed\\n" Modified bug reports: @@ -39,25 +39,52 @@ def execute(args): status: open -> closed <BLANKLINE> """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) if len(args) == 0: revision = None if len(args) == 1: revision = args[0] if len(args) > 1: - help() - raise cmdutil.UserError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError("Too many arguments.") + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) if bd.rcs.versioned == False: print "This directory is not revision-controlled." else: old_bd = bd.duplicate_bugdir(revision) r,m,a = diff.diff(old_bd, bd) - diff.diff_report((r,m,a), bd) + + optbugs = [] + if options.all == True: + options.new = options.modified = options.removed = True + if options.new == True: + optbugs.extend(a) + if options.modified == True: + optbugs.extend([new for old,new in m]) + if options.removed == True: + optbugs.extend(r) + if len(optbugs) > 0: + for bug in optbugs: + print bug.uuid + else : + print diff.diff_report((r,m,a), bd).encode(bd.encoding) bd.remove_duplicate_bugdir() def get_parser(): - parser = cmdutil.CmdOptionParser("be diff [SPECIFIER]") + parser = cmdutil.CmdOptionParser("be diff [options] REVISION") + # boolean options + bools = (("n", "new", "Print UUIDS for new bugs"), + ("m", "modified", "Print UUIDS for modified bugs"), + ("r", "removed", "Print UUIDS for removed bugs"), + ("a", "all", "Print UUIDS for all changed bugs")) + for s in bools: + attr = s[1].replace('-','_') + short = "-%c" % s[0] + long = "--%s" % s[1] + help = s[2] + parser.add_option(short, long, action="store_true", + dest=attr, help=help) return parser longhelp=""" @@ -65,7 +92,11 @@ Uses the RCS to compare the current tree with a previous tree, and prints a pretty report. If specifier is given, it is a specifier for the particular previous tree to use. Specifiers are specific to their RCS. -For Arch: a fully-qualified revision name +For Arch your specifier must be a fully-qualified revision name. + +Besides the standard summary output, you can use the options to output +UUIDS for the different categories. This output can be used as the +input to 'be show' to get and understanding of the current status. """ def help(): diff --git a/becommands/help.py b/becommands/help.py index bf0b4fc..7e0209d 100644 --- a/becommands/help.py +++ b/becommands/help.py @@ -21,20 +21,28 @@ __desc__ = __doc__ def execute(args): """ Print help of specified command. + >>> execute(["help"]) + Usage: be help [COMMAND] + <BLANKLINE> + Options: + -h, --help Print a help message + --complete Print a list of available completions + <BLANKLINE> + Print help for specified command or list of all commands. + <BLANKLINE> """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) if len(args) > 1: - raise cmdutil.UserError("Too many arguments.") + raise cmdutil.UsageError("Too many arguments.") if len(args) == 0: print cmdutil.help() else: try: print cmdutil.help(args[0]) except AttributeError: - print "No help available" - - return - + print "No help available" def get_parser(): parser = cmdutil.CmdOptionParser("be help [COMMAND]") @@ -46,3 +54,12 @@ Print help for specified command or list of all commands. def help(): return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + if "--complete" in args: + cmds = [command for command,module in cmdutil.iter_commands()] + raise cmdutil.GetCompletions(cmds) diff --git a/becommands/list.py b/becommands/list.py index 63e1cd6..8c69eaa 100644 --- a/becommands/list.py +++ b/becommands/list.py @@ -15,39 +15,39 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """List bugs""" -from libbe import cmdutil, bugdir -from libbe.bug import cmp_full, severity_values, status_values, \ - active_status_values, inactive_status_values +from libbe import cmdutil, bugdir, bug import os __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute([]) + >>> execute([], test=True) a:om: Bug A - >>> execute(["--status", "all"]) + >>> execute(["--status", "all"], test=True) a:om: Bug A b:cm: Bug B """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) if len(args) > 0: - help() - raise cmdutil.UserError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bd.load_all_bugs() # select status if options.status != None: if options.status == "all": - status = status_values + status = bug.status_values else: status = options.status.split(',') else: status = [] if options.active == True: - status.extend(list(active_status_values)) + status.extend(list(bug.active_status_values)) if options.unconfirmed == True: status.append("unconfirmed") if options.open == True: @@ -55,11 +55,11 @@ def execute(args): if options.test == True: status.append("test") if status == []: # set the default value - status = active_status_values + status = bug.active_status_values # select severity if options.severity != None: if options.severity == "all": - severity = severity_values + severity = bug.severity_values else: severity = options.severity.split(',') else: @@ -67,10 +67,10 @@ def execute(args): if options.wishlist == True: severity.extend("wishlist") if options.important == True: - serious = severity_values.index("serious") - severity.append(list(severity_values[serious:])) + serious = bug.severity_values.index("serious") + severity.append(list(bug.severity_values[serious:])) if severity == []: # set the default value - severity = severity_values + severity = bug.severity_values # select assigned if options.assigned != None: if options.assigned == "all": @@ -114,15 +114,18 @@ def execute(args): if len(bugs) == 0: print "No matching bugs found" - def list_bugs(cur_bugs, title=None, no_target=False): - cur_bugs.sort(cmp_full) + def list_bugs(cur_bugs, title=None, just_uuids=False): + cur_bugs.sort(bug.cmp_full) if len(cur_bugs) > 0: if title != None: print cmdutil.underlined(title) - for bug in cur_bugs: - print bug.string(shortlist=True) + for bg in cur_bugs: + if just_uuids: + print bg.uuid + else: + print bg.string(shortlist=True) - list_bugs(bugs, no_target=False) + list_bugs(bugs, just_uuids=options.uuids) def get_parser(): parser = cmdutil.CmdOptionParser("be list [options]") @@ -134,11 +137,12 @@ def get_parser(): help="List options matching ASSIGNED", default=None) parser.add_option("-t", "--target", metavar="TARGET", dest="target", help="List options matching TARGET", default=None) - # boolean shortucts. All of these are special cases of long forms - bools = (("w", "wishlist", "List bugs with 'wishlist' severity"), + # boolean options. All but uuids are special cases of long forms + bools = (("u", "uuids", "Only print the bug UUIDS"), + ("w", "wishlist", "List bugs with 'wishlist' severity"), ("i", "important", "List bugs with >= 'serious' severity"), ("A", "active", "List all active bugs"), - ("u", "unconfirmed", "List unconfirmed bugs"), + ("U", "unconfirmed", "List unconfirmed bugs"), ("o", "open", "List open bugs"), ("T", "test", "List bugs in testing"), ("m", "mine", "List bugs assigned to you"), @@ -152,9 +156,20 @@ def get_parser(): dest=attr, help=help) return parser -longhelp=""" -This command lists bugs. There are several criteria that you can -search by: + +def help(): + longhelp=""" +This command lists bugs. Normally it prints a short string like + 576:om: Allow attachments +Where + 576 the bug id + o the bug status is 'open' (first letter) + m the bug severity is 'minor' (first letter) + Allo... the bug summary string + +You can optionally (-u) print only the bug ids. + +There are several criteria that you can filter by: * status * severity * assigned (who the bug is assigned to) @@ -174,8 +189,17 @@ target In addition, there are some shortcut options that set boolean flags. The boolean options are ignored if the matching string option is used. -""" % (','.join(status_values), - ','.join(severity_values)) - -def help(): +""" % (','.join(bug.status_values), + ','.join(bug.severity_values)) return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + if option == "status": + raise cmdutil.GetCompletions(bug.status_values) + elif option == "severity": + raise cmdutil.GetCompletions(bug.severity_values) + raise cmdutil.GetCompletions() + if "--complete" in args: + raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/becommands/merge.py b/becommands/merge.py new file mode 100644 index 0000000..927bb63 --- /dev/null +++ b/becommands/merge.py @@ -0,0 +1,164 @@ +# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc. +# <abentley@panoramicfeedback.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +"""Merge duplicate bugs""" +from libbe import cmdutil, bugdir +import os, copy +__desc__ = __doc__ + +def execute(args, test=False): + """ + >>> from libbe import utility + >>> bd = bugdir.simple_bug_dir() + >>> a = bd.bug_from_shortname("a") + >>> a.comment_root.time = 0 + >>> dummy = a.new_comment("Testing") + >>> dummy.time = 1 + >>> dummy = dummy.new_reply("Testing...") + >>> dummy.time = 2 + >>> b = bd.bug_from_shortname("b") + >>> b.status = "open" + >>> b.comment_root.time = 0 + >>> dummy = b.new_comment("1 2") + >>> dummy.time = 1 + >>> dummy = dummy.new_reply("1 2 3 4") + >>> dummy.time = 2 + >>> bd.save() + >>> os.chdir(bd.root) + >>> execute(["a", "b"], test=True) + Merging bugs a and b + >>> bd._clear_bugs() + >>> a = bd.bug_from_shortname("a") + >>> a.load_comments() + >>> mergeA = a.comment_from_shortname(":3") + >>> mergeA.time = 3 + >>> print a.string(show_comments=True) + ID : a + Short name : a + Severity : minor + Status : open + Assigned : + Target : + Reporter : + Creator : John Doe <jdoe@example.com> + Created : Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000) + Bug A + --------- Comment --------- + Name: a:1 + From: wking <wking@thor.yang.physics.drexel.edu> + Date: Thu, 01 Jan 1970 00:00:01 +0000 + <BLANKLINE> + Testing + --------- Comment --------- + Name: a:2 + From: wking <wking@thor.yang.physics.drexel.edu> + Date: Thu, 01 Jan 1970 00:00:02 +0000 + <BLANKLINE> + Testing... + --------- Comment --------- + Name: a:3 + From: wking <wking@thor.yang.physics.drexel.edu> + Date: Thu, 01 Jan 1970 00:00:03 +0000 + <BLANKLINE> + Merged from bug b + --------- Comment --------- + Name: a:4 + From: wking <wking@thor.yang.physics.drexel.edu> + Date: Thu, 01 Jan 1970 00:00:01 +0000 + <BLANKLINE> + 1 2 + --------- Comment --------- + Name: a:5 + From: wking <wking@thor.yang.physics.drexel.edu> + Date: Thu, 01 Jan 1970 00:00:02 +0000 + <BLANKLINE> + 1 2 3 4 + >>> b = bd.bug_from_shortname("b") + >>> b.load_comments() + >>> mergeB = b.comment_from_shortname(":3") + >>> mergeB.time = 3 + >>> print b.string(show_comments=True) + ID : b + Short name : b + Severity : minor + Status : closed + Assigned : + Target : + Reporter : + Creator : Jane Doe <jdoe@example.com> + Created : Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000) + Bug B + --------- Comment --------- + Name: b:1 + From: wking <wking@thor.yang.physics.drexel.edu> + Date: Thu, 01 Jan 1970 00:00:01 +0000 + <BLANKLINE> + 1 2 + --------- Comment --------- + Name: b:2 + From: wking <wking@thor.yang.physics.drexel.edu> + Date: Thu, 01 Jan 1970 00:00:02 +0000 + <BLANKLINE> + 1 2 3 4 + --------- Comment --------- + Name: b:3 + From: wking <wking@thor.yang.physics.drexel.edu> + Date: Thu, 01 Jan 1970 00:00:03 +0000 + <BLANKLINE> + Merged into bug a + >>> print b.status + closed + """ + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True, + 1: lambda bug : bug.active==True}) + + if len(args) < 2: + raise cmdutil.UsageError("Please specify two bug ids.") + if len(args) > 2: + help() + raise cmdutil.UsageError("Too many arguments.") + + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) + bugA = bd.bug_from_shortname(args[0]) + bugA.load_comments() + bugB = bd.bug_from_shortname(args[1]) + bugB.load_comments() + mergeA = bugA.new_comment("Merged from bug %s" % bugB.uuid) + newCommTree = copy.deepcopy(bugB.comment_root) + for comment in newCommTree.traverse(): + comment.bug = bugA + for comment in newCommTree: + mergeA.add_reply(comment, allow_time_inversion=True) + bugB.new_comment("Merged into bug %s" % bugA.uuid) + bugB.status = "closed" + bd.save() + print "Merging bugs %s and %s" % (bugA.uuid, bugB.uuid) + +def get_parser(): + parser = cmdutil.CmdOptionParser("be merge BUG-ID BUG-ID") + return parser + +longhelp=""" +The second bug (B) is merged into the first (A). This adds merge +comments to both bugs, closes B, and appends B's comment tree to A's +merge comment. +""" + +def help(): + return get_parser().help_str() + longhelp diff --git a/becommands/new.py b/becommands/new.py index caa1549..1c5246c 100644 --- a/becommands/new.py +++ b/becommands/new.py @@ -15,39 +15,53 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Create a new bug""" -from libbe import cmdutil, bugdir +from libbe import cmdutil, bugdir, settings_object __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import os, time >>> from libbe import bug >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) >>> bug.uuid_gen = lambda: "X" - >>> execute (["this is a test",]) + >>> execute (["this is a test",], test=True) Created bug with ID X >>> bd.load() >>> bug = bd.bug_from_uuid("X") - >>> bug.summary - u'this is a test' + >>> print bug.summary + this is a test >>> bug.time <= int(time.time()) True - >>> bug.severity - u'minor' - >>> bug.target == None + >>> print bug.severity + minor + >>> bug.target == settings_object.EMPTY True """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) if len(args) != 1: - raise cmdutil.UserError("Please supply a summary message") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError("Please supply a summary message") + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.new_bug(summary=args[0]) + if options.reporter != None: + bug.reporter = options.reporter + else: + bug.reporter = bug.creator + if options.assigned != None: + bug.assigned = options.assigned + elif bd.default_assignee != settings_object.EMPTY: + bug.assigned = bd.default_assignee bd.save() print "Created bug with ID %s" % bd.bug_shortname(bug) def get_parser(): parser = cmdutil.CmdOptionParser("be new SUMMARY") + parser.add_option("-r", "--reporter", metavar="REPORTER", dest="reporter", + help="The user who reported the bug", default=None) + parser.add_option("-a", "--assigned", metavar="ASSIGNED", dest="assigned", + help="The developer in charge of the bug", default=None) return parser longhelp=""" diff --git a/becommands/open.py b/becommands/open.py index 788a183..7a18fd0 100644 --- a/becommands/open.py +++ b/becommands/open.py @@ -18,25 +18,27 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) >>> print bd.bug_from_shortname("b").status closed - >>> execute(["b"]) + >>> execute(["b"], test=True) >>> bd._clear_bugs() >>> print bd.bug_from_shortname("b").status open """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==False}) if len(args) == 0: - raise cmdutil.UserError("Please specify a bug id.") + raise cmdutil.UsageError, "Please specify a bug id." if len(args) > 1: - help() - raise cmdutil.UserError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError, "Too many arguments." + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(args[0]) bug.status = "open" bd.save() diff --git a/becommands/remove.py b/becommands/remove.py index 8f7c2c6..fa264b8 100644 --- a/becommands/remove.py +++ b/becommands/remove.py @@ -18,7 +18,7 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> from libbe import mapfile >>> import os @@ -26,7 +26,7 @@ def execute(args): >>> os.chdir(bd.root) >>> print bd.bug_from_shortname("b").status closed - >>> execute (["b"]) + >>> execute (["b"], test=True) Removed bug b >>> bd._clear_bugs() >>> try: @@ -35,10 +35,13 @@ def execute(args): ... print "Bug not found" Bug not found """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) if len(args) != 1: - raise cmdutil.UserError("Please specify a bug id.") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError, "Please specify a bug id." + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(args[0]) bd.remove_bug(bug) bd.save() diff --git a/becommands/set.py b/becommands/set.py index 287ceb4..b8a125e 100644 --- a/becommands/set.py +++ b/becommands/set.py @@ -15,38 +15,60 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Change tree settings""" -from libbe import cmdutil, bugdir +from libbe import cmdutil, bugdir, settings_object __desc__ = __doc__ -def execute(args): +def _value_string(bd, setting): + val = bd.settings.get(setting, settings_object.EMPTY) + if val == settings_object.EMPTY: + default = getattr(bd, bd._setting_name_to_attr_name(setting)) + if default != settings_object.EMPTY: + val = "None (%s)" % default + else: + val = None + return str(val) + +def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["target"]) + >>> execute(["target"], test=True) None - >>> execute(["target", "tomorrow"]) - >>> execute(["target"]) + >>> execute(["target", "tomorrow"], test=True) + >>> execute(["target"], test=True) tomorrow - >>> execute(["target", "none"]) - >>> execute(["target"]) + >>> execute(["target", "none"], test=True) + >>> execute(["target"], test=True) None """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) if len(args) > 2: - help() - raise cmdutil.UserError("Too many arguments.") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError, "Too many arguments" + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) if len(args) == 0: - keys = bd.settings.keys() + keys = bd.settings_properties keys.sort() for key in keys: - print "%16s: %s" % (key, bd.settings[key]) + print "%16s: %s" % (key, _value_string(bd, key)) elif len(args) == 1: - print bd.settings.get(args[0]) + print _value_string(bd, args[0]) else: if args[1] != "none": - bd.settings[args[0]] = args[1] + if args[0] not in bd.settings_properties: + msg = "Invalid setting %s\n" % args[0] + msg += 'Allowed settings:\n ' + msg += '\n '.join(bd.settings_properties) + raise cmdutil.UserError(msg) + old_setting = bd.settings.get(args[0]) + try: + setattr(bd, args[0], args[1]) + except bugdir.InvalidValue, e: + bd.settings[args[0]] = old_setting + bd.save() + raise cmdutil.UserError(e) else: del bd.settings[args[0]] bd.save() @@ -73,3 +95,15 @@ To unset a setting, set it to "none". def help(): return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option, value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + if pos == 0: # first positional argument is a setting name + props = bugdir.BugDir.settings_properties + raise cmdutil.GetCompletions(props) + raise cmdutil.GetCompletions() # no positional arguments for list diff --git a/becommands/set_root.py b/becommands/set_root.py index e17bd87..3749e28 100644 --- a/becommands/set_root.py +++ b/becommands/set_root.py @@ -19,7 +19,7 @@ import os.path from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> from libbe import utility, rcs >>> import os @@ -29,7 +29,7 @@ def execute(args): ... except bugdir.NoBugDir, e: ... True True - >>> execute([dir.path]) + >>> execute([dir.path], test=True) No revision control detected. Directory initialized. >>> del(dir) @@ -40,34 +40,33 @@ def execute(args): >>> rcs.init('.') >>> print rcs.name Arch - >>> execute([]) + >>> execute([], test=True) Using Arch for revision control. Directory initialized. >>> rcs.cleanup() >>> try: - ... execute(['.']) + ... execute(['.'], test=True) ... except cmdutil.UserError, e: ... str(e).startswith("Directory already initialized: ") True - >>> execute(['/highly-unlikely-to-exist']) + >>> execute(['/highly-unlikely-to-exist'], test=True) Traceback (most recent call last): UserError: No such directory: /highly-unlikely-to-exist >>> os.chdir('/') """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser) if len(args) > 1: - print help() - raise cmdutil.UserError, "Too many arguments" + raise cmdutil.UsageError if len(args) == 1: basedir = args[0] else: basedir = "." - if os.path.exists(basedir) == False: - pass - #raise cmdutil.UserError, "No such directory: %s" % basedir try: - bd = bugdir.BugDir(basedir, from_disk=False, sink_to_existing_root=False, assert_new_BugDir=True) + bd = bugdir.BugDir(basedir, from_disk=False, sink_to_existing_root=False, assert_new_BugDir=True, + manipulate_encodings=not test) except bugdir.NoRootEntry: raise cmdutil.UserError("No such directory: %s" % basedir) except bugdir.AlreadyInitialized: diff --git a/becommands/severity.py b/becommands/severity.py index 3adefaa..5d27222 100644 --- a/becommands/severity.py +++ b/becommands/severity.py @@ -15,29 +15,29 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Show or change a bug's severity level""" -from libbe import cmdutil, bugdir -from libbe.bug import severity_values, severity_description +from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["a"]) + >>> execute(["a"], test=True) minor - >>> execute(["a", "wishlist"]) - >>> execute(["a"]) + >>> execute(["a", "wishlist"], test=True) + >>> execute(["a"], test=True) wishlist - >>> execute(["a", "none"]) + >>> execute(["a", "none"], test=True) Traceback (most recent call last): UserError: Invalid severity level: none """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) if len(args) not in (1,2): - print help() - return - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: print bug.severity @@ -46,7 +46,7 @@ def execute(args): bug.severity = args[1] except ValueError, e: if e.name != "severity": - raise + raise e raise cmdutil.UserError ("Invalid severity level: %s" % e.value) bd.save() @@ -54,7 +54,8 @@ def get_parser(): parser = cmdutil.CmdOptionParser("be severity BUG-ID [SEVERITY]") return parser -longhelp=[""" +def help(): + longhelp=[""" Show or change a bug's severity level. If no severity is specified, the current value is printed. If a severity level @@ -62,13 +63,38 @@ is specified, it will be assigned to the bug. Severity levels are: """] -longest_severity_len = max([len(s) for s in severity_values]) -for severity in severity_values : - description = severity_description[severity] - s = "%*s : %s\n" % (longest_severity_len, severity, description) - longhelp.append(s) -longhelp = ''.join(longhelp) - - -def help(): + try: # See if there are any per-tree severity configurations + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) + except bugdir.NoBugDir, e: + pass # No tree, just show the defaults + longest_severity_len = max([len(s) for s in bug.severity_values]) + for severity in bug.severity_values : + description = bug.severity_description[severity] + s = "%*s : %s\n" % (longest_severity_len, severity, description) + longhelp.append(s) + longhelp = ''.join(longhelp) return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + try: # See if there are any per-tree severity configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir: + bd = None + if pos == 0: # fist positional argument is a bug id + ids = [] + if bd != None: + bd.load_all_bugs() + filter = lambda bg : bg.active==True + bugs = [bg for bg in bd if filter(bg)==True] + ids = [bd.bug_shortname(bg) for bg in bugs] + raise cmdutil.GetCompletions(ids) + elif pos == 1: # second positional argument is a severity + raise cmdutil.GetCompletions(bug.severity_values) + raise cmdutil.GetCompletions() diff --git a/becommands/show.py b/becommands/show.py index 1ee354c..7c48257 100644 --- a/becommands/show.py +++ b/becommands/show.py @@ -18,33 +18,52 @@ from libbe import cmdutil, bugdir __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute (["a",]) + >>> execute (["a",], test=True) ID : a Short name : a Severity : minor Status : open Assigned : Target : + Reporter : Creator : John Doe <jdoe@example.com> Created : Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000) Bug A <BLANKLINE> + >>> execute (["--xml", "a"], test=True) + <bug> + <uuid>a</uuid> + <short-name>a</short-name> + <severity>minor</severity> + <status>open</status> + <assigned><class 'libbe.settings_object.EMPTY'></assigned> + <target><class 'libbe.settings_object.EMPTY'></target> + <reporter><class 'libbe.settings_object.EMPTY'></reporter> + <creator>John Doe <jdoe@example.com></creator> + <created>Wed, 31 Dec 1969 19:00 (Thu, 01 Jan 1970 00:00:00 +0000)</created> + <summary>Bug A</summary> + </bug> """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) if len(args) == 0: - raise cmdutil.UserError("Please specify a bug id.") - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) for bugid in args: bug = bd.bug_from_shortname(bugid) if options.dumpXML: print bug.xml(show_comments=True) else: print bug.string(show_comments=True) + if bugid != args[-1]: + print "" # add a blank line between bugs def get_parser(): parser = cmdutil.CmdOptionParser("be show [options] BUG-ID [BUG-ID ...]") diff --git a/becommands/status.py b/becommands/status.py index a30b3d6..40e9b51 100644 --- a/becommands/status.py +++ b/becommands/status.py @@ -15,33 +15,33 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Show or change a bug's status""" -from libbe import cmdutil, bugdir -from libbe.bug import status_values, status_description +from libbe import cmdutil, bugdir, bug __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["a"]) + >>> execute(["a"], test=True) open - >>> execute(["a", "closed"]) - >>> execute(["a"]) + >>> execute(["a", "closed"], test=True) + >>> execute(["a"], test=True) closed - >>> execute(["a", "none"]) + >>> execute(["a", "none"], test=True) Traceback (most recent call last): UserError: Invalid status: none """ - options, args = get_parser().parse_args(args) + parser = get_parser() + options, args = parser.parse_args(args) + complete(options, args, parser) if len(args) not in (1,2): - print help() - return - bd = bugdir.BugDir(from_disk=True) + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: print bug.status - elif len(args) == 2: + else: try: bug.status = args[1] except ValueError, e: @@ -54,20 +54,46 @@ def get_parser(): parser = cmdutil.CmdOptionParser("be status BUG-ID [STATUS]") return parser -longhelp=[""" -Show or change a bug's severity level. -If no severity is specified, the current value is printed. If a severity level +def help(): + longhelp=[""" +Show or change a bug's status. + +If no status is specified, the current value is printed. If a status is specified, it will be assigned to the bug. -Severity levels are: +Status levels are: """] -longest_status_len = max([len(s) for s in status_values]) -for status in status_values : - description = status_description[status] - s = "%*s : %s\n" % (longest_status_len, status, description) - longhelp.append(s) -longhelp = ''.join(longhelp) - -def help(): + try: # See if there are any per-tree status configurations + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=False) + except bugdir.NoBugDir, e: + pass # No tree, just show the defaults + longest_status_len = max([len(s) for s in bug.status_values]) + for status in bug.status_values : + description = bug.status_description[status] + s = "%*s : %s\n" % (longest_status_len, status, description) + longhelp.append(s) + longhelp = ''.join(longhelp) return get_parser().help_str() + longhelp + +def complete(options, args, parser): + for option,value in cmdutil.option_value_pairs(options, parser): + if value == "--complete": + # no argument-options at the moment, so this is future-proofing + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + try: # See if there are any per-tree status configurations + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + except bugdir.NoBugDir: + bd = None + if pos == 0: # fist positional argument is a bug id + ids = [] + if bd != None: + bd.load_all_bugs() + ids = [bd.bug_shortname(bg) for bg in bd] + raise cmdutil.GetCompletions(ids) + elif pos == 1: # second positional argument is a status + raise cmdutil.GetCompletions(bug.status_values) + raise cmdutil.GetCompletions() diff --git a/becommands/target.py b/becommands/target.py index dce100f..c83ffa7 100644 --- a/becommands/target.py +++ b/becommands/target.py @@ -15,36 +15,38 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Show or change a bug's target for fixing""" -from libbe import cmdutil, bugdir +from libbe import cmdutil, bugdir, settings_object __desc__ = __doc__ -def execute(args): +def execute(args, test=False): """ >>> import os >>> bd = bugdir.simple_bug_dir() >>> os.chdir(bd.root) - >>> execute(["a"]) + >>> execute(["a"], test=True) No target assigned. - >>> execute(["a", "tomorrow"]) - >>> execute(["a"]) + >>> execute(["a", "tomorrow"], test=True) + >>> execute(["a"], test=True) tomorrow - >>> execute(["a", "none"]) - >>> execute(["a"]) + >>> execute(["a", "none"], test=True) + >>> execute(["a"], test=True) No target assigned. """ - options, args = get_parser().parse_args(args) - assert(len(args) in (0, 1, 2)) - if len(args) == 0: - print help() - return - bd = bugdir.BugDir(from_disk=True) + parser = get_parser() + options, args = parser.parse_args(args) + cmdutil.default_complete(options, args, parser, + bugid_args={0: lambda bug : bug.active==True}) + if len(args) not in (1, 2): + raise cmdutil.UsageError + bd = bugdir.BugDir(from_disk=True, manipulate_encodings=not test) bug = bd.bug_from_shortname(args[0]) if len(args) == 1: - if bug.target is None: + if bug.target is None or bug.target is settings_object.EMPTY: print "No target assigned." else: print bug.target - elif len(args) == 2: + else: + assert len(args) == 2 if args[1] == "none": bug.target = None else: diff --git a/libbe/arch.py b/libbe/arch.py index fd953a4..1173535 100644 --- a/libbe/arch.py +++ b/libbe/arch.py @@ -14,10 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import codecs import os +import re import shutil import time -import re import unittest import doctest @@ -133,13 +134,16 @@ class Arch(RCS): """ tagpath = os.path.join(path, "{arch}", "=tagging-method") lines_out = [] - for line in file(tagpath, "rb"): - line.decode("utf-8") + f = codecs.open(tagpath, "r", self.encoding) + for line in f: if line.startswith("source "): lines_out.append("source ^[._=a-zA-X0-9].*$\n") else: lines_out.append(line) - file(tagpath, "wb").write("".join(lines_out).encode("utf-8")) + f.close() + f = codecs.open(tagpath, "w", self.encoding) + f.write("".join(lines_out)) + f.close() def _add_project_code(self, path): # http://mwolson.org/projects/GettingStartedWithArch.html @@ -215,7 +219,9 @@ class Arch(RCS): return [os.path.join(root, p) for p in inv_str.split('\n')] def _add_dir_rule(self, rule, dirname, root): inv_path = os.path.join(dirname, '.arch-inventory') - file(inv_path, "ab").write(rule) + f = codecs.open(inv_path, "a", self.encoding) + f.write(rule) + f.close() if os.path.realpath(inv_path) not in self._list_added(root): paranoid = self.paranoid self.paranoid = False @@ -233,12 +239,16 @@ class Arch(RCS): pass def _rcs_get_file_contents(self, path, revision=None): if revision == None: - return file(self._u_abspath(path), "rb").read() + return RCS._rcs_get_file_contents(self, path, revision) else: status,output,error = \ self._invoke_client("file-find", path, revision) - path = output.rstrip('\n') - return file(self._u_abspath(path), "rb").read() + relpath = output.rstrip('\n') + abspath = os.path.join(self.rootdir, relpath) + f = codecs.open(abspath, "r", self.encoding) + contents = f.read() + f.close() + return contents def _rcs_duplicate_repo(self, directory, revision=None): if revision == None: RCS._rcs_duplicate_repo(self, directory, revision) diff --git a/libbe/bug.py b/libbe/bug.py index afa9e09..fe059fa 100644 --- a/libbe/bug.py +++ b/libbe/bug.py @@ -21,6 +21,10 @@ import time import doctest from beuuid import uuid_gen +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, cached_property, \ + primed_property, change_hook_property, settings_property +import settings_object import mapfile import comment import utility @@ -30,9 +34,9 @@ import utility # Use a tuple of (category, description) tuples since we don't have # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/ -# in order of increasing severity -severity_level_def = ( - ("wishlist","A feature that could improve usefullness, but not a bug."), +# in order of increasing severity. (name, description) pairs +severity_def = ( + ("wishlist","A feature that could improve usefulness, but not a bug."), ("minor","The standard bug level."), ("serious","A bug that requires workarounds."), ("critical","A bug that prevents some features from working at all."), @@ -48,86 +52,189 @@ active_status_def = ( inactive_status_def = ( ("closed", "The bug is no longer relevant."), ("fixed", "The bug should no longer occur."), - ("wontfix","It's not a bug, it's a feature."), - ("disabled", "?")) + ("wontfix","It's not a bug, it's a feature.")) ### Convert the description tuples to more useful formats -severity_values = tuple([val for val,description in severity_level_def]) -severity_description = dict(severity_level_def) +severity_values = () +severity_description = {} severity_index = {} -for i in range(len(severity_values)): - severity_index[severity_values[i]] = i - -active_status_values = tuple(val for val,description in active_status_def) -inactive_status_values = tuple(val for val,description in inactive_status_def) -status_values = active_status_values + inactive_status_values -status_description = dict(active_status_def+inactive_status_def) +def load_severities(severity_def): + global severity_values + global severity_description + global severity_index + if severity_def == settings_object.EMPTY: + return + severity_values = tuple([val for val,description in severity_def]) + severity_description = dict(severity_def) + severity_index = {} + for i,severity in enumerate(severity_values): + severity_index[severity] = i +load_severities(severity_def) + +active_status_values = [] +inactive_status_values = [] +status_values = [] +status_description = {} status_index = {} -for i in range(len(status_values)): - status_index[status_values[i]] = i - - -def checked_property(name, valid): +def load_status(active_status_def, inactive_status_def): + global active_status_values + global inactive_status_values + global status_values + global status_description + global status_index + if active_status_def == settings_object.EMPTY: + active_status_def = globals()["active_status_def"] + if inactive_status_def == settings_object.EMPTY: + inactive_status_def = globals()["inactive_status_def"] + active_status_values = tuple([val for val,description in active_status_def]) + inactive_status_values = tuple([val for val,description in inactive_status_def]) + status_values = active_status_values + inactive_status_values + status_description = dict(tuple(active_status_def) + tuple(inactive_status_def)) + status_index = {} + for i,status in enumerate(status_values): + status_index[status] = i +load_status(active_status_def, inactive_status_def) + + +class Bug(settings_object.SavedSettingsObject): """ - Provide access to an attribute name, testing for valid values. + >>> b = Bug() + >>> print b.status + open + >>> print b.severity + minor + + There are two formats for time, int and string. Setting either + one will adjust the other appropriately. The string form is the + one stored in the bug's settings file on disk. + >>> print type(b.time) + <type 'int'> + >>> print type(b.time_string) + <type 'str'> + >>> b.time = 0 + >>> print b.time_string + Thu, 01 Jan 1970 00:00:00 +0000 + >>> b.time_string="Thu, 01 Jan 1970 00:01:00 +0000" + >>> b.time + 60 + >>> print b.settings["time"] + Thu, 01 Jan 1970 00:01:00 +0000 """ - def getter(self): - value = getattr(self, "_"+name) - if value not in valid: - raise InvalidValue(name, value) - return value - - def setter(self, value): - if value not in valid: - raise InvalidValue(name, value) - return setattr(self, "_"+name, value) - return property(getter, setter) + settings_properties = [] + required_saved_properties = [] + _prop_save_settings = settings_object.prop_save_settings + _prop_load_settings = settings_object.prop_load_settings + def _versioned_property(settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"]=required_saved_properties + return settings_object.versioned_property(**kwargs) + + @_versioned_property(name="severity", + doc="A measure of the bug's importance", + default="minor", + check_fn=lambda s: s in severity_values, + require_save=True) + def severity(): return {} + + @_versioned_property(name="status", + doc="The bug's current status", + default="open", + check_fn=lambda s: s in status_values, + require_save=True) + def status(): return {} + + @property + def active(self): + return self.status in active_status_values + @_versioned_property(name="target", + doc="The deadline for fixing this bug") + def target(): return {} + + @_versioned_property(name="creator", + doc="The user who entered the bug into the system") + def creator(): return {} + + @_versioned_property(name="reporter", + doc="The user who reported the bug") + def reporter(): return {} + + @_versioned_property(name="assigned", + doc="The developer in charge of the bug") + def assigned(): return {} + + @_versioned_property(name="time", + doc="An RFC 2822 timestamp for bug creation") + def time_string(): return {} + + def _get_time(self): + if self.time_string == None or self.time_string == settings_object.EMPTY: + return None + return utility.str_to_time(self.time_string) + def _set_time(self, value): + self.time_string = utility.time_to_str(value) + time = property(fget=_get_time, + fset=_set_time, + doc="An integer version of .time_string") + + @_versioned_property(name="summary", + doc="A one-line bug description") + def summary(): return {} + + def _get_comment_root(self, load_full=False): + if self.sync_with_disk: + return comment.loadComments(self, load_full=load_full) + else: + return comment.Comment(self, uuid=comment.INVALID_UUID) -class Bug(object): - severity = checked_property("severity", severity_values) - status = checked_property("status", status_values) + @Property + @cached_property(generator=_get_comment_root) + @local_property("comment_root") + @doc_property(doc="The trunk of the comment tree") + def comment_root(): return {} - def _get_active(self): - return self.status in active_status_values + def _get_rcs(self): + if hasattr(self.bugdir, "rcs"): + return self.bugdir.rcs - active = property(_get_active) + @Property + @cached_property(generator=_get_rcs) + @local_property("rcs") + @doc_property(doc="A revision control system instance.") + def rcs(): return {} def __init__(self, bugdir=None, uuid=None, from_disk=False, load_comments=False, summary=None): + settings_object.SavedSettingsObject.__init__(self) self.bugdir = bugdir - if bugdir != None: - self.rcs = bugdir.rcs - else: - self.rcs = None + self.uuid = uuid if from_disk == True: - self._comments_loaded = False - self.uuid = uuid - self.load(load_comments=load_comments) + self.sync_with_disk = True else: - # Note: defaults should match those in Bug.load() - self._comments_loaded = True - if uuid != None: - self.uuid = uuid - else: + self.sync_with_disk = False + if uuid == None: self.uuid = uuid_gen() - self.summary = summary + self.time = int(time.time()) # only save to second precision if self.rcs != None: self.creator = self.rcs.get_user_id() - else: - self.creator = None - self.target = None - self.status = "open" - self.severity = "minor" - self.assigned = None - self.time = int(time.time()) # only save to second precision - self.comment_root = comment.Comment(self, uuid=comment.INVALID_UUID) + self.summary = summary def __repr__(self): return "Bug(uuid=%r)" % self.uuid + def _setting_attr_string(self, setting): + value = getattr(self, setting) + if value == settings_object.EMPTY: + return "" + else: + return str(value) + def xml(self, show_comments=False): if self.bugdir == None: shortname = self.uuid @@ -147,6 +254,7 @@ class Bug(object): ("status", self.status), ("assigned", self.assigned), ("target", self.target), + ("reporter", self.reporter), ("creator", self.creator), ("created", timestring), ("summary", self.summary)] @@ -155,9 +263,7 @@ class Bug(object): if v is not None: ret += ' <%s>%s</%s>\n' % (k,v,k) - if show_comments: - if self._comments_loaded == False: - self.load_comments() + if show_comments == True: comout = self.comment_root.xml_thread(auto_name_map=True, bug_shortname=shortname) ret += comout @@ -171,27 +277,20 @@ class Bug(object): else: shortname = self.bugdir.bug_shortname(self) if shortlist == False: - if self.time == None: - timestring = "" + if self.time_string == "": + timestring = self.time_string else: htime = utility.handy_time(self.time) - ftime = utility.time_to_str(self.time) - timestring = "%s (%s)" % (htime, ftime) + timestring = "%s (%s)" % (htime, self.time_string) info = [("ID", self.uuid), ("Short name", shortname), ("Severity", self.severity), ("Status", self.status), - ("Assigned", self.assigned), - ("Target", self.target), - ("Creator", self.creator), + ("Assigned", self._setting_attr_string("assigned")), + ("Target", self._setting_attr_string("target")), + ("Reporter", self._setting_attr_string("reporter")), + ("Creator", self._setting_attr_string("creator")), ("Created", timestring)] - newinfo = [] - for k,v in info: - if v == None: - newinfo.append((k,"")) - else: - newinfo.append((k,v)) - info = newinfo longest_key_len = max([len(k) for k,v in info]) infolines = [" %*s : %s\n" %(longest_key_len,k,v) for k,v in info] bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n') @@ -199,12 +298,13 @@ class Bug(object): statuschar = self.status[0] severitychar = self.severity[0] chars = "%c%c" % (statuschar, severitychar) - bugout = "%s:%s: %s" % (shortname, chars, self.summary.rstrip('\n')) + bugout = "%s:%s: %s" % (shortname,chars,self.summary.rstrip('\n')) if show_comments == True: - if self._comments_loaded == False: - self.load_comments() - comout = self.comment_root.string_thread(auto_name_map=True, + # take advantage of the string_thread(auto_name_map=True) + # SIDE-EFFECT of sorting by comment time. + comout = self.comment_root.string_thread(flatten=False, + auto_name_map=True, bug_shortname=shortname) output = bugout + '\n' + comout.rstrip('\n') else : @@ -224,75 +324,67 @@ class Bug(object): assert name in ["values", "comments"] return os.path.join(my_dir, name) - def load(self, load_comments=False): - map = mapfile.map_load(self.rcs, self.get_path("values")) - self.summary = map.get("summary") - self.creator = map.get("creator") - self.target = map.get("target") - self.status = map.get("status", "open") - self.severity = map.get("severity", "minor") - self.assigned = map.get("assigned") - self.time = map.get("time") - if self.time is not None: - self.time = utility.str_to_time(self.time) - - if load_comments == True: - self.load_comments() - - def load_comments(self): - self.comment_root = comment.loadComments(self) - self._comments_loaded = True + def load_settings(self): + self.settings = mapfile.map_load(self.rcs, self.get_path("values")) + self._setup_saved_settings() - def comments(self): - if self._comments_loaded == False: - self.load_comments() - for comment in self.comment_root.traverse(): - yield comment - - def _add_attr(self, map, name): - value = getattr(self, name) - if value is not None: - map[name] = value - - def save(self): + def load_comments(self, load_full=True): + if load_full == True: + # Force a complete load of the whole comment tree + self.comment_root = self._get_comment_root(load_full=True) + else: + # Setup for fresh lazy-loading. Clear _comment_root, so + # _get_comment_root returns a fresh version. Turn of + # syncing temporarily so we don't write our blank comment + # tree to disk. + self.sync_with_disk = False + self.comment_root = None + self.sync_with_disk = True + + def save_settings(self): assert self.summary != None, "Can't save blank bug" - map = {} - self._add_attr(map, "assigned") - self._add_attr(map, "summary") - self._add_attr(map, "creator") - self._add_attr(map, "target") - self._add_attr(map, "status") - self._add_attr(map, "severity") - if self.time is not None: - map["time"] = utility.time_to_str(self.time) - + self.rcs.mkdir(self.get_path()) path = self.get_path("values") - mapfile.map_save(self.rcs, path, map) + mapfile.map_save(self.rcs, path, self._get_saved_settings()) + + def save(self): + self.save_settings() - if self._comments_loaded: - if len(self.comment_root) > 0: - self.rcs.mkdir(self.get_path("comments")) - comment.saveComments(self) + if len(self.comment_root) > 0: + self.rcs.mkdir(self.get_path("comments")) + comment.saveComments(self) def remove(self): - self.load_comments() self.comment_root.remove() path = self.get_path() self.rcs.recursive_remove(path) + def comments(self): + for comment in self.comment_root.traverse(): + yield comment + def new_comment(self, body=None): - comm = comment.comment_root.new_reply(body=body) + comm = self.comment_root.new_reply(body=body) return comm def comment_from_shortname(self, shortname, *args, **kwargs): - return self.comment_root.comment_from_shortname(shortname, *args, **kwargs) + return self.comment_root.comment_from_shortname(shortname, + *args, **kwargs) def comment_from_uuid(self, uuid): return self.comment_root.comment_from_uuid(uuid) + def comment_shortnames(self, shortname=None): + """ + SIDE-EFFECT : Comment.comment_shortnames will sort the comment + tree by comment.time + """ + for id, comment in self.comment_root.comment_shortnames(shortname): + yield (id, comment) -# the general rule for bug sorting is that "more important" bugs are + +# The general rule for bug sorting is that "more important" bugs are # less than "less important" bugs. This way sorting a list of bugs # will put the most important bugs first in the list. When relative # importance is unclear, the sorting follows some arbitrary convention @@ -359,10 +451,15 @@ def cmp_attr(bug_1, bug_2, attr, invert=False): """ if not hasattr(bug_2, attr) : return 1 + val_1 = getattr(bug_1, attr) + val_2 = getattr(bug_2, attr) + if val_1 == settings_object.EMPTY: val_1 = None + if val_2 == settings_object.EMPTY: val_2 = None + if invert == True : - return -cmp(getattr(bug_1, attr), getattr(bug_2, attr)) + return -cmp(val_1, val_2) else : - return cmp(getattr(bug_1, attr), getattr(bug_2, attr)) + return cmp(val_1, val_2) # alphabetical rankings (a < z) cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator") diff --git a/libbe/bugdir.py b/libbe/bugdir.py index 7e4cf3e..7885224 100644 --- a/libbe/bugdir.py +++ b/libbe/bugdir.py @@ -22,10 +22,16 @@ import copy import unittest import doctest +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, fn_checked_property, \ + cached_property, primed_property, change_hook_property, \ + settings_property +import settings_object import mapfile import bug -import utility import rcs +import encoding +import utility class NoBugDir(Exception): @@ -64,28 +70,29 @@ class MultipleBugMatches(ValueError): TREE_VERSION_STRING = "Bugs Everywhere Tree 1 0\n" -def setting_property(name, valid=None, doc=None): - def getter(self): - value = self.settings.get(name) - if valid is not None: - if value not in valid and value != None: - raise InvalidValue(name, value) - return value +class BugDir (list, settings_object.SavedSettingsObject): + """ + Sink to existing root + ====================== - def setter(self, value): - if value != getter(self): - if value is None: - del self.settings[name] - else: - self.settings[name] = value - self._save_settings(self.get_path("settings"), self.settings) + Consider the following usage case: + You have a bug directory rooted in + /path/to/source + by which I mean the '.be' directory is at + /path/to/source/.be + However, you're of in some subdirectory like + /path/to/source/GUI/testing + and you want to comment on a bug. Setting sink_to_root=True wen + you initialize your BugDir will cause it to search for the '.be' + file in the ancestors of the path you passed in as 'root'. + /path/to/source/GUI/testing/.be miss + /path/to/source/GUI/.be miss + /path/to/source/.be hit! + So it still roots itself appropriately without much work for you. + + File-system access + ================== - return property(getter, setter, doc=doc) - - -class BugDir (list): - """ - File-system access: When rooted in non-bugdir directory, BugDirs live completely in memory until the first call to .save(). This creates a '.be' sub-directory containing configurations options, bugs, comments, @@ -95,13 +102,166 @@ class BugDir (list): will only load information from the file system when it loads new bugs/comments that it doesn't already have in memory, or when it explicitly asked to do so (e.g. .load() or __init__(from_disk=True)). + + Allow RCS initialization + ======================== + + This one is for testing purposes. Setting it to True allows the + BugDir to search for an installed RCS backend and initialize it in + the root directory. This is a convenience option for supporting + tests of versioning functionality (e.g. .duplicate_bugdir). + + Disable encoding manipulation + ============================= + + This one is for testing purposed. You might have non-ASCII + Unicode in your bugs, comments, files, etc. BugDir instances try + and support your preferred encoding scheme (e.g. "utf-8") when + dealing with stream and file input/output. For stream output, + this involves replacing sys.stdout and sys.stderr + (libbe.encode.set_IO_stream_encodings). However this messes up + doctest's output catching. In order to support doctest tests + using BugDirs, set manipulate_encodings=False, and stick to ASCII + in your tests. """ + + settings_properties = [] + required_saved_properties = [] + _prop_save_settings = settings_object.prop_save_settings + _prop_load_settings = settings_object.prop_load_settings + def _versioned_property(settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"]=required_saved_properties + return settings_object.versioned_property(**kwargs) + + @_versioned_property(name="target", + doc="The current project development target") + def target(): return {} + + def _guess_encoding(self): + return encoding.get_encoding() + def _check_encoding(value): + if value != None and value != settings_object.EMPTY: + return encoding.known_encoding(value) + def _setup_encoding(self, new_encoding): + if new_encoding != None and new_encoding != settings_object.EMPTY: + if self._manipulate_encodings == True: + encoding.set_IO_stream_encodings(new_encoding) + def _set_encoding(self, old_encoding, new_encoding): + self._setup_encoding(new_encoding) + self._prop_save_settings(old_encoding, new_encoding) + + @_versioned_property(name="encoding", + doc="""The default input/output encoding to use (e.g. "utf-8").""", + change_hook=_set_encoding, + generator=_guess_encoding, + check_fn=_check_encoding) + def encoding(): return {} + + def _setup_user_id(self, user_id): + self.rcs.user_id = user_id + def _guess_user_id(self): + return self.rcs.get_user_id() + def _set_user_id(self, old_user_id, new_user_id): + self._setup_user_id(new_user_id) + self._prop_save_settings(old_user_id, new_user_id) + + @_versioned_property(name="user_id", + doc= +"""The user's prefered name, e.g. 'John Doe <jdoe@example.com>'. Note +that the Arch RCS backend *enforces* ids with this format.""", + change_hook=_set_user_id, + generator=_guess_user_id) + def user_id(): return {} + + @_versioned_property(name="default_assignee", + doc= +"""The default assignee for new bugs e.g. 'John Doe <jdoe@example.com>'.""") + def default_assignee(): return {} + + @_versioned_property(name="rcs_name", + doc="""The name of the current RCS. Kept seperate to make saving/loading +settings easy. Don't set this attribute. Set .rcs instead, and +.rcs_name will be automatically adjusted.""", + default="None", + allowed=["None", "Arch", "bzr", "git", "hg"]) + def rcs_name(): return {} + + def _get_rcs(self, rcs_name=None): + """Get and root a new revision control system""" + if rcs_name == None: + rcs_name = self.rcs_name + new_rcs = rcs.rcs_by_name(rcs_name) + self._change_rcs(None, new_rcs) + return new_rcs + def _change_rcs(self, old_rcs, new_rcs): + new_rcs.encoding = self.encoding + new_rcs.root(self.root) + self.rcs_name = new_rcs.name + + @Property + @change_hook_property(hook=_change_rcs) + @cached_property(generator=_get_rcs) + @local_property("rcs") + @doc_property(doc="A revision control system instance.") + def rcs(): return {} + + def _bug_map_gen(self): + map = {} + for bug in self: + map[bug.uuid] = bug + for uuid in self.list_uuids(): + if uuid not in map: + map[uuid] = None + self._bug_map_value = map # ._bug_map_value used by @local_property + + @Property + @primed_property(primer=_bug_map_gen) + @local_property("bug_map") + @doc_property(doc="A dict of (bug-uuid, bug-instance) pairs.") + def _bug_map(): return {} + + def _setup_severities(self, severities): + if severities != None and severities != settings_object.EMPTY: + bug.load_severities(severities) + def _set_severities(self, old_severities, new_severities): + self._setup_severities(new_severities) + self._prop_save_settings(old_severities, new_severities) + @_versioned_property(name="severities", + doc="The allowed bug severities and their descriptions.", + change_hook=_set_severities) + def severities(): return {} + + def _setup_status(self, active_status, inactive_status): + bug.load_status(active_status, inactive_status) + def _set_active_status(self, old_active_status, new_active_status): + self._setup_status(new_active_status, self.inactive_status) + self._prop_save_settings(old_active_status, new_active_status) + @_versioned_property(name="active_status", + doc="The allowed active bug states and their descriptions.", + change_hook=_set_active_status) + def active_status(): return {} + + def _set_inactive_status(self, old_inactive_status, new_inactive_status): + self._setup_status(self.active_status, new_inactive_status) + self._prop_save_settings(old_inactive_status, new_inactive_status) + @_versioned_property(name="inactive_status", + doc="The allowed inactive bug states and their descriptions.", + change_hook=_set_inactive_status) + def inactive_status(): return {} + + def __init__(self, root=None, sink_to_existing_root=True, assert_new_BugDir=False, allow_rcs_init=False, + manipulate_encodings=True, from_disk=False, rcs=None): list.__init__(self) - self._save_user_id = False - self.settings = {} + settings_object.SavedSettingsObject.__init__(self) + self._manipulate_encodings = manipulate_encodings if root == None: root = os.getcwd() if sink_to_existing_root == True: @@ -110,16 +270,22 @@ class BugDir (list): if not os.path.exists(root): raise NoRootEntry(root) self.root = root + # get a temporary rcs until we've loaded settings + self.sync_with_disk = False + self.rcs = self._guess_rcs() + if from_disk == True: + self.sync_with_disk = True self.load() else: + self.sync_with_disk = False if assert_new_BugDir == True: if os.path.exists(self.get_path()): raise AlreadyInitialized, self.get_path() if rcs == None: rcs = self._guess_rcs(allow_rcs_init) self.rcs = rcs - user_id = self.rcs.get_user_id() + self._setup_user_id(self.user_id) def _find_root(self, path): """ @@ -128,7 +294,8 @@ class BugDir (list): """ if not os.path.exists(path): raise NoRootEntry(path) - versionfile = utility.search_parent_directories(path, os.path.join(".be", "version")) + versionfile=utility.search_parent_directories(path, + os.path.join(".be", "version")) if versionfile != None: beroot = os.path.dirname(versionfile) root = os.path.dirname(beroot) @@ -139,11 +306,11 @@ class BugDir (list): raise NoBugDir(path) return beroot - def get_version(self, path=None): - if self.rcs_name == None: - # Use a temporary RCS to check the version for the first time + def get_version(self, path=None, use_none_rcs=False): + if use_none_rcs == True: RCS = rcs.rcs_by_name("None") RCS.root(self.root) + RCS.encoding = encoding.get_encoding() else: RCS = self.rcs @@ -156,57 +323,6 @@ class BugDir (list): self.rcs.set_file_contents(self.get_path("version"), TREE_VERSION_STRING) - rcs_name = setting_property("rcs_name", - ("None", "bzr", "git", "Arch", "hg"), - doc= -"""The name of the current RCS. Kept seperate to make saving/loading -settings easy. Don't set this attribute. Set .rcs instead, and -.rcs_name will be automatically adjusted.""") - - _rcs = None - - def _get_rcs(self): - return self._rcs - - def _set_rcs(self, new_rcs): - if new_rcs == None: - new_rcs = rcs.rcs_by_name("None") - self._rcs = new_rcs - new_rcs.root(self.root) - self.rcs_name = new_rcs.name - - rcs = property(_get_rcs, _set_rcs, - doc="A revision control system (RCS) instance") - - _user_id = setting_property("user-id", doc= -"""The user's prefered name. Kept seperate to make saving/loading -settings easy. Don't set this attribute. Set .user_id instead, -and ._user_id will be automatically adjusted. This setting is -only saved if ._save_user_id == True""") - - def _get_user_id(self): - if self._user_id == None and self.rcs != None: - self._user_id = self.rcs.get_user_id() - return self._user_id - - def _set_user_id(self, user_id): - if self.rcs != None: - self.rcs.user_id = user_id - self._user_id = user_id - - user_id = property(_get_user_id, _set_user_id, doc= -"""The user's prefered name, e.g 'John Doe <jdoe@example.com>'. Note -that the Arch RCS backend *enforces* ids with this format.""") - - target = setting_property("target", - doc="The current project development target") - - def save_user_id(self, user_id=None): - if user_id == None: - user_id = self.user_id - self._save_user_id = True - self.user_id = user_id - def get_path(self, *args): my_dir = os.path.join(self.root, ".be") if len(args) == 0: @@ -224,24 +340,20 @@ that the Arch RCS backend *enforces* ids with this format.""") if allow_rcs_init == True: new_rcs = rcs.installed_rcs() new_rcs.init(self.root) - self.rcs = new_rcs return new_rcs def load(self): - version = self.get_version() + version = self.get_version(use_none_rcs=True) if version != TREE_VERSION_STRING: raise NotImplementedError, \ "BugDir cannot handle version '%s' yet." % version else: if not os.path.exists(self.get_path()): raise NoBugDir(self.get_path()) - self.settings = self._get_settings(self.get_path("settings")) + self.load_settings() self.rcs = rcs.rcs_by_name(self.rcs_name) - if self._user_id != None: # was a user name in the settings file - self.save_user_id() - - self._bug_map_gen() + self._setup_user_id(self.user_id) def load_all_bugs(self): "Warning: this could take a while." @@ -252,43 +364,35 @@ that the Arch RCS backend *enforces* ids with this format.""") def save(self): self.rcs.mkdir(self.get_path()) self.set_version() - self._save_settings(self.get_path("settings"), self.settings) + self.save_settings() self.rcs.mkdir(self.get_path("bugs")) for bug in self: bug.save() + def load_settings(self): + self.settings = self._get_settings(self.get_path("settings")) + self._setup_saved_settings() + self._setup_user_id(self.user_id) + self._setup_encoding(self.encoding) + self._setup_severities(self.severities) + self._setup_status(self.active_status, self.inactive_status) + def _get_settings(self, settings_path): - if self.rcs_name == None: - # Use a temporary RCS to loading settings the first time - RCS = rcs.rcs_by_name("None") - RCS.root(self.root) - else: - RCS = self.rcs - - allow_no_rcs = not RCS.path_in_root(settings_path) + allow_no_rcs = not self.rcs.path_in_root(settings_path) # allow_no_rcs=True should only be for the special case of # configuring duplicate bugdir settings try: - settings = mapfile.map_load(RCS, settings_path, allow_no_rcs) + settings = mapfile.map_load(self.rcs, settings_path, allow_no_rcs) except rcs.NoSuchFile: settings = {"rcs_name": "None"} return settings + def save_settings(self): + settings = self._get_saved_settings() + self._save_settings(self.get_path("settings"), settings) + def _save_settings(self, settings_path, settings): - this_dir_path = os.path.realpath(self.get_path("settings")) - if os.path.realpath(settings_path) == this_dir_path: - if not os.path.exists(self.get_path()): - # don't save settings until the bug directory has been - # initialized. this initialization happens the first time - # a bug directory is saved (BugDir.save()). If the user - # is just working with a BugDir in memory, we don't want - # to go cluttering up his file system with settings files. - return - if self._save_user_id == False: - if "user-id" in settings: - settings = copy.copy(settings) - del settings["user-id"] allow_no_rcs = not self.rcs.path_in_root(settings_path) # allow_no_rcs=True should only be for the special case of # configuring duplicate bugdir settings @@ -304,23 +408,17 @@ that the Arch RCS backend *enforces* ids with this format.""") duplicate_settings = self._get_settings(duplicate_settings_path) if "rcs_name" in duplicate_settings: duplicate_settings["rcs_name"] = "None" - duplicate_settings["user-id"] = self.user_id - self._save_settings(duplicate_settings_path, duplicate_settings) + duplicate_settings["user_id"] = self.user_id + if "disabled" in bug.status_values: + # Hack to support old versions of BE bugs + duplicate_settings["inactive_status"] = self.inactive_status + self._save_settings(duplicate_settings_path, duplicate_settings) - return BugDir(duplicate_path, from_disk=True) + return BugDir(duplicate_path, from_disk=True, manipulate_encodings=self._manipulate_encodings) def remove_duplicate_bugdir(self): self.rcs.remove_duplicate_repo() - def _bug_map_gen(self): - map = {} - for bug in self: - map[bug.uuid] = bug - for uuid in self.list_uuids(): - if uuid not in map: - map[uuid] = None - self._bug_map = map - def list_uuids(self): uuids = [] if os.path.exists(self.get_path()): @@ -338,6 +436,7 @@ that the Arch RCS backend *enforces* ids with this format.""") def _clear_bugs(self): while len(self) > 0: self.pop() + self._bug_map_gen() def _load_bug(self, uuid): bg = bug.Bug(bugdir=self, uuid=uuid, from_disk=True) @@ -420,7 +519,8 @@ def simple_bug_dir(): """ dir = utility.Dir() assert os.path.exists(dir.path) - bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True) + bugdir = BugDir(dir.path, sink_to_existing_root=False, allow_rcs_init=True, + manipulate_encodings=False) bugdir._dir_ref = dir # postpone cleanup since dir.__del__() removes dir. bug_a = bugdir.new_bug("a", summary="Bug A") bug_a.creator = "John Doe <jdoe@example.com>" @@ -495,6 +595,37 @@ class BugDirTestCase(unittest.TestCase): self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime)) self.bugdir.save() self.versionTest() + def testComments(self): + self.bugdir.new_bug(uuid="a", summary="Ant") + bug = self.bugdir.bug_from_uuid("a") + comm = bug.comment_root + rep = comm.new_reply("Ants are small.") + rep.new_reply("And they have six legs.") + self.bugdir.save() + self.bugdir._clear_bugs() + bug = self.bugdir.bug_from_uuid("a") + bug.load_comments() + self.failUnless(len(bug.comment_root)==1, len(bug.comment_root)) + for index,comment in enumerate(bug.comments()): + if index == 0: + repLoaded = comment + self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid) + self.failUnless(comment.sync_with_disk == True, + comment.sync_with_disk) + #load_settings() + self.failUnless(comment.content_type == "text/plain", + comment.content_type) + self.failUnless(repLoaded.settings["Content-type"]=="text/plain", + repLoaded.settings) + self.failUnless(repLoaded.body == "Ants are small.", + repLoaded.body) + elif index == 1: + self.failUnless(comment.in_reply_to == repLoaded.uuid, + repLoaded.uuid) + self.failUnless(comment.body == "And they have six legs.", + comment.body) + else: + self.failIf(True, "Invalid comment: %d\n%s" % (index, comment)) unitsuite = unittest.TestLoader().loadTestsFromTestCase(BugDirTestCase) suite = unittest.TestSuite([unitsuite])#, doctest.DocTestSuite()]) diff --git a/libbe/bzr.py b/libbe/bzr.py index a0ae715..38af6bb 100644 --- a/libbe/bzr.py +++ b/libbe/bzr.py @@ -55,7 +55,7 @@ class Bzr(RCS): pass def _rcs_get_file_contents(self, path, revision=None): if revision == None: - return file(os.path.join(self.rootdir, path), "rb").read() + return RCS._rcs_get_file_contents(self, path, revision) else: status,output,error = \ self._u_invoke_client("cat","-r",revision,path) diff --git a/libbe/cmdutil.py b/libbe/cmdutil.py index 6d7ab01..6be7540 100644 --- a/libbe/cmdutil.py +++ b/libbe/cmdutil.py @@ -16,14 +16,15 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import optparse import os -import locale from textwrap import TextWrapper from StringIO import StringIO +import sys import doctest import bugdir import plugin -import utility +import encoding + class UserError(Exception): def __init__(self, msg): @@ -34,6 +35,18 @@ class UserErrorWrap(UserError): UserError.__init__(self, str(exception)) self.exception = exception +class UsageError(Exception): + pass + +class GetHelp(Exception): + pass + +class GetCompletions(Exception): + def __init__(self, completions=[]): + msg = "Get allowed completions" + Exception.__init__(self, msg) + self.completions = completions + def iter_commands(): for name, module in plugin.iter_plugins("becommands"): yield name.replace("_", "-"), module @@ -52,9 +65,11 @@ def get_command(command_name): raise UserError("Unknown command %s" % command_name) return cmd + def execute(cmd, args): - encoding = locale.getpreferredencoding() or 'ascii' - return get_command(cmd).execute([a.decode(encoding) for a in args]) + enc = encoding.get_encoding() + get_command(cmd).execute([a.decode(enc) for a in args]) + return 0 def help(cmd=None): if cmd != None: @@ -71,24 +86,30 @@ def help(cmd=None): ret.append("be %s%*s %s" % (name, numExtraSpaces, "", desc)) return "\n".join(ret) -class GetHelp(Exception): - pass - - -class UsageError(Exception): - pass - +def completions(cmd): + parser = get_command(cmd).get_parser() + longopts = [] + for opt in parser.option_list: + longopts.append(opt.get_opt_string()) + return longopts def raise_get_help(option, opt, value, parser): raise GetHelp - +def raise_get_completions(option, opt, value, parser): + print "got completion arg" + raise GetCompletions(completions(sys.argv[1])) + class CmdOptionParser(optparse.OptionParser): def __init__(self, usage): optparse.OptionParser.__init__(self, usage) + self.disable_interspersed_args() self.remove_option("-h") self.add_option("-h", "--help", action="callback", callback=raise_get_help, help="Print a help message") + self.add_option("--complete", action="callback", + callback=raise_get_completions, + help="Print a list of available completions") def error(self, message): raise UsageError(message) @@ -102,6 +123,45 @@ class CmdOptionParser(optparse.OptionParser): self.print_help(f) return f.getvalue() +def option_value_pairs(options, parser): + """ + Iterate through OptionParser (option, value) pairs. + """ + for option in [o.dest for o in parser.option_list if o.dest != None]: + value = getattr(options, option) + yield (option, value) + +def default_complete(options, args, parser, bugid_args={}): + """ + A dud complete implementation for becommands to that the + --complete argument doesn't cause any problems. Use this + until you've set up a command-specific complete function. + + bugid_args is an optional dict where the keys are positional + arguments taking bug shortnames and the values are functions for + filtering, since that's a common enough operation. + e.g. for "be open [options] BUGID" + bugid_args = {0: lambda bug : bug.active == False} + """ + for option,value in option_value_pairs(options, parser): + if value == "--complete": + raise cmdutil.GetCompletions() + for pos,value in enumerate(args): + if value == "--complete": + if pos in bugid_args: + filter = bugid_args[pos] + bugshortnames = [] + try: + bd = bugdir.BugDir(from_disk=True, + manipulate_encodings=False) + bd.load_all_bugs() + bugs = [bug for bug in bd if filter(bug) == True] + bugshortnames = [bd.bug_shortname(bug) for bug in bugs] + except bugdir.NoBugDir: + pass + raise GetCompletions(bugshortnames) + raise GetCompletions() + def underlined(instring): """Produces a version of a string that is underlined with '=' diff --git a/libbe/comment.py b/libbe/comment.py index 87c1de0..e5c86c7 100644 --- a/libbe/comment.py +++ b/libbe/comment.py @@ -23,10 +23,23 @@ import textwrap import doctest from beuuid import uuid_gen +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, cached_property, \ + primed_property, change_hook_property, settings_property +import settings_object import mapfile from tree import Tree import utility + +class InvalidShortname(KeyError): + def __init__(self, shortname, shortnames): + msg = "Invalid shortname %s\n%s" % (shortname, shortnames) + KeyError.__init__(self, msg) + self.shortname = shortname + self.shortnames = shortnames + + INVALID_UUID = "!!~~\n INVALID-UUID \n~~!!" def _list_to_root(comments, bug): @@ -45,7 +58,8 @@ def _list_to_root(comments, bug): assert comment.uuid != None uuid_map[comment.uuid] = comment for comm in comments: - if comm.in_reply_to == None: + rep = comm.in_reply_to + if rep == None or rep == settings_object.EMPTY or rep == bug.uuid: root_comments.append(comm) else: parentUUID = comm.in_reply_to @@ -55,7 +69,11 @@ def _list_to_root(comments, bug): dummy_root.extend(root_comments) return dummy_root -def loadComments(bug): +def loadComments(bug, load_full=False): + """ + Set load_full=True when you want to load the comment completely + from disk *now*, rather than waiting and lazy loading as required. + """ path = bug.get_path("comments") if not os.path.isdir(path): return Comment(bug, uuid=INVALID_UUID) @@ -64,6 +82,9 @@ def loadComments(bug): if uuid.startswith('.'): continue comm = Comment(bug, uuid, from_disk=True) + if load_full == True: + comm.load_settings() + dummy = comm.body # force the body to load comments.append(comm) return _list_to_root(comments, bug) @@ -73,12 +94,89 @@ def saveComments(bug): for comment in bug.comment_root.traverse(): comment.save() -class Comment(Tree): + +class Comment(Tree, settings_object.SavedSettingsObject): + """ + >>> c = Comment() + >>> c.uuid != None + True + >>> c.uuid = "some-UUID" + >>> print c.content_type + text/plain + """ + + settings_properties = [] + required_saved_properties = [] + _prop_save_settings = settings_object.prop_save_settings + _prop_load_settings = settings_object.prop_load_settings + def _versioned_property(settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"]=required_saved_properties + return settings_object.versioned_property(**kwargs) + + @_versioned_property(name="From", + doc="The author of the comment") + def From(): return {} + + @_versioned_property(name="In-reply-to", + doc="UUID for parent comment or bug") + def in_reply_to(): return {} + + @_versioned_property(name="Content-type", + doc="Mime type for comment body", + default="text/plain", + require_save=True) + def content_type(): return {} + + @_versioned_property(name="Date", + doc="An RFC 2822 timestamp for comment creation") + def time_string(): return {} + + def _get_time(self): + if self.time_string == None: + return None + return utility.str_to_time(self.time_string) + def _set_time(self, value): + self.time_string = utility.time_to_str(value) + time = property(fget=_get_time, + fset=_set_time, + doc="An integer version of .time_string") + + def _get_comment_body(self): + if self.rcs != None and self.sync_with_disk == True: + import rcs + return self.rcs.get_file_contents(self.get_path("body")) + def _set_comment_body(self, value, force=False): + if (self.rcs != None and self.sync_with_disk == True) or force==True: + assert value != None, "Can't save empty comment" + self.rcs.set_file_contents(self.get_path("body"), value) + + @Property + @change_hook_property(hook=_set_comment_body) + @cached_property(generator=_get_comment_body) + @local_property("body") + @doc_property(doc="The meat of the comment") + def body(): return {} + + def _get_rcs(self): + if hasattr(self.bug, "rcs"): + return self.bug.rcs + + @Property + @cached_property(generator=_get_rcs) + @local_property("rcs") + @doc_property(doc="A revision control system instance.") + def rcs(): return {} + def __init__(self, bug=None, uuid=None, from_disk=False, in_reply_to=None, body=None): """ - Set from_disk=True to load an old bug. - Set from_disk=False to create a new bug. + Set from_disk=True to load an old comment. + Set from_disk=False to create a new comment. The uuid option is required when from_disk==True. @@ -89,26 +187,19 @@ class Comment(Tree): in_reply_to should be the uuid string of the parent comment. """ Tree.__init__(self) + settings_object.SavedSettingsObject.__init__(self) self.bug = bug - if bug != None: - self.rcs = bug.rcs - else: - self.rcs = None + self.uuid = uuid if from_disk == True: - self.uuid = uuid - self.load() + self.sync_with_disk = True else: - if uuid != None: - self.uuid = uuid - else: + self.sync_with_disk = False + if uuid == None: self.uuid = uuid_gen() - self.time = time.time() + self.time = int(time.time()) # only save to second precision if self.rcs != None: self.From = self.rcs.get_user_id() - else: - self.From = None self.in_reply_to = in_reply_to - self.content_type = "text/plain" self.body = body def traverse(self, *args, **kwargs): @@ -118,41 +209,54 @@ class Comment(Tree): continue yield comment - def _clean_string(self, value): - """ - >>> comm = Comment() - >>> comm._clean_string(None) - '' - >>> comm._clean_string("abc") - 'abc' - """ - if value == None: + def _setting_attr_string(self, setting): + value = getattr(self, setting) + if value == settings_object.EMPTY: return "" - return value + else: + return str(value) def xml(self, indent=0, shortname=None): + """ + >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") + >>> comm.uuid = "0123" + >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" + >>> print comm.xml(indent=2, shortname="com-1") + <comment> + <name>com-1</name> + <uuid>0123</uuid> + <from></from> + <date>Thu, 01 Jan 1970 00:00:00 +0000</date> + <body>Some + insightful + remarks</body> + </comment> + """ if shortname == None: shortname = self.uuid - ret = """<comment> - <name>%s</name> - <from>%s</from> - <date>%s</date> - <body>%s</body> -</comment>\n""" % (shortname, - self._clean_string(self.From), - utility.time_to_str(self.time), - self.body.rstrip('\n')) - return ret + lines = ["<comment>", + " <name>%s</name>" % (shortname,), + " <uuid>%s</uuid>" % self.uuid,] + if self.in_reply_to != None: + lines.append(" <in_reply_to>%s</in_reply_to>" % self.in_reply_to) + lines.extend([ + " <from>%s</from>" % self._setting_attr_string("From"), + " <date>%s</date>" % self.time_string, + " <body>%s</body>" % (self.body or "").rstrip('\n'), + "</comment>\n"]) + istring = ' '*indent + sep = '\n' + istring + return istring + sep.join(lines).rstrip('\n') def string(self, indent=0, shortname=None): """ >>> comm = Comment(bug=None, body="Some\\ninsightful\\nremarks\\n") - >>> comm.time = utility.str_to_time("Thu, 20 Nov 2008 15:55:11 +0000") + >>> comm.time_string = "Thu, 01 Jan 1970 00:00:00 +0000" >>> print comm.string(indent=2, shortname="com-1") --------- Comment --------- Name: com-1 From: - Date: Thu, 20 Nov 2008 15:55:11 +0000 + Date: Thu, 01 Jan 1970 00:00:00 +0000 <BLANKLINE> Some insightful @@ -163,12 +267,12 @@ class Comment(Tree): lines = [] lines.append("--------- Comment ---------") lines.append("Name: %s" % shortname) - lines.append("From: %s" % self._clean_string(self.From)) - lines.append("Date: %s" % utility.time_to_str(self.time)) + lines.append("From: %s" % (self._setting_attr_string("From"))) + lines.append("Date: %s" % self.time_string) lines.append("") - #lines.append(textwrap.fill(self._clean_string(self.body), + #lines.append(textwrap.fill(self.body or "", # width=(79-indent))) - lines.extend(self._clean_string(self.body).splitlines()) + lines.extend((self.body or "").splitlines()) # some comments shouldn't be wrapped... istring = ' '*indent @@ -179,7 +283,7 @@ class Comment(Tree): """ >>> comm = Comment(bug=None, body="Some insightful remarks") >>> comm.uuid = "com-1" - >>> comm.time = utility.str_to_time("Thu, 20 Nov 2008 15:55:11 +0000") + >>> comm.time_string = "Thu, 20 Nov 2008 15:55:11 +0000" >>> comm.From = "Jane Doe <jdoe@example.com>" >>> print comm --------- Comment --------- @@ -198,64 +302,68 @@ class Comment(Tree): assert name in ["values", "body"] return os.path.join(my_dir, name) - def load(self): - map = mapfile.map_load(self.rcs, self.get_path("values")) - self.time = utility.str_to_time(map["Date"]) - self.From = map["From"] - self.in_reply_to = map.get("In-reply-to") - self.content_type = map.get("Content-type", "text/plain") - self.body = self.rcs.get_file_contents(self.get_path("body")) + def load_settings(self): + self.settings = mapfile.map_load(self.rcs, self.get_path("values")) + self._setup_saved_settings() - def save(self): - assert self.rcs != None - map_file = {"Date": utility.time_to_str(self.time)} - self._add_headers(map_file, ("From", "in_reply_to", "content_type")) + def save_settings(self): + parent_dir = os.path.dirname(self.get_path()) + self.rcs.mkdir(parent_dir) self.rcs.mkdir(self.get_path()) - mapfile.map_save(self.rcs, self.get_path("values"), map_file) - self.rcs.set_file_contents(self.get_path("body"), self.body) - - def _add_headers(self, map, names): - map_names = {} - for name in names: - map_names[name] = self._pyname_to_header(name) - self._add_attrs(map, map_names) - - def _pyname_to_header(self, name): - return name.capitalize().replace('_', '-') - - def _add_attrs(self, map, map_names): - for name in map_names.keys(): - value = getattr(self, name) - if value is not None: - map[map_names[name]] = value + path = self.get_path("values") + mapfile.map_save(self.rcs, path, self._get_saved_settings()) + + def save(self): + assert self.body != None, "Can't save blank comment" + #if self.in_reply_to == None: + # raise Exception, str(self)+'\n'+str(self.settings)+'\n'+str(self._settings_loaded) + #assert self.in_reply_to != None, "Comment must be a reply to something" + self.save_settings() + self._set_comment_body(self.body, force=True) def remove(self): for comment in self.traverse(): path = comment.get_path() self.rcs.recursive_remove(path) - def add_reply(self, reply): - if reply.time != None and self.time != None: - assert reply.time >= self.time + def add_reply(self, reply, allow_time_inversion=False): if self.uuid != INVALID_UUID: reply.in_reply_to = self.uuid self.append(reply) + #raise Exception, "adding reply \n%s\n%s" % (self, reply) def new_reply(self, body=None): """ >>> comm = Comment(bug=None, body="Some insightful remarks") >>> repA = comm.new_reply("Critique original comment") >>> repB = repA.new_reply("Begin flamewar :p") + >>> repB.in_reply_to == repA.uuid + True """ reply = Comment(self.bug, body=body) self.add_reply(reply) + #raise Exception, "new reply added (%s),\n%s\n%s\n\t--%s--" % (body, self, reply, reply.in_reply_to) return reply - def string_thread(self, name_map={}, indent=0, + def string_thread(self, string_method_name="string", name_map={}, + indent=0, flatten=True, auto_name_map=False, bug_shortname=None): """ - Return a sting displaying a thread of comments. + Return a string displaying a thread of comments. bug_shortname is only used if auto_name_map == True. + + string_method_name (defaults to "string") is the name of the + Comment method used to generate the output string for each + Comment in the thread. The method must take the arguments + indent and shortname. + + SIDE-EFFECT: if auto_name_map==True, calls comment_shortnames() + which will sort the tree by comment.time. Avoid by calling + name_map = {} + for shortname,comment in comm.comment_shortnames(bug_shortname): + name_map[comment.uuid] = shortname + comm.sort(key=lambda c : c.From) # your sort + comm.string_thread(name_map=name_map) >>> a = Comment(bug=None, uuid="a", body="Insightful remarks") >>> a.time = utility.str_to_time("Thu, 20 Nov 2008 01:00:00 +0000") @@ -269,7 +377,7 @@ class Comment(Tree): >>> d.uuid = "d" >>> d.time = utility.str_to_time("Thu, 20 Nov 2008 04:00:00 +0000") >>> a.sort(key=lambda comm : comm.time) - >>> print a.string_thread() + >>> print a.string_thread(flatten=True) --------- Comment --------- Name: a From: @@ -325,33 +433,23 @@ class Comment(Tree): for shortname,comment in self.comment_shortnames(bug_shortname): name_map[comment.uuid] = shortname stringlist = [] - for depth,comment in self.thread(flatten=True): + for depth,comment in self.thread(flatten=flatten): ind = 2*depth+indent if comment.uuid in name_map: sname = name_map[comment.uuid] else: sname = None - stringlist.append(comment.string(indent=ind, shortname=sname)) + string_fn = getattr(comment, string_method_name) + stringlist.append(string_fn(indent=ind, shortname=sname)) return '\n'.join(stringlist) def xml_thread(self, name_map={}, indent=0, auto_name_map=False, bug_shortname=None): - if auto_name_map == True: - name_map = {} - for shortname,comment in self.comment_shortnames(bug_shortname): - name_map[comment.uuid] = shortname - stringlist = [] - for depth,comment in self.thread(flatten=True): - ind = 2*depth+indent - if comment.uuid in name_map: - sname = name_map[comment.uuid] - else: - sname = None - stringlist.append(comment.xml(indent=ind, shortname=sname)) - return '\n'.join(stringlist) - + return self.string_thread(string_method_name="xml", name_map=name_map, + indent=indent, auto_name_map=auto_name_map, + bug_shortname=bug_shortname) - def comment_shortnames(self, bug_shortname=""): + def comment_shortnames(self, bug_shortname=None): """ Iterate through (id, comment) pairs, in time order. (This is a user-friendly id, not the comment uuid). @@ -372,6 +470,8 @@ class Comment(Tree): bug-1:3 c bug-1:4 d """ + if bug_shortname == None: + bug_shortname = "" self.sort(key=lambda comm : comm.time) for num,comment in enumerate(self.traverse()): yield ("%s:%d" % (bug_shortname, num+1), comment) @@ -393,7 +493,8 @@ class Comment(Tree): for cur_name, comment in self.comment_shortnames(*args, **kwargs): if comment_shortname == cur_name: return comment - raise KeyError(comment_shortname) + raise InvalidShortname(comment_shortname, + list(self.comment_shortnames(*args, **kwargs))) def comment_from_uuid(self, uuid): """ diff --git a/libbe/config.py b/libbe/config.py index 79c0d6f..94c700e 100644 --- a/libbe/config.py +++ b/libbe/config.py @@ -15,30 +15,40 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import ConfigParser +import codecs +import locale import os.path +import sys import doctest +default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() + def path(): """Return the path to the per-user config file""" return os.path.expanduser("~/.bugs_everywhere") -def set_val(name, value, section="DEFAULT"): +def set_val(name, value, section="DEFAULT", encoding=None): """Set a value in the per-user config file :param name: The name of the value to set :param value: The new value to set (or None to delete the value) :param section: The section to store the name/value in """ + if encoding == None: + encoding = default_encoding config = ConfigParser.ConfigParser() - config.read(path()) + f = codecs.open(path(), "r", encoding) + config.readfp(f, path()) + f.close() if value is not None: config.set(section, name, value) else: config.remove_option(section, name) - config.write(file(path(), "wb")) - pass + f = codecs.open(path(), "w", encoding) + config.write(f) + f.close() -def get_val(name, section="DEFAULT"): +def get_val(name, section="DEFAULT", encoding=None): """ Get a value from the per-user config file @@ -49,13 +59,17 @@ def get_val(name, section="DEFAULT"): True >>> set_val("junk", "random") >>> get_val("junk") - 'random' + u'random' >>> set_val("junk", None) >>> get_val("junk") is None True """ + if encoding == None: + encoding = default_encoding config = ConfigParser.ConfigParser() - config.read(path()) + f = codecs.open(path(), "r", encoding) + config.readfp(f, path()) + f.close() try: return config.get(section, name) except ConfigParser.NoOptionError: diff --git a/libbe/diff.py b/libbe/diff.py index 86a91ca..17d6c50 100644 --- a/libbe/diff.py +++ b/libbe/diff.py @@ -15,9 +15,8 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Compare two bug trees""" -from libbe import cmdutil, bugdir +from libbe import cmdutil, bugdir, bug from libbe.utility import time_to_str -from libbe.bug import cmp_severity import doctest def diff(old_bugdir, new_bugdir): @@ -41,17 +40,18 @@ def diff(old_bugdir, new_bugdir): def diff_report(diff_data, bug_dir): (removed, modified, added) = diff_data def modified_cmp(left, right): - return cmp_severity(left[1], right[1]) + return bug.cmp_severity(left[1], right[1]) - added.sort(cmp_severity) - removed.sort(cmp_severity) + added.sort(bug.cmp_severity) + removed.sort(bug.cmp_severity) modified.sort(modified_cmp) - + lines = [] + if len(added) > 0: - print "New bug reports:" - for bug in added: - print bug.string(shortlist=True) - print "" + lines.append("New bug reports:") + for bg in added: + lines.extend(bg.string(shortlist=True).splitlines()) + lines.append("") if len(modified) > 0: printed = False @@ -61,15 +61,18 @@ def diff_report(diff_data, bug_dir): continue if not printed: printed = True - print "Modified bug reports:" - print change_str - print "" + lines.append("Modified bug reports:") + lines.extend(change_str.splitlines()) + if printed == True: + lines.append("") - if len(removed) > 0: - print "Removed bug reports:" - for bug in removed: - print bug.string(shortlist=True) - print "" + if len(removed) > 0: + lines.append("Removed bug reports:") + for bg in removed: + lines.extend(bg.string(shortlist=True).splitlines()) + lines.append("") + + return '\n'.join(lines) def change_lines(old, new, attributes): change_list = [] diff --git a/libbe/editor.py b/libbe/editor.py new file mode 100644 index 0000000..4a63e5c --- /dev/null +++ b/libbe/editor.py @@ -0,0 +1,103 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc. +# <abentley@panoramicfeedback.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA + +import codecs +import locale +import os +import sys +import tempfile +import doctest + +default_encoding = sys.getfilesystemencoding() or locale.getpreferredencoding() + +comment_marker = u"== Anything below this line will be ignored\n" + +class CantFindEditor(Exception): + def __init__(self): + Exception.__init__(self, "Can't find editor to get string from") + +def editor_string(comment=None, encoding=None): + """Invokes the editor, and returns the user_produced text as a string + + >>> if "EDITOR" in os.environ: + ... del os.environ["EDITOR"] + >>> if "VISUAL" in os.environ: + ... del os.environ["VISUAL"] + >>> editor_string() + Traceback (most recent call last): + CantFindEditor: Can't find editor to get string from + >>> os.environ["EDITOR"] = "echo bar > " + >>> editor_string() + u'bar\\n' + >>> os.environ["VISUAL"] = "echo baz > " + >>> editor_string() + u'baz\\n' + >>> del os.environ["EDITOR"] + >>> del os.environ["VISUAL"] + """ + if encoding == None: + encoding = default_encoding + for name in ('VISUAL', 'EDITOR'): + try: + editor = os.environ[name] + break + except KeyError: + pass + else: + raise CantFindEditor() + fhandle, fname = tempfile.mkstemp() + try: + if comment is not None: + os.write(fhandle, '\n'+comment_string(comment)) + os.close(fhandle) + oldmtime = os.path.getmtime(fname) + os.system("%s %s" % (editor, fname)) + f = codecs.open(fname, "r", encoding) + output = trimmed_string(f.read()) + f.close() + if output.rstrip('\n') == "": + output = None + finally: + os.unlink(fname) + return output + + +def comment_string(comment): + """ + >>> comment_string('hello') == comment_marker+"hello" + True + """ + return comment_marker + comment + + +def trimmed_string(instring): + """ + >>> trimmed_string("hello\\n"+comment_marker) + u'hello\\n' + >>> trimmed_string("hi!\\n" + comment_string('Booga')) + u'hi!\\n' + """ + out = [] + for line in instring.splitlines(True): + if line.startswith(comment_marker): + break + out.append(line) + return ''.join(out) + +suite = doctest.DocTestSuite() diff --git a/libbe/encoding.py b/libbe/encoding.py new file mode 100644 index 0000000..7f924eb --- /dev/null +++ b/libbe/encoding.py @@ -0,0 +1,53 @@ +# Bugs Everywhere, a distributed bugtracker +# Copyright (C) 2005 Aaron Bentley and Panometrics, Inc. +# <abentley@panoramicfeedback.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA +import codecs +import locale +import sys +import doctest + +def get_encoding(): + """ + Guess a useful input/output/filesystem encoding... Maybe we need + seperate encodings for input/output and filesystem? Hmm... + """ + encoding = locale.getpreferredencoding() or sys.getdefaultencoding() + if sys.platform != 'win32' or sys.version_info[:2] > (2, 3): + encoding = locale.getlocale(locale.LC_TIME)[1] or encoding + # Python 2.3 on windows doesn't know about 'XYZ' alias for 'cpXYZ' + return encoding + +def known_encoding(encoding): + """ + >>> known_encoding("highly-unlikely-encoding") + False + >>> known_encoding(get_encoding()) + True + """ + try: + codecs.lookup(encoding) + return True + except LookupError: + return False + +def set_IO_stream_encodings(encoding): + sys.stdin = codecs.getreader(encoding)(sys.__stdin__) + sys.stdout = codecs.getwriter(encoding)(sys.__stdout__) + sys.stderr = codecs.getwriter(encoding)(sys.__stderr__) + +suite = doctest.DocTestSuite() diff --git a/libbe/git.py b/libbe/git.py index 046e72e..e57014f 100644 --- a/libbe/git.py +++ b/libbe/git.py @@ -69,7 +69,7 @@ class Git(RCS): self._rcs_add(path) def _rcs_get_file_contents(self, path, revision=None): if revision == None: - return file(self._u_abspath(path), "rb").read() + return RCS._rcs_get_file_contents(self, path, revision) else: arg = "%s:%s" % (revision,path) status,output,error = self._u_invoke_client("show", arg) @@ -85,7 +85,7 @@ class Git(RCS): status,output,error = self._u_invoke_client('commit', '-a', '-F', commitfile) revision = None - revline = re.compile("Created (.*)commit (.*):(.*)") + revline = re.compile("(.*) (.*)[:\]] (.*)") match = revline.search(output) assert match != None, output+error assert len(match.groups()) == 3 diff --git a/libbe/hg.py b/libbe/hg.py index 27cbb79..c00d7e2 100644 --- a/libbe/hg.py +++ b/libbe/hg.py @@ -58,14 +58,14 @@ class Hg(RCS): pass def _rcs_get_file_contents(self, path, revision=None): if revision == None: - return file(os.path.join(self.rootdir, path), "rb").read() + return RCS._rcs_get_file_contents(self, path, revision) else: status,output,error = \ self._u_invoke_client("cat","-r",revision,path) return output def _rcs_duplicate_repo(self, directory, revision=None): if revision == None: - RCS._rcs_duplicate_repo(self, directory, revision) + return RCS._rcs_duplicate_repo(self, directory, revision) else: self._u_invoke_client("archive", "--rev", revision, directory) def _rcs_commit(self, commitfile): diff --git a/libbe/mapfile.py b/libbe/mapfile.py index 559d713..c36d454 100644 --- a/libbe/mapfile.py +++ b/libbe/mapfile.py @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +import yaml import os.path import errno import utility @@ -29,17 +30,20 @@ class IllegalValue(Exception): Exception.__init__(self, 'Illegal value "%s"' % value) self.value = value -def generate(map, context=3): - """Generate a format-2 mapfile content string. This is a simpler - format, but should merge better, because there's no chance of - confusion for appends, and lines are unique for both key and - value. - +def generate(map): + """Generate a YAML mapfile content string. >>> generate({"q":"p"}) - '\\n\\n\\nq=p\\n\\n\\n\\n' + 'q: p\\n\\n' + >>> generate({"q":u"Fran\u00e7ais"}) + 'q: Fran\\xc3\\xa7ais\\n\\n' + >>> generate({"q":u"hello"}) + 'q: hello\\n\\n' >>> generate({"q=":"p"}) Traceback (most recent call last): IllegalKey: Illegal key "q=" + >>> generate({"q:":"p"}) + Traceback (most recent call last): + IllegalKey: Illegal key "q:" >>> generate({"q\\n":"p"}) Traceback (most recent call last): IllegalKey: Illegal key "q\\n" @@ -53,7 +57,6 @@ def generate(map, context=3): Traceback (most recent call last): IllegalValue: Illegal value "p\\n" """ - assert(context > 0) keys = map.keys() keys.sort() for key in keys: @@ -61,6 +64,7 @@ def generate(map, context=3): assert not key.startswith('>') assert('\n' not in key) assert('=' not in key) + assert(':' not in key) assert(len(key) > 0) except AssertionError: raise IllegalKey(key.encode('string_escape')) @@ -69,20 +73,19 @@ def generate(map, context=3): lines = [] for key in keys: - for i in range(context): - lines.append("") - lines.append("%s=%s" % (key, map[key])) - for i in range(context): - lines.append("") - return '\n'.join(lines) + '\n' + lines.append(yaml.safe_dump({key: map[key]}, + default_flow_style=False, + allow_unicode=True)) + lines.append("") + return '\n'.join(lines) def parse(contents): """ - Parse a format-2 mapfile string. - >>> parse('\\n\\n\\nq=p\\n\\n\\n\\n')['q'] + Parse a YAML mapfile string. + >>> parse('q: p\\n\\n')['q'] + 'p' + >>> parse('q: \\'p\\'\\n\\n')['q'] 'p' - >>> parse('\\n\\nq=\\'p\\'\\n\\n\\n\\n')['q'] - "\'p\'" >>> contents = generate({"a":"b", "c":"d", "e":"f"}) >>> dict = parse(contents) >>> dict["a"] @@ -92,15 +95,25 @@ def parse(contents): >>> dict["e"] 'f' """ - result = {} + old_format = False for line in contents.splitlines(): - line = line.rstrip('\n') - if len(line) == 0: - continue - name,value = [field for field in line.split('=', 1)] - assert not result.has_key(name) - result[name] = value - return result + if len(line.split("=")) == 2: + old_format = True + break + if old_format: # translate to YAML. Hack to deal with old BE bugs. + newlines = [] + for line in contents.splitlines(): + line = line.rstrip('\n') + if len(line) == 0: + continue + fields = line.split("=") + if len(fields) == 2: + key,value = fields + newlines.append('%s: "%s"' % (key, value.replace('"','\\"'))) + else: + newlines.append(line) + contents = '\n'.join(newlines) + return yaml.load(contents) def map_save(rcs, path, map, allow_no_rcs=False): """Save the map as a mapfile to the specified path""" diff --git a/libbe/properties.py b/libbe/properties.py new file mode 100644 index 0000000..a8e89fb --- /dev/null +++ b/libbe/properties.py @@ -0,0 +1,479 @@ +# Bugs Everywhere - a distributed bugtracker +# Copyright (C) 2008 W. Trevor King <wking@drexel.edu> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +This module provides a series of useful decorators for defining +various types of properties. For example usage, consider the +unittests at the end of the module. + +See + http://www.python.org/dev/peps/pep-0318/ +and + http://www.phyast.pitt.edu/~micheles/python/documentation.html +for more information on decorators. +""" + +import unittest + + +class ValueCheckError (ValueError): + def __init__(self, name, value, allowed): + msg = "%s not in %s for %s" % (value, allowed, name) + ValueError.__init__(self, msg) + self.name = name + self.value = value + self.allowed = allowed + +def Property(funcs): + """ + End a chain of property decorators, returning a property. + """ + args = {} + args["fget"] = funcs.get("fget", None) + args["fset"] = funcs.get("fset", None) + args["fdel"] = funcs.get("fdel", None) + args["doc"] = funcs.get("doc", None) + + #print "Creating a property with" + #for key, val in args.items(): print key, value + return property(**args) + +def doc_property(doc=None): + """ + Add a docstring to a chain of property decorators. + """ + def decorator(funcs=None): + """ + Takes either a dict of funcs {"fget":fnX, "fset":fnY, ...} + or a function fn() returning such a dict. + """ + if hasattr(funcs, "__call__"): + funcs = funcs() # convert from function-arg to dict + funcs["doc"] = doc + return funcs + return decorator + +def local_property(name, null=None): + """ + Define get/set access to per-parent-instance local storage. Uses + ._<name>_value to store the value for a particular owner instance. + If the ._<name>_value attribute does not exist, returns null. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) + value = getattr(self, "_%s_value" % name, null) + return value + def _fset(self, value): + setattr(self, "_%s_value" % name, value) + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + +def settings_property(name, null=None): + """ + Similar to local_property, except where local_property stores the + value in instance._<name>_value, settings_property stores the + value in instance.settings[name]. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget", None) + fset = funcs.get("fset", None) + def _fget(self): + if fget is not None: + fget(self) + value = self.settings.get(name, null) + return value + def _fset(self, value): + self.settings[name] = value + if fset is not None: + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + funcs["name"] = name + return funcs + return decorator + +def defaulting_property(default=None, null=None): + """ + Define a default value for get access to a property. + If the stored value is null, then default is returned. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + def _fget(self): + value = fget(self) + if value == null: + return default + return value + funcs["fget"] = _fget + return funcs + return decorator + +def fn_checked_property(value_allowed_fn): + """ + Define allowed values for get/set access to a property. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "<unknown>") + def _fget(self): + value = fget(self) + if value_allowed_fn(value) != True: + raise ValueCheckError(name, value, value_allowed_fn) + return value + def _fset(self, value): + if value_allowed_fn(value) != True: + raise ValueCheckError(name, value, value_allowed_fn) + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def checked_property(allowed=[]): + """ + Define allowed values for get/set access to a property. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "<unknown>") + def _fget(self): + value = fget(self) + if value not in allowed: + raise ValueCheckError(name, value, allowed) + return value + def _fset(self, value): + if value not in allowed: + raise ValueCheckError(name, value, allowed) + fset(self, value) + funcs["fget"] = _fget + funcs["fset"] = _fset + return funcs + return decorator + +def cached_property(generator, initVal=None): + """ + Allow caching of values generated by generator(instance), where + instance is the instance to which this property belongs. Uses + ._<name>_cache to store a cache flag for a particular owner + instance. + + When the cache flag is True or missing and the stored value is + initVal, the first fget call triggers the generator function, + whiose output is stored in _<name>_cached_value. That and + subsequent calls to fget will return this cached value. + + If the input value is no longer initVal (e.g. a value has been + loaded from disk or set with fset), that value overrides any + cached value, and this property has no effect. + + When the cache flag is False and the stored value is initVal, the + generator is not cached, but is called on every fget. + + The cache flag is missing on initialization. Particular instances + may override by setting their own flag. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "<unknown>") + def _fget(self): + cache = getattr(self, "_%s_cache" % name, True) + value = fget(self) + if cache == True: + if value == initVal: + if hasattr(self, "_%s_cached_value" % name): + value = getattr(self, "_%s_cached_value" % name) + else: + value = generator(self) + setattr(self, "_%s_cached_value" % name, value) + else: + if value == initVal: + value = generator(self) + return value + funcs["fget"] = _fget + return funcs + return decorator + +def primed_property(primer, initVal=None): + """ + Just like a generator_property, except that instead of returning a + new value and running fset to cache it, the primer performs some + background manipulation (e.g. loads data into instance.settings) + such that a _second_ pass through fget succeeds. + + The 'cache' flag becomes a 'prime' flag, with priming taking place + whenever ._<name>_prime is True, or is False or missing and + value == initVal. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + name = funcs.get("name", "<unknown>") + def _fget(self): + prime = getattr(self, "_%s_prime" % name, False) + if prime == False: + value = fget(self) + if prime == True or (prime == False and value == initVal): + primer(self) + value = fget(self) + return value + funcs["fget"] = _fget + return funcs + return decorator + +def change_hook_property(hook): + """ + Call the function hook(instance, old_value, new_value) whenever a + value different from the current value is set (instance is a a + reference to the class instance to which this property belongs). + This is useful for saving changes to disk, etc. + """ + def decorator(funcs): + if hasattr(funcs, "__call__"): + funcs = funcs() + fget = funcs.get("fget") + fset = funcs.get("fset") + name = funcs.get("name", "<unknown>") + def _fset(self, value): + old_value = fget(self) + if value != old_value: + hook(self, old_value, value) + fset(self, value) + funcs["fset"] = _fset + return funcs + return decorator + + +class DecoratorTests(unittest.TestCase): + def testLocalDoc(self): + class Test(object): + @Property + @doc_property("A fancy property") + def x(): + return {} + self.failUnless(Test.x.__doc__ == "A fancy property", + Test.x.__doc__) + def testLocalProperty(self): + class Test(object): + @Property + @local_property(name="LOCAL") + def x(): + return {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("_LOCAL_value" in dir(t), dir(t)) + self.failUnless(t._LOCAL_value == 'z', t._LOCAL_value) + def testSettingsProperty(self): + class Test(object): + @Property + @settings_property(name="attr") + def x(): + return {} + def __init__(self): + self.settings = {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'z' # the first set initializes ._LOCAL_value + self.failUnless(t.x == 'z', str(t.x)) + self.failUnless("attr" in t.settings, t.settings) + self.failUnless(t.settings["attr"] == 'z', t.settings["attr"]) + def testDefaultingLocalProperty(self): + class Test(object): + @Property + @defaulting_property(default='y', null='x') + @local_property(name="DEFAULT") + def x(): return {} + t = Test() + self.failUnless(t.x == None, str(t.x)) + t.x = 'x' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'y' + self.failUnless(t.x == 'y', str(t.x)) + t.x = 'z' + self.failUnless(t.x == 'z', str(t.x)) + def testCheckedLocalProperty(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testTwoCheckedLocalProperties(self): + class Test(object): + @Property + @checked_property(allowed=['x', 'y', 'z']) + @local_property(name="X") + def x(): return {} + + @Property + @checked_property(allowed=['a', 'b', 'c']) + @local_property(name="A") + def a(): return {} + def __init__(self): + self._A_value = 'a' + self._X_value = 'x' + t = Test() + try: + t.x = 'a' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.x = 'x' + t.x = 'y' + t.x = 'z' + try: + t.a = 'x' + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + t.a = 'a' + t.a = 'b' + t.a = 'c' + def testFnCheckedLocalProperty(self): + class Test(object): + @Property + @fn_checked_property(lambda v : v in ['x', 'y', 'z']) + @local_property(name="CHECKED") + def x(): return {} + def __init__(self): + self._CHECKED_value = 'x' + t = Test() + self.failUnless(t.x == 'x', str(t.x)) + try: + t.x = None + e = None + except ValueCheckError, e: + pass + self.failUnless(type(e) == ValueCheckError, type(e)) + def testCachedLocalProperty(self): + class Gen(object): + def __init__(self): + self.i = 0 + def __call__(self, owner): + self.i += 1 + return self.i + class Test(object): + @Property + @cached_property(generator=Gen(), initVal=None) + @local_property(name="CACHED") + def x(): return {} + t = Test() + self.failIf("_CACHED_cache" in dir(t), getattr(t, "_CACHED_cache", None)) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + self.failUnless(t.x == 1, t.x) + t.x = 8 + self.failUnless(t.x == 8, t.x) + self.failUnless(t.x == 8, t.x) + t._CACHED_cache = False # Caching is off, but the stored value + val = t.x # is 8, not the initVal (None), so we + self.failUnless(val == 8, val) # get 8. + t._CACHED_value = None # Now we've set the stored value to None + val = t.x # so future calls to fget (like this) + self.failUnless(val == 2, val) # will call the generator every time... + val = t.x + self.failUnless(val == 3, val) + val = t.x + self.failUnless(val == 4, val) + t._CACHED_cache = True # We turn caching back on, and get + self.failUnless(t.x == 1, str(t.x)) # the original cached value. + del t._CACHED_cached_value # Removing that value forces a + self.failUnless(t.x == 5, str(t.x)) # single cache-regenerating call + self.failUnless(t.x == 5, str(t.x)) # to the genenerator, after which + self.failUnless(t.x == 5, str(t.x)) # we get the new cached value. + def testPrimedLocalProperty(self): + class Test(object): + def prime(self): + self.settings["PRIMED"] = "initialized" + @Property + @primed_property(primer=prime, initVal=None) + @settings_property(name="PRIMED") + def x(): return {} + def __init__(self): + self.settings={} + t = Test() + self.failIf("_PRIMED_prime" in dir(t), getattr(t, "_PRIMED_prime", None)) + self.failUnless(t.x == "initialized", t.x) + t.x = 1 + self.failUnless(t.x == 1, t.x) + t.x = None + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = True + t.x = 3 + self.failUnless(t.x == "initialized", t.x) + t._PRIMED_prime = False + t.x = 3 + self.failUnless(t.x == 3, t.x) + def testChangeHookLocalProperty(self): + class Test(object): + def _hook(self, old, new): + self.old = old + self.new = new + + @Property + @change_hook_property(_hook) + @local_property(name="HOOKED") + def x(): return {} + t = Test() + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 1 + self.failUnless(t.old == None, t.old) + self.failUnless(t.new == 1, t.new) + t.x = 2 + self.failUnless(t.old == 1, t.old) + self.failUnless(t.new == 2, t.new) + +suite = unittest.TestLoader().loadTestsFromTestCase(DecoratorTests) + diff --git a/libbe/rcs.py b/libbe/rcs.py index 3519c3d..786f9dd 100644 --- a/libbe/rcs.py +++ b/libbe/rcs.py @@ -15,13 +15,14 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from subprocess import Popen, PIPE +import codecs import os import os.path -from socket import gethostname import re +from socket import gethostname +import shutil import sys import tempfile -import shutil import unittest import doctest @@ -77,8 +78,9 @@ class PathNotInRoot(Exception): self.root = root class NoSuchFile(Exception): - def __init__(self, pathname): - Exception.__init__(self, "No such file: %s" % pathname) + def __init__(self, pathname, root="."): + path = os.path.abspath(os.path.join(root, pathname)) + Exception.__init__(self, "No such file: %s" % path) def new(): @@ -97,12 +99,13 @@ class RCS(object): name = "None" client = "" # command-line tool for _u_invoke_client versioned = False - def __init__(self, paranoid=False): + def __init__(self, paranoid=False, encoding=sys.getdefaultencoding()): self.paranoid = paranoid self.verboseInvoke = False self.rootdir = None self._duplicateBasedir = None self._duplicateDirname = None + self.encoding = encoding def __del__(self): self.cleanup() @@ -171,15 +174,15 @@ class RCS(object): pass def _rcs_get_file_contents(self, path, revision=None): """ - Get the file contents as they were in a given revision. Don't - worry about decoding the contents, the RCS.get_file_contents() - method will handle that. - + Get the file contents as they were in a given revision. Revision==None specifies the current revision. """ assert revision == None, \ "The %s RCS does not support revision specifiers" % self.name - return file(os.path.join(self.rootdir, path), "rb").read() + f = codecs.open(os.path.join(self.rootdir, path), "r", self.encoding) + contents = f.read() + f.close() + return contents def _rcs_duplicate_repo(self, directory, revision=None): """ Get the repository as it was in a given revision. @@ -297,14 +300,18 @@ class RCS(object): relpath = self._u_rel_path(path) contents = self._rcs_get_file_contents(relpath,revision) else: - contents = file(path, "rb").read() - return contents.decode("utf-8") + f = codecs.open(path, "r", self.encoding) + contents = f.read() + f.close() + return contents def set_file_contents(self, path, contents, allow_no_rcs=False): """ Set the file contents under version control. """ add = not os.path.exists(path) - file(path, "wb").write(contents.encode("utf-8")) + f = codecs.open(path, "w", self.encoding) + f.write(contents) + f.close() if self._use_rcs(path, allow_no_rcs): if add: @@ -537,13 +544,13 @@ class RCS(object): Split the commitfile created in self.commit() back into summary and header lines. """ - f = file(commitfile, "rb") + f = codecs.open(commitfile, "r", self.encoding) summary = f.readline() body = f.read() body.lstrip('\n') if len(body) == 0: body = None - f.close + f.close() return (summary, body) diff --git a/libbe/restconvert.py b/libbe/restconvert.py deleted file mode 100644 index 57148e4..0000000 --- a/libbe/restconvert.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright (C) 2005, 2006 Aaron Bentley and Panometrics, Inc. -# <abentley@panoramicfeedback.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -import re -from StringIO import StringIO -from docutils import nodes -from docutils.statemachine import StringList -from docutils.core import publish_file -from docutils.parsers import rst -from docutils.parsers.rst import directives -from docutils.parsers.rst.states import Inliner, MarkupMismatch, unescape -try : - from xml.etree import ElementTree # Python 2.5 (and greater?) -except ImportError : - from elementtree import ElementTree -import doctest - -def rest_xml(rest): - warnings = StringIO() - parser = rst.Parser(inliner=HelpLinkInliner()) - xmltext = publish_file(rest, writer_name="html", parser=parser, - settings_overrides={"warning_stream": warnings, - "halt_level": 5}) - warnings.seek(0) - return ElementTree.parse(StringIO(xmltext)).getroot(), warnings.read() - -class HelpLinkInliner(Inliner): - def __init__(self, roles=None): - Inliner.__init__(self, roles) - regex = re.compile('\[([^|]*)\|([^]]*)\]') - self.implicit_dispatch.append((regex, self.help_reference)) - - def parse(self, *args, **kwargs): - self.more_messages = [] - nodes, messages = Inliner.parse(self, *args, **kwargs) - return nodes, (messages + self.more_messages) - - def help_reference(self, match, lineno): - from wizardhelp.controllers import iter_help_pages - text,link = match.groups() - rawtext = match.group(0) - text, link, rawtext = [unescape(f, 1) for f in (text, link, rawtext)] - if link not in list(iter_help_pages()): - msg = self.reporter.warning('Broken link to "%s".' % link, - line=lineno) - self.more_messages.append(msg) - ref = "/help/%s/" % link - unescaped = text - node = nodes.reference(rawtext, text, refuri=ref) - node.set_class("helplink") - return [node] - - -def rst_directive(name=None, required_args=0, optional_args=0, - final_arg_ws=False, options=None, content='forbidden'): - """Decorator that simplifies creating ReST directives - - All arguments are optional. Name is, by default, determined from the - function name. - - The possible values for content are 'forbidden', 'allowed' (but not - required), and 'required' (a warning will be generated if not present). - """ - content_rules = {'forbidden': (False, False), 'allowed': (True, False), - 'required': (True, True)} - content_allowed, content_required = content_rules[content] - - def decorator_factory(func): - my_name = name - if my_name is None: - my_name = func.__name__ - - def decorator(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): - warn = state_machine.reporter.warning - if not content and content_required: - warn = state_machine.reporter.warning - warning = warn('%s is empty' % my_name, - nodes.literal_block(block_text, block_text), - line=lineno) - return [warning] - return func(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine) - - decorator.arguments = (required_args, optional_args, final_arg_ws) - decorator.options = options - decorator.content = content_allowed - directives.register_directive(my_name, decorator) - return decorator - return decorator_factory - - -@rst_directive(required_args=1, final_arg_ws=True, content='required') -def foldout(name, arguments, options, content, lineno, content_offset, - block_text, state, state_machine): - """\ - Generate a foldout section. - - On the ReST side, this merely involves marking the items with suitable - classes. A Kid match rule will be used to insert the appropriate - Javascript magic. - """ - text = '\n'.join(content) - foldout_title = nodes.paragraph([arguments[0]]) - foldout_title.set_class('foldout-title') - state.nested_parse(StringList([arguments[0]]), 0, foldout_title) - foldout_body = nodes.compound(text) - foldout_body.set_class('foldout-body') - state.nested_parse(content, content_offset, foldout_body) - foldout = nodes.compound(text) - foldout += foldout_title - foldout += foldout_body - foldout.set_class('foldout') - return [foldout] - -suite = doctest.DocTestSuite() diff --git a/libbe/settings_object.py b/libbe/settings_object.py new file mode 100644 index 0000000..1df3e6b --- /dev/null +++ b/libbe/settings_object.py @@ -0,0 +1,353 @@ +# Bugs Everywhere - a distributed bugtracker +# Copyright (C) 2008 W. Trevor King <wking@drexel.edu> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +""" +This module provides a base class implementing settings-dict based +property storage useful for BE objects with saved properties +(e.g. BugDir, Bug, Comment). For example usage, consider the +unittests at the end of the module. +""" + +import doctest +import unittest + +from properties import Property, doc_property, local_property, \ + defaulting_property, checked_property, fn_checked_property, \ + cached_property, primed_property, change_hook_property, \ + settings_property + + +class _Token (object): + """ + `Control' value class for properties. We want values that only + mean something to the settings_object module. + """ + pass + +class UNPRIMED (_Token): + "Property has not been primed." + pass + +class EMPTY (_Token): + """ + Property has been primed but has no user-set value, so use + default/generator value. + """ + pass + + +def prop_save_settings(self, old, new): + """ + The default action undertaken when a property changes. + """ + if self.sync_with_disk==True: + self.save_settings() + +def prop_load_settings(self): + """ + The default action undertaken when an UNPRIMED property is accessed. + """ + if self.sync_with_disk==True and self._settings_loaded==False: + self.load_settings() + else: + self._setup_saved_settings(flag_as_loaded=False) + +# Some name-mangling routines for pretty printing setting names +def setting_name_to_attr_name(self, name): + """ + Convert keys to the .settings dict into their associated + SavedSettingsObject attribute names. + >>> print setting_name_to_attr_name(None,"User-id") + user_id + """ + return name.lower().replace('-', '_') + +def attr_name_to_setting_name(self, name): + """ + The inverse of setting_name_to_attr_name. + >>> print attr_name_to_setting_name(None, "user_id") + User-id + """ + return name.capitalize().replace('_', '-') + + +def versioned_property(name, doc, + default=None, generator=None, + change_hook=prop_save_settings, + primer=prop_load_settings, + allowed=None, check_fn=None, + settings_properties=[], + required_saved_properties=[], + require_save=False): + """ + Combine the common decorators in a single function. + + Use zero or one (but not both) of default or generator, since a + working default will keep the generator from functioning. Use the + default if you know what you want the default value to be at + 'coding time'. Use the generator if you can write a function to + determine a valid default at run time. If both default and + generator are None, then the property will be a defaulting + property which defaults to None. + + allowed and check_fn have a similar relationship, although you can + use both of these if you want. allowed compares the proposed + value against a list determined at 'coding time' and check_fn + allows more flexible comparisons to take place at run time. + + Set require_save to True if you want to save the default/generated + value for a property, to protect against future changes. E.g., we + currently expect all comments to be 'text/plain' but in the future + we may want to default to 'text/html'. If we don't want the old + comments to be interpreted as 'text/html', we would require that + the content type be saved. + + change_hook, primer, settings_properties, and + required_saved_properties are only options to get their defaults + into our local scope. Don't mess with them. + """ + settings_properties.append(name) + if require_save == True: + required_saved_properties.append(name) + def decorator(funcs): + fulldoc = doc + if default != None: + defaulting = defaulting_property(default=default, null=EMPTY) + fulldoc += "\n\nThis property defaults to %s" % default + if generator != None: + cached = cached_property(generator=generator, initVal=EMPTY) + fulldoc += "\n\nThis property is generated with %s" % generator + if check_fn != None: + fn_checked = fn_checked_property(value_allowed_fn=check_fn) + fulldoc += "\n\nThis property is checked with %s" % check_fn + if allowed != None: + checked = checked_property(allowed=allowed) + fulldoc += "\n\nThe allowed values for this property are: %s." \ + % (', '.join(allowed)) + hooked = change_hook_property(hook=change_hook) + primed = primed_property(primer=primer, initVal=UNPRIMED) + settings = settings_property(name=name, null=UNPRIMED) + docp = doc_property(doc=fulldoc) + deco = hooked(primed(settings(docp(funcs)))) + if default != None: + deco = defaulting(deco) + if generator != None: + deco = cached(deco) + if default != None: + deco = defaulting(deco) + if allowed != None: + deco = checked(deco) + if check_fn != None: + deco = fn_checked(deco) + return Property(deco) + return decorator + +class SavedSettingsObject(object): + + # Keep a list of properties that may be stored in the .settings dict. + #settings_properties = [] + + # A list of properties that we save to disk, even if they were + # never set (in which case we save the default value). This + # protects against future changes in default values. + #required_saved_properties = [] + + _setting_name_to_attr_name = setting_name_to_attr_name + _attr_name_to_setting_name = attr_name_to_setting_name + + def __init__(self): + self._settings_loaded = False + self.sync_with_disk = False + self.settings = {} + + def load_settings(self): + """Load the settings from disk.""" + # Override. Must call ._setup_saved_settings() after loading. + self.settings = {} + self._setup_saved_settings() + + def _setup_saved_settings(self, flag_as_loaded=True): + """ + To be run after setting self.settings up from disk. Marks all + settings as primed. + """ + for property in self.settings_properties: + if property not in self.settings: + self.settings[property] = EMPTY + elif self.settings[property] == UNPRIMED: + self.settings[property] = EMPTY + if flag_as_loaded == True: + self._settings_loaded = True + + def save_settings(self): + """Load the settings from disk.""" + # Override. Should save the dict output of ._get_saved_settings() + settings = self._get_saved_settings() + pass # write settings to disk.... + + def _get_saved_settings(self): + settings = {} + for k,v in self.settings.items(): + if v != None and v != EMPTY: + settings[k] = v + for k in self.required_saved_properties: + settings[k] = getattr(self, self._setting_name_to_attr_name(k)) + return settings + + def clear_cached_setting(self, setting=None): + "If setting=None, clear *all* cached settings" + if setting != None: + if hasattr(self, "_%s_cached_value" % setting): + delattr(self, "_%s_cached_value" % setting) + else: + for setting in settings_properties: + self.clear_cached_setting(setting) + + +class SavedSettingsObjectTests(unittest.TestCase): + def testSimpleProperty(self): + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + # access missing setting + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(len(t.settings) == 0, len(t.settings)) + self.failUnless(t.content_type == EMPTY, t.content_type) + # accessing t.content_type triggers the priming, which runs + # t._setup_saved_settings, which fills out t.settings with + # EMPTY data. t._settings_loaded is still false though, since + # the default priming does not do any of the `official' loading + # that occurs in t.load_settings. + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + # load settings creates an EMPTY value in the settings array + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == EMPTY, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + # now we set a value + t.content_type = None + self.failUnless(t.settings["Content-type"] == None, + t.settings["Content-type"]) + self.failUnless(t.content_type == None, t.content_type) + self.failUnless(t.settings["Content-type"] == None, + t.settings["Content-type"]) + # now we set another value + t.content_type = "text/plain" + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == "text/plain", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, + t._get_saved_settings()) + # now we clear to the post-primed value + t.content_type = EMPTY + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t.content_type == EMPTY, t.content_type) + self.failUnless(len(t.settings) == 1, len(t.settings)) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + def testDefaultingProperty(self): + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._settings_loaded == False, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t._settings_loaded == False, t._settings_loaded) + t.load_settings() + self.failUnless(t._settings_loaded == True, t._settings_loaded) + self.failUnless(t.content_type == "text/plain", t.content_type) + self.failUnless(t.settings["Content-type"] == EMPTY, + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings() == {}, t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t.content_type == "text/html", + t.content_type) + self.failUnless(t.settings["Content-type"] == "text/html", + t.settings["Content-type"]) + self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, + t._get_saved_settings()) + def testRequiredDefaultingProperty(self): + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + @versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, + t._get_saved_settings()) + def testClassVersionedPropertyDefinition(self): + class Test(SavedSettingsObject): + settings_properties = [] + required_saved_properties = [] + def _versioned_property(settings_properties=settings_properties, + required_saved_properties=required_saved_properties, + **kwargs): + if "settings_properties" not in kwargs: + kwargs["settings_properties"] = settings_properties + if "required_saved_properties" not in kwargs: + kwargs["required_saved_properties"]=required_saved_properties + return versioned_property(**kwargs) + @_versioned_property(name="Content-type", + doc="A test property", + default="text/plain", + require_save=True) + def content_type(): return {} + def __init__(self): + SavedSettingsObject.__init__(self) + t = Test() + self.failUnless(t._get_saved_settings()=={"Content-type":"text/plain"}, + t._get_saved_settings()) + t.content_type = "text/html" + self.failUnless(t._get_saved_settings()=={"Content-type":"text/html"}, + t._get_saved_settings()) + +unitsuite=unittest.TestLoader().loadTestsFromTestCase(SavedSettingsObjectTests) +suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()]) diff --git a/libbe/tree.py b/libbe/tree.py index e6f144e..9e07ee3 100644 --- a/libbe/tree.py +++ b/libbe/tree.py @@ -39,6 +39,9 @@ class Tree(list): >>> a.branch_len() 5 + >>> a.sort(key=lambda node : -node.branch_len()) + >>> "".join([node.n for node in a.traverse()]) + 'acfhiebdg' >>> a.sort(key=lambda node : node.branch_len()) >>> "".join([node.n for node in a.traverse()]) 'abdgcefhi' @@ -93,7 +96,7 @@ class Tree(list): """ list.sort(self, *args, **kwargs) for child in self: - child.sort() + child.sort(*args, **kwargs) def traverse(self, depthFirst=True): """ diff --git a/libbe/utility.py b/libbe/utility.py index 2c77fcf..30240a9 100644 --- a/libbe/utility.py +++ b/libbe/utility.py @@ -15,10 +15,11 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import calendar -import time +import codecs import os -import tempfile import shutil +import tempfile +import time import doctest @@ -87,73 +88,5 @@ def str_to_time(str_time): def handy_time(time_val): return time.strftime("%a, %d %b %Y %H:%M", time.localtime(time_val)) -class CantFindEditor(Exception): - def __init__(self): - Exception.__init__(self, "Can't find editor to get string from") - -def editor_string(comment=None): - - """Invokes the editor, and returns the user_produced text as a string - - >>> if "EDITOR" in os.environ: - ... del os.environ["EDITOR"] - >>> if "VISUAL" in os.environ: - ... del os.environ["VISUAL"] - >>> editor_string() - Traceback (most recent call last): - CantFindEditor: Can't find editor to get string from - >>> os.environ["EDITOR"] = "echo bar > " - >>> editor_string() - u'bar\\n' - >>> os.environ["VISUAL"] = "echo baz > " - >>> editor_string() - u'baz\\n' - >>> del os.environ["EDITOR"] - >>> del os.environ["VISUAL"] - """ - for name in ('VISUAL', 'EDITOR'): - try: - editor = os.environ[name] - break - except KeyError: - pass - else: - raise CantFindEditor() - fhandle, fname = tempfile.mkstemp() - try: - if comment is not None: - os.write(fhandle, '\n'+comment_string(comment)) - os.close(fhandle) - oldmtime = os.path.getmtime(fname) - os.system("%s %s" % (editor, fname)) - output = trimmed_string(file(fname, "rb").read().decode("utf-8")) - if output.rstrip('\n') == "": - output = None - finally: - os.unlink(fname) - return output - - -def comment_string(comment): - """ - >>> comment_string('hello') - '== Anything below this line will be ignored ==\\nhello' - """ - return '== Anything below this line will be ignored ==\n' + comment - - -def trimmed_string(instring): - """ - >>> trimmed_string("hello\\n== Anything below this line will be ignored") - 'hello\\n' - >>> trimmed_string("hi!\\n" + comment_string('Booga')) - 'hi!\\n' - """ - out = [] - for line in instring.splitlines(True): - if line.startswith('== Anything below this line will be ignored'): - break - out.append(line) - return ''.join(out) suite = doctest.DocTestSuite() @@ -19,9 +19,12 @@ if len(sys.argv) > 1: for submodname in sys.argv[1:]: match = False mod = plugin.get_plugin("libbe", submodname) - if mod is not None and hasattr(mod, "suite"): - suite.addTest(mod.suite) - match = True + if mod is not None: + if hasattr(mod, "suite"): + suite.addTest(mod.suite) + match = True + else: + print "Module \"%s\" has no test suite" % submodname mod = plugin.get_plugin("becommands", submodname) if mod is not None: suite.addTest(doctest.DocTestSuite(mod)) diff --git a/test_usage.sh b/test_usage.sh index 43b5d4d..42c0f2f 100755 --- a/test_usage.sh +++ b/test_usage.sh @@ -11,7 +11,7 @@ # Note that this script uses the *installed* version of be, not the # one in your working tree. -set -e # exit imediately on failed command +set -e # exit immediately on failed command set -o pipefail # pipes fail if any stage fails set -v # verbose, echo commands to stdout @@ -95,6 +95,8 @@ echo "$OUT" BUG=`echo "$OUT" | sed -n 's/Created bug with ID //p'` echo "Working with bug: $BUG" be comment $BUG "This is an argument" +be set user_id "$ID" # get tired of guessing user id for none RCS +be set # show settings be comment $BUG:1 "No it isn't" # comment on the first comment be show $BUG # show details on a given bug be close $BUG # set bug status to 'closed' @@ -102,7 +104,6 @@ be comment $BUG "It's closed, but I can still comment." be open $BUG # set bug status to 'open' be comment $BUG "Reopend, comment again" be status $BUG fixed # set bug status to 'fixed' -be show $BUG # show bug details & comments be list # list all open bugs be list --status fixed # list all fixed bugs be assign $BUG # assign the bug to yourself @@ -111,6 +112,11 @@ be assign $BUG 'Joe' # assign the bug to Joe be list -a Joe -s fixed # list the fixed bugs assigned to Joe be assign $BUG none # assign the bug to noone be diff # see what has changed +OUT=`be new 'also having too much fun'` +BUGB=`echo "$OUT" | sed -n 's/Created bug with ID //p'` +be comment $BUGB "Blissfully unaware of a similar bug" +be merge $BUG $BUGB # join BUGB to BUG +be show $BUG # show bug details & comments be remove $BUG # decide that you don't like that bug after all cd / rm -rf $TESTDIR |