<commit_before># Framework for setting Input and Output expectations in a unit test.
#
# This file defines the class ExpectedInputOutput which can be used to override
# sys.stdout to validate that expected outputs are printed and sys.stdin to
# inject mock responses in the standard input. This is useful in unit test, to
# validate that a command line tool behaves as expected.
#
# The following example tests a program printing a few lines, asking a question and
# acting according to the response:
#
#   import io_expectation as expect
#
#   mock_io = expect.ExpectedInputOutput()
#   sys.stdin = mock_io
#   sys.stdout = mock_io
#
#   # Set expected I/Os.
#   mock_io.set_expected_io(
#     expect.InOrder(
#       expect.Contains('initialization'),
#       expect.Anything().repeatedly(),
#       expect.Equals('Proceed?'),
#       expect.Reply('yes'),
#       expect.Prefix('Success')))
#
#   # Run the program under test.
#   print('Program initialization...')
#   print('Loading resources...')
#   print('Initialization complete.')
#   print('Proceed? ')
#   if raw_input() == 'yes':
#     print('Success')
#   else:
#     print('Aborting')
#
#   # Validate that the program matched all expectations.
#   mock_io.assert_expectations_fulfilled()

import copy
import difflib
import re
import StringIO
import sys

def default_expectation(expected_io):
  """Defines the behavior of python standard types when used as expectation.

  This is used to allow syntactic sugar, where expectation can be specified
  using a lightweight syntax. For instance, the following two statement produce
  the same expectations:

  io.set_expected_io(AnyOrder(Contains('foo'), Contains('bar')))

  io.set_expected_io(['foo', 'bar'])

  Args:
    expected_io: python standard type

  Returns:
    ExpectBase subclass instance.
  """
  if isinstance(expected_io, (list, tuple)):
    return InOrder(*expected_io)
  elif isinstance(expected_io, set):
    return AnyOrder(*expected_io)
  elif isinstance(expected_io, basestring):
    return Contains(expected_io)
  else:
    return expected_io

class ExpectBase(object):
  """Base class for all expected response string matchers."""

  def __init__(self):
    self._fulfilled = False
    self._saturated = False
    self._greedy = False

  @property
  def fulfilled(self):
    """True when the expectation base condition has been met.
    The expectation could still accept more matches, until it's saturated.
    """
    return self._fulfilled

  @property
  def saturated(self):
    """True when the expectation reached it's upper limit of allowed matches."""
    return self._saturated

  @property
  def greedy(self):
    """If True, repeated expectation will match as much as possible, possibly
    starving the following expectations."""
    return self._greedy

  def consume(self, string):
    """Matches a string against this expectation.

    Consumer expectation sub-classes must override this function.

    The expectation's internal states is updated accordingly. For instance,
    sequence expectations advances their state to the next expected IO.

    Args:
      str: the string to match against the expectation.

    Returns:
      bool: True if the string matched successfully."""
    return False

  def test_consume(self, string):
    """Tests if the expectation would success at consuming a string.

    Consumer expectation sub-classes must override this function.

    The internal state of the expectation are left untouched.

    Returns:
      bool: True if the string would match successfully.
    """
    return False

  def produce(self):
    """Produces a string, as if entered by the user in response to a prompt.

    Producer expectation sub-classes must override this function.

    Returns:
      str, the string produced by the expectation.
    """
    return None

  def repeatedly(self):
    """Shorthand for making this expectation repeat indefinitely.

    Returns:
      Repeatedly instance: wrapped version of self, which repeats 0 or more
          times.
    """
    return Repeatedly(self)

  def times(self, min_repetition, max_repetition=None):
    """Shorthand for making this expectation repeat a given number of times.

    Args:
      min_repetition: int, the minimum number of times this expectation needs to
        match for it to be fulfilled.
      max_repetition: int, the number of repetition after which this expectation
        is saturated and won't successfully match another time.

    Returns:
      Repeatedly instance: wrapped version of self, which repeats from
      min_repetition to max_repetition times.
    """
    max_repetition = max_repetition or min_repetition
    return Repeatedly(self, min_repetition, max_repetition)

  def apply_transform(self, fn):
    """Apply a transformation on the expectation definition.

    The ExpectedInputOutput class calls this function on every expectations
    registered, which is useful for 'patching' all expectations with the same
    transformation. A common way to use this is to fix path delimiters to match
    the OS format in expectations containing paths.

    Args:
      fn: callback function to call to transform the expectation. Callback
          takes a string as input and returns a transforms version of that
          string.
    """
    pass

class ExpectStringBase(ExpectBase):

  def __init__(self, expected):
    super(ExpectStringBase, self).__init__()
    self._expected = expected

  def consume(self, string):
    self._fulfilled = self._saturated = self.test_consume(string)
    return self._fulfilled

  def test_consume(self, string):
    return self._match(string)

  def apply_transform(self, fn):
    self._expected = fn(self._expected)

  def _match(self, string):
    raise NotImplementedError()

  def __repr__(self):
    return '%s(%s)' % (type(self).__name__, repr(self._expected))

class Equals(ExpectStringBase):
  """Matches a string equal to the specified string.

  Leading and trailing white-spaces are stripped.
  """

  def _match(self, string):
    return string.strip() == self._expected.strip()

class Contains(ExpectStringBase):
  """Matches a string containing a specific sub-string."""

  def _match(self, string):
    return self._expected in string

class Prefix(ExpectStringBase):
  """Matches a string having a specific prefix."""

  def _match(self, string):
    return string.strip().startswith(self._expected)

class Regex(ExpectStringBase):
  """Matches a string using a regular expression."""

  def _match(self, other):
    return re.match(self._expected, other, re.DOTALL)

class Anything(ExpectBase):
  """Matches anything once."""

  def consume(self, string):
    self._fulfilled = self._saturated = self.test_consume(string)
    return True

  def test_consume(self, string):
    return True

  def __repr__(self):
    return '%s()' % type(self).__name__

class Repeatedly(ExpectBase):
  """Wraps an expectation to make it repeat a given number of times."""

  def __init__(self, sub_expectation, min_repetition=0, max_repetition=None):
    super(Repeatedly, self).__init__()
    self._min_repetition = min_repetition
    self._max_repetition = max_repetition
    self._sub_expectation = default_expectation(sub_expectation)
    self._current_repetition = 0
    self._current_expectation = copy.deepcopy(self._sub_expectation)

  @property
  def fulfilled(self):
    return self._current_repetition >= self._min_repetition

  @property
  def saturated(self):
    return (self._max_repetition is not None and
            self._current_repetition >= self._max_repetition)

  def consume(self, string):
    result = self._current_expectation.consume(string)
    if self._current_expectation.saturated:
      self._current_repetition += 1
      self._current_expectation = copy.deepcopy(self._sub_expectation)
    return result

  def test_consume(self, string):
    return self._current_expectation.test_consume(string)

  def produce(self):
    result = self._current_expectation.produce()
    if self._current_expectation.saturated:
      self._current_repetition += 1
      self._current_expectation = copy.deepcopy(self._sub_expectation)
    return result

  def apply_transform(self, fn):
    self._sub_expectation.apply_transform(fn)

  def __repr__(self):
    arg1 = max(self._min_repetition - self._current_repetition, 0)
    arg2 = (self._max_repetition - self._current_repetition
            if self._max_repetition is not None else None)
    return '%s(%s%s%s)' % (
      type(self).__name__, repr(self._sub_expectation),
      ', %d' % arg1 if arg1 > 0 or arg2 is not None else '',
      ', %d' % arg2 if arg2 else '')

class ExpectSequenceBase(ExpectBase):
  """Base class for all sequence-based expectations."""

  def __init__(self, *args):
    super(ExpectSequenceBase, self).__init__()
    self._expected_list = [default_expectation(expected) for expected in args]

  @property
  def fulfilled(self):
    return all(e.fulfilled for e in self._expected_list)

  @property
  def saturated(self):
    return all(e.saturated for e in self._expected_list)

  def apply_transform(self, fn):
    for expected in self._expected_list:
      expected.apply_transform(fn)

  def __repr__(self):
    unfulfilled = [repr(a) for a in self._expected_list if not a.fulfilled]
    if len(unfulfilled) == 1:
      return unfulfilled[0]
    else:
      return '%s(%s)' % (type(self).__name__, ', '.join(unfulfilled))

class InOrder(ExpectSequenceBase):
  """Sequence of expectations that must match in right order."""

  def consume(self, string):
    to_consume = None
    for i, expected in enumerate(self._expected_list):
      matches = expected.test_consume(string)
      if matches:
        to_consume = (i, expected)
        if expected.greedy or not expected.fulfilled:
          break

      elif expected.fulfilled:
        continue

      else:
        break

    if to_consume is not None:
      i, expected = to_consume
      consumed = expected.consume(string)
      assert(consumed)
      # We got a match somewhere down the sequence. Discard any preceding
      # fulfilled expectations.
      self._expected_list = self._expected_list[i:]
      if expected.saturated:
        self._expected_list.remove(expected)
      return consumed

    return False

  def test_consume(self, string):
    for expected in self._expected_list:
      if expected.test_consume(string):
        return True
      elif not expected.fulfilled:
        return False
    return False

  def produce(self):
    for i, expected in enumerate(self._expected_list):
      result = expected.produce()
      if result:
        # We got a match somewhere down the sequence. Discard any preceding
        # fulfilled expectations.
        self._expected_list = self._expected_list[i:]
        if expected.saturated:
          self._expected_list.remove(expected)
        return result
      elif not expected.fulfilled:
        return None
    return None

