Import python-cssselect2_0.2.1.orig.tar.gz

[dgit import orig python-cssselect2_0.2.1.orig.tar.gz]
This commit is contained in:
Michael Fladischer 2018-11-13 19:13:59 +01:00
commit 4e8927610a
32 changed files with 4554 additions and 0 deletions

10
.coveragerc Normal file
View File

@ -0,0 +1,10 @@
[run]
branch = True
[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError
omit =
.*

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
*.pyc
*.egg-info
/.cache
/.coverage
/.eggs
/build
/dist
/env
/htmlcov
/profile

30
.travis.yml Normal file
View File

@ -0,0 +1,30 @@
language: python
sudo: false
git:
submodules: false
matrix:
include:
- os: linux
python: 2.7
- os: linux
python: 3.3
- os: linux
python: 3.4
- os: linux
python: 3.5
- os: linux
python: 3.6
- os: osx
language: generic
env: PYTHON_VERSION=3
before_install:
- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install python3; fi
install:
- pip$PYTHON_VERSION install --upgrade -e.[test]
script:
- python$PYTHON_VERSION setup.py test

29
CHANGES Normal file
View File

@ -0,0 +1,29 @@
cssselect2 changelog
====================
Version 0.2.1
-------------
Released on 2017-10-02.
* Fix documentation.
Version 0.2.0
-------------
Released on 2017-08-16.
* Fix some selectors for HTML documents with no namespace.
* Don't crash when the attribute comparator is unknown.
* Don't crash when there are empty attribute classes.
* Follow semantic versioning.
Version 0.1
-----------
Released on 2017-07-07.
* Initial release.

31
LICENSE Normal file
View File

@ -0,0 +1,31 @@
Copyright (c) 2012 - 2013 by Simon Sapin, 2017 by Guillaume Ayoub.
Some rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3
MANIFEST.in Normal file
View File

@ -0,0 +1,3 @@
include README.rst CHANGES LICENSE tox.ini .coveragerc
recursive-include docs *
prune docs/_build

39
PKG-INFO Normal file
View File

@ -0,0 +1,39 @@
Metadata-Version: 1.0
Name: cssselect2
Version: 0.2.1
Summary: CSS selectors for Python ElementTree
Home-page: http://packages.python.org/cssselect2/
Author: Simon Sapin
Author-email: simon.sapin@exyr.org
License: BSD
Description-Content-Type: UNKNOWN
Description: cssselect2: CSS selectors for Python ElementTree
################################################
cssselect2 is a straightforward implementation of `CSS3 Selectors`_ for markup
documents (HTML, XML, etc.) that can be read by `ElementTree`_-like parsers
(including cElementTree, lxml_, html5lib_, etc.)
Unlike cssselect_, it does not translate selectors to XPath_ and therefore does
not have all the correctness corner cases that are hard or impossible to fix in
cssselect.
.. _ElementTree: http://docs.python.org/3/library/xml.etree.elementtree.html
.. _CSS3 Selectors: http://www.w3.org/TR/2011/REC-css3-selectors-20110929/
.. _lxml: http://lxml.de/
.. _html5lib: https://github.com/html5lib/html5lib-python
.. _cssselect: http://packages.python.org/cssselect/
.. _XPath: http://www.w3.org/TR/xpath/
Quick facts:
* Free software: BSD licensed
* Compatible with Python 2.7+ and 3.3+
* Latest documentation: http://cssselect2.readthedocs.io/
* Source, issues and pull requests `on Github
<https://github.com/Kozea/cssselect2/>`_
* Releases `on PyPI <http://pypi.python.org/pypi/cssselect2>`_
* Install with ``pip install cssselect2``
Platform: UNKNOWN

28
README.rst Normal file
View File

@ -0,0 +1,28 @@
cssselect2: CSS selectors for Python ElementTree
################################################
cssselect2 is a straightforward implementation of `CSS3 Selectors`_ for markup
documents (HTML, XML, etc.) that can be read by `ElementTree`_-like parsers
(including cElementTree, lxml_, html5lib_, etc.)
Unlike cssselect_, it does not translate selectors to XPath_ and therefore does
not have all the correctness corner cases that are hard or impossible to fix in
cssselect.
.. _ElementTree: http://docs.python.org/3/library/xml.etree.elementtree.html
.. _CSS3 Selectors: http://www.w3.org/TR/2011/REC-css3-selectors-20110929/
.. _lxml: http://lxml.de/
.. _html5lib: https://github.com/html5lib/html5lib-python
.. _cssselect: http://packages.python.org/cssselect/
.. _XPath: http://www.w3.org/TR/xpath/
Quick facts:
* Free software: BSD licensed
* Compatible with Python 2.7+ and 3.3+
* Latest documentation: http://cssselect2.readthedocs.io/
* Source, issues and pull requests `on Github
<https://github.com/Kozea/cssselect2/>`_
* Releases `on PyPI <http://pypi.python.org/pypi/cssselect2>`_
* Install with ``pip install cssselect2``

View File

@ -0,0 +1,39 @@
Metadata-Version: 1.0
Name: cssselect2
Version: 0.2.1
Summary: CSS selectors for Python ElementTree
Home-page: http://packages.python.org/cssselect2/
Author: Simon Sapin
Author-email: simon.sapin@exyr.org
License: BSD
Description-Content-Type: UNKNOWN
Description: cssselect2: CSS selectors for Python ElementTree
################################################
cssselect2 is a straightforward implementation of `CSS3 Selectors`_ for markup
documents (HTML, XML, etc.) that can be read by `ElementTree`_-like parsers
(including cElementTree, lxml_, html5lib_, etc.)
Unlike cssselect_, it does not translate selectors to XPath_ and therefore does
not have all the correctness corner cases that are hard or impossible to fix in
cssselect.
.. _ElementTree: http://docs.python.org/3/library/xml.etree.elementtree.html
.. _CSS3 Selectors: http://www.w3.org/TR/2011/REC-css3-selectors-20110929/
.. _lxml: http://lxml.de/
.. _html5lib: https://github.com/html5lib/html5lib-python
.. _cssselect: http://packages.python.org/cssselect/
.. _XPath: http://www.w3.org/TR/xpath/
Quick facts:
* Free software: BSD licensed
* Compatible with Python 2.7+ and 3.3+
* Latest documentation: http://cssselect2.readthedocs.io/
* Source, issues and pull requests `on Github
<https://github.com/Kozea/cssselect2/>`_
* Releases `on PyPI <http://pypi.python.org/pypi/cssselect2>`_
* Install with ``pip install cssselect2``
Platform: UNKNOWN

View File

@ -0,0 +1,31 @@
.coveragerc
.gitignore
.travis.yml
CHANGES
LICENSE
MANIFEST.in
README.rst
example.py
setup.cfg
setup.py
cssselect2/__init__.py
cssselect2/_compat.py
cssselect2/compiler.py
cssselect2/parser.py
cssselect2/tree.py
cssselect2.egg-info/PKG-INFO
cssselect2.egg-info/SOURCES.txt
cssselect2.egg-info/dependency_links.txt
cssselect2.egg-info/requires.txt
cssselect2.egg-info/top_level.txt
cssselect2/tests/LICENSE
cssselect2/tests/__init__.py
cssselect2/tests/content.xhtml
cssselect2/tests/ids.html
cssselect2/tests/invalid_selectors.json
cssselect2/tests/make_valid_selectors_json.sh
cssselect2/tests/shakespeare.html
cssselect2/tests/test_cssselect2.py
cssselect2/tests/valid_selectors.json
docs/conf.py
docs/index.rst

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,7 @@
tinycss2
[test]
pytest-runner
pytest-cov
pytest-flake8
pytest-isort

View File

@ -0,0 +1 @@
cssselect2

114
cssselect2/__init__.py Normal file
View File

@ -0,0 +1,114 @@
# coding: utf8
"""
cssselect2
----------
CSS selectors for ElementTree.
:copyright: (c) 2012 by Simon Sapin, 2017 by Guillaume Ayoub.
:license: BSD, see LICENSE for more details.
"""
from __future__ import unicode_literals
import operator
from webencodings import ascii_lower
# Classes are imported here to expose them at the top level of the module
from .compiler import compile_selector_list # noqa
from .parser import SelectorError # noqa
from .tree import ElementWrapper # noqa
VERSION = '0.2.1'
class Matcher(object):
"""A CSS selectors storage that can match against HTML elements."""
def __init__(self):
self.id_selectors = {}
self.class_selectors = {}
self.lower_local_name_selectors = {}
self.namespace_selectors = {}
self.other_selectors = []
self.order = 0
def add_selector(self, selector, payload):
"""
Add a selector and its payload to the matcher.
:param selector:
A :class:`CompiledSelector` object.
:param payload:
Some data associated to the selector,
such as :class:`declarations <~tinycss2.ast.Declaration>`
parsed from the :attr:`~tinycss2.ast.QualifiedRule.content`
of a style rule.
It can be any Python object,
and will be returned as-is by :meth:`match`.
"""
self.order += 1
if selector.never_matches:
return
entry = (
selector.test, selector.specificity, self.order,
selector.pseudo_element, payload)
if selector.id is not None:
self.id_selectors.setdefault(selector.id, []).append(entry)
elif selector.class_name is not None:
self.class_selectors.setdefault(selector.class_name, []) \
.append(entry)
elif selector.local_name is not None:
self.lower_local_name_selectors.setdefault(
selector.lower_local_name, []).append(entry)
elif selector.namespace is not None:
self.namespace_selectors.setdefault(selector.namespace, []) \
.append(entry)
else:
self.other_selectors.append(entry)
def match(self, element):
"""
Match selectors against the given element.
:param element:
An :class:`ElementWrapper`.
:returns:
A list of the :obj:`payload` objects associated
to selectors that match element,
in order of lowest to highest :attr:`~CompiledSelector.specificity`
and in order of addition with :meth:`add_selector`
among selectors of equal specificity.
"""
relevant_selectors = []
if element.id is not None:
relevant_selectors.append(self.id_selectors.get(element.id, []))
for class_name in element.classes:
relevant_selectors.append(self.class_selectors.get(class_name, []))
relevant_selectors.append(
self.lower_local_name_selectors.get(
ascii_lower(element.local_name), []))
relevant_selectors.append(
self.namespace_selectors.get(element.namespace_url, []))
relevant_selectors.append(self.other_selectors)
results = [
(specificity, order, pseudo, payload)
for selector_list in relevant_selectors
for test, specificity, order, pseudo, payload in selector_list
if test(element)
]
results.sort(key=SORT_KEY)
return results
SORT_KEY = operator.itemgetter(0, 1)

9
cssselect2/_compat.py Normal file
View File

@ -0,0 +1,9 @@
try:
basestring = basestring
except NameError:
basestring = str
try:
from itertools import ifilter
except ImportError:
ifilter = filter

336
cssselect2/compiler.py Normal file
View File

@ -0,0 +1,336 @@
# coding: utf8
from __future__ import unicode_literals
import re
from tinycss2.nth import parse_nth
from webencodings import ascii_lower
from . import parser
from .parser import SelectorError
# http://dev.w3.org/csswg/selectors/#whitespace
split_whitespace = re.compile('[^ \t\r\n\f]+').findall
def compile_selector_list(input, namespaces=None):
"""Compile a (comma-separated) list of selectors.
:param input:
A :term:`tinycss2:string`,
or an iterable of tinycss2 :term:`tinycss2:component values` such as
the :attr:`~tinycss2.ast.QualifiedRule.predule` of a style rule.
:param namespaces:
A optional dictionary of all `namespace prefix declarations
<http://www.w3.org/TR/selectors/#nsdecl>`_ in scope for this selector.
Keys are namespace prefixes as strings, or ``None`` for the default
namespace.
Values are namespace URLs as strings.
If omitted, assume that no prefix is declared.
:returns:
A list of opaque :class:`CompiledSelector` objects.
"""
return [
CompiledSelector(selector)
for selector in parser.parse(input, namespaces)
]
class CompiledSelector(object):
def __init__(self, parsed_selector):
source = _compile_node(parsed_selector.parsed_tree)
self.never_matches = source == '0'
self.test = eval(
'lambda el: ' + source,
{'split_whitespace': split_whitespace, 'ascii_lower': ascii_lower},
{},
)
self.specificity = parsed_selector.specificity
self.pseudo_element = parsed_selector.pseudo_element
self.id = None
self.class_name = None
self.local_name = None
self.lower_local_name = None
self.namespace = None
node = parsed_selector.parsed_tree
if isinstance(node, parser.CombinedSelector):
node = node.right
for simple_selector in node.simple_selectors:
if isinstance(simple_selector, parser.IDSelector):
self.id = simple_selector.ident
elif isinstance(simple_selector, parser.ClassSelector):
self.class_name = simple_selector.class_name
elif isinstance(simple_selector, parser.LocalNameSelector):
self.local_name = simple_selector.local_name
self.lower_local_name = simple_selector.lower_local_name
elif isinstance(simple_selector, parser.NamespaceSelector):
self.namespace = simple_selector.namespace
def _compile_node(selector):
"""Return a boolean expression, as a Python source string.
When evaluated in a context where the `el` variable is an
:class:`~cssselect2.tree.Element` object,
tells whether the element is a subject of `selector`.
"""
# To avoid precedence-related bugs, any sub-expression that is passed
# around must be "atomic": add parentheses when the top-level would be
# an operator. Bare literals and function calls are fine.
# 1 and 0 are used for True and False to avoid global lookups.
if isinstance(selector, parser.CombinedSelector):
left_inside = _compile_node(selector.left)
if left_inside == '0':
return '0' # 0 and x == 0
elif left_inside == '1':
# 1 and x == x, but the element matching 1 still needs to exist.
if selector.combinator in (' ', '>'):
left = 'el.parent is not None'
elif selector.combinator in ('~', '+'):
left = 'el.previous is not None'
else:
raise SelectorError('Unknown combinator', selector.combinator)
# Rebind the `el` name inside a generator-expressions (in a new scope)
# so that 'left_inside' applies to different elements.
elif selector.combinator == ' ':
left = 'any((%s) for el in el.iter_ancestors())' % left_inside
elif selector.combinator == '>':
left = ('next(el is not None and (%s) for el in [el.parent])'
% left_inside)
elif selector.combinator == '+':
left = ('next(el is not None and (%s) for el in [el.previous])'
% left_inside)
elif selector.combinator == '~':
left = ('any((%s) for el in el.iter_previous_siblings())'
% left_inside)
else:
raise SelectorError('Unknown combinator', selector.combinator)
right = _compile_node(selector.right)
if right == '0':
return '0' # 0 and x == 0
elif right == '1':
return left # 1 and x == x
else:
# Evaluate combinators right to left:
return '(%s) and (%s)' % (right, left)
elif isinstance(selector, parser.CompoundSelector):
sub_expressions = [
expr for expr in map(_compile_node, selector.simple_selectors)
if expr != '1']
if len(sub_expressions) == 1:
test = sub_expressions[0]
elif '0' in sub_expressions:
test = '0'
elif sub_expressions:
test = ' and '.join('(%s)' % e for e in sub_expressions)
else:
test = '1' # all([]) == True
if isinstance(selector, parser.NegationSelector):
if test == '0':
return '1'
elif test == '1':
return '0'
else:
return 'not (%s)' % test
else:
return test
elif isinstance(selector, parser.LocalNameSelector):
return ('el.local_name == (%r if el.in_html_document else %r)'
% (selector.lower_local_name, selector.local_name))
elif isinstance(selector, parser.NamespaceSelector):
return 'el.namespace_url == %r' % selector.namespace
elif isinstance(selector, parser.ClassSelector):
return '%r in el.classes' % selector.class_name
elif isinstance(selector, parser.IDSelector):
return 'el.id == %r' % selector.ident
elif isinstance(selector, parser.AttributeSelector):
if selector.namespace is not None:
if selector.namespace:
key = '(%r if el.in_html_document else %r)' % (
'{%s}%s' % (selector.namespace, selector.lower_name),
'{%s}%s' % (selector.namespace, selector.name),
)
else:
key = ('(%r if el.in_html_document else %r)'
% (selector.lower_name, selector.name))
value = selector.value
if selector.operator is None:
return 'el.etree_element.get(%s) is not None' % key
elif selector.operator == '=':
return 'el.etree_element.get(%s) == %r' % (key, value)
elif selector.operator == '~=':
if len(value.split()) != 1 or value.strip() != value:
return '0'
else:
return (
'%r in split_whitespace(el.etree_element.get(%s, ""))'
% (value, key))
elif selector.operator == '|=':
return ('next(v == %r or (v is not None and v.startswith(%r))'
' for v in [el.etree_element.get(%s)])'
% (value, value + '-', key))
elif selector.operator == '^=':
if value:
return 'el.etree_element.get(%s, "").startswith(%r)' % (
key, value)
else:
return '0'
elif selector.operator == '$=':
if value:
return 'el.etree_element.get(%s, "").endswith(%r)' % (
key, value)
else:
return '0'
elif selector.operator == '*=':
if value:
return '%r in el.etree_element.get(%s, "")' % (value, key)
else:
return '0'
else:
raise SelectorError(
'Unknown attribute operator', selector.operator)
else: # In any namespace
raise NotImplementedError # TODO
elif isinstance(selector, parser.PseudoClassSelector):
if selector.name == 'link':
return ('%s and el.etree_element.get("href") is not None'
% html_tag_eq('a', 'area', 'link'))
elif selector.name == 'enabled':
return (
'(%s and el.etree_element.get("disabled") is None'
' and not el.in_disabled_fieldset) or'
'(%s and el.etree_element.get("disabled") is None) or '
'(%s and el.etree_element.get("href") is not None)'
% (
html_tag_eq('button', 'input', 'select', 'textarea',
'option'),
html_tag_eq('optgroup', 'menuitem', 'fieldset'),
html_tag_eq('a', 'area', 'link'),
)
)
elif selector.name == 'disabled':
return (
'(%s and (el.etree_element.get("disabled") is not None'
' or el.in_disabled_fieldset)) or'
'(%s and el.etree_element.get("disabled") is not None)' % (
html_tag_eq('button', 'input', 'select', 'textarea',
'option'),
html_tag_eq('optgroup', 'menuitem', 'fieldset'),
)
)
elif selector.name == 'checked':
return (
'(%s and el.etree_element.get("checked") is not None and'
' ascii_lower(el.etree_element.get("type", "")) '
' in ("checkbox", "radio"))'
'or (%s and el.etree_element.get("selected") is not None)'
% (
html_tag_eq('input', 'menuitem'),
html_tag_eq('option'),
)
)
elif selector.name in ('visited', 'hover', 'active', 'focus',
'target'):
# Not applicable in a static context: never match.
return '0'
elif selector.name == 'root':
return 'el.parent is None'
elif selector.name == 'first-child':
return 'el.index == 0'
elif selector.name == 'last-child':
return 'el.index + 1 == len(el.etree_siblings)'
elif selector.name == 'first-of-type':
return ('all(s.tag != el.etree_element.tag'
' for s in el.etree_siblings[:el.index])')
elif selector.name == 'last-of-type':
return ('all(s.tag != el.etree_element.tag'
' for s in el.etree_siblings[el.index + 1:])')
elif selector.name == 'only-child':
return 'len(el.etree_siblings) == 1'
elif selector.name == 'only-of-type':
return ('all(s.tag != el.etree_element.tag or i == el.index'
' for i, s in enumerate(el.etree_siblings))')
elif selector.name == 'empty':
return 'not (el.etree_children or el.etree_element.text)'
else:
raise SelectorError('Unknown pseudo-class', selector.name)
elif isinstance(selector, parser.FunctionalPseudoClassSelector):
if selector.name == 'lang':
tokens = [
t for t in selector.arguments
if t.type != 'whitespace'
]
if len(tokens) == 1 and tokens[0].type == 'ident':
lang = tokens[0].lower_value
else:
raise SelectorError('Invalid arguments for :lang()')
return ('el.lang == %r or el.lang.startswith(%r)'
% (lang, lang + '-'))
else:
if selector.name == 'nth-child':
count = 'el.index'
elif selector.name == 'nth-last-child':
count = '(len(el.etree_siblings) - el.index - 1)'
elif selector.name == 'nth-of-type':
count = ('sum(1 for s in el.etree_siblings[:el.index]'
' if s.tag == el.etree_element.tag)')
elif selector.name == 'nth-last-of-type':
count = ('sum(1 for s in el.etree_siblings[el.index + 1:]'
' if s.tag == el.etree_element.tag)')
else:
raise SelectorError('Unknown pseudo-class', selector.name)
result = parse_nth(selector.arguments)
if result is None:
raise SelectorError(
'Invalid arguments for :%s()' % selector.name)
a, b = result
# x is the number of siblings before/after the element
# Matches if a positive or zero integer n exists so that:
# x = a*n + b-1
# x = a*n + B
B = b - 1
if a == 0:
# x = B
return '%s == %i' % (count, B)
else:
# n = (x - B) / a
return ('next(r == 0 and n >= 0'
' for n, r in [divmod(%s - %i, %i)])'
% (count, B, a))
else:
raise TypeError(type(selector), selector)
def html_tag_eq(*local_names):
if len(local_names) == 1:
return (
'((el.local_name == %r) if el.in_html_document else '
'(el.etree_element.tag == %r))' % (
local_names[0],
'{http://www.w3.org/1999/xhtml}' + local_names[0]))
else:
return (
'((el.local_name in (%s)) if el.in_html_document else '
'(el.etree_element.tag in (%s)))' % (
', '.join(repr(n) for n in local_names),
', '.join(repr('{http://www.w3.org/1999/xhtml}' + n)
for n in local_names)))

427
cssselect2/parser.py Normal file
View File

@ -0,0 +1,427 @@
# coding: utf8
"""
cssselect2.parser
-----------------
A parser for CSS selectors, based on the tinycss tokenizer.
:copyright: (c) 2012 by Simon Sapin, 2017 by Guillaume Ayoub.
:license: BSD, see LICENSE for more details.
"""
from __future__ import unicode_literals
from tinycss2 import parse_component_value_list
from ._compat import basestring
__all__ = ['parse']
def parse(input, namespaces=None):
"""
:param input:
A :term:`string`, or an iterable of :term:`component values`.
"""
if isinstance(input, basestring):
input = parse_component_value_list(input)
tokens = TokenStream(input)
namespaces = namespaces or {}
yield parse_selector(tokens, namespaces)
tokens.skip_whitespace_and_comment()
while 1:
next = tokens.next()
if next is None:
return
elif next == ',':
yield parse_selector(tokens, namespaces)
else:
raise SelectorError(next, 'unpexpected %s token.' % next.type)
def parse_selector(tokens, namespaces):
result, pseudo_element = parse_compound_selector(tokens, namespaces)
while 1:
has_whitespace = tokens.skip_whitespace()
while tokens.skip_comment():
has_whitespace = tokens.skip_whitespace() or has_whitespace
if pseudo_element is not None:
return Selector(result, pseudo_element)
peek = tokens.peek()
if peek is None or peek == ',':
return Selector(result, pseudo_element)
elif peek in ('>', '+', '~'):
combinator = peek.value
tokens.next()
elif has_whitespace:
combinator = ' '
else:
return Selector(result, pseudo_element)
compound, pseudo_element = parse_compound_selector(tokens, namespaces)
result = CombinedSelector(result, combinator, compound)
def parse_compound_selector(tokens, namespaces):
type_selectors = parse_type_selector(tokens, namespaces)
simple_selectors = type_selectors if type_selectors is not None else []
while 1:
simple_selector, pseudo_element = parse_simple_selector(
tokens, namespaces)
if pseudo_element is not None or simple_selector is None:
break
simple_selectors.append(simple_selector)
if (simple_selectors or type_selectors is not None or
pseudo_element is not None):
return CompoundSelector(simple_selectors), pseudo_element
else:
peek = tokens.peek()
raise SelectorError(peek, 'expected a compound selector, got %s'
% (peek.type if peek else 'EOF'))
def parse_type_selector(tokens, namespaces):
tokens.skip_whitespace()
qualified_name = parse_qualified_name(tokens, namespaces)
if qualified_name is None:
return None
simple_selectors = []
namespace, local_name = qualified_name
if local_name is not None:
simple_selectors.append(LocalNameSelector(local_name))
if namespace is not None:
simple_selectors.append(NamespaceSelector(namespace))
return simple_selectors
def parse_simple_selector(tokens, namespaces, in_negation=False):
peek = tokens.peek()
if peek is None:
return None, None
if peek.type == 'hash' and peek.is_identifier:
tokens.next()
return IDSelector(peek.value), None
elif peek == '.':
tokens.next()
next = tokens.next()
if next is None or next.type != 'ident':
raise SelectorError(
next, 'Expected a class name, got %s' % next)
return ClassSelector(next.value), None
elif peek.type == '[] block':
tokens.next()
attr = parse_attribute_selector(TokenStream(peek.content), namespaces)
return attr, None
elif peek == ':':
tokens.next()
next = tokens.next()
if next == ':':
next = tokens.next()
if next is None or next.type != 'ident':
raise SelectorError(
next, 'Expected a pseudo-element name, got %s' % next)
return None, next.lower_value
elif next is not None and next.type == 'ident':
name = next.lower_value
if name in ('before', 'after', 'first-line', 'first-letter'):
return None, name
else:
return PseudoClassSelector(name), None
elif next is not None and next.type == 'function':
name = next.lower_name
if name == 'not':
if in_negation:
raise SelectorError(next, 'nested :not()')
return parse_negation(next, namespaces), None
else:
return (
FunctionalPseudoClassSelector(name, next.arguments), None)
else:
raise SelectorError(next, 'unexpected %s token.' % next)
else:
return None, None
def parse_negation(negation_token, namespaces):
tokens = TokenStream(negation_token.arguments)
type_selectors = parse_type_selector(tokens, namespaces)
if type_selectors is not None:
return NegationSelector(type_selectors)
simple_selector, pseudo_element = parse_simple_selector(
tokens, namespaces, in_negation=True)
tokens.skip_whitespace()
if pseudo_element is None and tokens.next() is None:
return NegationSelector([simple_selector])
else:
raise SelectorError(
negation_token, ':not() only accepts a simple selector')
def parse_attribute_selector(tokens, namespaces):
tokens.skip_whitespace()
qualified_name = parse_qualified_name(
tokens, namespaces, is_attribute=True)
if qualified_name is None:
next = tokens.next()
raise SelectorError(
next, 'expected attribute name, got %s' % next)
namespace, local_name = qualified_name
tokens.skip_whitespace()
peek = tokens.peek()
if peek is None:
operator = None
value = None
elif peek in ('=', '~=', '|=', '^=', '$=', '*='):
operator = peek.value
tokens.next()
tokens.skip_whitespace()
next = tokens.next()
if next is None or next.type not in ('ident', 'string'):
next_type = 'None' if next is None else next.type
raise SelectorError(
next, 'expected attribute value, got %s' % next_type)
value = next.value
else:
raise SelectorError(
peek, 'expected attribute selector operator, got %s' % peek)
tokens.skip_whitespace()
next = tokens.next()
if next is not None:
raise SelectorError(next, 'expected ], got %s' % next.type)
return AttributeSelector(namespace, local_name, operator, value)
def parse_qualified_name(tokens, namespaces, is_attribute=False):
"""Returns None (not a qualified name) or (ns, local),
in which None is a wildcard. The empty string for ns is "no namespace".
"""
peek = tokens.peek()
if peek is None:
return None
if peek.type == 'ident':
first_ident = tokens.next()
peek = tokens.peek()
if peek != '|':
namespace = '' if is_attribute else namespaces.get(None, None)
return namespace, (first_ident.value, first_ident.lower_value)
tokens.next()
namespace = namespaces.get(first_ident.value)
if namespace is None:
raise SelectorError(
first_ident,
'undefined namespace prefix: ' + first_ident.value)
elif peek == '*':
next = tokens.next()
peek = tokens.peek()
if peek != '|':
if is_attribute:
raise SelectorError(
next, 'Expected local name, got %s' % next.type)
return namespaces.get(None, None), None
tokens.next()
namespace = None
elif peek == '|':
tokens.next()
namespace = ''
else:
return None
# If we get here, we just consumed '|' and set ``namespace``
next = tokens.next()
if next.type == 'ident':
return namespace, (next.value, next.lower_value)
elif next == '*' and not is_attribute:
return namespace, None
else:
raise SelectorError(next, 'Expected local name, got %s' % next.type)
class SelectorError(ValueError):
"""A specialized ``ValueError`` for invalid selectors."""
class TokenStream(object):
def __init__(self, tokens):
self.tokens = iter(tokens)
self.peeked = [] # In reversed order
def next(self):
if self.peeked:
return self.peeked.pop()
else:
return next(self.tokens, None)
def peek(self):
if not self.peeked:
self.peeked.append(next(self.tokens, None))
return self.peeked[-1]
def skip(self, skip_types):
found = False
while 1:
peek = self.peek()
if peek is None or peek.type not in skip_types:
break
self.next()
found = True
return found
def skip_whitespace(self):
return self.skip(['whitespace'])
def skip_comment(self):
return self.skip(['comment'])
def skip_whitespace_and_comment(self):
return self.skip(['comment', 'whitespace'])
class Selector(object):
def __init__(self, tree, pseudo_element=None):
self.parsed_tree = tree
if pseudo_element is None:
self.pseudo_element = pseudo_element
#: Tuple of 3 integers: http://www.w3.org/TR/selectors/#specificity
self.specificity = tree.specificity
else:
self.pseudo_element = pseudo_element
a, b, c = tree.specificity
self.specificity = a, b, c + 1
def __repr__(self):
if self.pseudo_element is None:
return repr(self.parsed_tree)
else:
return '%r::%s' % (self.parsed_tree, self.pseudo_element)
class CombinedSelector(object):
def __init__(self, left, combinator, right):
#: Combined or compound selector
self.left = left
# One of `` `` (a single space), ``>``, ``+`` or ``~``.
self.combinator = combinator
#: compound selector
self.right = right
@property
def specificity(self):
a1, b1, c1 = self.left.specificity
a2, b2, c2 = self.right.specificity
return a1 + a2, b1 + b2, c1 + c2
def __repr__(self):
return '%r%s%r' % (self.left, self.combinator, self.right)
class CompoundSelector(object):
"""Aka. sequence of simple selectors, in Level 3."""
def __init__(self, simple_selectors):
self.simple_selectors = simple_selectors
@property
def specificity(self):
if self.simple_selectors:
# zip(*foo) turns [(a1, b1, c1), (a2, b2, c2), ...]
# into [(a1, a2, ...), (b1, b2, ...), (c1, c2, ...)]
return tuple(map(sum, zip(
*(sel.specificity for sel in self.simple_selectors))))
else:
return 0, 0, 0
def __repr__(self):
return ''.join(map(repr, self.simple_selectors))
class LocalNameSelector(object):
specificity = 0, 0, 1
def __init__(self, local_name):
self.local_name, self.lower_local_name = local_name
def __repr__(self):
return self.local_name
class NamespaceSelector(object):
specificity = 0, 0, 0
def __init__(self, namespace):
#: The namespace URL as a string,
#: or the empty string for elements not in any namespace.
self.namespace = namespace
def __repr__(self):
if self.namespace == '':
return '|'
else:
return '{%s}|' % self.namespace
class IDSelector(object):
specificity = 1, 0, 0
def __init__(self, ident):
self.ident = ident
def __repr__(self):
return '#' + self.ident
class ClassSelector(object):
specificity = 0, 1, 0
def __init__(self, class_name):
self.class_name = class_name
def __repr__(self):
return '.' + self.class_name
class AttributeSelector(object):
specificity = 0, 1, 0
def __init__(self, namespace, name, operator, value):
self.namespace = namespace
self.name, self.lower_name = name
#: A string like ``=`` or ``~=``, or None for ``[attr]`` selectors
self.operator = operator
#: A string, or None for ``[attr]`` selectors
self.value = value
def __repr__(self):
namespace = ('*|' if self.namespace is None
else '{%s}' % self.namespace)
return '[%s%s%s%r]' % (namespace, self.name, self.operator, self.value)
class PseudoClassSelector(object):
specificity = 0, 1, 0
def __init__(self, name):
self.name = name
def __repr__(self):
return ':' + self.name
class FunctionalPseudoClassSelector(object):
specificity = 0, 1, 0
def __init__(self, name, arguments):
self.name = name
self.arguments = arguments
def __repr__(self):
return ':%s%r' % (self.name, tuple(self.arguments))
class NegationSelector(CompoundSelector):
def __repr__(self):
return ':not(%r)' % CompoundSelector.__repr__(self)

4
cssselect2/tests/LICENSE Normal file
View File

@ -0,0 +1,4 @@
These files are taken form the web-platform-test repository
and used under a 3-clause BSD License.
https://github.com/w3c/web-platform-tests/tree/master/selectors-api

View File

@ -0,0 +1,11 @@
# coding: utf8
"""
cssselect2.tests
----------------
Test suite for cssselect2.
:copyright: (c) 2012 by Simon Sapin, 2017 by Guillaume Ayoub.
:license: BSD, see LICENSE for more details.
"""

View File

@ -0,0 +1,372 @@
<!DOCTYPE html>
<html id="html" lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head id="head">
<title id="title">Selectors-API Test Suite: HTML with Selectors Level 2 using TestHarness: Test Document</title>
<!-- Links for :link and :visited pseudo-class test -->
<link id="pseudo-link-link1" href=""/>
<link id="pseudo-link-link2" href="http://example.org/"/>
<link id="pseudo-link-link3"/>
</head>
<body id="body">
<div id="root">
<div id="target"></div>
<div id="universal">
<p id="universal-p1">Universal selector tests inside element with <code id="universal-code1">id="universal"</code>.</p>
<hr id="universal-hr1"/>
<pre id="universal-pre1">Some preformatted text with some <span id="universal-span1">embedded code</span></pre>
<p id="universal-p2">This is a normal link: <a id="universal-a1" href="http://www.w3.org/">W3C</a></p>
<address id="universal-address1">Some more nested elements <code id="universal-code2"><a href="#" id="universal-a2">code hyperlink</a></code></address>
</div>
<div id="attr-presence">
<div class="attr-presence-div1" id="attr-presence-div1" align="center"></div>
<div class="attr-presence-div2" id="attr-presence-div2" align=""></div>
<div class="attr-presence-div3" id="attr-presence-div3" valign="center"></div>
<div class="attr-presence-div4" id="attr-presence-div4" alignv="center"></div>
<p id="attr-presence-p1"><a id="attr-presence-a1" tItLe=""></a><span id="attr-presence-span1" TITLE="attr-presence-span1"></span></p>
<pre id="attr-presence-pre1" data-attr-presence="pre1"></pre>
<blockquote id="attr-presence-blockquote1" data-attr-presence="blockquote1"></blockquote>
<ul id="attr-presence-ul1" data-中文=""></ul>
<select id="attr-presence-select1">
<option id="attr-presence-select1-option1">A</option>
<option id="attr-presence-select1-option2">B</option>
<option id="attr-presence-select1-option3">C</option>
<option id="attr-presence-select1-option4">D</option>
</select>
<select id="attr-presence-select2">
<option id="attr-presence-select2-option1">A</option>
<option id="attr-presence-select2-option2">B</option>
<option id="attr-presence-select2-option3">C</option>
<option id="attr-presence-select2-option4" selected="selected">D</option>
</select>
<select id="attr-presence-select3" multiple="multiple">
<option id="attr-presence-select3-option1">A</option>
<option id="attr-presence-select3-option2" selected="">B</option>
<option id="attr-presence-select3-option3" selected="selected">C</option>
<option id="attr-presence-select3-option4">D</option>
</select>
</div>
<div id="attr-value">
<div id="attr-value-div1" align="center"></div>
<div id="attr-value-div2" align=""></div>
<div id="attr-value-div3" data-attr-value="&#xE9;"></div>
<div id="attr-value-div4" data-attr-value_foo="&#xE9;"></div>
<form id="attr-value-form1">
<input id="attr-value-input1" type="text"/>
<input id="attr-value-input2" type="password"/>
<input id="attr-value-input3" type="hidden"/>
<input id="attr-value-input4" type="radio"/>
<input id="attr-value-input5" type="checkbox"/>
<input id="attr-value-input6" type="radio"/>
<input id="attr-value-input7" type="text"/>
<input id="attr-value-input8" type="hidden"/>
<input id="attr-value-input9" type="radio"/>
</form>
<div id="attr-value-div5" data-attr-value="中文"></div>
</div>
<div id="attr-whitespace">
<div id="attr-whitespace-div1" class="foo div1 bar"></div>
<div id="attr-whitespace-div2" class=""></div>
<div id="attr-whitespace-div3" class="foo div3 bar"></div>
<div id="attr-whitespace-div4" data-attr-whitespace="foo &#xE9; bar"></div>
<div id="attr-whitespace-div5" data-attr-whitespace_foo="&#xE9; foo"></div>
<a id="attr-whitespace-a1" rel="next bookmark"></a>
<a id="attr-whitespace-a2" rel="tag nofollow"></a>
<a id="attr-whitespace-a3" rel="tag bookmark"></a>
<a id="attr-whitespace-a4" rel="book mark"></a> <!-- Intentional space in "book mark" -->
<a id="attr-whitespace-a5" rel="nofollow"></a>
<a id="attr-whitespace-a6" rev="bookmark nofollow"></a>
<a id="attr-whitespace-a7" rel="prev next tag alternate nofollow author help icon noreferrer prefetch search stylesheet tag"></a>
<p id="attr-whitespace-p1" title="Chinese 中文 characters"></p>
</div>
<div id="attr-hyphen">
<div id="attr-hyphen-div1"></div>
<div id="attr-hyphen-div2" lang="fr"></div>
<div id="attr-hyphen-div3" lang="en-AU"></div>
<div id="attr-hyphen-div4" lang="es"></div>
</div>
<div id="attr-begins">
<a id="attr-begins-a1" href="http://www.example.org"></a>
<a id="attr-begins-a2" href="http://example.org/"></a>
<a id="attr-begins-a3" href="http://www.example.com/"></a>
<div id="attr-begins-div1" lang="fr"></div>
<div id="attr-begins-div2" lang="en-AU"></div>
<div id="attr-begins-div3" lang="es"></div>
<div id="attr-begins-div4" lang="en-US"></div>
<div id="attr-begins-div5" lang="en"></div>
<p id="attr-begins-p1" class=" apple"></p> <!-- Intentional space in class value " apple". -->
</div>
<div id="attr-ends">
<a id="attr-ends-a1" href="http://www.example.org"></a>
<a id="attr-ends-a2" href="http://example.org/"></a>
<a id="attr-ends-a3" href="http://www.example.org"></a>
<div id="attr-ends-div1" lang="fr"></div>
<div id="attr-ends-div2" lang="de-CH"></div>
<div id="attr-ends-div3" lang="es"></div>
<div id="attr-ends-div4" lang="fr-CH"></div>
<p id="attr-ends-p1" class="apple "></p> <!-- Intentional space in class value "apple ". -->
</div>
<div id="attr-contains">
<a id="attr-contains-a1" href="http://www.example.org"></a>
<a id="attr-contains-a2" href="http://example.org/"></a>
<a id="attr-contains-a3" href="http://www.example.com/"></a>
<div id="attr-contains-div1" lang="fr"></div>
<div id="attr-contains-div2" lang="en-AU"></div>
<div id="attr-contains-div3" lang="de-CH"></div>
<div id="attr-contains-div4" lang="es"></div>
<div id="attr-contains-div5" lang="fr-CH"></div>
<div id="attr-contains-div6" lang="en-US"></div>
<p id="attr-contains-p1" class=" apple banana orange "></p>
</div>
<div id="pseudo-nth">
<table id="pseudo-nth-table1">
<tr id="pseudo-nth-tr1"><td id="pseudo-nth-td1"></td><td id="pseudo-nth-td2"></td><td id="pseudo-nth-td3"></td><td id="pseudo-nth-td4"></td><td id="pseudo-nth--td5"></td><td id="pseudo-nth-td6"></td></tr>
<tr id="pseudo-nth-tr2"><td id="pseudo-nth-td7"></td><td id="pseudo-nth-td8"></td><td id="pseudo-nth-td9"></td><td id="pseudo-nth-td10"></td><td id="pseudo-nth-td11"></td><td id="pseudo-nth-td12"></td></tr>
<tr id="pseudo-nth-tr3"><td id="pseudo-nth-td13"></td><td id="pseudo-nth-td14"></td><td id="pseudo-nth-td15"></td><td id="pseudo-nth-td16"></td><td id="pseudo-nth-td17"></td><td id="pseudo-nth-td18"></td></tr>
</table>
<ol id="pseudo-nth-ol1">
<li id="pseudo-nth-li1"></li>
<li id="pseudo-nth-li2"></li>
<li id="pseudo-nth-li3"></li>
<li id="pseudo-nth-li4"></li>
<li id="pseudo-nth-li5"></li>
<li id="pseudo-nth-li6"></li>
<li id="pseudo-nth-li7"></li>
<li id="pseudo-nth-li8"></li>
<li id="pseudo-nth-li9"></li>
<li id="pseudo-nth-li10"></li>
<li id="pseudo-nth-li11"></li>
<li id="pseudo-nth-li12"></li>
</ol>
<p id="pseudo-nth-p1">
<span id="pseudo-nth-span1">span1</span>
<em id="pseudo-nth-em1">em1</em>
<!-- comment node-->
<em id="pseudo-nth-em2">em2</em>
<span id="pseudo-nth-span2">span2</span>
<strong id="pseudo-nth-strong1">strong1</strong>
<em id="pseudo-nth-em3">em3</em>
<span id="pseudo-nth-span3">span3</span>
<span id="pseudo-nth-span4">span4</span>
<strong id="pseudo-nth-strong2">strong2</strong>
<em id="pseudo-nth-em4">em4</em>
</p>
</div>
<div id="pseudo-first-child">
<div id="pseudo-first-child-div1"></div>
<div id="pseudo-first-child-div2"></div>
<div id="pseudo-first-child-div3"></div>
<p id="pseudo-first-child-p1"><span id="pseudo-first-child-span1"></span><span id="pseudo-first-child-span2"></span></p>
<p id="pseudo-first-child-p2"><span id="pseudo-first-child-span3"></span><span id="pseudo-first-child-span4"></span></p>
<p id="pseudo-first-child-p3"><span id="pseudo-first-child-span5"></span><span id="pseudo-first-child-span6"></span></p>
</div>
<div id="pseudo-last-child">
<p id="pseudo-last-child-p1"><span id="pseudo-last-child-span1"></span><span id="pseudo-last-child-span2"></span></p>
<p id="pseudo-last-child-p2"><span id="pseudo-last-child-span3"></span><span id="pseudo-last-child-span4"></span></p>
<p id="pseudo-last-child-p3"><span id="pseudo-last-child-span5"></span><span id="pseudo-last-child-span6"></span></p>
<div id="pseudo-last-child-div1"></div>
<div id="pseudo-last-child-div2"></div>
<div id="pseudo-last-child-div3"></div>
</div>
<div id="pseudo-only">
<p id="pseudo-only-p1">
<span id="pseudo-only-span1"></span>
</p>
<p id="pseudo-only-p2">
<span id="pseudo-only-span2"></span>
<span id="pseudo-only-span3"></span>
</p>
<p id="pseudo-only-p3">
<span id="pseudo-only-span4"></span>
<em id="pseudo-only-em1"></em>
<span id="pseudo-only-span5"></span>
</p>
</div>>
<div id="pseudo-empty">
<p id="pseudo-empty-p1"></p>
<p id="pseudo-empty-p2"><!-- comment node --></p>
<p id="pseudo-empty-p3"> </p>
<p id="pseudo-empty-p4">Text node</p>
<p id="pseudo-empty-p5"><span id="pseudo-empty-span1"></span></p>
</div>
<div id="pseudo-link">
<a id="pseudo-link-a1" href="">with href</a>
<a id="pseudo-link-a2" href="http://example.org/">with href</a>
<a id="pseudo-link-a3">without href</a>
<map name="pseudo-link-map1" id="pseudo-link-map1">
<area id="pseudo-link-area1" href=""/>
<area id="pseudo-link-area2"/>
</map>
</div>
<div id="pseudo-lang">
<div id="pseudo-lang-div1"></div>
<div id="pseudo-lang-div2" lang="fr"></div>
<div id="pseudo-lang-div3" lang="en-AU"></div>
<div id="pseudo-lang-div4" lang="es"></div>
</div>
<div id="pseudo-ui">
<input id="pseudo-ui-input1" type="text"/>
<input id="pseudo-ui-input2" type="password"/>
<input id="pseudo-ui-input3" type="radio"/>
<input id="pseudo-ui-input4" type="radio" checked="checked"/>
<input id="pseudo-ui-input5" type="checkbox"/>
<input id="pseudo-ui-input6" type="checkbox" checked="checked"/>
<input id="pseudo-ui-input7" type="submit"/>
<input id="pseudo-ui-input8" type="button"/>
<input id="pseudo-ui-input9" type="hidden"/>
<textarea id="pseudo-ui-textarea1"></textarea>
<button id="pseudo-ui-button1">Enabled</button>
<input id="pseudo-ui-input10" disabled="disabled" type="text"/>
<input id="pseudo-ui-input11" disabled="disabled" type="password"/>
<input id="pseudo-ui-input12" disabled="disabled" type="radio"/>
<input id="pseudo-ui-input13" disabled="disabled" type="radio" checked="checked"/>
<input id="pseudo-ui-input14" disabled="disabled" type="checkbox"/>
<input id="pseudo-ui-input15" disabled="disabled" type="checkbox" checked="checked"/>
<input id="pseudo-ui-input16" disabled="disabled" type="submit"/>
<input id="pseudo-ui-input17" disabled="disabled" type="button"/>
<input id="pseudo-ui-input18" disabled="disabled" type="hidden"/>
<textarea id="pseudo-ui-textarea2" disabled="disabled"></textarea>
<button id="pseudo-ui-button2" disabled="disabled">Disabled</button>
</div>
<div id="not">
<div id="not-div1"></div>
<div id="not-div2"></div>
<div id="not-div3"></div>
<p id="not-p1"><span id="not-span1"></span><em id="not-em1"></em></p>
<p id="not-p2"><span id="not-span2"></span><em id="not-em2"></em></p>
<p id="not-p3"><span id="not-span3"></span><em id="not-em3"></em></p>
</div>
<div id="pseudo-element">All pseudo-element tests</div>
<div id="class">
<p id="class-p1" class="foo class-p bar"></p>
<p id="class-p2" class="class-p foo bar"></p>
<p id="class-p3" class="foo bar class-p"></p>
<!-- All permutations of the classes should match -->
<div id="class-div1" class="apple orange banana"></div>
<div id="class-div2" class="apple banana orange"></div>
<p id="class-p4" class="orange apple banana"></p>
<div id="class-div3" class="orange banana apple"></div>
<p id="class-p6" class="banana apple orange"></p>
<div id="class-div4" class="banana orange apple"></div>
<div id="class-div5" class="apple orange"></div>
<div id="class-div6" class="apple banana"></div>
<div id="class-div7" class="orange banana"></div>
<span id="class-span1" class="台北Táiběi 台北"></span>
<span id="class-span2" class="台北"></span>
<span id="class-span3" class="foo:bar"></span>
<span id="class-span4" class="test.foo[5]bar"></span>
</div>
<div id="id">
<div id="id-div1"></div>
<div id="id-div2"></div>
<ul id="id-ul1">
<li id="id-li-duplicate"></li>
<li id="id-li-duplicate"></li>
<li id="id-li-duplicate"></li>
<li id="id-li-duplicate"></li>
</ul>
<span id="台北Táiběi"></span>
<span id="台北"></span>
<span id="#foo:bar"></span>
<span id="test.foo[5]bar"></span>
</div>
<div id="descendant">
<div id="descendant-div1" class="descendant-div1">
<div id="descendant-div2" class="descendant-div2">
<div id="descendant-div3" class="descendant-div3">
</div>
</div>
</div>
<div id="descendant-div4" class="descendant-div4"></div>
</div>
<div id="child">
<div id="child-div1" class="child-div1">
<div id="child-div2" class="child-div2">
<div id="child-div3" class="child-div3">
</div>
</div>
</div>
<div id="child-div4" class="child-div4"></div>
</div>
<div id="adjacent">
<div id="adjacent-div1" class="adjacent-div1"></div>
<div id="adjacent-div2" class="adjacent-div2">
<div id="adjacent-div3" class="adjacent-div3"></div>
</div>
<div id="adjacent-div4" class="adjacent-div4">
<p id="adjacent-p1" class="adjacent-p1"></p>
<div id="adjacent-div5" class="adjacent-div5"></div>
</div>
<div id="adjacent-div6" class="adjacent-div6"></div>
<p id="adjacent-p2" class="adjacent-p2"></p>
<p id="adjacent-p3" class="adjacent-p3"></p>
</div>
<div id="sibling">
<div id="sibling-div1" class="sibling-div"></div>
<div id="sibling-div2" class="sibling-div">
<div id="sibling-div3" class="sibling-div"></div>
</div>
<div id="sibling-div4" class="sibling-div">
<p id="sibling-p1" class="sibling-p"></p>
<div id="sibling-div5" class="sibling-div"></div>
</div>
<div id="sibling-div6" class="sibling-div"></div>
<p id="sibling-p2" class="sibling-p"></p>
<p id="sibling-p3" class="sibling-p"></p>
</div>
<div id="group">
<em id="group-em1"></em>
<strong id="group-strong1"></strong>
</div>
</div>
</body>
</html>

48
cssselect2/tests/ids.html Normal file
View File

@ -0,0 +1,48 @@
<html id="html" xmlns="http://www.w3.org/1999/xhtml"><head>
<link id="link-href" href="foo" />
<link id="link-nohref" />
</head><body>
<div id="outer-div">
<a id="name-anchor" name="foo"></a>
<a id="tag-anchor" rel="tag" href="http://localhost/foo">link</a>
<a id="nofollow-anchor" rel="nofollow" href="https://example.org">
link</a>
<ol id="first-ol" class="a b c">
<li id="first-li">content</li>
<li id="second-li" lang="En-us">
<div id="li-div">
</div>
</li>
<li id="third-li" class="ab c"></li>
<li id="fourth-li" class="ab
c"></li>
<li id="fifth-li"></li>
<li id="sixth-li"></li>
<li id="seventh-li"> </li>
</ol>
<p id="paragraph">
<b id="p-b">hi</b> <em id="p-em">there</em>
<b id="p-b2">guy</b>
<input type="checkbox" id="checkbox-unchecked" />
<input type="checkbox" id="checkbox-disabled" disabled="" />
<input type="text" id="text-checked" checked="checked" />
<input type="hidden" id="input-hidden" />
<input type="hidden" id="input-hidden-disabled" disabled="disabled" />
<input type="checkbox" id="checkbox-checked" checked="checked" />
<input type="checkbox" id="checkbox-disabled-checked"
disabled="disabled" checked="checked" />
<fieldset id="fieldset" disabled="disabled">
<input type="checkbox" id="checkbox-fieldset-disabled" />
<input type="hidden" id="hidden-fieldset-disabled" />
</fieldset>
</p>
<ol id="second-ol">
</ol>
<map name="dummymap">
<area shape="circle" coords="200,250,25" href="foo.html" id="area-href" />
<area shape="default" id="area-nohref" />
</map>
</div>
<div id="foobar-div" foobar="ab bc
cde"><span id="foobar-span"></span></div>
</body></html>

View File

@ -0,0 +1,36 @@
[
{"name": "Empty String", "selector": ""},
{"name": "Invalid character", "selector": "["},
{"name": "Invalid character", "selector": "]"},
{"name": "Invalid character", "selector": "("},
{"name": "Invalid character", "selector": ")"},
{"name": "Invalid character", "selector": "{"},
{"name": "Invalid character", "selector": "}"},
{"name": "Invalid character", "selector": "<"},
{"name": "Invalid character", "selector": ">"},
{"name": "Invalid character", "selector": ":"},
{"name": "Invalid character", "selector": "::"},
{"name": "Invalid ID", "selector": "#"},
{"name": "Invalid group of selectors", "selector": "div,"},
{"name": "Invalid class", "selector": "."},
{"name": "Invalid class", "selector": ".5cm"},
{"name": "Invalid class", "selector": "..test"},
{"name": "Invalid class", "selector": ".foo..quux"},
{"name": "Invalid class", "selector": ".bar."},
{"name": "Invalid combinator", "selector": "div & address, p"},
{"name": "Invalid combinator", "selector": "div >> address, p"},
{"name": "Invalid combinator", "selector": "div ++ address, p"},
{"name": "Invalid combinator", "selector": "div ~~ address, p"},
{"name": "Invalid [att=value] selector", "selector": "[*=test]"},
{"name": "Invalid [att=value] selector", "selector": "[*|*=test]"},
{"name": "Invalid [att=value] selector", "selector": "[class= space unquoted ]"},
{"name": "Unknown pseudo-class", "selector": "div:example"},
{"name": "Unknown pseudo-class", "selector": ":example"},
{"name": "Unknown pseudo-element", "selector": "div::example", "xfail": true},
{"name": "Unknown pseudo-element", "selector": "::example", "xfail": true},
{"name": "Invalid pseudo-element", "selector": ":::before"},
{"name": "Undeclared namespace", "selector": "ns|div"},
{"name": "Undeclared namespace", "selector": ":not(ns|div)"},
{"name": "Invalid namespace", "selector": "^|div"},
{"name": "Invalid namespace", "selector": "$|div"}
]

View File

@ -0,0 +1,17 @@
#!/bin/sh
WEB_PLATFORM_TESTS="$1"
if [ -f "$WEB_PLATFORM_TESTS/selectors-api/selectors.js" ]
then
(
cat "$WEB_PLATFORM_TESTS/selectors-api/selectors.js"
echo "validSelectors.map(function(selector) {"
echo " delete selector.testType;"
echo "});"
echo "console.log(JSON.stringify(validSelectors, null, ' '))"
) | node
else
echo "Usage: $0 path/to/web-plateform-test"
exit;
fi

View File

@ -0,0 +1,307 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" debug="true">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
<div id="test">
<div class="dialog">
<h2>As You Like It</h2>
<div id="playwright">
by William Shakespeare
</div>
<div class="dialog scene thirdClass" id="scene1">
<h3>ACT I, SCENE III. A room in the palace.</h3>
<div class="dialog">
<div class="direction">Enter CELIA and ROSALIND</div>
</div>
<div id="speech1" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.1">Why, cousin! why, Rosalind! Cupid have mercy! not a word?</div>
</div>
<div id="speech2" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.2">Not one to throw at a dog.</div>
</div>
<div id="speech3" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.3">No, thy words are too precious to be cast away upon</div>
<div id="scene1.3.4">curs; throw some of them at me; come, lame me with reasons.</div>
</div>
<div id="speech4" class="character">ROSALIND</div>
<div id="speech5" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.8">But is all this for your father?</div>
</div>
<div class="dialog">
<div id="scene1.3.5">Then there were two cousins laid up; when the one</div>
<div id="scene1.3.6">should be lamed with reasons and the other mad</div>
<div id="scene1.3.7">without any.</div>
</div>
<div id="speech6" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.9">No, some of it is for my child's father. O, how</div>
<div id="scene1.3.10">full of briers is this working-day world!</div>
</div>
<div id="speech7" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.11">They are but burs, cousin, thrown upon thee in</div>
<div id="scene1.3.12">holiday foolery: if we walk not in the trodden</div>
<div id="scene1.3.13">paths our very petticoats will catch them.</div>
</div>
<div id="speech8" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.14">I could shake them off my coat: these burs are in my heart.</div>
</div>
<div id="speech9" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.15">Hem them away.</div>
</div>
<div id="speech10" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.16">I would try, if I could cry 'hem' and have him.</div>
</div>
<div id="speech11" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.17">Come, come, wrestle with thy affections.</div>
</div>
<div id="speech12" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.18">O, they take the part of a better wrestler than myself!</div>
</div>
<div id="speech13" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.19">O, a good wish upon you! you will try in time, in</div>
<div id="scene1.3.20">despite of a fall. But, turning these jests out of</div>
<div id="scene1.3.21">service, let us talk in good earnest: is it</div>
<div id="scene1.3.22">possible, on such a sudden, you should fall into so</div>
<div id="scene1.3.23">strong a liking with old Sir Rowland's youngest son?</div>
</div>
<div id="speech14" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.24">The duke my father loved his father dearly.</div>
</div>
<div id="speech15" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.25">Doth it therefore ensue that you should love his son</div>
<div id="scene1.3.26">dearly? By this kind of chase, I should hate him,</div>
<div id="scene1.3.27">for my father hated his father dearly; yet I hate</div>
<div id="scene1.3.28">not Orlando.</div>
</div>
<div id="speech16" class="character">ROSALIND</div>
<div title="wtf" class="dialog">
<div id="scene1.3.29">No, faith, hate him not, for my sake.</div>
</div>
<div id="speech17" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.30">Why should I not? doth he not deserve well?</div>
</div>
<div id="speech18" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.31">Let me love him for that, and do you love him</div>
<div id="scene1.3.32">because I do. Look, here comes the duke.</div>
</div>
<div id="speech19" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.33">With his eyes full of anger.</div>
<div class="direction">Enter DUKE FREDERICK, with Lords</div>
</div>
<div id="speech20" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.34">Mistress, dispatch you with your safest haste</div>
<div id="scene1.3.35">And get you from our court.</div>
</div>
<div id="speech21" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.36">Me, uncle?</div>
</div>
<div id="speech22" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.37">You, cousin</div>
<div id="scene1.3.38">Within these ten days if that thou be'st found</div>
<div id="scene1.3.39">So near our public court as twenty miles,</div>
<div id="scene1.3.40">Thou diest for it.</div>
</div>
<div id="speech23" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.41"> I do beseech your grace,</div>
<div id="scene1.3.42">Let me the knowledge of my fault bear with me:</div>
<div id="scene1.3.43">If with myself I hold intelligence</div>
<div id="scene1.3.44">Or have acquaintance with mine own desires,</div>
<div id="scene1.3.45">If that I do not dream or be not frantic,--</div>
<div id="scene1.3.46">As I do trust I am not--then, dear uncle,</div>
<div id="scene1.3.47">Never so much as in a thought unborn</div>
<div id="scene1.3.48">Did I offend your highness.</div>
</div>
<div id="speech24" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.49">Thus do all traitors:</div>
<div id="scene1.3.50">If their purgation did consist in words,</div>
<div id="scene1.3.51">They are as innocent as grace itself:</div>
<div id="scene1.3.52">Let it suffice thee that I trust thee not.</div>
</div>
<div id="speech25" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.53">Yet your mistrust cannot make me a traitor:</div>
<div id="scene1.3.54">Tell me whereon the likelihood depends.</div>
</div>
<div id="speech26" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.55">Thou art thy father's daughter; there's enough.</div>
</div>
<div id="speech27" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.56">So was I when your highness took his dukedom;</div>
<div id="scene1.3.57">So was I when your highness banish'd him:</div>
<div id="scene1.3.58">Treason is not inherited, my lord;</div>
<div id="scene1.3.59">Or, if we did derive it from our friends,</div>
<div id="scene1.3.60">What's that to me? my father was no traitor:</div>
<div id="scene1.3.61">Then, good my liege, mistake me not so much</div>
<div id="scene1.3.62">To think my poverty is treacherous.</div>
</div>
<div id="speech28" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.63">Dear sovereign, hear me speak.</div>
</div>
<div id="speech29" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.64">Ay, Celia; we stay'd her for your sake,</div>
<div id="scene1.3.65">Else had she with her father ranged along.</div>
</div>
<div id="speech30" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.66">I did not then entreat to have her stay;</div>
<div id="scene1.3.67">It was your pleasure and your own remorse:</div>
<div id="scene1.3.68">I was too young that time to value her;</div>
<div id="scene1.3.69">But now I know her: if she be a traitor,</div>
<div id="scene1.3.70">Why so am I; we still have slept together,</div>
<div id="scene1.3.71">Rose at an instant, learn'd, play'd, eat together,</div>
<div id="scene1.3.72">And wheresoever we went, like Juno's swans,</div>
<div id="scene1.3.73">Still we went coupled and inseparable.</div>
</div>
<div id="speech31" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.74">She is too subtle for thee; and her smoothness,</div>
<div id="scene1.3.75">Her very silence and her patience</div>
<div id="scene1.3.76">Speak to the people, and they pity her.</div>
<div id="scene1.3.77">Thou art a fool: she robs thee of thy name;</div>
<div id="scene1.3.78">And thou wilt show more bright and seem more virtuous</div>
<div id="scene1.3.79">When she is gone. Then open not thy lips:</div>
<div id="scene1.3.80">Firm and irrevocable is my doom</div>
<div id="scene1.3.81">Which I have pass'd upon her; she is banish'd.</div>
</div>
<div id="speech32" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.82">Pronounce that sentence then on me, my liege:</div>
<div id="scene1.3.83">I cannot live out of her company.</div>
</div>
<div id="speech33" class="character">DUKE FREDERICK</div>
<div class="dialog">
<div id="scene1.3.84">You are a fool. You, niece, provide yourself:</div>
<div id="scene1.3.85">If you outstay the time, upon mine honour,</div>
<div id="scene1.3.86">And in the greatness of my word, you die.</div>
<div class="direction">Exeunt DUKE FREDERICK and Lords</div>
</div>
<div id="speech34" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.87">O my poor Rosalind, whither wilt thou go?</div>
<div id="scene1.3.88">Wilt thou change fathers? I will give thee mine.</div>
<div id="scene1.3.89">I charge thee, be not thou more grieved than I am.</div>
</div>
<div id="speech35" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.90">I have more cause.</div>
</div>
<div id="speech36" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.91"> Thou hast not, cousin;</div>
<div id="scene1.3.92">Prithee be cheerful: know'st thou not, the duke</div>
<div id="scene1.3.93">Hath banish'd me, his daughter?</div>
</div>
<div id="speech37" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.94">That he hath not.</div>
</div>
<div id="speech38" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.95">No, hath not? Rosalind lacks then the love</div>
<div id="scene1.3.96">Which teacheth thee that thou and I am one:</div>
<div id="scene1.3.97">Shall we be sunder'd? shall we part, sweet girl?</div>
<div id="scene1.3.98">No: let my father seek another heir.</div>
<div id="scene1.3.99">Therefore devise with me how we may fly,</div>
<div id="scene1.3.100">Whither to go and what to bear with us;</div>
<div id="scene1.3.101">And do not seek to take your change upon you,</div>
<div id="scene1.3.102">To bear your griefs yourself and leave me out;</div>
<div id="scene1.3.103">For, by this heaven, now at our sorrows pale,</div>
<div id="scene1.3.104">Say what thou canst, I'll go along with thee.</div>
</div>
<div id="speech39" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.105">Why, whither shall we go?</div>
</div>
<div id="speech40" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.106">To seek my uncle in the forest of Arden.</div>
</div>
<div id="speech41" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.107">Alas, what danger will it be to us,</div>
<div id="scene1.3.108">Maids as we are, to travel forth so far!</div>
<div id="scene1.3.109">Beauty provoketh thieves sooner than gold.</div>
</div>
<div id="speech42" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.110">I'll put myself in poor and mean attire</div>
<div id="scene1.3.111">And with a kind of umber smirch my face;</div>
<div id="scene1.3.112">The like do you: so shall we pass along</div>
<div id="scene1.3.113">And never stir assailants.</div>
</div>
<div id="speech43" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.114">Were it not better,</div>
<div id="scene1.3.115">Because that I am more than common tall,</div>
<div id="scene1.3.116">That I did suit me all points like a man?</div>
<div id="scene1.3.117">A gallant curtle-axe upon my thigh,</div>
<div id="scene1.3.118">A boar-spear in my hand; and--in my heart</div>
<div id="scene1.3.119">Lie there what hidden woman's fear there will--</div>
<div id="scene1.3.120">We'll have a swashing and a martial outside,</div>
<div id="scene1.3.121">As many other mannish cowards have</div>
<div id="scene1.3.122">That do outface it with their semblances.</div>
</div>
<div id="speech44" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.123">What shall I call thee when thou art a man?</div>
</div>
<div id="speech45" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.124">I'll have no worse a name than Jove's own page;</div>
<div id="scene1.3.125">And therefore look you call me Ganymede.</div>
<div id="scene1.3.126">But what will you be call'd?</div>
</div>
<div id="speech46" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.127">Something that hath a reference to my state</div>
<div id="scene1.3.128">No longer Celia, but Aliena.</div>
</div>
<div id="speech47" class="character">ROSALIND</div>
<div class="dialog">
<div id="scene1.3.129">But, cousin, what if we assay'd to steal</div>
<div id="scene1.3.130">The clownish fool out of your father's court?</div>
<div id="scene1.3.131">Would he not be a comfort to our travel?</div>
</div>
<div id="speech48" class="character">CELIA</div>
<div class="dialog">
<div id="scene1.3.132">He'll go along o'er the wide world with me;</div>
<div id="scene1.3.133">Leave me alone to woo him. Let's away,</div>
<div id="scene1.3.134">And get our jewels and our wealth together,</div>
<div id="scene1.3.135">Devise the fittest time and safest way</div>
<div id="scene1.3.136">To hide us from pursuit that will be made</div>
<div id="scene1.3.137">After my flight. Now go we in content</div>
<div id="scene1.3.138">To liberty and not to banishment.</div>
<div class="direction">Exeunt</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,317 @@
# coding: utf8
"""
cssselect2.tests
----------------
Test suite for cssselect2.
:copyright: (c) 2012 by Simon Sapin, 2017 by Guillaume Ayoub.
:license: BSD, see LICENSE for more details.
"""
import json
import os.path
import xml.etree.ElementTree as etree
import pytest
from cssselect2 import ElementWrapper, SelectorError, compile_selector_list
def resource(filename):
return os.path.join(os.path.dirname(__file__), filename)
def load_json(filename):
return json.load(open(resource(filename)))
def get_test_document():
document = etree.parse(resource('content.xhtml'))
parent = next(e for e in document.getiterator() if e.get('id') == 'root')
# Setup namespace tests
for id in ('any-namespace', 'no-namespace'):
div = etree.SubElement(parent, '{http://www.w3.org/1999/xhtml}div')
div.set('id', id)
etree.SubElement(div, '{http://www.w3.org/1999/xhtml}div') \
.set('id', id + '-div1')
etree.SubElement(div, '{http://www.w3.org/1999/xhtml}div') \
.set('id', id + '-div2')
etree.SubElement(div, 'div').set('id', id + '-div3')
etree.SubElement(div, '{http://www.example.org/ns}div') \
.set('id', id + '-div4')
return document
TEST_DOCUMENT = get_test_document()
@pytest.mark.parametrize('test', load_json('invalid_selectors.json'))
def test_invalid_selectors(test):
if test.get('xfail'):
pytest.xfail()
try:
compile_selector_list(test['selector'])
except SelectorError:
pass
else:
raise AssertionError('Should be invalid: %(selector)r %(name)s' % test)
@pytest.mark.parametrize('test', load_json('valid_selectors.json'))
def test_valid_selectors(test):
if test.get('xfail'):
pytest.xfail()
exclude = test.get('exclude', ())
if 'document' in exclude or 'xhtml' in exclude:
return
root = ElementWrapper.from_xml_root(TEST_DOCUMENT)
result = [e.id for e in root.query_all(test['selector'])]
if result != test['expect']:
print(test['selector'])
print(result)
print('!=')
print(test['expect'])
raise AssertionError(test['name'])
def test_lang():
doc = etree.fromstring('''
<html xmlns="http://www.w3.org/1999/xhtml"></html>
''')
assert not ElementWrapper.from_xml_root(doc).matches(':lang(fr)')
doc = etree.fromstring('''
<html xmlns="http://www.w3.org/1999/xhtml">
<meta http-equiv="Content-Language" content=" fr \t"/>
</html>
''')
root = ElementWrapper.from_xml_root(doc, content_language='en')
assert root.matches(':lang(fr)')
doc = etree.fromstring('''
<html>
<meta http-equiv="Content-Language" content=" fr \t"/>
</html>
''')
root = ElementWrapper.from_xml_root(doc, content_language='en')
assert root.matches(':lang(en)')
doc = etree.fromstring('<html></html>')
root = ElementWrapper.from_xml_root(doc, content_language='en')
assert root.matches(':lang(en)')
root = ElementWrapper.from_xml_root(doc, content_language='en, es')
assert not root.matches(':lang(en)')
root = ElementWrapper.from_xml_root(doc)
assert not root.matches(':lang(en)')
doc = etree.fromstring('<html lang="eN"></html>')
root = ElementWrapper.from_html_root(doc)
assert root.matches(':lang(en)')
doc = etree.fromstring('<html lang="eN"></html>')
root = ElementWrapper.from_xml_root(doc)
assert not root.matches(':lang(en)')
def test_select():
root = etree.fromstring(HTML_IDS)
def select_ids(selector, html_only):
xml_ids = [element.etree_element.get('id', 'nil') for element in
ElementWrapper.from_xml_root(root).query_all(selector)]
html_ids = [element.etree_element.get('id', 'nil') for element in
ElementWrapper.from_html_root(root).query_all(selector)]
if html_only:
assert xml_ids == []
else:
assert xml_ids == html_ids
return html_ids
def pcss(main, *selectors, **kwargs):
html_only = kwargs.pop('html_only', False)
result = select_ids(main, html_only)
for selector in selectors:
assert select_ids(selector, html_only) == result
return result
all_ids = pcss('*')
assert all_ids[:6] == [
'html', 'nil', 'link-href', 'link-nohref', 'nil', 'outer-div']
assert all_ids[-1:] == ['foobar-span']
assert pcss('div') == ['outer-div', 'li-div', 'foobar-div']
assert pcss('DIV', html_only=True) == [
'outer-div', 'li-div', 'foobar-div'] # case-insensitive in HTML
assert pcss('div div') == ['li-div']
assert pcss('div, div div') == ['outer-div', 'li-div', 'foobar-div']
assert pcss('div , div div') == ['outer-div', 'li-div', 'foobar-div']
assert pcss('a[name]') == ['name-anchor']
assert pcss('a[NAme]', html_only=True) == [
'name-anchor'] # case-insensitive in HTML:
assert pcss('a[rel]') == ['tag-anchor', 'nofollow-anchor']
assert pcss('a[rel="tag"]') == ['tag-anchor']
assert pcss('a[href*="localhost"]') == ['tag-anchor']
assert pcss('a[href*=""]') == []
assert pcss('a[href^="http"]') == ['tag-anchor', 'nofollow-anchor']
assert pcss('a[href^="http:"]') == ['tag-anchor']
assert pcss('a[href^=""]') == []
assert pcss('a[href$="org"]') == ['nofollow-anchor']
assert pcss('a[href$=""]') == []
assert pcss('div[foobar~="bc"]', 'div[foobar~="cde"]') == [
'foobar-div']
assert pcss('[foobar~="ab bc"]',
'[foobar~=""]', '[foobar~=" \t"]') == []
assert pcss('div[foobar~="cd"]') == []
assert pcss('*[lang|="En"]', '[lang|="En-us"]') == ['second-li']
# Attribute values are case sensitive
assert pcss('*[lang|="en"]', '[lang|="en-US"]') == []
assert pcss('*[lang|="e"]') == []
# ... :lang() is not.
assert pcss(
':lang(EN)', '*:lang(en-US)'
':lang(En)'
) == ['second-li', 'li-div']
assert pcss(':lang(e)' # , html_only=True
) == []
assert pcss('li:nth-child(3)') == ['third-li']
assert pcss('li:nth-child(10)') == []
assert pcss('li:nth-child(2n)', 'li:nth-child(even)',
'li:nth-child(2n+0)') == [
'second-li', 'fourth-li', 'sixth-li']
assert pcss('li:nth-child(+2n+1)', 'li:nth-child(odd)') == [
'first-li', 'third-li', 'fifth-li', 'seventh-li']
assert pcss('li:nth-child(2n+4)') == ['fourth-li', 'sixth-li']
assert pcss('li:nth-child(3n+1)') == [
'first-li', 'fourth-li', 'seventh-li']
assert pcss('li:nth-last-child(1)') == ['seventh-li']
assert pcss('li:nth-last-child(0)') == []
assert pcss('li:nth-last-child(2n+2)', 'li:nth-last-child(even)') == [
'second-li', 'fourth-li', 'sixth-li']
assert pcss('li:nth-last-child(2n+4)') == ['second-li', 'fourth-li']
assert pcss('ol:first-of-type') == ['first-ol']
assert pcss('ol:nth-child(1)') == []
assert pcss('ol:nth-of-type(2)') == ['second-ol']
assert pcss('ol:nth-last-of-type(2)') == ['first-ol']
assert pcss('span:only-child') == ['foobar-span']
assert pcss('div:only-child') == ['li-div']
assert pcss('div *:only-child') == ['li-div', 'foobar-span']
assert pcss('p *:only-of-type') == ['p-em', 'fieldset']
assert pcss('p:only-of-type') == ['paragraph']
assert pcss('a:empty', 'a:EMpty') == ['name-anchor']
assert pcss('li:empty') == [
'third-li', 'fourth-li', 'fifth-li', 'sixth-li']
assert pcss(':root', 'html:root') == ['html']
assert pcss('li:root', '* :root') == []
assert pcss('.a', '.b', '*.a', 'ol.a') == ['first-ol']
assert pcss('.c', '*.c') == ['first-ol', 'third-li', 'fourth-li']
assert pcss('ol *.c', 'ol li.c', 'li ~ li.c', 'ol > li.c') == [
'third-li', 'fourth-li']
assert pcss('#first-li', 'li#first-li', '*#first-li') == ['first-li']
assert pcss('li div', 'li > div', 'div div') == ['li-div']
assert pcss('div > div') == []
assert pcss('div>.c', 'div > .c') == ['first-ol']
assert pcss('div + div') == ['foobar-div']
assert pcss('a ~ a') == ['tag-anchor', 'nofollow-anchor']
assert pcss('a[rel="tag"] ~ a') == ['nofollow-anchor']
assert pcss('ol#first-ol li:last-child') == ['seventh-li']
assert pcss('ol#first-ol *:last-child') == ['li-div', 'seventh-li']
assert pcss('#outer-div:first-child') == ['outer-div']
assert pcss('#outer-div :first-child') == [
'name-anchor', 'first-li', 'li-div', 'p-b',
'checkbox-fieldset-disabled', 'area-href']
assert pcss('a[href]') == ['tag-anchor', 'nofollow-anchor']
assert pcss(':not(*)') == []
assert pcss('a:not([href])') == ['name-anchor']
assert pcss('ol :Not([class])') == [
'first-li', 'second-li', 'li-div',
'fifth-li', 'sixth-li', 'seventh-li']
# Invalid characters in XPath element names, should not crash
assert pcss(r'di\a0 v', r'div\[') == []
assert pcss(r'[h\a0 ref]', r'[h\]ref]') == []
assert pcss(':link') == [
'link-href', 'tag-anchor', 'nofollow-anchor', 'area-href']
assert pcss('HTML :link', html_only=True) == [
'link-href', 'tag-anchor', 'nofollow-anchor', 'area-href']
assert pcss(':visited') == []
assert pcss(':enabled') == [
'link-href', 'tag-anchor', 'nofollow-anchor',
'checkbox-unchecked', 'text-checked', 'input-hidden',
'checkbox-checked', 'area-href']
assert pcss(':disabled') == [
'checkbox-disabled', 'input-hidden-disabled',
'checkbox-disabled-checked', 'fieldset',
'checkbox-fieldset-disabled',
'hidden-fieldset-disabled']
assert pcss(':checked') == [
'checkbox-checked', 'checkbox-disabled-checked']
def test_select_shakespeare():
document = etree.fromstring(HTML_SHAKESPEARE)
body = document.find('.//{http://www.w3.org/1999/xhtml}body')
body = ElementWrapper.from_xml_root(body)
def count(selector):
return sum(1 for _ in body.query_all(selector))
# Data borrowed from http://mootools.net/slickspeed/
# # Changed from original; probably because I'm only
# # searching the body.
# assert count('*') == 252
assert count('*') == 246
# assert count('div:contains(CELIA)') == 26
assert count('div:only-child') == 22 # ?
assert count('div:nth-child(even)') == 106
assert count('div:nth-child(2n)') == 106
assert count('div:nth-child(odd)') == 137
assert count('div:nth-child(2n+1)') == 137
assert count('div:nth-child(n)') == 243
assert count('div:last-child') == 53
assert count('div:first-child') == 51
assert count('div > div') == 242
assert count('div + div') == 190
assert count('div ~ div') == 190
assert count('body') == 1
assert count('body div') == 243
assert count('div') == 243
assert count('div div') == 242
assert count('div div div') == 241
assert count('div, div, div') == 243
assert count('div, a, span') == 243
assert count('.dialog') == 51
assert count('div.dialog') == 51
assert count('div .dialog') == 51
assert count('div.character, div.dialog') == 99
assert count('div.direction.dialog') == 0
assert count('div.dialog.direction') == 0
assert count('div.dialog.scene') == 1
assert count('div.scene.scene') == 1
assert count('div.scene .scene') == 0
assert count('div.direction .dialog ') == 0
assert count('div .dialog .direction') == 4
assert count('div.dialog .dialog .direction') == 4
assert count('#speech5') == 1
assert count('div#speech5') == 1
assert count('div #speech5') == 1
assert count('div.scene div.dialog') == 49
assert count('div#scene1 div.dialog div') == 142
assert count('#scene1 #speech1') == 1
assert count('div[class]') == 103
assert count('div[class=dialog]') == 50
assert count('div[class^=dia]') == 51
assert count('div[class$=log]') == 50
assert count('div[class*=sce]') == 1
assert count('div[class|=dialog]') == 50 # ? Seems right
# assert count('div[class!=madeup]') == 243 # ? Seems right
assert count('div[class~=dialog]') == 51 # ? Seems right
HTML_IDS = open(resource('ids.html')).read()
HTML_SHAKESPEARE = open(resource('shakespeare.html')).read()

File diff suppressed because it is too large Load Diff

366
cssselect2/tree.py Normal file
View File

@ -0,0 +1,366 @@
# coding: utf8
from __future__ import unicode_literals
import xml.etree.ElementTree as etree
from webencodings import ascii_lower
from ._compat import basestring, ifilter
from .compiler import compile_selector_list, split_whitespace
class cached_property(object):
# Borrowed from Werkzeug
# https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/utils.py
def __init__(self, func, name=None, doc=None):
self.__name__ = name or func.__name__
self.__module__ = func.__module__
self.__doc__ = doc or func.__doc__
self.func = func
def __get__(self, obj, type=None, __missing=object()):
if obj is None:
return self
value = obj.__dict__.get(self.__name__, __missing)
if value is __missing:
value = self.func(obj)
obj.__dict__[self.__name__] = value
return value
class ElementWrapper(object):
"""
A wrapper for an ElementTree :class:`~xml.etree.ElementTree.Element`
for Selector matching.
This class should not be instanciated directly.
:meth:`from_xml_root` or :meth:`from_html_root` should be used
for the root element of a document,
and other elements should be accessed (and wrappers generated)
using methods such as :meth:`iter_children` and :meth:`iter_subtree`.
:class:`ElementWrapper` objects compare equal
if their underlying :class:`~xml.etree.ElementTree.Element` do.
"""
@classmethod
def from_xml_root(cls, root, content_language=None):
"""Wrap for selector matching the root of an XML or XHTML document.
:param root:
An ElementTree :class:`~xml.etree.ElementTree.Element`
for the root element of a document.
If the given element is not the root,
selector matching will behave is if it were.
In other words, selectors will be `scope-contained`_
to the subtree rooted at that element.
:returns:
A new :class:`ElementWrapper`
.. _scope-contained:
http://dev.w3.org/csswg/selectors4/#scope-contained-selectors
"""
return cls._from_root(root, content_language, in_html_document=False)
@classmethod
def from_html_root(cls, root, content_language=None):
"""Same as :meth:`from_xml_root`,
but for documents parsed with an HTML parser
like `html5lib <http://html5lib.readthedocs.org/>`_,
which should be the case of documents with the ``text/html`` MIME type.
Compared to :meth:`from_xml_root`,
this makes element attribute names in Selectors case-insensitive.
"""
return cls._from_root(root, content_language, in_html_document=True)
@classmethod
def _from_root(cls, root, content_language, in_html_document=True):
if hasattr(root, 'getroot'):
root = root.getroot()
return cls(root, parent=None, index=0, previous=None,
in_html_document=in_html_document,
content_language=content_language)
def __init__(self, etree_element, parent, index, previous,
in_html_document, content_language=None):
#: The underlying ElementTree :class:`~xml.etree.ElementTree.Element`
self.etree_element = etree_element
#: The parent :class:`ElementWrapper`,
#: or :obj:`None` for the root element.
self.parent = parent
#: The previous sibling :class:`ElementWrapper`,
#: or :obj:`None` for the root element.
self.previous = previous
if parent is not None:
#: The :attr:`parent`s children
#: as a list of
#: ElementTree :class:`~xml.etree.ElementTree.Element`s.
#: For the root (which has no parent)
self.etree_siblings = parent.etree_children
else:
self.etree_siblings = [etree_element]
#: The position within the :attr:`parent`s children, counting from 0.
#: ``e.etree_siblings[e.index]`` is always ``e.etree_element``.
self.index = index
self.in_html_document = in_html_document
self.transport_content_language = content_language
def __eq__(self, other):
return (type(self) == type(other) and
self.etree_element == other.etree_element)
def __ne__(self, other):
return not (self == other)
def __hash__(self):
return hash((type(self), self.etree_element))
def __iter__(self):
for element in self.iter_children():
yield element
def iter_ancestors(self):
"""Return an iterator of existing :class:`ElementWrapper` objects
for this elements ancestors,
in reversed tree order (from :attr:`parent` to the root)
The element itself is not included,
this is an empty sequence for the root element.
"""
element = self
while element.parent is not None:
element = element.parent
yield element
def iter_previous_siblings(self):
"""Return an iterator of existing :class:`ElementWrapper` objects
for this elements previous siblings,
in reversed tree order.
The element itself is not included,
this is an empty sequence for a first child or the root element.
"""
element = self
while element.previous is not None:
element = element.previous
yield element
def iter_children(self):
"""Return an iterator of newly-created :class:`ElementWrapper` objects
for this elements child elements,
in tree order.
"""
child = None
for i, etree_child in enumerate(self.etree_children):
child = type(self)(
etree_child,
parent=self,
index=i,
previous=child,
in_html_document=self.in_html_document,
)
yield child
def iter_subtree(self):
"""Return an iterator of newly-created :class:`ElementWrapper` objects
for the entire subtree rooted at this element,
in tree order.
Unlike in other methods, the element itself *is* included.
This loops over an entire document:
.. code-block:: python
for element in ElementWrapper.from_root(root_etree).iter_subtree():
...
"""
stack = [iter([self])]
while stack:
element = next(stack[-1], None)
if element is None:
stack.pop()
else:
yield element
stack.append(element.iter_children())
@staticmethod
def _compile(selectors):
return [
compiled_selector.test
for selector in selectors
for compiled_selector in (
[selector] if hasattr(selector, 'test')
else compile_selector_list(selector)
)
if compiled_selector.pseudo_element is None and
not compiled_selector.never_matches
]
def matches(self, *selectors):
"""Return wether this elememt matches any of the given selectors.
:param selectors:
Each given selector is either a :class:`CompiledSelector`,
or an argument to :func:`compile_selector_list`.
"""
return any(test(self) for test in self._compile(selectors))
def query_all(self, *selectors):
"""
Return elements, in tree order, that match any of the given selectors.
Selectors are `scope-filtered`_ to the subtree rooted at this element.
.. _scope-filtered: http://dev.w3.org/csswg/selectors4/#scope-filtered
:param selectors:
Each given selector is either a :class:`CompiledSelector`,
or an argument to :func:`compile_selector_list`.
:returns:
An iterator of newly-created :class:`ElementWrapper` objects.
"""
tests = self._compile(selectors)
if len(tests) == 1:
return ifilter(tests[0], self.iter_subtree())
elif selectors:
return (
element
for element in self.iter_subtree()
if any(test(element) for test in tests)
)
else:
return iter(())
def query(self, *selectors):
"""Return the first element (in tree order)
that matches any of the given selectors.
:param selectors:
Each given selector is either a :class:`CompiledSelector`,
or an argument to :func:`compile_selector_list`.
:returns:
A newly-created :class:`ElementWrapper` object,
or :obj:`None` if there is no match.
"""
return next(self.query_all(*selectors), None)
@cached_property
def etree_children(self):
"""This elements children,
as a list of ElementTree :class:`~xml.etree.ElementTree.Element`.
Other ElementTree nodes such as
:class:`comments <~xml.etree.ElementTree.Comment>` and
:class:`processing instructions
<~xml.etree.ElementTree.ProcessingInstruction>`
are not included.
"""
return [c for c in self.etree_element if isinstance(c.tag, basestring)]
@cached_property
def local_name(self):
"""The local name of this element, as a string."""
namespace_url, local_name = _split_etree_tag(self.etree_element.tag)
self.__dict__[str('namespace_url')] = namespace_url
return local_name
@cached_property
def namespace_url(self):
"""The namespace URL of this element, as a string."""
namespace_url, local_name = _split_etree_tag(self.etree_element.tag)
self.__dict__[str('local_name')] = local_name
return namespace_url
@cached_property
def id(self):
"""The ID of this element, as a string."""
return self.etree_element.get('id')
@cached_property
def classes(self):
"""The classes of this element, as a :class:`set` of strings."""
return set(split_whitespace(self.etree_element.get('class', '')))
@cached_property
def lang(self):
"""The language of this element, as a string."""
# http://whatwg.org/C#language
xml_lang = self.etree_element.get(
'{http://www.w3.org/XML/1998/namespace}lang')
if xml_lang is not None:
return ascii_lower(xml_lang)
is_html = (
self.in_html_document or
self.namespace_url == 'http://www.w3.org/1999/xhtml')
if is_html:
lang = self.etree_element.get('lang')
if lang is not None:
return ascii_lower(lang)
if self.parent is not None:
return self.parent.lang
# Root elememnt
if is_html:
content_language = None
for meta in etree_iter(self.etree_element,
'{http://www.w3.org/1999/xhtml}meta'):
http_equiv = meta.get('http-equiv', '')
if ascii_lower(http_equiv) == 'content-language':
content_language = _parse_content_language(
meta.get('content'))
if content_language is not None:
return ascii_lower(content_language)
# Empty string means unknown
return _parse_content_language(self.transport_content_language) or ''
@cached_property
def in_disabled_fieldset(self):
if self.parent is None:
return False
if (self.parent.etree_element.tag == (
'{http://www.w3.org/1999/xhtml}fieldset') and
self.parent.etree_element.get('disabled') is not None and (
self.etree_element.tag != (
'{http://www.w3.org/1999/xhtml}legend') or
any(s.etree_element.tag == (
'{http://www.w3.org/1999/xhtml}legend')
for s in self.iter_previous_siblings()))):
return True
return self.parent.in_disabled_fieldset
def _split_etree_tag(tag):
pos = tag.rfind('}')
if pos == -1:
return '', tag
else:
assert tag[0] == '{'
return tag[1:pos], tag[pos + 1:]
if hasattr(etree.Element, 'iter'):
def etree_iter(element, tag=None):
return element.iter(tag)
else:
def etree_iter(element, tag=None):
return element.getiterator(tag)
def _parse_content_language(value):
if value is not None and ',' not in value:
parts = split_whitespace(value)
if len(parts) == 1:
return parts[0]

80
docs/conf.py Normal file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# cssselect2 documentation build configuration file.
import codecs
import re
from os import path
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'cssselect2'
copyright = '2012-2017, Simon Sapin'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The full version, including alpha/beta/rc tags.
release = re.search("VERSION = '([^']+)'", codecs.open(
path.join(path.dirname(path.dirname(__file__)), 'cssselect2', '__init__.py'),
encoding='utf-8',
).read().strip()).group(1)
# The short X.Y version.
version = '.'.join(release.split('.')[:2])
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Output file base name for HTML help builder.
htmlhelp_basename = 'cssselect2doc'
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'cssselect2', 'cssselect2 Documentation',
['Simon Sapin'], 1)
]
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'cssselect2', 'cssselect2 Documentation',
'Simon Sapin', 'cssselect2', 'One line description of project.',
'Miscellaneous'),
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'py2': ('http://docs.python.org/2', None),
'py3': ('http://docs.python.org/3', None),
'webencodings': ('http://pythonhosted.org/webencodings/', None)}

