<commit_before># Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Create / interact with gcloud logging connections."""

from gcloud import connection as base_connection

class Connection(base_connection.JSONConnection):
    """A connection to Google Cloud Logging via the JSON REST API.

    :type credentials: :class:`oauth2client.client.OAuth2Credentials`
    :param credentials: (Optional) The OAuth2 Credentials to use for this
                        connection.

    :type http: :class:`httplib2.Http` or class that defines ``request()``.
    :param http: (Optional) HTTP object to make requests.

    :type api_base_url: string
    :param api_base_url: The base of the API call URL. Defaults to the value
                         :attr:`Connection.API_BASE_URL`.
    """

    API_BASE_URL = 'https://logging.googleapis.com'
    """The base of the API call URL."""

    API_VERSION = 'v2beta1'
    """The version of the API, used in building the API call's URL."""

    API_URL_TEMPLATE = '{api_base_url}/{api_version}{path}'
    """A template for the URL of a particular API call."""

    SCOPE = ('https://www.googleapis.com/auth/logging.read',
             'https://www.googleapis.com/auth/logging.write',
             'https://www.googleapis.com/auth/logging.admin',
             'https://www.googleapis.com/auth/cloud-platform')
    """The scopes required for authenticating as a Cloud Logging consumer."""

class _LoggingAPI(object):
    """Helper mapping logging-related APIs.

    :type connection: :class:`gcloud.logging.connection.Connection`
    :param connection: the connection used to make API requests.
    """
    def __init__(self, connection):
        self._connection = connection

    def list_entries(self, projects, filter_=None, order_by=None,
                     page_size=None, page_token=None):
        """Return a page of log entry resources.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/list

        :type projects: list of strings
        :param projects: project IDs to include. If not passed,
                            defaults to the project bound to the client.

        :type filter_: str
        :param filter_: a filter expression. See:
                        https://cloud.google.com/logging/docs/view/advanced_filters

        :type order_by: str
        :param order_by: One of :data:`gcloud.logging.ASCENDING` or
                         :data:`gcloud.logging.DESCENDING`.

        :type page_size: int
        :param page_size: maximum number of entries to return, If not passed,
                          defaults to a value set by the API.

        :type page_token: str
        :param page_token: opaque marker for the next "page" of entries. If not
                           passed, the API will return the first page of
                           entries.

        :rtype: tuple, (list, str)
        :returns: list of mappings, plus a "next page token" string:
                  if not None, indicates that more entries can be retrieved
                    with another call (pass that value as ``page_token``).
        """
        params = {'projectIds': projects}

        if filter_ is not None:
            params['filter'] = filter_

        if order_by is not None:
            params['orderBy'] = order_by

        if page_size is not None:
            params['pageSize'] = page_size

        if page_token is not None:
            params['pageToken'] = page_token

        resp = self._connection.api_request(
            method='POST', path='/entries:list', data=params)

        return resp.get('entries', ()), resp.get('nextPageToken')

    def write_entries(self, entries, logger_name=None, resource=None,
                      labels=None):
        """API call:  log an entry resourc3 via a POST request

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/write

        :type entries: sequence of mapping
        :param entries: the log entry resources to log.

        :type logger_name: string
        :param logger_name: name of default logger to which to log the entries;
                            individual entries may override.

        :type resource: mapping
        :param resource: default resource to associate with entries;
                         individual entries may override.

        :type labels: mapping
        :param labels: default labels to associate with entries;
                       individual entries may override.
        """
        data = {'entries': list(entries)}

        if logger_name is not None:
            data['logName'] = logger_name

        if resource is not None:
            data['resource'] = resource

        if labels is not None:
            data['labels'] = labels

        self._connection.api_request(method='POST', path='/entries:write',
                                     data=data)

    def logger_delete(self, project, logger_name):
        """API call:  delete all entries in a logger via a DELETE request

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.logs/delete

        :type project: string
        :param project: ID of project containing the log entries to delete

        :type logger_name: string
        :param logger_name: name of logger containing the log entries to delete
        """
        path = '/projects/%s/logs/%s' % (project, logger_name)
        self._connection.api_request(method='DELETE', path=path)

class _SinksAPI(object):
    """Helper mapping sink-related APIs.

    :type connection: :class:`gcloud.logging.connection.Connection`
    :param connection: the connection used to make API requests.
    """
    def __init__(self, connection):
        self._connection = connection

    def list_sinks(self, project, page_size=None, page_token=None):
        """List sinks for the project associated with this client.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/list

        :type project: string
        :param project: ID of the project whose sinks are to be listed.

        :type page_size: int
        :param page_size: maximum number of sinks to return, If not passed,
                          defaults to a value set by the API.

        :type page_token: str
        :param page_token: opaque marker for the next "page" of sinks. If not
                           passed, the API will return the first page of
                           sinks.

        :rtype: tuple, (list, str)
        :returns: list of :class:`gcloud.logging.sink.Sink`, plus a
                  "next page token" string:  if not None, indicates that
                  more sinks can be retrieved with another call (pass that
                  value as ``page_token``).
        """
        params = {}

        if page_size is not None:
            params['pageSize'] = page_size

        if page_token is not None:
            params['pageToken'] = page_token

        path = '/projects/%s/sinks' % (project,)
        resp = self._connection.api_request(
            method='GET', path=path, query_params=params)
        sinks = resp.get('sinks', ())
        return sinks, resp.get('nextPageToken')

    def sink_create(self, project, sink_name, filter_, destination):
        """API call:  create a sink resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/create

        :type project: string
        :param project: ID of the project in which to create the sink.

        :type sink_name: string
        :param sink_name: the name of the sink

        :type filter_: string
        :param filter_: the advanced logs filter expression defining the
                        entries exported by the sink.

        :type destination: string
        :param destination: destination URI for the entries exported by
                            the sink.
        """
        target = '/projects/%s/sinks' % (project,)
        data = {
            'name': sink_name,
            'filter': filter_,
            'destination': destination,
        }
        self._connection.api_request(method='POST', path=target, data=data)

    def sink_get(self, project, sink_name):
        """API call:  retrieve a sink resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/get

        :type project: string
        :param project: ID of the project containing the sink.

        :type sink_name: string
        :param sink_name: the name of the sink
        """
        target = '/projects/%s/sinks/%s' % (project, sink_name)
        return self._connection.api_request(method='GET', path=target)

    def sink_update(self, project, sink_name, filter_, destination):
        """API call:  update a sink resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/update

        :type project: string
        :param project: ID of the project containing the sink.

        :type sink_name: string
        :param sink_name: the name of the sink

        :type filter_: string
        :param filter_: the advanced logs filter expression defining the
                        entries exported by the sink.

        :type destination: string
        :param destination: destination URI for the entries exported by
                            the sink.
        """
        target = '/projects/%s/sinks/%s' % (project, sink_name)
        data = {
            'name': sink_name,
            'filter': filter_,
            'destination': destination,
        }
        self._connection.api_request(method='PUT', path=target, data=data)

    def sink_delete(self, project, sink_name):
        """API call:  delete a sink resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/delete

        :type project: string
        :param project: ID of the project containing the sink.

        :type sink_name: string
        :param sink_name: the name of the sink
        """
        target = '/projects/%s/sinks/%s' % (project, sink_name)
        self._connection.api_request(method='DELETE', path=target)

class _MetricsAPI(object):
    """Helper mapping sink-related APIs.

    :type connection: :class:`gcloud.logging.connection.Connection`
    :param connection: the connection used to make API requests.
    """
    def __init__(self, connection):
        self._connection = connection

    def list_metrics(self, project, page_size=None, page_token=None):
        """List metrics for the project associated with this client.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/list

        :type project: string
        :param project: ID of the project whose metrics are to be listed.

        :type page_size: int
        :param page_size: maximum number of metrics to return, If not passed,
                          defaults to a value set by the API.

        :type page_token: str
        :param page_token: opaque marker for the next "page" of metrics. If not
                           passed, the API will return the first page of
                           metrics.

        :rtype: tuple, (list, str)
        :returns: list of :class:`gcloud.logging.metric.Metric`, plus a
                  "next page token" string:  if not None, indicates that
                  more metrics can be retrieved with another call (pass that
                  value as ``page_token``).
        """
        params = {}

        if page_size is not None:
            params['pageSize'] = page_size

        if page_token is not None:
            params['pageToken'] = page_token

        path = '/projects/%s/metrics' % (project,)
        resp = self._connection.api_request(
            method='GET', path=path, query_params=params)
        metrics = resp.get('metrics', ())
        return metrics, resp.get('nextPageToken')

    def metric_create(self, project, metric_name, filter_, description=None):
        """API call:  create a metric resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/create

        :type project: string
        :param project: ID of the project in which to create the metric.

        :type metric_name: string
        :param metric_name: the name of the metric

        :type filter_: string
        :param filter_: the advanced logs filter expression defining the
                        entries exported by the metric.

        :type description: string
        :param description: description of the metric.
        """
        target = '/projects/%s/metrics' % (project,)
        data = {
            'name': metric_name,
            'filter': filter_,
            'description': description,
        }
        self._connection.api_request(method='POST', path=target, data=data)

    def metric_get(self, project, metric_name):
        """API call:  retrieve a metric resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/get

        :type project: string
        :param project: ID of the project containing the metric.

        :type metric_name: string
        :param metric_name: the name of the metric
        """
        target = '/projects/%s/metrics/%s' % (project, metric_name)
        return self._connection.api_request(method='GET', path=target)

    def metric_update(self, project, metric_name, filter_, description):
        """API call:  update a metric resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/update

        :type project: string
        :param project: ID of the project containing the metric.

        :type metric_name: string
        :param metric_name: the name of the metric

        :type filter_: string
        :param filter_: the advanced logs filter expression defining the
                        entries exported by the metric.

        :type description: string
        :param description: description of the metric.
        """
        target = '/projects/%s/metrics/%s' % (project, metric_name)
        data = {
            'name': metric_name,
            'filter': filter_,
            'description': description,
        }
        self._connection.api_request(method='PUT', path=target, data=data)

    def metric_delete(self, project, metric_name):
        """API call:  delete a metric resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/delete

        :type project: string
        :param project: ID of the project containing the metric.

        :type metric_name: string
        :param metric_name: the name of the metric
        """
        target = '/projects/%s/metrics/%s' % (project, metric_name)
        self._connection.api_request(method='DELETE', path=target)