class AnyOrder(ExpectSequenceBase):
  """Sequence of expectation that can match in any order."""

  def consume(self, string):
    to_consume = None
    for expected in self._expected_list:
      if expected.test_consume(string):
        to_consume = expected
        if expected.greedy or not expected.fulfilled:
          break

    if to_consume is not None:
      consumed = to_consume.consume(string)
      assert(consumed)
      if to_consume.saturated:
        self._expected_list.remove(to_consume)
      return True

    return False

  def test_consume(self, string):
    return any(expected.test_consume(string)
               for expected in self._expected_list)

  def produce(self):
    for expected in self._expected_list:
      result = expected.produce()
      if result:
        if expected.saturated:
          self._expected_list.remove(expected)
        return result
    return None

def Somewhere(expectation):
  """Match an expectation anywhere in a document."""
  return InOrder(Anything().repeatedly(),
                 expectation,
                 Anything().repeatedly())

class Reply(ExpectBase):
  """Expects a read to the input pipe and replies with a specific string."""

  def __init__(self, reply_string):
    super(Reply, self).__init__()
    self._reply_string = reply_string

  def produce(self):
    self._fulfilled = self._saturated = True
    return self._reply_string

  def _consume(self, line):
    raise AssertionError('Expecting user input but got output line: %s' % line)

  def __repr__(self):
    return '%s(%s)' % (type(self).__name__, self._reply_string)

class ExpectedInputOutput(object):
  """File object for overriding stdin/out, mocking inputs & checking outputs."""

  def __init__(self):
    self.set_transform_fn(None)
    self.set_expected_io(None)
    self._original_stdin = sys.stdin
    self._original_stdout = sys.stdout

  def set_transform_fn(self, transform_fn):
    """Callback to transform all expectations passed in set_expected_io.

    Useful for 'patching' all expectations with the same transformation. A
    common way to use this is to fix path delimiters to match the OS format in
    expectations containing paths.

    Args:
      transform_fn: callback function to call to transform the expectation.
          Callback takes a string as input and returns a transforms version of
          that string.
    """
    self._transform_fn = transform_fn

  def set_expected_io(self, expected_io):
    """Set an expectation for the next sequence of IOs.

    Args:
      expected_io: instance of an ExpectBase subclass, the expectation to apply
          all input and outputs against.
    """
    self._expected_io = default_expectation(expected_io)

    if self._expected_io and self._transform_fn:
      self._expected_io.apply_transform(self._transform_fn)

    self._cmd_output = StringIO.StringIO()

  def write(self, string):
    """File object 'write' method, matched against the next expected output.

    Args:
      string: str, string being written to stdout.
    """
    self._original_stdout.write(string)
    self._cmd_output.write(string)

  def flush(self):
    """File object 'flush' method."""
    self._original_stdout.flush()

  def readline(self):
    """File object 'readline' method, replied using the next expected input.

    Returns:
      str, the mock string faking a read from stdin.

    Raises:
      AssertionError: raised when IOs do not match expectations.
    """
    if self._expected_io is None:
      raise AssertionError('Trying to readline but no expected IO was set.')

    self._match_pending_outputs()
    if self._expected_io.saturated:
      raise AssertionError('No more user input prompt expected')
    reply = self._expected_io.produce()
    if not reply:
      raise AssertionError('Unexpected user input prompt request. Expected:\n'
                           '%s' % repr(self._expected_io))
    reply += '\n'
    self._original_stdout.write(reply)
    return reply

  def assert_expectations_fulfilled(self):
    """Asserts that all expectation are fulfilled.

    Resets this object, ready to restart with a new set_expected_io.

    Raises:
      AssertionError: raised when IOs do not match expectations.
    """
    self._match_pending_outputs()
    if self._expected_io:
      if not self._expected_io.fulfilled:
        raise AssertionError('Pending IO expectation never fulfulled:\n%s' %
                             repr(self._expected_io))

    self.set_expected_io(None)

  def _match_pending_outputs(self):
    """Match any pending IO against the expectations.

    Raises:
      AssertionError: raised when IOs do not match expectations.
    """
    output_lines = self._cmd_output.getvalue().splitlines(True)
    self._cmd_output = StringIO.StringIO()

    if self._expected_io:
      for line in output_lines:
        if self._expected_io.saturated:
          raise AssertionError('No more output expected, but got: \'%s\'' %
                               line)
        if not self._expected_io.consume(line):
          raise AssertionError('Unexpected output:\n'
                               '%s' % '\n'.join(difflib.ndiff(
                                 repr(self._expected_io).splitlines(True),
                                 repr(line).splitlines(True))))
<commit_msg>Keeps line below 80 characters.<commit_after># Framework for setting Input and Output expectations in a unit test.
#
# This file defines the class ExpectedInputOutput which can be used to override
# sys.stdout to validate that expected outputs are printed and sys.stdin to
# inject mock responses in the standard input. This is useful in unit test, to
# validate that a command line tool behaves as expected.
#
# The following example tests a program printing a few lines, asking a question
# and acting according to the response:
#
#   import io_expectation as expect
#
#   mock_io = expect.ExpectedInputOutput()
#   sys.stdin = mock_io
#   sys.stdout = mock_io
#
#   # Set expected I/Os.
#   mock_io.set_expected_io(
#     expect.InOrder(
#       expect.Contains('initialization'),
#       expect.Anything().repeatedly(),
#       expect.Equals('Proceed?'),
#       expect.Reply('yes'),
#       expect.Prefix('Success')))
#
#   # Run the program under test.
#   print('Program initialization...')
#   print('Loading resources...')
#   print('Initialization complete.')
#   print('Proceed? ')
#   if raw_input() == 'yes':
#     print('Success')
#   else:
#     print('Aborting')
#
#   # Validate that the program matched all expectations.
#   mock_io.assert_expectations_fulfilled()

import copy
import difflib
import re
import StringIO
import sys

def default_expectation(expected_io):
  """Defines the behavior of python standard types when used as expectation.

  This is used to allow syntactic sugar, where expectation can be specified
  using a lightweight syntax. For instance, the following two statement produce
  the same expectations:

  io.set_expected_io(AnyOrder(Contains('foo'), Contains('bar')))

  io.set_expected_io(['foo', 'bar'])

  Args:
    expected_io: python standard type

  Returns:
    ExpectBase subclass instance.
  """
  if isinstance(expected_io, (list, tuple)):
    return InOrder(*expected_io)
  elif isinstance(expected_io, set):
    return AnyOrder(*expected_io)
  elif isinstance(expected_io, basestring):
    return Contains(expected_io)
  else:
    return expected_io

class ExpectBase(object):
  """Base class for all expected response string matchers."""

  def __init__(self):
    self._fulfilled = False
    self._saturated = False
    self._greedy = False

  @property
  def fulfilled(self):
    """True when the expectation base condition has been met.
    The expectation could still accept more matches, until it's saturated.
    """
    return self._fulfilled

  @property
  def saturated(self):
    """True when the expectation reached it's upper limit of allowed matches."""
    return self._saturated

  @property
  def greedy(self):
    """If True, repeated expectation will match as much as possible, possibly
    starving the following expectations."""
    return self._greedy

  def consume(self, string):
    """Matches a string against this expectation.

    Consumer expectation sub-classes must override this function.

    The expectation's internal states is updated accordingly. For instance,
    sequence expectations advances their state to the next expected IO.

    Args:
      str: the string to match against the expectation.

    Returns:
      bool: True if the string matched successfully."""
    return False

  def test_consume(self, string):
    """Tests if the expectation would success at consuming a string.

    Consumer expectation sub-classes must override this function.

    The internal state of the expectation are left untouched.

    Returns:
      bool: True if the string would match successfully.
    """
    return False

  def produce(self):
    """Produces a string, as if entered by the user in response to a prompt.

    Producer expectation sub-classes must override this function.

    Returns:
      str, the string produced by the expectation.
    """
    return None

  def repeatedly(self):
    """Shorthand for making this expectation repeat indefinitely.

    Returns:
      Repeatedly instance: wrapped version of self, which repeats 0 or more
          times.
    """
    return Repeatedly(self)

  def times(self, min_repetition, max_repetition=None):
    """Shorthand for making this expectation repeat a given number of times.

    Args:
      min_repetition: int, the minimum number of times this expectation needs to
        match for it to be fulfilled.
      max_repetition: int, the number of repetition after which this expectation
        is saturated and won't successfully match another time.

    Returns:
      Repeatedly instance: wrapped version of self, which repeats from
      min_repetition to max_repetition times.
    """
    max_repetition = max_repetition or min_repetition
    return Repeatedly(self, min_repetition, max_repetition)

  def apply_transform(self, fn):
    """Apply a transformation on the expectation definition.

    The ExpectedInputOutput class calls this function on every expectations
    registered, which is useful for 'patching' all expectations with the same
    transformation. A common way to use this is to fix path delimiters to match
    the OS format in expectations containing paths.

    Args:
      fn: callback function to call to transform the expectation. Callback
          takes a string as input and returns a transforms version of that
          string.
    """
    pass

class ExpectStringBase(ExpectBase):

  def __init__(self, expected):
    super(ExpectStringBase, self).__init__()
    self._expected = expected

  def consume(self, string):
    self._fulfilled = self._saturated = self.test_consume(string)
    return self._fulfilled

  def test_consume(self, string):
    return self._match(string)

  def apply_transform(self, fn):
    self._expected = fn(self._expected)

  def _match(self, string):
    raise NotImplementedError()

  def __repr__(self):
    return '%s(%s)' % (type(self).__name__, repr(self._expected))

class Equals(ExpectStringBase):
  """Matches a string equal to the specified string.

  Leading and trailing white-spaces are stripped.
  """

  def _match(self, string):
    return string.strip() == self._expected.strip()

class Contains(ExpectStringBase):
  """Matches a string containing a specific sub-string."""

  def _match(self, string):
    return self._expected in string

class Prefix(ExpectStringBase):
  """Matches a string having a specific prefix."""

  def _match(self, string):
    return string.strip().startswith(self._expected)

