<commit_before>from typing import cast, List, Optional, Tuple, TYPE_CHECKING, Union

if TYPE_CHECKING:
    from .console import (
        Console,
        ConsoleOptions,
        RenderableType,
        RenderResult,
    )
from .jupyter import JupyterMixin
from .measure import Measurement
from .style import Style
from .segment import Segment

PaddingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]

class Padding(JupyterMixin):
    """Draw space around content.

    Example:
        >>> print(Padding("Hello", (2, 4), style="on blue"))

    Args:
        renderable (RenderableType): String or other renderable.
        pad (Union[int, Tuple[int]]): Padding for top, right, bottom, and left borders.
            May be specified with 1, 2, or 4 integers (CSS style).
        style (Union[str, Style], optional): Style for padding characters. Defaults to "none".
        expand (bool, optional): Expand padding to fit available width. Defaults to True.
    """

    def __init__(
        self,
        renderable: "RenderableType",
        pad: "PaddingDimensions" = (0, 0, 0, 0),
        *,
        style: Union[str, Style] = "none",
        expand: bool = True,
    ):
        self.renderable = renderable
        self.top, self.right, self.bottom, self.left = self.unpack(pad)
        self.style = style
        self.expand = expand

    @classmethod
    def indent(cls, renderable: "RenderableType", level: int) -> "Padding":
        """Make padding instance to render an indent.

        Args:
            renderable (RenderableType): String or other renderable.
            level (int): Number of characters to indent.

        Returns:
            Padding: A Padding instance.
        """

        return Padding(renderable, pad=(0, 0, 0, level), expand=False)

    @staticmethod
    def unpack(pad: "PaddingDimensions") -> Tuple[int, int, int, int]:
        """Unpack padding specified in CSS style."""
        if isinstance(pad, int):
            return (pad, pad, pad, pad)
        if len(pad) == 1:
            _pad = pad[0]
            return (_pad, _pad, _pad, _pad)
        if len(pad) == 2:
            pad_top, pad_right = cast(Tuple[int, int], pad)
            return (pad_top, pad_right, pad_top, pad_right)
        if len(pad) == 4:
            top, right, bottom, left = cast(Tuple[int, int, int, int], pad)
            return (top, right, bottom, left)
        raise ValueError(f"1, 2 or 4 integers required for padding; {len(pad)} given")

    def __repr__(self) -> str:
        return f"Padding({self.renderable!r}, ({self.top},{self.right},{self.bottom},{self.left}))"

    def __rich_console__(
        self, console: "Console", options: "ConsoleOptions"
    ) -> "RenderResult":

        style = console.get_style(self.style)
        if self.expand:
            width = options.max_width
        else:
            width = min(
                Measurement.get(console, self.renderable, options.max_width).maximum
                + self.left
                + self.right,
                options.max_width,
            )
        lines = console.render_lines(
            self.renderable,
            options.update_width(width - self.left - self.right),
            style=style,
            pad=True,
        )
        _Segment = Segment

        left = _Segment(" " * self.left, style) if self.left else None
        right = _Segment(f'{" " * self.right}\n', style)
        blank_line: Optional[List[Segment]] = None
        if self.top:
            blank_line = [_Segment(f'{" " * width}\n', style)]
            yield from blank_line * self.top
        for line in lines:
            if left is not None:
                yield left
            yield from line
            yield right
        if self.bottom:
            blank_line = blank_line or [_Segment(f'{" " * width}\n', style)]
            yield from blank_line * self.bottom

    def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
        extra_width = self.left + self.right
        if max_width - extra_width < 1:
            return Measurement(max_width, max_width)
        measure_min, measure_max = Measurement.get(
            console, self.renderable, max(0, max_width - extra_width)
        )
        measurement = Measurement(measure_min + extra_width, measure_max + extra_width)
        measurement = measurement.with_maximum(max_width)
        return measurement

if __name__ == "__main__":  #  pragma: no cover
    from rich import print

    print(Padding("Hello, World", (2, 4), style="on blue"))
<commit_msg>test fixes<commit_after>from typing import cast, List, Optional, Tuple, TYPE_CHECKING, Union

if TYPE_CHECKING:
    from .console import (
        Console,
        ConsoleOptions,
        RenderableType,
        RenderResult,
    )
from .jupyter import JupyterMixin
from .measure import Measurement
from .style import Style
from .segment import Segment

PaddingDimensions = Union[int, Tuple[int], Tuple[int, int], Tuple[int, int, int, int]]

class Padding(JupyterMixin):
    """Draw space around content.

    Example:
        >>> print(Padding("Hello", (2, 4), style="on blue"))

    Args:
        renderable (RenderableType): String or other renderable.
        pad (Union[int, Tuple[int]]): Padding for top, right, bottom, and left borders.
            May be specified with 1, 2, or 4 integers (CSS style).
        style (Union[str, Style], optional): Style for padding characters. Defaults to "none".
        expand (bool, optional): Expand padding to fit available width. Defaults to True.
    """

    def __init__(
        self,
        renderable: "RenderableType",
        pad: "PaddingDimensions" = (0, 0, 0, 0),
        *,
        style: Union[str, Style] = "none",
        expand: bool = True,
    ):
        self.renderable = renderable
        self.top, self.right, self.bottom, self.left = self.unpack(pad)
        self.style = style
        self.expand = expand

    @classmethod
    def indent(cls, renderable: "RenderableType", level: int) -> "Padding":
        """Make padding instance to render an indent.

        Args:
            renderable (RenderableType): String or other renderable.
            level (int): Number of characters to indent.

        Returns:
            Padding: A Padding instance.
        """

        return Padding(renderable, pad=(0, 0, 0, level), expand=False)

    @staticmethod
    def unpack(pad: "PaddingDimensions") -> Tuple[int, int, int, int]:
        """Unpack padding specified in CSS style."""
        if isinstance(pad, int):
            return (pad, pad, pad, pad)
        if len(pad) == 1:
            _pad = pad[0]
            return (_pad, _pad, _pad, _pad)
        if len(pad) == 2:
            pad_top, pad_right = cast(Tuple[int, int], pad)
            return (pad_top, pad_right, pad_top, pad_right)
        if len(pad) == 4:
            top, right, bottom, left = cast(Tuple[int, int, int, int], pad)
            return (top, right, bottom, left)
        raise ValueError(f"1, 2 or 4 integers required for padding; {len(pad)} given")

    def __repr__(self) -> str:
        return f"Padding({self.renderable!r}, ({self.top},{self.right},{self.bottom},{self.left}))"

    def __rich_console__(
        self, console: "Console", options: "ConsoleOptions"
    ) -> "RenderResult":

        style = console.get_style(self.style)
        if self.expand:
            width = options.max_width
        else:
            width = min(
                Measurement.get(console, self.renderable, options.max_width).maximum
                + self.left
                + self.right,
                options.max_width,
            )
        lines = console.render_lines(
            self.renderable,
            options.update_width(width - self.left - self.right),
            style=style,
            pad=True,
        )
        _Segment = Segment

        left = _Segment(" " * self.left, style) if self.left else None
        right = (
            [_Segment(f'{" " * self.right}', style), _Segment.line()]
            if self.right
            else [_Segment.line()]
        )
        blank_line: Optional[List[Segment]] = None
        if self.top:
            blank_line = [_Segment(f'{" " * width}\n', style)]
            yield from blank_line * self.top
        if left:
            for line in lines:
                yield left
                yield from line
                yield from right
        else:
            for line in lines:
                yield from line
                yield from right
        if self.bottom:
            blank_line = blank_line or [_Segment(f'{" " * width}\n', style)]
            yield from blank_line * self.bottom

    def __rich_measure__(self, console: "Console", max_width: int) -> "Measurement":
        extra_width = self.left + self.right
        if max_width - extra_width < 1:
            return Measurement(max_width, max_width)
        measure_min, measure_max = Measurement.get(
            console, self.renderable, max(0, max_width - extra_width)
        )
        measurement = Measurement(measure_min + extra_width, measure_max + extra_width)
        measurement = measurement.with_maximum(max_width)
        return measurement

if __name__ == "__main__":  #  pragma: no cover
    from rich import print

    print(Padding("Hello, World", (2, 4), style="on blue"))
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
# vim: sw=4:ts=4:expandtab
"""
riko.parsers
~~~~~~~~~~~~
Provides utility classes and functions
"""
import re

from io import StringIO
from html.entities import name2codepoint
from html.parser import HTMLParser
from urllib.error import URLError

import pygogo as gogo

from riko.utils import fetch
from meza.fntools import Objectify, remove_keys, listize
from meza.process import merge
from meza.compat import decode
from ijson import items

logger = gogo.Gogo(__name__, verbose=False, monolog=True).logger

try:
    from lxml import etree, html
except ImportError:
    try:
        import xml.etree.cElementTree as etree
    except ImportError:
        logger.debug('xml parser: ElementTree')
        import xml.etree.ElementTree as etree
        from xml.etree.ElementTree import ElementTree
    else:
        logger.debug('xml parser: cElementTree')
        from xml.etree.cElementTree import ElementTree

    import html5lib as html
    html5parser = None
else:
    logger.debug('xml parser: lxml')
    from lxml.html import html5parser

try:
    import speedparser3 as speedparser
except ImportError:
    import feedparser
    logger.debug('rss parser: feedparser')
    speedparser = None
else:
    logger.debug('rss parser: speedparser')

rssparser = speedparser or feedparser

NAMESPACES = {
    'owl': 'http://www.w3.org/2002/07/owl#',
    'xhtml': 'http://www.w3.org/1999/xhtml'}

ESCAPE = {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}

SKIP_SWITCH = {
    'contains': lambda text, value: text.lower() in value.lower(),
    'intersection': lambda text, value: set(text).intersection(value),
    're.search': lambda text, value: re.search(text, value),
}

class LinkParser(HTMLParser):
    def reset(self):
        HTMLParser.reset(self)
        self.data = StringIO()

    def handle_data(self, data):
        self.data.write('%s\n' % decode(data))

def get_text(html, convert_charrefs=False):
    try:
        parser = LinkParser(convert_charrefs=convert_charrefs)
    except TypeError:
        parser = LinkParser()

    try:
        parser.feed(html)
    except TypeError:
        parser.feed(decode(html))

    return parser.data.getvalue()

def parse_rss(url=None, **kwargs):
    try:
        f = fetch(decode(url), **kwargs)
    except (ValueError, URLError):
        parsed = rssparser.parse(url)
    else:
        content = f.read() if speedparser else f

        try:
            parsed = rssparser.parse(content)
        finally:
            f.close()

    return parsed

