Introdução
O projeto Templates Genéricos visa disponibilizar uma biblioteca geral para expressar estruturas de dados complexas em código Python. Trata-se ainda de uma abordagem experimental. O conceito básico é simples:
A princípio, o template acima é bastante similar a qualquer outro escrito em XML, ou em um dicionário Python. Qual seriam as vantagens, então?
Legibilidade: o código em Python é bem estruturado e legível. Já o código em XML não é tão legível, e é mais difícil de editar manualmente. O dicionário também se torna confuso, porque muitas definições ficam dentro de strings, o que polui o código com aspas e chaves que não são nada 'pitônicas';
Integração: o código pode residir dentro de um arquivo convencional Python. Não há necessidade de ler a descrição de outro lugar.
Orientação a objeto: é muito fácil criar novas classes e construir estruturas dinâmicas e inteligentes, que processam os atributos de forma automatizada. Estas estruturas podem ser herdadas e reutilizadas dentro do ambiente normal de programação Python.
Aplicações
O sistema foi criado a partir de uma idéia ambiciosa: um ambiente de desenvolvimento de aplicações comerciais, capaz de converter definições de telas de entrada em múltiplos formatos de saída. Para viabilizar o desenvolvimento, optamos pelo crescimento gradual. A biblioteca de templates está sendo focada em duas aplicações até o momento:
Templates para Web: permite a especificação de páginas com layouts complexos, incluindo forms de entrada de dados. A descrição final é facilmente legível, e o uso de objetos facilita a composição de telas complexas, com vários elementos, em tempo de execução.
Exemplos:
1 class Page(htmlcontainer.HtmlPage):
2 class head(htmlcontainer.HtmlPage.head):
3 stylesheet = CSSStyleSheet
4 class body(htmlcontainer.HtmlPage.body):
5 contents = """
6 Hello World!
7 """
8
9 class FormEdicaoUsuario(Form):
10 title = 'Dados do usuário'
11 class dadosbasicos(Panel):
12 style = 'form-section'
13 apelido = EditBox(caption = 'Identificação', size = 15)
14 senha = EditBox(caption = 'Senha', size = 10, password = True)
15 nome = EditBox(caption = 'Nome completo', size = 40)
16 class endereco(Panel):
17 style = 'form-section'
18 endereco = EditBox(caption = 'Endereço', size = 40)
19 bairro = EditBox(caption = 'Bairro', size = 40)
20 cidade = EditBox(caption = 'Cidade', size = 40)
21 class extras(Panel):
22 style = 'form-section'
23 observacao= EditBox(caption = 'Observações',
24 multiline= True, rows = 10, cols = 40)
Arquivos de inicialização (.ini): permite a especificação de uma estrutura de arquivo de inicialização .ini, com a divisão em seções. Cada atributo pode ter seu tipo e um valor default definido de forma simples e legível:
O tipo TypedAttribute é um atributo especial, que quando colocado dentro de um Template, gera automaticamente uma 'property' que checa o tipo do argumento. O tipo é inferido a partir do valor default. Assim, as seguintes atribuições serão tratadas de formas diferentes:
Funcionamento
O sistema de templates depende de algumas regras básicas para garantir o funcionamento transparente. As regras são:
- Todas as classes aninhadas devem ser herdadas de Template. Classes não herdadas de Template poderão ser aninhadas, mas sem garantia de comportamento correto.
Os atributos aninhados que não forem classes devem ser herdeiros de GenericAttribute. Esta classe já tem o código necessário para operar em conjunto com o GenericTemplate.
Os atributos simples (strings, inteiros, etc.) podem ser especificados diretamente no código. A metaclasse que cria o GenericTemplate processa automaticamente estes valores, e os encapsula dentro de um atributo genérico (sem tipo).
O TypedAttribute é um herdeiro do atributo genérico, que verifica o tipo do argumento nas chamadas ao método __set__.
O sistema depende de auto-instanciamento das classes aninhadas. Por motivos diversos, é necessário que dentro de uma classe, todos os membros aninhados sejam também classes; e dentro de uma instância, todos os membros aninhados sejam instâncias. Isso é necessário para dar consistância ao sistema e evitar efeitos colaterais indesejados. Assim, ao inicializar uma classe (por exemplo, o SimpleIni() apresentado acima), todas as classes aninhadas serão automaticamente instanciadas, e a instância criada conterá somente instâncias.
Código fonte
De que vale esta conversa sem o código fonte? (Ainda não coloquei a licença; preciso de ajuda com isso! aceito sugestões!)
PS: os comentários e nomes estão em inglês. O código se propõe a ser de uso livre, e não faria sentido escrevê-lo em português. Espero que todos compreendam.
1 """
2 metatemplate.py
3
4 Template class that can be used to write complex data structures using
5 nested classes. Template classes can store other template classes (nested)
6 or user-defined attributes (typed or untyped). The original definition
7 order information is preserved, allowing for true templating use for
8 applications such as html templates, data entry forms, and configuration
9 files.
10
11 (c) 2004 Carlos Ribeiro
12 carribeiro@gmail.com
13 http:///pythonnotes.blogspot.com
14
15 """
16
17 import sys
18 from inspect import isclass, isdatadescriptor
19 from types import StringType, IntType, FloatType, ListType
20 import itertools
21
22 #----------------------------------------------------------------------
23 # Debug constants. I don't intend to remove them, even from production
24 # code, but I intend to use the logging module to print the messages
25
26 debug_generic_attribute = 0
27 debug_typed_attribute = 0
28 debug_auto_instantiation = 0
29
30 #----------------------------------------------------------------------
31 # AbstractAttribute is the ancestor of all classes that can be used
32 # in the metacontainer framework.
33
34 class AbstractAttribute(object):
35 pass
36
37 #----------------------------------------------------------------------
38 # GenericAttribute is the ancestor of all simple elements that are
39 # used as attributes of user defined Container subclasses
40 #
41 # GenericAttributes are simpler than full containers. They're both
42 # derived from the same AbstractAttribute class, but GenericAttributes
43 # have only a single value associated with them.
44 #
45 # When referred from a instance, the __get__ method returns the value
46 # associated with the attribute. If called from the class, the __get__
47 # method returns the property itself.
48
49 class GenericAttribute(AbstractAttribute):
50 """ Generic attributes for generic containers """
51 def __init__(self, default = None):
52 self._seqno = next_attribute_id()
53 self.value = default
54 def __repr__(self):
55 return "<Attr '%s'>" % (self.__class__.__name__)
56 def __get__(self, instance, owner):
57 if debug_generic_attribute:
58 print "GET self:[%s], instance:[%s], owner:[%s]" % \
59 (self, instance, owner)
60 if instance:
61 attrdict = instance.__dict__.setdefault('__attr__', {})
62 return attrdict.get(self.name, self.value)
63 else:
64 return owner
65 def __set__(self, instance, value):
66 if debug_generic_attribute:
67 print "SET self:[%s], instance:[%s], value:[%s]" % \
68 (self, instance, value)
69 attrdict = instance.__dict__.setdefault('__attr__', {})
70 attrdict[self.name] = value
71
72 class TypedAttribute(GenericAttribute):
73 """ Typed attributes for generic containers """
74 def __init__(self, default = None, mytype = None):
75 self._seqno = next_attribute_id()
76 self.value = default
77 if mytype:
78 if isclass(mytype):
79 self.mytype = mytype
80 else:
81 raise TypeError("Argument <mytype> expects None "
82 "or a valid type/class")
83 else:
84 self.mytype = type(default)
85 def __repr__(self):
86 return "<TypedAttr '%s':%s>" % \
87 (self.__class__.__name__, self.mytype.__name__)
88 def __get__(self, instance, owner):
89 if debug_typed_attribute:
90 print "GET self:[%s], instance:[%s], owner:[%s]" % \
91 (self, instance, owner)
92 if instance:
93 attrdict = instance.__dict__.setdefault('__attr__', {})
94 return attrdict.get(self.name, self.value)
95 else:
96 return self.value
97 def __set__(self, instance, value):
98 if debug_typed_attribute:
99 print "SET self:[%s], instance:[%s], value:[%s]" % \
100 (self, instance, value)
101 if not isinstance(value, self.mytype):
102 # if it's a string, tries to convert to the correct
103 # target type (this is needed because most things read
104 # from files will be strings anyway)
105 if isinstance(value, StringType):
106 value = self.mytype(value)
107 else:
108 raise TypeError, "Expected %s attribute" % \
109 self.mytype.__name__
110 attrdict = instance.__dict__.setdefault('__attr__', {})
111 attrdict[self.name] = value
112
113 #----------------------------------------------------------------------
114 # auxiliary functions
115
116 next_attribute_id = itertools.count().next
117
118 def getfields(dct):
119 """
120 takes a dictionary of class attributes and returns a decorated list
121 containing all valid field instances and their relative position.
122
123 """
124 for fname, fobj in dct.items():
125 if isinstance(fobj,GenericAttribute):
126 yield (fobj._seqno, (fname, fobj))
127 elif isclass(fobj) and issubclass(fobj,AbstractAttribute):
128 yield (fobj._seqno, (fname, fobj))
129 elif (fname[0] != '_'):
130 # conventional attributes from basic types are just stored
131 # as GenericAttributes, and put at the end of the list,
132 # in alphabetical order
133 if (isinstance(fobj,StringType) or
134 isinstance(fobj,IntType) or
135 isinstance(fobj,FloatType) or
136 isinstance(fobj,ListType)):
137 yield (sys.maxint, (fname, GenericAttribute(fobj)))
138 else:
139 yield (0, (fname, fobj))
140 else:
141 yield (0, (fname, fobj))
142
143 def makefieldsdict(dct, bases):
144 # build the field list and sort it
145 fields = list(getfields(dct))
146 fields.sort()
147 # undecorate the list and build a dict that will be returned later
148 sorted_field_list = [field[1] for field in fields]
149 field_dict = dict(sorted_field_list)
150 # finds all attributes and nested classes that are containers
151 attribute_list = [field for field in sorted_field_list
152 if (isinstance(field[1],AbstractAttribute) or
153 (isclass(field[1]) and
154 issubclass(field[1],AbstractAttribute)
155 ))]
156 # check baseclasses for attributes inherited but not overriden
157 # !!WARNING: this code does not checks correctly for multiple
158 # base classes if there are name clashes between overriden
159 # members. This is not recommended anyway.
160 inherited = []
161 for baseclass in bases:
162 base_field_list = getattr(baseclass, '_fields', None)
163 # looks for a valid _fields attribute in an ancestor
164 if isinstance(base_field_list, ListType):
165 fnames = [f[0] for f in attribute_list]
166 for fname, fobj in base_field_list:
167 # checks for overriden attributes
168 if (fname in fnames):
169 # overriden - inherited list contains the new value
170 newobj = field_dict[fname]
171 inherited.append((fname, newobj))
172 # remove attribute and quick check field names list
173 attribute_list.remove((fname, field_dict[fname]))
174 fnames.remove(fname)
175 else:
176 # copy the original entry into the inherited list
177 inherited.append((fname, fobj))
178 field_dict['_fields'] = inherited + attribute_list
179 return field_dict
180
181 #----------------------------------------------------------------------
182 # MetaTemplate metaclass
183 #
184 # Most of the hard work is done outside the class by the auxiliary
185 # functions makefieldsdict() and getfields()
186
187 class MetaTemplate(type):
188 def __new__(cls, name, bases, dct):
189 # creates the class using only the processed field list
190 newdct = makefieldsdict(dct, bases)
191 newclass = type.__new__(cls, name, bases, newdct)
192 newclass._seqno = next_attribute_id()
193 newclass.name = name
194 return newclass
195
196 #----------------------------------------------------------------------
197 # GenericTemplate superclass
198
199 class GenericTemplate(AbstractAttribute):
200 __metaclass__ = MetaTemplate
201
202 def __init__(self):
203 """ instantiates all nested classes upon creation """
204
205 # builds a copy of the field list. this is needed to allow
206 # customizations of the instance not to be reflected in the
207 # original class field list.
208 self._fields = list(self.__class__._fields)
209
210 # auto instantiates nested classes and attributes
211 if debug_auto_instantiation:
212 print "AutoInstantiation <%s>: fieldlist = %s" % \
213 (self.name, self._fields)
214 for fname, fobj in self._fields:
215 if isclass(fobj) and issubclass(fobj,Container):
216 # found a nested class
217 if debug_auto_instantiation:
218 print "AutoInstantiation <%s>: field[%s] is a "
219 "Container Subclass" % (self.name, fname)
220 fobj = fobj()
221 setattr(self, fname, fobj)
222 elif isinstance(fobj, AbstractAttribute):
223 # found an attribute instance
224 if debug_auto_instantiation:
225 print "AutoInstantiation <%s>: field[%s] is an "
226 "Attribute Instance" % (self.name, fname)
227 # removed: parent links are still being thought out,
228 # and I'm not even sure if they're a good idea
229 # setattr(fobj, 'parent', self)
230 setattr(fobj, 'name', fname)
231 else:
232 if debug_auto_instantiation:
233 print "AutoInstantiation <%s>: field[%s] is "
234 "unknown" % (self.name, fname)
235
236 def iterfields(self):
237 for fname, fobj in self._fields:
238 yield getattr(self, fname)
239
240 def __repr__(self):
241 return "<%s '%s'>" % (self.__class__.__name__, self.name,)
Exemplo de aplicação: leitor de arquivos INI
O exemplo de arquivos INI ainda está incompleto, mas já é capaz de ler arquivos usando a descrição dada pela classe. Há algumas situações que ainda não tem seu tratamento devidamente discutido; por exemplo, dentro de uma seção, podem existir subseções. O sistema ainda não sabe 'voltar' corretamente para a seção do nível anterior, dependendo da forma como o aninhamento for feito.
1 """
2 inifile.py
3
4 Reads INI configuration files based on a class template.
5 (c) 2004 Carlos Ribeiro
6 carribeiro@gmail.com
7 http:///pythonnotes.blogspot.com
8
9 """
10
11 class IniSection(GenericTemplate):
12
13 re_section = re.compile(r'^\[(.*)\]')
14
15 def read(self, fileobj):
16 for line in fileobj:
17 line = line.strip()
18 if not line: continue
19 matchresult = self.re_section.match(line)
20 if matchresult:
21 sectionname = matchresult.group(1)
22 print "section: ", sectionname
23 if sectionname in self._fields:
24 # found a known section
25 section = getattr(self, sectionname, None)
26 if isinstance(section, IniSection):
27 section.read(fileobj)
28 else:
29 pass #should raise a fatal exception
30 else:
31 # found a unknown section, back to the previous level
32 return
33 else:
34 # found an attribute
35 print line
36 name, value = line.split('=',1)
37 setattr(self, name, value)
38
39 class IniFile(IniSection):
40 def load(self, fname=None):
41 if not fname:
42 fname = self.name + '.ini'
43 inifile = open(fname, 'r')
44 self.read(inifile)
45 inifile.close()
46
47 def save(self, fname):
48 pass