<commit_msg>Docstring indentation.<commit_after># Copyright 2016 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Create / interact with gcloud logging connections."""

from gcloud import connection as base_connection

class Connection(base_connection.JSONConnection):
    """A connection to Google Cloud Logging via the JSON REST API.

    :type credentials: :class:`oauth2client.client.OAuth2Credentials`
    :param credentials: (Optional) The OAuth2 Credentials to use for this
                        connection.

    :type http: :class:`httplib2.Http` or class that defines ``request()``.
    :param http: (Optional) HTTP object to make requests.

    :type api_base_url: string
    :param api_base_url: The base of the API call URL. Defaults to the value
                         :attr:`Connection.API_BASE_URL`.
    """

    API_BASE_URL = 'https://logging.googleapis.com'
    """The base of the API call URL."""

    API_VERSION = 'v2beta1'
    """The version of the API, used in building the API call's URL."""

    API_URL_TEMPLATE = '{api_base_url}/{api_version}{path}'
    """A template for the URL of a particular API call."""

    SCOPE = ('https://www.googleapis.com/auth/logging.read',
             'https://www.googleapis.com/auth/logging.write',
             'https://www.googleapis.com/auth/logging.admin',
             'https://www.googleapis.com/auth/cloud-platform')
    """The scopes required for authenticating as a Cloud Logging consumer."""

class _LoggingAPI(object):
    """Helper mapping logging-related APIs.

    :type connection: :class:`gcloud.logging.connection.Connection`
    :param connection: the connection used to make API requests.
    """
    def __init__(self, connection):
        self._connection = connection

    def list_entries(self, projects, filter_=None, order_by=None,
                     page_size=None, page_token=None):
        """Return a page of log entry resources.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/list

        :type projects: list of strings
        :param projects: project IDs to include. If not passed,
                            defaults to the project bound to the client.

        :type filter_: str
        :param filter_: a filter expression. See:
                        https://cloud.google.com/logging/docs/view/advanced_filters

        :type order_by: str
        :param order_by: One of :data:`gcloud.logging.ASCENDING` or
                         :data:`gcloud.logging.DESCENDING`.

        :type page_size: int
        :param page_size: maximum number of entries to return, If not passed,
                          defaults to a value set by the API.

        :type page_token: str
        :param page_token: opaque marker for the next "page" of entries. If not
                           passed, the API will return the first page of
                           entries.

        :rtype: tuple, (list, str)
        :returns: list of mappings, plus a "next page token" string:
                  if not None, indicates that more entries can be retrieved
                   with another call (pass that value as ``page_token``).
        """
        params = {'projectIds': projects}

        if filter_ is not None:
            params['filter'] = filter_

        if order_by is not None:
            params['orderBy'] = order_by

        if page_size is not None:
            params['pageSize'] = page_size

        if page_token is not None:
            params['pageToken'] = page_token

        resp = self._connection.api_request(
            method='POST', path='/entries:list', data=params)

        return resp.get('entries', ()), resp.get('nextPageToken')

    def write_entries(self, entries, logger_name=None, resource=None,
                      labels=None):
        """API call:  log an entry resourc3 via a POST request

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/entries/write

        :type entries: sequence of mapping
        :param entries: the log entry resources to log.

        :type logger_name: string
        :param logger_name: name of default logger to which to log the entries;
                            individual entries may override.

        :type resource: mapping
        :param resource: default resource to associate with entries;
                         individual entries may override.

        :type labels: mapping
        :param labels: default labels to associate with entries;
                       individual entries may override.
        """
        data = {'entries': list(entries)}

        if logger_name is not None:
            data['logName'] = logger_name

        if resource is not None:
            data['resource'] = resource

        if labels is not None:
            data['labels'] = labels

        self._connection.api_request(method='POST', path='/entries:write',
                                     data=data)

    def logger_delete(self, project, logger_name):
        """API call:  delete all entries in a logger via a DELETE request

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.logs/delete

        :type project: string
        :param project: ID of project containing the log entries to delete

        :type logger_name: string
        :param logger_name: name of logger containing the log entries to delete
        """
        path = '/projects/%s/logs/%s' % (project, logger_name)
        self._connection.api_request(method='DELETE', path=path)

class _SinksAPI(object):
    """Helper mapping sink-related APIs.

    :type connection: :class:`gcloud.logging.connection.Connection`
    :param connection: the connection used to make API requests.
    """
    def __init__(self, connection):
        self._connection = connection

    def list_sinks(self, project, page_size=None, page_token=None):
        """List sinks for the project associated with this client.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/list

        :type project: string
        :param project: ID of the project whose sinks are to be listed.

        :type page_size: int
        :param page_size: maximum number of sinks to return, If not passed,
                          defaults to a value set by the API.

        :type page_token: str
        :param page_token: opaque marker for the next "page" of sinks. If not
                           passed, the API will return the first page of
                           sinks.

        :rtype: tuple, (list, str)
        :returns: list of :class:`gcloud.logging.sink.Sink`, plus a
                  "next page token" string:  if not None, indicates that
                  more sinks can be retrieved with another call (pass that
                  value as ``page_token``).
        """
        params = {}

        if page_size is not None:
            params['pageSize'] = page_size

        if page_token is not None:
            params['pageToken'] = page_token

        path = '/projects/%s/sinks' % (project,)
        resp = self._connection.api_request(
            method='GET', path=path, query_params=params)
        sinks = resp.get('sinks', ())
        return sinks, resp.get('nextPageToken')

    def sink_create(self, project, sink_name, filter_, destination):
        """API call:  create a sink resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/create

        :type project: string
        :param project: ID of the project in which to create the sink.

        :type sink_name: string
        :param sink_name: the name of the sink

        :type filter_: string
        :param filter_: the advanced logs filter expression defining the
                        entries exported by the sink.

        :type destination: string
        :param destination: destination URI for the entries exported by
                            the sink.
        """
        target = '/projects/%s/sinks' % (project,)
        data = {
            'name': sink_name,
            'filter': filter_,
            'destination': destination,
        }
        self._connection.api_request(method='POST', path=target, data=data)

    def sink_get(self, project, sink_name):
        """API call:  retrieve a sink resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/get

        :type project: string
        :param project: ID of the project containing the sink.

        :type sink_name: string
        :param sink_name: the name of the sink
        """
        target = '/projects/%s/sinks/%s' % (project, sink_name)
        return self._connection.api_request(method='GET', path=target)

    def sink_update(self, project, sink_name, filter_, destination):
        """API call:  update a sink resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/update

        :type project: string
        :param project: ID of the project containing the sink.

        :type sink_name: string
        :param sink_name: the name of the sink

        :type filter_: string
        :param filter_: the advanced logs filter expression defining the
                        entries exported by the sink.

        :type destination: string
        :param destination: destination URI for the entries exported by
                            the sink.
        """
        target = '/projects/%s/sinks/%s' % (project, sink_name)
        data = {
            'name': sink_name,
            'filter': filter_,
            'destination': destination,
        }
        self._connection.api_request(method='PUT', path=target, data=data)

    def sink_delete(self, project, sink_name):
        """API call:  delete a sink resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.sinks/delete

        :type project: string
        :param project: ID of the project containing the sink.

        :type sink_name: string
        :param sink_name: the name of the sink
        """
        target = '/projects/%s/sinks/%s' % (project, sink_name)
        self._connection.api_request(method='DELETE', path=target)

class _MetricsAPI(object):
    """Helper mapping sink-related APIs.

    :type connection: :class:`gcloud.logging.connection.Connection`
    :param connection: the connection used to make API requests.
    """
    def __init__(self, connection):
        self._connection = connection

    def list_metrics(self, project, page_size=None, page_token=None):
        """List metrics for the project associated with this client.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/list

        :type project: string
        :param project: ID of the project whose metrics are to be listed.

        :type page_size: int
        :param page_size: maximum number of metrics to return, If not passed,
                          defaults to a value set by the API.

        :type page_token: str
        :param page_token: opaque marker for the next "page" of metrics. If not
                           passed, the API will return the first page of
                           metrics.

        :rtype: tuple, (list, str)
        :returns: list of :class:`gcloud.logging.metric.Metric`, plus a
                  "next page token" string:  if not None, indicates that
                  more metrics can be retrieved with another call (pass that
                  value as ``page_token``).
        """
        params = {}

        if page_size is not None:
            params['pageSize'] = page_size

        if page_token is not None:
            params['pageToken'] = page_token

        path = '/projects/%s/metrics' % (project,)
        resp = self._connection.api_request(
            method='GET', path=path, query_params=params)
        metrics = resp.get('metrics', ())
        return metrics, resp.get('nextPageToken')

    def metric_create(self, project, metric_name, filter_, description=None):
        """API call:  create a metric resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/create

        :type project: string
        :param project: ID of the project in which to create the metric.

        :type metric_name: string
        :param metric_name: the name of the metric

        :type filter_: string
        :param filter_: the advanced logs filter expression defining the
                        entries exported by the metric.

        :type description: string
        :param description: description of the metric.
        """
        target = '/projects/%s/metrics' % (project,)
        data = {
            'name': metric_name,
            'filter': filter_,
            'description': description,
        }
        self._connection.api_request(method='POST', path=target, data=data)

    def metric_get(self, project, metric_name):
        """API call:  retrieve a metric resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/get

        :type project: string
        :param project: ID of the project containing the metric.

        :type metric_name: string
        :param metric_name: the name of the metric
        """
        target = '/projects/%s/metrics/%s' % (project, metric_name)
        return self._connection.api_request(method='GET', path=target)

    def metric_update(self, project, metric_name, filter_, description):
        """API call:  update a metric resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/update

        :type project: string
        :param project: ID of the project containing the metric.

        :type metric_name: string
        :param metric_name: the name of the metric

        :type filter_: string
        :param filter_: the advanced logs filter expression defining the
                        entries exported by the metric.

        :type description: string
        :param description: description of the metric.
        """
        target = '/projects/%s/metrics/%s' % (project, metric_name)
        data = {
            'name': metric_name,
            'filter': filter_,
            'description': description,
        }
        self._connection.api_request(method='PUT', path=target, data=data)

    def metric_delete(self, project, metric_name):
        """API call:  delete a metric resource.

        See:
        https://cloud.google.com/logging/docs/api/ref_v2beta1/rest/v2beta1/projects.metrics/delete

        :type project: string
        :param project: ID of the project containing the metric.

        :type metric_name: string
        :param metric_name: the name of the metric
        """
        target = '/projects/%s/metrics/%s' % (project, metric_name)
        self._connection.api_request(method='DELETE', path=target)
