5
\$\begingroup\$

I've created a simple (300 SLOC) API for a new configuration format (I'm not the author of it). Unfortunately, the specification is only in Polish at the moment, but you can see how it looks here.

The format has a few additional features besides simple data storage. For example, many files can be linked together to create a nice configuration system, and attributes can be referenced from outside of the main tree.

The code repository, and also a mini-tutorial for the API, is here.

It's my first more complex project, so the code is probably quite bad (especially that spaghetti code for the parsing method).

tsi_file.py:

from treestructinfo import tsi_elements

FORMAT_VERSION = 0.1


class TsiFile:
    def __init__(self, path="tsi_file.tsinfo", flags=['text', 'write']):
        self.path = path

         # TsiFile is also a node
        self.current_node = ""
        self.elements = []

        self.flags = get_flags(flags)  # converts parameter list into string understandable for open() method

        if not file_does_exist(self.path):
                raise FileNotFoundError

        parse_data(self, self.path, self.flags)

    def save(self):
        with open(self.path+'test', "w+") as f:
            refs = []  # list of referenced elements is used to correctly place definitions and declarations
            indent = 0  # number of spaces before line (+2 for each level of nesting)

            f.write('tsinfo version "{}"\n'.format(FORMAT_VERSION))

            def iterate(ele):
                nonlocal indent
                if type(ele) is tsi_elements.TsiAttr:
                    name = ele.name
                    if ele.ref:
                        refs.append(ele)
                        f.write('{}ref attr {}\n'.format(' ' * indent, name))
                    else:
                        value_list = ele.value.split('\n')  # multiline values are separated by \n char
                        line = '{}attr {}'.format(' ' * indent, name)
                        if len(value_list) == 1:  # value is a single line
                            value = value_list[0]
                        else:  # joining multiline strings
                            value = '\n'.join([' ' * line.length() + '"' + split + '"' for split in value_list])
                        f.write('{} "{}"\n'.format(line, value))
                else:  # type is tsi_elements.TsiNode
                    if not ele.ref and ele.link is None:
                        f.write('{}node {}\n'.format(' ' * indent, ele.name))
                        indent += 2
                        for element in ele.elements:
                            iterate(element)
                            if element == ele.elements[-1]:  # adding closing tag after all the content is saved
                                indent -= 2
                                f.write('{}end\n'.format(' ' * indent))
                    elif ele.link is not None:
                        f.write('{}link "{}" as "{}"\n'.format(' ' * indent, ele.link, ele.name))
                    else:  # ele.ref
                        refs.append(ele)
                        f.write('{}ref node {}\n'.format(' ' * indent, ele.name))

            def iterate_ref(ele):
                ref_counter = 1
                nonlocal indent
                if type(ele) is tsi_elements.TsiAttr:
                    if ele.ref:
                        value_list = ele.value.split('\n')  # multiline values are separated by \n char
                        line = '{}ref attr {}'.format(' ' * indent, ele.name)
                        if len(value_list) == 1:  # value is a single line
                            value = value_list[0]
                        else:  # joining multiline strings
                            value = '\n'.join([' ' * line.length() + '"' + split + '"' for split in value_list])
                        f.write('{} "{}"\n\n'.format(line, value))
                else:  # type is tsi_elements.TsiNode
                    f.write('{}ref node {}\n'.format(' ' * indent, ele.name))
                    indent += 2
                    for element in ele.elements:
                        if element.ref:
                            refs.insert(refs.index(ele) + ref_counter, element)
                            ref_counter += 1
                            if type(element) is tsi_elements.TsiNode:
                                f.write('{}ref node {}\n'.format(' ' * indent, element.name))
                            else:
                                f.write('{}ref attr {}\n'.format(' ' * indent, element.name))
                        else:
                            iterate(element)
                        if element == ele.elements[-1]:
                            indent -= 2
                            f.write('{}end ref\n\n'.format(' ' * indent))

            for element in self.elements:
                iterate(element)
                if element == self.elements[-1]:
                    f.write('\nend tree\n\n')

            for element in refs:
                iterate_ref(element)

    def get_element_value(self, attr_path):  # base method, returns value with str type
        return self._get_attr(attr_path).value

    def write_value(self, attr_path, value):  # base writing method
        self._get_attr(attr_path).value = value

    def _get_attr(self, attr_path):
        path_list = attr_path.split("\\")
        current_node = self
        for path_item in path_list:
            if path_item == path_list[-1]:  # last item is an attribute
                for element in current_node.elements:
                    if type(element) is tsi_elements.TsiAttr and element.name == path_item:
                        return element

            for element in current_node.elements:  # all the rest are nodes
                if type(element) is tsi_elements.TsiNode and element.name == path_item:
                    current_node = element


