add a script which compares two yams schemas (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

This commit is first naive implementation. Later improvement is wished.

authorRémi Cardona <remi.cardona@logilab.fr>
changesetb739d650ba6a
branchdefault
phasepublic
hiddenno
parent revision#7ea1ee1f433a merge with stable
child revision#99ef582141c7 [schema] Add a new TYPE_PROPERTIES for adding new data types on the fly, #9c6c703b77c7 [schema] Add a new TYPE_PROPERTIES for adding new data types on the fly, #f84e4fafad0c [buildobj] Allow to define specific parameters in _make_type function , closes #124342, #490deb287d19 [schema] Add a new TYPE_PROPERTIES for adding new data types on the fly, #13cc4ee3396e [buildobj] Allow to define specific parameters in _make_type function , closes #124342, #c47bc83c252c [buildobj] Allow to define specific parameters in _make_type function , closes #124342
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 1363886946 -3600
# Thu Mar 21 18:29:06 2013 +0100
# Node ID b739d650ba6aff7b6bfa49faf950b16b55e6a646
# Parent 7ea1ee1f433a874c00299edf8dda5fbce6507f4f
add a script which compares two yams schemas (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

This commit is first naive implementation. Later improvement is wished.

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