Source code for c4dtools.resource

# coding: utf-8
#
# Copyright (c) 2012-2013, Niklas Rosenstein
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met: 
# 
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer. 
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution. 
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# 
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be interpreted
# as representing official policies,  either expressed or implied, of
# the FreeBSD Project.
r"""
c4dtools.resource
~~~~~~~~~~~~~~~~~
"""

import os
import re
import c4d
import json
import inspect
import warnings
import functools

from c4dtools import utils
from c4dtools import helpers

[docs]def load(filename, use_cache=True, cache_suffix='cache'): r""" Load the symbols of a Cinema 4D resource file. The symbols will be loaded directly from the symbols file when *use_cache* is False. In the other case, the symbols are loaded from the cached symbols if the symbols haven't changed since the cache has been generated. If the cache is not available, it will be generated when *use_cache* is True. The advantage of caching the symbols in a seperate file is the improved speed of reading in the symbols. :Returns: ``(symbols_dict, changed)`` :Raises: OSError if *filename* does not exist or does not point to a file. """ if not os.path.isfile(filename): raise OSError('passed filename does not exist or does not point to ' 'a file: %s' % filename) cache_name = utils.change_suffix(filename, cache_suffix) # Load the cache if desired and available. load_from_source = True original_changed = False load_from_cache = False if use_cache and os.path.isfile(cache_name): original_changed = utils.file_changed(filename, cache_name) load_from_cache = not original_changed if load_from_cache: fp = open(cache_name, 'rb') data = None try: data = json.load(fp) except ValueError as exc: warnings.warn(exc.message + '. Loading symbols from source.') finally: fp.close() if isinstance(data, dict): # Convert every key in the dictionary from unicode to # a string. for key, value in data.items(): del data[key] data[key.encode('utf-8')] = value load_from_source = False symbols = data elif data is not None: message = 'loaded %s, expected dict from JSON. Loading symbols ' \ 'from source.' message = message % data.__class__.__name__ warnings.warn(message) # If the cache could not be loaded, load the symbols from the # original file. if load_from_source: fp = open(filename, 'rb') try: symbols = parse_symbols(fp.read()) finally: fp.close() # If the cache should be used, we will now generate it. if use_cache: with open(cache_name, 'wb') as cache_fp: json.dump(symbols, cache_fp) return symbols, original_changed
[docs]def parse_symbols(string): r""" Parse symbols from the passed string containing the enumerations to load. """ # Remove all comments from the source. string = ' '.join(line.split('//')[0] for line in string.splitlines()) string = ' '.join(re.split(r'\/\*.*\*\/', string)) # Extract all enumeration declarations from the source. enumerations = [ text.split('{')[1].split('}')[0] for text in re.split(r'\benum\b', string)[1:] ] # Load the symbols. symbols = {} for enum in enumerations: last_value = -1 for name in enum.split(','): if '=' in name: name, value = name.split('=') value = int(value) else: value = last_value + 1 name = name.strip() if name: last_value = value symbols[name] = value return symbols
[docs]class Resource(object): r""" An instance of this class is used to store the symbols of a C4D symbols file and several other information to work with the resource of a plugin, such as easily grabbing files from that folder, etc. .. attribute:: dirname The directory name of the resource-folder. Not garuanteed to exist! .. attribute:: c4dres The :class:`c4d.plugins.GeResource` object passed on construction. This is usually the ``__res__`` variable passed through from a Python plugin. .. attribute:: string A :class:`StringLoader` instance associated with the resource object. Used to load resource strings. .. code-block:: python # Load a resource-string with the IDC_MYSTRING symbol-name # without formatting arguments. res.string.IDC_MYSTRING() # Or call the str() function on the returned ResourceString # instance. str(res.string.IDC_MYSTRING) # Format the resource string by replacing the hashes in # the string with the passed arguments. res.string.IDC_FILENOTFOUND(filename) # Unpack the tuple returned by the `both` property. # Shortcut for: # container.SetString( # res.IDC_CONTEXTMENU_1, # res.string.IDC_CONTEXTMENU_1()) container.SetString(*res.string.IDC_CONTEXTMENU_1.both) .. attribute:: changed This attribute is set by the :func:`load` function and is only True when the resource was cached *and* has changed, therefore the cache was rebuilt. When symbol-caching is deactivated, this attribute will always be False. """ def __init__(self, dirname, c4dres, symbols={}): super(Resource, self).__init__() self.dirname = dirname self.c4dres = c4dres self.string = StringLoader(self) self.symbols = symbols self.changed = False def __getattr__(self, name): return self.symbols[name] @property def symbols(self): return self._symbols @symbols.setter def symbols(self, symbols): self.highest_symbol = -100000 self._symbols = {} self.add_symbols(symbols)
[docs] def get(self, name): r""" Returns the value of the symbol with the passed name, or raises KeyError if no symbol was found. """ return self.symbols[name]
def has_symbol(self, name): return name in self.symbols
[docs] def add_symbols(self, symbols): r""" Add the dictionary *symbols* to the resources symbols. Raises: TypeError if *symbols* is not a dict instance. KeyError if a key in *symbols* is already defined in the resource and their value differs. """ utils.ensure_type(symbols, dict) res_symbols = self.symbols for key, value in symbols.iteritems(): if key in res_symbols and res_symbols[key] != value: msg = 'key %r already defined in the resource and ' \ 'the value differs from the updating symbols.' raise KeyError(msg % key) if value > self.highest_symbol: self.highest_symbol = value res_symbols.update(symbols)
[docs] def get_symbol_name(self, id_): r""" Returns the name of the passed symbol id. """ for k, v in self.symbols.iteritems(): if v == id_: return k
[docs] def file(self, *path_parts): r""" Concatenate the resource folders path with the passed filename. """ return os.path.join(self.dirname, *path_parts)
[docs]class StringLoader(object): r""" This class is used for conveniently loading strings from the c4d_strings.str file. It is basically a wrapper for the ``c4d.plugin.GeLoadString`` function. Accessing an attribute on an instance of this class will return a callable object accepting the same parameters as the previously mentioned API call, but with the symbol-id already passed. """ def __init__(self, resource): r""" Initialize the StringLoader instance with an instance of the Resource class. Raises: TypeError when *resource* is not a Resource instance. """ if not isinstance(resource, Resource): raise TypeError('expected %s.Resource instance.' % inspect.getmodule(Resource)) self.resource = resource def __getattr__(self, name): r""" Load a string from the resource by the given *name*. Returns a :class:`ResourceString` object. .. code-block:: python id = res.IDC_MYSTRING name = res.string.IDC_MYSTRING() # same as id, name = res.string.IDC_MYSTRING.tuple """ symbol_id = self.resource.get(name) return ResourceString(symbol_id, self.resource.c4dres) def get(self, name): if isinstance(name, int): return ResourceString(name, self.resource.c4dres) return getattr(self, name) def has_symbol(self, name): return self.resource.has_symbol(name)
[docs]class ResourceString(object): r""" This class represents a resource-string loaded from plugin resource. """ def __init__(self, id, c4dres): super(ResourceString, self).__init__() self.id = id self.c4dres = c4dres def __call__(self, *args): r""" Wrapper for the :func:`c4d.plugins.GeLoadString` function for loading the actual string from the resource. """ string = self.c4dres.LoadString(self.id) # Simulate the behaviour of c4d.plugins.GeLoadString by replacing # all hashes (`#`) with a passed arguments. for arg in args: string = string.replace('#', str(arg), 1) return string def __str__(self): r""" New in 1.2.8. Equal to :met:`__call__` without passing parameters. """ return self() @property
[docs] def both(self): r""" Returns a tuple of the ``(id, string)`` where *string* is loaded from the plugin's resource. """ return self.id, self()