1# Copyright 2004-2008 Roman Yakovenko. 2# Distributed under the Boost Software License, Version 1.0. (See 3# accompanying file LICENSE_1_0.txt or copy at 4# http://www.boost.org/LICENSE_1_0.txt) 5 6import os 7import sys 8import time 9import types 10import warnings 11from . import module_builder 12 13from pygccxml import parser 14from pygccxml.utils import utils as pygccxml_utils 15from pygccxml import declarations as decls_package 16 17from pyplusplus import utils 18from pyplusplus import _logging_ 19from pyplusplus import decl_wrappers 20from pyplusplus import file_writers 21from pyplusplus import code_creators 22from pyplusplus import creators_factory 23 24if sys.version_info.major == 3: 25 timer = time.perf_counter 26else: 27 timer = time.clock 28 29class builder_t(module_builder.module_builder_t): 30 """ 31 This class provides users with simple and intuitive interface to `Py++` 32 and/or pygccxml functionality. If this is your first attempt to use `Py++` 33 consider to read tutorials. 34 """ 35 36 def __init__( self 37 , files 38 , gccxml_path='' 39 , xml_generator_path='' 40 , working_directory='.' 41 , include_paths=None 42 , define_symbols=None 43 , undefine_symbols=None 44 , start_with_declarations=None 45 , compilation_mode=None 46 , cache=None 47 , optimize_queries=True 48 , ignore_gccxml_output=False 49 , indexing_suite_version=1 50 , cflags="" 51 , encoding='ascii' 52 , compiler=None 53 , gccxml_config=None 54 , xml_generator_config=None): 55 """ 56 :param files: list of files, declarations from them you want to export 57 :type files: list of strings or :class:`parser.file_configuration_t` instances 58 59 :param xml_generator_path: path to gccxml/castxml binary. If you don't pass this argument, 60 pygccxml parser will try to locate it using your environment PATH variable 61 :type xml_generator_path: str 62 63 :param include_paths: additional header files location. You don't have to 64 specify system and standard directories. 65 :type include_paths: list of strings 66 67 :param define_symbols: list of symbols to be defined for preprocessor. 68 :param define_symbols: list of strings 69 70 :param undefine_symbols: list of symbols to be undefined for preprocessor. 71 :param undefine_symbols: list of strings 72 73 :param cflags: Raw string to be added to xml generator command line. 74 75 :param xml_generator_config: instance of pygccxml.parser.xml_generator_configuration_t class, holds 76 xml generator configuration. You can use this 77 argument instead of passing the compiler configuration separately. 78 79 :param gccxml_path: DEPRECATED 80 :param gccxml_config: DEPRECATED 81 """ 82 module_builder.module_builder_t.__init__( self, global_ns=None, encoding=encoding ) 83 84 # handle deprecated parameters 85 if not gccxml_path == '' and xml_generator_path == '': 86 xml_generator_path = gccxml_path 87 if gccxml_config and not xml_generator_config: 88 xml_generator_config = gccxml_config 89 90 if not xml_generator_config: 91 xml_generator_config = parser.xml_generator_configuration_t( xml_generator_path=xml_generator_path 92 , working_directory=working_directory 93 , include_paths=include_paths 94 , define_symbols=define_symbols 95 , undefine_symbols=undefine_symbols 96 , start_with_declarations=start_with_declarations 97 , ignore_gccxml_output=ignore_gccxml_output 98 , cflags=cflags 99 , compiler=compiler) 100 101 #may be in future I will add those directories to user_defined_directories to self.__code_creator. 102 self.__parsed_files = list(map( pygccxml_utils.normalize_path 103 , parser.project_reader_t.get_os_file_names( files ) )) 104 tmp = [os.path.split( file_ )[0] for file_ in self.__parsed_files] 105 self.__parsed_dirs = [_f for _f in tmp if _f] 106 107 self.global_ns = self.__parse_declarations( files 108 , xml_generator_config 109 , compilation_mode 110 , cache 111 , indexing_suite_version) 112 self.global_ns.decls(recursive=True, allow_empty=True)._code_generator = decl_wrappers.CODE_GENERATOR_TYPES.CTYPES 113 114 self.__code_creator = None 115 if optimize_queries: 116 self.run_query_optimizer() 117 118 self.__declarations_code_head = [] 119 self.__declarations_code_tail = [] 120 121 self.__registrations_code_head = [] 122 self.__registrations_code_tail = [] 123 124 125 126 def register_module_dependency( self, other_module_generated_code_dir ): 127 """ 128 `already_exposed` solution is pretty good when you mix hand-written 129 modules with `Py++` generated. It doesn't work/scale for "true" 130 multi-module development. This is exactly the reason why `Py++` 131 offers "semi automatic" solution. 132 133 For every exposed module, `Py++` generates `exposed_decl.pypp.txt` file. 134 This file contains the list of all parsed declarations and whether they 135 were included or excluded. Later, when you work on another module, you 136 can tell `Py++` that the current module depends on the previously 137 generated one. `Py++` will load `exposed_decl.pypp.txt` file and update 138 the declarations. 139 """ 140 141 db = utils.exposed_decls_db_t() 142 db.load( other_module_generated_code_dir ) 143 db.update_decls( self.global_ns ) 144 145 146 def __parse_declarations( self, files, xml_generator_config, compilation_mode, cache, indexing_suite_version ): 147 if None is xml_generator_config: 148 xml_generator_config = parser.xml_generator_configuration_t() 149 if None is compilation_mode: 150 compilation_mode = parser.COMPILATION_MODE.FILE_BY_FILE 151 start_time = timer() 152 self.logger.debug( 'parsing files - started' ) 153 reader = parser.project_reader_t( xml_generator_config, cache, decl_wrappers.dwfactory_t() ) 154 decls = reader.read_files( files, compilation_mode ) 155 156 self.logger.debug( 'parsing files - done( %f seconds )' % ( timer() - start_time ) ) 157 self.logger.debug( 'settings declarations defaults - started' ) 158 159 global_ns = decls_package.matcher.get_single( 160 decls_package.namespace_matcher_t( name='::' ) 161 , decls ) 162 if indexing_suite_version != 1: 163 for cls in global_ns.classes(): 164 cls.indexing_suite_version = indexing_suite_version 165 for cls in global_ns.decls(decl_type=decls_package.class_declaration_t): 166 cls.indexing_suite_version = indexing_suite_version 167 168 start_time = timer() 169 self.__apply_decls_defaults(decls) 170 self.logger.debug( 'settings declarations defaults - done( %f seconds )' 171 % ( timer() - start_time ) ) 172 return global_ns 173 174 def __filter_by_location( self, flatten_decls ): 175 for declaration in flatten_decls: 176 if not declaration.location: 177 continue 178 fpath = pygccxml_utils.normalize_path( declaration.location.file_name ) 179 if pygccxml_utils.contains_parent_dir( fpath, self.__parsed_dirs ): 180 continue 181 if fpath in self.__parsed_files: 182 continue 183 found = False 184 for pfile in self.__parsed_files: 185 if fpath.endswith( pfile ): 186 found = True 187 break 188 if not found: 189 declaration.exclude() 190 191 def __apply_decls_defaults(self, decls): 192 flatten_decls = decls_package.make_flatten( decls ) 193 self.__filter_by_location( flatten_decls ) 194 call_policies_resolver = creators_factory.built_in_resolver_t() 195 calldefs = [declaration for declaration in flatten_decls if isinstance( declaration, decls_package.calldef_t )] 196 for calldef in calldefs: 197 calldef.set_call_policies( call_policies_resolver( calldef ) ) 198 mem_vars = [declaration for declaration in flatten_decls if isinstance( declaration, decls_package.variable_t ) 199 and isinstance( declaration.parent, decls_package.class_t )] 200 for mem_var in mem_vars: 201 mem_var.set_getter_call_policies( call_policies_resolver( mem_var, 'get' ) ) 202 for mem_var in mem_vars: 203 mem_var.set_setter_call_policies( call_policies_resolver( mem_var, 'set' ) ) 204 205 @property 206 def declarations_code_head( self ): 207 "A list of the user code, which will be added to the head of the declarations section." 208 return self.__declarations_code_head 209 210 @property 211 def declarations_code_tail( self ): 212 "A list of the user code, which will be added to the tail of the declarations section." 213 return self.__declarations_code_tail 214 215 @property 216 def registrations_code_head( self ): 217 "A list of the user code, which will be added to the head of the registrations section." 218 return self.__registrations_code_head 219 220 @property 221 def registrations_code_tail( self ): 222 "A list of the user code, which will be added to the tail of the registrations section." 223 return self.__registrations_code_tail 224 225 def build_code_creator( self 226 , module_name 227 , boost_python_ns_name='bp' 228 , call_policies_resolver_=None 229 , types_db=None 230 , target_configuration=None 231 , enable_indexing_suite=True 232 , doc_extractor=None): 233 """ 234 Creates :class:`code_creators.bpmodule_t` code creator. 235 236 :param module_name: module name 237 :type module_name: str 238 239 :param boost_python_ns_name: boost::python namespace alias, by default it is `bp` 240 :type boost_python_ns_name: str 241 242 :param call_policies_resolver_: callable, that will be invoked on every calldef object. It should return call policies. 243 :type call_policies_resolver_: callable 244 245 :param doc_extractor: callable, that takes as argument reference to declaration and returns documentation string 246 :type doc_extractor: callable or None 247 """ 248 249 creator = creators_factory.bpcreator_t( self.global_ns 250 , module_name 251 , boost_python_ns_name 252 , call_policies_resolver_ 253 , types_db 254 , target_configuration 255 , enable_indexing_suite ) 256 self.__code_creator = creator.create() 257 self.__code_creator.replace_included_headers(self.__parsed_files) 258 self.__code_creator.update_documentation( doc_extractor ) 259 return self.__code_creator 260 261 @property 262 def code_creator( self ): 263 "reference to :class:`code_creators.bpmodule_t` instance" 264 if not self.__code_creator: 265 raise RuntimeError( "self.module is equal to None. Did you forget to call build_code_creator function?" ) 266 return self.__code_creator 267 268 def has_code_creator( self ): 269 """ 270 Function, that will return True if build_code_creator function has been 271 called and False otherwise 272 """ 273 return not ( None is self.__code_creator ) 274 275 def add_declaration_code( self, code, tail=True ): 276 """adds the user code to the generated one""" 277 if tail: 278 self.__declarations_code_tail.append( code ) 279 else: 280 self.__declarations_code_head.append( code ) 281 282 def add_registration_code( self, code, tail=True ): 283 """adds the user code to the generated one""" 284 if tail: 285 self.__registrations_code_tail.append( code ) 286 else: 287 self.__registrations_code_head.append( code ) 288 289 def add_constants( self, **keywds ): 290 """ 291 adds code that exposes some constants to Python. 292 293 For example: 294 .. code-block:: python 295 296 mb.add_constants( version='"1.2.3"' ) 297 # or 298 constants = dict( version:'"1.2.3"' ) 299 mb.add_constants( \\*\\*constants ) 300 301 will generate the following code: 302 303 .. code-block:: c++ 304 305 boost::python::scope().attr("version") = "1.2.3"; 306 307 """ 308 tmpl = 'boost::python::scope().attr("%(name)s") = %(value)s;' 309 for name, value in list(keywds.items()): 310 if not isinstance( value, str ): 311 value = str( value ) 312 self.add_registration_code( tmpl % dict( name=name, value=value) ) 313 314 315 def __merge_user_code( self ): 316 for code in self.__declarations_code_tail: 317 self.code_creator.add_declaration_code( code, -1 ) 318 319 for code in self.__declarations_code_head: 320 self.code_creator.add_declaration_code( code, 0 ) 321 322 body = self.code_creator.body 323 324 for code in self.__registrations_code_tail: 325 body.adopt_creator( code_creators.custom_text_t( code ), -1 ) 326 327 for code in self.__registrations_code_head: 328 body.adopt_creator( code_creators.custom_text_t( code ), 0 ) 329 330 331 def write_module( self, file_name ): 332 """ 333 Writes module to a single file 334 335 :param file_name: file name 336 :type file_name: string 337 338 """ 339 self.__merge_user_code() 340 file_writers.write_file( self.code_creator, file_name, encoding=self.encoding ) 341 342 def __work_on_unused_files( self, dir_name, written_files, on_unused_file_found ): 343 all_files = os.listdir( dir_name ) 344 all_files = [os.path.join( dir_name, fname ) for fname in all_files] 345 all_files = list(filter( file_writers.has_pypp_extenstion, all_files )) 346 347 unused_files = set( all_files ).difference( set( written_files ) ) 348 for fpath in unused_files: 349 try: 350 if on_unused_file_found is os.remove: 351 self.logger.info( 'removing file "%s"' % fpath ) 352 on_unused_file_found( fpath ) 353 except Exception as error: 354 self.logger.exception( "Exception was catched, while executing 'on_unused_file_found' function." ) 355 356 def split_module( self 357 , dir_name 358 , huge_classes=None 359 , on_unused_file_found=os.remove 360 , use_files_sum_repository=False): 361 """ 362 writes module to multiple files 363 364 :param dir_name: directory name 365 :type dir_name: str 366 367 :param huge_classes: list that contains reference to classes, that should be split 368 369 :param on_unused_file_found: callable object that represents the action that should be taken on 370 file, which is no more in use 371 372 :param use_files_sum_repository: `Py++` can generate file, which will contain `md5` sum of every generated file. 373 Next time you generate code, md5sum will be loaded from the file and compared. 374 This could speed-up code generation process by 10-15%. 375 """ 376 self.__merge_user_code() 377 378 files_sum_repository = None 379 if use_files_sum_repository: 380 cache_file = os.path.join( dir_name, self.code_creator.body.name + '.md5.sum' ) 381 files_sum_repository = file_writers.cached_repository_t( cache_file ) 382 383 written_files = [] 384 if None is huge_classes: 385 written_files = file_writers.write_multiple_files( 386 self.code_creator 387 , dir_name 388 , files_sum_repository=files_sum_repository 389 , encoding=self.encoding) 390 else: 391 written_files = file_writers.write_class_multiple_files( 392 self.code_creator 393 , dir_name 394 , huge_classes 395 , files_sum_repository=files_sum_repository 396 , encoding=self.encoding) 397 self.__work_on_unused_files( dir_name, written_files, on_unused_file_found ) 398 399 return written_files 400 401 def balanced_split_module( self 402 , dir_name 403 , number_of_files 404 , on_unused_file_found=os.remove 405 , use_files_sum_repository=False): 406 """ 407 Writes module to fixed number of multiple cpp files 408 409 :param number_of_files: the desired number of generated cpp files 410 :type number_of_files: int 411 412 :param dir_name: directory name 413 :type dir_name: string 414 415 :param on_unused_file_found: callable object that represents the action that should be taken on 416 file, which is no more in use 417 418 :param use_files_sum_repository: `Py++` can generate file, which will contain md5 sum of every generated file. 419 Next time you generate code, md5sum will be loaded from the file and compared. 420 This could speed-up code generation process by 10-15%. 421 """ 422 self.__merge_user_code() 423 424 files_sum_repository = None 425 if use_files_sum_repository: 426 cache_file = os.path.join( dir_name, self.code_creator.body.name + '.md5.sum' ) 427 files_sum_repository = file_writers.cached_repository_t( cache_file ) 428 429 written_files = file_writers.write_balanced_files( self.code_creator 430 , dir_name 431 , number_of_buckets=number_of_files 432 , files_sum_repository=files_sum_repository 433 , encoding=self.encoding) 434 435 self.__work_on_unused_files( dir_name, written_files, on_unused_file_found ) 436 437 return written_files 438 439 def _get_BOOST_PYTHON_MAX_ARITY( self ): 440 return decl_wrappers.calldef_t.BOOST_PYTHON_MAX_ARITY 441 def _set_BOOST_PYTHON_MAX_ARITY( self, value ): 442 decl_wrappers.calldef_t.BOOST_PYTHON_MAX_ARITY = value 443 BOOST_PYTHON_MAX_ARITY = property( _get_BOOST_PYTHON_MAX_ARITY, _set_BOOST_PYTHON_MAX_ARITY ) 444