41
docs/index.rst Normal file
View File

@ -0,0 +1,41 @@
.. include:: ../README.rst
Installation
============
Installing cssselect2 with pip_ should Just Work::
pip install cssselect2
This will also automatically install cssselect2s only dependency, tinycss2_.
cssselect2 and tinycss2 both only contain Python code and should work on any
Python implementation, although theyre only tested on CPython.
.. _pip: http://pip-installer.org/
.. _tinycss2: http://tinycss2.readthedocs.io/
Basic Example
=============
Here is a classical cssselect2 workflow:
- parse a CSS stylesheet using tinycss2_,
- store the CSS rules in a :meth:`~cssselect2.Matcher` object,
- parse an HTML document using an ElementTree-like parser,
- wrap the HTML tree in a :meth:`~cssselect2.ElementWrapper` object,
- find the CSS rules matching each HTML tag, using the matcher and the wrapper.
.. literalinclude:: ../example.py
.. module:: cssselect2
.. autoclass:: Matcher
:members:
.. autofunction:: compile_selector_list
.. autoclass:: ElementWrapper
:members:
.. autoclass:: SelectorError
.. include:: ../CHANGES

49
example.py Normal file
View File

@ -0,0 +1,49 @@
from xml.etree import ElementTree
import cssselect2
import tinycss2
# Parse CSS and add rules to the matcher
matcher = cssselect2.Matcher()
rules = tinycss2.parse_stylesheet('''
body { font-size: 2em }
body p { background: red }
p { color: blue }
''', skip_whitespace=True)
for rule in rules:
selectors = cssselect2.compile_selector_list(rule.prelude)
selector_string = tinycss2.serialize(rule.prelude)
content_string = tinycss2.serialize(rule.content)
payload = (selector_string, content_string)
for selector in selectors:
matcher.add_selector(selector, payload)
# Parse HTML and find CSS rules applying to each tag
html_tree = ElementTree.fromstring('''
<html>
<body>
<p>Test</p>
</body>
</html>
''')
wrapper = cssselect2.ElementWrapper.from_html_root(html_tree)
for element in wrapper.iter_subtree():
tag = element.etree_element.tag.split('}')[-1]
print('Found tag "{}" in HTML'.format(tag))
matches = matcher.match(element)
if matches:
for match in matches:
specificity, order, pseudo, payload = match
selector_string, content_string = payload
print('Matching selector "{}" ({})'.format(
selector_string, content_string))
else:
print('No rule matching this tag')
print()