class Regex(ExpectStringBase):
  """Matches a string using a regular expression."""

  def _match(self, other):
    return re.match(self._expected, other, re.DOTALL)

class Anything(ExpectBase):
  """Matches anything once."""

  def consume(self, string):
    self._fulfilled = self._saturated = self.test_consume(string)
    return True

  def test_consume(self, string):
    return True

  def __repr__(self):
    return '%s()' % type(self).__name__

class Repeatedly(ExpectBase):
  """Wraps an expectation to make it repeat a given number of times."""

  def __init__(self, sub_expectation, min_repetition=0, max_repetition=None):
    super(Repeatedly, self).__init__()
    self._min_repetition = min_repetition
    self._max_repetition = max_repetition
    self._sub_expectation = default_expectation(sub_expectation)
    self._current_repetition = 0
    self._current_expectation = copy.deepcopy(self._sub_expectation)

  @property
  def fulfilled(self):
    return self._current_repetition >= self._min_repetition

  @property
  def saturated(self):
    return (self._max_repetition is not None and
            self._current_repetition >= self._max_repetition)

  def consume(self, string):
    result = self._current_expectation.consume(string)
    if self._current_expectation.saturated:
      self._current_repetition += 1
      self._current_expectation = copy.deepcopy(self._sub_expectation)
    return result

  def test_consume(self, string):
    return self._current_expectation.test_consume(string)

  def produce(self):
    result = self._current_expectation.produce()
    if self._current_expectation.saturated:
      self._current_repetition += 1
      self._current_expectation = copy.deepcopy(self._sub_expectation)
    return result

  def apply_transform(self, fn):
    self._sub_expectation.apply_transform(fn)

  def __repr__(self):
    arg1 = max(self._min_repetition - self._current_repetition, 0)
    arg2 = (self._max_repetition - self._current_repetition
            if self._max_repetition is not None else None)
    return '%s(%s%s%s)' % (
      type(self).__name__, repr(self._sub_expectation),
      ', %d' % arg1 if arg1 > 0 or arg2 is not None else '',
      ', %d' % arg2 if arg2 else '')

class ExpectSequenceBase(ExpectBase):
  """Base class for all sequence-based expectations."""

  def __init__(self, *args):
    super(ExpectSequenceBase, self).__init__()
    self._expected_list = [default_expectation(expected) for expected in args]

  @property
  def fulfilled(self):
    return all(e.fulfilled for e in self._expected_list)

  @property
  def saturated(self):
    return all(e.saturated for e in self._expected_list)

  def apply_transform(self, fn):
    for expected in self._expected_list:
      expected.apply_transform(fn)

  def __repr__(self):
    unfulfilled = [repr(a) for a in self._expected_list if not a.fulfilled]
    if len(unfulfilled) == 1:
      return unfulfilled[0]
    else:
      return '%s(%s)' % (type(self).__name__, ', '.join(unfulfilled))

class InOrder(ExpectSequenceBase):
  """Sequence of expectations that must match in right order."""

  def consume(self, string):
    to_consume = None
    for i, expected in enumerate(self._expected_list):
      matches = expected.test_consume(string)
      if matches:
        to_consume = (i, expected)
        if expected.greedy or not expected.fulfilled:
          break

      elif expected.fulfilled:
        continue

      else:
        break

    if to_consume is not None:
      i, expected = to_consume
      consumed = expected.consume(string)
      assert(consumed)
      # We got a match somewhere down the sequence. Discard any preceding
      # fulfilled expectations.
      self._expected_list = self._expected_list[i:]
      if expected.saturated:
        self._expected_list.remove(expected)
      return consumed

    return False

  def test_consume(self, string):
    for expected in self._expected_list:
      if expected.test_consume(string):
        return True
      elif not expected.fulfilled:
        return False
    return False

  def produce(self):
    for i, expected in enumerate(self._expected_list):
      result = expected.produce()
      if result:
        # We got a match somewhere down the sequence. Discard any preceding
        # fulfilled expectations.
        self._expected_list = self._expected_list[i:]
        if expected.saturated:
          self._expected_list.remove(expected)
        return result
      elif not expected.fulfilled:
        return None
    return None

class AnyOrder(ExpectSequenceBase):
  """Sequence of expectation that can match in any order."""

  def consume(self, string):
    to_consume = None
    for expected in self._expected_list:
      if expected.test_consume(string):
        to_consume = expected
        if expected.greedy or not expected.fulfilled:
          break

    if to_consume is not None:
      consumed = to_consume.consume(string)
      assert(consumed)
      if to_consume.saturated:
        self._expected_list.remove(to_consume)
      return True

    return False

  def test_consume(self, string):
    return any(expected.test_consume(string)
               for expected in self._expected_list)

  def produce(self):
    for expected in self._expected_list:
      result = expected.produce()
      if result:
        if expected.saturated:
          self._expected_list.remove(expected)
        return result
    return None

def Somewhere(expectation):
  """Match an expectation anywhere in a document."""
  return InOrder(Anything().repeatedly(),
                 expectation,
                 Anything().repeatedly())

class Reply(ExpectBase):
  """Expects a read to the input pipe and replies with a specific string."""

  def __init__(self, reply_string):
    super(Reply, self).__init__()
    self._reply_string = reply_string

  def produce(self):
    self._fulfilled = self._saturated = True
    return self._reply_string

  def _consume(self, line):
    raise AssertionError('Expecting user input but got output line: %s' % line)

  def __repr__(self):
    return '%s(%s)' % (type(self).__name__, self._reply_string)

class ExpectedInputOutput(object):
  """File object for overriding stdin/out, mocking inputs & checking outputs."""

  def __init__(self):
    self.set_transform_fn(None)
    self.set_expected_io(None)
    self._original_stdin = sys.stdin
    self._original_stdout = sys.stdout

  def set_transform_fn(self, transform_fn):
    """Callback to transform all expectations passed in set_expected_io.

    Useful for 'patching' all expectations with the same transformation. A
    common way to use this is to fix path delimiters to match the OS format in
    expectations containing paths.

    Args:
      transform_fn: callback function to call to transform the expectation.
          Callback takes a string as input and returns a transforms version of
          that string.
    """
    self._transform_fn = transform_fn

  def set_expected_io(self, expected_io):
    """Set an expectation for the next sequence of IOs.

    Args:
      expected_io: instance of an ExpectBase subclass, the expectation to apply
          all input and outputs against.
    """
    self._expected_io = default_expectation(expected_io)

    if self._expected_io and self._transform_fn:
      self._expected_io.apply_transform(self._transform_fn)

    self._cmd_output = StringIO.StringIO()

  def write(self, string):
    """File object 'write' method, matched against the next expected output.

    Args:
      string: str, string being written to stdout.
    """
    self._original_stdout.write(string)
    self._cmd_output.write(string)

  def flush(self):
    """File object 'flush' method."""
    self._original_stdout.flush()

  def readline(self):
    """File object 'readline' method, replied using the next expected input.

    Returns:
      str, the mock string faking a read from stdin.

    Raises:
      AssertionError: raised when IOs do not match expectations.
    """
    if self._expected_io is None:
      raise AssertionError('Trying to readline but no expected IO was set.')

    self._match_pending_outputs()
    if self._expected_io.saturated:
      raise AssertionError('No more user input prompt expected')
    reply = self._expected_io.produce()
    if not reply:
      raise AssertionError('Unexpected user input prompt request. Expected:\n'
                           '%s' % repr(self._expected_io))
    reply += '\n'
    self._original_stdout.write(reply)
    return reply

  def assert_expectations_fulfilled(self):
    """Asserts that all expectation are fulfilled.

    Resets this object, ready to restart with a new set_expected_io.

    Raises:
      AssertionError: raised when IOs do not match expectations.
    """
    self._match_pending_outputs()
    if self._expected_io:
      if not self._expected_io.fulfilled:
        raise AssertionError('Pending IO expectation never fulfulled:\n%s' %
                             repr(self._expected_io))

    self.set_expected_io(None)

  def _match_pending_outputs(self):
    """Match any pending IO against the expectations.

    Raises:
      AssertionError: raised when IOs do not match expectations.
    """
    output_lines = self._cmd_output.getvalue().splitlines(True)
    self._cmd_output = StringIO.StringIO()

    if self._expected_io:
      for line in output_lines:
        if self._expected_io.saturated:
          raise AssertionError('No more output expected, but got: \'%s\'' %
                               line)
        if not self._expected_io.consume(line):
          raise AssertionError('Unexpected output:\n'
                               '%s' % '\n'.join(difflib.ndiff(
                                 repr(self._expected_io).splitlines(True),
                                 repr(line).splitlines(True))))
<|endoftext|>


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

import glob
import os
from os.path import abspath, dirname, expanduser, join
import sys
import tempfile
from copy import copy
from reportlab.platypus.flowables import Image, Flowable
from reportlab.lib.units import *

import six

if six.PY3:
    from urllib.request import urlretrieve
else:
    from urllib import urlretrieve

from .opt_imports import PILImage, pdfinfo
from .log import log, nodeid

try:
    from .svgimage import SVGImage
except ModuleNotFoundError:
    # svglib may optionally not be installed, which causes this error
    pass

# This assignment could be overridden by an extension module
VectorPdf = None

# find base path
if hasattr(sys, 'frozen'):
    PATH = abspath(dirname(sys.executable))
else:
    PATH = abspath(dirname(__file__))

missing = os.path.join(PATH, 'images', 'image-missing.jpg')

def defaultimage(filename, width=None, height=None, kind='direct',
                                        mask="auto", lazy=1, srcinfo=None):
    ''' We have multiple image backends, including the stock reportlab one.
        This wrapper around the reportlab one allows us to pass the client
        RstToPdf object and the uri into all our backends, which they can
        use or not as necessary.
    '''
    return Image(filename, width, height, kind, mask, lazy)

