# -*- coding: utf-8 -*-
"""
    sphinx.ext.autosummary.generate
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    Usable as a library or script to generate automatic RST source files for
    items referred to in autosummary:: directives.

    Each generated RST file contains a single auto*:: directive which
    extracts the docstring of the referred item.

    Example Makefile rule::

       generate:
               sphinx-autogen -o source/generated source/*.rst

    :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

import os
import re
import sys
import pydoc
import optparse
import inspect

from jinja2 import FileSystemLoader, TemplateNotFound
from jinja2.sandbox import SandboxedEnvironment

from sphinx import package_dir
from ..autosummary import import_by_name, get_documenter
from sphinx.jinja2glue import BuiltinTemplateLoader
from sphinx.util.osutil import ensuredir
from sphinx.util.inspect import safe_getattr

def main(argv=sys.argv):
    usage = """%prog [OPTIONS] SOURCEFILE ..."""
    p = optparse.OptionParser(usage.strip())
    p.add_option("-o", "--output-dir", action="store", type="string",
                 dest="output_dir", default=None,
                 help="Directory to place all output in")
    p.add_option("-s", "--suffix", action="store", type="string",
                 dest="suffix", default="rst",
                 help="Default suffix for files (default: %default)")
    p.add_option("-t", "--templates", action="store", type="string",
                 dest="templates", default=None,
                 help="Custom template directory (default: %default)")
    options, args = p.parse_args(argv[1:])

    if len(args) < 1:
        p.error('no input files given')

    generate_autosummary_docs(args, options.output_dir,
                              "." + options.suffix,
                              template_dir=options.templates)

def _simple_info(msg):
    print msg

def _simple_warn(msg):
    print >> sys.stderr, 'WARNING: ' + msg

# -- Generating output ---------------------------------------------------------

def generate_autosummary_docs(sources, output_dir=None, suffix='.rst',
                              warn=_simple_warn, info=_simple_info,
                              base_path=None, builder=None, template_dir=None):

    showed_sources = list(sorted(sources))
    if len(showed_sources) > 20:
        showed_sources = showed_sources[:10] + ['...'] + showed_sources[-10:]
    info('[autosummary] generating autosummary for: %s' %
         ', '.join(showed_sources))

    if output_dir:
        info('[autosummary] writing to %s' % output_dir)

    if base_path is not None:
        sources = [os.path.join(base_path, filename) for filename in sources]

    # create our own templating environment
    template_dirs = [os.path.join(package_dir, 'ext',
                                  'autosummary', 'templates')]
    if builder is not None:
        # allow the user to override the templates
        template_loader = BuiltinTemplateLoader()
        template_loader.init(builder, dirs=template_dirs)
    else:
        if template_dir:
            template_dirs.insert(0, template_dir)
        template_loader = FileSystemLoader(template_dirs)
    template_env = SandboxedEnvironment(loader=template_loader)

    # read
    items = find_autosummary_in_files(sources)

    # remove possible duplicates
    items = dict([(item, True) for item in items]).keys()

    # keep track of new files
    new_files = []

    # write
    for name, path, template_name in sorted(items):
        if path is None:
            # The corresponding autosummary:: directive did not have
            # a :toctree: option
            continue

        path = output_dir or os.path.abspath(path)
        ensuredir(path)

        try:
    	    name, obj, parent = import_by_name(name)
        except ImportError, e:
            warn('[autosummary] failed to import %r: %s' % (name, e))
            continue

        fn = os.path.join(path, name + suffix)

        # skip it if it exists
        if os.path.isfile(fn):
            continue

        new_files.append(fn)

        f = open(fn, 'w')

        try:
            doc = get_documenter(obj, parent)

            if template_name is not None:
                template = template_env.get_template(template_name)
            else:
                try:
                    template = template_env.get_template('autosummary/%s.rst'
                                                         % doc.objtype)
                except TemplateNotFound:
                    template = template_env.get_template('autosummary/base.rst')

            def get_members(obj, typ, include_public=[]):
                items = []
                for name in dir(obj):

                    if sys.skip_member(name, obj): continue
                    if typ in ['class', 'function']:
                        c = getattr(obj, name)
                        if inspect.isclass(c) or inspect.isfunction(c):
                            if (c.__module__!=obj.__name__+".base" and
                                c.__module__!=obj.__name__):
                                    continue
                    try:
                        documenter = get_documenter(safe_getattr(obj, name), obj)
                    except AttributeError:
                        continue
                    if documenter.objtype == typ:
                        items.append(name)
                public = [x for x in items
                          if x in include_public or not x.startswith('_')]
                return public, items

            def def_members(obj, typ, include_public=[]):
                items = []
                try:
                    obj_dict = safe_getattr(obj, '__dict__')
                except AttributeError:
                    return []
                defined = obj_dict.keys()
                defined.sort()
                for name in defined:
                    if sys.skip_member(name, obj): continue
                    try:
                        documenter = get_documenter(safe_getattr(obj, name), obj)
                    except AttributeError:
                        continue
                    if documenter.objtype == typ:
                        items.append(name)
                public = [x for x in items
                          if x in include_public or not x.startswith('_')]
                return public

            ns = {}

            if doc.objtype == 'module':
                ns['all_members'] = dir(obj)

                ns['classes'], ns['all_classes'] = \
                                 get_members(obj, 'class')
                ns['functions'], ns['all_functions'] = \
                                   get_members(obj, 'function')
                ns['exceptions'], ns['all_exceptions'] = \
                                   get_members(obj, 'exception')
                documented = ns['classes']+ns['functions']+ns['exceptions']

                if sys.all_submodules.has_key(obj.__name__):
                    ns['submodules'] = sys.all_submodules[obj.__name__]
                    documented += ns['submodules']

                ns['members'] = ns['all_members']
                try:
                    obj_dict = safe_getattr(obj, '__dict__')
                except AttributeError:
                    obj_dict = []

                public = [x for x in obj_dict if not x.startswith('_')]
                for item in documented:
                    if item in public:
                        public.remove(item)

                public.sort()
                ns['members'] = public
                ns['constants'] = [x for x in public
                                   if not sys.skip_member(x, obj)]

            elif doc.objtype == 'class':
                ns['members'] = dir(obj)
                ns['events'], ns['all_events'] = \
                                 get_members(obj, 'event')
                ns['methods'], ns['all_methods'] = \
                                 get_members(obj, 'method', ['__init__'])
                ns['attributes'], ns['all_attributes'] = \
                                 get_members(obj, 'attribute')

                ns['def_events'] = def_members(obj, 'event')
                ns['def_methods'] = def_members(obj, 'method', ['__init__'])
                ns['def_attributes'] = def_members(obj, 'attribute')

                ns['inherited'] = []
                for t in ['events', 'methods', 'attributes']:
                    key = 'inh_' + t
                    ns[key]=[]
                    for item in ns[t]:
                        if not item in ns['def_' + t]:
                            ns['inherited'].append(item)
                            ns[key].append(item)

            parts = name.split('.')
            if doc.objtype in ('method', 'attribute'):
                mod_name = '.'.join(parts[:-2])
                cls_name = parts[-2]
                obj_name = '.'.join(parts[-2:])
                ns['class'] = cls_name
            else:
                mod_name, obj_name = '.'.join(parts[:-1]), parts[-1]

            ns['fullname'] = name
            ns['module'] = mod_name
            ns['objname'] = obj_name
            ns['name'] = parts[-1]

            ns['objtype'] = doc.objtype
            ns['underline'] = len(name) * '='

            rendered = template.render(**ns)
            f.write(rendered)
        finally:
            f.close()

    # descend recursively to new files
    if new_files:
        generate_autosummary_docs(new_files, output_dir=output_dir,
                                  suffix=suffix, warn=warn, info=info,
                                  base_path=base_path, builder=builder,
                                  template_dir=template_dir)

# -- Finding documented entries in files ---------------------------------------

def find_autosummary_in_files(filenames):
    """Find out what items are documented in source/*.rst.

    See `find_autosummary_in_lines`.
    """
    documented = []
    for filename in filenames:
        f = open(filename, 'r')
        lines = f.read().splitlines()
        documented.extend(find_autosummary_in_lines(lines, filename=filename))
        f.close()
    return documented

def find_autosummary_in_docstring(name, module=None, filename=None):
    """Find out what items are documented in the given object's docstring.

    See `find_autosummary_in_lines`.
    """
    try:
        real_name, obj, parent = import_by_name(name)
        lines = pydoc.getdoc(obj).splitlines()
        return find_autosummary_in_lines(lines, module=name, filename=filename)
    except AttributeError:
        pass
    except ImportError, e:
        print "Failed to import '%s': %s" % (name, e)
    return []

def find_autosummary_in_lines(lines, module=None, filename=None):
    """Find out what items appear in autosummary:: directives in the
    given lines.

    Returns a list of (name, toctree, template) where *name* is a name
    of an object and *toctree* the :toctree: path of the corresponding
    autosummary directive (relative to the root of the file name), and
    *template* the value of the :template: option. *toctree* and
    *template* ``None`` if the directive does not have the
    corresponding options set.
    """
    autosummary_re = re.compile(r'^(\s*)\.\.\s+autosummary::\s*')
    automodule_re = re.compile(
        r'^\s*\.\.\s+automodule::\s*([A-Za-z0-9_.]+)\s*$')
    module_re = re.compile(
        r'^\s*\.\.\s+(current)?module::\s*([a-zA-Z0-9_.]+)\s*$')
    autosummary_item_re = re.compile(r'^\s+(~?[_a-zA-Z][a-zA-Z0-9_.]*)\s*.*?')
    toctree_arg_re = re.compile(r'^\s+:toctree:\s*(.*?)\s*$')
    template_arg_re = re.compile(r'^\s+:template:\s*(.*?)\s*$')

    documented = []

    toctree = None
    template = None
    current_module = module
    in_autosummary = False
    base_indent = ""

    for line in lines:
        if in_autosummary:
            m = toctree_arg_re.match(line)
            if m:
                toctree = m.group(1)
                if filename:
                    toctree = os.path.join(os.path.dirname(filename),
                                           toctree)
                continue

            m = template_arg_re.match(line)
            if m:
                template = m.group(1).strip()
                continue

            if line.strip().startswith(':'):
                continue # skip options

            m = autosummary_item_re.match(line)
            if m:
                name = m.group(1).strip()
                if name.startswith('~'):
                    name = name[1:]
                if current_module and \
                       not name.startswith(current_module + '.'):
                    name = "%s.%s" % (current_module, name)
                documented.append((name, toctree, template))
                continue

            if not line.strip() or line.startswith(base_indent + " "):
                continue

            in_autosummary = False

        m = autosummary_re.match(line)
        if m:
            in_autosummary = True
            base_indent = m.group(1)
            toctree = None
            template = None
            continue

        m = automodule_re.search(line)
        if m:
            current_module = m.group(1).strip()
            # recurse into the automodule docstring
            documented.extend(find_autosummary_in_docstring(
                current_module, filename=filename))
            continue

        m = module_re.match(line)
        if m:
            current_module = m.group(2)
            continue

    return documented

if __name__ == '__main__':
    main()


#!/usr/bin/python
# -*- coding: utf-8 -*-

"Visual Inspector (using wx TreeListCtrl) of gui2py component's"

__author__ = "Mariano Reingart (reingart@gmail.com)"
__copyright__ = "Copyright (C) 2013- Mariano Reingart"
__license__ = "LGPL 3.0"

# some parts where inspired or borrowed from wxPython examples

import  wx
import  wx.gizmos   as  gizmos

from gui import registry
from gui.tools.propeditor import PropertyEditorPanel
from gui.tools.toolbox import set_drop_target

DEBUG = False
#----------------------------------------------------------------------

class InspectorPanel(wx.Panel):
    def __init__(self, parent, propeditor, log):
        self.log = log
        self.propeditor = propeditor
        self.designer = None
        self.highlighting = None
        self.selected_obj = None   # used by the toolbox as default drop target

        wx.Panel.__init__(self, parent, -1)
        self.Bind(wx.EVT_SIZE, self.OnSize)

        self.tree = gizmos.TreeListCtrl(self, -1, style =
                                        wx.TR_DEFAULT_STYLE
                                        | wx.TR_HAS_BUTTONS
                                        #| wx.TR_TWIST_BUTTONS
                                        | wx.TR_ROW_LINES
                                        | wx.TR_COLUMN_LINES
                                        #| wx.TR_NO_LINES
                                        | wx.TR_FULL_ROW_HIGHLIGHT
                                   )

        isz = (16,16)
        il = wx.ImageList(isz[0], isz[1])
        fldridx     = il.Add(wx.ArtProvider_GetBitmap(wx.ART_FOLDER,      wx.ART_OTHER, isz))
        fldropenidx = il.Add(wx.ArtProvider_GetBitmap(wx.ART_FILE_OPEN,   wx.ART_OTHER, isz))
        fileidx     = il.Add(wx.ArtProvider_GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, isz))
        self.images_map = {}
        for name, ctrl in registry.CONTROLS.items() + registry.WINDOWS.items() + registry.MENU.items():
            if ctrl._image:
                #bmp = wx.Bitmap(os.path.join(path, filename), wx.BITMAP_TYPE_XPM)
                self.images_map[name] = il.Add(ctrl._image.GetBitmap().ConvertToImage().Scale(16,16).ConvertToBitmap())

        self.tree.SetImageList(il)
        self.il = il

        # create some columns
        self.tree.AddColumn("Object name")
        self.tree.AddColumn("Class")
        #self.tree.AddColumn("Column 2")
        self.tree.SetMainColumn(0) # the one with the tree in it...
        self.tree.SetColumnWidth(0, 175)

        self.tree.GetMainWindow().Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
        self.tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnActivate)
        self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelect)

    def set_designer(self, designer):
        "track designer for context menu"
        self.designer = designer

    def load_object(self, obj=None):
        "Add the object and all their childs"
        # if not obj is given, do a full reload using the current root
        if obj:
            self.root_obj = obj
        else:
            obj = self.root_obj
        self.tree.DeleteAllItems()
        self.root = self.tree.AddRoot("application")
        self.tree.SetItemText(self.root, "App", 1)
        self.tree.SetItemText(self.root, "col 2 root", 2)
        #self.tree.SetItemImage(self.root, fldridx, which = wx.TreeItemIcon_Normal)
        #self.tree.SetItemImage(self.root, fldropenidx, which = wx.TreeItemIcon_Expanded)

        self.build_tree(self.root, obj)
        self.tree.Expand(self.root)

    def inspect(self, obj, context_menu=False, edit_prop=False, mouse_pos=None):
        "Select the object and show its properties"
        child = self.tree.FindItem(self.root, obj.name)
        if DEBUG: print "inspect child", child
        if child:
            self.tree.ScrollTo(child)
            self.tree.SetCurrentItem(child)
            self.tree.SelectItem(child)
            child.Selected = True
            self.activate_item(child, edit_prop)
            if context_menu:
                self.show_context_menu(child, mouse_pos)

    def build_tree(self, parent, obj):
        if DEBUG: print "building", obj.__class__.__name__
        child = self.tree.AppendItem(parent, obj.name)
        self.tree.SetItemText(child, obj.__class__.__name__, 1)
        if obj._meta.name in self.images_map:
            self.tree.SetItemImage(child, self.images_map[obj._meta.name])
        self.tree.SetItemData(child, wx.TreeItemData(obj))
        #self.tree.SetItemImage(child, fldropenidx, which = wx.TreeItemIcon_Expanded)
        for ctrl in obj:
            self.build_tree(child, ctrl)
        self.tree.Expand(child)

    def OnActivate(self, evt):
        child = evt.GetItem()
        print('OnActivate: %s' % self.tree.GetItemText(child))
        self.activate_item(child, select=True)

    def activate_item(self, child, edit_prop=False, select=False):
        "load the selected item in the property editor"
        d = self.tree.GetItemData(child)
        if d:
            o = d.GetData()
            self.selected_obj = o
            callback = lambda o=o, **kwargs: self.update(o, **kwargs)
            self.propeditor.load_object(o, callback)
            if edit_prop:
                wx.CallAfter(self.propeditor.edit)
            if select and self.designer:
                self.designer.select(o)
        else:
            self.selected_obj = None

    def update(self, obj, **kwargs):
        "Update the tree item when the object name changes"
        # search for the old name:
        child = self.tree.FindItem(self.root, kwargs['name'])
        if DEBUG: print "update child", child, kwargs
        if child:
            self.tree.ScrollTo(child)
            self.tree.SetCurrentItem(child)
            self.tree.SelectItem(child)
            child.Selected = True
            # update the new name
            self.tree.SetItemText(child, obj.name, 0)

    def do_highlight(self, tlw, rect, colour, pen_width=2):
        if not self.highlighting and tlw:
            self.highlighting = tlw
            if not tlw.IsFrozen():
                tlw.Freeze()

            dc = wx.ScreenDC()
            dco = None

            dc.SetPen(wx.Pen(colour, pen_width))
            dc.SetBrush(wx.TRANSPARENT_BRUSH)

            drawRect = wx.Rect(*rect)
            dc.DrawRectangleRect(drawRect)

            drawRect.Inflate(2,2)
            pos = tlw.ScreenToClient(drawRect.GetPosition())
            drawRect.SetPosition(pos)
            wx.CallLater(300, self.do_unhighlight, tlw, drawRect)
            return dc, dco

    def do_unhighlight(self, tlw, rect):
        if not tlw:
            return
        if tlw.IsFrozen():
            tlw.Thaw()
        tlw.RefreshRect(rect)
        self.highlighting = None

    def highlight(self, win):
        if win and isinstance(win, wx.Window):
            rect = win.GetClientRect()
            tlw = win.GetTopLevelParent()
            pos = win.ClientToScreen((0,0))
            rect.SetPosition(pos)
            self.do_highlight(tlw, rect, 'red')

    def OnSelect(self, evt):
        child = evt.GetItem()
        if DEBUG: print('OnSelect: %s' % self.tree.GetItemText(child))
        d = self.tree.GetItemData(child)
        if d:
            o = d.GetData()
            self.highlight(o.wx_obj if o else None)

    def OnRightUp(self, evt):
        pos = evt.GetPosition()
        item, flags, col = self.tree.HitTest(pos)
        self.show_context_menu(item)

    def show_context_menu(self, item, mouse_pos=None):
        "Open a popup menu with options regarding the selected object"
        if item:
            d = self.tree.GetItemData(item)
            if d:
                obj = d.GetData()
                if obj:
                    # highligh and store the selected object:
                    self.highlight(obj.wx_obj)
                    self.obj = obj

                    # make the context menu
                    menu = wx.Menu()
                    id_del, id_dup, id_raise, id_lower = [wx.NewId() for i
                                                            in range(4)]
                    menu.Append(id_del, "Delete")
                    menu.Append(id_dup, "Duplicate")
                    menu.Append(id_raise, "Bring to Front")
                    menu.Append(id_lower, "Send to Back")

                    # make submenu!
                    sm = wx.Menu()
                    for ctrl in sorted(obj._meta.valid_children,
                                       key=lambda c:
                                            registry.ALL.index(c._meta.name)):
                        new_id = wx.NewId()
                        sm.Append(new_id, ctrl._meta.name)
                        self.Bind(wx.EVT_MENU,
                                  lambda evt, ctrl=ctrl: self.add_child(ctrl, mouse_pos),
                                  id=new_id)

                    menu.AppendMenu(wx.NewId(), "Add child", sm)

                    self.Bind(wx.EVT_MENU, self.delete, id=id_del)
                    self.Bind(wx.EVT_MENU, self.duplicate, id=id_dup)
                    self.Bind(wx.EVT_MENU, self.bring_to_front, id=id_raise)
                    self.Bind(wx.EVT_MENU, self.send_to_back, id=id_lower)

                    self.PopupMenu(menu)
                    menu.Destroy()
                    self.load_object(self.root_obj)     # reload the tree

    def delete(self, evt, obj=None):
        # remove the object from parent and destroy it:
        if obj:
            self.obj = obj
        self.obj.destroy()
        del self.obj

    def duplicate(self, evt):
        # create a similar copy of the object
        obj2 = self.obj.duplicate()

    def bring_to_front(self, evt):
        self.obj.reindex(-1)

    def send_to_back(self, evt):
        self.obj.reindex(0)

    def add_child(self, ctrl, mouse_pos=None):
        new_id = wx.NewId()
        obj = ctrl(self.obj,
               name="%s_%s" % (ctrl._meta.name.lower(), new_id),
               id=new_id,
               designer=self.designer)
        # in design mode set drag & drop target for the toolbox:
        if self.designer and obj._meta.container and self.obj.drop_target:
            set_drop_target(obj, self.root_obj, self.designer, self)
        # move the new object to the mouse position of the click evt (if given)
        if mouse_pos:
            x, y = self.obj.wx_obj.ScreenToClient(mouse_pos)
            obj.pos = (x, y)
        # update the object at the inspector (to show the new control)
        wx.CallAfter(self.inspect, obj)

    def OnSize(self, evt):
        self.tree.SetSize(self.GetSize())

#----------------------------------------------------------------------

class InspectorTool:

    def __init__(self, parent=None, log=None):
        if log is None:
            import sys
            log = sys.stdout

        self.f1 = wx.Frame(parent, pos=(600,0), size=(300, 300))
        self.f2 = wx.Frame(parent, pos=(600,350), size=(300, 300))
        self.propeditor = PropertyEditorPanel(self.f2, log)
        self.inspector = InspectorPanel(self.f1, self.propeditor, log)

    def show(self, root_obj, visible=True):
        self.inspector.load_object(root_obj)
        self.f1.Show(visible)
        self.f2.Show(visible)

#----------------------------------------------------------------------

if __name__ == '__main__':
    import sys,os
    app = wx.App(redirect=None)

    from gui.controls import Button, Label, TextBox, CheckBox, ListBox, ComboBox
    from gui.windows import Window

    w = Window(title="hello world", name="frmTest", tool_window=False,
               resizable=True, visible=False, pos=(180, 0))

    o = Button(w, name="btnTest", label="click me!", default=True)
    o = Label(w, name="lblTest", alignment="right", size=(-1, 500), text="hello!")
    o = TextBox(w, name="txtTest", border=False, text="hello world!")
    o = CheckBox(w, name="chkTest", border='none', label="Check me!")
    o = ListBox(w, name="lstTest", border='none',
                items={'datum1': 'a', 'datum2':'b', 'datum3':'c'},
                multiselect="--multiselect" in sys.argv)
    o = ComboBox(w, name="cboTest",
                items={'datum1': 'a', 'datum2':'b', 'datum3':'c'},
                readonly='--readonly' in sys.argv,
                )
    w.show()

    InspectorTool().show(w)

    app.MainLoop()


#!/usr/bin/python
# -*- coding: utf-8 -*-

"Timer support "

__author__ = "Mariano Reingart (reingart@gmail.com)"
__copyright__ = "Copyright (C) 2016- Mariano Reingart"
__license__ = "LGPL 3.0"

import wx

from .component import SubComponent, Spec
from . import images
from . import registry

class Timer(SubComponent):
    "A simple timer to execute code at specified intervals"
    _wx_class = wx.Timer
    #_image = images.statusbar
    _registry = registry.MISC

    def __init__(self, parent=None, **kwargs):
        "Custom constructor that just creates the wx.Timer instance"
        self.wx_obj = self._wx_class(None)
        self.set_parent(parent, init=True)

    def set_parent(self, new_parent, init=False):
        "Re-parent a child control with the new wx_obj parent (owner)"
        ##SubComponent.set_parent(self, new_parent, init)
        self.wx_obj.SetOwner(new_parent.wx_obj.GetEventHandler())

    def stop(self):
        self.wx_obj.Stop()

    def start(self, interval=-1):
        self.wx_obj.Start(interval)

    interval = Spec(lambda self: self.wx_obj.GetInterval(),
                lambda self, value: self.wx_obj.Start(value),
                default=1000, type="integer",
                doc="current interval for the timer (in milliseconds)")

if __name__ == "__main__":
    from gui.windows import Window
    app = wx.App(redirect=False)

    w = Window(title="hello world", name="frmTest", tool_window=False,
               resizable=True, visible=False, pos=(180, 0))
    t = Timer(w)
    def timer(evt):
        print("timer!")
    w.ontimer = timer
    t.interval = 100
    w.show()
    app.MainLoop()


"""
An image serializer field for use with sorl and Django REST Framework.

Provides an easy way of exposing a scaled version of an image rather than the
full-size one, and if you prefer many different versions (thumb, large etc.)

Example Usage:

    # urls.py
    from django.conf.urls import url, include
    from models import TestModel
    from rest_framework import routers, serializers, viewsets
    from sorl_thumbnail_serializer.fields import HyperlinkedSorlImageField

    class TestModelSerializer(serializers.HyperlinkedModelSerializer):
        class Meta:
            model = TestModel

        # A thumbnail image, sorl options and read-only
        thumbnail = HyperlinkedSorlImageField(
            '128x128',
            options={"crop": "center"},
            source='image',
            read_only=True
        )
        # A larger version of the image, allows writing
        image = HyperlinkedSorlImageField('1024')

    class TestModelViewSet(viewsets.ModelViewSet):
        queryset = TestModel.objects.all()
        serializer_class = TestModelSerializer

    router = routers.DefaultRouter()
    router.register(r'test_models', TestModelViewSet)

    urlpatterns = [
        url(r'^', include(router.urls)),
    ]
"""

from rest_framework import serializers
from sorl.thumbnail import get_thumbnail

class HyperlinkedSorlImageField(serializers.ImageField):

    """A Django REST Framework Field class returning hyperlinked scaled and cached images."""

    def __init__(self, geometry_string, options={}, *args, **kwargs):
        """
        Create an instance of the HyperlinkedSorlImageField image serializer.

        Args:
            geometry_string (str): The size of your cropped image.
            options (Optional[dict]): A dict of sorl options.
            *args: (Optional) Default serializers.ImageField arguments.
            **kwargs: (Optional) Default serializers.ImageField keyword
            arguments.

        For a description of sorl geometry strings and additional sorl options,
        please see https://sorl-thumbnail.readthedocs.org/en/latest/examples.html?highlight=geometry#low-level-api-examples
        """  # NOQA
        self.geometry_string = geometry_string
        self.options = options

        super(HyperlinkedSorlImageField, self).__init__(*args, **kwargs)

    def to_representation(self, value):
        """
        Perform the actual serialization.

        Args:
            value: the image to transform
        Returns:
            a url pointing at a scaled and cached image
        """
        if not value:
            return None

        image = get_thumbnail(value, self.geometry_string, **self.options)

        try:
            request = self.context.get('request', None)
            return request.build_absolute_uri(image.url)
        except:
            try:
                return super(HyperlinkedSorlImageField, self).to_representation(image)
            except AttributeError:  # NOQA
                return super(HyperlinkedSorlImageField, self).to_native(image.url)  # NOQA
    to_native = to_representation


from contextlib import contextmanager

from capybara.selector.expression_filter import ExpressionFilter
from capybara.selector.filter_set import filter_sets
from capybara.selector.node_filter import NodeFilter
from capybara.utils import setter_decorator

selectors = {}
# Dict[str, Selector]: A dictionary of :class:`Selector` objects keyed by name. """

class Selector(object):
    """
    A callable object used for selecting elements in a document or node.

    Args:
        name (str): The name of the selector.
        label (str, optional): The label to use when describing this selector.
        descriptions (List[Callable[[Dict[str, Any]], str]]): Functions that build a description of
            the given filter options.
        css (Callable[[str], str], optional): A function to generate a CSS selector given a locator
            string.
        xpath (Callable[[str], str], optional): A function to generate an XPath query given a
            locator string.
        filters (Dict[str, AbstractFilter]): A dictionary of filters this selector should use to
            identify matching elements. Defaults to {}.
    """

    def __init__(self, name, label=None, descriptions=None, css=None, xpath=None, filters=None):
        self.name = name
        self.label = label
        self.descriptions = descriptions or []
        self.css = css
        self.xpath = xpath
        self.format = "xpath" if xpath else "css"
        self.filters = filters or {}

    def __call__(self, locator):
        assert self.format, "selector has no format"
        return getattr(self, self.format)(locator)

    def description(self, options):
        """
        Returns a description of the given filter options relevant to this selector.

        Args:
            options (Dict[str, Any]): The filter options to describe.

        Returns:
            str: A description of the filter options.
        """

        return "".join([describe(options) for describe in self.descriptions])

    @property
    def expression_filters(self):
        """ Dict[str, ExpressionFilter]: Returns the expression filters for this selector. """

        return {
            name: filter for name, filter in iter(self.filters.items())
            if isinstance(filter, ExpressionFilter)}

    @property
    def node_filters(self):
        """ Dict[str, NodeFilter]: Returns the node filters for this selector. """

        return {
            name: filter for name, filter in iter(self.filters.items())
            if isinstance(filter, NodeFilter)}

class SelectorFactory(object):
    """
    A factory for configuring and building :class:`Selector` instances.

    Args:
        name (str): The name of the selector.
    """

    def __init__(self, name):
        self.name = name
        self.label = None
        self.descriptions = []
        self.func = None
        self.format = None
        self.filters = {}

    def describe(self, func):
        """
        Decorates a function that builds a description of some selector options.

        Args:
            func (Callable[[Dict[str, Any]], str]): The description builder function.
        """

        self.descriptions.append(func)

    @setter_decorator
    def css(self, func):
        """
        Sets the given function as the CSS selector generation function.

        Args:
            func (Callable[[str], str]): The CSS selector generation function.
        """

        self.func = func
        self.format = "css"

    @setter_decorator
    def xpath(self, func):
        """
        Sets the given function as the XPath query generation function.

        Args:
            func (Callable[[str], str]): The XPath query generation function.
        """

        self.func = func
        self.format = "xpath"

    def expression_filter(self, name, **kwargs):
        """
        Returns a decorator function for adding an expression filter.

        Args:
            name (str): The name of the filter.
            **kwargs: Variable keyword arguments for the filter.

        Returns:
            Callable[[Callable[[AbstractExpression, Any], AbstractExpression]]]: A decorator
                function for adding an expression filter.
        """

        def decorator(func):
            self.filters[name] = ExpressionFilter(name, func, **kwargs)

        return decorator

    def node_filter(self, name, **kwargs):
        """
        Returns a decorator function for adding a node filter.

        Args:
            name (str): The name of the filter.
            **kwargs: Variable keyword arguments for the filter.

        Returns:
            Callable[[Callable[[Element, Any], bool]]]: A decorator function for adding a node
                filter.
        """

        def decorator(func):
            self.filters[name] = NodeFilter(name, func, **kwargs)

        return decorator

    def filter_set(self, name):
        """
        Adds filters from a particular global :class:`FilterSet`.

        Args:
            name (str): The name of the set whose filters should be added.
        """

        filter_set = filter_sets[name]
        for name, filter in iter(filter_set.filters.items()):
            self.filters[name] = filter
        self.descriptions += filter_set.descriptions

    def build_selector(self):
        """ Selector: Returns a new :class:`Selector` instance with the current configuration. """

        kwargs = {
            'label': self.label,
            'descriptions': self.descriptions,
            'filters': self.filters}
        if self.format == "xpath":
            kwargs['xpath'] = self.func
        if self.format == "css":
            kwargs['css'] = self.func

        return Selector(self.name, **kwargs)

@contextmanager
def add_selector(name):
    """
    Builds and registers a :class:`Selector` object with the given name and configuration.

    Args:
        name (str): The name of the selector.

    Yields:
        SelectorFactory: The factory that will build the :class:`Selector`.
    """

    factory = SelectorFactory(name)
    yield factory
    selectors[name] = factory.build_selector()

def remove_selector(name):
    """
    Unregisters selector with the given name.

    Args:
        name (str): The name of the selector.
    """

    selectors.pop(name, None)


from capybara.helpers import desc, toregex
from capybara.queries.base_query import BaseQuery

class StyleQuery(BaseQuery):
    """
    Queries for computed style values of a node.

    Args:
        expected_styles (Dict[str, str]): The expected style names and values.
        wait (bool | int | float, optional): Whether and how long to wait for synchronization.
            Defaults to :data:`capybara.default_max_wait_time`.
    """

    def __init__(self, expected_styles, wait=None):
        self.expected_styles = expected_styles
        self.actual_styles = {}
        self.options = {
            "wait": wait}
        self.node = None

    @property
    def wait(self):
        """ int | float: How long to wait for synchronization. """
        return self.normalize_wait(self.options["wait"])

    def resolves_for(self, node):
        """
        Resolves this query relative to the given node.

        Args:
            node (node.Base): The node to be evaluated.

        Returns:
            int: The number of matches found.
        """

        self.node = node
        self.actual_styles = node.style(*self.expected_styles.keys())

        return all(
            toregex(value).search(self.actual_styles[style])
            for style, value in iter(self.expected_styles.items()))

    @property
    def failure_message(self):
        """ str: A message describing the query failure. """
        return (
            "Expected node to have styles {expected}. "
            "Actual styles were {actual}").format(
                expected=desc(self.expected_styles),
                actual=desc(self.actual_styles))


from capybara.exceptions import ExpectationNotMet
from capybara.queries.current_path_query import CurrentPathQuery

class SessionMatchersMixin(object):
    def assert_current_path(self, path, **kwargs):
        """
        Asserts that the page has the given path. By default this will compare against the
        path+query portion of the full URL.

        Args:
            path (str | RegexObject): The string or regex that the current "path" should match.
            **kwargs: Arbitrary keyword arguments for :class:`CurrentPathQuery`.

        Returns:
            True

        Raises:
            ExpectationNotMet: If the assertion hasn't succeeded during the wait time.
        """

        query = CurrentPathQuery(path, **kwargs)

        @self.document.synchronize
        def assert_current_path():
            if not query.resolves_for(self):
                raise ExpectationNotMet(query.failure_message)
        assert_current_path()

        return True

    def assert_no_current_path(self, path, **kwargs):
        """
        Asserts that the page doesn't have the given path.

        Args:
            path (str | RegexObject): The string or regex that the current "path" should match.
            **kwargs: Arbitrary keyword arguments for :class:`CurrentPathQuery`.

        Returns:
            True

        Raises:
            ExpectationNotMet: If the assertion hasn't succeeded during the wait time.
        """

        query = CurrentPathQuery(path, **kwargs)

        @self.document.synchronize
        def assert_no_current_path():
            if query.resolves_for(self):
                raise ExpectationNotMet(query.negative_failure_message)

        assert_no_current_path()

        return True

    def has_current_path(self, path, **kwargs):
        """
        Checks if the page has the given path.

        Args:
            path (str | RegexObject): The string or regex that the current "path" should match.
            **kwargs: Arbitrary keyword arguments for :class:`CurrentPathQuery`.

        Returns:
            bool: Whether it matches.
        """

        try:
            return self.assert_current_path(path, **kwargs)
        except ExpectationNotMet:
            return False

    def has_no_current_path(self, path, **kwargs):
        """
        Checks if the page doesn't have the given path.

        Args:
            path (str | RegexObject): The string or regex that the current "path" should match.
            **kwargs: Arbitrary keyword arguments for :class:`CurrentPathQuery`.

        Returns:
            bool: Whether it doesn't match.
        """

        try:
            return self.assert_no_current_path(path, **kwargs)
        except ExpectationNotMet:
            return False


from warnings import warn

import capybara
from capybara.node.base import Base, synchronize

class Element(Base):
    """
    An :class:`Element` represents a single element on the page. It is possible to interact with the
    contents of this element the same as with a document::

        session = Session("selenium", my_app)

        bar = session.find("#bar")       # from capybara.node.finders.FindersMixin
        bar.select("Baz", field="Quox")  # from capybara.node.actions.ActionsMixin

    :class:`Element` also has access to HTML attributes and other properties of the element::

        bar.value
        bar.text
        bar["title"]
    """

    def __init__(self, session, base, query_scope, query):
        super(type(self), self).__init__(session, base)
        self.query_scope = query_scope
        self.query = query
        self._scope = None

    def __repr__(self):
        try:
            return "<capybara.node.element.Element tag=\"{tag}\" path=\"{path}\">".format(
                tag=self.base.tag_name,
                path=self.base.path)
        except Exception as e:
            if isinstance(e, self.session.driver.invalid_element_errors):
                return "Obsolete <capybara.node.element.Element>"
            else:
                raise e

    @property
    def native(self):
        """ object: The native element from the driver. """
        return self.base.native

    def execute_script(self, script, *args):
        self.session.execute_script("""
          (function() {{
            {script}
          }}).apply(arguments[0], Array.prototype.slice.call(arguments, 1));
        """.format(script=script), self, *args)

    def evaluate_script(self, script, *args):
        return self.session.evaluate_script("""
          (function() {{
            return {script}
          }}).apply(arguments[0], Array.prototype.slice.call(arguments, 1));
        """.format(script=script.strip()), self, *args)

    def evaluate_async_script(self, script, *args):
        return self.session.evaluate_async_script("""
          (function() {{
            {script}
          }}).apply(arguments[0], Array.prototype.slice.call(arguments, 1));
        """.format(script=script), self, *args)

    def reload(self):
        if self.allow_reload:
            query_scope = self.query_scope.reload()
            reloaded = query_scope.find_first(
                self.query.name, self.query.locator, **self.query.kwargs)
            if reloaded:
                self.base = reloaded.base
        return self

    @property
    @synchronize
    def tag_name(self):
        """ str: The tag name of the element. """
        return self.base.tag_name

    @property
    @synchronize
    def visible(self):
        """ bool: Whether or not the element is visible. """
        return self.base.visible

    @synchronize
    def style(self, *styles):
        """
        Retrieve the given CSS styles. ::

            element.style("color", "font-size")  # => Computed values of CSS "color" and "font-size" styles

        Args:
            *styles (str): Names of the desired CSS properties.

        Returns:
            Dict[str, str]: Dictionary of the CSS property names to computed values.
        """

        return self.base.style(styles)

    @property
    @synchronize
    def value(self):
        """ str: The value of the form element. """
        return self.base.value

    @property
    def text(self):
        """
        Retrieve the text of the element. If :data:`capybara.ignore_hidden_elements` is ``True``,
        which it is by default, then this will return only text which is visible. The exact
        semantics of this may differ between drivers, but generally any text within elements with
        ``display: none`` is ignored.

        Returns:
            str: The text of the element.
        """

        if capybara.ignore_hidden_elements or capybara.visible_text_only:
            return self.visible_text
        else:
            return self.all_text

    @property
    @synchronize
    def all_text(self):
        """ str: All of the text of the element. """
        return self.base.all_text

    @property
    @synchronize
    def visible_text(self):
        """ str: Only the visible text of the element. """
        return self.base.visible_text

    @property
    @synchronize
    def checked(self):
        """ bool: Whether or not the element is checked. """
        return self.base.checked

    @property
    @synchronize
    def selected(self):
        """ bool: Whether or not the element is selected. """
        return self.base.selected

    @property
    @synchronize
    def disabled(self):
        """ bool: Whether or not the element is disabled. """
        return self.base.disabled

    @property
    @synchronize
    def readonly(self):
        """ bool: Whether or not the element is readonly. """
        return self.base.readonly

    @property
    @synchronize
    def multiple(self):
        """ bool: Whether or not the element supports multiple results. """
        return self.base.multiple

    @property
    @synchronize
    def path(self):
        """ str: An XPath expression describing where on the page the element can be found. """
        return self.base.path

    @synchronize
    def __getitem__(self, name):
        """
        Retrieve the given attribute. ::

            element["title"]  # => HTML title attribute

        Args:
            name (str): The attribute to retrieve.

        Returns:
            str: The value of the attribute.
        """

        return self.base[name]

    @synchronize
    def click(self, *keys, **offset):
        """
        Click the element.

        Both ``x`` and ``y`` must be specified if an offset is wanted. If not specified, the click
        will occur at the middle of the element.

        Args:
            keys (List[Keys], optional): Keys to be held down when clicking.
            offset (Dict[str, int], optional): X- and Y-coordinates to offset the click location
                from the toop left corner of the element.
        """

        self.base.click(*keys, **offset)

    @synchronize
    def double_click(self, *keys, **offset):
        """
        Double-click the element.

        Both ``x`` and ``y`` must be specified if an offset is wanted. If not specified, the click
        will occur at the middle of the element.

        Args:
            keys (List[Keys], optional): Keys to be held down when clicking.
            offset (Dict[str, int], optional): X- and Y-coordinates to offset the click location
                from the toop left corner of the element.
        """
        self.base.double_click(*keys, **offset)

    @synchronize
    def drag_to(self, node):
        """
        Drag the element to the given other element. ::

            source = page.find("#foo")
            target = page.find("#bar")
            source.drag_to(target)

        Args:
            node (Element): The element to drag to.
        """

        self.base.drag_to(node.base)

    @synchronize
    def send_keys(self, *args):
        """
        Send keystrokes to the element.

        Examples::

            from selenium.webdriver.common.keys import Keys

            element.send_keys("foo")                  # => value: "foo"
            element.send_keys("tet", Keys.LEFT, "s")  # => value: "test"

        Args:
            *args: Variable length list of keys to send.
        """

        self.base.send_keys(*args)

    @synchronize
    def hover(self):
        """ Hover on the element. """
        self.base.hover()

    @synchronize
    def right_click(self, *keys, **offset):
        """
        Right-click the element.

        Both ``x`` and ``y`` must be specified if an offset is wanted. If not specified, the click
        will occur at the middle of the element.

        Args:
            keys (List[Keys], optional): Keys to be held down when clicking.
            offset (Dict[str, int], optional): X- and Y-coordinates to offset the click location
                from the toop left corner of the element.
        """
        self.base.right_click(*keys, **offset)

    @synchronize
    def select_option(self):
        """ Select this node if it is an option element inside a select tag. """
        if self.disabled:
            warn("Attempt to select disabled option: {}".format(self.value or self.text))
        self.base.select_option()

    @synchronize
    def set(self, value, **options):
        """
        Set the value of the form element to the given value.

        Args:
            value (bool | str): The new value.
            **options: Driver-specific options for how to set the value.
        """

        self.base.set(value, **options)

    @synchronize
    def unselect_option(self):
        """ Unselect this node if it is an option element inside a multiple select tag. """
        self.base.unselect_option()


from warnings import warn

from capybara.selector.abstract_filter import AbstractFilter

class ExpressionFilter(AbstractFilter):
    def apply_filter(self, expr, value):
        """
        Returns the given expression filtered by the given value.

        Args:
            expr (xpath.expression.AbstractExpression): The expression to filter.
            value (object): The desired value with which the expression should be filtered.

        Returns:
            xpath.expression.AbstractExpression: The filtered expression.
        """

        if self.skip(value):
            return expr

        if not self._valid_value(value):
            msg = "Invalid value {value} passed to filter {name} - ".format(
                value=repr(value),
                name=self.name)

            if self.default is not None:
                warn(msg + "defaulting to {}".format(self.default))
                value = self.default
            else:
                warn(msg + "skipping")
                return expr

        return self.func(expr, value)


from selenium import webdriver

def get_browser(browser_name, capabilities=None, **options):
    """
    Returns an instance of the given browser with the given capabilities.

    Args:
        browser_name (str): The name of the desired browser.
        capabilities (Dict[str, str | bool], optional): The desired capabilities of the browser.
            Defaults to None.
        options: Arbitrary keyword arguments for the browser-specific subclass of
            :class:`webdriver.Remote`.

    Returns:
        WebDriver: An instance of the desired browser.
    """

    if browser_name == "chrome":
        return webdriver.Chrome(desired_capabilities=capabilities, **options)
    if browser_name == "edge":
        return webdriver.Edge(capabilities=capabilities, **options)
    if browser_name in ["ff", "firefox"]:
        return webdriver.Firefox(capabilities=capabilities, **options)
    if browser_name in ["ie", "internet_explorer"]:
        return webdriver.Ie(capabilities=capabilities, **options)
    if browser_name == "phantomjs":
        return webdriver.PhantomJS(desired_capabilities=capabilities, **options)
    if browser_name == "remote":
        return webdriver.Remote(desired_capabilities=capabilities, **options)
    if browser_name == "safari":
        return webdriver.Safari(desired_capabilities=capabilities, **options)

    raise ValueError("unsupported browser: {}".format(repr(browser_name)))


from collections import Hashable
from functools import reduce
import re
from xpath.expression import AbstractExpression
from xpath.renderer import to_xpath

import capybara
from capybara.compat import bytes_, str_
from capybara.helpers import desc, normalize_text, toregex
from capybara.queries.base_query import BaseQuery
from capybara.result import Result
from capybara.selector import selectors
from capybara.utils import isregex

VALID_MATCH = ["first", "one", "prefer_exact", "smart"]

class SelectorQuery(BaseQuery):
    """
    Queries for elements using a selector.

    If no locator is provided and the given selector is not a valid selector, the first
    argument is assumed to be the locator and the default selector will be used.

    Args:
        selector (str): The name of the selector to use.
        locator (str): An identifying string to use to locate desired elements.
        between (Iterable[int], optional): A range of acceptable counts.
        count (int, optional): The number of times the selector should match. Defaults to any number
            of times greater than zero.
        exact (bool, optional): Whether to exactly match the locator string. Defaults to
            :data:`capybara.exact`.
        exact_text (bool | str, optional): Whether to exactly match the text, or the exact text to
            match. Defaults to False.
        filter (Callable[[Element], bool], optional): A function for filtering results.
        match (str, optional): The matching strategy to use. Defaults to :data:`capybara.match`.
        maximum (int, optional): The maximum number of times the selector should match. Defaults to
            infinite.
        minimum (int, optional): The minimum number of times the selector should match. Defaults to
            1.
        text (str | RegexObject, optional): Text that should be contained in matched elements.
        visible (bool | str, optional): The desired element visibility. Defaults to
            :data:`capybara.ignore_hidden_elements`.
        wait (bool | int | float, optional): Whether and how long to wait for synchronization.
            Defaults to :data:`capybara.default_max_wait_time`.
        **filter_options: Arbitrary keyword arguments for the selector's filters.
    """

    def __init__(self, selector, locator=None, between=None, count=None, exact=None,
                 exact_text=None, filter=None, match=None, maximum=None, minimum=None, text=None,
                 visible=None, wait=None, **filter_options):
        if locator is None and (
            not isinstance(selector, Hashable) or
            selector not in selectors
        ):
            locator = selector
            selector = capybara.default_selector

        self.selector = selectors[selector]
        self.expression = self.selector(locator)
        self.locator = locator
        self.options = {
            "between": between,
            "count": count,
            "exact": exact,
            "exact_text": exact_text,
            "filter": filter,
            "match": match,
            "maximum": maximum,
            "minimum": minimum,
            "text": text,
            "visible": visible,
            "wait": wait}
        self.filter_options = filter_options

        assert self.match in VALID_MATCH, \
            "invalid option {match} for match, should be one of {valid_values}".format(
                match=desc(self.match),
                valid_values=", ".join(desc(VALID_MATCH)))

    @property
    def name(self):
        """ str: The name of selector. """
        return self.selector.name

    @property
    def label(self):
        """ str: A short description of the selector. """
        return self.selector.label or self.selector.name

    @property
    def kwargs(self):
        """ Dict[str, Any]: The keyword arguments with which this query was initialized. """
        kwargs = {}
        kwargs.update(self.options)
        kwargs.update(self.filter_options)
        return kwargs

    @property
    def description(self):
        """ str: A long description of this query. """

        description = self.label

        if self.locator:
            description += " {}".format(desc(self.locator))
        if self.options["text"] is not None:
            description += " with text {}".format(desc(self.options["text"]))

        description += self.selector.description(self.filter_options)

        return description

    @property
    def exact(self):
        """ bool: Whether to exactly match the locator string. """
        if self.options["exact"] is not None:
            return self.options["exact"]
        else:
            return capybara.exact

    @property
    def exact_text(self):
        return self.options["exact_text"]

    @property
    def match(self):
        """ str: The matching strategy to use. """
        if self.options["match"] is not None:
            return self.options["match"]
        else:
            return capybara.match

    @property
    def visible(self):
        """ str: The desired element visibility. """
        if self.options["visible"] is not None:
            if self.options["visible"] is True:
                return "visible"
            elif self.options["visible"] is False:
                return "all"
            else:
                return self.options["visible"]
        else:
            if capybara.ignore_hidden_elements:
                return "visible"
            else:
                return "all"

    @property
    def wait(self):
        """ int | float: How long to wait for synchronization. """
        return self.normalize_wait(self.options["wait"])

    def css(self):
        """ str: The CSS query for this selector. """
        return self.expression

    def xpath(self, exact=None):
        """
        Returns the XPath query for this selector.

        Args:
            exact (bool, optional): Whether to exactly match text.

        Returns:
            str: The XPath query for this selector.
        """

        exact = exact if exact is not None else self.exact

        if isinstance(self.expression, AbstractExpression):
            expression = self._apply_expression_filters(self.expression)

            return to_xpath(expression, exact=exact)
        else:
            return str_(self.expression)

    def resolve_for(self, node, exact=None):
        """
        Resolves this query relative to the given node.

        Args:
            node (node.Base): The node relative to which this query should be resolved.
            exact (bool, optional): Whether to exactly match text.

        Returns:
            list[Element]: A list of elements matched by this query.
        """

        from capybara.driver.node import Node
        from capybara.node.element import Element
        from capybara.node.simple import Simple

        @node.synchronize
        def resolve():
            if self.selector.format == "css":
                children = node._find_css(self.css())
            else:
                children = node._find_xpath(self.xpath(exact))

            def wrap(child):
                if isinstance(child, Node):
                    return Element(node.session, child, node, self)
                else:
                    return Simple(child)

            children = [wrap(child) for child in children]

            return Result(children, self)

        return resolve()

    def matches_filters(self, node):
        """
        Returns whether the given node matches all filters.

        Args:
            node (Element): The node to evaluate.

        Returns:
            bool: Whether the given node matches.
        """

        visible = self.visible

        if self.options["text"]:
            if isregex(self.options["text"]):
                regex = self.options["text"]
            elif self.exact_text is True:
                regex = re.compile(r"\A{}\Z".format(re.escape(self.options["text"])))
            else:
                regex = toregex(self.options["text"])

            text = normalize_text(
                node.all_text if visible == "all" else node.visible_text)

            if not regex.search(text):
                return False

        if isinstance(self.exact_text, (bytes_, str_)):
            regex = re.compile(r"\A{}\Z".format(re.escape(self.exact_text)))

            text = normalize_text(
                node.all_text if visible == "all" else node.visible_text)

            if not regex.search(text):
                return False

        if visible == "visible":
            if not node.visible:
                return False
        elif visible == "hidden":
            if node.visible:
                return False

        for name, node_filter in iter(self._node_filters.items()):
            if name in self.filter_options:
                if not node_filter.matches(node, self.filter_options[name]):
                    return False
            elif node_filter.has_default:
                if not node_filter.matches(node, node_filter.default):
                    return False

        if self.options["filter"] and not self.options["filter"](node):
            return False

        return True

    def _apply_expression_filters(self, expr):
        def apply_filter(memo, item):
            name, ef = item

            if name in self.filter_options:
                return ef.apply_filter(memo, self.filter_options[name])
            elif ef.has_default:
                return ef.apply_filter(memo, ef.default)
            else:
                return memo

        return reduce(apply_filter, iter(self._expression_filters.items()), expr)

    @property
    def _expression_filters(self):
        return self.selector.expression_filters

    @property
    def _node_filters(self):
        return self.selector.node_filters


from collections import Hashable
from contextlib import contextmanager
from datetime import datetime
from functools import wraps
import os
import random

import capybara
from capybara.compat import ParseResult, urlparse
from capybara.driver.node import Node
from capybara.exceptions import ScopeError, WindowError
from capybara.node.base import Base
from capybara.node.document import Document
from capybara.node.element import Element
from capybara.selector import selectors
from capybara.server import Server
from capybara.session_matchers import SessionMatchersMixin
from capybara.utils import cached_property, encode_string
from capybara.window import Window

_DOCUMENT_METHODS = ["assert_no_title", "assert_title", "has_no_title", "has_title"]
_DOCUMENT_PROPERTIES = ["title"]
_NODE_METHODS = [
    "assert_all_of_selectors", "assert_no_selector", "assert_none_of_selectors", "assert_no_text",
    "assert_selector", "assert_style", "assert_text", "attach_file", "check", "choose",
    "click_button", "click_link", "click_link_or_button", "click_on", "fill_in", "find", "find_all",
    "find_button", "find_by_id", "find_field", "find_first", "find_link", "has_all_of_selectors",
    "has_button", "has_checked_field", "has_content", "has_css", "has_field", "has_link",
    "has_no_button", "has_no_checked_field", "has_no_css", "has_no_field", "has_no_link",
    "has_no_select", "has_no_selector", "has_no_table", "has_no_text", "has_no_unchecked_field",
    "has_no_xpath", "has_none_of_selectors", "has_select", "has_selector", "has_table", "has_text",
    "has_unchecked_field", "has_xpath", "select", "uncheck", "unselect"]
_NODE_PROPERTIES = ["text"]
_SESSION_METHODS = [
    "accept_alert", "accept_confirm", "accept_prompt", "assert_current_path",
    "assert_no_current_path", "dismiss_confirm", "dismiss_prompt", "evaluate_async_script",
    "evaluate_script", "execute_script", "fieldset", "frame", "go_back", "go_forward",
    "has_current_path", "has_no_current_path", "open_new_window", "refresh", "reset", "save_page",
    "save_screenshot", "scope", "switch_to_frame", "switch_to_window", "table", "visit", "window",
    "window_opened_by"]
_SESSION_PROPERTIES = ["current_host", "current_path", "current_url", "current_window", "windows"]

DSL_METHODS = _DOCUMENT_METHODS + _NODE_METHODS + _SESSION_METHODS

class Session(SessionMatchersMixin, object):
    """
    The Session class represents a single user's interaction with the system. The Session can use
    any of the underlying drivers. A session can be initialized manually like this::

        session = Session("selenium", MyWSGIApp)

    The application given as the second argument is optional. When running Capybara against an
    external page, you might want to leave it out::

        session = Session("selenium")
        session.visit("http://www.google.com")

    Session provides a number of methods and properties for controlling the navigation of the page,
    such as :meth:`visit`, :attr:`current_path`, and so on. It also delegates a number of methods to
    a :class:`Document`, representing the current HTML document. This allows interaction::

        session.fill_in("q", value="Capybara")
        session.click_button("Search")
        assert session.has_text("Capybara")

    When using :mod:`capybara.dsl`, the Session is initialized automatically for you.

    Args:
        mode (str): The name of the driver to use.
        app (object): The WSGI-compliant app with which to interact.
    """

    def __init__(self, mode, app):
        self.mode = mode
        self.app = app
        self.server = Server(app).boot() if app and self.driver.needs_server else None
        self.synchronized = False
        self._scopes = [None]

    @cached_property
    def driver(self):
        """ driver.Base: The driver for the current session. """
        return capybara.drivers[self.mode](self.app)

    @cached_property
    def document(self):
        """ Document: The document for the current page. """
        return Document(self, self.driver)

    @property
    def current_scope(self):
        """ node.Base: The current node relative to which all interaction will be scoped. """
        scope = self._scopes[-1]
        if scope in [None, "frame"]:
            scope = self.document
        return scope

    @property
    def html(self):
        """ str: A snapshot of the DOM of the current document, as it looks right now. """
        return self.driver.html

    body = html
    """ Alias for :attr:`html`. """

    source = html
    """ Alias for :attr:`html`. """

    @property
    def current_path(self):
        """ str: Path of the current page, without any domain information. """

        if not self.current_url:
            return

        path = urlparse(self.current_url).path
        return path if path else None

    @property
    def current_host(self):
        """ str: Host of the current page. """

        if not self.current_url:
            return

        result = urlparse(self.current_url)
        scheme, netloc = result.scheme, result.netloc
        host = netloc.split(":")[0] if netloc else None
        return "{0}://{1}".format(scheme, host) if host else None

    @property
    def current_url(self):
        """ str: Fully qualified URL of the current page. """
        return self.driver.current_url

    def visit(self, visit_uri):
        """
        Navigate to the given URL. The URL can either be a relative URL or an absolute URL. The
        behavior of either depends on the driver. ::

            session.visit("/foo")
            session.visit("http://google.com")

        For drivers which can run against an external application, such as the Selenium driver,
        giving an absolute URL will navigate to that page. This allows testing applications running
        on remote servers. For these drivers, setting :data:`capybara.app_host` will make the
        remote server the default. For example::

            capybara.app_host = "http://google.com"
            session.visit("/")  # visits the Google homepage

        Args:
            visit_uri (str): The URL to navigate to.
        """

        self.raise_server_error()

        visit_uri = urlparse(visit_uri)

        if capybara.app_host:
            uri_base = urlparse(capybara.app_host)
        elif self.server:
            uri_base = urlparse("http://{}:{}".format(self.server.host, self.server.port))
        else:
            uri_base = None

        visit_uri = ParseResult(
            scheme=visit_uri.scheme or (uri_base.scheme if uri_base else ""),
            netloc=visit_uri.netloc or (uri_base.netloc if uri_base else ""),
            path=visit_uri.path,
            params=visit_uri.params,
            query=visit_uri.query,
            fragment=visit_uri.fragment)

        self.driver.visit(visit_uri.geturl())

    def refresh(self):
        """ Refresh the page. """
        self.raise_server_error()
        self.driver.refresh()

    def go_back(self):
        """ Move back a single entry in the browser's history. """
        self.driver.go_back()

    def go_forward(self):
        """ Move forward a single entry in the browser's history. """
        self.driver.go_forward()

    @contextmanager
    def scope(self, *args, **kwargs):
        """
        Executes the wrapped code within the context of a node. ``scope`` takes the same options
        as :meth:`find`. For the duration of the context, any command to Capybara will be handled
        as though it were scoped to the given element. ::

            with scope("xpath", "//div[@id='delivery-address']"):
                fill_in("Street", value="12 Main Street")

        Just as with :meth:`find`, if multiple elements match the selector given to ``scope``, an
        error will be raised, and just as with :meth:`find`, this behavior can be controlled
        through the ``match`` and ``exact`` options.

        It is possible to omit the first argument, in that case, the selector is assumed to be of
        the type set in :data:`capybara.default_selector`. ::

            with scope("div#delivery-address"):
                fill_in("Street", value="12 Main Street")

        Note that a lot of uses of ``scope`` can be replaced more succinctly with chaining::

            find("div#delivery-address").fill_in("Street", value="12 Main Street")

        Args:
            *args: Variable length argument list for the call to :meth:`find`.
            **kwargs: Arbitrary keywords arguments for the call to :meth:`find`.
        """

        new_scope = args[0] if isinstance(args[0], Base) else self.find(*args, **kwargs)
        self._scopes.append(new_scope)
        try:
            yield
        finally:
            self._scopes.pop()

    @contextmanager
    def fieldset(self, locator):
        """
        Execute the wrapped code within a specific fieldset given the id or legend of that
        fieldset.

        Args:
            locator (str): The id or legend of the fieldset.
        """

        with self.scope("fieldset", locator):
            yield

    @contextmanager
    def table(self, locator):
        """
        Execute the wrapped code within a specific table given the id or caption of that table.

        Args:
            locator (str): The id or caption of the table.
        """

        with self.scope("table", locator):
            yield

    @contextmanager
    def frame(self, locator=None, *args, **kwargs):
        """
        Execute the wrapped code within the given iframe using the given frame or frame name/id.
        May not be supported by all drivers.

        Args:
            locator (str | Element, optional): The name/id of the frame or the frame's element.
                Defaults to the only frame in the document.
        """

        self.switch_to_frame(self._find_frame(locator, *args, **kwargs))
        try:
            yield
        finally:
            self.switch_to_frame("parent")

    def _find_frame(self, locator=None, *args, **kwargs):
        if isinstance(locator, Element):
            return locator

        if isinstance(locator, Hashable) and locator in selectors:
            selector = locator
            return self.find(selector, *args, **kwargs)

        return self.find("frame", locator, **kwargs)

    @property
    def current_window(self):
        """ Window: The current window. """
        return Window(self, self.driver.current_window_handle)

    @property
    def windows(self):
        """
        Get all opened windows. The order of the windows in the returned list is not defined. The
        driver may sort windows by their creation time but it's not required.

        Returns:
            List[Window]: A list of all windows.
        """

        return [Window(self, window_handle) for window_handle in self.driver.window_handles]

    def open_new_window(self):
        """
        Open new window. The current window doesn't change as a result of this call. It should be
        switched explicitly.

        Returns:
            Window: The window that has been opened.
        """

        return self.window_opened_by(lambda: self.driver.open_new_window())

    def switch_to_frame(self, frame):
        """
        Switch to the given frame.

        If you use this method you are responsible for making sure you switch back to the parent
        frame when done in the frame changed to. :meth:`frame` is preferred over this method and
        should be used when possible. May not be supported by all drivers.

        Args:
            frame (Element | str): The iframe/frame element to switch to.
        """

        if isinstance(frame, Element):
            self.driver.switch_to_frame(frame)
            self._scopes.append("frame")
        elif frame == "parent":
            if self._scopes[-1] != "frame":
                raise ScopeError("`switch_to_frame(\"parent\")` cannot be called "
                                 "from inside a descendant frame's `scope` context.")
            self._scopes.pop()
            self.driver.switch_to_frame("parent")
        elif frame == "top":
            if "frame" in self._scopes:
                idx = self._scopes.index("frame")
                if any([scope not in ["frame", None] for scope in self._scopes[idx:]]):
                    raise ScopeError("`switch_to_frame(\"top\")` cannot be called "
                                     "from inside a descendant frame's `scope` context.")
                self._scopes = self._scopes[:idx]
                self.driver.switch_to_frame("top")
        else:
            raise ValueError(
                "You must provide a frame element, \"parent\", or \"top\" "
                "when calling switch_to_frame")

    def switch_to_window(self, window, wait=None):
        """
        If ``window`` is a lambda, it switches to the first window for which ``window`` returns a
        value other than False or None. If a window that matches can't be found, the window will be
        switched back and :exc:`WindowError` will be raised.

        Args:
            window (Window | lambda): The window that should be switched to, or a filtering lambda.
            wait (int | float, optional): The number of seconds to wait to find the window.

        Returns:
            Window: The new current window.

        Raises:
            ScopeError: If this method is invoked inside :meth:`scope, :meth:`frame`, or
                :meth:`window`.
            WindowError: If no window matches the given lambda.
        """

        if len(self._scopes) > 1:
            raise ScopeError(
                "`switch_to_window` is not supposed to be invoked from "
                "within `scope`s, `frame`s, or other `window`s.")

        if isinstance(window, Window):
            self.driver.switch_to_window(window.handle)
            return window
        else:
            @self.document.synchronize(errors=(WindowError,), wait=wait)
            def switch_and_get_matching_window():
                original_window_handle = self.driver.current_window_handle
                try:
                    for handle in self.driver.window_handles:
                        self.driver.switch_to_window(handle)
                        result = window()
                        if result:
                            return Window(self, handle)
                except Exception:
                    self.driver.switch_to_window(original_window_handle)
                    raise

                self.driver.switch_to_window(original_window_handle)
                raise WindowError("Could not find a window matching lambda")

            return switch_and_get_matching_window()

    @contextmanager
    def window(self, window):
        """
        This method does the following:

        1. Switches to the given window (it can be located by window instance/lambda/string).
        2. Executes the given block (within window located at previous step).
        3. Switches back (this step will be invoked even if exception happens at second step).

        Args:
            window (Window | lambda): The desired :class:`Window`, or a lambda that will be run in
                the context of each open window and returns ``True`` for the desired window.
        """

        original = self.current_window
        if window != original:
            self.switch_to_window(window)
        self._scopes.append(None)
        try:
            yield
        finally:
            self._scopes.pop()
            if original != window:
                self.switch_to_window(original)

    def window_opened_by(self, trigger_func, wait=None):
        """
        Get the window that has been opened by the passed lambda. It will wait for it to be opened
        (in the same way as other Capybara methods wait). It's better to use this method than
        ``windows[-1]`` `as order of windows isn't defined in some drivers`__.

        __ https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html#h_note_10

        Args:
            trigger_func (func): The function that should trigger the opening of a new window.
            wait (int | float, optional): Maximum wait time. Defaults to
                :data:`capybara.default_max_wait_time`.

        Returns:
            Window: The window that has been opened within the lambda.

        Raises:
            WindowError: If lambda passed to window hasn't opened window or opened more than one
                window.
        """

        old_handles = set(self.driver.window_handles)
        trigger_func()

        @self.document.synchronize(wait=wait, errors=(WindowError,))
        def get_new_window():
            opened_handles = set(self.driver.window_handles) - old_handles
            if len(opened_handles) != 1:
                raise WindowError("lambda passed to `window_opened_by` "
                                  "opened {0} windows instead of 1".format(len(opened_handles)))
            return Window(self, list(opened_handles)[0])

        return get_new_window()

    def execute_script(self, script, *args):
        """
        Execute the given script, not returning a result. This is useful for scripts that return
        complex objects, such as jQuery statements. ``execute_script`` should be used over
        :meth:`evaluate_script` whenever possible.

        Args:
            script (str): A string of JavaScript to execute.
            *args: Variable length argument list to pass to the executed JavaScript string.
        """

        args = [arg.base if isinstance(arg, Base) else arg for arg in args]
        self.driver.execute_script(script, *args)

    def evaluate_script(self, script, *args):
        """
        Evaluate the given JavaScript and return the result. Be careful when using this with
        scripts that return complex objects, such as jQuery statements. :meth:`execute_script`
        might be a better alternative.

        Args:
            script (str): A string of JavaScript to evaluate.
            *args: Variable length argument list to pass to the executed JavaScript string.

        Returns:
            object: The result of the evaluated JavaScript (may be driver specific).
        """

        args = [arg.base if isinstance(arg, Base) else arg for arg in args]
        result = self.driver.evaluate_script(script, *args)
        return self._wrap_element_script_result(result)

    def evaluate_async_script(self, script, *args):
        """
        Evaluate the given JavaScript and obtain the result from a callback function which will be
        passed as the last argument to the script.

        Args:
            script (str): A string of JavaScript to evaluate.
            *args: Variable length argument list to pass to the executed JavaScript string.

        Returns:
            object: The result of the evaluated JavaScript (may be driver specific).
        """

        args = [arg.base if isinstance(arg, Base) else arg for arg in args]
        result = self.driver.evaluate_async_script(script, *args)
        return self._wrap_element_script_result(result)

    @contextmanager
    def accept_alert(self, text=None, wait=None):
        """
        Execute the wrapped code, accepting an alert.

        Args:
            text (str | RegexObject, optional): Text to match against the text in the modal.
            wait (int | float, optional): Maximum time to wait for the modal to appear after
                executing the wrapped code.

        Raises:
            ModalNotFound: If a modal dialog hasn't been found.
        """

        wait = wait or capybara.default_max_wait_time
        with self.driver.accept_modal("alert", text=text, wait=wait):
            yield

    @contextmanager
    def accept_confirm(self, text=None, wait=None):
        """
        Execute the wrapped code, accepting a confirm.

        Args:
            text (str | RegexObject, optional): Text to match against the text in the modal.
            wait (int | float, optional): Maximum time to wait for the modal to appear after
                executing the wrapped code.

        Raises:
            ModalNotFound: If a modal dialog hasn't been found.
        """

        with self.driver.accept_modal("confirm", text=text, wait=wait):
            yield

    @contextmanager
    def dismiss_confirm(self, text=None, wait=None):
        """
        Execute the wrapped code, dismissing a confirm.

        Args:
            text (str | RegexObject, optional): Text to match against the text in the modal.
            wait (int | float, optional): Maximum time to wait for the modal to appear after
                executing the wrapped code.

        Raises:
            ModalNotFound: If a modal dialog hasn't been found.
        """

        with self.driver.dismiss_modal("confirm", text=text, wait=wait):
            yield

    @contextmanager
    def accept_prompt(self, text=None, response=None, wait=None):
        """
        Execute the wrapped code, accepting a prompt, optionally responding to the prompt.

        Args:
            text (str | RegexObject, optional): Text to match against the text in the modal.
            response (str, optional): Response to provide to the prompt.
            wait (int | float, optional): Maximum time to wait for the modal to appear after
                executing the wrapped code.

        Raises:
            ModalNotFound: If a modal dialog hasn't been found.
        """

        with self.driver.accept_modal("prompt", text=text, response=response, wait=wait):
            yield

    @contextmanager
    def dismiss_prompt(self, text=None, wait=None):
        """
        Execute the wrapped code, dismissing a prompt.

        Args:
            text (str | RegexObject, optional): Text to match against the text in the modal.
            wait (int | float, optional): Maximum time to wait for the modal to appear after
                executing the wrapped code.

        Raises:
            ModalNotFound: If a modal dialog hasn't been found.
        """

        with self.driver.dismiss_modal("prompt", text=text, wait=wait):
            yield

    def save_page(self, path=None):
        """
        Save a snapshot of the page.

        If invoked without arguments, it will save a file to :data:`capybara.save_path` and the
        file will be given a randomly generated filename. If invoked with a relative path, the path
        will be relative to :data:`capybara.save_path`.

        Args:
            path (str, optional): The path to where it should be saved.

        Returns:
            str: The path to which the file was saved.
        """

        path = _prepare_path(path, "html")

        with open(path, "wb") as f:
            f.write(encode_string(self.body))

        return path

    def save_screenshot(self, path=None, **kwargs):
        """
        Save a screenshot of the page.

        If invoked without arguments, it will save a file to :data:`capybara.save_path` and the
        file will be given a randomly generated filename. If invoked with a relative path, the path
        will be relative to :data:`capybara.save_path`.

        Args:
            path (str, optional): The path to where it should be saved.
            **kwargs: Arbitrary keywords arguments for the driver.

        Returns:
            str: The path to which the file was saved.
        """

        path = _prepare_path(path, "png")
        self.driver.save_screenshot(path, **kwargs)
        return path

    def reset(self):
        """
        Reset the session (i.e., remove cookies and navigate to a blank page).

        This method does not:
        * accept modal dialogs if they are present (the Selenium driver does, but others may not),
        * clear the browser cache/HTML 5 local storage/IndexedDB/Web SQL database/etc., or
        * modify the state of the driver/underlying browser in any other way

        as doing so would result in performance downsides and it's not needed to do everything
        from the list above for most apps.

        If you want to do anything from the list above on a general basis you can write a test
        teardown method.
        """

        self.driver.reset()
        if self.server:
            self.server.wait_for_pending_requests()
        self.raise_server_error()

    def raise_server_error(self):
        """ Raise errors encountered by the server. """
        if self.server and self.server.error:
            try:
                if capybara.raise_server_errors:
                    raise self.server.error
            finally:
                self.server.reset_error()

    cleanup = reset
    """ Alias for :meth:`reset`. """

    reset_session = reset
    """ Alias for :meth:`reset`. """

    def _wrap_element_script_result(self, arg):
        if isinstance(arg, list):
            return [self._wrap_element_script_result(e) for e in arg]
        elif isinstance(arg, dict):
            return {k: self._wrap_element_script_result(v) for k, v in iter(arg.items())}
        elif isinstance(arg, Node):
            return Element(self, arg, None, None)
        else:
            return arg

def _define_document_method(method_name):
    @wraps(getattr(Document, method_name))
    def func(self, *args, **kwargs):
        return getattr(self.document, method_name)(*args, **kwargs)
    setattr(Session, method_name, func)

def _define_document_property(property_name):
    def fget(self):
        return getattr(self.document, property_name)
    fdoc = getattr(Document, property_name).__doc__
    setattr(Session, property_name, property(fget, None, None, fdoc))

def _define_node_method(method_name):
    @wraps(getattr(Base, method_name))
    def func(self, *args, **kwargs):
        return getattr(self.current_scope, method_name)(*args, **kwargs)
    setattr(Session, method_name, func)

def _define_node_property(property_name):
    def fget(self):
        return getattr(self.current_scope, property_name)
    fdoc = getattr(Base, property_name).__doc__
    setattr(Session, property_name, property(fget, None, None, fdoc))

for method_name in _DOCUMENT_METHODS:
    _define_document_method(method_name)

for property_name in _DOCUMENT_PROPERTIES:
    _define_document_property(property_name)

for method_name in _NODE_METHODS:
    _define_node_method(method_name)

for property_name in _NODE_PROPERTIES:
    _define_node_property(property_name)

def _prepare_path(path, extension):
    save_path = capybara.save_path or os.getcwd()
    path = os.path.normpath(os.path.join(save_path, path or _default_fn(extension)))

    if not os.path.exists(os.path.dirname(path)):
        os.makedirs(os.path.dirname(path))

    return path

def _default_fn(extension):
    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
    return "capybara-{timestamp}{random}.{extension}".format(
        timestamp=timestamp,
        random=random.randint(0, 10**10),
        extension=extension)


from warnings import warn

from capybara.selector.abstract_filter import AbstractFilter

class NodeFilter(AbstractFilter):
    def matches(self, node, value):
        """
        Returns whether the given node matches the filter rule with the given value.

        Args:
            node (Element): The node to filter.
            value (object): The desired value with which the node should be evaluated.

        Returns:
            bool: Whether the given node matches.
        """

        if self.skip(value):
            return True

        if not self._valid_value(value):
            msg = "Invalid value {value} passed to filter {name} - ".format(
                value=repr(value),
                name=self.name)

            if self.default is not None:
                warn(msg + "defaulting to {}".format(self.default))
                value = self.default
            else:
                warn(msg + "skipping")
                return True

        return self.func(node, value)


from glob import glob
import os
from setuptools import setup, find_packages
import sys

tests_require = ["selenium < 4", "werkzeug"]
if sys.version_info < (3, 3):
    tests_require.append("mock")

driver_verification_tests_require = [
    "flaky",
    "flask",
    "py",
    "pytest ~= 3.0, != 3.3.*, < 3.7",
    "werkzeug"]
if sys.version_info < (3, 3):
    driver_verification_tests_require.append("mock")

tests_require += driver_verification_tests_require

def read(filename):
    """
    Returns the contents of the given package file.

    Args:
        filename (str): The name of the file to read, relative to the current
            directory.

    Returns:
        str: The contents of the given package file.
    """

    path = os.path.join(os.path.dirname(__file__), filename)

    with open(path) as f:
        return f.read()

def get_version():
    """ str: The package version. """

    global_vars = {}

    # Compile and execute the individual file to prevent
    # the package from being automatically loaded.
    source = read(os.path.join("capybara", "version.py"))
    code = compile(source, "version.py", "exec")
    exec(code, global_vars)

    return global_vars['__version__']

setup(
    name="capybara-py",
    version=get_version(),
    description="Acceptance test framework for web applications",
    long_description=read("README.rst"),
    url="https://github.com/elliterate/capybara.py",
    author="Ian Lesperance",
    author_email="ian@elliterate.com",
    license="MIT",
    keywords="capybara",
    classifiers=[
        "Development Status :: 4 - Beta",
        "Intended Audience :: Developers",
        "Intended Audience :: Information Technology",
        "License :: OSI Approved :: MIT License",
        "Natural Language :: English",
        "Programming Language :: Python :: 2.7",
        "Programming Language :: Python :: 3.5",
        "Topic :: Software Development :: Libraries :: Python Modules",
        "Topic :: Software Development :: Quality Assurance",
        "Topic :: Software Development :: Testing"],
    packages=find_packages(exclude=["tests", "tests.*"]),
    data_files=(
        glob("capybara/tests/app/static/*") +
        glob("capybara/tests/app/templates/*") +
        glob("capybara/tests/fixtures/*")),
    install_requires=["cssselect", "lxml", "xpath-py ~= 0.1.2"],
    setup_requires=["pytest-runner"],
    tests_require=tests_require,
    extras_require={
        "development": ["bumpversion", "Sphinx"],

        # Expose test dependencies for external scripts, like pip.
        "test": tests_require})


import re

from capybara.helpers import desc, normalize_text, toregex
from capybara.queries.base_query import BaseQuery
from capybara.utils import isregex

class TitleQuery(BaseQuery):
    """
    Queries the title content of a node.

    Args:
        expected_title (str | RegexObject): The desired title.
        exact (bool, optional): Whether the text should match exactly. Defaults to False.
        wait (bool | int | float, optional): Whether and how long to wait for synchronization.
            Defaults to :data:`capybara.default_max_wait_time`.
    """

    def __init__(self, expected_title, exact=False, wait=None):
        self.expected_title = (expected_title if isregex(expected_title)
                               else normalize_text(expected_title))
        self.actual_title = None
        self.search_regexp = toregex(expected_title, exact=exact)

        self.options = {
            "wait": wait}

    def resolves_for(self, node):
        """
        Resolves this query relative to the given node.

        Args:
            node (node.Document): The node to be evaluated.

        Returns:
            bool: Whether the given node matches this query.
        """

        self.actual_title = normalize_text(node.title)
        return bool(self.search_regexp.search(self.actual_title))

    @property
    def wait(self):
        """ int | float: How long to wait for synchronization. """
        return self.normalize_wait(self.options["wait"])

    @property
    def failure_message(self):
        """ str: A message describing the query failure. """
        return self._build_message()

    @property
    def negative_failure_message(self):
        """ str: A message describing the negative query failure. """
        return self._build_message(" not")

    def _build_message(self, negated=""):
        verb = "match" if isregex(self.expected_title) else "include"
        return "expected {actual}{negated} to {verb} {expected}".format(
            actual=desc(self.actual_title),
            negated=negated,
            verb=verb,
            expected=desc(self.expected_title))