def parse_data(master_element, path, flags):
    with open(path, flags) as f:
        lines = [l for l in (line.strip(" \t\n") for line in f) if l]  # removing blank lines and side whitespaces
        current_attr = None
        current_node = ""
        ref_elements = []
        ref_counter = 0
        for l in lines:
            if current_node is None:  # we are in the main tree
                if l.startswith("node"):
                    name = l.split(maxsplit=1)[1].strip()
                    current_node = tsi_elements.TsiNode(name)
                    master_element.elements.append(current_node)
                elif l.startswith("attr"):
                    quote_index = l.index("\"")
                    name = l[l.index("attr") + 5:quote_index]
                    current_attr = tsi_elements.TsiAttr(name, l[quote_index:l.rindex("\"")])
                    master_element.elements.append(current_attr)
                elif l.startswith("\""):
                    value = "\n" + l[1:l.rindex("\"")]
                    current_attr.value += value
                    master_element.elements[-1].value += value
                elif l.startswith("ref attr"):
                    name = l.split(maxsplit=2)[2].strip()
                    attr = tsi_elements.TsiAttr(name, ref=True)
                    ref_elements.insert(ref_counter + 1, attr)
                    master_element.elements.append(attr)
                elif l.startswith("ref node"):
                    node = tsi_elements.TsiNode(l.split(maxsplit=2)[2].strip(), current_node, ref=True)
                    ref_elements.append(node)
                    master_element.elements.append(node)
                elif l.startswith("end tree"):
                    current_node = ""
                elif l.startswith("link"):
                    split = l.split("\"")
                    for i, x in enumerate(split):
                        split[i] = split[i].strip()
                        if split[i] == "":
                            del split[i]
                    file_name = split[1]
                    name = split[3]
                    node = tsi_elements.TsiNode(name, current_node, link=file_name)
                    current_node.elements.append(node)
                    flags = split[5:]
                    if not flags:
                        parse_data(node, file_name, "r+")
                    else:
                        parse_data(node, file_name, get_flags(split[5:]))

            elif type(current_node) is str:  # we are outside of the main tree (referenced elements here)
                if l.startswith("tsinfo"):
                    current_node = None
                elif l.startswith("ref node"):
                    node = ref_elements[0]
                    current_node = node
                elif l.startswith("ref attr"):
                    attr = ref_elements.pop(0)
                    attr.value = l[l.index("\"") + 1:l.rindex("\"")]
                    current_attr = attr
                elif l.startswith("\""):
                    value = "\n" + l[1:l.rindex("\"")]
                    current_attr.value += value

            elif not current_node.ref:
                if l.startswith("node"):
                    name = l.split(maxsplit=1)[1].strip()
                    node = tsi_elements.TsiNode(name, current_node)
                    current_node.elements.append(node)
                    current_node = node
                elif l.startswith("end"):
                    current_node = current_node.upper_node
                elif l.startswith("attr"):
                    quote_index = l.index("\"")
                    name = l[l.index("attr") + 4:quote_index].strip()
                    current_attr = tsi_elements.TsiAttr(name, l[quote_index + 1:l.rindex("\"")])
                    current_node.elements.append(current_attr)
                elif l.startswith("\""):
                    value = "\n" + l[1:l.rindex("\"")]
                    current_attr.value += value
                elif l.startswith("ref node"):
                    node = tsi_elements.TsiNode(l.split(maxsplit=2)[2].strip(), current_node, ref=True)
                    ref_elements.append(node)
                    current_node.elements.append(node)
                elif l.startswith("ref attr"):
                    name = l.split(maxsplit=2)[2].strip()
                    attr = tsi_elements.TsiAttr(name, ref=True)
                    ref_elements.insert(ref_counter + 1, attr)
                    current_node.elements.append(attr)
                elif l.startswith("link"):
                    split = l.split("\"")
                    for i, x in enumerate(split):
                        split[i] = split[i].strip()
                        if split[i] == "":
                            del split[i]
                    file_name = split[1]
                    name = split[3]
                    node = tsi_elements.TsiNode(name, current_node, link=file_name)
                    current_node.elements.append(node)
                    flags = split[5:]
                    if not flags:
                        parse_data(node, file_name, "r+")
                    else:
                        parse_data(node, file_name, get_flags(split[5:]))

            elif current_node.ref:
                if l.startswith("node"):
                    name = l.split(maxsplit=1)[1].strip()
                    node = tsi_elements.TsiNode(name, current_node)
                    current_node.elements.append(node)
                    current_node = node
                elif l.startswith("end ref"):
                    ref_elements.remove(current_node)
                    current_node = ""
                    ref_counter = 0
                elif l.startswith("attr"):
                    quote_index = l.index("\"")
                    name = l[l.index("attr") + 5:quote_index].strip()
                    current_attr = tsi_elements.TsiAttr(name, l[quote_index + 1:l.rindex("\"")])
                    current_node.elements.append(current_attr)
                elif l.startswith("ref node"):
                    node = tsi_elements.TsiNode(l.split(maxsplit=2)[2].strip(), current_node, ref=True)
                    ref_elements.insert(ref_counter + 1, node)
                    ref_counter += 1
                    current_node.elements.append(node)
                elif l.startswith("ref attr"):
                    name = l.split(maxsplit=2)[2].strip()
                    attr = tsi_elements.TsiAttr(name, ref=True)
                    ref_elements.insert(ref_counter + 1, attr)
                    current_node.elements.append(attr)
                elif l.startswith("\""):
                    value = "\n" + l[1:l.rindex("\"")]
                    current_attr.value += value
                elif l.startswith("link"):
                    split = l.split("\"")
                    for i, x in enumerate(split):
                        split[i] = split[i].strip()
                        if split[i] == "":
                            del split[i]
                    file_name = split[1]
                    name = split[3]
                    node = tsi_elements.TsiNode(name, current_node, link=file_name)
                    current_node.elements.append(node)
                    flags = split[5:]
                    if not flags:
                        parse_data(node, file_name, "r+")
                    else:
                        parse_data(node, file_name, get_flags(split[5:]))