class MyImage (Flowable):
    """A Image subclass that can:

    1. Take a 'percentage_of_container' kind,
       which resizes it on wrap() to use... well, a percentage of the
       container's width.

    2. Take vector formats and instantiates the right "backend" flowable

    """

    warned = False

    @classmethod
    def support_warning(cls):
        if cls.warned or PILImage:
            return
        cls.warned = True
        log.warning("Support for images other than JPG,"
            " is now limited. Please install Pillow.")

    @staticmethod
    def split_uri(uri):
        ''' A really minimalistic split -- doesn't cope with http:, etc.
            HOWEVER, it tries to do so in a fashion that allows a clueless
            user to have '#' inside his filename without screwing anything
            up.
        '''
        basename, extra = os.path.splitext(uri)
        extra = extra.split('#', 1) + ['']
        fname = basename + extra[0]
        extension = extra[0][1:].lower()
        options = extra[1]
        return fname, extension, options

    def __init__(self, filename, width=None, height=None,
                 kind='direct', mask="auto", lazy=1, client=None, target=None):
        # Client is mandatory.  Perhaps move it farther up if we refactor
        assert client is not None
        self.__kind=kind

        if filename.split("://")[0].lower() in ('http','ftp','https'):
            try:
                filename2, _ = urlretrieve(filename)
                if filename != filename2:
                    client.to_unlink.append(filename2)
                    filename = filename2
            except IOError:
                filename = missing
        self.filename, self._backend=self.get_backend(filename, client)
        srcinfo = client, self.filename

        if kind == 'percentage_of_container':
            self.image=self._backend(self.filename, width, height,
                'direct', mask, lazy, srcinfo)
            self.image.drawWidth=width
            self.image.drawHeight=height
            self.__width=width
            self.__height=height
        else:
            self.image=self._backend(self.filename, width, height,
                kind, mask, lazy, srcinfo)
        self.__ratio=float(self.image.imageWidth)/self.image.imageHeight
        self.__wrappedonce=False
        self.target = target

    @classmethod
    def raster(self, filename, client):
        """Takes a filename and converts it to a raster image
        reportlab can process"""

        if not os.path.exists(filename):
            log.error("Missing image file: %s",filename)
            return missing

        try:
            # First try to rasterize using the suggested backend
            backend = self.get_backend(filename, client)[1]
            return backend.raster(filename, client)
        except:
            pass

        # Last resort: try everything

        if PILImage:
            ext='.png'
        else:
            ext='.jpg'

        extension = os.path.splitext(filename)[-1][1:].lower()

        if PILImage: # See if pil can process it
            try:
                PILImage.open(filename)
                return filename
            except:
                # Can't read it
                pass

        # PIL can't, so we can't
        self.support_warning()
        log.error("Couldn't load image [%s]"%filename)
        return missing

    @classmethod
    def get_backend(self, uri, client):
        '''Given the filename of an image, returns (fname, backend)
        where fname is the filename to be used (could be the same as
        filename, or something different if the image had to be converted
        or is missing), and backend is an Image class that can handle
        fname.

        If uri ensd with '.*' then the returned filename will be the best
        quality supported at the moment.

        That means:  PDF > SVG > anything else

        '''

        backend = defaultimage

        # Extract all the information from the URI
        filename, extension, options = self.split_uri(uri)

        if '*' in filename:
            preferred=['gif','jpg','png', 'svg', 'pdf']

            # Find out what images are available
            available = glob.glob(filename)
            cfn=available[0]
            cv=-10
            for fn in available:
                ext=fn.split('.')[-1]
                if ext in preferred:
                    v=preferred.index(ext)
                else:
                    v=-1
                if v > cv:
                    cv=v
                    cfn=fn
            # cfn should have our favourite type of
            # those available
            filename = cfn
            extension = cfn.split('.')[-1]
            uri = filename

        # If the image doesn't exist, we use a 'missing' image
        if not os.path.exists(filename):
            log.error("Missing image file: %s",filename)
            filename = missing

        if extension in ['svg','svgz']:
            log.info('Backend for %s is SVGIMage'%filename)
            backend=SVGImage

        elif extension in ['pdf']:
            if VectorPdf is not None and filename is not missing:
                backend = VectorPdf
                filename = uri
            else:
                log.warning("Minimal PDF image support "\
                    "requires the vectorpdf extension [%s]", filename)
                filename = missing
        elif extension != 'jpg' and not PILImage:
            # No way to make this work
            log.error('To use a %s image you need Pillow installed [%s]',extension,filename)
            filename=missing
        return filename, backend

    @classmethod
    def size_for_node(self, node, client):
        '''Given a docutils image node, returns the size the image should have
        in the PDF document, and what 'kind' of size that is.
        That involves lots of guesswork'''

        uri = str(node.get("uri"))
        if uri.split("://")[0].lower() not in ('http','ftp','https'):
            uri = os.path.join(client.basedir,uri)
        else:
            uri, _ = urlretrieve(uri)
            client.to_unlink.append(uri)

        srcinfo = client, uri
        # Extract all the information from the URI
        imgname, extension, options = self.split_uri(uri)

        if not os.path.isfile(imgname):
            imgname = missing

        scale = float(node.get('scale', 100))/100
        size_known = False

        # Figuring out the size to display of an image is ... annoying.
        # If the user provides a size with a unit, it's simple, adjustUnits
        # will return it in points and we're done.

        # However, often the unit wil be "%" (specially if it's meant for
        # HTML originally. In which case, we will use a percentage of
        # the containing frame.

        # Find the image size in pixels:
        kind = 'direct'
        xdpi, ydpi = client.styles.def_dpi, client.styles.def_dpi
        extension = imgname.split('.')[-1].lower()
        if extension in ['svg','svgz']:
            iw, ih = SVGImage(imgname, srcinfo=srcinfo).wrap(0, 0)
            # These are in pt, so convert to px
            iw = iw * xdpi / 72
            ih = ih * ydpi / 72

        elif extension == 'pdf':
            if VectorPdf is not None:
                xobj = VectorPdf.load_xobj(srcinfo)
                iw, ih = xobj.w, xobj.h
            else:
                reader = pdfinfo.PdfFileReader(open(imgname, 'rb'))
                box = [float(x) for x in reader.getPage(0)['/MediaBox']]
                iw, ih = x2 - x1, y2 - y1
            # These are in pt, so convert to px
            iw = iw * xdpi / 72.0
            ih = ih * ydpi / 72.0
            size_known = True  # Assume size from original PDF is OK

        else:
            keeptrying = True
            if PILImage:
                try:
                    img = PILImage.open(imgname)
                    img.load()
                    iw, ih = img.size
                    xdpi, ydpi = img.info.get('dpi', (xdpi, ydpi))
                    keeptrying = False
                except IOError: # PIL throws this when it's a broken/unknown image
                    pass
            if keeptrying:
                if extension not in ['jpg', 'jpeg']:
                    log.error("The image (%s, %s) is broken or in an unknown format"
                                , imgname, nodeid(node))
                    raise ValueError
                else:
                    # Can be handled by reportlab
                    log.warning("Can't figure out size of the image (%s, %s). Install PIL for better results."
                                , imgname, nodeid(node))
                    iw = 1000
                    ih = 1000

        # Try to get the print resolution from the image itself via PIL.
        # If it fails, assume a DPI of 300, which is pretty much made up,
        # and then a 100% size would be iw*inch/300, so we pass
        # that as the second parameter to adjustUnits
        #
        # Some say the default DPI should be 72. That would mean
        # the largest printable image in A4 paper would be something
        # like 480x640. That would be awful.
        #

        w = node.get('width')
        h = node.get('height')
        if h is None and w is None: # Nothing specified
            # Guess from iw, ih
            log.debug("Using image %s without specifying size."
                "Calculating based on image size at %ddpi [%s]",
                imgname, xdpi, nodeid(node))
            w = iw*inch/xdpi
            h = ih*inch/ydpi
        elif w is not None:
            # Node specifies only w
            # In this particular case, we want the default unit
            # to be pixels so we work like rst2html
            if w[-1] == '%':
                kind = 'percentage_of_container'
                w=int(w[:-1])
            else:
                # This uses default DPI setting because we
                # are not using the image's "natural size"
                # this is what LaTeX does, according to the
                # docutils mailing list discussion
                w = client.styles.adjustUnits(w, client.styles.tw,
                                            default_unit='px')

            if h is None:
                # h is set from w with right aspect ratio
                h = w*ih/iw
            else:
                h = client.styles.adjustUnits(h, ih*inch/ydpi, default_unit='px')
        elif h is not None and w is None:
            if h[-1] != '%':
                h = client.styles.adjustUnits(h, ih*inch/ydpi, default_unit='px')

                # w is set from h with right aspect ratio
                w = h*iw/ih
            else:
                log.error('Setting height as a percentage does **not** work. '\
                          'ignoring height parameter [%s]', nodeid(node))
                # Set both from image data
                w = iw*inch/xdpi
                h = ih*inch/ydpi

        # Apply scale factor
        w = w*scale
        h = h*scale

        # And now we have this probably completely bogus size!
        log.info("Image %s size calculated:  %fcm by %fcm [%s]",
            imgname, w/cm, h/cm, nodeid(node))

        return w, h, kind

    def _restrictSize(self,aW,aH):
        return self.image._restrictSize(aW, aH)

    def _unRestrictSize(self,aW,aH):
        return self.image._unRestrictSize(aW, aH)

    def __deepcopy__(self, *whatever):
        # ImageCore class is not deep copyable.  Stop the copy at this
        # class.  If you remove this, re-test for issue #126.
        return copy(self)

    def wrap(self, availWidth, availHeight):
        if self.__kind=='percentage_of_container':
            w, h= self.__width, self.__height
            if not w:
                log.warning('Scaling image as % of container with w unset.'
                'This should not happen, setting to 100')
                w = 100
            scale=w/100.
            w = availWidth*scale
            h = w/self.__ratio
            self.image.drawWidth, self.image.drawHeight = w, h
            return w, h
        else:
            if self.image.drawHeight > availHeight:
                if not getattr(self, '_atTop', True):
                    return self.image.wrap(availWidth, availHeight)
                else:
                    # It's the first thing in the frame, probably
                    # Wrapping it will not make it work, so we
                    # adjust by height
                    # FIXME get rst file info (line number)
                    # here for better error message
                    log.warning('image %s is too tall for the '\
                                'frame, rescaling'%\
                                self.filename)
                    self.image.drawHeight = availHeight
                    self.image.drawWidth = availHeight*self.__ratio
            elif self.image.drawWidth > availWidth:
                log.warning('image %s is too wide for the frame, rescaling'%\
                            self.filename)
                self.image.drawWidth = availWidth
                self.image.drawHeight = availWidth / self.__ratio
            return self.image.wrap(availWidth, availHeight)

    def drawOn(self, canv, x, y, _sW=0):
        if self.target:
            offset = 0
            if self.image.hAlign == 'CENTER':
                offset = _sW / 2.
            elif self.image.hAlign == 'RIGHT':
                offset = _sW
            canv.linkURL(self.target,
                    (
                    x + offset, y,
                    x + offset + self.image.drawWidth,
                    y + self.image.drawHeight),
                    relative = True,
                    #thickness = 3,
                    )
        return self.image.drawOn(canv, x, y, _sW)
