#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
**parsers.py**
**Platform:**
Windows, Linux, Mac Os X.
**Description:**
Defines the :class:`SectionsFileParser` class, :class:`PlistFileParser` class
and others parsing related objects.
**Others:**
Portions of the code from Fredrik Lundh: http://effbot.org/zone/element-iterparse.htm
"""
#**********************************************************************************************************************
#*** Future imports.
#**********************************************************************************************************************
from __future__ import unicode_literals
#**********************************************************************************************************************
#*** External imports.
#**********************************************************************************************************************
import base64
import datetime
import re
import sys
from xml.etree import ElementTree
if sys.version_info[:2] <= (2, 6):
from ordereddict import OrderedDict
else:
from collections import OrderedDict
#**********************************************************************************************************************
#*** Internal imports.
#**********************************************************************************************************************
import foundations.common
import foundations.dataStructures
import foundations.exceptions
import foundations.io
import foundations.namespace
import foundations.strings
import foundations.verbose
import foundations.walkers
#**********************************************************************************************************************
#*** Module attributes.
#**********************************************************************************************************************
__author__ = "Thomas Mansencal"
__copyright__ = "Copyright (C) 2008 - 2014 - Thomas Mansencal"
__license__ = "GPL V3.0 - http://www.gnu.org/licenses/"
__maintainer__ = "Thomas Mansencal"
__email__ = "[email protected]"
__status__ = "Production"
__all__ = ["LOGGER", "AttributeCompound", "SectionsFileParser", "PlistFileParser", "getAttributeCompound"]
LOGGER = foundations.verbose.installLogger()
#**********************************************************************************************************************
#*** Module classes and definitions.
#**********************************************************************************************************************
[docs]class AttributeCompound(foundations.dataStructures.Structure):
"""
Defines a storage object for attributes compounds usually encountered in
`sIBL_GUI <https://github.com/KelSolaar/sIBL_GUI>`_ Templates files.
Some attributes compounds:
- Name = @Name | Standard | String | Template Name
- Background|BGfile = @BGfile
- showCamerasDialog = @showCamerasDialog | 0 | Boolean | Cameras Selection Dialog
"""
def __init__(self, **kwargs):
"""
Initializes the class.
Usage::
AttributeCompound(name="showCamerasDialog",
value="0",
link="@showCamerasDialog",
type="Boolean",
alias="Cameras Selection Dialog")
:param \*\*kwargs: name, value, link, type, alias.
:type \*\*kwargs: dict
"""
LOGGER.debug("> Initializing '{0}()' class.".format(self.__class__.__name__))
foundations.dataStructures.Structure.__init__(self, **kwargs)
[docs]class SectionsFileParser(foundations.io.File):
"""
Defines methods to parse sections file format files,
an alternative configuration file parser is available directly with Python: :class:`ConfigParser.ConfigParser`.
The parser given by this class has some major differences with Python :class:`ConfigParser.ConfigParser`:
- | Sections and attributes are stored in their appearance order by default.
( Using Python :class:`collections.OrderedDict` )
- | A default section ( **_default** ) will store orphans attributes
( Attributes appearing before any declared section ).
- File comments are stored inside the :obj:`SectionsFileParser.comments` class property.
- | Sections, attributes and values are whitespaces stripped by default
but can also be stored with their leading and trailing whitespaces.
- | Values are quotations markers stripped by default
but can also be stored with their leading and trailing quotations markers.
- Attributes are namespaced by default allowing sections merge without keys collisions.
"""
def __init__(self,
file=None,
splitters=("=", ":"),
namespaceSplitter="|",
commentLimiters=(";", "#"),
commentMarker="#",
quotationMarkers=("\"", "'", "`"),
rawSectionContentIdentifier="__raw__",
defaultsSection="_defaults",
preserveOrder=True):
"""
Initializes the class.
Usage::
>>> content = ["[Section A]\\n", "; Comment.\\n", "Attribute 1 = \\"Value A\\"\\n", "\\n", \
"[Section B]\\n", "Attribute 2 = \\"Value B\\"\\n"]
>>> sectionsFileParser = SectionsFileParser()
>>> sectionsFileParser.content = content
>>> sectionsFileParser.parse(stripComments=False)
<foundations.parsers.SectionsFileParser object at 0x293892011>
>>> sectionsFileParser.sections.keys()
[u'Section A', u'Section B']
>>> sectionsFileParser.comments
OrderedDict([(u'Section A|#0', {u'content': u'Comment.', u'id': 0})])
:param file: Current file path.
:type file: unicode
:param splitters: Splitter characters.
:type splitters: tuple or list
:param namespaceSplitter: Namespace splitters character.
:type namespaceSplitter: unicode
:param commentLimiters: Comment limiters characters.
:type commentLimiters: tuple or list
:param commentMarker: Character use to prefix extracted comments idientifiers.
:type commentMarker: unicode
:param quotationMarkers: Quotation markers characters.
:type quotationMarkers: tuple or list
:param rawSectionContentIdentifier: Raw section content identifier.
:type rawSectionContentIdentifier: unicode
:param defaultsSection: Default section name.
:type defaultsSection: unicode
:param preserveOrder: Data order is preserved.
:type preserveOrder: bool
"""
LOGGER.debug("> Initializing '{0}()' class.".format(self.__class__.__name__))
foundations.io.File.__init__(self, file)
# --- Setting class attributes. ---
self.__splitters = None
self.splitters = splitters
self.__namespaceSplitter = None
self.namespaceSplitter = namespaceSplitter
self.__commentLimiters = None
self.commentLimiters = commentLimiters
self.__commentMarker = None
self.commentMarker = commentMarker
self.__quotationMarkers = None
self.quotationMarkers = quotationMarkers
self.__rawSectionContentIdentifier = None
self.rawSectionContentIdentifier = rawSectionContentIdentifier
self.__defaultsSection = None
self.defaultsSection = defaultsSection
self.__preserveOrder = None
self.preserveOrder = preserveOrder
if not preserveOrder:
self.__sections = {}
self.__comments = {}
else:
self.__sections = OrderedDict()
self.__comments = OrderedDict()
self.__parsingErrors = []
#******************************************************************************************************************
#*** Attributes properties.
#******************************************************************************************************************
@property
def splitters(self):
"""
Property for **self.__splitters** attribute.
:return: self.__splitters.
:rtype: tuple or list
"""
return self.__splitters
@splitters.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def splitters(self, value):
"""
Setter for **self.__splitters** attribute.
:param value: Attribute value.
:type value: tuple or list
"""
if value is not None:
assert type(value) in (tuple, list), "'{0}' attribute: '{1}' type is not 'tuple' or 'list'!".format(
"splitters", value)
for element in value:
assert type(element) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format(
"splitters", element)
assert len(element) == 1, "'{0}' attribute: '{1}' has multiples characters!".format("splitter", element)
assert not re.search(r"\w", element), "'{0}' attribute: '{1}' is an alphanumeric character!".format(
"splitter", element)
self.__splitters = value
@splitters.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def splitters(self):
"""
Deleter for **self.__splitters** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "splitters"))
@property
def namespaceSplitter(self):
"""
Property for **self.__namespaceSplitter** attribute.
:return: self.__namespaceSplitter.
:rtype: unicode
"""
return self.__namespaceSplitter
@namespaceSplitter.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def namespaceSplitter(self, value):
"""
Setter for **self.__namespaceSplitter** attribute.
:param value: Attribute value.
:type value: unicode
"""
if value is not None:
assert type(value) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format(
"namespaceSplitter", value)
assert len(value) == 1, "'{0}' attribute: '{1}' has multiples characters!".format("namespaceSplitter",
value)
assert not re.search(r"\w", value), "'{0}' attribute: '{1}' is an alphanumeric character!".format(
"namespaceSplitter", value)
self.__namespaceSplitter = value
@namespaceSplitter.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def namespaceSplitter(self):
"""
Deleter for **self.__namespaceSplitter** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "namespaceSplitter"))
@property
def commentLimiters(self):
"""
Property for **self.__commentLimiters** attribute.
:return: self.__commentLimiters.
:rtype: tuple or list
"""
return self.__commentLimiters
@commentLimiters.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def commentLimiters(self, value):
"""
Setter for **self.__commentLimiters** attribute.
:param value: Attribute value.
:type value: tuple or list
"""
if value is not None:
assert type(value) in (tuple, list), "'{0}' attribute: '{1}' type is not 'tuple' or 'list'!".format(
"commentLimiters", value)
for element in value:
assert type(element) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format(
"commentLimiters", element)
self.__commentLimiters = value
@commentLimiters.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
@property
def commentMarker(self):
"""
Property for **self.__commentMarker** attribute.
:return: self.__commentMarker.
:rtype: unicode
"""
return self.__commentMarker
@commentMarker.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def commentMarker(self, value):
"""
Setter for **self.__commentMarker** attribute.
:param value: Attribute value.
:type value: unicode
"""
if value is not None:
assert type(value) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format(
"commentMarker", value)
assert not re.search(r"\w", value), "'{0}' attribute: '{1}' is an alphanumeric character!".format(
"commentMarker", value)
self.__commentMarker = value
@commentMarker.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
@property
def quotationMarkers(self):
"""
Property for **self.__quotationMarkers** attribute.
:return: self.__quotationMarkers.
:rtype: tuple or list
"""
return self.__quotationMarkers
@quotationMarkers.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def quotationMarkers(self, value):
"""
Setter for **self.__quotationMarkers** attribute.
:param value: Attribute value.
:type value: tuple or list
"""
if value is not None:
assert type(value) in (tuple, list), "'{0}' attribute: '{1}' type is not 'tuple' or 'list'!".format(
"quotationMarkers", value)
for element in value:
assert type(element) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format(
"quotationMarkers", element)
assert len(element) == 1, "'{0}' attribute: '{1}' has multiples characters!".format("quotationMarkers",
element)
assert not re.search(r"\w", element), "'{0}' attribute: '{1}' is an alphanumeric character!".format(
"quotationMarkers", element)
self.__quotationMarkers = value
@quotationMarkers.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def quotationMarkers(self):
"""
Deleter for **self.__quotationMarkers** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "quotationMarkers"))
@property
def rawSectionContentIdentifier(self):
"""
Property for **self. __rawSectionContentIdentifier** attribute.
:return: self.__rawSectionContentIdentifier.
:rtype: unicode
"""
return self.__rawSectionContentIdentifier
@rawSectionContentIdentifier.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def rawSectionContentIdentifier(self, value):
"""
Setter for **self. __rawSectionContentIdentifier** attribute.
:param value: Attribute value.
:type value: unicode
"""
if value is not None:
assert type(value) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format(
"rawSectionContentIdentifier", value)
self.__rawSectionContentIdentifier = value
@rawSectionContentIdentifier.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def rawSectionContentIdentifier(self):
"""
Deleter for **self. __rawSectionContentIdentifier** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "rawSectionContentIdentifier"))
@property
def defaultsSection(self):
"""
Property for **self.__defaultsSection** attribute.
:return: self.__defaultsSection.
:rtype: unicode
"""
return self.__defaultsSection
@defaultsSection.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def defaultsSection(self, value):
"""
Setter for **self.__defaultsSection** attribute.
:param value: Attribute value.
:type value: unicode
"""
if value is not None:
assert type(value) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format(
"defaultsSection", value)
self.__defaultsSection = value
@defaultsSection.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def defaultsSection(self):
"""
Deleter for **self.__defaultsSection** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "defaultsSection"))
@property
def sections(self):
"""
Property for **self.__sections** attribute.
:return: self.__sections.
:rtype: OrderedDict or dict
"""
return self.__sections
@sections.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def sections(self, value):
"""
Setter for **self.__sections** attribute.
:param value: Attribute value.
:type value: OrderedDict or dict
"""
if value is not None:
assert type(value) in (OrderedDict, dict), "'{0}' attribute: '{1}' type is not \
'OrderedDict' or 'dict'!".format("sections", value)
for key, element in value.iteritems():
assert type(key) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format(
"sections", key)
assert type(element) in (OrderedDict, dict), "'{0}' attribute: '{1}' type is not \
'OrderedDict' or 'dict'!".format("sections", key)
self.__sections = value
@sections.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def sections(self):
"""
Deleter for **self.__sections** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "sections"))
@property
def comments(self):
"""
Property for **self.__comments** attribute.
:return: self.__comments.
:rtype: OrderedDict or dict
"""
return self.__comments
@comments.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def comments(self, value):
"""
Setter for **self.__comments** attribute.
:param value: Attribute value.
:type value: OrderedDict or dict
"""
if value is not None:
assert type(value) in (OrderedDict, dict), "'{0}' attribute: '{1}' type is not \
'OrderedDict' or 'dict'!".format("comments", value)
for key, element in value.iteritems():
assert type(key) is unicode, "'{0}' attribute: '{1}' type is not 'unicode'!".format(
"comments", key)
assert type(element) in (OrderedDict, dict), "'{0}' attribute: '{1}' type is not \
'OrderedDict' or 'dict'!".format("comments", key)
self.__comments = value
@comments.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
@property
def parsingErrors(self):
"""
Property for **self.__parsingErrors** attribute.
:return: self.__parsingErrors.
:rtype: list
"""
return self.__parsingErrors
@parsingErrors.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def parsingErrors(self, value):
"""
Setter for **self.__parsingErrors** attribute.
:param value: Attribute value.
:type value: list
"""
if value is not None:
assert type(value) is list, "'{0}' attribute: '{1}' type is not 'list'!".format("parsingErrors", value)
for element in value:
assert issubclass(element.__class__, foundations.exceptions.AbstractParsingError), \
"'{0}' attribute: '{1}' is not a '{2}' subclass!".format(
"parsingErrors", element, foundations.exceptions.AbstractParsingError.__class__.__name__)
self.__parsingErrors = value
@parsingErrors.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def parsingErrors(self):
"""
Deleter for **self.__parsingErrors** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "parsingErrors"))
@property
def preserveOrder(self):
"""
Property for **self.__preserveOrder** attribute.
:return: self.__preserveOrder.
:rtype: bool
"""
return self.__preserveOrder
@preserveOrder.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def preserveOrder(self, value):
"""
Setter method for **self.__preserveOrder** attribute.
:param value: Attribute value.
:type value: bool
"""
if value is not None:
assert type(value) is bool, "'{0}' attribute: '{1}' type is not 'bool'!".format("preserveOrder", value)
self.__preserveOrder = value
@preserveOrder.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def preserveOrder(self):
"""
Deleter method for **self.__preserveOrder** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "preserveOrder"))
#******************************************************************************************************************
#*** Class methods.
#******************************************************************************************************************
def __getitem__(self, section):
"""
Reimplements the :meth:`object.__getitem__` method.
:param section: Section name.
:type section: unicode
:return: Layout.
:rtype: Layout
"""
return self.__sections.__getitem__(section)
def __setitem__(self, section, value):
"""
Reimplements the :meth:`object.__getitem__` method.
:param section: Section name.
:type section: unicode
:param section: Value.
:type section: dict
:return: Layout.
:rtype: Layout
"""
return self.__sections.__setitem__(section, value)
def __iter__(self):
"""
Reimplements the :meth:`object.__iter__` method.
:return: Layouts iterator.
:rtype: object
"""
return self.__sections.iteritems()
def __contains__(self, section):
"""
Reimplements the :meth:`object.__contains__` method.
:param section: Section name.
:type section: unicode
:return: Section existence.
:rtype: bool
"""
return self.sectionExists(section)
def __len__(self):
"""
Reimplements the :meth:`object.__len__` method.
:return: Sections count.
:rtype: int
"""
return len(self.__sections)
# Oncilla: Statement commented by auto-documentation process:
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.FileStructureParsingError)
[docs] def parse(self,
rawSections=None,
namespaces=True,
stripComments=True,
stripWhitespaces=True,
stripQuotationMarkers=True,
raiseParsingErrors=True):
"""
Process the file content and extracts the sections / attributes
as nested :class:`collections.OrderedDict` dictionaries or dictionaries.
Usage::
>>> content = ["; Comment.\\n", "Attribute 1 = \\"Value A\\"\\n", "Attribute 2 = \\"Value B\\"\\n"]
>>> sectionsFileParser = SectionsFileParser()
>>> sectionsFileParser.content = content
>>> sectionsFileParser.parse(stripComments=False)
<foundations.parsers.SectionsFileParser object at 0x860323123>
>>> sectionsFileParser.sections.keys()
[u'_defaults']
>>> sectionsFileParser.sections["_defaults"].values()
[u'Value A', u'Value B']
>>> sectionsFileParser.parse(stripComments=False, stripQuotationMarkers=False)
<foundations.parsers.SectionsFileParser object at 0x860323123>
>>> sectionsFileParser.sections["_defaults"].values()
[u'"Value A"', u'"Value B"']
>>> sectionsFileParser.comments
OrderedDict([(u'_defaults|#0', {u'content': u'Comment.', u'id': 0})])
>>> sectionsFileParser.parse()
<foundations.parsers.SectionsFileParser object at 0x860323123>
>>> sectionsFileParser.sections["_defaults"]
OrderedDict([(u'_defaults|Attribute 1', u'Value A'), (u'_defaults|Attribute 2', u'Value B')])
>>> sectionsFileParser.parse(namespaces=False)
<foundations.parsers.SectionsFileParser object at 0x860323123>
>>> sectionsFileParser.sections["_defaults"]
OrderedDict([(u'Attribute 1', u'Value A'), (u'Attribute 2', u'Value B')])
:param rawSections: Ignored raw sections.
:type rawSections: tuple or list
:param namespaces: Attributes and comments are namespaced.
:type namespaces: bool
:param stripComments: Comments are stripped.
:type stripComments: bool
:param stripWhitespaces: Whitespaces are stripped.
:type stripWhitespaces: bool
:param stripQuotationMarkers: Attributes values quotation markers are stripped.
:type stripQuotationMarkers: bool
:param raiseParsingErrors: Raise parsing errors.
:type raiseParsingErrors: bool
:return: SectionFileParser instance.
:rtype: SectionFileParser
"""
LOGGER.debug("> Reading sections from: '{0}'.".format(self.path))
if not self.content:
self.read()
attributes = {} if not self.__preserveOrder else OrderedDict()
section = self.__defaultsSection
rawSections = rawSections or []
commentId = 0
for i, line in enumerate(self.content):
# Comments matching.
search = re.search(r"^\s*[{0}](?P<comment>.+)$".format("".join(self.__commentLimiters)), line)
if search:
if not stripComments:
comment = namespaces and foundations.namespace.setNamespace(section, "{0}{1}".format(
self.__commentMarker, commentId), self.__namespaceSplitter) or \
"{0}{1}".format(self.__commentMarker, commentId)
self.__comments[comment] = {"id": commentId, "content": stripWhitespaces and \
search.group(
"comment").strip() or search.group(
"comment")}
commentId += 1
continue
# Sections matching.
search = re.search(r"^\s*\[(?P<section>.+)\]\s*$", line)
if search:
section = stripWhitespaces and search.group("section").strip() or search.group("section")
if not self.__preserveOrder:
attributes = {}
else:
attributes = OrderedDict()
rawContent = []
continue
if section in rawSections:
rawContent.append(line)
attributes[self.__rawSectionContentIdentifier] = rawContent
else:
# Empty line matching.
search = re.search(r"^\s*$", line)
if search:
continue
# Attributes matching.
search = re.search(r"^(?P<attribute>.+?)[{0}](?P<value>.+)$".format("".join(self.__splitters)), line) \
or re.search(r"^(?P<attribute>.+?)[{0}]\s*$".format("".join(self.__splitters)), line)
if search:
attribute = search.group("attribute").strip() if stripWhitespaces else search.group("attribute")
attribute = foundations.namespace.setNamespace(section, attribute, self.__namespaceSplitter) \
if namespaces else attribute
if len(search.groups()) == 2:
value = search.group("value").strip() if stripWhitespaces else search.group("value")
attributes[attribute] = value.strip("".join(self.__quotationMarkers)) \
if stripQuotationMarkers else value
else:
attributes[attribute] = None
else:
self.__parsingErrors.append(foundations.exceptions.AttributeStructureParsingError(
"Attribute structure is invalid: {0}".format(line), i + 1))
self.__sections[section] = attributes
LOGGER.debug("> Sections: '{0}'.".format(self.__sections))
LOGGER.debug("> '{0}' file parsing done!".format(self.path))
if self.__parsingErrors and raiseParsingErrors:
raise foundations.exceptions.FileStructureParsingError(
"{0} | '{1}' structure is invalid, parsing exceptions occured!".format(self.__class__.__name__,
self.path))
return self
[docs] def sectionExists(self, section):
"""
Checks if given section exists.
Usage::
>>> content = ["[Section A]\\n", "; Comment.\\n", "Attribute 1 = \\"Value A\\"\\n", "\\n", \
"[Section B]\\n", "Attribute 2 = \\"Value B\\"\\n"]
>>> sectionsFileParser = SectionsFileParser()
>>> sectionsFileParser.content = content
>>> sectionsFileParser.parse()
<foundations.parsers.SectionsFileParser object at 0x845683844>
>>> sectionsFileParser.sectionExists("Section A")
True
>>> sectionsFileParser.sectionExists("Section C")
False
:param section: Section to check existence.
:type section: unicode
:return: Section existence.
:rtype: bool
"""
if section in self.__sections:
LOGGER.debug("> '{0}' section exists in '{1}'.".format(section, self))
return True
else:
LOGGER.debug("> '{0}' section doesn't exists in '{1}'.".format(section, self))
return False
[docs] def attributeExists(self, attribute, section):
"""
Checks if given attribute exists.
Usage::
>>> content = ["[Section A]\\n", "; Comment.\\n", "Attribute 1 = \\"Value A\\"\\n", "\\n", \
"[Section B]\\n", "Attribute 2 = \\"Value B\\"\\n"]
>>> sectionsFileParser = SectionsFileParser()
>>> sectionsFileParser.content = content
>>> sectionsFileParser.parse()
<foundations.parsers.SectionsFileParser object at 0x234564563>
>>> sectionsFileParser.attributeExists("Attribute 1", "Section A")
True
>>> sectionsFileParser.attributeExists("Attribute 2", "Section A")
False
:param attribute: Attribute to check existence.
:type attribute: unicode
:param section: Section to search attribute into.
:type section: unicode
:return: Attribute existence.
:rtype: bool
"""
if foundations.namespace.removeNamespace(attribute, rootOnly=True) in self.getAttributes(section,
stripNamespaces=True):
LOGGER.debug("> '{0}' attribute exists in '{1}' section.".format(attribute, section))
return True
else:
LOGGER.debug("> '{0}' attribute doesn't exists in '{1}' section.".format(attribute, section))
return False
[docs] def getAttributes(self, section, stripNamespaces=False):
"""
Returns given section attributes.
Usage::
>>> content = ["[Section A]\\n", "; Comment.\\n", "Attribute 1 = \\"Value A\\"\\n", "\\n", \
"[Section B]\\n", "Attribute 2 = \\"Value B\\"\\n"]
>>> sectionsFileParser = SectionsFileParser()
>>> sectionsFileParser.content = content
>>> sectionsFileParser.parse()
<foundations.parsers.SectionsFileParser object at 0x125698322>
>>> sectionsFileParser.getAttributes("Section A")
OrderedDict([(u'Section A|Attribute 1', u'Value A')])
>>> sectionsFileParser.preserveOrder=False
>>> sectionsFileParser.getAttributes("Section A")
{u'Section A|Attribute 1': u'Value A'}
>>> sectionsFileParser.preserveOrder=True
>>> sectionsFileParser.getAttributes("Section A", stripNamespaces=True)
OrderedDict([(u'Attribute 1', u'Value A')])
:param section: Section containing the requested attributes.
:type section: unicode
:param stripNamespaces: Strip namespaces while retrieving attributes.
:type stripNamespaces: bool
:return: Attributes.
:rtype: OrderedDict or dict
"""
LOGGER.debug("> Getting section '{0}' attributes.".format(section))
attributes = OrderedDict() if self.__preserveOrder else dict()
if not self.sectionExists(section):
return attributes
if stripNamespaces:
for attribute, value in self.__sections[section].iteritems():
attributes[foundations.namespace.removeNamespace(attribute, rootOnly=True)] = value
else:
attributes.update(self.__sections[section])
LOGGER.debug("> Attributes: '{0}'.".format(attributes))
return attributes
[docs] def getAllAttributes(self):
"""
Returns all sections attributes.
Usage::
>>> content = ["[Section A]\\n", "; Comment.\\n", "Attribute 1 = \\"Value A\\"\\n", "\\n", \
"[Section B]\\n", "Attribute 2 = \\"Value B\\"\\n"]
>>> sectionsFileParser = SectionsFileParser()
>>> sectionsFileParser.content = content
>>> sectionsFileParser.parse()
<foundations.parsers.SectionsFileParser object at 0x845683844>
>>> sectionsFileParser.getAllAttributes()
OrderedDict([(u'Section A|Attribute 1', u'Value A'), (u'Section B|Attribute 2', u'Value B')])
>>> sectionsFileParser.preserveOrder=False
>>> sectionsFileParser.getAllAttributes()
{u'Section B|Attribute 2': u'Value B', u'Section A|Attribute 1': u'Value A'}
:return: All sections / files attributes.
:rtype: OrderedDict or dict
"""
allAttributes = OrderedDict() if self.__preserveOrder else dict()
for attributes in self.__sections.itervalues():
for attribute, value in attributes.iteritems():
allAttributes[attribute] = value
return allAttributes
# Oncilla: Statement commented by auto-documentation process:
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.FileStructureParsingError)
[docs] def getValue(self, attribute, section, default=""):
"""
Returns requested attribute value.
Usage::
>>> content = ["[Section A]\\n", "; Comment.\\n", "Attribute 1 = \\"Value A\\"\\n", "\\n", \
"[Section B]\\n", "Attribute 2 = \\"Value B\\"\\n"]
>>> sectionsFileParser = SectionsFileParser()
>>> sectionsFileParser.content = content
>>> sectionsFileParser.parse()
<foundations.parsers.SectionsFileParser object at 0x679302423>
>>> sectionsFileParser.getValue("Attribute 1", "Section A")
u'Value A'
:param attribute: Attribute name.
:type attribute: unicode
:param section: Section containing the searched attribute.
:type section: unicode
:param default: Default return value.
:type default: object
:return: Attribute value.
:rtype: unicode
"""
if not self.attributeExists(attribute, section):
return default
if attribute in self.__sections[section]:
value = self.__sections[section][attribute]
elif foundations.namespace.setNamespace(section, attribute) in self.__sections[section]:
value = self.__sections[section][foundations.namespace.setNamespace(section, attribute)]
LOGGER.debug("> Attribute: '{0}', value: '{1}'.".format(attribute, value))
return value
[docs] def setValue(self, attribute, section, value):
"""
Sets requested attribute value.
Usage::
>>> content = ["[Section A]\\n", "; Comment.\\n", "Attribute 1 = \\"Value A\\"\\n", "\\n", \
"[Section B]\\n", "Attribute 2 = \\"Value B\\"\\n"]
>>> sectionsFileParser = SectionsFileParser()
>>> sectionsFileParser.content = content
>>> sectionsFileParser.parse()
<foundations.parsers.SectionsFileParser object at 0x109304209>
>>> sectionsFileParser.setValue("Attribute 3", "Section C", "Value C")
True
:param attribute: Attribute name.
:type attribute: unicode
:param section: Section containing the searched attribute.
:type section: unicode
:param value: Attribute value.
:type value: object
:return: Definition success.
:rtype: bool
"""
if not self.sectionExists(section):
LOGGER.debug("> Adding '{0}' section.".format(section))
self.__sections[section] = OrderedDict() if self.__preserveOrder else dict()
self.__sections[section][attribute] = value
return True
[docs] def write(self,
namespaces=False,
splitter="=",
commentLimiter=(";"),
spacesAroundSplitter=True,
spaceAfterCommentLimiter=True):
"""
Writes defined file using :obj:`SectionsFileParser.sections` and
:obj:`SectionsFileParser.comments` class properties content.
Usage::
>>> sections = {"Section A": {"Section A|Attribute 1": "Value A"}, \
"Section B": {"Section B|Attribute 2": "Value B"}}
>>> sectionsFileParser = SectionsFileParser("SectionsFile.rc")
>>> sectionsFileParser.sections = sections
>>> sectionsFileParser.write()
True
>>> sectionsFileParser.read()
u'[Section A]\\nAttribute 1 = Value A\\n\\n[Section B]\\nAttribute 2 = Value B\\n'
:param namespaces: Attributes are namespaced.
:type namespaces: bool
:param splitter: Splitter character.
:type splitter: unicode
:param commentLimiter: Comment limiter character.
:type commentLimiter: unicode
:param spacesAroundSplitter: Spaces around attributes and value splitters.
:type spacesAroundSplitter: bool
:param spaceAfterCommentLimiter: Space after comments limiter.
:type spaceAfterCommentLimiter: bool
:return: Method success.
:rtype: bool
"""
self.uncache()
LOGGER.debug("> Setting '{0}' file content.".format(self.path))
attributeTemplate = "{{0}} {0} {{1}}\n".format(splitter) if spacesAroundSplitter else \
"{{0}}{0}{{1}}\n".format(splitter)
attributeTemplate = foundations.strings.replace(attributeTemplate, {"{{" : "{", "}}" : "}"})
commentTemplate = spaceAfterCommentLimiter and "{0} {{0}}\n".format(commentLimiter) or \
"{0}{{0}}\n".format(commentLimiter)
if self.__defaultsSection in self.__sections:
LOGGER.debug("> Appending '{0}' default section.".format(self.__defaultsSection))
if self.__comments:
for comment, value in self.__comments.iteritems():
if self.__defaultsSection in comment:
value = value["content"] or ""
LOGGER.debug("> Appending '{0}' comment with '{1}' value.".format(comment, value))
self.content.append(commentTemplate.format(value))
for attribute, value in self.__sections[self.__defaultsSection].iteritems():
attribute = namespaces and attribute or foundations.namespace.removeNamespace(attribute,
self.__namespaceSplitter,
rootOnly=True)
value = value or ""
LOGGER.debug("> Appending '{0}' attribute with '{1}' value.".format(attribute, value))
self.content.append(attributeTemplate.format(attribute, value))
self.content.append("\n")
for i, section in enumerate(self.__sections):
LOGGER.debug("> Appending '{0}' section.".format(section))
self.content.append("[{0}]\n".format(section))
if self.__comments:
for comment, value in self.__comments.iteritems():
if section in comment:
value = value["content"] or ""
LOGGER.debug("> Appending '{0}' comment with '{1}' value.".format(comment, value))
self.content.append(commentTemplate.format(value))
for attribute, value in self.__sections[section].iteritems():
if foundations.namespace.removeNamespace(attribute) == self.__rawSectionContentIdentifier:
LOGGER.debug("> Appending '{0}' raw section content.".format(section))
for line in value:
self.content.append(line)
else:
LOGGER.debug("> Appending '{0}' section.".format(section))
attribute = namespaces and attribute or foundations.namespace.removeNamespace(attribute,
self.__namespaceSplitter,
rootOnly=True)
value = value or ""
LOGGER.debug("> Appending '{0}' attribute with '{1}' value.".format(attribute, value))
self.content.append(attributeTemplate.format(attribute, value))
if i != len(self.__sections) - 1:
self.content.append("\n")
foundations.io.File.write(self)
return True
[docs]class PlistFileParser(foundations.io.File):
"""
Defines methods to parse plist files.
"""
def __init__(self, file=None):
"""
Initializes the class.
Usage::
>>> plistFileParser = PlistFileParser("standard.plist")
>>> plistFileParser.parse()
True
>>> plistFileParser.elements.keys()
[u'Dictionary A', u'Number A', u'Array A', u'String A', u'Date A', u'Boolean A', u'Data A']
>>> plistFileParser.elements["Dictionary A"]
{u'String C': u'My Value C', u'String B': u'My Value B'}
:param file: Current file path.
:type file: unicode
"""
LOGGER.debug("> Initializing '{0}()' class.".format(self.__class__.__name__))
foundations.io.File.__init__(self, file)
# --- Setting class attributes. ---
self.__elements = None
self.__parsingErrors = None
self.__unserializers = {"array": lambda x: [value.text for value in x],
"dict": lambda x: dict((x[i].text, x[i + 1].text) for i in range(0, len(x), 2)),
"key": lambda x: foundations.strings.toString(x.text) or "",
"string": lambda x: foundations.strings.toString(x.text) or "",
"data": lambda x: base64.decodestring(x.text or ""),
"date": lambda x: datetime.datetime(*map(int, re.findall("\d+", x.text))),
"true": lambda x: True,
"false": lambda x: False,
"real": lambda x: float(x.text),
"integer": lambda x: int(x.text)}
#******************************************************************************************************************
#*** Attributes properties.
#******************************************************************************************************************
@property
def elements(self):
"""
Property for **self.__elements** attribute.
:return: self.__elements.
:rtype: OrderedDict or dict
"""
return self.__elements
@elements.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def elements(self, value):
"""
Setter for **self.__elements** attribute.
:param value: Attribute value.
:type value: OrderedDict or dict
"""
if value is not None:
assert type(value) is dict, "'{0}' attribute: '{1}' type is not dict'!".format("elements", value)
self.__elements = value
@elements.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def elements(self):
"""
Deleter for **self.__elements** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "elements"))
@property
def parsingErrors(self):
"""
Property for **self.__parsingErrors** attribute.
:return: self.__parsingErrors.
:rtype: list
"""
return self.__parsingErrors
@parsingErrors.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(AssertionError)
def parsingErrors(self, value):
"""
Setter for **self.__parsingErrors** attribute.
:param value: Attribute value.
:type value: list
"""
if value is not None:
assert type(value) is list, "'{0}' attribute: '{1}' type is not 'list'!".format("parsingErrors", value)
for element in value:
assert issubclass(element.__class__, foundations.exceptions.AbstractParsingError), \
"'{0}' attribute: '{1}' is not a '{2}' subclass!".format(
"parsingErrors", element, foundations.exceptions.AbstractParsingError.__class__.__name__)
self.__parsingErrors = value
@parsingErrors.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def parsingErrors(self):
"""
Deleter for **self.__parsingErrors** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "parsingErrors"))
@property
def unserializers(self):
"""
Property for **self.__unserializers** attribute.
:return: self.__unserializers.
:rtype: dict
"""
return self.__unserializers
@unserializers.setter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
def unserializers(self, value):
"""
Setter for **self.__unserializers** attribute.
:param value: Attribute value.
:type value: dict
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is read only!".format(self.__class__.__name__, "unserializers"))
@unserializers.deleter
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.ProgrammingError)
[docs] def unserializers(self):
"""
Deleter for **self.__unserializers** attribute.
"""
raise foundations.exceptions.ProgrammingError(
"{0} | '{1}' attribute is not deletable!".format(self.__class__.__name__, "unserializers"))
#******************************************************************************************************************
#*** Class methods.
#******************************************************************************************************************
# Oncilla: Statement commented by auto-documentation process: @foundations.exceptions.handleExceptions(foundations.exceptions.FileStructureParsingError)
[docs] def parse(self, raiseParsingErrors=True):
"""
Process the file content.
Usage::
>>> plistFileParser = PlistFileParser("standard.plist")
>>> plistFileParser.parse()
True
>>> plistFileParser.elements.keys()
[u'Dictionary A', u'Number A', u'Array A', u'String A', u'Date A', u'Boolean A', u'Data A']
:param raiseParsingErrors: Raise parsing errors.
:type raiseParsingErrors: bool
:return: Method success.
:rtype: bool
"""
LOGGER.debug("> Reading elements from: '{0}'.".format(self.path))
elementTreeParser = ElementTree.iterparse(self.path)
self.__parsingErrors = []
for action, element in elementTreeParser:
unmarshal = self.__unserializers.get(element.tag)
if unmarshal:
data = unmarshal(element)
element.clear()
element.text = data
elif element.tag != "plist":
self.__parsingErrors.append(foundations.exceptions.FileStructureParsingError(
"Unknown element: {0}".format(element.tag)))
if self.__parsingErrors:
if raiseParsingErrors:
raise foundations.exceptions.FileStructureParsingError(
"{0} | '{1}' structure is invalid, parsing exceptions occured!".format(self.__class__.__name__,
self.path))
else:
self.__elements = foundations.common.getFirstItem(elementTreeParser.root).text
return True
[docs] def elementExists(self, element):
"""
Checks if given element exists.
Usage::
>>> plistFileParser = PlistFileParser("standard.plist")
>>> plistFileParser.parse()
True
>>> plistFileParser.elementExists("String A")
True
>>> plistFileParser.elementExists("String Nemo")
False
:param element: Element to check existence.
:type element: unicode
:return: Element existence.
:rtype: bool
"""
if not self.__elements:
return False
for item in foundations.walkers.dictionariesWalker(self.__elements):
path, key, value = item
if key == element:
LOGGER.debug("> '{0}' attribute exists.".format(element))
return True
LOGGER.debug("> '{0}' element doesn't exists.".format(element))
return False
[docs] def filterValues(self, pattern, flags=0):
"""
| Filters the :meth:`PlistFileParser.elements` class property elements using given pattern.
| Will return a list of matching elements values, if you want to get only one element value, use
the :meth:`PlistFileParser.getValue` method instead.
Usage::
>>> plistFileParser = PlistFileParser("standard.plist")
>>> plistFileParser.parse()
True
>>> plistFileParser.filterValues(r"String A")
[u'My Value A']
>>> plistFileParser.filterValues(r"String.*")
[u'My Value C', u'My Value B', u'My Value A']
:param pattern: Regex filtering pattern.
:type pattern: unicode
:param flags: Regex flags.
:type flags: int
:return: Values.
:rtype: list
"""
values = []
if not self.__elements:
return values
for item in foundations.walkers.dictionariesWalker(self.__elements):
path, element, value = item
if re.search(pattern, element, flags):
values.append(value)
return values
[docs] def getValue(self, element):
"""
| Returns the given element value.
| If multiple elements with the same name exists, only the first encountered will be returned.
Usage::
>>> plistFileParser = PlistFileParser("standard.plist")
>>> plistFileParser.parse()
True
>>> plistFileParser.getValue("String A")
u'My Value A'
:param element: Element to get the value.
:type element: unicode
:return: Element value.
:rtype: object
"""
if not self.__elements:
return
values = self.filterValues(r"^{0}$".format(element))
return foundations.common.getFirstItem(values)
[docs]def getAttributeCompound(attribute, value=None, splitter="|", bindingIdentifier="@"):
"""
Returns an attribute compound.
Usage::
>>> data = "@Link | Value | Boolean | Link Parameter"
>>> attributeCompound = foundations.parsers.getAttributeCompound("Attribute Compound", data)
>>> attributeCompound.name
u'Attribute Compound'
>>> attributeCompound.value
u'Value'
>>> attributeCompound.link
u'@Link'
>>> attributeCompound.type
u'Boolean'
>>> attributeCompound.alias
u'Link Parameter'
:param attribute: Attribute.
:type attribute: unicode
:param value: Attribute value.
:type value: object
:param splitter: Splitter.
:type splitter: unicode
:param bindingIdentifier: Binding identifier.
:type bindingIdentifier: unicode
:return: Attribute compound.
:rtype: AttributeCompound
"""
LOGGER.debug("> Attribute: '{0}', value: '{1}'.".format(attribute, value))
if type(value) is unicode:
if splitter in value:
valueTokens = value.split(splitter)
if len(valueTokens) >= 3 and re.search(r"{0}\w*".format(bindingIdentifier), valueTokens[0]):
return AttributeCompound(name=attribute,
value=valueTokens[1].strip(),
link=valueTokens[0].strip(),
type=valueTokens[2].strip(),
alias=len(valueTokens) == 4 and valueTokens[3].strip() or None)
else:
if re.search(r"{0}\w*".format(bindingIdentifier), value):
return AttributeCompound(name=attribute, value=None, link=value, type=None, alias=None)
return AttributeCompound(name=attribute, value=value, link=None, type=None, alias=None)