Merge branch 'NoctuaNivalis-master'
This commit is contained in:
commit
2636467ea7
@ -26,6 +26,8 @@ Added
|
|||||||
- A new command `:paste-primary` got added to paste the primary selection, and
|
- A new command `:paste-primary` got added to paste the primary selection, and
|
||||||
`<Shift-Insert>` got added as a binding so it pastes primary rather than
|
`<Shift-Insert>` got added as a binding so it pastes primary rather than
|
||||||
clipboard.
|
clipboard.
|
||||||
|
- New mode `word` for `hints -> mode` which uses a dictionary and link-texts
|
||||||
|
for hints instead of single characters.
|
||||||
|
|
||||||
Changed
|
Changed
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
@ -149,6 +149,7 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* Martin Tournoij
|
* Martin Tournoij
|
||||||
* Raphael Pierzina
|
* Raphael Pierzina
|
||||||
* Joel Torstensson
|
* Joel Torstensson
|
||||||
|
* Felix Van der Jeugt
|
||||||
* Claude
|
* Claude
|
||||||
* meles5
|
* meles5
|
||||||
* Patric Schmitz
|
* Patric Schmitz
|
||||||
@ -171,7 +172,6 @@ Contributors, sorted by the number of commits in descending order:
|
|||||||
* error800
|
* error800
|
||||||
* Zach-Button
|
* Zach-Button
|
||||||
* Halfwit
|
* Halfwit
|
||||||
* Felix Van der Jeugt
|
|
||||||
* rikn00
|
* rikn00
|
||||||
* Michael Ilsaas
|
* Michael Ilsaas
|
||||||
* Martin Zimmermann
|
* Martin Zimmermann
|
||||||
|
@ -179,6 +179,7 @@
|
|||||||
|<<hints-min-chars,min-chars>>|Minimum number of chars used for hint strings.
|
|<<hints-min-chars,min-chars>>|Minimum number of chars used for hint strings.
|
||||||
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb).
|
|<<hints-scatter,scatter>>|Whether to scatter hint key chains (like Vimium) or not (like dwb).
|
||||||
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|
|<<hints-uppercase,uppercase>>|Make chars in hint strings uppercase.
|
||||||
|
|<<hints-dictionary,dictionary>>|The dictionary file to be used by the word hints.
|
||||||
|<<hints-auto-follow,auto-follow>>|Whether to auto-follow a hint if there's only one left.
|
|<<hints-auto-follow,auto-follow>>|Whether to auto-follow a hint if there's only one left.
|
||||||
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
|
|<<hints-next-regexes,next-regexes>>|A comma-separated list of regexes to use for 'next' links.
|
||||||
|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
|
|<<hints-prev-regexes,prev-regexes>>|A comma-separated list of regexes to use for 'prev' links.
|
||||||
@ -1538,6 +1539,7 @@ Valid values:
|
|||||||
|
|
||||||
* +number+: Use numeric hints.
|
* +number+: Use numeric hints.
|
||||||
* +letter+: Use the chars in the hints -> chars setting.
|
* +letter+: Use the chars in the hints -> chars setting.
|
||||||
|
* +word+: Use hints words based on the html elements and the extra words.
|
||||||
|
|
||||||
Default: +pass:[letter]+
|
Default: +pass:[letter]+
|
||||||
|
|
||||||
@ -1575,6 +1577,12 @@ Valid values:
|
|||||||
|
|
||||||
Default: +pass:[false]+
|
Default: +pass:[false]+
|
||||||
|
|
||||||
|
[[hints-dictionary]]
|
||||||
|
=== dictionary
|
||||||
|
The dictionary file to be used by the word hints.
|
||||||
|
|
||||||
|
Default: +pass:[/usr/share/dict/words]+
|
||||||
|
|
||||||
[[hints-auto-follow]]
|
[[hints-auto-follow]]
|
||||||
=== auto-follow
|
=== auto-follow
|
||||||
Whether to auto-follow a hint if there's only one left.
|
Whether to auto-follow a hint if there's only one left.
|
||||||
|
@ -19,9 +19,11 @@
|
|||||||
|
|
||||||
"""A HintManager to draw hints over links."""
|
"""A HintManager to draw hints over links."""
|
||||||
|
|
||||||
import math
|
|
||||||
import functools
|
|
||||||
import collections
|
import collections
|
||||||
|
import functools
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
|
||||||
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
from PyQt5.QtCore import (pyqtSignal, pyqtSlot, QObject, QEvent, Qt, QUrl,
|
||||||
QTimer)
|
QTimer)
|
||||||
@ -34,7 +36,8 @@ from qutebrowser.config import config
|
|||||||
from qutebrowser.keyinput import modeman, modeparsers
|
from qutebrowser.keyinput import modeman, modeparsers
|
||||||
from qutebrowser.browser import webelem
|
from qutebrowser.browser import webelem
|
||||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||||
from qutebrowser.utils import usertypes, log, qtutils, message, objreg
|
from qutebrowser.utils import (usertypes, log, qtutils, message,
|
||||||
|
objreg)
|
||||||
from qutebrowser.misc import guiprocess
|
from qutebrowser.misc import guiprocess
|
||||||
|
|
||||||
|
|
||||||
@ -47,6 +50,11 @@ Target = usertypes.enum('Target', ['normal', 'tab', 'tab_fg', 'tab_bg',
|
|||||||
'spawn'])
|
'spawn'])
|
||||||
|
|
||||||
|
|
||||||
|
class WordHintingError(Exception):
|
||||||
|
|
||||||
|
"""Exception raised on errors during word hinting."""
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot(usertypes.KeyMode)
|
@pyqtSlot(usertypes.KeyMode)
|
||||||
def on_mode_entered(mode, win_id):
|
def on_mode_entered(mode, win_id):
|
||||||
"""Stop hinting when insert mode was entered."""
|
"""Stop hinting when insert mode was entered."""
|
||||||
@ -146,6 +154,7 @@ class HintManager(QObject):
|
|||||||
self._win_id = win_id
|
self._win_id = win_id
|
||||||
self._tab_id = tab_id
|
self._tab_id = tab_id
|
||||||
self._context = None
|
self._context = None
|
||||||
|
self._word_hinter = WordHinter()
|
||||||
mode_manager = objreg.get('mode-manager', scope='window',
|
mode_manager = objreg.get('mode-manager', scope='window',
|
||||||
window=win_id)
|
window=win_id)
|
||||||
mode_manager.left.connect(self.on_mode_left)
|
mode_manager.left.connect(self.on_mode_left)
|
||||||
@ -198,7 +207,14 @@ class HintManager(QObject):
|
|||||||
Return:
|
Return:
|
||||||
A list of hint strings, in the same order as the elements.
|
A list of hint strings, in the same order as the elements.
|
||||||
"""
|
"""
|
||||||
if config.get('hints', 'mode') == 'number':
|
hint_mode = config.get('hints', 'mode')
|
||||||
|
if hint_mode == 'word':
|
||||||
|
try:
|
||||||
|
return self._word_hinter.hint(elems)
|
||||||
|
except WordHintingError as e:
|
||||||
|
message.error(self._win_id, str(e), immediately=True)
|
||||||
|
# falls back on letter hints
|
||||||
|
if hint_mode == 'number':
|
||||||
chars = '0123456789'
|
chars = '0123456789'
|
||||||
else:
|
else:
|
||||||
chars = config.get('hints', 'chars')
|
chars = config.get('hints', 'chars')
|
||||||
@ -373,7 +389,7 @@ class HintManager(QObject):
|
|||||||
label.setStyleProperty('left', '{}px !important'.format(left))
|
label.setStyleProperty('left', '{}px !important'.format(left))
|
||||||
label.setStyleProperty('top', '{}px !important'.format(top))
|
label.setStyleProperty('top', '{}px !important'.format(top))
|
||||||
|
|
||||||
def _draw_label(self, elem, string):
|
def _draw_label(self, elem, text):
|
||||||
"""Draw a hint label over an element.
|
"""Draw a hint label over an element.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -398,7 +414,7 @@ class HintManager(QObject):
|
|||||||
label = webelem.WebElementWrapper(parent.lastChild())
|
label = webelem.WebElementWrapper(parent.lastChild())
|
||||||
label['class'] = 'qutehint'
|
label['class'] = 'qutehint'
|
||||||
self._set_style_properties(elem, label)
|
self._set_style_properties(elem, label)
|
||||||
label.setPlainText(string)
|
label.setPlainText(text)
|
||||||
return label
|
return label
|
||||||
|
|
||||||
def _show_url_error(self):
|
def _show_url_error(self):
|
||||||
@ -662,14 +678,14 @@ class HintManager(QObject):
|
|||||||
elems = [e for e in elems if filterfunc(e)]
|
elems = [e for e in elems if filterfunc(e)]
|
||||||
if not elems:
|
if not elems:
|
||||||
raise cmdexc.CommandError("No elements found.")
|
raise cmdexc.CommandError("No elements found.")
|
||||||
strings = self._hint_strings(elems)
|
hints = self._hint_strings(elems)
|
||||||
for e, string in zip(elems, strings):
|
for e, hint in zip(elems, hints):
|
||||||
label = self._draw_label(e, string)
|
label = self._draw_label(e, hint)
|
||||||
self._context.elems[string] = ElemTuple(e, label)
|
self._context.elems[hint] = ElemTuple(e, label)
|
||||||
keyparsers = objreg.get('keyparsers', scope='window',
|
keyparsers = objreg.get('keyparsers', scope='window',
|
||||||
window=self._win_id)
|
window=self._win_id)
|
||||||
keyparser = keyparsers[usertypes.KeyMode.hint]
|
keyparser = keyparsers[usertypes.KeyMode.hint]
|
||||||
keyparser.update_bindings(strings)
|
keyparser.update_bindings(hints)
|
||||||
|
|
||||||
def follow_prevnext(self, frame, baseurl, prev=False, tab=False,
|
def follow_prevnext(self, frame, baseurl, prev=False, tab=False,
|
||||||
background=False, window=False):
|
background=False, window=False):
|
||||||
@ -812,11 +828,11 @@ class HintManager(QObject):
|
|||||||
def handle_partial_key(self, keystr):
|
def handle_partial_key(self, keystr):
|
||||||
"""Handle a new partial keypress."""
|
"""Handle a new partial keypress."""
|
||||||
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
log.hints.debug("Handling new keystring: '{}'".format(keystr))
|
||||||
for (string, elems) in self._context.elems.items():
|
for (text, elems) in self._context.elems.items():
|
||||||
try:
|
try:
|
||||||
if string.startswith(keystr):
|
if text.startswith(keystr):
|
||||||
matched = string[:len(keystr)]
|
matched = text[:len(keystr)]
|
||||||
rest = string[len(keystr):]
|
rest = text[len(keystr):]
|
||||||
match_color = config.get('colors', 'hints.fg.match')
|
match_color = config.get('colors', 'hints.fg.match')
|
||||||
elems.label.setInnerXml(
|
elems.label.setInnerXml(
|
||||||
'<font color="{}">{}</font>{}'.format(
|
'<font color="{}">{}</font>{}'.format(
|
||||||
@ -917,8 +933,8 @@ class HintManager(QObject):
|
|||||||
# Show all hints again
|
# Show all hints again
|
||||||
self.filter_hints(None)
|
self.filter_hints(None)
|
||||||
# Undo keystring highlighting
|
# Undo keystring highlighting
|
||||||
for (string, elems) in self._context.elems.items():
|
for (text, elems) in self._context.elems.items():
|
||||||
elems.label.setInnerXml(string)
|
elems.label.setInnerXml(text)
|
||||||
handler()
|
handler()
|
||||||
|
|
||||||
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
|
@cmdutils.register(instance='hintmanager', scope='tab', hide=True,
|
||||||
@ -961,3 +977,110 @@ class HintManager(QObject):
|
|||||||
# hinting.
|
# hinting.
|
||||||
return
|
return
|
||||||
self._cleanup()
|
self._cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
class WordHinter:
|
||||||
|
|
||||||
|
"""Generator for word hints.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
words: A set of words to be used when no "smart hint" can be
|
||||||
|
derived from the hinted element.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# will be initialized on first use.
|
||||||
|
self.words = set()
|
||||||
|
|
||||||
|
def ensure_initialized(self):
|
||||||
|
"""Generate the used words if yet uninialized."""
|
||||||
|
if not self.words:
|
||||||
|
dictionary = config.get("hints", "dictionary")
|
||||||
|
try:
|
||||||
|
with open(dictionary, encoding="UTF-8") as wordfile:
|
||||||
|
alphabet = set(string.ascii_lowercase)
|
||||||
|
hints = set()
|
||||||
|
lines = (line.rstrip().lower() for line in wordfile)
|
||||||
|
for word in lines:
|
||||||
|
if set(word) - alphabet:
|
||||||
|
# contains none-alphabetic chars
|
||||||
|
continue
|
||||||
|
if len(word) > 4:
|
||||||
|
# we don't need words longer than 4
|
||||||
|
continue
|
||||||
|
for i in range(len(word)):
|
||||||
|
# remove all prefixes of this word
|
||||||
|
hints.discard(word[:i + 1])
|
||||||
|
hints.add(word)
|
||||||
|
self.words.update(hints)
|
||||||
|
except IOError as e:
|
||||||
|
error = "Word hints requires reading the file at {}: {}"
|
||||||
|
raise WordHintingError(error.format(dictionary, str(e)))
|
||||||
|
|
||||||
|
def extract_tag_words(self, elem):
|
||||||
|
"""Extract tag words form the given element."""
|
||||||
|
attr_extractors = {
|
||||||
|
"alt": lambda elem: elem["alt"],
|
||||||
|
"name": lambda elem: elem["name"],
|
||||||
|
"title": lambda elem: elem["title"],
|
||||||
|
"src": lambda elem: elem["src"].split('/')[-1],
|
||||||
|
"href": lambda elem: elem["href"].split('/')[-1],
|
||||||
|
"text": str,
|
||||||
|
}
|
||||||
|
|
||||||
|
extractable_attrs = collections.defaultdict(
|
||||||
|
list, {
|
||||||
|
"IMG": ["alt", "title", "src"],
|
||||||
|
"A": ["title", "href", "text"],
|
||||||
|
"INPUT": ["name"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return (attr_extractors[attr](elem)
|
||||||
|
for attr in extractable_attrs[elem.tagName()]
|
||||||
|
if attr in elem or attr == "text")
|
||||||
|
|
||||||
|
def tag_words_to_hints(self, words):
|
||||||
|
"""Take words and transform them to proper hints if possible."""
|
||||||
|
for candidate in words:
|
||||||
|
if not candidate:
|
||||||
|
continue
|
||||||
|
match = re.search('[A-Za-z]{3,}', candidate)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
if 4 < match.end() - match.start() < 8:
|
||||||
|
yield candidate[match.start():match.end()].lower()
|
||||||
|
|
||||||
|
def any_prefix(self, hint, existing):
|
||||||
|
return any(hint.startswith(e) or e.startswith(hint)
|
||||||
|
for e in existing)
|
||||||
|
|
||||||
|
def new_hint_for(self, elem, existing):
|
||||||
|
"""Return a hint for elem, not conflicting with the existing."""
|
||||||
|
new = self.tag_words_to_hints(self.extract_tag_words(elem))
|
||||||
|
no_prefixes = (h for h in new if not self.any_prefix(h, existing))
|
||||||
|
# either the first good, or None
|
||||||
|
return next(no_prefixes, None)
|
||||||
|
|
||||||
|
def hint(self, elems):
|
||||||
|
"""Produce hint labels based on the html tags.
|
||||||
|
|
||||||
|
Produce hint words based on the link text and random words
|
||||||
|
from the words arg as fallback.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
words: Words to use as fallback when no link text can be used.
|
||||||
|
elems: The elements to get hint strings for.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
A list of hint strings, in the same order as the elements.
|
||||||
|
"""
|
||||||
|
self.ensure_initialized()
|
||||||
|
hints = []
|
||||||
|
used_hints = set()
|
||||||
|
words = iter(self.words)
|
||||||
|
for elem in elems:
|
||||||
|
hint = self.new_hint_for(elem, used_hints) or next(words)
|
||||||
|
used_hints.add(hint)
|
||||||
|
hints.append(hint)
|
||||||
|
return hints
|
||||||
|
@ -863,7 +863,9 @@ def data(readonly=False):
|
|||||||
valid_values=typ.ValidValues(
|
valid_values=typ.ValidValues(
|
||||||
('number', "Use numeric hints."),
|
('number', "Use numeric hints."),
|
||||||
('letter', "Use the chars in the hints -> "
|
('letter', "Use the chars in the hints -> "
|
||||||
"chars setting.")
|
"chars setting."),
|
||||||
|
('word', "Use hints words based on the html "
|
||||||
|
"elements and the extra words."),
|
||||||
)), 'letter'),
|
)), 'letter'),
|
||||||
"Mode to use for hints."),
|
"Mode to use for hints."),
|
||||||
|
|
||||||
@ -888,6 +890,10 @@ def data(readonly=False):
|
|||||||
SettingValue(typ.Bool(), 'false'),
|
SettingValue(typ.Bool(), 'false'),
|
||||||
"Make chars in hint strings uppercase."),
|
"Make chars in hint strings uppercase."),
|
||||||
|
|
||||||
|
('dictionary',
|
||||||
|
SettingValue(typ.File(required=False), '/usr/share/dict/words'),
|
||||||
|
"The dictionary file to be used by the word hints."),
|
||||||
|
|
||||||
('auto-follow',
|
('auto-follow',
|
||||||
SettingValue(typ.Bool(), 'true'),
|
SettingValue(typ.Bool(), 'true'),
|
||||||
"Whether to auto-follow a hint if there's only one left."),
|
"Whether to auto-follow a hint if there's only one left."),
|
||||||
|
@ -901,6 +901,10 @@ class File(BaseType):
|
|||||||
|
|
||||||
"""A file on the local filesystem."""
|
"""A file on the local filesystem."""
|
||||||
|
|
||||||
|
def __init__(self, required=True, **kwargs):
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.required = required
|
||||||
|
|
||||||
def transform(self, value):
|
def transform(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
@ -909,7 +913,7 @@ class File(BaseType):
|
|||||||
if not os.path.isabs(value):
|
if not os.path.isabs(value):
|
||||||
cfgdir = standarddir.config()
|
cfgdir = standarddir.config()
|
||||||
assert cfgdir is not None
|
assert cfgdir is not None
|
||||||
return os.path.join(cfgdir, value)
|
value = os.path.join(cfgdir, value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
@ -925,15 +929,13 @@ class File(BaseType):
|
|||||||
raise configexc.ValidationError(
|
raise configexc.ValidationError(
|
||||||
value, "must be an absolute path when not using a "
|
value, "must be an absolute path when not using a "
|
||||||
"config directory!")
|
"config directory!")
|
||||||
elif not os.path.isfile(os.path.join(cfgdir, value)):
|
value = os.path.join(cfgdir, value)
|
||||||
raise configexc.ValidationError(
|
not_isfile_message = ("must be a valid path relative to the "
|
||||||
value, "must be a valid path relative to the config "
|
"config directory!")
|
||||||
"directory!")
|
else:
|
||||||
else:
|
not_isfile_message = "must be a valid file!"
|
||||||
return
|
if self.required and not os.path.isfile(value):
|
||||||
elif not os.path.isfile(value):
|
raise configexc.ValidationError(value, not_isfile_message)
|
||||||
raise configexc.ValidationError(
|
|
||||||
value, "must be a valid file!")
|
|
||||||
except UnicodeEncodeError as e:
|
except UnicodeEncodeError as e:
|
||||||
raise configexc.ValidationError(value, e)
|
raise configexc.ValidationError(value, e)
|
||||||
|
|
||||||
|
@ -1268,12 +1268,21 @@ class TestRegexList:
|
|||||||
assert klass().transform(val) == expected
|
assert klass().transform(val) == expected
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def unrequired_class(**kwargs):
|
||||||
|
return configtypes.File(required=False, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures('qapp')
|
@pytest.mark.usefixtures('qapp')
|
||||||
class TestFileAndUserStyleSheet:
|
class TestFileAndUserStyleSheet:
|
||||||
|
|
||||||
"""Test File/UserStyleSheet."""
|
"""Test File/UserStyleSheet."""
|
||||||
|
|
||||||
@pytest.fixture(params=[configtypes.File, configtypes.UserStyleSheet])
|
@pytest.fixture(params=[
|
||||||
|
configtypes.File,
|
||||||
|
configtypes.UserStyleSheet,
|
||||||
|
unrequired_class,
|
||||||
|
])
|
||||||
def klass(self, request):
|
def klass(self, request):
|
||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
@ -1293,6 +1302,8 @@ class TestFileAndUserStyleSheet:
|
|||||||
return arg
|
return arg
|
||||||
elif klass is configtypes.UserStyleSheet:
|
elif klass is configtypes.UserStyleSheet:
|
||||||
return QUrl.fromLocalFile(arg)
|
return QUrl.fromLocalFile(arg)
|
||||||
|
elif klass is unrequired_class:
|
||||||
|
return arg
|
||||||
else:
|
else:
|
||||||
assert False, klass
|
assert False, klass
|
||||||
|
|
||||||
@ -1309,6 +1320,11 @@ class TestFileAndUserStyleSheet:
|
|||||||
with pytest.raises(configexc.ValidationError):
|
with pytest.raises(configexc.ValidationError):
|
||||||
configtypes.File().validate('foobar')
|
configtypes.File().validate('foobar')
|
||||||
|
|
||||||
|
def test_validate_does_not_exist_optional_file(self, os_mock):
|
||||||
|
"""Test validate with a file which does not exist (File)."""
|
||||||
|
os_mock.path.isfile.return_value = False
|
||||||
|
configtypes.File(required=False).validate('foobar')
|
||||||
|
|
||||||
def test_validate_does_not_exist_userstylesheet(self, os_mock):
|
def test_validate_does_not_exist_userstylesheet(self, os_mock):
|
||||||
"""Test validate with a file which does not exist (UserStyleSheet)."""
|
"""Test validate with a file which does not exist (UserStyleSheet)."""
|
||||||
os_mock.path.isfile.return_value = False
|
os_mock.path.isfile.return_value = False
|
||||||
@ -1343,7 +1359,9 @@ class TestFileAndUserStyleSheet:
|
|||||||
(configtypes.File(), 'foobar', True),
|
(configtypes.File(), 'foobar', True),
|
||||||
(configtypes.UserStyleSheet(), 'foobar', False),
|
(configtypes.UserStyleSheet(), 'foobar', False),
|
||||||
(configtypes.UserStyleSheet(), '\ud800', True),
|
(configtypes.UserStyleSheet(), '\ud800', True),
|
||||||
], ids=['file-foobar', 'userstylesheet-foobar', 'userstylesheet-unicode'])
|
(configtypes.File(required=False), 'foobar', False),
|
||||||
|
], ids=['file-foobar', 'userstylesheet-foobar', 'userstylesheet-unicode',
|
||||||
|
'file-optional-foobar'])
|
||||||
def test_validate_rel_inexistent(self, os_mock, monkeypatch, configtype,
|
def test_validate_rel_inexistent(self, os_mock, monkeypatch, configtype,
|
||||||
value, raises):
|
value, raises):
|
||||||
"""Test with a relative path and standarddir.config returning None."""
|
"""Test with a relative path and standarddir.config returning None."""
|
||||||
|
Loading…
Reference in New Issue
Block a user