<commit_msg>Make it still work with Python 2<commit_after># -*- coding: utf-8 -*-

import glob
import os
from os.path import abspath, dirname, expanduser, join
import sys
import tempfile
from copy import copy
from reportlab.platypus.flowables import Image, Flowable
from reportlab.lib.units import *

import six

if six.PY3:
    from urllib.request import urlretrieve
else:
    from urllib import urlretrieve

from .opt_imports import PILImage, pdfinfo
from .log import log, nodeid

try:
    from .svgimage import SVGImage
except ImportError:
    # svglib may optionally not be installed, which causes this error
    pass

# This assignment could be overridden by an extension module
VectorPdf = None

# find base path
if hasattr(sys, 'frozen'):
    PATH = abspath(dirname(sys.executable))
else:
    PATH = abspath(dirname(__file__))

missing = os.path.join(PATH, 'images', 'image-missing.jpg')

def defaultimage(filename, width=None, height=None, kind='direct',
                                        mask="auto", lazy=1, srcinfo=None):
    ''' We have multiple image backends, including the stock reportlab one.
        This wrapper around the reportlab one allows us to pass the client
        RstToPdf object and the uri into all our backends, which they can
        use or not as necessary.
    '''
    return Image(filename, width, height, kind, mask, lazy)

class MyImage (Flowable):
    """A Image subclass that can:

    1. Take a 'percentage_of_container' kind,
       which resizes it on wrap() to use... well, a percentage of the
       container's width.

    2. Take vector formats and instantiates the right "backend" flowable

    """

    warned = False

    @classmethod
    def support_warning(cls):
        if cls.warned or PILImage:
            return
        cls.warned = True
        log.warning("Support for images other than JPG,"
            " is now limited. Please install Pillow.")

    @staticmethod
    def split_uri(uri):
        ''' A really minimalistic split -- doesn't cope with http:, etc.
            HOWEVER, it tries to do so in a fashion that allows a clueless
            user to have '#' inside his filename without screwing anything
            up.
        '''
        basename, extra = os.path.splitext(uri)
        extra = extra.split('#', 1) + ['']
        fname = basename + extra[0]
        extension = extra[0][1:].lower()
        options = extra[1]
        return fname, extension, options

    def __init__(self, filename, width=None, height=None,
                 kind='direct', mask="auto", lazy=1, client=None, target=None):
        # Client is mandatory.  Perhaps move it farther up if we refactor
        assert client is not None
        self.__kind=kind

        if filename.split("://")[0].lower() in ('http','ftp','https'):
            try:
                filename2, _ = urlretrieve(filename)
                if filename != filename2:
                    client.to_unlink.append(filename2)
                    filename = filename2
            except IOError:
                filename = missing
        self.filename, self._backend=self.get_backend(filename, client)
        srcinfo = client, self.filename

        if kind == 'percentage_of_container':
            self.image=self._backend(self.filename, width, height,
                'direct', mask, lazy, srcinfo)
            self.image.drawWidth=width
            self.image.drawHeight=height
            self.__width=width
            self.__height=height
        else:
            self.image=self._backend(self.filename, width, height,
                kind, mask, lazy, srcinfo)
        self.__ratio=float(self.image.imageWidth)/self.image.imageHeight
        self.__wrappedonce=False
        self.target = target

    @classmethod
    def raster(self, filename, client):
        """Takes a filename and converts it to a raster image
        reportlab can process"""

        if not os.path.exists(filename):
            log.error("Missing image file: %s",filename)
            return missing

        try:
            # First try to rasterize using the suggested backend
            backend = self.get_backend(filename, client)[1]
            return backend.raster(filename, client)
        except:
            pass

        # Last resort: try everything

        if PILImage:
            ext='.png'
        else:
            ext='.jpg'

        extension = os.path.splitext(filename)[-1][1:].lower()

        if PILImage: # See if pil can process it
            try:
                PILImage.open(filename)
                return filename
            except:
                # Can't read it
                pass

        # PIL can't, so we can't
        self.support_warning()
        log.error("Couldn't load image [%s]"%filename)
        return missing

    @classmethod
    def get_backend(self, uri, client):
        '''Given the filename of an image, returns (fname, backend)
        where fname is the filename to be used (could be the same as
        filename, or something different if the image had to be converted
        or is missing), and backend is an Image class that can handle
        fname.

        If uri ensd with '.*' then the returned filename will be the best
        quality supported at the moment.

        That means:  PDF > SVG > anything else

        '''

        backend = defaultimage

        # Extract all the information from the URI
        filename, extension, options = self.split_uri(uri)

        if '*' in filename:
            preferred=['gif','jpg','png', 'svg', 'pdf']

            # Find out what images are available
            available = glob.glob(filename)
            cfn=available[0]
            cv=-10
            for fn in available:
                ext=fn.split('.')[-1]
                if ext in preferred:
                    v=preferred.index(ext)
                else:
                    v=-1
                if v > cv:
                    cv=v
                    cfn=fn
            # cfn should have our favourite type of
            # those available
            filename = cfn
            extension = cfn.split('.')[-1]
            uri = filename

        # If the image doesn't exist, we use a 'missing' image
        if not os.path.exists(filename):
            log.error("Missing image file: %s",filename)
            filename = missing

        if extension in ['svg','svgz']:
            log.info('Backend for %s is SVGIMage'%filename)
            backend=SVGImage

        elif extension in ['pdf']:
            if VectorPdf is not None and filename is not missing:
                backend = VectorPdf
                filename = uri
            else:
                log.warning("Minimal PDF image support "\
                    "requires the vectorpdf extension [%s]", filename)
                filename = missing
        elif extension != 'jpg' and not PILImage:
            # No way to make this work
            log.error('To use a %s image you need Pillow installed [%s]',extension,filename)
            filename=missing
        return filename, backend

    @classmethod
    def size_for_node(self, node, client):
        '''Given a docutils image node, returns the size the image should have
        in the PDF document, and what 'kind' of size that is.
        That involves lots of guesswork'''

        uri = str(node.get("uri"))
        if uri.split("://")[0].lower() not in ('http','ftp','https'):
            uri = os.path.join(client.basedir,uri)
        else:
            uri, _ = urlretrieve(uri)
            client.to_unlink.append(uri)

        srcinfo = client, uri
        # Extract all the information from the URI
        imgname, extension, options = self.split_uri(uri)

        if not os.path.isfile(imgname):
            imgname = missing

        scale = float(node.get('scale', 100))/100
        size_known = False

        # Figuring out the size to display of an image is ... annoying.
        # If the user provides a size with a unit, it's simple, adjustUnits
        # will return it in points and we're done.

        # However, often the unit wil be "%" (specially if it's meant for
        # HTML originally. In which case, we will use a percentage of
        # the containing frame.

        # Find the image size in pixels:
        kind = 'direct'
        xdpi, ydpi = client.styles.def_dpi, client.styles.def_dpi
        extension = imgname.split('.')[-1].lower()
        if extension in ['svg','svgz']:
            iw, ih = SVGImage(imgname, srcinfo=srcinfo).wrap(0, 0)
            # These are in pt, so convert to px
            iw = iw * xdpi / 72
            ih = ih * ydpi / 72

        elif extension == 'pdf':
            if VectorPdf is not None:
                xobj = VectorPdf.load_xobj(srcinfo)
                iw, ih = xobj.w, xobj.h
            else:
                reader = pdfinfo.PdfFileReader(open(imgname, 'rb'))
                box = [float(x) for x in reader.getPage(0)['/MediaBox']]
                iw, ih = x2 - x1, y2 - y1
            # These are in pt, so convert to px
            iw = iw * xdpi / 72.0
            ih = ih * ydpi / 72.0
            size_known = True  # Assume size from original PDF is OK

        else:
            keeptrying = True
            if PILImage:
                try:
                    img = PILImage.open(imgname)
                    img.load()
                    iw, ih = img.size
                    xdpi, ydpi = img.info.get('dpi', (xdpi, ydpi))
                    keeptrying = False
                except IOError: # PIL throws this when it's a broken/unknown image
                    pass
            if keeptrying:
                if extension not in ['jpg', 'jpeg']:
                    log.error("The image (%s, %s) is broken or in an unknown format"
                                , imgname, nodeid(node))
                    raise ValueError
                else:
                    # Can be handled by reportlab
                    log.warning("Can't figure out size of the image (%s, %s). Install PIL for better results."
                                , imgname, nodeid(node))
                    iw = 1000
                    ih = 1000

        # Try to get the print resolution from the image itself via PIL.
        # If it fails, assume a DPI of 300, which is pretty much made up,
        # and then a 100% size would be iw*inch/300, so we pass
        # that as the second parameter to adjustUnits
        #
        # Some say the default DPI should be 72. That would mean
        # the largest printable image in A4 paper would be something
        # like 480x640. That would be awful.
        #

        w = node.get('width')
        h = node.get('height')
        if h is None and w is None: # Nothing specified
            # Guess from iw, ih
            log.debug("Using image %s without specifying size."
                "Calculating based on image size at %ddpi [%s]",
                imgname, xdpi, nodeid(node))
            w = iw*inch/xdpi
            h = ih*inch/ydpi
        elif w is not None:
            # Node specifies only w
            # In this particular case, we want the default unit
            # to be pixels so we work like rst2html
            if w[-1] == '%':
                kind = 'percentage_of_container'
                w=int(w[:-1])
            else:
                # This uses default DPI setting because we
                # are not using the image's "natural size"
                # this is what LaTeX does, according to the
                # docutils mailing list discussion
                w = client.styles.adjustUnits(w, client.styles.tw,
                                            default_unit='px')

            if h is None:
                # h is set from w with right aspect ratio
                h = w*ih/iw
            else:
                h = client.styles.adjustUnits(h, ih*inch/ydpi, default_unit='px')
        elif h is not None and w is None:
            if h[-1] != '%':
                h = client.styles.adjustUnits(h, ih*inch/ydpi, default_unit='px')

                # w is set from h with right aspect ratio
                w = h*iw/ih
            else:
                log.error('Setting height as a percentage does **not** work. '\
                          'ignoring height parameter [%s]', nodeid(node))
                # Set both from image data
                w = iw*inch/xdpi
                h = ih*inch/ydpi

        # Apply scale factor
        w = w*scale
        h = h*scale

        # And now we have this probably completely bogus size!
        log.info("Image %s size calculated:  %fcm by %fcm [%s]",
            imgname, w/cm, h/cm, nodeid(node))

        return w, h, kind

    def _restrictSize(self,aW,aH):
        return self.image._restrictSize(aW, aH)

    def _unRestrictSize(self,aW,aH):
        return self.image._unRestrictSize(aW, aH)

    def __deepcopy__(self, *whatever):
        # ImageCore class is not deep copyable.  Stop the copy at this
        # class.  If you remove this, re-test for issue #126.
        return copy(self)

    def wrap(self, availWidth, availHeight):
        if self.__kind=='percentage_of_container':
            w, h= self.__width, self.__height
            if not w:
                log.warning('Scaling image as % of container with w unset.'
                'This should not happen, setting to 100')
                w = 100
            scale=w/100.
            w = availWidth*scale
            h = w/self.__ratio
            self.image.drawWidth, self.image.drawHeight = w, h
            return w, h
        else:
            if self.image.drawHeight > availHeight:
                if not getattr(self, '_atTop', True):
                    return self.image.wrap(availWidth, availHeight)
                else:
                    # It's the first thing in the frame, probably
                    # Wrapping it will not make it work, so we
                    # adjust by height
                    # FIXME get rst file info (line number)
                    # here for better error message
                    log.warning('image %s is too tall for the '\
                                'frame, rescaling'%\
                                self.filename)
                    self.image.drawHeight = availHeight
                    self.image.drawWidth = availHeight*self.__ratio
            elif self.image.drawWidth > availWidth:
                log.warning('image %s is too wide for the frame, rescaling'%\
                            self.filename)
                self.image.drawWidth = availWidth
                self.image.drawHeight = availWidth / self.__ratio
            return self.image.wrap(availWidth, availHeight)

    def drawOn(self, canv, x, y, _sW=0):
        if self.target:
            offset = 0
            if self.image.hAlign == 'CENTER':
                offset = _sW / 2.
            elif self.image.hAlign == 'RIGHT':
                offset = _sW
            canv.linkURL(self.target,
                    (
                    x + offset, y,
                    x + offset + self.image.drawWidth,
                    y + self.image.drawHeight),
                    relative = True,
                    #thickness = 3,
                    )
        return self.image.drawOn(canv, x, y, _sW)