def get_flags(flag_list):  # converts parameter list into string understandable for open() method
    flags = "r"  # read
    if 'text' and 'binary' in flag_list:
        raise Exception("Flag can't be 'text' and 'binary' at the same time.")

    if 'write' in flag_list:
        flags += "+"

    if 'text' in flag_list:
        flags += "t"
    elif 'binary' in flag_list:
        flags += "b"
    else:
        raise ValueError("Wrong flags provided, specify 'text' or 'binary' flag")

    return flags

def file_does_exist(file_path):
    from os import path
    if path.isfile(path.join(file_path)):
        return True
    return False

tsi_elements.py:

class TsiNode():

    def __init__(self, name, upper_node=None, ref=False, link=None):
        self.upper_node = upper_node
        self.name = name
        self.ref = ref
        self.link = link
        self.elements = []


class TsiAttr():

    def __init__(self, attr_name, attr_value=None, ref=False):
        self.name = attr_name
        self.value = attr_value
        self.ref = ref
\$\endgroup\$
3
\$\begingroup\$

Note: I realize this was asked in 2014, and many python style requirements may have been different.

  • Docstrings: You should include a docstring at the beginning of every method, class, and module you write. This will help any documentation identify what your program is supposed to do.
  • Imports: You imported path from os in your method file_does_exist. This does not follow proper PEP-8 import formatting. All your imports should be placed at the top of your code, regardless of when you use them. They should also be grouped (standard library imports, related third party imports, then local application imports) and separated by a blank line.
  • Unused variables in loops: Many times you do for i, x in enumerate(split):, yet you never use the x. You can instead use an _, which will make it clear that that variable is to be, essentially, ignored.
  • Variable Naming: Using one letter names isn't recommended, as it's hard to know what they're used for without looking at the code around them. This practices can greatly improve readability of your code.
  • Simplify boolean comparisons:

In your code you had:

if path.isfile(path.join(file_path)):
    return True
return False

Instead of returning pure True/False, you can return the condition instead, since it returns a boolean anyway, like so:

return path.isfile(path.join(file_path))
  • type vs isinstance: This StackOverflow answer provides a very good and in-depth explanation about type and isinstance and when to use them.
  • Variable and operator spacing: In your code you had: with open(self.path+'test', "w+") as file: While this is the only place in the code (that I saw) that doesn't space out, you should be consistent. Spacing out code improves readability of your code.
  • Indentation: Stay consistent with your indentation. A few times you indented four spaces too much. This can put variables/methods in an entirely different scope.
  • Redeclaring from outside scope: You import path, yet also have a parameter with the name path. This can cause some naming issues. In your circumstance, changing the parameter to file_path clears this concern, and also provides clarity about what the path actually is.

Updated Code

"""
Module Docstring:
A description about your program goes here
"""

from os import path

from treestructinfo import tsi_elements

FORMAT_VERSION = 0.1

class TsiFile:
    """
    Class for a TsiFile
    """
    def __init__(self, file_path="tsi_file.tsinfo", flags=['text', 'write']):
        self.path = file_path

         # TsiFile is also a node
        self.current_node = ""
        self.elements = []

        # converts parameter list into string understandable for open() method
        self.flags = get_flags(flags)

        if not file_does_exist(self.path):
            raise FileNotFoundError

        parse_data(self, self.path, self.flags)

    def save(self):
        """
        Saves content to a file at `self.path`
        """
        with open(self.path + 'test', "w+") as file:
            refs = []  # list of referenced elements is used to correctly place definitions and declarations
            indent = 0  # number of spaces before line (+2 for each level of nesting)

            file.write('tsinfo version "{}"\n'.format(FORMAT_VERSION))

            def iterate(ele):
                """
                Iterates over the current element, `ele`

                :param ele: Element to be iterated over

                """
                nonlocal indent
                if isinstance(ele, tsi_elements.TsiAttr):
                    name = ele.name
                    if ele.ref:
                        refs.append(ele)
                        file.write('{}ref attr {}\n'.format(' ' * indent, name))
                    else:
                        value_list = ele.value.split('\n')  # multiline values are separated by \n char
                        line = '{}attr {}'.format(' ' * indent, name)
                        if len(value_list) == 1:  # value is a single line
                            value = value_list[0]
                        else:  # joining multiline strings
                            value = '\n'.join([' ' * line.length() + '"' + split + '"' for split in value_list])
                        file.write('{} "{}"\n'.format(line, value))
                else:  # type is tsi_elements.TsiNode
                    if not ele.ref and ele.link is None:
                        file.write('{}node {}\n'.format(' ' * indent, ele.name))
                        indent += 2
                        for element in ele.elements:
                            iterate(element)
                            if element == ele.elements[-1]:  # adding closing tag after all the content is saved
                                indent -= 2
                                file.write('{}end\n'.format(' ' * indent))
                    elif ele.link is not None:
                        file.write('{}link "{}" as "{}"\n'.format(' ' * indent, ele.link, ele.name))
                    else:  # ele.ref
                        refs.append(ele)
                        file.write('{}ref node {}\n'.format(' ' * indent, ele.name))

            def iterate_ref(ele):
                """
                Iterates over the reference of the passed element, `ele`

                :param ele: Element whos reference is to be iterated over

                """
                ref_counter = 1
                nonlocal indent
                if isinstance(ele, tsi_elements.TsiAttr):
                    if ele.ref:
                        value_list = ele.value.split('\n')  # multiline values are separated by \n char
                        line = '{}ref attr {}'.format(' ' * indent, ele.name)
                        if len(value_list) == 1:  # value is a single line
                            value = value_list[0]
                        else:  # joining multiline strings
                            value = '\n'.join([' ' * line.length() + '"' + split + '"' for split in value_list])
                        file.write('{} "{}"\n\n'.format(line, value))
                else:  # type is tsi_elements.TsiNode
                    file.write('{}ref node {}\n'.format(' ' * indent, ele.name))
                    indent += 2
                    for element in ele.elements:
                        if element.ref:
                            refs.insert(refs.index(ele) + ref_counter, element)
                            ref_counter += 1
                            if isinstance(element, tsi_elements.TsiNode):
                                file.write('{}ref node {}\n'.format(' ' * indent, element.name))
                            else:
                                file.write('{}ref attr {}\n'.format(' ' * indent, element.name))
                        else:
                            iterate(element)
                        if element == ele.elements[-1]:
                            indent -= 2
                            file.write('{}end ref\n\n'.format(' ' * indent))

            for element in self.elements:
                iterate(element)
                if element == self.elements[-1]:
                    file.write('\nend tree\n\n')

            for element in refs:
                iterate_ref(element)

    def get_element_value(self, attr_path):
        """
        Base method, returns value with str type

        :param attr_path: Path to the attribute

        """
        return self._get_attr(attr_path).value

    def write_value(self, attr_path, value):
        """
        Base writing method

        :param attr_path: Path to the attribute
        :param value: Value to be assigned to the attribute

        """
        self._get_attr(attr_path).value = value

    def _get_attr(self, attr_path):
        """
        Returns the attribute in the passed `attr_path` path

        :param attr_path: Path to the attribute

        """
        path_list = attr_path.split("\\")
        current_node = self
        for path_item in path_list:
            if path_item == path_list[-1]:  # last item is an attribute
                for element in current_node.elements:
                    if isinstance(element, tsi_elements.TsiAttr) and element.name == path_item:
                        return element

            for element in current_node.elements:  # all the rest are nodes
                if isinstance(element, tsi_elements.TsiNode) and element.name == path_item:
                    current_node = element
        return None


