Saturday, January 21, 2006

ทดลองเขียน Plugin Class Diagram บน Trac ตอนที่ 2

ตอนก่อนพูดถึงการเขียน parser ขึ้นมาเพื่อแปลง syntax
ที่เรากำหนดให้กลายเป็น syntax ของ graphviz ไปแล้ว

ตอนนี้จะดูว่า การ implement plugin ใน trac ต้องทำอย่างไร
ถ้าเราดูเอกสาร Trac Component Architecture
จะเห็นว่า Trac design architecture ในลักษณะเดียวกับ
Eclipse Plugin (ซึ่ง base จาก osgi framework อีกที)

model ของ Trac plugin


model ของ eclipse plugin


ดังนั้นการที่เราจะ implement plugin ของเราขึ้นมา
เราก็ต้องรู้ก่อนว่า plugin ของเรา จะ integrate เข้ากับ extension point
อะไร

ในกรณีของเรา เราต้องการทำเป็น wiki macro
ดังนั้น extension point ของเราก็คือ
trac.wiki.api.IWikiMacroProvider
code ของเรา ก็ต้องเริ่มต้นด้วย
class UmlMacro(Component):
implements(IWikiMacroProvider)

def __init__(self):
self.log.info("UmlMacro start")
self.parser = Parser();

def get_macros(self):
yield 'uml'

def get_macro_description(self, name):
return "Use this macro for render class diagram"


จะเห็นว่ามีการ call implements function ที่ชื่อ IWikiMacroProvider
method หลักๆที่ต้องเขียนมีอยู่ 3 อันคือ
  • get_macros
    ต้อง return เป็น iterable
  • get_macro_description
  • render_macro
    อันนี้เป็นส่วน implement การทำงานจริง โดยมีการ pass ค่า
    content (ซึ่งเป็น string ที่เราต้องแปลง) เข้ามา


ประเด็นถัดไปก็คือ uml plugin ของเรา
ไม่ต้องการ เรียก graphviz process เอง
แต่อยากส่งต่อให้ graphviz plugin ไปทำงานต่อ

ตรงนี้ใช้เวลาหาอยู่เกือบ 2 ชั่วโมง
ตอนแรก พยายามใช้ผ่าน plugin manager
แต่หาวิธีเรียกไม่ได้สักที
สุดท้ายก็ไปปิ๊ง idea ว่า ในเมื่อมันเป็น plugin architecture
เราก็เรียกมันผ่านทาง extension point นั่นแหล่ะ
วิธีการก็คือ
  def render_macro(self, req, name, content):
wiki = WikiSystem(self.env)

for provider in wiki.macro_providers:
if "graphviz" in list(provider.get_macros()):
return provider.render_macro(req, "graphviz",
self.parser.parse(content).to_dot())
return "none"


เริ่มด้วยการ new WikiSystem (คิดว่ามันเป็น singleton pattern นะ)
แล้วเรียกหา macrosprovider list มา
เพื่อทำการ iterate หา Graphviz Plugin Provider

เมื่อได้มา ก็แค่ส่งต่อ syntax ที่เราใช้ parser แปลงไว้แล้วไป

สรุป
===
Trac มีการออกแบบโครงสร้าง Architecture ไว้ดีมาก
code python ที่เขียนไว้ ก็อ่านทำความเข้าใจง่าย
น่าสนใจที่นำมาใช้เป็นแกนหลัก ในการทำ project management

ส่วน shot ถัดไป
ผมกำลังมองเรื่องการ integrate mylar เข้ากับ trac

Related link from Roti

Friday, January 20, 2006

ทดลองเขียน Plugin Class Diagram บน Trac ตอนที่ 1

(อันนี้เป็น python โปรแกรมแรกของผม
เขียนไปงงไป code อาจจะดูขัดตาหน่อย)

สืบเนื่องจากการใช้ Trac wiki มาเป็นเครื่องมือ design
ก็เลยเกิดความต้องการ อยากให้ render class diagram ใน wiki ได้ด้วย