<|endoftext|>


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

from copy import copy
import glob
import os
from os.path import abspath, dirname
import sys
from urllib.request import urlretrieve

from PIL import Image as PILImage
from reportlab.platypus.flowables import Image, Flowable
from reportlab.lib.units import cm, inch

from .log import log, nodeid

try:
    # FIXME: We should handle this being missing or make it a requirement
    from .svgimage import SVGImage
except ImportError:
    # svglib may optionally not be installed, which causes this error
    pass

# This assignment could be overridden by an extension module
VectorPdf = None

# find base path
if hasattr(sys, 'frozen'):
    PATH = abspath(dirname(sys.executable))
else:
    PATH = abspath(dirname(__file__))

missing = os.path.join(PATH, 'images', 'image-missing.jpg')

def defaultimage(
    filename, width=None, height=None, kind='direct', mask="auto", lazy=1, srcinfo=None,
):
    ''' We have multiple image backends, including the stock reportlab one.
        This wrapper around the reportlab one allows us to pass the client
        RstToPdf object and the uri into all our backends, which they can
        use or not as necessary.
    '''
    return Image(filename, width, height, kind, mask, lazy)

class MyImage(Flowable):
    """A Image subclass that can:

    1. Take a 'percentage_of_container' kind,
       which resizes it on wrap() to use... well, a percentage of the
       container's width.

    2. Take vector formats and instantiates the right "backend" flowable

    """

    warned = False

    @classmethod
    def support_warning(cls):
        if cls.warned or PILImage:
            return
        cls.warned = True
        log.warning(
            "Support for images other than JPG,"
            " is now limited. Please install Pillow."
        )

    @staticmethod
    def split_uri(uri):
        ''' A really minimalistic split -- doesn't cope with http:, etc.
            HOWEVER, it tries to do so in a fashion that allows a clueless
            user to have '#' inside his filename without screwing anything
            up.
        '''
        basename, extra = os.path.splitext(uri)
        extra = extra.split('#', 1) + ['']
        fname = basename + extra[0]
        extension = extra[0][1:].lower()
        options = extra[1]
        return fname, extension, options

    def __init__(
        self,
        filename,
        width=None,
        height=None,
        kind='direct',
        mask="auto",
        lazy=1,
        client=None,
        target=None,
    ):
        # Client is mandatory.  Perhaps move it farther up if we refactor
        assert client is not None
        self.__kind = kind

        if filename.split("://")[0].lower() in ('http', 'ftp', 'https'):
            try:
                filename2, _ = urlretrieve(filename)
                if filename != filename2:
                    client.to_unlink.append(filename2)
                    filename = filename2
            except IOError:
                filename = missing
        self.filename, self._backend = self.get_backend(filename, client)
        srcinfo = client, self.filename

        if kind == 'percentage_of_container':
            self.image = self._backend(
                self.filename, width, height, 'direct', mask, lazy, srcinfo
            )
            self.image.drawWidth = width
            self.image.drawHeight = height
            self.__width = width
            self.__height = height
        else:
            self.image = self._backend(
                self.filename, width, height, kind, mask, lazy, srcinfo
            )
        self.__ratio = float(self.image.imageWidth) / self.image.imageHeight
        self.__wrappedonce = False
        self.target = target

    @classmethod
    def raster(self, filename, client):
        """Takes a filename and converts it to a raster image
        reportlab can process"""

        if not os.path.exists(filename):
            log.error("Missing image file: %s", filename)
            return missing

        try:
            # First try to rasterize using the suggested backend
            backend = self.get_backend(filename, client)[1]
            return backend.raster(filename, client)
        except Exception:
            pass

        # Last resort: try everything

        if PILImage:  # See if pil can process it
            try:
                PILImage.open(filename)
                return filename
            except Exception:
                # Can't read it
                pass

        # PIL can't, so we can't
        self.support_warning()
        log.error("Couldn't load image [%s]" % filename)
        return missing

    @classmethod
    def get_backend(self, uri, client):
        '''Given the filename of an image, returns (fname, backend)
        where fname is the filename to be used (could be the same as
        filename, or something different if the image had to be converted
        or is missing), and backend is an Image class that can handle
        fname.

        If uri ensd with '.*' then the returned filename will be the best
        quality supported at the moment.

        That means:  PDF > SVG > anything else
        '''

        backend = defaultimage

        # Extract all the information from the URI
        filename, extension, options = self.split_uri(uri)

        if '*' in filename:
            preferred = ['gif', 'jpg', 'png', 'svg', 'pdf']

            # Find out what images are available
            available = glob.glob(filename)
            cfn = available[0]
            cv = -10
            for fn in available:
                ext = fn.split('.')[-1]
                if ext in preferred:
                    v = preferred.index(ext)
                else:
                    v = -1
                if v > cv:
                    cv = v
                    cfn = fn
            # cfn should have our favourite type of
            # those available
            filename = cfn
            extension = cfn.split('.')[-1]
            uri = filename

        # If the image doesn't exist, we use a 'missing' image
        if not os.path.exists(filename):
            log.error("Missing image file: %s", filename)
            filename = missing

        if extension in ['svg', 'svgz']:
            log.info('Backend for %s is SVGIMage' % filename)
            backend = SVGImage

        elif extension in ['pdf']:
            if VectorPdf is not None and filename is not missing:
                backend = VectorPdf
                filename = uri
            else:
                log.warning(
                    "Minimal PDF image support "
                    "requires the vectorpdf extension [%s]",
                    filename,
                )
                filename = missing
        elif extension != 'jpg' and not PILImage:
            # No way to make this work
            log.error(
                'To use a %s image you need Pillow installed [%s]', extension, filename,
            )
            filename = missing
        return filename, backend

    @classmethod
    def size_for_node(self, node, client):
        '''Given a docutils image node, returns the size the image should have
        in the PDF document, and what 'kind' of size that is.
        That involves lots of guesswork'''

        uri = str(node.get("uri"))
        if uri.split("://")[0].lower() not in ('http', 'ftp', 'https'):
            uri = os.path.join(client.basedir, uri)
        else:
            uri, _ = urlretrieve(uri)
            client.to_unlink.append(uri)

        srcinfo = client, uri
        # Extract all the information from the URI
        imgname, extension, options = self.split_uri(uri)

        if not os.path.isfile(imgname):
            imgname = missing

        scale = float(node.get('scale', 100)) / 100

        # Figuring out the size to display of an image is ... annoying.
        # If the user provides a size with a unit, it's simple, adjustUnits
        # will return it in points and we're done.

        # However, often the unit wil be "%" (specially if it's meant for
        # HTML originally. In which case, we will use a percentage of
        # the containing frame.

        # Find the image size in pixels:
        kind = 'direct'
        xdpi, ydpi = client.styles.def_dpi, client.styles.def_dpi
        extension = imgname.split('.')[-1].lower()
        if extension in ['svg', 'svgz']:
            iw, ih = SVGImage(imgname, srcinfo=srcinfo).wrap(0, 0)
            # These are in pt, so convert to px
            iw = iw * xdpi / 72
            ih = ih * ydpi / 72
        elif extension == 'pdf':
            if VectorPdf is not None:
                xobj = VectorPdf.load_xobj(srcinfo)
                iw, ih = xobj.w, xobj.h
            else:
                raise Exception('Need VectorPDF extension')

            # These are in pt, so convert to px
            iw = iw * xdpi / 72.0
            ih = ih * ydpi / 72.0
        else:
            keeptrying = True
            if PILImage:
                try:
                    img = PILImage.open(imgname)
                    img.load()
                    iw, ih = img.size
                    xdpi, ydpi = img.info.get('dpi', (xdpi, ydpi))
                    keeptrying = False
                except IOError:  # PIL throws this when it's a broken/unknown image
                    pass
            if keeptrying:
                if extension not in ['jpg', 'jpeg']:
                    log.error(
                        "The image (%s, %s) is broken or in an unknown format",
                        imgname,
                        nodeid(node),
                    )
                    raise ValueError
                else:
                    # Can be handled by reportlab
                    log.warning(
                        "Can't figure out size of the image (%s, %s). Install PIL for better results.",
                        imgname,
                        nodeid(node),
                    )
                    iw = 1000
                    ih = 1000

        # Try to get the print resolution from the image itself via PIL.
        # If it fails, assume a DPI of 300, which is pretty much made up,
        # and then a 100% size would be iw*inch/300, so we pass
        # that as the second parameter to adjustUnits
        #
        # Some say the default DPI should be 72. That would mean
        # the largest printable image in A4 paper would be something
        # like 480x640. That would be awful.
        #

        w = node.get('width')
        h = node.get('height')
        if h is None and w is None:  # Nothing specified
            # Guess from iw, ih
            log.debug(
                "Using image %s without specifying size."
                "Calculating based on image size at %ddpi [%s]",
                imgname,
                xdpi,
                nodeid(node),
            )
            w = iw * inch / xdpi
            h = ih * inch / ydpi
        elif w is not None:
            # Node specifies only w
            # In this particular case, we want the default unit
            # to be pixels so we work like rst2html
            if w[-1] == '%':
                kind = 'percentage_of_container'
                w = int(w[:-1])
            else:
                # This uses default DPI setting because we
                # are not using the image's "natural size"
                # this is what LaTeX does, according to the
                # docutils mailing list discussion
                w = client.styles.adjustUnits(w, client.styles.tw, default_unit='px')

            if h is None:
                # h is set from w with right aspect ratio
                h = w * ih / iw
            else:
                h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px')
        elif h is not None and w is None:
            if h[-1] != '%':
                h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px')

                # w is set from h with right aspect ratio
                w = h * iw / ih
            else:
                log.error(
                    'Setting height as a percentage does **not** work. '
                    'ignoring height parameter [%s]',
                    nodeid(node),
                )
                # Set both from image data
                w = iw * inch / xdpi
                h = ih * inch / ydpi

        # Apply scale factor
        w = w * scale
        h = h * scale

        # And now we have this probably completely bogus size!
        log.info(
            "Image %s size calculated:  %fcm by %fcm [%s]",
            imgname,
            w / cm,
            h / cm,
            nodeid(node),
        )

        return w, h, kind

    def _restrictSize(self, aW, aH):
        return self.image._restrictSize(aW, aH)

    def _unRestrictSize(self, aW, aH):
        return self.image._unRestrictSize(aW, aH)

    def __deepcopy__(self, *whatever):
        # ImageCore class is not deep copyable.  Stop the copy at this
        # class.  If you remove this, re-test for issue #126.
        return copy(self)

    def wrap(self, availWidth, availHeight):
        if self.__kind == 'percentage_of_container':
            w, h = self.__width, self.__height
            if not w:
                log.warning(
                    'Scaling image as % of container with w unset.'
                    'This should not happen, setting to 100'
                )
                w = 100
            scale = w / 100.0
            w = availWidth * scale
            h = w / self.__ratio
            self.image.drawWidth, self.image.drawHeight = w, h
            return w, h
        else:
            if self.image.drawHeight > availHeight:
                if not getattr(self, '_atTop', True):
                    return self.image.wrap(availWidth, availHeight)
                else:
                    # It's the first thing in the frame, probably
                    # Wrapping it will not make it work, so we
                    # adjust by height
                    # FIXME get rst file info (line number)
                    # here for better error message
                    log.warning(
                        'image %s is too tall for the '
                        'frame, rescaling' % self.filename
                    )
                    self.image.drawHeight = availHeight
                    self.image.drawWidth = availHeight * self.__ratio
            elif self.image.drawWidth > availWidth:
                log.warning(
                    'image %s is too wide for the frame, rescaling' % self.filename
                )
                self.image.drawWidth = availWidth
                self.image.drawHeight = availWidth / self.__ratio
            return self.image.wrap(availWidth, availHeight)

    def drawOn(self, canv, x, y, _sW=0):
        if self.target:
            offset = 0
            if self.image.hAlign == 'CENTER':
                offset = _sW / 2.0
            elif self.image.hAlign == 'RIGHT':
                offset = _sW
            canv.linkURL(
                self.target,
                (
                    x + offset,
                    y,
                    x + offset + self.image.drawWidth,
                    y + self.image.drawHeight,
                ),
                relative=True,
                # thickness = 3,
            )
        return self.image.drawOn(canv, x, y, _sW)