def parse_data(master_element, file_path, flags):
    """
    Parses passed data

    :param master_element: The master element
    :param file_path: The path to the file
    :param flags: Specific flags to open file with

    """
    with open(file_path, flags) as file:
        lines = [l for l in (line.strip(" \t\n") for line in file) if l]  # removing blank lines and side whitespaces
        current_attr = None
        current_node = ""
        ref_elements = []
        ref_counter = 0
        for line in lines:
            if current_node is None:  # we are in the main tree
                if line.startswith("node"):
                    name = line.split(maxsplit=1)[1].strip()
                    current_node = tsi_elements.TsiNode(name)
                    master_element.elements.append(current_node)
                elif line.startswith("attr"):
                    quote_index = line.index("\"")
                    name = line[line.index("attr") + 5:quote_index]
                    current_attr = tsi_elements.TsiAttr(name, line[quote_index:line.rindex("\"")])
                    master_element.elements.append(current_attr)
                elif line.startswith("\""):
                    value = "\n" + line[1:line.rindex("\"")]
                    current_attr.value += value
                    master_element.elements[-1].value += value
                elif line.startswith("ref attr"):
                    name = line.split(maxsplit=2)[2].strip()
                    attr = tsi_elements.TsiAttr(name, ref=True)
                    ref_elements.insert(ref_counter + 1, attr)
                    master_element.elements.append(attr)
                elif line.startswith("ref node"):
                    node = tsi_elements.TsiNode(line.split(maxsplit=2)[2].strip(), current_node, ref=True)
                    ref_elements.append(node)
                    master_element.elements.append(node)
                elif line.startswith("end tree"):
                    current_node = ""
                elif line.startswith("link"):
                    split = line.split("\"")
                    for i, _ in enumerate(split):
                        split[i] = split[i].strip()
                        if split[i] == "":
                            del split[i]
                    file_name = split[1]
                    name = split[3]
                    node = tsi_elements.TsiNode(name, current_node, link=file_name)
                    current_node.elements.append(node)
                    flags = split[5:]
                    if not flags:
                        parse_data(node, file_name, "r+")
                    else:
                        parse_data(node, file_name, get_flags(split[5:]))

            elif isinstance(current_node, str):  # we are outside of the main tree (referenced elements here)
                if line.startswith("tsinfo"):
                    current_node = None
                elif line.startswith("ref node"):
                    node = ref_elements[0]
                    current_node = node
                elif line.startswith("ref attr"):
                    attr = ref_elements.pop(0)
                    attr.value = line[line.index("\"") + 1:line.rindex("\"")]
                    current_attr = attr
                elif line.startswith("\""):
                    value = "\n" + line[1:line.rindex("\"")]
                    current_attr.value += value

            elif not current_node.ref:
                if line.startswith("node"):
                    name = line.split(maxsplit=1)[1].strip()
                    node = tsi_elements.TsiNode(name, current_node)
                    current_node.elements.append(node)
                    current_node = node
                elif line.startswith("end"):
                    current_node = current_node.upper_node
                elif line.startswith("attr"):
                    quote_index = line.index("\"")
                    name = line[line.index("attr") + 4:quote_index].strip()
                    current_attr = tsi_elements.TsiAttr(name, line[quote_index + 1:line.rindex("\"")])
                    current_node.elements.append(current_attr)
                elif line.startswith("\""):
                    value = "\n" + line[1:line.rindex("\"")]
                    current_attr.value += value
                elif line.startswith("ref node"):
                    node = tsi_elements.TsiNode(line.split(maxsplit=2)[2].strip(), current_node, ref=True)
                    ref_elements.append(node)
                    current_node.elements.append(node)
                elif line.startswith("ref attr"):
                    name = line.split(maxsplit=2)[2].strip()
                    attr = tsi_elements.TsiAttr(name, ref=True)
                    ref_elements.insert(ref_counter + 1, attr)
                    current_node.elements.append(attr)
                elif line.startswith("link"):
                    split = line.split("\"")
                    for i, _ in enumerate(split):
                        split[i] = split[i].strip()
                        if split[i] == "":
                            del split[i]
                    file_name = split[1]
                    name = split[3]
                    node = tsi_elements.TsiNode(name, current_node, link=file_name)
                    current_node.elements.append(node)
                    flags = split[5:]
                    if not flags:
                        parse_data(node, file_name, "r+")
                    else:
                        parse_data(node, file_name, get_flags(split[5:]))

            elif current_node.ref:
                if line.startswith("node"):
                    name = line.split(maxsplit=1)[1].strip()
                    node = tsi_elements.TsiNode(name, current_node)
                    current_node.elements.append(node)
                    current_node = node
                elif line.startswith("end ref"):
                    ref_elements.remove(current_node)
                    current_node = ""
                    ref_counter = 0
                elif line.startswith("attr"):
                    quote_index = line.index("\"")
                    name = line[line.index("attr") + 5:quote_index].strip()
                    current_attr = tsi_elements.TsiAttr(name, line[quote_index + 1:line.rindex("\"")])
                    current_node.elements.append(current_attr)
                elif line.startswith("ref node"):
                    node = tsi_elements.TsiNode(line.split(maxsplit=2)[2].strip(), current_node, ref=True)
                    ref_elements.insert(ref_counter + 1, node)
                    ref_counter += 1
                    current_node.elements.append(node)
                elif line.startswith("ref attr"):
                    name = line.split(maxsplit=2)[2].strip()
                    attr = tsi_elements.TsiAttr(name, ref=True)
                    ref_elements.insert(ref_counter + 1, attr)
                    current_node.elements.append(attr)
                elif line.startswith("\""):
                    value = "\n" + line[1:line.rindex("\"")]
                    current_attr.value += value
                elif line.startswith("link"):
                    split = line.split("\"")
                    for i, _ in enumerate(split):
                        split[i] = split[i].strip()
                        if split[i] == "":
                            del split[i]
                    file_name = split[1]
                    name = split[3]
                    node = tsi_elements.TsiNode(name, current_node, link=file_name)
                    current_node.elements.append(node)
                    flags = split[5:]
                    if not flags:
                        parse_data(node, file_name, "r+")
                    else:
                        parse_data(node, file_name, get_flags(split[5:]))


def get_flags(flag_list):
    """
    Converts parameter list into string understandable for open() method

    :param flag_list: List of flags for opening files

    """
    flags = "r"  # read
    if 'text' and 'binary' in flag_list:
        raise Exception("Flag can't be 'text' and 'binary' at the same time.")

    if 'write' in flag_list:
        flags += "+"

    if 'text' in flag_list:
        flags += "t"
    elif 'binary' in flag_list:
        flags += "b"
    else:
        raise ValueError("Wrong flags provided, specify 'text' or 'binary' flag")

    return flags

def file_does_exist(file_path):
    """
    Returns a boolean based on if the file at `file_path` exists

    :param file_path: File to check for existence

    """
    return path.isfile(path.join(file_path))
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks for the feedback! Didn't expect this, nice to see how my code changed in 5 years ;) \$\endgroup\$ – Andrzej Sołtysik Aug 5 '19 at 12:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.