def xpath(tree, path='/', pos=0, namespace=None):
    try:
        elements = tree.xpath(path)
    except AttributeError:
        stripped = path.lstrip('/')
        tags = stripped.split('/') if stripped else []

        try:
            # TODO: consider replacing with twisted.words.xish.xpath
            elements = tree.getElementsByTagName(tags[pos]) if tags else [tree]
        except AttributeError:
            element_name = str(tree).split(' ')[1]

            if not namespace and {'{', '}'}.issubset(element_name):
                start, end = element_name.find('{') + 1, element_name.find('}')
                ns = element_name[start:end]
                ns_iter = (name for name in NAMESPACES if name in ns)
                namespace = next(ns_iter, namespace)

            prefix = ('/%s:' % namespace) if namespace else '/'
            match = './%s%s' % (prefix, prefix.join(tags[1:]))
            elements = tree.findall(match, NAMESPACES)
        except IndexError:
            elements = [tree]
        else:
            for element in elements:
                return xpath(element, path, pos + 1)

    return iter(elements)

def xml2etree(f, xml=True, html5=False):
    if xml:
        element_tree = etree.parse(f)
    elif html5 and html5parser:
        element_tree = html5parser.parse(f)
    elif html5parser:
        element_tree = html.parse(f)
    else:
        # html5lib's parser returns an Element, so we must convert it into an
        # ElementTree
        element_tree = ElementTree(html.parse(f))

    return element_tree

def _make_content(i, value=None, tag='content', append=True, strip=False):
    content = i.get(tag)

    try:
        value = value.strip() if value and strip else value
    except AttributeError:
        pass

    if content and value and append:
        content = listize(content)
        content.append(value)
    elif content and value:
        content = ''.join([content, value])
    elif value:
        content = value

    return {tag: content} if content else {}

def etree2dict(element):
    """Convert an element tree into a dict imitating how Yahoo Pipes does it.
    """
    i = dict(element.items())
    i.update(_make_content(i, element.text, strip=True))

    for child in element:
        tag = child.tag
        value = etree2dict(child)
        i.update(_make_content(i, value, tag))

    if element.text and not set(i).difference(['content']):
        # element is leaf node and doesn't have attributes
        i = i.get('content')

    return i

def any2dict(f, ext='xml', html5=False, path=None):
    path = path or ''

    if ext in {'xml', 'html'}:
        xml = ext == 'xml'
        root = xml2etree(f, xml, html5).getroot()
        replaced = '/'.join(path.split('.'))
        tree = next(xpath(root, replaced)) if replaced else root
        content = etree2dict(tree)
    elif ext == 'json':
        content = next(items(f, path))
    else:
        raise TypeError("Invalid file type: '%s'" % ext)

    return content

def get_value(item, conf=None, force=False, default=None, **kwargs):
    item = item or {}

    try:
        value = item.get(conf['subkey'], **kwargs)
    except KeyError:
        if conf and not (hasattr(conf, 'delete') or force):
            raise TypeError('conf must be of type DotDict')
        elif force:
            value = conf
        elif conf:
            value = conf.get(**kwargs)
        else:
            value = default
    except (TypeError, AttributeError):
        # conf is already set to a value so use it or the default
        value = default if conf is None else conf
    except (ValueError):
        # error converting subkey value with OPS['func'] so use the default
        value = default

    return value

def parse_conf(item, **kwargs):
    kw = Objectify(kwargs, defaults={}, conf={})
    # TODO: fix so .items() returns a DotDict instance
    # parsed = {k: get_value(item, v) for k, v in kw.conf.items()}
    sentinel = {'subkey', 'value', 'terminal'}
    not_dict = not hasattr(kw.conf, 'keys')

    if not_dict or (len(kw.conf) == 1 and sentinel.intersection(kw.conf)):
        objectified = get_value(item, **kwargs)
    else:
        no_conf = remove_keys(kwargs, 'conf')
        parsed = {k: get_value(item, kw.conf[k], **no_conf) for k in kw.conf}
        result = merge([kw.defaults, parsed])
        objectified = Objectify(result) if kw.objectify else result

    return objectified

def get_skip(item, skip_if=None, **kwargs):
    """ Determine whether or not to skip an item

    Args:
        item (dict): The entry to process
        skip_if (func or Iter[dict]): The skipping criteria

    Returns:
        bool: whether or not to skip

    Examples:
        >>> item = {'content': 'Some content'}
        >>> get_skip(item, lambda x: x['content'] == 'Some content')
        True
        >>> get_skip(item)
        False
        >>> get_skip(item, {'field': 'content'})
        False
        >>> bool(get_skip(item, {'field': 'content', 'include': True}))
        True
        >>> get_skip(item, {'field': 'content', 'text': 'some'})
        True
        >>> get_skip(item, {'field': 'content', 'text': 'some', 'include': True})
        False
        >>> get_skip(item, {'field': 'content', 'text': 'other'})
        False
        >>> get_skip(item, {'field': 'content', 'text': 'other', 'include': True})
        True
    """
    item = item or {}

    if callable(skip_if):
        skip = skip_if(item)
    elif skip_if:
        skips = listize(skip_if)

        for _skip in skips:
            value = item.get(_skip['field'], '')
            text = _skip.get('text')
            op = _skip.get('op', 'contains')
            match = not SKIP_SWITCH[op](text, value) if text else value
            skip = match if _skip.get('include') else not match

            if skip:
                break
    else:
        skip = False

    return skip

def get_field(item, field=None, **kwargs):
    return item.get(field, **kwargs) if field else item

def text2entity(text):
    """Convert HTML/XML special chars to entity references
    """
    return ESCAPE.get(text, text)

def entity2text(entitydef):
    """Convert an HTML entity reference into unicode.
    http://stackoverflow.com/a/58125/408556
    """
    if entitydef.startswith('&#x'):
        cp = int(entitydef[3:-1], 16)
    elif entitydef.startswith('&#'):
        cp = int(entitydef[2:-1])
    elif entitydef.startswith('&'):
        cp = name2codepoint[entitydef[1:-1]]
    else:
        logger.debug(entitydef)
        cp = None

    return chr(cp) if cp else entitydef
<commit_msg>[ENH] Allow skips to contain a callable <commit_after># -*- coding: utf-8 -*-
# vim: sw=4:ts=4:expandtab
"""
riko.parsers
~~~~~~~~~~~~
Provides utility classes and functions
"""
import re

from io import StringIO
from html.entities import name2codepoint
from html.parser import HTMLParser
from urllib.error import URLError

import pygogo as gogo

from riko.utils import fetch
from meza.fntools import Objectify, remove_keys, listize
from meza.process import merge
from meza.compat import decode
from ijson import items

logger = gogo.Gogo(__name__, verbose=False, monolog=True).logger

try:
    from lxml import etree, html
except ImportError:
    try:
        import xml.etree.cElementTree as etree
    except ImportError:
        logger.debug('xml parser: ElementTree')
        import xml.etree.ElementTree as etree
        from xml.etree.ElementTree import ElementTree
    else:
        logger.debug('xml parser: cElementTree')
        from xml.etree.cElementTree import ElementTree

    import html5lib as html
    html5parser = None
else:
    logger.debug('xml parser: lxml')
    from lxml.html import html5parser

try:
    import speedparser3 as speedparser
except ImportError:
    import feedparser
    logger.debug('rss parser: feedparser')
    speedparser = None
else:
    logger.debug('rss parser: speedparser')

rssparser = speedparser or feedparser

NAMESPACES = {
    'owl': 'http://www.w3.org/2002/07/owl#',
    'xhtml': 'http://www.w3.org/1999/xhtml'}