<|endoftext|>


<commit_before>#!/usr/bin/python2.7

"""
This library is designed for lightweight generation of website content.

The idea is simple. We put a make.py in each subdirectory of the hierarchy
we want in the final version. *.data files are basically html, except they
can include <python> </python> tags the content of which will be run as
python code, and the tags replaced with that codes output.

How it works:
This file should be imported into a file named "make.py" at the root
of your file hierarchies.

Variables for your use:
- src_base: this is the root of the source hierarchy
- dest_base: should never be changed, this is the root of the destination
    hierarchy
- curdir: the directory (relative to src_base) of the file being run

Particularly interesting functions:
- run_make_subdirs() can be used to run "make.py" in each subdirectory.
each subsequent layer of make.py will gain the context of the make.py
that called it. Normally used in a make.py.

- pages_from_datafiles() can be used to interpret all *.data files in that
directory. These files are basically html, but can include tags of the form
"<python> python_code </python>". The python_code will be run (all code in
the file should be run in the same context). The file will be written out to
the destination hierarchy as a .html file with the python tag replaced with
the output of it's code to standard out. Normally used in a make.py.

Dependencies:
 - beautifulsoup (debian: python-bs4)
     this is just for prettification, removing is trivial
 - httplib2 (debian: python-httplib2)
"""

import os
import sys
import time
import errno
import StringIO
import HTMLParser
from bs4 import BeautifulSoup

src_base = 'dummy'
dest_base = 'dummy'
curdir = 'dummy'

def init(argv):
  global src_base
  global dest_base
  global curdir
  # In case it was run from some other path
  # it's important we work relative to the binaries location
  os.chdir(os.path.dirname(argv[0]))
  # This will be used by subsequent files to figure out the hierarchy
  # they are in.
  src_base = os.getcwd()
  # Assume we're writing to the directory below this one
  dest_base = os.path.split(os.getcwd())[0] + '/public'
  # This will be overridden at each file layer to be the current files dir
  curdir = '.'
  print '*** Initializing htmlgen ***'
  print 'curdir:', curdir
  print 'src_base:', src_base
  print 'dest_base:', dest_base

### Some utility functions
def panic(msg):
  """ Just what you think, call for fatal errors. """
  print(msg)
  sys.exit(1)

def listdir(d):
  """ A simple wrapper that skips special files. """
  return ('/'.join([d,f]) for f in os.listdir(d) if f[0] != '.')

def clean(abspath, nodelete_abspath=None):
  """ Deletes a directory hierarchy.

  abspath -- The path of the directory under which everything will be deleted.
  nodelete_abspath -- a path that may be below "path" which should not be
      deleted
  Returns: None
  """
  print 'Cleaning', abspath, nodelete_abspath
  for tuple in os.walk(abspath, topdown=False):
    (path, subdirs, files) = tuple
    # if our source directory is a prefix, we're looking at a source file
    # don't delete it!
    if nodelete_abspath:
      common = os.path.commonprefix([path, nodelete_abspath])
      if common == nodelete_abspath:
        continue
    for f in files:
      os.unlink(os.path.join(path, f))
    for s in subdirs:
      # This is just for the special case of the "src" directory
      common = os.path.commonprefix([os.path.join(path, s), nodelete_abspath])
      if nodelete_abspath and common == nodelete_abspath:
        continue
      os.rmdir(os.path.join(path, s))

def set_perms(file):
  """ Sets the permissions for a file, so it's readable for serving etc.

  file -- The the file to set permissions on
  Returns: None
  """
  try:
    os.chown(file, os.getuid(), web_group)
    if os.path.isdir(file):
      os.chmod(file, 0o775)
    else:
      os.chmod(file, 0o664)
  except:
    pass

def create_dest(src_base, dest_base, curdir):
  """ Create a destination directory to match a dir in the source tree.

  src_base -- The source tree basepath
  dest_base -- The destination tree basepath
  curdir -- The directory in the source tree that we want created
    in the destination tree.
  Return: New destination directory
  """
  dest_path = os.path.join(dest_base,
      os.path.relpath(curdir, src_base))
  try:
    os.makedirs(dest_path)
  except:
    pass
  return dest_path

def computeurl(cur_path_from_base, rel_link_path):
  """ Build a local link.

  Example:
    cur_path_from_base = /home/mbrewer/website/stuff
    rel_link_path = other_stuff/foo.html
    returns a link to /home/mbrewer/other_stuff/foo

  cur_path_from_base -- path we are at relative to the base of the hierarchy.
  rel_link_path -- path relative to that base.
  Return: a link to rel_link_path for use in a  tag
  """
  filename = []
  if cur_path_from_base != '.':
    filename = ['..' for t in cur_path_from_base.split('/')]
  filename.append(rel_link_path)
  return '/'.join(filename)

def dump_file(dest_path, data):
  """ Output a file.
  dest_path -- destination to write to.
  data -- a list of strings to be output (newlines between each string).
  Returns: None
  """
  # And dump the content to the suggested file
  f = open(os.path.join(dest_path), mode='w')
  f.write(data)

def symlink_files(src_path, dest_path):
  """ symlink files in dest_path to src_path.

  src_path -- the path of the source directory to symlink to
  dest_path -- the path of the destination path to place symlinks in
  Returns: None
  """
  # Create the directory if it doesn't exist
  src_path = os.path.normpath(src_path)
  dest_path = os.path.normpath(dest_path)
  try:
    os.makedirs(dest_path)
    set_perms(dest_path)
  except:
    pass
  files = listdir(src_path)
  # Create symlinks to all the files
  for f_src_path in files:
    f = os.path.basename(f_src_path)
    # skip build and .data files
    if f == 'make.py':
      continue
    if f[-5:] == '.data':
      continue
    if os.path.isdir(f_src_path):
      continue
    # copy would work too, this is easier in python for some reason
    # It's kindof nice for large files anyway
    os.symlink(os.path.join(src_path, f), os.path.join(dest_path, f))
    set_perms(os.path.join(src_path, f))
    set_perms(os.path.join(dest_path, f))