จากการสืบค้นใน google ก็พบว่า มี plugin ที่ชื่อ graphvizplugin อยู่ตัวหนึ่ง
ซึ่งจากการทดลองใช้ ก็ใช้ได้ดี
วิธีการใช้ ก็เพียงแต่แทรก code ลงไปอย่างนี้

{{{
#!graphviz
digraph G {
....
....
}
}}}


แต่ก็ยังมีความยุ่งยากในการเขียน dot expression ให้แสดงผลเป็น
class diagram อยู่บ้าง
(จริงๆไม่ยุ่งหรอก เขียนง่ายตรงไปตรงมาดี แต่ไม่พอใจซะงั้น)
ก็เลยอยากทำ expression ที่ layer สูงขึ้นไปอีกชั้น
เช่น เขียน

class Order {
name;
passwd;
enabled;
};

แล้วให้มันแปลงเป็น แบบนี้

digraph uml {
node [shape=record,fontsize=10,fontname="Helvetica"]
struct2 [label="{User\n|name\lpasswd\lenabled}"];


วิธีการใช้งาน ก็ประมาณนี้



จาก Requirement ที่ว่ามา ก็มาถึงขั้นตอนการ implement
การ implement plugin ตัวนี้ แบ่งออกเป็น 2 ส่วนคือ
  • ส่วน parser ที่ใช้แปลง custom language ของเรา ให้เป็น dot language
  • ส่วน plugin ที่ register ตัวเองเป็น macro ของ Trac
    และทำหน้าที่ render ให้เป็น graph ออกมา


ส่วน parser เลือกใช้ library ที่ชื่อ PLY (Python Lex-Yacc)
โดยเขียน lexer ดังนี้

import lex

# List of token names. This is always required
tokens = (
'CLASS',
'IDENT',
'SEMI',
'LBRACE',
'RBRACE'
)

# Regular expression rules for simple tokens
t_ignore = ' \t'
t_SEMI = ';'
t_LBRACE = '{'
t_RBRACE = '}'

def t_IDENT(t):
r'[a-zA-Z_][\w_]*'
# แยกความแตกต่างระหว่าง Ident กับ Reserved word
t.type = reserved.get(t.value,'IDENT')
return t

def t_newline(t):
r'\n+'
t.lineno += len(t.value)

def t_error(t):
print "Illegal character '%s'" % t.value[0]
t.skip(1)

reserved = {
'class' : 'CLASS'
}

lex.lex()


ตัว parser ของ เรามี gramma ประมาณนี้

diagram : class_list

class_list : class_list
| class_statement

class_statement : CLASS IDENT SEMI
| CLASS IDENT LBRACE class_body_list RBRACE SEMI

class_body_list : class_body_list
| class_body

class_body : IDENT SEMI


การ implement gramma ใน PLY ตรงไปตรงมาดี
เริ่มที่ class_body แล้วกัน

def p_class_body(t):
'class_body : IDENT SEMI'
t[0] = t[1]

t[0] คือค่าที่จะ return กลับไป
ส่วน t[1] ก็คือ token ลำดับที่ 1 นั่นก็คือค่าของ IDENT นั่นเอง

ส่วน class_body_list ต้อง implement แยกเป็น 2 function

def p_class_body_list(t):
'class_body_list : class_body'
lst = [t[1]]
t[0] = lst

def p_class_body_list2(t):
'class_body_list : class_body_list class_body '
t[1].append(t[2])
t[0] = t[1]

อันแรกตรงไปตรงมา class_body return อะไรมา ก็ให้แปลงเป็น list ก่อน return กลับ
ส่วนอันที่สอง datatype ของอันแรกจะเป็น list
ดังนั้นก็นำเอาสิ่งที่ return มาจาก class_body append เข้าไปยัง list ที่ได้
ก่อน return กลับไป
สรุปว่า class_body_list จะ return ค่ากลับเป็น list เสมอ

ส่วน class_statement ก็ แยกเป็น 2 ส่วนเหมือนกัน

def p_class_statement(t):
'class_statement : CLASS IDENT SEMI'
cls = TypeClass(t[2])
t[0] = cls

def p_class_statement2(t):
'class_statement : CLASS IDENT LBRACE class_body_list RBRACE SEMI'
cls = TypeClass(t[2])
cls.members = t[4]
t[0] = cls

เนื่องจาก data แถวนี้เริ่มซับซ้อนขึ้น ก็เลยต้องสร้าง class ที่ชื่อ TypeClass
ขึ้นมารองรับข้อมูลในส่วนนี้

ชุดสุดท้าย ก็คือ diagram กับ class_list

def p_diagram(t):
'diagram_statement : class_list'
d = Diagram()
d.classes = t[1]
t[0] = d

def p_class_list(t):
'class_list : class_statement'
lst = [t[1]]
t[0] = lst

def p_class_list_2(t):
'class_list : class_list class_statement'
t[1].append(t[2])
t[0] = t[1]

ตรงนี้มีการสร้าง class ที่ชื่อ Diagram ขึ้นมารองรับข้อมูล

พอได้ structure แล้วการแปลงเป็น dot language ก็ไม่ยากแล้ว
class Diagram และ TypeClass หน้าตาเป็นแบบนี้
class Diagram(object):
def __init__(self):
self.classes = []

def to_dot(self):
buf = StringIO()
buf.write('digraph g {\n')
buf.write("node [shape=record,fontsize=10,fontname=\"Helvetica\"]\n")
for cls in self.classes:
buf.write("%s" % cls.to_dot())
buf.write('\n}')
return buf.getvalue()

class TypeClass(object):
def __init__(self, name):
self.name = name
self.members = []

def to_dot(self):
buf = StringIO()
buf.write('%s [label="{%s\\n|' % (self.name, self.name))
for member in self.members:
buf.write('%s\\l' % member)
buf.write("}\"]\n")
return buf.getvalue()


ไว้คราวหน้าจะพูดถึงประเด็นการเขียน plugin ใน Trac ต่อ

Related link from Roti

Trac กับ Ticket custom field

วันนี้คุยกับคุณยุทธเรื่อง Trac กัน
คุณยุทธ show url http://projects.edgewall.com/trac/wiki/TimeTracking ให้ผมดู
ซึ่งข้างในมีประเด็นเรื่อง การเพิ่ม field ที่เกี่ยวกับ time tracking ลงไปใน Trac
ซึ่งประเด็นนี้ ผมก็เคยเห็นผ่านตาใน Trac Gantt Pluginเหมือนกัน
แต่ไม่เคยดูละเอียด ก็เลยไปนั่งอ่านอย่างละเอียดดู

สุดท้ายก็อ่านเจอว่า
Trac เตรียม feature Ticket custom-field มาให้เราเรียบร้อยแล้ว
รายละเอียดอยู่ในเอกสาร Custom Ticket Fields

การเก็บข้อมูล จะเก็บลง table ที่ชื่อ ticket_custom
โดย table ticket_custom ที่ trac เตรียมไว้ หน้าตาเป็นแบบนี้

CREATE TABLE ticket_custom (
ticket integer,
name text,
value text,
UNIQUE (ticket,name)
);

การเก็บอย่างนี้มีข้อดีคือยืดหยุ่น แต่ข้อเสีย ก็คือ ถ้าอยากนำไปออก report
ก็ต้อง join 1 ครั้งต่อ 1 field ที่เพิ่มเข้าไป

เมื่อ add field ได้ ก็ต้องเอามาออก report ได้
Module Report ของ Trac มีให้เลือก 2 แบบคือ
  • TracReports
    อันนี้ สามารถสร้าง report ใหม่ๆ เข้าไปได้
    แต่ต้อง define sql เอง
    โดยเราต้องมี permission REPORT_CREATE ก่อน
    จึงจะมี link ที่ทำให้สร้าง report ได้
  • TracQuery
    การสร้าง report ใหม่ๆก็จะทำผ่านหน้าจอ Query จากนั้นก็จด option
    ที่ใช้ ไปใส่ไว้ใน wiki ผ่านทาง TrackLinks หรือ macro


การออก report ที่ต้องการแสดง custom-field ด้วย รู้สึกว่าจะต้องใช้วิธี TracReports

Feature ที่ชอบอีกอย่าง ก็คือ
เราสามารถ add svn post commit hook เข้าไป
เพื่อที่จะนำเอา message ที่ developer commit เข้ามา
เอามา integrate เข้ากับ trac ticket ได้เช่น

svn commit -m 'Added time tracking, closes #1, spent 1h'

เมื่อ post hook script พบ คำว่า closed #1 -> ก็จะไปปิด Ticket #1 ให้เลย

รายละเอียดเพิ่มเติมดูใน Subversion commit hook

Related link from Roti

Wednesday, January 18, 2006

ปรับแต่ง Trac ให้รองรับ svn repository ที่ encode ด้วย tis620

ช่วงนี้กำลังจะเริ่มใช้ Trac ในการ control project
แต่มีปัญหาภาษาไทยนิดหน่อย
เริ่มตั้งแต่การ convert repository จาก cvs ไปเป็น svn

ผมใช้ cvs2svn ช่วยในการ convert
แต่มีปัญหาก็คือ ไม่สามารถระบุ --encoding=tis620 ได้
ถ้าระบุ ก็จะเกิด error message ว่า unknown encoding: tis620
โปรแกรม cvs2svn นี้เขียนด้วย python
ความมันส์เริ่มต้นนี้แหล่ะ เพราะผมไม่เคยเขียน python ที่ยาวเกิน 3 บรรทัด
ก็เลยสนุกกับการพยายามประติดประต่อว่า error ควรจะเป็นเรื่องไหน และเพราะอะไร
(โชคดีที่ code python อ่านไม่ยาก)

ประเด็นปัญหาก็คือ cvs2svn พยายามเรียกใช้ unicode แบบนี้

xx = unicode(sometext, "tis620")

ซึ่งปัญหาก็คือ python2.3 ของผม มันไม่มี encoding tis620
แต่หลังจาก search ใน google แล้ว ก็พบว่า
มี tis_620.py อยู่ใน sourceforge
ก็เลยจัดการ download มา, พร้อมกับแก้ไข aliases.py ให้
register encoding ใหม่นี้ลงไปด้วย

หลังจาก convert ไปเป็น svn เรียบร้อยแล้ว
ปัญหาต่อมาก็คือ
พอลอง browse code ดูใน Trac ก็พบว่า
มันยังแสดงภาษาไทยผิดอยู่

ปัญหาที่เจอ ก็คือ file browser.py
ที่รับผิดชอบการ render code มันใช้วิธี detect charset
โดยการดูค่าจาก mime-type ที่เก็บอยู่ใน svn properties
ที่ชื่อ svn:mime-type
(ใน svn เราสามารถ set property ให้กับ file
โดยใช้คำสั่ง svn propset ได้)

ค่า svn:mime-type เป็นค่า ที่ svn ออกแบบไว้ให้
subversion apache module
ใช้ในการ render stream กลับไปยัง browser

แน่นอนว่าค่านี้โดยปกติ จะไม่มีการ set ดังนั้น
browser.py ก็เลย render ด้วย encoding แบบ default กลับมา

ทางเลือกในการแก้ไข ก็มี 2 ทาง คือ
  • เขียนโปรแกรมเพื่อ set propset svn:mime-type ให้กับ file ทุกๆ file
  • แก้ไข browser.py ให้มีวิธี lookup ค่า default charset ที่ถูกต้อง


เลือกใช้วิธีที่ 2 คือเข้าไปแก้ไข code ใน browser.py
โดยเลือกว่า ถ้าหา charset ใน mime-type ไม่เจอ
ก็ให้ใช้ค่า default จาก trac.ini แทน (configuration file ของ trac project)

ประเด็นถัดไป ก็คือ การ browse change set ยังแสดงผลไม่ถูกต้อง
หลังจากไล่ดู source แล้ว ก็พบว่า
changeset.py มีการเตรียมการรองรับเรื่อง encoding ไว้อยู่แล้ว
โดยเราสามารถเข้าไปแก้ไข trac.ini ให้เป็น
default_charset = tis620 ได้เลย

ข้อคิดที่ได้
  • python เป็นภาษาที่เข้าใจง่าย
  • Editor ที่ render สวยๆ นี่ช่วยให้ใล่ source code ได้ง่ายขึ้นเยอะ
    ผมลองเปรียบเทียบ vi กับ textmate ดู
    ผมรู้สึกว่า textmate มีแรงเสียดทานในการอ่าน source code น้อยกว่า


Note:
หลังจากเข้าไป check ดูใน Trac Project แล้ว
พบว่าเขามีการ refactor แก้ไขส่วน mime-type กับ charset ให้ถูกต้องแล้ว
ใน changeset 2653
แต่ยังอยู่ใน trunk อยู่ ยังไม่ได้ release ออกมา

Related link from Roti

Tuesday, January 17, 2006

Ruby String & Unicode

ใครที่ใช้ rails + utf8 โปรดระวัง String ของคุณให้ดี
ลอง run code นี้ดู
puts "สวัสดีครับ"[0..6]


อ่านเพิ่มเติม
Encoding in Rails

Related link from Roti

Monday, January 16, 2006

Lisp เข้าสิง

ตั้งแต่นั่งเรียน SICP ผ่าน online course
หลังๆนี่รู้สึก จิตใจไม่อยู่กับเนื้อกับตัวเท่าไร
java นี่ดูจึดๆไปเลย (ความรู้สึกคล้ายๆกับที่เคยรู้สึกกับ cobol)
python ที่ซื้อหนังสือมา ก็วางกองไว้บนหิ้ง
ruby ก็ชักคลายเสน่ห์

emacs ที่เคย ส่ายหัวให้
ก็ดูสวยขึ้นทุกวัน
laptop ที่ลง linux ไว้ ก็ config ให้ boot
ขึ้นมาที่ level 3
(ใช้ text base เยอะเข้า ชักเคยตัว)

mouse ที่เคยจับบ่อยๆ ก็ลดน้อยถอยลง
(อิทธิพลจาก emacs)

เวลามองหา topic อ่านบน net
เจอคำว่า lisp ไม่ได้ ต้องแอบ click เข้าไปดู

มีอยู่คนหนึ่งเขาเขียนไว้ใน comment ของ Lisp is sin
ตลกดี

lisp is like the curse of the Pharaoes.
...
Once you have read the papyrus carrying the curse, the symbols stick in your mind. You find you are spending more and more time contemplating them, trying to unriddle them. First it's in the back of your mind. Then it's what you think about for hours of the day. Eventually you spend every waking moment mumbling its strange words to yourself, words which create a private world in your own mind, a world that you lose yourself in. Eventually, you are no longer a man.

Related link from Roti

ลง Gentoo

พักหลังๆมานี่ ผมเกิดอาการเบื่อ Ubuntu ขึ้นมา
เพราะตั้งแต่ upgrade มันมานี่ มัน boot ช้าเหลือเกิน
ก็เลยหาเหาใส่หัว หา distro ใหม่มาลงดีกว่า

ตอนแรกก็มอง SuSe ไว้
เพราะเคยใช้ 9.0 แล้วมันก็ใช้ง่ายดี
แต่ไม่รู้อะไรเกิดดลใจ
ก็เลยลอง Gentoo ดู

ได้เรื่องเลย
เวลาหายไป 3 วัน
วันแรกหมดไปกับการ config kernel
เพราะไม่เคยทำมาก่อน
อันนี้สนุกดี ได้ความรู้

วันที่สองหมดไปกับการ build gnome
(10 กว่าชั่วโมง)
กับการทำสวย และภาษาไทย

วันที่ 3
วันนี้หมดไปกับ ปรับ สถาพแวดล้อมการทำงาน
เช่น ปรับแต่ง Emacs
โดยแก้ script(el) ที่ใช้อยู่บน mac os ให้
สามารถ sync ไปใช้บน Linux ได้ทันที

หมดไป 3 วัน แต่ก็คุ้ม เพราะได้เรียนรู้อะไรใหม่ๆเต็มไปหมดเลย

สุดท้ายก็ได้เครื่องที่ boot เร็วสมใจ
เหลือแต่ยังทำ sleep mode ไม่ได้
ว่าจะลองเอา kernel source ที่ patch suspend2 มาลอง

Related link from Roti