add a script which compared two yams schema (closes #112914)

  • by default : entities, attributes and properties of each schema are stored ordered in a file
  • a diff tool can be specified to compare the two generated files
authorR?mi Cardona <remi.cardona@logilab.fr>
changesetd3cc7a73bce8
branchdefault
phasedraft
hiddenyes
parent revision#b74ee5b6da96 backport stable
child revision<not specified>
files modified by this revision
diff.py
test/data/schema1.txt
test/data/schema2.txt
test/unittest_diff.py
# HG changeset patch
# User RĂ©mi Cardona <remi.cardona@logilab.fr>
# Date 1354288794 -3600
# Fri Nov 30 16:19:54 2012 +0100
# Node ID d3cc7a73bce8df7c3b5e367c196d49ebee550653
# Parent b74ee5b6da96fbaeeda3206f2da1d6b35e095565
add a script which compared two yams schema (closes #112914)

- by default : entities, attributes and properties of each schema are stored ordered in a file

- a diff tool can be specified to compare the two generated files

diff --git a/diff.py b/diff.py
@@ -0,0 +1,114 @@
1 +import subprocess
2 +import tempfile
3 +import os
4 +
5 +from yams import MARKER
6 +from yams.buildobjs import REL_PROPERTIES
7 +from yams.constraints import (SizeConstraint,
8 +                              UniqueConstraint,
9 +                              StaticVocabularyConstraint)
10 +from yams.reader import SchemaLoader
11 +
12 +
13 +ATTR_CARD_CONVERTER = {'11': True, '?1': False}
14 +
15 +def properties_from(attr, is_relation=False):
16 +    """return a dictionnaries containing properties of an attributes
17 +    or a relation (if specified with is_relation option)"""
18 +    ret = {}
19 +    for prop in REL_PROPERTIES:
20 +        try:
21 +            val = getattr(attr, prop)
22 +        except AttributeError:
23 +            continue
24 +        if prop == 'cardinality' and not is_relation: #for attibutes only
25 +            prop = 'required'
26 +            val = ATTR_CARD_CONVERTER[val]
27 +
28 +        if prop == 'constraints' and val is not MARKER:
29 +            for constraint in val:
30 +                if isinstance(constraint, SizeConstraint):
31 +                    ret['maxsize'] = constraint.max
32 +                elif isinstance(constraint, UniqueConstraint):
33 +                    ret['unique'] = True
34 +                elif isinstance(constraint, StaticVocabularyConstraint):
35 +                    ret['vocabulary'] = constraint.vocabulary()
36 +            continue
37 +
38 +        if val is not MARKER:
39 +            ret[prop] = val
40 +
41 +    return ret
42 +
43 +def schema2descr(schema):
44 +    """convert a yams schema into a text description"""
45 +    txt = ""
46 +    for entity in sorted(schema.entities(), key=lambda e: e.type):
47 +        txt += "%s\n" % str(entity.type)
48 +
49 +        attributes = [(attr[0].type,
50 +                       attr[1].type,
51 +                       properties_from(entity.rdef(attr[0].type)))
52 +                      for attr in entity.attribute_definitions()]
53 +        for attr_name, attr_type, attr_props in attributes:
54 +            txt += "\t%s: %s\n" % (attr_name, attr_type)
55 +            for k, v in attr_props.iteritems():
56 +                txt +="\t\t%s=%s\n" % (k, v)
57 +
58 +        relations = [(rel[0].type,
59 +                      rel[1][0].type,
60 +                      properties_from(entity.rdef(rel[0].type), True))
61 +                     for rel in entity.relation_definitions() if rel[2] == 'subject']
62 +        for rel_name, rel_type, rel_props in relations:
63 +            txt += "\t%s: %s\n" % (rel_name, rel_type)
64 +            for k, v in rel_props.iteritems():
65 +                txt += "\t\t%s=%s\n" % (k, v)
66 +    return txt
67 +
68 +def schema2file(schema, output):
69 +    """Save schema description of schema find
70 +    in directory schema_dir into output file"""
71 +    description_file = open(output, "w")
72 +    description_file.write(schema2descr(schema))
73 +    description_file.close()
74 +
75 +def schema_diff(schema1, schema2, diff_tool=None):
76 +    """schema 1 and 2 are CubicwebSchema
77 +    diff_tool is the name of tool use for file comparison
78 +    """
79 +    tmpdir = tempfile.mkdtemp()
80 +    output1 = os.path.join(tmpdir, "schema1.txt")
81 +    output2 = os.path.join(tmpdir, "schema2.txt")
82 +    schema2file(schema1, output1)
83 +    schema2file(schema2, output2)
84 +    if diff_tool:
85 +        cmd = "%s %s %s" %(diff_tool,
86 +                           output1, output2)
87 +        process = subprocess.Popen(cmd, shell=True)
88 +    else:
89 +        print "description files save in %s and %s" % (output1, output2)
90 +    return output1, output2
91 +
92 +if __name__ == "__main__":
93 +    from optparse import OptionParser
94 +
95 +    parser = OptionParser()
96 +    parser.add_option("-f", "--first-schema", dest="schema1",
97 +                      help= "Specify the directory of the first schema")
98 +    parser.add_option("-s", "--second-schema", dest="schema2",
99 +                      help= "Specify the directory of the second schema")
100 +    parser.add_option("-d", "--diff-tool", dest="diff_tool",
101 +                      help= "Specify the name of the diff tool")
102 +    (options, args) = parser.parse_args()
103 +    if options.schema1 and options.schema2:
104 +        schema1 = SchemaLoader().load([options.schema1])
105 +        schema2 = SchemaLoader().load([options.schema2])
106 +        output1, output2 = schema_diff(schema1, schema2)
107 +        if options.diff_tool:
108 +            cmd = "%s %s %s" %(options.diff_tool,
109 +                               output1, output2)
110 +            process = subprocess.Popen(cmd, shell=True)
111 +        else:
112 +            print "description files save in %s and %s" % (output1, output2)
113 +    else:
114 +        parser.error("An input file name must be specified")
diff --git a/test/data/schema1.txt b/test/data/schema1.txt
@@ -0,0 +1,65 @@
115 +Affaire
116 +	nom: String
117 +		uid=False
118 +		default=None
119 +		internationalizable=False
120 +		required=False
121 +		fulltextindexed=False
122 +		indexed=False
123 +		order=1
124 +		description=
125 +	associate_affaire: Affaire
126 +		composite=None
127 +		cardinality=**
128 +		order=2
129 +		description=
130 +BigInt
131 +Boolean
132 +Bytes
133 +Date
134 +Datetime
135 +Decimal
136 +Float
137 +Int
138 +Interval
139 +Password
140 +PersonAttrMod
141 +	nom: String
142 +		uid=False
143 +		default=None
144 +		internationalizable=False
145 +		required=False
146 +		fulltextindexed=False
147 +		indexed=False
148 +		order=1
149 +		description=
150 +	prenom: Float
151 +		description=
152 +		default=None
153 +		required=False
154 +		indexed=False
155 +		order=2
156 +		uid=False
157 +PersonBase
158 +	nom: String
159 +		uid=False
160 +		default=None
161 +		internationalizable=False
162 +		required=False
163 +		fulltextindexed=False
164 +		indexed=False
165 +		order=1
166 +		description=
167 +	prenom: String
168 +		uid=False
169 +		default=None
170 +		internationalizable=False
171 +		required=False
172 +		fulltextindexed=False
173 +		indexed=False
174 +		order=2
175 +		description=
176 +String
177 +TZDatetime
178 +TZTime
179 +Time
diff --git a/test/data/schema2.txt b/test/data/schema2.txt
@@ -0,0 +1,61 @@
180 +Affaire
181 +	nom: String
182 +		uid=False
183 +		default=None
184 +		internationalizable=False
185 +		required=False
186 +		fulltextindexed=False
187 +		maxsize=150
188 +		indexed=False
189 +		order=1
190 +		description=
191 +	numero: Int
192 +		description=
193 +		default=None
194 +		required=False
195 +		indexed=False
196 +		order=2
197 +		uid=False
198 +	associate_affaire: Affaire
199 +		composite=None
200 +		cardinality=**
201 +		order=3
202 +		description=
203 +	associate_person: PersonBase
204 +		composite=None
205 +		cardinality=**
206 +		order=4
207 +		description=
208 +BigInt
209 +Boolean
210 +Bytes
211 +Date
212 +Datetime
213 +Decimal
214 +Float
215 +Int
216 +Interval
217 +Password
218 +PersonBase
219 +	nom: String
220 +		uid=False
221 +		default=None
222 +		internationalizable=False
223 +		required=False
224 +		fulltextindexed=False
225 +		indexed=False
226 +		order=1
227 +		description=
228 +	prenom: String
229 +		uid=False
230 +		default=None
231 +		internationalizable=False
232 +		required=False
233 +		fulltextindexed=False
234 +		indexed=False
235 +		order=2
236 +		description=
237 +String
238 +TZDatetime
239 +TZTime
240 +Time
diff --git a/test/unittest_diff.py b/test/unittest_diff.py
@@ -0,0 +1,130 @@
241 +# copyright 2004-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
242 +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
243 +#
244 +# This file is part of yams.
245 +#
246 +# yams is free software: you can redistribute it and/or modify it under the
247 +# terms of the GNU Lesser General Public License as published by the Free
248 +# Software Foundation, either version 2.1 of the License, or (at your option)
249 +# any later version.
250 +#
251 +# yams is distributed in the hope that it will be useful, but WITHOUT ANY
252 +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
253 +# A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
254 +# details.
255 +#
256 +# You should have received a copy of the GNU Lesser General Public License along
257 +# with yams. If not, see <http://www.gnu.org/licenses/>.
258 +"""unit tests for module yams.diff"""
259 +
260 +from logilab.common.testlib import TestCase, unittest_main
261 +
262 +from yams.schema import Schema
263 +from yams.reader import build_schema_from_namespace
264 +from yams.diff import properties_from, schema_diff
265 +from yams.buildobjs import (String, Int, EntityType,
266 +                            Float, SubjectRelation)
267 +import os.path as osp
268 +
269 +DATADIR = osp.abspath(osp.join(osp.dirname(__file__), 'data'))
270 +
271 +def read_file(filename):
272 +    f = open(filename, "r")
273 +    schema = f.read()
274 +    f.close()
275 +    return schema
276 +
277 +class PersonBase(EntityType):
278 +    nom    = String()
279 +    prenom = String()
280 +
281 +class PersonAttrMod(EntityType):
282 +    nom    = String()
283 +    prenom = Float()
284 +
285 +class PersonAttrAdd(EntityType):
286 +    nom    = String()
287 +    prenom = String()
288 +    age = Int()
289 +    is_friend_of = SubjectRelation('PersonAttrMod')
290 +
291 +class PersonAttrAdd2(EntityType):
292 +    nom    = String()
293 +    prenom = String()
294 +    salaire = Float()
295 +    is_friend_of = SubjectRelation('PersonAttrAdd')
296 +
297 +class PersonAttrAdd3(EntityType):
298 +    nom     = String()
299 +    prenom  = String()
300 +    age     = Int()
301 +    salaire = Float()
302 +    is_spouse_of = SubjectRelation('PersonAttrAdd')
303 +
304 +def create_schema_1():
305 +    class Affaire(EntityType):
306 +        nom = String()
307 +        associate_affaire = SubjectRelation('Affaire')
308 +    schema = build_schema_from_namespace([('PersonBase', PersonBase),
309 +                                          ('Affaire',    Affaire),
310 +                                          ('PersonAttrMod', PersonAttrMod)])
311 +    return schema
312 +
313 +
314 +def create_schema_2():
315 +    class Affaire(EntityType):
316 +        nom = String(maxsize=150)
317 +        numero = Int()
318 +        associate_affaire = SubjectRelation('Affaire', cardinality="**")
319 +        associate_person = SubjectRelation('PersonBase')
320 +    schema = build_schema_from_namespace([('PersonBase', PersonBase),
321 +                                          ('Affaire',    Affaire)])
322 +    return schema
323 +
324 +class PropertiesFromTC(TestCase):
325 +
326 +    def test_properties_from_final_attributes(self):
327 +
328 +        props_ref = {'required': True, 'default': "toto", 'composite': True,
329 +                     'inlined': True, 'description': "something"}
330 +        s = String(**props_ref)
331 +        props = properties_from(s)
332 +        self.assertTrue(props == props_ref)
333 +
334 +        s = String()
335 +        self.assertTrue(properties_from(s) == {'required': False})
336 +
337 +        props_ref = {'default': None, 'required': False}
338 +        s = String(**props_ref)
339 +        self.assertTrue(properties_from(s) == props_ref)
340 +
341 +    def test_constraint_properties(self):
342 +
343 +        props_ref = {'maxsize': 150, 'required': False}
344 +        s = String(**props_ref)
345 +        self.assertTrue(properties_from(s) == props_ref)
346 +
347 +        props_ref = {'unique': True, 'required': False}
348 +        s = String(**props_ref)
349 +        self.assertTrue(properties_from(s) == props_ref)
350 +
351 +        props_ref = {'vocabulary': ('aaa', 'bbbb', 'ccccc'), 'required': False, "maxsize": 20}
352 +        s = String(**props_ref)
353 +        props_ref['maxsize'] = 5
354 +        self.assertTrue(properties_from(s) == props_ref)
355 +
356 +
357 +class SchemaDiff(TestCase):
358 +
359 +
360 +    def test_schema_diff(self):
361 +        schema1 = create_schema_1()
362 +        schema2 = create_schema_2()
363 +        output1, output2 = schema_diff(schema1, schema2)
364 +        ref1 = osp.join(DATADIR, 'schema1.txt')
365 +        ref2 = osp.join(DATADIR, 'schema2.txt')
366 +        self.assertEqual(read_file(output1), read_file(ref1))
367 +        self.assertEqual(read_file(output2), read_file(ref2))
368 +
369 +if __name__ == '__main__':
370 +    unittest_main()