14
setup.cfg Normal file
View File

@ -0,0 +1,14 @@
[aliases]
test = pytest
[bdist_wheel]
universal = 1
[tool:pytest]
addopts = --cov=cssselect2 --flake8 --isort cssselect2
norecursedirs = dist .cache .git build *.egg-info .eggs venv
[egg_info]
tag_build =
tag_date = 0

36
setup.py Executable file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env python
# coding: utf8
import os.path
import re
import sys
from setuptools import setup
ROOT = os.path.dirname(__file__)
README = open(os.path.join(ROOT, 'README.rst')).read()
INIT_PY = open(os.path.join(ROOT, 'cssselect2', '__init__.py')).read()
VERSION = re.search("VERSION = '([^']+)'", INIT_PY).group(1)
needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv)
pytest_runner = ['pytest-runner'] if needs_pytest else []
setup(
name='cssselect2',
version=VERSION,
author='Simon Sapin',
author_email='simon.sapin@exyr.org',
description='CSS selectors for Python ElementTree',
long_description=README,
url='http://packages.python.org/cssselect2/',
license='BSD',
packages=['cssselect2'],
package_data={'cssselect2': ['tests/*']},
install_requires=['tinycss2'],
setup_requires=pytest_runner,
test_suite='cssselect2.tests',
tests_require=[
'pytest-runner', 'pytest-cov', 'pytest-flake8', 'pytest-isort'],
extras_require={'test': [
'pytest-runner', 'pytest-cov', 'pytest-flake8', 'pytest-isort']},
)