<commit_msg>image: Handle missing 'svglib'<commit_after># -*- coding: utf-8 -*-

from copy import copy
import glob
import os
from os.path import abspath, dirname
import sys
from urllib.request import urlretrieve

from PIL import Image as PILImage
from reportlab.platypus.flowables import Image, Flowable
from reportlab.lib.units import cm, inch

from .log import log, nodeid

try:
    from .svgimage import SVGImage
except ImportError:
    # svglib may optionally not be installed, which causes this error
    SVGImage = None

# This assignment could be overridden by an extension module
VectorPdf = None

# find base path
if hasattr(sys, 'frozen'):
    PATH = abspath(dirname(sys.executable))
else:
    PATH = abspath(dirname(__file__))

missing = os.path.join(PATH, 'images', 'image-missing.jpg')

def defaultimage(
    filename, width=None, height=None, kind='direct', mask='auto', lazy=1, srcinfo=None,
):
    """Get default image backend.

    We have multiple image backends, including the stock ReportLab one.  This
    wrapper around the ReportLab one allows us to pass the client ``RstToPdf``
    object and the URI into all our backends, which they can use (or not) as
    necessary.
    """
    return Image(filename, width, height, kind, mask, lazy)

class MyImage(Flowable):
    """A Image subclass that can:

    1. Take a ``percentage_of_container`` kind, which resizes it on ``wrap(``
       to use a percentage of the container's width.
    2. Take vector formats and instantiates the right "backend" flowable.
    """

    warned = False

    @classmethod
    def support_warning(cls):
        if cls.warned or PILImage:
            return
        cls.warned = True
        log.warning(
            'Support for images other than JPG is now limited. Please install '
            'Pillow.'
        )

    @staticmethod
    def split_uri(uri):
        """Split provided URI.

        A really minimalistic split -- doesn't cope with http:, etc.  HOWEVER,
        it tries to do so in a fashion that allows a clueless user to have
        ``#`` inside his filename without screwing anything up.
        """
        basename, extra = os.path.splitext(uri)
        extra = extra.split('#', 1) + ['']
        fname = basename + extra[0]
        extension = extra[0][1:].lower()
        options = extra[1]
        return fname, extension, options

    def __init__(
        self,
        filename,
        width=None,
        height=None,
        kind='direct',
        mask='auto',
        lazy=1,
        client=None,
        target=None,
    ):
        # Client is mandatory.  Perhaps move it farther up if we refactor
        assert client is not None
        self.__kind = kind

        if filename.split("://")[0].lower() in ('http', 'ftp', 'https'):
            try:
                filename2, _ = urlretrieve(filename)
                if filename != filename2:
                    client.to_unlink.append(filename2)
                    filename = filename2
            except IOError:
                filename = missing
        self.filename, self._backend = self.get_backend(filename, client)
        srcinfo = client, self.filename

        if kind == 'percentage_of_container':
            self.image = self._backend(
                self.filename, width, height, 'direct', mask, lazy, srcinfo
            )
            self.image.drawWidth = width
            self.image.drawHeight = height
            self.__width = width
            self.__height = height
        else:
            self.image = self._backend(
                self.filename, width, height, kind, mask, lazy, srcinfo
            )
        self.__ratio = float(self.image.imageWidth) / self.image.imageHeight
        self.__wrappedonce = False
        self.target = target

    @classmethod
    def raster(self, filename, client):
        """Convert image to raster image.

        Takes a filename and converts it to a raster image reportlab can
        process.
        """

        if not os.path.exists(filename):
            log.error('Missing image file: %s', filename)
            return missing

        try:
            # First try to rasterize using the suggested backend
            backend = self.get_backend(filename, client)[1]
            return backend.raster(filename, client)
        except Exception:
            pass

        # Last resort: try everything

        if PILImage:  # See if pil can process it
            try:
                PILImage.open(filename)
                return filename
            except Exception:
                # Can't read it
                pass

        # PIL can't, so we can't
        self.support_warning()
        log.error('Could not load image: %s', filename)
        return missing

    @classmethod
    def get_backend(self, uri, client):
        """Get backend for an image.

        Given the filename of an image, returns ``(fname, backend)``, where
        ``fname`` is the filename to be used (could be the same as filename, or
        something different if the image had to be converted or is missing),
        and ``backend`` is an ``Image`` class that can handle ``fname``.

        If ``uri`` ends with '.*' then the returned filename will be the best
        quality supported at the moment.

        That means: PDF > SVG > anything else
        """

        backend = defaultimage

        # Extract all the information from the URI
        filename, extension, options = self.split_uri(uri)

        if '*' in filename:
            preferred = ['gif', 'jpg', 'png', 'svg', 'pdf']

            # Find out what images are available
            available = glob.glob(filename)
            cfn = available[0]
            cv = -10
            for fn in available:
                ext = fn.split('.')[-1]
                if ext in preferred:
                    v = preferred.index(ext)
                else:
                    v = -1
                if v > cv:
                    cv = v
                    cfn = fn

            # cfn should have our favourite type of those available
            filename = cfn
            extension = cfn.split('.')[-1]
            uri = filename

        # If the image doesn't exist, we use a 'missing' image
        if not os.path.exists(filename):
            log.error('Missing image file: %s', filename)
            filename = missing

            return filename, backend

        if extension in ['svg', 'svgz']:
            if SVGImage is not None:
                log.info('Backend for %s is SVGImage', filename)
                backend = SVGImage
            else:
                log.error('SVG image support requires svglib: %s', filename)
                filename = missing
        elif extension in ['pdf']:
            if VectorPdf is not None:
                log.debug('Backend for %s is VectorPdf', filename)
                backend = VectorPdf
                filename = uri
            else:
                log.error(
                    'PDF image support requires the vectorpdf extension: %s', filename,
                )
                filename = missing
        elif extension != 'jpg' and not PILImage:
            # No way to make this work
            log.error(
                '%s image support requires Pillow: %s', extension.upper(), filename,
            )
            filename = missing

        return filename, backend

    @classmethod
    def size_for_node(self, node, client):
        """Get size for image node.

        Given a docutils image node, returns the size the image should have in
        the PDF document, and what "kind" of size that is. That involves lots
        of guesswork.
        """

        uri = str(node.get('uri'))
        if uri.split('://')[0].lower() not in ('http', 'ftp', 'https'):
            uri = os.path.join(client.basedir, uri)
        else:
            uri, _ = urlretrieve(uri)
            client.to_unlink.append(uri)

        srcinfo = client, uri
        # Extract all the information from the URI
        imgname, extension, options = self.split_uri(uri)

        if not os.path.isfile(imgname):
            imgname = missing

        scale = float(node.get('scale', 100)) / 100

        # Figuring out the size to display of an image is ... annoying.
        # If the user provides a size with a unit, it's simple, adjustUnits
        # will return it in points and we're done.

        # However, often the unit wil be "%" (specially if it's meant for
        # HTML originally. In which case, we will use a percentage of
        # the containing frame.

        # Find the image size in pixels:
        kind = 'direct'
        xdpi, ydpi = client.styles.def_dpi, client.styles.def_dpi
        extension = imgname.split('.')[-1].lower()
        if extension in ['svg', 'svgz']:
            if not SVGImage:
                raise RuntimeError(
                    'Documentation uses SVG image but svglib is not installed.'
                )

            iw, ih = SVGImage(imgname, srcinfo=srcinfo).wrap(0, 0)
            # These are in pt, so convert to px
            iw = iw * xdpi / 72
            ih = ih * ydpi / 72
        elif extension == 'pdf':
            if VectorPdf is not None:
                xobj = VectorPdf.load_xobj(srcinfo)
                iw, ih = xobj.w, xobj.h
            else:
                raise Exception(
                    'Documentation uses embedded PDFs which require the VectorPDF '
                    'extension'
                )

            # These are in pt, so convert to px
            iw = iw * xdpi / 72.0
            ih = ih * ydpi / 72.0
        else:
            keeptrying = True

            if PILImage:
                try:
                    img = PILImage.open(imgname)
                    img.load()
                    iw, ih = img.size
                    xdpi, ydpi = img.info.get('dpi', (xdpi, ydpi))
                    keeptrying = False
                except IOError:  # PIL throws this when it's a broken/unknown image
                    pass

            if keeptrying:
                if extension not in ['jpg', 'jpeg']:
                    log.error(
                        "The image (%s, %s) is broken or in an unknown format",
                        imgname,
                        nodeid(node),
                    )
                    raise ValueError
                else:
                    # Can be handled by reportlab
                    log.warning(
                        "Can't figure out size of the image (%s, %s). Install PIL for better results.",
                        imgname,
                        nodeid(node),
                    )
                    iw = 1000
                    ih = 1000

        # Try to get the print resolution from the image itself via PIL.
        # If it fails, assume a DPI of 300, which is pretty much made up,
        # and then a 100% size would be iw*inch/300, so we pass
        # that as the second parameter to adjustUnits
        #
        # Some say the default DPI should be 72. That would mean
        # the largest printable image in A4 paper would be something
        # like 480x640. That would be awful.
        #

        w = node.get('width')
        h = node.get('height')
        if h is None and w is None:  # Nothing specified
            # Guess from iw, ih
            log.debug(
                "Using image %s without specifying size."
                "Calculating based on image size at %ddpi [%s]",
                imgname,
                xdpi,
                nodeid(node),
            )
            w = iw * inch / xdpi
            h = ih * inch / ydpi
        elif w is not None:
            # Node specifies only w
            # In this particular case, we want the default unit
            # to be pixels so we work like rst2html
            if w[-1] == '%':
                kind = 'percentage_of_container'
                w = int(w[:-1])
            else:
                # This uses default DPI setting because we
                # are not using the image's "natural size"
                # this is what LaTeX does, according to the
                # docutils mailing list discussion
                w = client.styles.adjustUnits(w, client.styles.tw, default_unit='px')

            if h is None:
                # h is set from w with right aspect ratio
                h = w * ih / iw
            else:
                h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px')
        elif h is not None and w is None:
            if h[-1] != '%':
                h = client.styles.adjustUnits(h, ih * inch / ydpi, default_unit='px')

                # w is set from h with right aspect ratio
                w = h * iw / ih
            else:
                log.error(
                    'Setting height as a percentage does **not** work. '
                    'ignoring height parameter [%s]',
                    nodeid(node),
                )
                # Set both from image data
                w = iw * inch / xdpi
                h = ih * inch / ydpi

        # Apply scale factor
        w = w * scale
        h = h * scale

        # And now we have this probably completely bogus size!
        log.info(
            'Image %s size calculated: %fcm by %fcm [%s]',
            imgname,
            w / cm,
            h / cm,
            nodeid(node),
        )

        return w, h, kind

    def _restrictSize(self, aW, aH):
        return self.image._restrictSize(aW, aH)

    def _unRestrictSize(self, aW, aH):
        return self.image._unRestrictSize(aW, aH)

    def __deepcopy__(self, *whatever):
        # ImageCore class is not deep copyable.  Stop the copy at this
        # class.  If you remove this, re-test for issue #126.
        return copy(self)

    def wrap(self, availWidth, availHeight):
        if self.__kind == 'percentage_of_container':
            w, h = self.__width, self.__height
            if not w:
                log.warning(
                    'Scaling image as % of container with w unset. '
                    'This should not happen, setting to 100'
                )
                w = 100
            scale = w / 100.0
            w = availWidth * scale
            h = w / self.__ratio
            self.image.drawWidth, self.image.drawHeight = w, h
            return w, h
        else:
            if self.image.drawHeight > availHeight:
                if not getattr(self, '_atTop', True):
                    return self.image.wrap(availWidth, availHeight)
                else:
                    # It's the first thing in the frame, probably
                    # Wrapping it will not make it work, so we
                    # adjust by height
                    # FIXME get rst file info (line number)
                    # here for better error message
                    log.warning(
                        'Image %s is too tall for the frame, rescaling', self.filename
                    )
                    self.image.drawHeight = availHeight
                    self.image.drawWidth = availHeight * self.__ratio
            elif self.image.drawWidth > availWidth:
                log.warning(
                    'Image %s is too wide for the frame, rescaling', self.filename
                )
                self.image.drawWidth = availWidth
                self.image.drawHeight = availWidth / self.__ratio
            return self.image.wrap(availWidth, availHeight)

    def drawOn(self, canv, x, y, _sW=0):
        if self.target:
            offset = 0
            if self.image.hAlign == 'CENTER':
                offset = _sW / 2.0
            elif self.image.hAlign == 'RIGHT':
                offset = _sW
            canv.linkURL(
                self.target,
                (
                    x + offset,
                    y,
                    x + offset + self.image.drawWidth,
                    y + self.image.drawHeight,
                ),
                relative=True,
                # thickness = 3,
            )
        return self.image.drawOn(canv, x, y, _sW)
<|endoftext|>


<commit_before><commit_msg>Delete __init__.py<commit_after><|endoftext|>