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