ESCAPE = {'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}

SKIP_SWITCH = {
    'contains': lambda text, value: text.lower() in value.lower(),
    'intersection': lambda text, value: set(text).intersection(value),
    're.search': lambda text, value: re.search(text, value),
}

class LinkParser(HTMLParser):
    def reset(self):
        HTMLParser.reset(self)
        self.data = StringIO()

    def handle_data(self, data):
        self.data.write('%s\n' % decode(data))

def get_text(html, convert_charrefs=False):
    try:
        parser = LinkParser(convert_charrefs=convert_charrefs)
    except TypeError:
        parser = LinkParser()

    try:
        parser.feed(html)
    except TypeError:
        parser.feed(decode(html))

    return parser.data.getvalue()

def parse_rss(url=None, **kwargs):
    try:
        f = fetch(decode(url), **kwargs)
    except (ValueError, URLError):
        parsed = rssparser.parse(url)
    else:
        content = f.read() if speedparser else f

        try:
            parsed = rssparser.parse(content)
        finally:
            f.close()

    return parsed

def xpath(tree, path='/', pos=0, namespace=None):
    try:
        elements = tree.xpath(path)
    except AttributeError:
        stripped = path.lstrip('/')
        tags = stripped.split('/') if stripped else []

        try:
            # TODO: consider replacing with twisted.words.xish.xpath
            elements = tree.getElementsByTagName(tags[pos]) if tags else [tree]
        except AttributeError:
            element_name = str(tree).split(' ')[1]

            if not namespace and {'{', '}'}.issubset(element_name):
                start, end = element_name.find('{') + 1, element_name.find('}')
                ns = element_name[start:end]
                ns_iter = (name for name in NAMESPACES if name in ns)
                namespace = next(ns_iter, namespace)

            prefix = ('/%s:' % namespace) if namespace else '/'
            match = './%s%s' % (prefix, prefix.join(tags[1:]))
            elements = tree.findall(match, NAMESPACES)
        except IndexError:
            elements = [tree]
        else:
            for element in elements:
                return xpath(element, path, pos + 1)

    return iter(elements)

def xml2etree(f, xml=True, html5=False):
    if xml:
        element_tree = etree.parse(f)
    elif html5 and html5parser:
        element_tree = html5parser.parse(f)
    elif html5parser:
        element_tree = html.parse(f)
    else:
        # html5lib's parser returns an Element, so we must convert it into an
        # ElementTree
        element_tree = ElementTree(html.parse(f))

    return element_tree

def _make_content(i, value=None, tag='content', append=True, strip=False):
    content = i.get(tag)

    try:
        value = value.strip() if value and strip else value
    except AttributeError:
        pass

    if content and value and append:
        content = listize(content)
        content.append(value)
    elif content and value:
        content = ''.join([content, value])
    elif value:
        content = value

    return {tag: content} if content else {}

def etree2dict(element):
    """Convert an element tree into a dict imitating how Yahoo Pipes does it.
    """
    i = dict(element.items())
    i.update(_make_content(i, element.text, strip=True))

    for child in element:
        tag = child.tag
        value = etree2dict(child)
        i.update(_make_content(i, value, tag))

    if element.text and not set(i).difference(['content']):
        # element is leaf node and doesn't have attributes
        i = i.get('content')

    return i

def any2dict(f, ext='xml', html5=False, path=None):
    path = path or ''

    if ext in {'xml', 'html'}:
        xml = ext == 'xml'
        root = xml2etree(f, xml, html5).getroot()
        replaced = '/'.join(path.split('.'))
        tree = next(xpath(root, replaced)) if replaced else root
        content = etree2dict(tree)
    elif ext == 'json':
        content = next(items(f, path))
    else:
        raise TypeError("Invalid file type: '%s'" % ext)

    return content

def get_value(item, conf=None, force=False, default=None, **kwargs):
    item = item or {}

    try:
        value = item.get(conf['subkey'], **kwargs)
    except KeyError:
        if conf and not (hasattr(conf, 'delete') or force):
            raise TypeError('conf must be of type DotDict')
        elif force:
            value = conf
        elif conf:
            value = conf.get(**kwargs)
        else:
            value = default
    except (TypeError, AttributeError):
        # conf is already set to a value so use it or the default
        value = default if conf is None else conf
    except (ValueError):
        # error converting subkey value with OPS['func'] so use the default
        value = default

    return value

def parse_conf(item, **kwargs):
    kw = Objectify(kwargs, defaults={}, conf={})
    # TODO: fix so .items() returns a DotDict instance
    # parsed = {k: get_value(item, v) for k, v in kw.conf.items()}
    sentinel = {'subkey', 'value', 'terminal'}
    not_dict = not hasattr(kw.conf, 'keys')

    if not_dict or (len(kw.conf) == 1 and sentinel.intersection(kw.conf)):
        objectified = get_value(item, **kwargs)
    else:
        no_conf = remove_keys(kwargs, 'conf')
        parsed = {k: get_value(item, kw.conf[k], **no_conf) for k in kw.conf}
        result = merge([kw.defaults, parsed])
        objectified = Objectify(result) if kw.objectify else result

    return objectified

def get_skip(item, skip_if=None, **kwargs):
    """ Determine whether or not to skip an item

    Args:
        item (dict): The entry to process
        skip_if (func or Iter[dict]): The skipping criteria

    Returns:
        bool: whether or not to skip

    Examples:
        >>> item = {'content': 'Some content'}
        >>> get_skip(item, lambda x: x['content'] == 'Some content')
        True
        >>> get_skip(item)
        False
        >>> get_skip(item, {'field': 'content'})
        False
        >>> bool(get_skip(item, {'field': 'content', 'include': True}))
        True
        >>> get_skip(item, {'field': 'content', 'text': 'some'})
        True
        >>> get_skip(item, {'field': 'content', 'text': 'some', 'include': True})
        False
        >>> get_skip(item, {'field': 'content', 'text': 'other'})
        False
        >>> get_skip(item, {'field': 'content', 'text': 'other', 'include': True})
        True
    """
    item = item or {}
    skips = listize(skip_if)

    for _skip in skips:
        if callable(_skip):
            skip = _skip(item)
        elif _skip:
            value = item.get(_skip['field'], '')
            text = _skip.get('text')
            op = _skip.get('op', 'contains')
            match = not SKIP_SWITCH[op](text, value) if text else value
            skip = match if _skip.get('include') else not match
        else:
            skip = False

        if skip:
            break

    return skip

def get_field(item, field=None, **kwargs):
    return item.get(field, **kwargs) if field else item

def text2entity(text):
    """Convert HTML/XML special chars to entity references
    """
    return ESCAPE.get(text, text)

def entity2text(entitydef):
    """Convert an HTML entity reference into unicode.
    http://stackoverflow.com/a/58125/408556
    """
    if entitydef.startswith('&#x'):
        cp = int(entitydef[3:-1], 16)
    elif entitydef.startswith('&#'):
        cp = int(entitydef[2:-1])
    elif entitydef.startswith('&'):
        cp = name2codepoint[entitydef[1:-1]]
    else:
        logger.debug(entitydef)
        cp = None

    return chr(cp) if cp else entitydef
<|endoftext|>


<commit_before>#
# Title: Import wxPython to its own thread.
#
# Author: Pearu Peterson <pearu@cens.ioc.ee>
# Created: October, 2003
#

__all__ = ['wxPython_thread']

import re
import os
import sys
import new
import types
import thread
import inspect
import atexit
import imp
from scipy_base import ParallelExec

def get_extmodules(module,pexec,state0=None):
    # .. that need wrappers
    if module != 'wx':
        assert not sys.modules.has_key(module),\
        module+' is already imported, cannot proceed'
    if state0 is None:
        state0 = sys.modules.keys()
    if module == 'wxPython':
        pexec('import '+module,wait=1)

    state1 = sys.modules.keys()
    return [k for k in state1 if k not in state0 \
            and (not hasattr(sys.modules[k],'__file__') \
                 or os.path.splitext(sys.modules[k].__file__)[1][:3]!='.py')]

def wrap_extmodule(module, call_holder):
    """ Return wrapper module.
    Current design assumes wx extension modules.
    """
    new_module = new.module(module.__name__)
    for m in inspect.getmembers(module):
        n,v = m
        t = type(v)
        if t in [types.NoneType,types.StringType,types.IntType]:
            # No wrappers are needed.
            setattr(new_module,n,v)
        elif t is types.BuiltinFunctionType:
            setattr(new_module,n,wrap_builtinfunction(v,call_holder))
        elif t is types.ClassType:
            if str(v)=='wxPython.wxc.wxPyAssertionError':
                # Do we need a wrapper for it?
                setattr(new_module,n,v)
            else:
                raise NotImplementedError,`t`
        elif n in ('wxPyCoreAPI', '_wxPyCoreAPI'):
            # wxPyCoreAPI is not used by the python part of wx, so
            # no wrapper is necessary
            setattr(new_module,n,v)
        elif n=='cvar':
            # In wx all cvar attributes are of string type:
            #print [type(getattr(v,a)) for a in get_cvar_attrs(v)]
            # So no wrappers are necessary.
            setattr(new_module,n,v)
        else:
            raise NotImplementedError,`t`
    # This gives the way how to recognize if module is a wrapper:
    #new_module._old_module = module
    return new_module

def wrap_builtinfunction(func, call_holder):
    func_name = func.__name__
    if func_name in ('wxApp_CleanUp', 'App_CleanUp'):
        return func
    main_thread_id = call_holder.main_thread_id
    func_tmpl = """\
def %(func_name)s(*args,**kws):
    if get_ident()==%(main_thread_id)s:
        call_holder.call('call_holder.result=old_func(*args,**kws)')
        result = call_holder.result
        call_holder.result = None
        return result
    return old_func(*args,**kws)
func_code=%(func_name)s.func_code"""
    exec func_tmpl % (locals())
    globs = {'old_func':func,
             'get_ident':thread.get_ident,
             'call_holder':call_holder}
    return new.function(func_code,globs,func_name)

def _import_wx_core(wx_pth, pexec):
    """Imports the core modules for wx.  This is necessary for
    wxPython-2.5.x.
    """
    # Find the suffix.
    suffix = '.so'
    flag = 0
    for x in [x[0] for x in imp.get_suffixes() if x[-1] is imp.C_EXTENSION]:
        if os.path.exists(os.path.join(wx_pth, '_core_' + x)):
            suffix = x
            flag = 1
            break
    if not flag:
        return 0

    # Create a dummy module in sys.modules to inhibit importing the
    # actual one.  We will reload(wx) to get it right later.
    m = new.module('wx')
    m.__file__ = os.path.join('wx_pth', '__init__.py')
    sys.modules['wx'] = m

    # Now import all the modules manually.
    pexec('import os.path, imp, glob')
    code = """\
libs = glob.glob(os.path.join('%(wx_pth)s', '*' + '%(suffix)s'))
done = []
for i in ['_core_', '_controls_', '_misc_', '_windows_', '_gdi_']:
    p = os.path.join('%(wx_pth)s', i + '%(suffix)s')
    imp.load_dynamic('wx.' + i, p)
    done.append(p)
libs = [x for x in libs if x not in done]
for i in libs:
    p = os.path.splitext(os.path.basename(i))[0]
    imp.load_dynamic('wx.' + p, i)
"""%locals()
    pexec(code, wait=1)
    return 1

def wxPython_thread():
    class AttrHolder: pass

    call_holder = AttrHolder()
    call_holder.call = None
    call_holder.main_thread_id = thread.get_ident()
    pexec = ParallelExec()

    # Check if 'wx' namespace based modules are used.
    mod_name = 'wxPython'
    state0 = None
    for path in sys.path:
        wx_pth = os.path.join(path, 'wx')
        if os.path.exists(os.path.join(wx_pth, '__init__.py')):
            assert not sys.modules.has_key('wx'), \
                   'wx is already imported, cannot proceed'
            state0 = sys.modules.keys()
            if _import_wx_core(wx_pth, pexec):
                mod_name = 'wx'
            else:
                state0 = None
            break
    #print [x for x in sys.modules.keys() if x.find('wx') > -1]

    # Create wrappers to wxPython extension modules:
    for name in get_extmodules(mod_name,pexec,state0):
        module = sys.modules[name]
        if module is None: # happens with gui_thread.wxPython
            continue
        sys.modules[name] = wrap_extmodule(module,call_holder)
        #print name

    if mod_name == 'wx':
        pexec('import wx; reload(wx)', wait=1)
        #print [x for x in sys.modules.keys() if x.find('wx') > -1]
        import wx
    else:
        pexec('import wxPython; reload(wxPython)', wait=1)
        import wxPython

    pexec('from wxBackgroundApp import wxBackgroundApp')
    pexec('call_holder.call=wxBackgroundApp()',wait=1)
    # Start wxPython application in background:
    pexec('call_holder.call.run()')

    atexit.register(pexec.shutdown)
    return
<commit_msg>ENH: Modified to work with both wxPython 2.4.x and 2.5.x.  Also cleaned up the code.<commit_after>#
# Title: Import wxPython to its own thread.
#
# Author: Pearu Peterson <pearu@cens.ioc.ee>
# Created: October, 2003
#

__all__ = ['wxPython_thread']

import os
import sys
import new
import types
import thread
import inspect
import atexit
import imp
import glob
from scipy_base import ParallelExec

def get_extmodules(pexec):
    # .. that need wrappers

    state0 = sys.modules.keys()
    _do_import(pexec)
    state1 = sys.modules.keys()

    return [k for k in state1 if k not in state0 \
            and (not hasattr(sys.modules[k],'__file__') \
                 or os.path.splitext(sys.modules[k].__file__)[1][:3]!='.py')]

def wrap_extmodule(module, call_holder):
    """ Return wrapper module.
    Current design assumes wx extension modules.
    """
    new_module = new.module(module.__name__)
    for m in inspect.getmembers(module):
        n,v = m
        t = type(v)
        if t in [types.NoneType,types.StringType,types.IntType]:
            # No wrappers are needed.
            setattr(new_module,n,v)
        elif t is types.BuiltinFunctionType:
            setattr(new_module,n,wrap_builtinfunction(v,call_holder))
        elif t is types.ClassType:
            if str(v)=='wxPython.wxc.wxPyAssertionError':
                # Do we need a wrapper for it?
                setattr(new_module,n,v)
            else:
                raise NotImplementedError,`t`
        elif n in ('wxPyCoreAPI', '_wxPyCoreAPI'):
            # wxPyCoreAPI is not used by the python part of wx, so
            # no wrapper is necessary
            setattr(new_module,n,v)
        elif n=='cvar':
            # In wx all cvar attributes are of string type:
            #print [type(getattr(v,a)) for a in get_cvar_attrs(v)]
            # So no wrappers are necessary.
            setattr(new_module,n,v)
        else:
            raise NotImplementedError,`t`
    # This gives the way how to recognize if module is a wrapper:
    #new_module._old_module = module
    return new_module

def wrap_builtinfunction(func, call_holder):
    func_name = func.__name__
    if func_name in ('wxApp_CleanUp', 'App_CleanUp'):
        return func
    main_thread_id = call_holder.main_thread_id
    func_tmpl = """\
def %(func_name)s(*args,**kws):
    if get_ident()==%(main_thread_id)s:
        call_holder.call('call_holder.result=old_func(*args,**kws)')
        result = call_holder.result
        call_holder.result = None
        return result
    return old_func(*args,**kws)
func_code=%(func_name)s.func_code"""
    exec func_tmpl % (locals())
    globs = {'old_func':func,
             'get_ident':thread.get_ident,
             'call_holder':call_holder}
    return new.function(func_code,globs,func_name)

def _do_import(pexec):
    """This imports the wxPython modules.  This code handles both
    wxPython 2.4.x and 2.5.x versions.

    """
    sfx = [x[0] for x in imp.get_suffixes() if x[-1] is imp.C_EXTENSION]
    suffix = '.so'

    # Handle the imports for wxPython-2.5.x
    flag = 0
    for path in sys.path:
        wx_pth = os.path.join(path, 'wx')
        if os.path.exists(os.path.join(wx_pth, '__init__.py')):
            for x in sfx:
                if os.path.exists(os.path.join(wx_pth, '_core_' + x)):
                    suffix = x
                    flag = 1
                    assert not sys.modules.has_key('wx'), \
                           'wx is already imported, cannot proceed'
                    # Now do necessary stuff for wx.
                    _import_wx_core(pexec, wx_pth, suffix)
                    break

    # wxPython-2.4.x
    if not flag:
        assert not sys.modules.has_key('wxPython'),\
               'wxPython is already imported, cannot proceed'
        pexec('import wxPython', wait=1)
        path = os.path.dirname(sys.modules['wxPython'].__file__)
        suffix = os.path.splitext(sys.modules['wxPython.wxc'].__file__)[1]
        exts = glob.glob(os.path.join(path, '*' + suffix))
        for x in [os.path.splitext(os.path.basename(i))[0] for i in exts]:
            pexec('import wxPython.%s'%x, wait=1)

def _import_wx_core(pexec, wx_pth, suffix):
    """Imports the core modules for wx.  This is necessary for
    wxPython-2.5.x.

    """
    # Create a dummy module in sys.modules to inhibit importing the
    # actual one.  We will reload(wx) to get it right later.
    m = new.module('wx')
    m.__file__ = os.path.join('wx_pth', '__init__.py')
    sys.modules['wx'] = m

    # Now import all the extension modules manually.
    pexec('import imp')
    libs = glob.glob(os.path.join(wx_pth, '*' + suffix))
    done = []
    for i in ['_core_', '_controls_', '_misc_', '_windows_', '_gdi_']:
        p = os.path.join(wx_pth, i + suffix)
        pexec('imp.load_dynamic("wx." + "%s", "%s")'%(i, p), wait=1)
        done.append(p)
    libs = [x for x in libs if x not in done]
    for i in libs:
        p = os.path.splitext(os.path.basename(i))[0]
        pexec('imp.load_dynamic("wx." + "%s", "%s")'%(p, i), wait=1)

def wxPython_thread():
    class AttrHolder: pass

    call_holder = AttrHolder()
    call_holder.call = None
    call_holder.main_thread_id = thread.get_ident()
    pexec = ParallelExec()

    # Create wrappers to wxPython extension modules:
    for name in get_extmodules(pexec):
        module = sys.modules[name]
        if module is None: # happens with gui_thread.wxPython
            continue
        sys.modules[name] = wrap_extmodule(module,call_holder)

    if sys.modules.has_key('wxPython.wxc'):
        pexec('import wxPython; reload(wxPython)', wait=1)
        import wxPython
    else:
        pexec('import wx; reload(wx)', wait=1)
        import wx

    pexec('from wxBackgroundApp import wxBackgroundApp')
    pexec('call_holder.call=wxBackgroundApp()',wait=1)
    # Start wxPython application in background:
    pexec('call_holder.call.run()')

    atexit.register(pexec.shutdown)
    return
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-

from os.path import exists, join

from mojo.roboFont import RFont, RGlyph
from mutatorMath.objects.mutator import buildMutator

from fontMath.mathGlyph import MathGlyph
from fontMath.mathInfo import MathInfo
from fontMath.mathKerning import MathKerning

from jkRFTools.info import setFontInfo

from jkRFoTools.weight import getPanoseAndWeightForStyle

def notify(title, subtitle, message):
    from os import system
    t = '-title {!r}'.format(title)
    s = '-subtitle {!r}'.format(subtitle)
    m = '-message {!r}'.format(message)
    a = '-sender {!r}'.format("com.typemytype.robofont")
    system('terminal-notifier {}'.format(' '.join([m, t, s, a])))

class jkInstance(object):

    def __init__(self, location, style_name, features):
        self.features = features
        self.location = location
        self.style_name = style_name

        self.font = None

class jkInterpolator(object):

    def __init__(self, family_name, base_path=None, instance_dir=None, progressWindow=None):
        self.family_name = family_name
        self.base_path = base_path
        self.instance_dir = instance_dir

        self.masters = []
        self.instances = []
        self.target_glyphset = []

        if progressWindow is not None:
            self.progress = progressWindow
        else:
            self.progress = None

        if exists("/usr/bin/terminal-notifier"):
            self.use_notifications = True
        else:
            self.use_notifications = False
            print "In order to use notifications, install the command line program with:"
            print "$ sudo gem install terminal-notifier"

    def add_master(self, font, location):
        self.masters.append((location, font))

    def add_instance(self, location, style_name, features):
        self.instances.append(jkInstance(location, style_name, features))

    def set_target_glyphset(self, glyphnames=[]):
        self.target_glyphset = glyphnames

    def get_font_at_location(self, location, showUI=False):
        #print("\nBuilding font at location %s" % location)
        i_font = RFont(showUI=showUI) # showUI=False
        if self.target_glyphset == []:
            glyphset = self.masters[0][1].glyphOrder
        else:
            glyphset = self.target_glyphset
        self.interpolate_font_info(location, self.masters, i_font)
        groups = self.masters[0][1].groups
        for k in groups.keys():
            i_font.groups[k] = groups[k]
        self.interpolate_kerning(location, self.masters, i_font)
        self.interpolate_glyph_set(location, glyphset, self.masters, i_font)
        i_font.glyphOrder = glyphset
        return i_font

    def interpolate(self):
        if self.progress is not None:
            p = self.progress("Interpolation", tickCount = len(self.instances))
        for instance in self.instances:
            if self.progress is not None:
                p.update("%s %s %s ..." % (
                    self.family_name,
                    instance.style_name,
                    instance.location
                ))
            font = self.get_font_at_location(instance.location)
            weight_class, panose_weight = getPanoseAndWeightForStyle(instance.style_name)
            setFontInfo(font, self.family_name, instance.style_name, weight_class)
            if font.info.openTypeOS2Panose is not None:
                font.info.openTypeOS2Panose[2] = panose_weight
            font.features.text = instance.features
            instance.font = font
        if self.progress is not None:
            p.close()

    def generate(self):
        if self.progress is not None:
            p = self.progress("Generation", tickCount = len(self.instances))
        i = 0
        for instance in self.instances:
            if instance.font is not None:
                font = instance.font
                ps_name = font.info.postscriptFontName
                if self.progress is not None:
                    p.update("Generating %s ..." % ps_name)

                # save space by removing unneeded layers
                for l in font.layerOrder:
                    font.removeLayer(l)

                # save UFO
                font.save(join(self.base_path, self.instance_dir, "%s.ufo" % ps_name))

                # save OTF
                font_path = join(self.base_path, self.instance_dir, "%s.otf" % ps_name)
                font.generate(
                    path=font_path,
                    format="otf",
                    decompose=True,
                    checkOutlines=True,
                    autohint=False,
                    releaseMode=True,
                    glyphOrder=None,
                    progressBar=None,
                    useMacRoman=False,
                )
                if self.use_notifications:
                    notify("Font was generated", ps_name, font_path)
                i += 1
        if self.use_notifications:
            notify(
                "Fonts Generated",
                "%i fonts were generated." % i,
                "%s" % join(self.base_path, self.instance_dir)
            )
        if self.progress is not None:
            p.close()

    def interpolate_font_info(self, instanceLocation, masters, targetFont):
        infoMasters = [
            (infoLocation, MathInfo(masterFont.info))
            for infoLocation, masterFont in masters
        ]
        try:
            bias, iM = buildMutator(infoMasters)
            instanceInfo = iM.makeInstance(instanceLocation)
            instanceInfo.extractInfo(targetFont.info)
        except:
            print(u'Couldn’t interpolate font info')

    def interpolate_kerning(self, instanceLocation, masters, targetFont):
        kerningMasters = [
            (kerningLocation, MathKerning(masterFont.kerning))
            for kerningLocation, masterFont in masters
        ]
        try:
            bias, iK = buildMutator(kerningMasters)
            instanceKerning = iK.makeInstance(instanceLocation)
            instanceKerning.extractKerning(targetFont)
        except:
            print(u'Couldn’t interpolate font kerning')

    def interpolate_glyph_set(self, instanceLocation, glyphSet, masters, targetFont):
        for glyphName in glyphSet:
            masterGlyphs = [
                (masterLocation, MathGlyph(masterFont[glyphName]))
                for masterLocation, masterFont in masters
            ]
            try:
                bias, gM = buildMutator(masterGlyphs)
                newGlyph = RGlyph()
                instanceGlyph = gM.makeInstance(instanceLocation)
                targetFont.insertGlyph(instanceGlyph.extractGlyph(newGlyph), glyphName)
                targetFont[glyphName].unicode = masterFont[glyphName].unicode
                targetFont[glyphName].round()
                self.fix_component_order(masterFont[glyphName], targetFont[glyphName])
            except:
                #print(u'Incompatible glyph: %s' % glyphName)
                targetFont.newGlyph(glyphName)
                continue

    def fix_component_order(self, source_glyph, target_glyph):
        pass

<commit_msg>Add support for a post-interpolation callback function. This function is called after interpolating an instance, with the instance font as argument.<commit_after># -*- coding: utf-8 -*-

from os.path import exists, join

from mojo.roboFont import RFont, RGlyph
from mutatorMath.objects.mutator import buildMutator

from fontMath.mathGlyph import MathGlyph
from fontMath.mathInfo import MathInfo
from fontMath.mathKerning import MathKerning

from jkRFTools.info import setFontInfo

from jkRFoTools.weight import getPanoseAndWeightForStyle

def notify(title, subtitle, message):
    from os import system
    t = '-title {!r}'.format(title)
    s = '-subtitle {!r}'.format(subtitle)
    m = '-message {!r}'.format(message)
    a = '-sender {!r}'.format("com.typemytype.robofont")
    system('terminal-notifier {}'.format(' '.join([m, t, s, a])))

class jkInstance(object):

    def __init__(self, location, style_name, features):
        self.features = features
        self.location = location
        self.style_name = style_name

        self.font = None

class jkInterpolator(object):

    def __init__(self, family_name, base_path=None, instance_dir=None, progressWindow=None, postInterpolateCallback=None):
        self.family_name = family_name
        self.base_path = base_path
        self.instance_dir = instance_dir

        self.masters = []
        self.instances = []
        self.target_glyphset = []

        if progressWindow is not None:
            self.progress = progressWindow
        else:
            self.progress = None

        self.postInterpolateCallback = postInterpolateCallback

        if exists("/usr/bin/terminal-notifier"):
            self.use_notifications = True
        else:
            self.use_notifications = False
            print "In order to use notifications, install the command line program with:"
            print "$ sudo gem install terminal-notifier"

    def add_master(self, font, location):
        self.masters.append((location, font))

    def add_instance(self, location, style_name, features):
        self.instances.append(jkInstance(location, style_name, features))

    def set_target_glyphset(self, glyphnames=[]):
        self.target_glyphset = glyphnames

    def get_font_at_location(self, location, showUI=False):
        #print("\nBuilding font at location %s" % location)
        i_font = RFont(showUI=showUI) # showUI=False
        if self.target_glyphset == []:
            glyphset = self.masters[0][1].glyphOrder
        else:
            glyphset = self.target_glyphset
        self.interpolate_font_info(location, self.masters, i_font)
        groups = self.masters[0][1].groups
        for k in groups.keys():
            i_font.groups[k] = groups[k]
        self.interpolate_kerning(location, self.masters, i_font)
        self.interpolate_glyph_set(location, glyphset, self.masters, i_font)
        i_font.glyphOrder = glyphset
        return i_font

    def interpolate(self):
        if self.progress is not None:
            p = self.progress("Interpolation", tickCount = len(self.instances))
        for instance in self.instances:
            if self.progress is not None:
                p.update("%s %s %s ..." % (
                    self.family_name,
                    instance.style_name,
                    instance.location
                ))
            font = self.get_font_at_location(instance.location)
            weight_class, panose_weight = getPanoseAndWeightForStyle(instance.style_name)
            setFontInfo(font, self.family_name, instance.style_name, weight_class)
            if font.info.openTypeOS2Panose is not None:
                font.info.openTypeOS2Panose[2] = panose_weight
            font.features.text = instance.features
            if self.postInterpolateCallback is not None:
                self.postInterpolateCallback(font)
            instance.font = font
        if self.progress is not None:
            p.close()

    def generate(self):
        if self.progress is not None:
            p = self.progress("Generation", tickCount = len(self.instances))
        i = 0
        for instance in self.instances:
            if instance.font is not None:
                font = instance.font
                ps_name = font.info.postscriptFontName
                if self.progress is not None:
                    p.update("Generating %s ..." % ps_name)

                # save space by removing unneeded layers
                for l in font.layerOrder:
                    font.removeLayer(l)

                # save UFO
                font.save(join(self.base_path, self.instance_dir, "%s.ufo" % ps_name))

                # save OTF
                font_path = join(self.base_path, self.instance_dir, "%s.otf" % ps_name)
                font.generate(
                    path=font_path,
                    format="otf",
                    decompose=True,
                    checkOutlines=True,
                    autohint=False,
                    releaseMode=True,
                    glyphOrder=None,
                    progressBar=None,
                    useMacRoman=False,
                )
                if self.use_notifications:
                    notify("Font was generated", ps_name, font_path)
                i += 1
        if self.use_notifications:
            notify(
                "Fonts Generated",
                "%i fonts were generated." % i,
                "%s" % join(self.base_path, self.instance_dir)
            )
        if self.progress is not None:
            p.close()

    def interpolate_font_info(self, instanceLocation, masters, targetFont):
        infoMasters = [
            (infoLocation, MathInfo(masterFont.info))
            for infoLocation, masterFont in masters
        ]
        try:
            bias, iM = buildMutator(infoMasters)
            instanceInfo = iM.makeInstance(instanceLocation)
            instanceInfo.extractInfo(targetFont.info)
        except:
            print(u'Couldn’t interpolate font info')

    def interpolate_kerning(self, instanceLocation, masters, targetFont):
        kerningMasters = [
            (kerningLocation, MathKerning(masterFont.kerning))
            for kerningLocation, masterFont in masters
        ]
        try:
            bias, iK = buildMutator(kerningMasters)
            instanceKerning = iK.makeInstance(instanceLocation)
            instanceKerning.extractKerning(targetFont)
        except:
            print(u'Couldn’t interpolate font kerning')

    def interpolate_glyph_set(self, instanceLocation, glyphSet, masters, targetFont):
        for glyphName in glyphSet:
            masterGlyphs = [
                (masterLocation, MathGlyph(masterFont[glyphName]))
                for masterLocation, masterFont in masters
            ]
            try:
                bias, gM = buildMutator(masterGlyphs)
                newGlyph = RGlyph()
                instanceGlyph = gM.makeInstance(instanceLocation)
                targetFont.insertGlyph(instanceGlyph.extractGlyph(newGlyph), glyphName)
                targetFont[glyphName].unicode = masterFont[glyphName].unicode
                targetFont[glyphName].round()
                self.fix_component_order(masterFont[glyphName], targetFont[glyphName])
            except:
                #print(u'Incompatible glyph: %s' % glyphName)
                targetFont.newGlyph(glyphName)
                continue

    def fix_component_order(self, source_glyph, target_glyph):
        pass

<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
import array
import logging

from fontTools import ttLib
from fontTools.pens.hashPointPen import HashPointPen
from fontTools.ttLib.tables._g_l_y_f import (
    OVERLAP_COMPOUND,
    ROUND_XY_TO_GRID,
    USE_MY_METRICS,
)

logger = logging.getLogger(__name__)

TRUETYPE_INSTRUCTIONS_KEY = "public.truetype.instructions"
TRUETYPE_ROUND_KEY = "public.truetype.roundOffsetToGrid"
TRUETYPE_METRICS_KEY = "public.truetype.useMyMetrics"
TRUETYPE_OVERLAP_KEY = "public.truetype.overlap"
OBJECT_LIBS_KEY = "public.objectLibs"

class InstructionCompiler(object):
    def __init__(self, ufo, ttf):
        self.ufo = ufo
        self.font = ttf

    def _compile_program(self, key, table_tag):
        assert table_tag in ("prep", "fpgm")
        ttdata = self.ufo.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
        if ttdata:
            formatVersion = ttdata.get("formatVersion", None)
            if int(formatVersion) != 1:
                logger.error(
                    f"Unknown formatVersion {formatVersion} "
                    f"in key '{key}', "
                    f"table '{table_tag}' will be empty in font."
                )
                return
            asm = ttdata.get(key, None)
            if asm is not None:
                self.font[table_tag] = table = ttLib.newTable(table_tag)
                table.program = ttLib.tables.ttProgram.Program()
                table.program.fromAssembly(asm)
                # Roundtrip once, or if the font is dumped to XML before having
                # been saved, the assembly code if will look awful.
                table.program.fromBytecode(table.program.getBytecode())

    def compile_cvt(self):
        cvts = []
        ttdata = self.ufo.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
        if ttdata:
            formatVersion = ttdata.get("formatVersion", None)
            if int(formatVersion) != 1:
                logger.error(
                    f"Unknown formatVersion {formatVersion} "
                    f"in key 'controlValue', "
                    f"table 'cvt' will be empty in font."
                )
                return
            cvt_list = ttdata.get("controlValue", None)
            if cvt_list is not None:
                # Convert string keys to int
                cvt_dict = {int(v["id"]): v["value"] for v in cvt_list}
                # Find the maximum cvt index.
                # We can't just use the dict keys because the cvt must be
                # filled consecutively.
                max_cvt = max(cvt_dict.keys())
                # Make value list, filling entries for missing keys with 0
                cvts = [cvt_dict.get(i, 0) for i in range(max_cvt + 1)]

        if cvts:
            # Only write cvt to font if it contains any values
            self.font["cvt "] = cvt = ttLib.newTable("cvt ")
            cvt.values = array.array("h", cvts)

    def compile_fpgm(self):
        self._compile_program("fontProgram", "fpgm")

    def compile_glyf(self):
        for name in sorted(self.ufo.keys()):
            glyph = self.ufo[name]
            ttdata = glyph.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
            if name not in self.font["glyf"]:
                if ttdata is not None:
                    logger.warning(
                        f"Glyph '{name}' not found in font, "
                        "skipping compilation of TrueType instructions "
                        "for this glyph."
                    )
                logger.debug(f"UFO keys: {list(self.ufo.keys())}")
                logger.debug(f"glyf keys: {list(self.font['glyf'].keys())}")
                continue

            glyf = self.font["glyf"][name]
            if ttdata is not None:
                formatVersion = ttdata.get("formatVersion", None)
                if int(formatVersion) != 1:
                    logger.error(
                        f"Unknown formatVersion {formatVersion} "
                        "in glyph '{name}', it will have "
                        "no instructions in font."
                    )
                    continue

                # Check if glyph hash matches the current outlines
                hash_pen = HashPointPen(glyph.width, self.ufo)
                glyph.drawPoints(hash_pen)
                glyph_id = ttdata.get("id", None)
                if glyph_id is None or glyph_id != hash_pen.hash:
                    logger.error(
                        f"Glyph hash mismatch, glyph '{name}' will have "
                        "no instructions in font."
                    )
                    continue

                # Compile the glyph program
                asm = ttdata.get("assembly", None)
                if asm is not None:
                    glyf.program = ttLib.tables.ttProgram.Program()
                    glyf.program.fromAssembly(asm)
                    # Roundtrip once, or if the font is dumped to XML before
                    # having been saved, the assembly code if will look awful.
                    glyf.program.fromBytecode(glyf.program.getBytecode())

            # Handle composites
            if glyf.isComposite():
                # Remove empty glyph programs from composite glyphs
                if hasattr(glyf, "program") and not glyf.program:
                    delattr(glyf, "program")

                # Set component flags

                # We need to decide when to set the flags.
                # Let's assume if any lib key is not there, or the component
                # doesn't have an identifier, we should leave the flags alone.
                use_my_metrics_comp = None
                for i, c in enumerate(glyf.components):
                    if i >= len(glyph.components):
                        logger.error(
                            "Number of components differ between UFO and TTF "
                            f"in glyph '{name}' ({len(glyph.components)} vs. "
                            f"{len(glyf.components)}, not setting flags in "
                            "additional components."
                        )
                        break
                    ufo_component_id = glyph.components[i].identifier
                    if (
                        ufo_component_id is not None
                        and OBJECT_LIBS_KEY in glyph.lib
                        and ufo_component_id in glyph.lib[OBJECT_LIBS_KEY]
                        and (
                            TRUETYPE_ROUND_KEY
                            in glyph.lib[OBJECT_LIBS_KEY][ufo_component_id]
                            or TRUETYPE_METRICS_KEY
                            in glyph.lib[OBJECT_LIBS_KEY][ufo_component_id]
                        )
                    ):
                        component_lib = glyph.lib[OBJECT_LIBS_KEY][ufo_component_id]

                        c.flags &= ~ROUND_XY_TO_GRID
                        if component_lib.get(TRUETYPE_ROUND_KEY, False):
                            c.flags |= ROUND_XY_TO_GRID

                        c.flags &= ~USE_MY_METRICS
                        if component_lib.get(TRUETYPE_METRICS_KEY, False):
                            if use_my_metrics_comp:
                                logger.warning(
                                    f"Ignoring USE_MY_METRICS flag on component '{ufo_component_id}' because"
                                    f"it has been set on component '{use_my_metrics_comp}' already."
                                )
                            else:
                                c.flags |= USE_MY_METRICS
                                use_my_metrics_comp = ufo_component_id

                    # We might automatically set the flags if no data is present,
                    # but:
                    # - https://github.com/googlefonts/ufo2ft/pull/425 recommends
                    #   against setting the ROUND_XY_TO_GRID flag
                    # - USE_MY_METRICS has been set already by
                    #   outlineCompiler.OutlineTTFCompiler.autoUseMyMetrics

                    if i == 0 and TRUETYPE_OVERLAP_KEY in glyph.lib:
                        # Set OVERLAP_COMPOUND on the first component only
                        c.flags &= ~OVERLAP_COMPOUND
                        if glyph.lib.get(TRUETYPE_OVERLAP_KEY, False):
                            c.flags |= OVERLAP_COMPOUND

    def compile_maxp(self):
        maxp = self.font["maxp"]
        ttdata = self.ufo.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
        if ttdata:
            for name in (
                "maxStorage",
                "maxFunctionDefs",
                "maxInstructionDefs",
                "maxStackElements",
                # "maxSizeOfInstructions",  # Is recalculated below
                "maxZones",
                "maxTwilightPoints",
            ):
                value = ttdata.get(name, None)
                if value is not None:
                    setattr(maxp, name, value)

        # Recalculate maxp.maxSizeOfInstructions
        sizes = [
            len(glyph.program.getBytecode())
            for glyph in self.font["glyf"].glyphs.values()
            if hasattr(glyph, "program")
        ] + [0]
        maxp.maxSizeOfInstructions = max(sizes)

    def compile_prep(self):
        self._compile_program("controlValueProgram", "prep")

    def compile(self):
        self.compile_cvt()
        self.compile_fpgm()
        self.compile_prep()
        self.compile_glyf()
        # maxp depends on the other programs, to it needs to be last
        self.compile_maxp()
<commit_msg>Lint lint lint<commit_after># -*- coding: utf-8 -*-
import array
import logging

from fontTools import ttLib
from fontTools.pens.hashPointPen import HashPointPen
from fontTools.ttLib.tables._g_l_y_f import (
    OVERLAP_COMPOUND,
    ROUND_XY_TO_GRID,
    USE_MY_METRICS,
)

logger = logging.getLogger(__name__)

TRUETYPE_INSTRUCTIONS_KEY = "public.truetype.instructions"
TRUETYPE_ROUND_KEY = "public.truetype.roundOffsetToGrid"
TRUETYPE_METRICS_KEY = "public.truetype.useMyMetrics"
TRUETYPE_OVERLAP_KEY = "public.truetype.overlap"
OBJECT_LIBS_KEY = "public.objectLibs"

class InstructionCompiler(object):
    def __init__(self, ufo, ttf):
        self.ufo = ufo
        self.font = ttf

    def _compile_program(self, key, table_tag):
        assert table_tag in ("prep", "fpgm")
        ttdata = self.ufo.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
        if ttdata:
            formatVersion = ttdata.get("formatVersion", None)
            if int(formatVersion) != 1:
                logger.error(
                    f"Unknown formatVersion {formatVersion} "
                    f"in key '{key}', "
                    f"table '{table_tag}' will be empty in font."
                )
                return
            asm = ttdata.get(key, None)
            if asm is not None:
                self.font[table_tag] = table = ttLib.newTable(table_tag)
                table.program = ttLib.tables.ttProgram.Program()
                table.program.fromAssembly(asm)
                # Roundtrip once, or if the font is dumped to XML before having
                # been saved, the assembly code if will look awful.
                table.program.fromBytecode(table.program.getBytecode())

    def compile_cvt(self):
        cvts = []
        ttdata = self.ufo.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
        if ttdata:
            formatVersion = ttdata.get("formatVersion", None)
            if int(formatVersion) != 1:
                logger.error(
                    f"Unknown formatVersion {formatVersion} "
                    f"in key 'controlValue', "
                    f"table 'cvt' will be empty in font."
                )
                return
            cvt_list = ttdata.get("controlValue", None)
            if cvt_list is not None:
                # Convert string keys to int
                cvt_dict = {int(v["id"]): v["value"] for v in cvt_list}
                # Find the maximum cvt index.
                # We can't just use the dict keys because the cvt must be
                # filled consecutively.
                max_cvt = max(cvt_dict.keys())
                # Make value list, filling entries for missing keys with 0
                cvts = [cvt_dict.get(i, 0) for i in range(max_cvt + 1)]

        if cvts:
            # Only write cvt to font if it contains any values
            self.font["cvt "] = cvt = ttLib.newTable("cvt ")
            cvt.values = array.array("h", cvts)

    def compile_fpgm(self):
        self._compile_program("fontProgram", "fpgm")

    def compile_glyf(self):
        for name in sorted(self.ufo.keys()):
            glyph = self.ufo[name]
            ttdata = glyph.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
            if name not in self.font["glyf"]:
                if ttdata is not None:
                    logger.warning(
                        f"Glyph '{name}' not found in font, "
                        "skipping compilation of TrueType instructions "
                        "for this glyph."
                    )
                logger.debug(f"UFO keys: {list(self.ufo.keys())}")
                logger.debug(f"glyf keys: {list(self.font['glyf'].keys())}")
                continue

            glyf = self.font["glyf"][name]
            if ttdata is not None:
                formatVersion = ttdata.get("formatVersion", None)
                if int(formatVersion) != 1:
                    logger.error(
                        f"Unknown formatVersion {formatVersion} "
                        "in glyph '{name}', it will have "
                        "no instructions in font."
                    )
                    continue

                # Check if glyph hash matches the current outlines
                hash_pen = HashPointPen(glyph.width, self.ufo)
                glyph.drawPoints(hash_pen)
                glyph_id = ttdata.get("id", None)
                if glyph_id is None or glyph_id != hash_pen.hash:
                    logger.error(
                        f"Glyph hash mismatch, glyph '{name}' will have "
                        "no instructions in font."
                    )
                    continue

                # Compile the glyph program
                asm = ttdata.get("assembly", None)
                if asm is not None:
                    glyf.program = ttLib.tables.ttProgram.Program()
                    glyf.program.fromAssembly(asm)
                    # Roundtrip once, or if the font is dumped to XML before
                    # having been saved, the assembly code if will look awful.
                    glyf.program.fromBytecode(glyf.program.getBytecode())

            # Handle composites
            if glyf.isComposite():
                # Remove empty glyph programs from composite glyphs
                if hasattr(glyf, "program") and not glyf.program:
                    delattr(glyf, "program")

                # Set component flags

                # We need to decide when to set the flags.
                # Let's assume if any lib key is not there, or the component
                # doesn't have an identifier, we should leave the flags alone.
                use_my_metrics_comp = None
                for i, c in enumerate(glyf.components):
                    if i >= len(glyph.components):
                        logger.error(
                            "Number of components differ between UFO and TTF "
                            f"in glyph '{name}' ({len(glyph.components)} vs. "
                            f"{len(glyf.components)}, not setting flags in "
                            "additional components."
                        )
                        break
                    ufo_component_id = glyph.components[i].identifier
                    if (
                        ufo_component_id is not None
                        and OBJECT_LIBS_KEY in glyph.lib
                        and ufo_component_id in glyph.lib[OBJECT_LIBS_KEY]
                        and (
                            TRUETYPE_ROUND_KEY
                            in glyph.lib[OBJECT_LIBS_KEY][ufo_component_id]
                            or TRUETYPE_METRICS_KEY
                            in glyph.lib[OBJECT_LIBS_KEY][ufo_component_id]
                        )
                    ):
                        component_lib = glyph.lib[OBJECT_LIBS_KEY][ufo_component_id]

                        c.flags &= ~ROUND_XY_TO_GRID
                        if component_lib.get(TRUETYPE_ROUND_KEY, False):
                            c.flags |= ROUND_XY_TO_GRID

                        c.flags &= ~USE_MY_METRICS
                        if component_lib.get(TRUETYPE_METRICS_KEY, False):
                            if use_my_metrics_comp:
                                logger.warning(
                                    "Ignoring USE_MY_METRICS flag on component "
                                    f"'{ufo_component_id}' because it has been set on "
                                    f"component '{use_my_metrics_comp}' already."
                                )
                            else:
                                c.flags |= USE_MY_METRICS
                                use_my_metrics_comp = ufo_component_id

                    # We might automatically set the flags if no data is present,
                    # but:
                    # - https://github.com/googlefonts/ufo2ft/pull/425 recommends
                    #   against setting the ROUND_XY_TO_GRID flag
                    # - USE_MY_METRICS has been set already by
                    #   outlineCompiler.OutlineTTFCompiler.autoUseMyMetrics

                    if i == 0 and TRUETYPE_OVERLAP_KEY in glyph.lib:
                        # Set OVERLAP_COMPOUND on the first component only
                        c.flags &= ~OVERLAP_COMPOUND
                        if glyph.lib.get(TRUETYPE_OVERLAP_KEY, False):
                            c.flags |= OVERLAP_COMPOUND

    def compile_maxp(self):
        maxp = self.font["maxp"]
        ttdata = self.ufo.lib.get(TRUETYPE_INSTRUCTIONS_KEY, None)
        if ttdata:
            for name in (
                "maxStorage",
                "maxFunctionDefs",
                "maxInstructionDefs",
                "maxStackElements",
                # "maxSizeOfInstructions",  # Is recalculated below
                "maxZones",
                "maxTwilightPoints",
            ):
                value = ttdata.get(name, None)
                if value is not None:
                    setattr(maxp, name, value)

        # Recalculate maxp.maxSizeOfInstructions
        sizes = [
            len(glyph.program.getBytecode())
            for glyph in self.font["glyf"].glyphs.values()
            if hasattr(glyph, "program")
        ] + [0]
        maxp.maxSizeOfInstructions = max(sizes)

    def compile_prep(self):
        self._compile_program("controlValueProgram", "prep")

    def compile(self):
        self.compile_cvt()
        self.compile_fpgm()
        self.compile_prep()
        self.compile_glyf()
        # maxp depends on the other programs, to it needs to be last
        self.compile_maxp()
<|endoftext|>


<commit_before># coding: utf-8

from modernrpc.conf import settings as modernrpc_settings
from modernrpc.core import RPCMethod, rpc_method, JSONRPC_PROTOCOL, XMLRPC_PROTOCOL

def dummy_empty():
    pass

class TestRpcMethod:
    """Check wether entry_points and protocol specifications works as expected when registering method"""

    def test_method_always_available(self):
        rpc_method(dummy_empty, 'dummy_name')
        m = RPCMethod(dummy_empty)

        assert m.available_for_entry_point(modernrpc_settings.MODERNRPC_DEFAULT_ENTRYPOINT_NAME)
        assert m.available_for_entry_point('random_entry_point')

        assert m.is_available_in_xml_rpc()
        assert m.is_available_in_json_rpc()

    def test_method_xmlrpc_only(self):
        rpc_method(dummy_empty, 'dummy_name', protocol=XMLRPC_PROTOCOL)
        m = RPCMethod(dummy_empty)

        assert m.is_available_in_xml_rpc()
        assert not m.is_available_in_json_rpc()

    def test_method_jsonrpc_only(self):
        rpc_method(dummy_empty, 'dummy_name', protocol=JSONRPC_PROTOCOL)
        m = RPCMethod(dummy_empty)

        assert not m.is_available_in_xml_rpc()
        assert m.is_available_in_json_rpc()

    def test_method_repr(self):
        rpc_method(dummy_empty, 'dummy_name', protocol=JSONRPC_PROTOCOL)
        m = RPCMethod(dummy_empty)
        assert 'dummy_name' in repr(m)

    def test_method_available_for_entry_point(self):
        rpc_method(dummy_empty, 'dummy_name', entry_point='my_entry_point')
        m = RPCMethod(dummy_empty)

        assert not m.available_for_entry_point(modernrpc_settings.MODERNRPC_DEFAULT_ENTRYPOINT_NAME)
        assert m.available_for_entry_point('my_entry_point')

class TestNoDocNoTypeFunction:
    """Make sure docstring and introspection works as expected"""

    def test_args_and_return(self):
        rpc_method(dummy_empty)
        m = RPCMethod(dummy_empty)
        assert m.args == []
        assert m.args_doc == {}
        assert m.return_doc == {"type": "", "text": ""}

    def test_doc(self):
        rpc_method(dummy_empty)
        m = RPCMethod(dummy_empty)
        assert m.raw_docstring == ""
        assert m.html_doc == ""

@rpc_method
def dummy_with_args_no_doc(a, b, c):
    pass

class TestNoDocNoTypeFunctionWithArgs:
    """Make sure docstring and introspection works as expected"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_with_args_no_doc)
        assert m.args == ["a", "b", "c"]
        assert m.args_doc == {
            'a': {'type': '', 'text': ''},
            'b': {'type': '', 'text': ''},
            'c': {'type': '', 'text': ''}
        }
        assert m.return_doc == {"type": "", "text": ""}

    def test_doc(self):
        m = RPCMethod(dummy_with_args_no_doc)
        assert m.raw_docstring == ""
        assert m.html_doc == ""

@rpc_method
def dummy_args_with_typehints(a: int, b: str, c: list) -> dict:
    pass

class TestNoDocNoTypeFunctionWithArgsTypehints:
    """Make sure docstring and introspection works as expected"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_args_with_typehints)
        assert m.args == ["a", "b", "c"]
        assert m.args_doc == {
            'a': {'type': 'int', 'text': ''},
            'b': {'type': 'str', 'text': ''},
            'c': {'type': 'list', 'text': ''}
        }
        assert m.return_doc == {"type": "dict", "text": ""}

    def test_doc(self):
        m = RPCMethod(dummy_args_with_typehints)
        assert m.raw_docstring == ""
        assert m.html_doc == ""

@rpc_method
def dummy_args_with_typehints_and_text(a: str, b: dict, c: float) -> float:
    """

    :param a: param A
    :param b: param B
    :param c: param C
    :return: A decimal value
    """

class TestEmptyDocTypeHintsFunction:
    """Make sure docstring and introspection works as expected"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_args_with_typehints_and_text)

        assert m.args == ["a", "b", "c"]
        assert m.args_doc == {
            'a': {'type': 'str', 'text': 'param A'},
            'b': {'type': 'dict', 'text': 'param B'},
            'c': {'type': 'float', 'text': 'param C'}
        }
        assert m.return_doc == {"type": "float", "text": "A decimal value"}

    def test_doc(self):
        m = RPCMethod(dummy_args_with_typehints_and_text)

        assert m.raw_docstring == ""
        assert m.html_doc == ""

@rpc_method
def dummy_with_typehints_doctypes_and_text(x: list, y: str) -> dict:
    """This is the docstring part of the function

    It also contains a multi-line description
    :param x: abcd
    :param y: xyz
    :return: efgh
    :type x: float
    :type y: int
    :rtype: str
    """

class TestWithDoctypesAndTypeHintsFunction:
    """Make sure docstring and introspection works as expected"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_with_typehints_doctypes_and_text)

        assert m.args == ["x", "y"]
        assert m.args_doc == {
            'x': {'type': 'float', 'text': 'abcd'},
            'y': {'type': 'int', 'text': 'xyz'},
        }
        assert m.return_doc == {"type": "str", "text": "efgh"}

    def test_doc(self):
        m = RPCMethod(dummy_with_typehints_doctypes_and_text)

        assert m.raw_docstring == (
            "This is the docstring part of the function\n\nIt also contains a multi-line description"
        )
        assert m.html_doc == (
            "
This is the docstring part of the function

It also contains a multi-line description
"
        )

def dummy_function_with_doc(name, birthdate, sex):
    """
    This is the textual doc of the method
    :param name: A name
    :param birthdate: A birthdate
    :param sex: Male or Female
    :type name: str
    :type birthdate: datetime.datetime
    :type sex: str
    :return: A string representation of given arguments
    """
    return '{} ({}) born on {}'.format(name, str(sex), str(birthdate))

def single_line_documented():
    """*italic*, **strong**, normal text"""
    return 111

def multi_line_documented_1():
    """
    This method has multi-lines documentation.

    The content is indented when raw ``__doc__`` attribute function is read.
    The indentation should not interfere with semantic interpretation of the docstring.
    """
    return 111

def multi_line_documented_2():
    """
    This method has *multi-lines* **documentation**.

    Here is a quote block:

        abcdef 123456

    """
    return "abc"

class TestMethodDocumentation:
    def test_raw_documentation(self, settings):
        settings.MODERNRPC_DOC_FORMAT = ''

        rpc_method(single_line_documented, 'dummy_name')
        method = RPCMethod(single_line_documented)
        assert '*italic*, **strong**, normal text' == method.raw_docstring

        rpc_method(multi_line_documented_1, 'dummy_name_2')
        method = RPCMethod(multi_line_documented_1)
        assert ('This method has multi-lines documentation.\n\nThe content is indented when '
                'raw ``__doc__`` attribute function is read.\nThe indentation should not interfere '
                'with semantic interpretation of the docstring.') == method.raw_docstring

        rpc_method(multi_line_documented_2, 'dummy_name_3')
        method = RPCMethod(multi_line_documented_2)
        assert ('This method has *multi-lines* **documentation**.\n\nHere is '
                'a quote block:\n\n    abcdef 123456') == method.raw_docstring

    def test_html_documentation_simple(self, settings):
        settings.MODERNRPC_DOC_FORMAT = ''

        rpc_method(single_line_documented, 'dummy_name')
        method = RPCMethod(single_line_documented)
        assert '*italic*, **strong**, normal text' in method.html_doc

        rpc_method(multi_line_documented_1, 'dummy_name_2')
        method = RPCMethod(multi_line_documented_1)
        # We ensure no
 is added to the text when the original docstring contained single \n characters
        assert 'attribute function is read. The indentation should not' in method.html_doc

    def test_html_documentation_markdown(self, settings):
        settings.MODERNRPC_DOC_FORMAT = 'md'

        rpc_method(single_line_documented, 'dummy_name')
        method = RPCMethod(single_line_documented)
        assert 'italic, strong, normal text' in method.html_doc

        rpc_method(multi_line_documented_1, 'dummy_name_2')
        method = RPCMethod(multi_line_documented_1)
        assert '
' not in method.html_doc

        rpc_method(multi_line_documented_2, 'dummy_name_3')
        method = RPCMethod(multi_line_documented_2)
        assert 'multi-lines' in method.html_doc.replace('\n', '')
        assert 'documentation' in method.html_doc.replace('\n', '')
        assert '
abcdef 123456
' in method.html_doc.replace('\n', '')

    def test_html_documentation_reST(self, settings):
        settings.MODERNRPC_DOC_FORMAT = 'rst'

        rpc_method(single_line_documented, 'dummy_name')
        method = RPCMethod(single_line_documented)
        assert 'italic, strong, normal text' in method.html_doc

        rpc_method(multi_line_documented_1, 'dummy_name_2')
        method = RPCMethod(multi_line_documented_1)
        assert '
' not in method.html_doc

        rpc_method(multi_line_documented_2, 'dummy_name_3')
        method = RPCMethod(multi_line_documented_2)
        assert 'multi-lines' in method.html_doc.replace('\n', '')
        assert 'documentation' in method.html_doc.replace('\n', '')
        assert '
abcdef 123456
' in method.html_doc.replace('\n', '')
<commit_msg>Fix unit tests with new docstring & typehints features<commit_after># coding: utf-8

from modernrpc.conf import settings as modernrpc_settings
from modernrpc.core import RPCMethod, rpc_method, JSONRPC_PROTOCOL, XMLRPC_PROTOCOL

def dummy_empty():
    pass

class TestRpcMethodEntryPointProtocol:
    """Check whether entry_points and protocol specifications works as expected when registering method"""

    def test_method_always_available(self):
        rpc_method(dummy_empty, 'dummy_name')
        m = RPCMethod(dummy_empty)

        assert m.available_for_entry_point(modernrpc_settings.MODERNRPC_DEFAULT_ENTRYPOINT_NAME)
        assert m.available_for_entry_point('random_entry_point')

        assert m.is_available_in_xml_rpc()
        assert m.is_available_in_json_rpc()

    def test_method_xmlrpc_only(self):
        rpc_method(dummy_empty, 'dummy_name', protocol=XMLRPC_PROTOCOL)
        m = RPCMethod(dummy_empty)

        assert m.is_available_in_xml_rpc()
        assert not m.is_available_in_json_rpc()

    def test_method_jsonrpc_only(self):
        rpc_method(dummy_empty, 'dummy_name', protocol=JSONRPC_PROTOCOL)
        m = RPCMethod(dummy_empty)

        assert not m.is_available_in_xml_rpc()
        assert m.is_available_in_json_rpc()

    def test_method_repr(self):
        rpc_method(dummy_empty, 'dummy_name', protocol=JSONRPC_PROTOCOL)
        m = RPCMethod(dummy_empty)
        assert 'dummy_name' in repr(m)

    def test_method_available_for_entry_point(self):
        rpc_method(dummy_empty, 'dummy_name', entry_point='my_entry_point')
        m = RPCMethod(dummy_empty)

        assert not m.available_for_entry_point(modernrpc_settings.MODERNRPC_DEFAULT_ENTRYPOINT_NAME)
        assert m.available_for_entry_point('my_entry_point')

@rpc_method
def dummy_no_doc_no_args():
    pass

class TestNoDocNoArgs:
    """Make sure docstring and introspection works as expected"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_no_doc_no_args)
        assert m.args == []
        assert m.args_doc == {}
        assert m.return_doc == {"type": "", "text": ""}

    def test_doc(self):
        m = RPCMethod(dummy_no_doc_no_args)
        assert m.raw_docstring == ""
        assert m.html_doc == ""

@rpc_method
def dummy_no_doc_with_args(a, b, c):
    pass

class TestNoDocWithArgs:
    """Make sure docstring and introspection works as expected"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_no_doc_with_args)
        assert m.args == ["a", "b", "c"]
        assert m.args_doc == {
            'a': {'type': '', 'text': ''},
            'b': {'type': '', 'text': ''},
            'c': {'type': '', 'text': ''}
        }
        assert m.return_doc == {"type": "", "text": ""}

    def test_doc(self):
        m = RPCMethod(dummy_no_doc_with_args)
        assert m.raw_docstring == ""
        assert m.html_doc == ""

@rpc_method
def dummy_args_with_typehints(a: int, b: str, c: list) -> dict:
    pass

class TestArgsTypeHint:
    """Type hinting is correctly returned using introspection module"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_args_with_typehints)
        assert m.args == ["a", "b", "c"]
        assert m.args_doc == {
            'a': {'type': 'int', 'text': ''},
            'b': {'type': 'str', 'text': ''},
            'c': {'type': 'list', 'text': ''}
        }
        assert m.return_doc == {"type": "dict", "text": ""}

    def test_doc(self):
        m = RPCMethod(dummy_args_with_typehints)
        assert m.raw_docstring == ""
        assert m.html_doc == ""

@rpc_method
def dummy_typehints_and_standard_docstring(a: str, b: dict, c: float) -> float:
    """

    :param a: param A
    :param b: param B
    :param c: param C
    :return: A decimal value
    """

class TestWithTypeHintAndDocstring:
    """Typehint + standard reStructuredText docstring"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_typehints_and_standard_docstring)

        assert m.args == ["a", "b", "c"]
        assert m.args_doc == {
            'a': {'type': 'str', 'text': 'param A'},
            'b': {'type': 'dict', 'text': 'param B'},
            'c': {'type': 'float', 'text': 'param C'}
        }
        assert m.return_doc == {"type": "float", "text": "A decimal value"}

    def test_doc(self):
        m = RPCMethod(dummy_typehints_and_standard_docstring)

        assert m.raw_docstring == ""
        assert m.html_doc == ""

@rpc_method
def dummy_typehints_and_rst_docstring_types(x: list, y: str) -> dict:
    """This is the docstring part of the function

    It also contains a multi-line description
    :param x: abcd
    :param y: xyz
    :return: efgh
    :type x: float
    :type y: int
    :rtype: str
    """

class TestWithTypeHintAndDocstringTypes:
    """Legacy standard docstring must override typehint"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_typehints_and_rst_docstring_types)

        assert m.args == ["x", "y"]
        assert m.args_doc == {
            'x': {'type': 'float', 'text': 'abcd'},
            'y': {'type': 'int', 'text': 'xyz'},
        }
        assert m.return_doc == {"type": "str", "text": "efgh"}

    def test_doc(self):
        m = RPCMethod(dummy_typehints_and_rst_docstring_types)

        assert m.raw_docstring == (
            "This is the docstring part of the function\n\nIt also contains a multi-line description"
        )
        assert m.html_doc == (
            "
This is the docstring part of the function

It also contains a multi-line description
"
        )

@rpc_method
def dummy_rst_docstring(name, birthdate, sex):
    """
    This is the textual doc of the method
    :param name: A name
    :param birthdate: A birthdate
    :param sex: Male or Female
    :type name: str
    :type birthdate: datetime.datetime
    :type sex: str
    :return: A string representation of given arguments
    """
    return '{} ({}) born on {}'.format(name, str(sex), str(birthdate))

class TestWithDoctstringOnlyFunction:
    """No typehint but everything in reStructuredText docstring"""

    def test_args_and_return(self):
        m = RPCMethod(dummy_rst_docstring)

        assert m.args == ['name', 'birthdate', 'sex']
        assert dict(m.args_doc) == {
            'name': {'type': 'str', 'text': 'A name'},
            'birthdate': {'type': 'datetime.datetime', 'text': 'A birthdate'},
            'sex': {'type': 'str', 'text': 'Male or Female'}
        }
        assert m.return_doc == {"type": "", "text": "A string representation of given arguments"}

    def test_doc(self):
        m = RPCMethod(dummy_rst_docstring)
        assert m.raw_docstring == "This is the textual doc of the method"
        assert m.html_doc == "
This is the textual doc of the method
"

@rpc_method
def single_line_documented():
    """*italic*, **strong**, normal text"""
    return 111

class TestSingleLineDoc:
    """Standard single-line docstring with various parsers"""

    @staticmethod
    def method():
        return RPCMethod(single_line_documented)

    def test_raw_doc(self):
        method = self.method()
        assert '*italic*, **strong**, normal text' == method.raw_docstring
        assert '
*italic*, **strong**, normal text
' == method.html_doc

    def test_markdown_to_html(self, settings):
        settings.MODERNRPC_DOC_FORMAT = 'md'
        assert '
italic, strong, normal text
' == self.method().html_doc

    def test_rst_to_html(self, settings):
        settings.MODERNRPC_DOC_FORMAT = 'rst'
        assert '
italic, strong, normal text
\n' == self.method().html_doc

@rpc_method
def multiline_doc_simple():
    """
    This method has multi-lines documentation.

    The content is indented when raw ``__doc__`` attribute function is read.
    The indentation should not interfere with semantic interpretation of the docstring.
    """
    return 111

class TestMultiLinesDocSimple:
    """Standard multi-line docstring"""

    @staticmethod
    def method():
        return RPCMethod(multiline_doc_simple)

    def test_raw_doc(self):
        method = self.method()
        assert method.raw_docstring == (
            "This method has multi-lines documentation.\n\n"
            "The content is indented when raw ``__doc__`` attribute function is read.\n"
            "The indentation should not interfere with semantic interpretation of the docstring."
        )
        assert self.method().html_doc == (
            '
This method has multi-lines documentation.
'
            '
The content is indented when raw ``__doc__`` attribute function is read. '
            'The indentation should not interfere with semantic interpretation of the docstring.
'
        )

    def test_markdown_to_html(self, settings):
        settings.MODERNRPC_DOC_FORMAT = 'md'
        assert self.method().html_doc == (
            '
This method has multi-lines documentation.
\n'
            '
The content is indented when raw __doc__ attribute function is read.\n'
            'The indentation should not interfere with semantic interpretation of the docstring.
'
        )

    def test_rst_to_html(self, settings):
        settings.MODERNRPC_DOC_FORMAT = 'rst'
        assert self.method().html_doc == (
            '
This method has multi-lines documentation.
\n'
            '
The content is indented when raw <tt class="docutils literal">__doc__</tt> attribute function is '
            'read.\nThe indentation should not interfere with semantic interpretation of the docstring.
\n'
        )

@rpc_method
def multiline_doc_with_block():
    """
    This method has *multi-lines* **documentation**.

    Here is a quote block:

        abcdef 123456

    """
    return "abc"

class TestMultiLinesWithBlock:
    """Standard multi-line docstring with an indented block"""

    @staticmethod
    def method():
        return RPCMethod(multiline_doc_with_block)

    def test_raw_doc(self):
        method = self.method()
        assert method.raw_docstring == (
            "This method has *multi-lines* **documentation**.\n\nHere is a quote block:\n\n    abcdef 123456"
        )
        assert self.method().html_doc == (
            "
This method has *multi-lines* **documentation**.
"
            "
Here is a quote block:
"
            "
    abcdef 123456
"
        )

    def test_markdown_to_html(self, settings):
        settings.MODERNRPC_DOC_FORMAT = 'md'
        assert self.method().html_doc == (
            "
This method has multi-lines documentation.
\n"
            "
Here is a quote block:
\n"
            "
abcdef 123456\n
"
        )

    def test_rst_to_html(self, settings):
        settings.MODERNRPC_DOC_FORMAT = 'rst'
        assert self.method().html_doc == (
            '
This method has multi-lines documentation.
\n'
            '
Here is a quote block:
\n'
            '
\nabcdef 123456
\n'
        )
<|endoftext|>


<commit_before>from plugin import Plugin, PluginDocument

from database import TableBase, Column, Text
from database import engine

class PanoDocument(TableBase, PluginDocument):

    __tablename__ = 'pano'

    def __init__(self, owner, title='Untitled', text=''):
        super().__init__()
        self.owner = owner
        self.title = title
        self.text = text
        self.description = 'not support description'

    text = Column(Text())

class TextFile(Plugin):
    def __init__(self):
        super().__init__()
        print('example plugin loaded')
        self.documents.set_document_type('text')
        self.documents.set_document_table(TextFileDocument)
        self.name = 'example_plugin'
        try:
            TextFileDocument.__table__.create(engine)
        except:
            pass

    def process(self, request):
        print('example plugin got a request:' + str(request))
        if request['action'] == 'new':
            doc = TextFileDocument(self.user.user_id)
            self.documents.create(doc)
            return {'id': doc.id}
        elif request['action'] == 'save':
            doc = self.documents.get(request['id'])
            doc.title = request['title']
            doc.text = request['text']
            self.documents.update(doc)
            return {}
        elif request['action'] == 'delete':
            doc = self.documents.get(request['id'])
            self.documents.delete(doc)
            return {}
        elif request['action'] == 'get':
            doc = self.documents.get(request['id'])
            return {'id': doc.id, 'owner': doc.owner, 'title': doc.title, 'text': doc.text,
                    'last_modified': doc.last_modified}
        else:
            return {'success': False, 'reason': 'unknown action: ' + request['action']}

    def unload(self):
        print('example plugin unloaded')

__plugin__ = TextFile()
<commit_msg>delete unfinished pano, wzb will code it<commit_after><|endoftext|>