# This function can be used as the only line in a file
# to index that directory and all below it
def simple_index(src_dirpath, src_base, dest_base, gen_header, gen_footer):
  """ Build an index of a directory tree.

  src_dirpath -- directory to build the tree in.
  src_base -- base of the source hieararchy.
  dest_base -- base of the destination hieararchy.
  gen_header -- function outputing anything that should be added to the header
    of the file. (takes title and path)
  gen_footer -- function outputing anything that should be added to the footer
    of the file. (takes title and path)
  Returns: None
  """
  src_dirpath = os.path.join(src_base, src_dirpath)
  print 'simple_index', src_dirpath
  rel_path = os.path.relpath(src_dirpath, src_base)
  dest_dirpath = os.path.join(dest_base, rel_path)
  os.makedirs(dest_dirpath)
  # Walk subdirectories
  # symlink the files
  # create the directories
  # and build index.html files for each dir
  for tup in os.walk(src_dirpath, topdown=True):
    (src_path, subdirs, files) = tup
    # Create the directory
    rel_path = os.path.relpath(src_path, src_base)
    dest_path = os.path.join(dest_base, rel_path)
    # title is just the directory name
    title = src_path.split('/')[-1]
    # Index the directories and files
    symlink_files(src_path, dest_path)
    subdirs.sort()
    files.sort()
    entries = subdirs + files
    # get the urls for those files
    entries = [computeurl('.', f) for f in entries if f != 'make.py']
    # and output it
    data = []
    data += [gen_header(title, rel_path)]
    data += ['
']
    for entry in entries:
      data += ['
 ']
      if entry in subdirs:
        data += ['' + entry + '']
      else:
         data += [entry]
      data += ['
']
    data += ['
']
    data += [gen_footer(title, rel_path)]
    print 'dumping to', os.path.join(dest_path, 'index.html')
    dump_file(os.path.join(dest_path, 'index.html'), '\n'.join(data))

def run_python_tag(text, context):
  # fix tabbing
  lines = text.split('\n')
  # strip all spaces from the first line
  if lines[0]:
    num_spaces = 0
    while lines[0][num_spaces] == ' ':
      num_spaces += 1
    lines[0] = lines[0][num_spaces:]
  # strip the same number as line 2 from the rest
  if len(lines) > 1:
    if lines[1]:
      num_spaces = 0
      while lines[1][num_spaces] == ' ':
        num_spaces += 1
      first_line = lines[0]
      lines = [t[num_spaces:] for t in lines]
      lines[0] = first_line
  text = '\n'.join(lines)
  # run the code we have
  new_context = context.copy()
  output = StringIO.StringIO()
  old_stdout = sys.stdout
  sys.stdout = output
  exec text in new_context
  sys.stdout = old_stdout
  return output.getvalue()

def run_python_html(code, context, document_name):
  """ Run <python> tags and compile the result into an HTML string.

  code -- HTML string with <python> tags (or not).
  context -- context to run it in
  document_name -- name of the document (for debugging purposes)
  Returns: an HTML string.
  """

  # Note: We could do this by escaping all the HTML and sticking
  # "print" in front of it, this works great in languages that aren't python
  # in python we don't use "}" for blocks, so we can't have loops across those
  # statements anyway - therefore there's no gain, and I wrote this first.
  class MyHTMLParser(HTMLParser.HTMLParser):

    def __init__(self, context, document):
      HTMLParser.HTMLParser.__init__(self)
      self._in_pytag = False
      self._code = ''
      self._result = ''
      self._context = context
      self._document = document

    def handle_starttag(self, tag, attrs):
        if tag == 'python':
          self._in_pytag = True
          return
        try:
          attr_string = ' '.join(l + '="' + v + '"' for (l,v) in attrs)
          string = '<' + tag + ' ' + attr_string + '>'
        except:
          print 'ERROR:', self._document, 'Position:', self.getpos(), 'TAG:', tag
          string = ''
        if self._in_pytag:
          self._code += string
        else:
          self._result += string

    def handle_endtag(self, tag):
        if tag == 'python':
          self._in_pytag = False
          self._result += run_python_tag(self._code, self._context)
          self._code = ''
          return
        if self._in_pytag:
          self._code += '</' + tag + '>'
          return
        self._result += '</' + tag + '>'

    def handle_data(self, data):
        if self._in_pytag:
          self._code += data
          return
        self._result += data

    def get_result(self):
      return self._result

  parser = MyHTMLParser(context, document_name)
  parser.feed(code)
  soup = BeautifulSoup(parser.get_result())
  return soup.prettify()

def pages_from_datafiles(directory, src_base, dest_base, context):
  """ find .data files interpret them and output .html to destination.

  find <python> </python> tags in the HTML and pull out the code.
  Run the code and capture the output from stdout
  output the original HTML code with python tags replaced by their output.

  directory -- directory to search for files in
  src_base -- base of the source hierarchy
  dest_base -- base of the destination hierarchy
  context -- context to *copy* to then run these in
  Returns: None
  """
  if directory == '':
    directory = '.'
  directory = os.path.relpath(directory, src_base)
  src_path = os.path.join(src_base, directory)
  dest_path = os.path.join(dest_base, directory)
  symlink_files(src_path, dest_path)
  l = listdir(src_path)
  for src_f_path in l:
    if os.path.isdir(src_f_path):
      continue
    f_name = os.path.basename(src_f_path)
    if f_name[-5:] != '.data':
      continue
    f = open(src_f_path)
    try:
      os.makedirs(dest_path)
    except:
      pass
    data = run_python_html(f.read(), context, src_f_path)
    dump_file(os.path.join(dest_path, f_name[:-5]+'.html'), data)

def run_file(srcfile, src_base, dest_base, context):
  """ Run a python file.

  Note - this depends on and modifies global variables!
    curdir (changed)
    src_base (used)
    dest_base (used)

  srcfile -- source file to run.
  Returns: None
  """
  if srcfile[-3:] != '.py':
    panic('attempted to run a non sourcefile')
  print('running: ' + srcfile)
  # make a new namespace, so subdirs don't pollute supers
  new_context = context.copy()
  # give it the new directory path
  new_context['htmlgen'].curdir = os.path.relpath(os.path.dirname(srcfile), src_base)
  execfile(srcfile, new_context)

def run_make_subdirs(directory, src_base, dest_base, context):
  """ Runs python make.py in all subdirectories.

  directory -- directory to look in for subdirectories with makefiles.
  Returns: None
  """
  for subdir in listdir(directory):
    if not os.path.isdir(subdir):
      continue
    run_file(os.path.join(subdir, 'make.py'), src_base, dest_base, context)
<commit_msg>cleaning up calling conventions<commit_after>#!/usr/bin/python2.7

"""
This library is designed for lightweight generation of website content.

The idea is simple. We put a make.py in each subdirectory of the hierarchy
we want in the final version. *.data files are basically html, except they
can include <python> </python> tags the content of which will be run as
python code, and the tags replaced with that codes output.

How it works:
This file should be imported into a file named "make.py" at the root
of your file hierarchies.

Variables for your use:
- src_base: this is the root of the source hierarchy
- dest_base: should never be changed, this is the root of the destination
    hierarchy
- curdir: the directory (relative to src_base) of the file being run

Particularly interesting functions:
- run_make_subdirs() can be used to run "make.py" in each subdirectory.
each subsequent layer of make.py will gain the context of the make.py
that called it. Normally used in a make.py.

- pages_from_datafiles() can be used to interpret all *.data files in that
directory. These files are basically html, but can include tags of the form
"<python> python_code </python>". The python_code will be run (all code in
the file should be run in the same context). The file will be written out to
the destination hierarchy as a .html file with the python tag replaced with
the output of it's code to standard out. Normally used in a make.py.

Dependencies:
 - beautifulsoup (debian: python-bs4)
     this is just for prettification, removing is trivial
 - httplib2 (debian: python-httplib2)
"""

import errno
import HTMLParser
import os
import re
import sys
import StringIO
import time
from bs4 import BeautifulSoup

src_base = 'dummy'
dest_base = 'dummy'
curdir = 'dummy'

def init(argv):
  global src_base
  global dest_base
  global curdir
  # In case it was run from some other path
  # it's important we work relative to the binaries location
  os.chdir(os.path.dirname(argv[0]))
  # This will be used by subsequent files to figure out the hierarchy
  # they are in.
  src_base = os.getcwd()
  # Assume we're writing to the directory below this one
  dest_base = os.path.split(os.getcwd())[0] + '/public'
  # This will be overridden at each file layer to be the current files dir
  curdir = '.'
  print '*** Initializing htmlgen ***'
  print 'curdir:', curdir
  print 'src_base:', src_base
  print 'dest_base:', dest_base

### Some utility functions
def panic(msg):
  """ Just what you think, call for fatal errors. """
  print(msg)
  sys.exit(1)

def listdir(directory, exclude_patterns=None):
  """ A simple wrapper that skips special files. """
  ld = os.listdir(directory)
  if exclude_patterns:
    # go through each pattern, and drop everythign that matches it
    for p in exclude_patterns:
      ld = [d for d in ld if not re.match(p, d)]
  # put the directories back on
  return ('/'.join([directory,f]) for f in ld if f[0] != '.')

def clean(abspath=None, nodelete_abspath=None):
  """ Deletes a directory hierarchy.
  globals used:
    src_base: default for nodelete_abspath
    dest_base: default for abspath

  abspath -- The path of the directory under which everything will be deleted.
  nodelete_abspath -- a path that may be below "path" which should not be
      deleted
  Returns: None
  """
  if abspath is None:
    abspath = dest_base
  if nodelete_abspath is None:
    nodelete_abspath = src_base
  print 'Cleaning', abspath, nodelete_abspath
  for tuple in os.walk(abspath, topdown=False):
    (path, subdirs, files) = tuple
    # if our source directory is a prefix, we're looking at a source file
    # don't delete it!
    if nodelete_abspath:
      common = os.path.commonprefix([path, nodelete_abspath])
      if common == nodelete_abspath:
        continue
    for f in files:
      os.unlink(os.path.join(path, f))
    for s in subdirs:
      # This is just for the special case of the "src" directory
      common = os.path.commonprefix([os.path.join(path, s), nodelete_abspath])
      if nodelete_abspath and common == nodelete_abspath:
        continue
      os.rmdir(os.path.join(path, s))

def set_perms(file):
  """ Sets the permissions for a file, so it's readable for serving etc.

  file -- The the file to set permissions on
  Returns: None
  """
  try:
    os.chown(file, os.getuid(), web_group)
    if os.path.isdir(file):
      os.chmod(file, 0o775)
    else:
      os.chmod(file, 0o664)
  except:
    pass

def create_dest(curdir):
  """ Create a destination directory to match a dir in the source tree.
  globals used:
    src_base: The source tree basepath
    dest_base: The destination tree basepath

  curdir -- The directory in the source tree that we want created
    in the destination tree.
  Return: New destination directory
  """
  dest_path = os.path.join(dest_base,
      os.path.relpath(curdir, src_base))
  try:
    os.makedirs(dest_path)
  except:
    pass
  return dest_path

def computeurl(cur_path_from_base, rel_link_path):
  """ Build a local link.

  Example:
    cur_path_from_base = /home/mbrewer/website/stuff
    rel_link_path = other_stuff/foo.html
    returns a link to /home/mbrewer/other_stuff/foo

  cur_path_from_base -- path we are at relative to the base of the hierarchy.
  rel_link_path -- path relative to that base.
  Return: a link to rel_link_path for use in a  tag
  """
  filename = []
  if cur_path_from_base != '.':
    filename = ['..' for t in cur_path_from_base.split('/')]
  filename.append(rel_link_path)
  return '/'.join(filename)

def dump_file(dest_path, data):
  """ Output a file.
  dest_path -- destination to write to.
  data -- a list of strings to be output (newlines between each string).
  Returns: None
  """
  # And dump the content to the suggested file
  f = open(os.path.join(dest_path), mode='w')
  f.write(data)

def symlink_files(src_path, dest_path):
  """ symlink files in dest_path to src_path.

  src_path -- the path of the source directory to symlink to
  dest_path -- the path of the destination path to place symlinks in
  Returns: None
  """
  # Create the directory if it doesn't exist
  src_path = os.path.normpath(src_path)
  dest_path = os.path.normpath(dest_path)
  try:
    os.makedirs(dest_path)
    set_perms(dest_path)
  except:
    pass
  files = listdir(src_path)
  # Create symlinks to all the files
  for f_src_path in files:
    f = os.path.basename(f_src_path)
    # skip build and .data files
    if f == 'make.py':
      continue
    if f[-5:] == '.data':
      continue
    if os.path.isdir(f_src_path):
      continue
    # copy would work too, this is easier in python for some reason
    # It's kindof nice for large files anyway
    os.symlink(os.path.join(src_path, f), os.path.join(dest_path, f))
    set_perms(os.path.join(src_path, f))
    set_perms(os.path.join(dest_path, f))

# This function can be used as the only line in a file
# to index that directory and all below it
def simple_index(gen_header, gen_footer, src_dirpath=None):
  """ Build an index of a directory tree.
  globals used:
    curdir: directory to index (overriden by src_dirpath)
    src_base: base of the source hieararchy.
    dest_base: base of the destination hieararchy.

  gen_header -- function outputing anything that should be added to the header
    of the file. (takes title and path)
  gen_footer -- function outputing anything that should be added to the footer
    of the file. (takes title and path)
  src_dirpath -- directory to build the tree in.
  Returns: None
  """
  global curdir
  global src_base
  global dest_base
  if src_dirpath is None:
    src_dirpath = curdir
  src_dirpath = os.path.join(src_base, src_dirpath)
  print 'simple_index', src_dirpath
  rel_path = os.path.relpath(src_dirpath, src_base)
  dest_dirpath = os.path.join(dest_base, rel_path)
  os.makedirs(dest_dirpath)
  # Walk subdirectories
  # symlink the files
  # create the directories
  # and build index.html files for each dir
  for tup in os.walk(src_dirpath, topdown=True):
    (src_path, subdirs, files) = tup
    # Create the directory
    rel_path = os.path.relpath(src_path, src_base)
    dest_path = os.path.join(dest_base, rel_path)
    # title is just the directory name
    title = src_path.split('/')[-1]
    # Index the directories and files
    symlink_files(src_path, dest_path)
    subdirs.sort()
    files.sort()
    entries = subdirs + files
    # get the urls for those files
    entries = [computeurl('.', f) for f in entries if f != 'make.py']
    # and output it
    data = []
    data += [gen_header(title, rel_path)]
    data += ['
']
    for entry in entries:
      data += ['
 ']
      if entry in subdirs:
        data += ['' + entry + '']
      else:
         data += [entry]
      data += ['
']
    data += ['
']
    data += [gen_footer(title, rel_path)]
    print 'dumping to', os.path.join(dest_path, 'index.html')
    dump_file(os.path.join(dest_path, 'index.html'), '\n'.join(data))

def run_python_html(code, context, document_name):
  """ Run <python> tags and compile the result into an HTML string.

  code -- HTML string with <python> tags (or not).
  context -- context to run it in
  document_name -- name of the document (for debugging purposes)
  Returns: an HTML string.
  """

  def run_python_tag(text, context):
    # fix tabbing
    lines = text.split('\n')
    # strip all spaces from the first line
    if lines[0]:
      num_spaces = 0
      while lines[0][num_spaces] == ' ':
        num_spaces += 1
      lines[0] = lines[0][num_spaces:]
    # strip the same number as line 2 from the rest
    if len(lines) > 1:
      if lines[1]:
        num_spaces = 0
        while lines[1][num_spaces] == ' ':
          num_spaces += 1
        first_line = lines[0]
        lines = [t[num_spaces:] for t in lines]
        lines[0] = first_line
    text = '\n'.join(lines)
    # run the code we have
    new_context = context.copy()
    output = StringIO.StringIO()
    old_stdout = sys.stdout
    sys.stdout = output
    exec text in new_context
    sys.stdout = old_stdout
    return output.getvalue()

  # Note: We could do this by escaping all the HTML and sticking
  # "print" in front of it, this works great in languages that aren't python
  # in python we don't use "}" for blocks, so we can't have loops across those
  # statements anyway - therefore there's no gain, and I wrote this first.
  class MyHTMLParser(HTMLParser.HTMLParser):

    def __init__(self, context, document):
      HTMLParser.HTMLParser.__init__(self)
      self._in_pytag = False
      self._code = ''
      self._result = ''
      self._context = context
      self._document = document

    def handle_starttag(self, tag, attrs):
        if tag == 'python':
          self._in_pytag = True
          return
        try:
          attr_string = ' '.join(l + '="' + v + '"' for (l,v) in attrs)
          string = '<' + tag + ' ' + attr_string + '>'
        except:
          print 'ERROR:', self._document, 'Position:', self.getpos(), 'TAG:', tag
          string = ''
        if self._in_pytag:
          self._code += string
        else:
          self._result += string

    def handle_endtag(self, tag):
        if tag == 'python':
          self._in_pytag = False
          self._result += run_python_tag(self._code, self._context)
          self._code = ''
          return
        if self._in_pytag:
          self._code += '</' + tag + '>'
          return
        self._result += '</' + tag + '>'

    def handle_data(self, data):
        if self._in_pytag:
          self._code += data
          return
        self._result += data

    def get_result(self):
      return self._result

  parser = MyHTMLParser(context, document_name)
  parser.feed(code)
  soup = BeautifulSoup(parser.get_result())
  return soup.prettify()

def pages_from_datafiles(context, directory=None):
  """ find .data files interpret them and output .html to destination.

  find <python> </python> tags in the HTML and pull out the code.
  Run the code and capture the output from stdout
  output the original HTML code with python tags replaced by their output.

  uses globals:
    curdir: current directory
    src_base: base of the source hierarchy
    dest_base: base of the destination hierarchy

  directory -- directory to search for files in
  context -- context to *copy* to then run these in
  Returns: None
  """
  global curdir
  if directory is None:
    directory = curdir
  if directory == '':
    directory = '.'
  directory = os.path.relpath(directory, src_base)
  src_path = os.path.join(src_base, directory)
  dest_path = os.path.join(dest_base, directory)
  symlink_files(src_path, dest_path)
  l = listdir(src_path)
  for src_f_path in l:
    if os.path.isdir(src_f_path):
      continue
    f_name = os.path.basename(src_f_path)
    if f_name[-5:] != '.data':
      continue
    f = open(src_f_path)
    try:
      os.makedirs(dest_path)
    except:
      pass
    data = run_python_html(f.read(), context, src_f_path)
    dump_file(os.path.join(dest_path, f_name[:-5]+'.html'), data)

def run_python_file(context, srcfile):
  """ Run a python file.

  Note - this depends on global variables!
    curdir
    src_base
    dest_base

  context -- context in which the file will be run
  srcfile -- source file to run.
  Returns: None
  """
  if srcfile[-3:] != '.py':
    panic('attempted to run a non sourcefile')
  print('running: ' + srcfile)
  # make a new namespace, so subdirs don't pollute supers
  new_context = context.copy()
  # give it the new directory path
  new_context['htmlgen'].curdir = os.path.relpath(os.path.dirname(srcfile), src_base)
  execfile(srcfile, new_context)

def run_make_subdirs(context, directory=None, exclude_patterns=None):
  """ Runs python make.py in all subdirectories.

  directory -- directory to look in for subdirectories with makefiles.
  Returns: None
  """
  global curdir
  if directory is None:
    directory = curdir
  ld = listdir(directory, exclude_patterns=exclude_patterns)
  for subdir in ld:
    if not os.path.isdir(subdir):
      continue
    run_python_file(context, os.path.join(subdir, 'make.py'))
<|endoftext|>


<commit_before>#!/usr/bin/env python
import glob
import os
import sys
import unittest

import common

program = None
if len(sys.argv) < 2:
    raise ValueError('Need at least 2 parameters: runtests.py <build-dir> '
                     '<test-module-1> <test-module-2> ...')

buildDir = sys.argv[1]
files = sys.argv[2:]

common.importModules(buildDir=buildDir)

dir = os.path.split(os.path.abspath(__file__))[0]
os.chdir(dir)

def gettestnames():
    names = map(lambda x: x[:-3], files)
    return names

suite = unittest.TestSuite()
loader = unittest.TestLoader()

for name in gettestnames():
    if program and program not in name:
        continue
    suite.addTest(loader.loadTestsFromName(name))

testRunner = unittest.TextTestRunner()
testRunner.run(suite)
<commit_msg>Increase a bit verbosity of tests so people know which test failed<commit_after>#!/usr/bin/env python
import glob
import os
import sys
import unittest

import common

program = None
if len(sys.argv) < 2:
    raise ValueError('Need at least 2 parameters: runtests.py <build-dir> '
                     '<test-module-1> <test-module-2> ...')

buildDir = sys.argv[1]
files = sys.argv[2:]

common.importModules(buildDir=buildDir)

dir = os.path.split(os.path.abspath(__file__))[0]
os.chdir(dir)

def gettestnames():
    names = map(lambda x: x[:-3], files)
    return names

suite = unittest.TestSuite()
loader = unittest.TestLoader()

for name in gettestnames():
    if program and program not in name:
        continue
    suite.addTest(loader.loadTestsFromName(name))

testRunner = unittest.TextTestRunner(verbosity=2)
testRunner.run(suite)
<|endoftext|>


<commit_before>from __future__ import print_function, division, absolute_import
from .booleanOperationManager import BooleanOperationManager

__version__ = "0.5.1.dev1"

# export BooleanOperationManager static methods
union = BooleanOperationManager.union
difference = BooleanOperationManager.difference
intersection = BooleanOperationManager.intersection
xor = BooleanOperationManager.xor
getIntersections = BooleanOperationManager.getIntersections
<commit_msg>0.5.1: now both wheel and sdist are being uploaded to PyPI<commit_after>from __future__ import print_function, division, absolute_import
from .booleanOperationManager import BooleanOperationManager

__version__ = "0.5.1"

# export BooleanOperationManager static methods
union = BooleanOperationManager.union
difference = BooleanOperationManager.difference
intersection = BooleanOperationManager.intersection
xor = BooleanOperationManager.xor
getIntersections = BooleanOperationManager.getIntersections
<|endoftext|>


<commit_before>"""
Shell integration

See https://groups.google.com/d/msg/iterm2-discuss/URKCBtS0228/rs5Ive4PCAAJ
for documentation on the sequences,
https://github.com/gnachman/iterm2-website/tree/master/source/misc for example
implementations, and https://iterm2.com/shell_integration.html for a list of
what this lets you do in iTerm2.

Usage
=====

Say you have a basic REPL like::

    input> run-command
    command output

where ``input>`` is the prompt, ``run-command`` is the command typed by the user,
and ``command output`` is the output of ``run-command``. The basic REPL (in Python
3), would be::

    while True:
        before_prompt()
        print("input> ", end='')
        after_prompt()
        command = input()
        before_output()
        return_val = run_command(command)
        after_output(return_val)

(here ``return_val`` should be in the range 0-255).

Note that it is recommended to use the functions (like ``before_prompt()``) or the
context managers (like ``with Prompt()``) rather than the variables (like
``BEFORE_PROMPT``) directly. These print the codes directly to stdout, avoiding
potential issues with character counting.

It may be preferable to use the context managers rather than the functions,
in which case, the REPL would be::

    while True:
        with Prompt():
            print("input> ", end='')
        command = input() # raw_input() in Python 2
        with Output() as o:
            return_val = run_command(command)
            o.set_command_status(return_val)

However, in many cases, it is impossible to run functions before and after the
prompt, e.g., when the prompt text is passed to ``(raw_)input()`` directly. In
that case, you should use the codes directly, wrapped with
``readline_invisible()``, like::

    while True:
        command = input(
            readline_invisible(BEFORE_PROMPT) +
            "input> " +
            readline_invisible(AFTER_PROMPT
        ) # raw_input() in Python 2
        with Output() as o:
            return_val = run_command(command)
            o.set_command_status(return_val)

Using ``readline_invisible()`` is important as it tells readline to not count the
codes as visible text. Without this, readline's editing and history commands
will truncate text.

Notes about iTerm2:

- iTerm2 assumes that the prompt sequences will be presented in a reasonable
  way. Using the context managers should prevent most issues.

- The text that comes after the prompt before the first newline is read as a
  command. If there is no command, or the command is just whitespace, the
  output is effectively ignored (the same as if two before/after prompt
  sequences were performed without any output sequence).

- iTerm2 does  not support capturing  multiline commands, although  the output
  won't  include  any part  of  the  command  if ``before_output()``  is  used
  correctly.

- iTerm2 expects there to be nothing between ``AFTER_OUTPUT`` and
  ``BEFORE_PROMPT``, except possibly more shell sequences. At the time of this
  writing, iTerm2's "Select Output of Last Command" actually selects the text
  between ``BEFORE_OUTPUT`` and ``BEFORE_PROMPT``, not ``BEFORE_OUTPUT`` and
  ``AFTER_OUTPUT`` as one would expect.

- Multiline prompts are supported just fine, although the arrow will always be
  presented on the first line. It is not recommended to attempt to change this
  by not including part of the prompt between the prompt sequences (see the
  previous bullet point).

"""
from __future__ import print_function, division, absolute_import

import sys
from contextlib import contextmanager

# The "FinalTerm" shell sequences

BEFORE_PROMPT = '\033]133;A\a'
AFTER_PROMPT = '\033]133;B\a'
BEFORE_OUTPUT = '\033]133;C\a'
AFTER_OUTPUT = '\033]133;D;{command_status}\a' # command_status is the command status, 0-255

# iTerm2 specific sequences. All optional.

SET_USER_VAR = '\033]1337;SetUserVar={user_var_key}={user_var_value}\a'

# The current shell integration version is 1. We don't use this as an outdated
# shell integration version would only prompt the user to upgrade the
# integration that comes with iTerm2.
SHELL_INTEGRATION_VERSION = '\033]1337;ShellIntegrationVersion={shell_integration_version}\a'

# REMOTE_HOST and CURRENT_DIR are best echoed right after AFTER_OUTPUT.

# remote_host_hostname should be the fully qualified hostname. Integrations
# should allow users to set remote_host_hostname in case DNS is slow.
REMOTE_HOST = '\033]1337;RemoteHost={remote_host_username}@{remote_host_hostname}\a'
CURRENT_DIR = '\033]1337;CurrentDir={current_dir}\a'

def readline_invisible(code):
    """
    Wrap ``code`` with the special characters to tell readline that it is
    invisible.
    """
    return '\001%s\002' % code

def before_prompt():
    """
    Shell sequence to be run before the prompt.
    """
    sys.stdout.write(BEFORE_PROMPT)

def after_prompt():
    """
    Shell sequence to be run after the prompt.
    """
    sys.stdout.write(AFTER_PROMPT)

def before_output():
    """
    Shell sequence to be run before the command output.
    """
    sys.stdout.write(BEFORE_OUTPUT)

def after_output(command_status):
    """
    Shell sequence to be run after the command output.

    The ``command_status`` should be in the range 0-255.
    """
    if command_status not in range(256):
        raise ValueError("command_status must be an integer in the range 0-255")
    sys.stdout.write(AFTER_OUTPUT.format(command_status=command_status))

@contextmanager
def Prompt():
    """
    iTerm2 shell integration prompt context manager

    Use like::

        with Prompt():
            print("Prompt:", end='')
    """
    before_prompt()
    yield
    after_prompt()

class Output(object):
    """
    iTerm2 shell integration output context manager

    Use like::

        with Output() as o:
            print("output")
            o.set_command_status(status)

    The command status should be in the range 0-255. The default status is 0.
    """
    def __init__(self):
        self.command_status = 0

    def set_command_status(self, status):
        self.command_status = status

    def __enter__(self):
        before_output()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        after_output(self.command_status)
<commit_msg>Flush stdout in before_output and before_prompt<commit_after>"""
Shell integration

See https://groups.google.com/d/msg/iterm2-discuss/URKCBtS0228/rs5Ive4PCAAJ
for documentation on the sequences,
https://github.com/gnachman/iterm2-website/tree/master/source/misc for example
implementations, and https://iterm2.com/shell_integration.html for a list of
what this lets you do in iTerm2.

Usage
=====

Say you have a basic REPL like::

    input> run-command
    command output

where ``input>`` is the prompt, ``run-command`` is the command typed by the user,
and ``command output`` is the output of ``run-command``. The basic REPL (in Python
3), would be::

    while True:
        before_prompt()
        print("input> ", end='')
        after_prompt()
        command = input()
        before_output()
        return_val = run_command(command)
        after_output(return_val)

(here ``return_val`` should be in the range 0-255).

Note that it is recommended to use the functions (like ``before_prompt()``) or the
context managers (like ``with Prompt()``) rather than the variables (like
``BEFORE_PROMPT``) directly. These print the codes directly to stdout, avoiding
potential issues with character counting.

It may be preferable to use the context managers rather than the functions,
in which case, the REPL would be::

    while True:
        with Prompt():
            print("input> ", end='')
        command = input() # raw_input() in Python 2
        with Output() as o:
            return_val = run_command(command)
            o.set_command_status(return_val)

However, in many cases, it is impossible to run functions before and after the
prompt, e.g., when the prompt text is passed to ``(raw_)input()`` directly. In
that case, you should use the codes directly, wrapped with
``readline_invisible()``, like::

    while True:
        command = input(
            readline_invisible(BEFORE_PROMPT) +
            "input> " +
            readline_invisible(AFTER_PROMPT
        ) # raw_input() in Python 2
        with Output() as o:
            return_val = run_command(command)
            o.set_command_status(return_val)

Using ``readline_invisible()`` is important as it tells readline to not count the
codes as visible text. Without this, readline's editing and history commands
will truncate text.

Notes about iTerm2:

- iTerm2 assumes that the prompt sequences will be presented in a reasonable
  way. Using the context managers should prevent most issues.

- The text that comes after the prompt before the first newline is read as a
  command. If there is no command, or the command is just whitespace, the
  output is effectively ignored (the same as if two before/after prompt
  sequences were performed without any output sequence).

- iTerm2 does  not support capturing  multiline commands, although  the output
  won't  include  any part  of  the  command  if ``before_output()``  is  used
  correctly.

- iTerm2 expects there to be nothing between ``AFTER_OUTPUT`` and
  ``BEFORE_PROMPT``, except possibly more shell sequences. At the time of this
  writing, iTerm2's "Select Output of Last Command" actually selects the text
  between ``BEFORE_OUTPUT`` and ``BEFORE_PROMPT``, not ``BEFORE_OUTPUT`` and
  ``AFTER_OUTPUT`` as one would expect.

- Multiline prompts are supported just fine, although the arrow will always be
  presented on the first line. It is not recommended to attempt to change this
  by not including part of the prompt between the prompt sequences (see the
  previous bullet point).

"""
from __future__ import print_function, division, absolute_import

import sys
from contextlib import contextmanager

# The "FinalTerm" shell sequences

BEFORE_PROMPT = '\033]133;A\a'
AFTER_PROMPT = '\033]133;B\a'
BEFORE_OUTPUT = '\033]133;C\a'
AFTER_OUTPUT = '\033]133;D;{command_status}\a' # command_status is the command status, 0-255

# iTerm2 specific sequences. All optional.

SET_USER_VAR = '\033]1337;SetUserVar={user_var_key}={user_var_value}\a'

# The current shell integration version is 1. We don't use this as an outdated
# shell integration version would only prompt the user to upgrade the
# integration that comes with iTerm2.
SHELL_INTEGRATION_VERSION = '\033]1337;ShellIntegrationVersion={shell_integration_version}\a'

# REMOTE_HOST and CURRENT_DIR are best echoed right after AFTER_OUTPUT.

# remote_host_hostname should be the fully qualified hostname. Integrations
# should allow users to set remote_host_hostname in case DNS is slow.
REMOTE_HOST = '\033]1337;RemoteHost={remote_host_username}@{remote_host_hostname}\a'
CURRENT_DIR = '\033]1337;CurrentDir={current_dir}\a'

def readline_invisible(code):
    """
    Wrap ``code`` with the special characters to tell readline that it is
    invisible.
    """
    return '\001%s\002' % code

def before_prompt():
    """
    Shell sequence to be run before the prompt.
    """
    sys.stdout.write(BEFORE_PROMPT)
    # Flushing is important as the command timing feature is based on
    # BEFORE_OUTPUT and BEFORE_PROMPT
    sys.stdout.flush()

def after_prompt():
    """
    Shell sequence to be run after the prompt.
    """
    sys.stdout.write(AFTER_PROMPT)

def before_output():
    """
    Shell sequence to be run before the command output.
    """
    sys.stdout.write(BEFORE_OUTPUT)
    # Flushing is important as the command timing feature is based on
    # BEFORE_OUTPUT and BEFORE_PROMPT
    sys.stdout.flush()

def after_output(command_status):
    """
    Shell sequence to be run after the command output.

    The ``command_status`` should be in the range 0-255.
    """
    if command_status not in range(256):
        raise ValueError("command_status must be an integer in the range 0-255")
    sys.stdout.write(AFTER_OUTPUT.format(command_status=command_status))

@contextmanager
def Prompt():
    """
    iTerm2 shell integration prompt context manager

    Use like::

        with Prompt():
            print("Prompt:", end='')
    """
    before_prompt()
    yield
    after_prompt()

class Output(object):
    """
    iTerm2 shell integration output context manager

    Use like::

        with Output() as o:
            print("output")
            o.set_command_status(status)

    The command status should be in the range 0-255. The default status is 0.
    """
    def __init__(self):
        self.command_status = 0

    def set_command_status(self, status):
        self.command_status = status

    def __enter__(self):
        before_output()
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        after_output(self.command_status)
<|endoftext|>


<commit_before>import psutil

def run(config):
    return psutil.net_io_counters(pernic=True)<commit_msg>Update network.py<commit_after>#!/usr/bin/env python

import pickle
import sys
import psutil

pickle.dump(psutil.net_io_counters(pernic=True), sys.stdout)
<|endoftext|>


<commit_before>#------------------------------------------------------------------------------
# Copyright (c) 2005-2013, Enthought, Inc.
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in /LICENSE.txt and may be redistributed only
# under the conditions described in the aforementioned license.  The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
# Thanks for using Enthought open source!
#------------------------------------------------------------------------------
""" Trait assert mixin class to simplify test implementation for Trait
Classes.

"""

import contextlib
import threading

# Compatibility layer for Python 2.6: try loading unittest2
import sys
if sys.version_info[:2] == (2, 6):
    try:
        import unittest2 as unittest
    except ImportError:
        import unittest

else:
    import unittest

from traits.api import (Any, Event, HasStrictTraits, Instance, Int, List,
        Property, Str)
from traits.util.async_trait_wait import wait_for_condition

class _AssertTraitChangesContext(object):
    """ A context manager used to implement the trait change assert methods.

    Attributes
    ----------
    obj : HasTraits
        The HasTraits class instance who's class trait will change.

    xname : str
        The extended trait name of trait changes to listen too.

    count : int, optional
        The expected number of times the event should be fired. When None
        (default value) there is no check for the number of times the
        change event was fired.

    events : list of tuples
        A list with tuple elements containing the arguments of an
        `on_trait_change` event signature (<object>, <name>, <old>, <new>).

    Raises
    ------
    AssertionError :
          When the desired number of trait changed did not take place or when
          `count = None` and no trait change took place.

    """

    def __init__(self, obj, xname, count, test_case):
        """ Initialize the trait change assertion context manager.

        Parameters
        ----------
        obj : HasTraits
            The HasTraits class instance who's class trait will change.

        xname : str
            The extended trait name of trait changes to listen too.

        count : int, optional
            The expected number of times the event should be fired. When None
            (default value) there is no check for the number of times the
            change event was fired.

        test_case : TestCase
            A unittest TestCase where to raise the failureException if
            necessary.

        Notes
        -----
        - Checking if the provided xname corresponds to valid traits in
          the class is not implemented yet.

        """
        self.obj = obj
        self.xname = xname
        self.count = count
        self.events = []
        self.failureException = test_case.failureException

    def _listener(self, obj, name, old, new):
        """ Dummy trait listener
        """
        self.events.append((obj, name, old, new))

    def __enter__(self):
        """ Bind the trait listener
        """
        self.obj.on_trait_change(self._listener, self.xname)
        return self

    def __exit__(self, exc_type, exc_value, tb):
        """ Remove the trait listener

        """
        if exc_type is not None:
            return

        self.obj.on_trait_change(self._listener, self.xname, remove=True)
        if self.count is not None and len(self.events) != self.count:
            msg = 'Change event for {0} was fired {1} times instead of {2}'
            items = self.xname, len(self.events), self.count
            raise self.failureException(msg.format(*items))
        elif self.count is None and not self.events:
            msg = 'A change event was not fired for: {0}'.format(self.xname)
            raise self.failureException(msg)

class _TraitsChangeCollector(HasStrictTraits):
    """
    Class allowing thread-safe recording of events.

    """
    # The object we're listening to.
    obj = Any

    # The (possibly extended) trait name.
    trait = Str

    # Read-only event count.
    event_count = Property(Int)

    # Event that's triggered when the event count is updated.
    event_count_updated = Event

    # Private list of events.
    events = List(Any)

    # Lock used to allow access to events by multiple threads
    # simultaneously.
    _lock = Instance(threading.Lock, ())

    def start_collecting(self):
        self.obj.on_trait_change(
            self._event_handler,
            self.trait,
        )

    def stop_collecting(self):
        self.obj.on_trait_change(
            self._event_handler,
            self.trait,
            remove=True,
        )

    def _event_handler(self, new):
        with self._lock:
            self.events.append(new)
        self.event_count_updated = True

    def _get_event_count(self):
        """
        Traits property getter.

        Thread-safe access to event count.

        """
        with self._lock:
            return len(self.events)

class UnittestTools(object):
    """ Mixin class to augment the unittest.TestCase class with useful trait
    related assert methods.

    """

    def assertTraitChanges(self, obj, trait, count=None):
        """ Assert that the class trait changes exactly n times.

        Used in a with statement to assert that a class trait has changed
        during the execution of the code inside the with context block
        (similar to the assertRaises method).

        Please note that the context manager returns itself and the user can
        introspect the information of the fired events by accessing the
        ``events`` attribute of the context object. The attribute is a list
        with tuple elements containing the arguments of an `on_trait_change`
        event signature (<object>, <name>, <old>, <new>).

        **Example**::

            class MyClass(HasTraits):
               number = Float(2.0)

            my_class = MyClass()

            with self.assertTraitChanges(my_class, 'number') as ctx:
               my_class.number = 3.0

            self.assert(ctx.events, [(my_class, 'number', 2.0, 3.0)]

        Parameters
        ----------
        obj : HasTraits
            The HasTraits class instance who's class trait will change.

        xname : str
            The extended trait name of trait changes to listen too.

        count : int, optional
            The expected number of times the event should be fired. When None
            (default value) there is no check for the number of times the
            change event was fired.

        Notes
        -----
        - Checking if the provided xname corresponds to valid traits in
          the class is not implemented yet.

        """
        return _AssertTraitChangesContext(obj, trait, count, self)

    def assertTraitDoesNotChange(self, obj, xname):
        """ Assert that no trait event is fired.

        Used in a with statement to assert that a class trait has not changed
        during the execution of the code inside the with statement block.

        **Example**::

            class MyClass(HasTraits):
                number = Float(2.0)
                name = String

            my_class = MyClass()

            with self.assertTraitDoesNotChange(my_class, 'name'):
                 my_class.number = 2.0

        Parameters
        ----------
        obj : HasTraits
            The HasTraits class instance who's class trait will change.

        xname : str
            The extended trait name of trait changes to listen too.

        Notes
        -----
        - Checking if the provided xname corresponds to valid traits in
          the class is not implemented yet.

        """
        return _AssertTraitChangesContext(obj, xname, 0, self)

    @contextlib.contextmanager
    def assertTraitChangesAsync(self, obj, trait, count=1, timeout=5.0):
        """Assert an object trait eventually changes.

        Context manager used to assert that the given trait changes at
        least `count` times within the given timeout, as a result of
        execution of the body of the corresponding with block.

        The trait changes are permitted to occur asynchronously.

        Example usage:

        with self.assertTraitChangesAsync(my_object, 'SomeEvent', count=4):
            <do stuff that should cause my_object.SomeEvent to be
             fired at least 4 times within the next 5 seconds>

        """
        collector = _TraitsChangeCollector(obj=obj, trait=trait)

        # Pass control to body of the with statement.
        collector.start_collecting()
        try:
            yield collector

            # Wait for the expected number of events to arrive.
            try:
                wait_for_condition(
                    condition=lambda obj: obj.event_count >= count,
                    obj=collector,
                    trait='event_count_updated',
                    timeout=timeout,
                )
            except RuntimeError:
                actual_event_count = collector.event_count
                msg = ("Expected {0} event on {1} to be fired at least {2} "
                       "times, but the event was only fired {3} times "
                       "before timeout ({4} seconds).").format(
                    trait,
                    obj,
                    count,
                    actual_event_count,
                    timeout,
                )
                self.fail(msg)

        finally:
            collector.stop_collecting()

    def assertEventuallyTrue(self, obj, trait, condition, timeout=5.0):
        """Assert that the given condition is eventually true.

        Fail if the condition is not satisfied within the given timeout.

        `condition` takes no arguments, and should return a Boolean.

        `timeout` gives the maximum time (in seconds) to wait for the
        condition to become true.  Default is 5.0 seconds.  A value of
        `None` can be used to indicate no timeout.

        (obj, trait) give an object and trait to listen to for
        indication of a possible change: whenever the trait changes,
        the condition is re-evaluated.

        """
        try:
            wait_for_condition(
                condition=lambda obj: condition(),
                obj=obj,
                trait=trait,
                timeout=timeout,
            )
        except RuntimeError:
            # Helpful to know whether we timed out because the
            # condition never became true, or because the expected
            # event was never issued.
            condition_at_timeout = condition()
            self.fail(
                "Timed out waiting for condition. "
                "At timeout, condition was {0}.".format(condition_at_timeout))
<commit_msg>Changing signature of condition in assertEventuallyTrue so that obj is taken as an argument.<commit_after>#------------------------------------------------------------------------------
# Copyright (c) 2005-2013, Enthought, Inc.
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in /LICENSE.txt and may be redistributed only
# under the conditions described in the aforementioned license.  The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
# Thanks for using Enthought open source!
#------------------------------------------------------------------------------
""" Trait assert mixin class to simplify test implementation for Trait
Classes.

"""

import contextlib
import threading

# Compatibility layer for Python 2.6: try loading unittest2
import sys
if sys.version_info[:2] == (2, 6):
    try:
        import unittest2 as unittest
    except ImportError:
        import unittest

else:
    import unittest

from traits.api import (Any, Event, HasStrictTraits, Instance, Int, List,
        Property, Str)
from traits.util.async_trait_wait import wait_for_condition

class _AssertTraitChangesContext(object):
    """ A context manager used to implement the trait change assert methods.

    Attributes
    ----------
    obj : HasTraits
        The HasTraits class instance who's class trait will change.

    xname : str
        The extended trait name of trait changes to listen too.

    count : int, optional
        The expected number of times the event should be fired. When None
        (default value) there is no check for the number of times the
        change event was fired.

    events : list of tuples
        A list with tuple elements containing the arguments of an
        `on_trait_change` event signature (<object>, <name>, <old>, <new>).

    Raises
    ------
    AssertionError :
          When the desired number of trait changed did not take place or when
          `count = None` and no trait change took place.

    """

    def __init__(self, obj, xname, count, test_case):
        """ Initialize the trait change assertion context manager.

        Parameters
        ----------
        obj : HasTraits
            The HasTraits class instance who's class trait will change.

        xname : str
            The extended trait name of trait changes to listen too.

        count : int, optional
            The expected number of times the event should be fired. When None
            (default value) there is no check for the number of times the
            change event was fired.

        test_case : TestCase
            A unittest TestCase where to raise the failureException if
            necessary.

        Notes
        -----
        - Checking if the provided xname corresponds to valid traits in
          the class is not implemented yet.

        """
        self.obj = obj
        self.xname = xname
        self.count = count
        self.events = []
        self.failureException = test_case.failureException

    def _listener(self, obj, name, old, new):
        """ Dummy trait listener
        """
        self.events.append((obj, name, old, new))

    def __enter__(self):
        """ Bind the trait listener
        """
        self.obj.on_trait_change(self._listener, self.xname)
        return self

    def __exit__(self, exc_type, exc_value, tb):
        """ Remove the trait listener

        """
        if exc_type is not None:
            return

        self.obj.on_trait_change(self._listener, self.xname, remove=True)
        if self.count is not None and len(self.events) != self.count:
            msg = 'Change event for {0} was fired {1} times instead of {2}'
            items = self.xname, len(self.events), self.count
            raise self.failureException(msg.format(*items))
        elif self.count is None and not self.events:
            msg = 'A change event was not fired for: {0}'.format(self.xname)
            raise self.failureException(msg)

class _TraitsChangeCollector(HasStrictTraits):
    """
    Class allowing thread-safe recording of events.

    """
    # The object we're listening to.
    obj = Any

    # The (possibly extended) trait name.
    trait = Str

    # Read-only event count.
    event_count = Property(Int)

    # Event that's triggered when the event count is updated.
    event_count_updated = Event

    # Private list of events.
    events = List(Any)

    # Lock used to allow access to events by multiple threads
    # simultaneously.
    _lock = Instance(threading.Lock, ())

    def start_collecting(self):
        self.obj.on_trait_change(
            self._event_handler,
            self.trait,
        )

    def stop_collecting(self):
        self.obj.on_trait_change(
            self._event_handler,
            self.trait,
            remove=True,
        )

    def _event_handler(self, new):
        with self._lock:
            self.events.append(new)
        self.event_count_updated = True

    def _get_event_count(self):
        """
        Traits property getter.

        Thread-safe access to event count.

        """
        with self._lock:
            return len(self.events)

class UnittestTools(object):
    """ Mixin class to augment the unittest.TestCase class with useful trait
    related assert methods.

    """

    def assertTraitChanges(self, obj, trait, count=None):
        """ Assert that the class trait changes exactly n times.

        Used in a with statement to assert that a class trait has changed
        during the execution of the code inside the with context block
        (similar to the assertRaises method).

        Please note that the context manager returns itself and the user can
        introspect the information of the fired events by accessing the
        ``events`` attribute of the context object. The attribute is a list
        with tuple elements containing the arguments of an `on_trait_change`
        event signature (<object>, <name>, <old>, <new>).

        **Example**::

            class MyClass(HasTraits):
               number = Float(2.0)

            my_class = MyClass()

            with self.assertTraitChanges(my_class, 'number') as ctx:
               my_class.number = 3.0

            self.assert(ctx.events, [(my_class, 'number', 2.0, 3.0)]

        Parameters
        ----------
        obj : HasTraits
            The HasTraits class instance who's class trait will change.

        xname : str
            The extended trait name of trait changes to listen too.

        count : int, optional
            The expected number of times the event should be fired. When None
            (default value) there is no check for the number of times the
            change event was fired.

        Notes
        -----
        - Checking if the provided xname corresponds to valid traits in
          the class is not implemented yet.

        """
        return _AssertTraitChangesContext(obj, trait, count, self)

    def assertTraitDoesNotChange(self, obj, xname):
        """ Assert that no trait event is fired.

        Used in a with statement to assert that a class trait has not changed
        during the execution of the code inside the with statement block.

        **Example**::

            class MyClass(HasTraits):
                number = Float(2.0)
                name = String

            my_class = MyClass()

            with self.assertTraitDoesNotChange(my_class, 'name'):
                 my_class.number = 2.0

        Parameters
        ----------
        obj : HasTraits
            The HasTraits class instance who's class trait will change.

        xname : str
            The extended trait name of trait changes to listen too.

        Notes
        -----
        - Checking if the provided xname corresponds to valid traits in
          the class is not implemented yet.

        """
        return _AssertTraitChangesContext(obj, xname, 0, self)

    @contextlib.contextmanager
    def assertTraitChangesAsync(self, obj, trait, count=1, timeout=5.0):
        """Assert an object trait eventually changes.

        Context manager used to assert that the given trait changes at
        least `count` times within the given timeout, as a result of
        execution of the body of the corresponding with block.

        The trait changes are permitted to occur asynchronously.

        Example usage:

        with self.assertTraitChangesAsync(my_object, 'SomeEvent', count=4):
            <do stuff that should cause my_object.SomeEvent to be
             fired at least 4 times within the next 5 seconds>

        """
        collector = _TraitsChangeCollector(obj=obj, trait=trait)

        # Pass control to body of the with statement.
        collector.start_collecting()
        try:
            yield collector

            # Wait for the expected number of events to arrive.
            try:
                wait_for_condition(
                    condition=lambda obj: obj.event_count >= count,
                    obj=collector,
                    trait='event_count_updated',
                    timeout=timeout,
                )
            except RuntimeError:
                actual_event_count = collector.event_count
                msg = ("Expected {0} event on {1} to be fired at least {2} "
                       "times, but the event was only fired {3} times "
                       "before timeout ({4} seconds).").format(
                    trait,
                    obj,
                    count,
                    actual_event_count,
                    timeout,
                )
                self.fail(msg)

        finally:
            collector.stop_collecting()

    def assertEventuallyTrue(self, obj, trait, condition, timeout=5.0):
        """Assert that the given condition is eventually true.

        Fail if the condition is not satisfied within the given timeout.

        `condition` takes `obj` as an argument, and should return a Boolean
        indicating whether the condition is satisfied or not.

        `timeout` gives the maximum time (in seconds) to wait for the
        condition to become true.  Default is 5.0 seconds.  A value of
        `None` can be used to indicate no timeout.

        (obj, trait) give an object and trait to listen to for
        indication of a possible change: whenever the trait changes,
        the condition is re-evaluated.

        """
        try:
            wait_for_condition(
                condition=condition,
                obj=obj,
                trait=trait,
                timeout=timeout,
            )
        except RuntimeError:
            # Helpful to know whether we timed out because the
            # condition never became true, or because the expected
            # event was never issued.
            condition_at_timeout = condition(obj)
            self.fail(
                "Timed out waiting for condition. "
                "At timeout, condition was {0}.".format(condition_at_timeout))
<|endoftext|>