From 7e793b9419a02188bb86f8ae7e45c7f92619753c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 11 Oct 2013 19:14:22 -0300 Subject: [PATCH 1/4] adding first version of plugins_index script --HG-- branch : plugins-index --- doc/en/plugins_index.py | 120 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 doc/en/plugins_index.py diff --git a/doc/en/plugins_index.py b/doc/en/plugins_index.py new file mode 100644 index 000000000..8a31a6d73 --- /dev/null +++ b/doc/en/plugins_index.py @@ -0,0 +1,120 @@ +from collections import namedtuple +from distutils.version import LooseVersion +import itertools +import os +import sys +import xmlrpclib + + +#=================================================================================================== +# iter_pypi_plugins +#=================================================================================================== +def iter_pypi_plugins(client): + for plug_data in client.search({'name' : 'pytest-'}): + yield plug_data['name'], plug_data['version'] + + +#=================================================================================================== +# get_latest_versions +#=================================================================================================== +def get_latest_versions(plugins): + plugins = [(name, LooseVersion(version)) for (name, version) in plugins] + for name, grouped_plugins in itertools.groupby(plugins, key=lambda x: x[0]): + name, loose_version = list(grouped_plugins)[-1] + yield name, str(loose_version) + + +#=================================================================================================== +# obtain_plugins_table +#=================================================================================================== +def obtain_plugins_table(plugins, client): + rows = [] + RowData = namedtuple('RowData', 'text link') + headers = ['Name', 'Version', 'Author', 'Summary'] + + # pluginname and latest version, pypi link, maintainer/author, repository link, + # one-line description, test status py27/py33 + for package_name, version in plugins: + release_data = client.release_data(package_name, version) + row = ( + RowData(package_name, release_data['package_url']), + RowData(version, release_data['release_url']), + RowData(release_data['author'], release_data['author_email']), + RowData(release_data['summary'], None), + ) + assert len(row) == len(headers) + rows.append(row) + + return headers, rows + + +#=================================================================================================== +# generate_plugins_index_from_table +#=================================================================================================== +def generate_plugins_index_from_table(headers, rows, basename): + + def get_row_limiter(char): + return ' '.join(char * length for length in column_lengths) + + def ref(s, link): + return s + '_' if link else s + + table_texts = [] + for row in rows: + row_texts = [] + for i, row_data in enumerate(row): + text = '`%s <%s>`_' % (row_data.text, row_data.link) if row_data.link else row_data.text + row_texts.append(text) + table_texts.append(row_texts) + + column_lengths = [len(x) for x in headers] + for row_texts in table_texts: + for i, row_text in enumerate(row_texts): + column_lengths[i] = max(column_lengths[i], len(row_text) + 2) + + with file(basename, 'w') as f: + print >> f, '.. _plugins_index:' + print >> f + print >> f, 'List of Third-Party Plugins' + print >> f, '===========================' + print >> f + print >> f + print >> f, get_row_limiter('=') + for i, header in enumerate(headers): + print >> f, '{:^{fill}}'.format(header, fill=column_lengths[i]), + print >> f + print >> f, get_row_limiter('=') + + for row_texts in table_texts: + for i, row_text in enumerate(row_texts): + print >> f, '{:^{fill}}'.format(row_text, fill=column_lengths[i]), + print >> f + print >> f + print >> f, get_row_limiter('=') + print >> f + + +#=================================================================================================== +# generate_plugins_index +#=================================================================================================== +def generate_plugins_index(client, basename): + plugins = get_latest_versions(iter_pypi_plugins(client)) + headers, rows = obtain_plugins_table(plugins, client) + generate_plugins_index_from_table(headers, rows, basename) + + +#=================================================================================================== +# main +#=================================================================================================== +def main(argv): + client = xmlrpclib.ServerProxy('http://pypi.python.org/pypi') + basename = os.path.join(os.path.dirname(__file__), 'plugins_index.txt') + generate_plugins_index(client, basename) + print 'OK' + return 0 + +#=================================================================================================== +# main +#=================================================================================================== +if __name__ == '__main__': + sys.exit(main(sys.argv)) From d92322a574e49c0aec393beaeda1d41adb23eca5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 11 Oct 2013 19:47:40 -0300 Subject: [PATCH 2/4] added plugins_index documentation and generated plugins_index file --HG-- branch : plugins-index --- doc/en/plugins_index.py | 113 ++++++++++++++++++++++++++------------- doc/en/plugins_index.txt | 61 +++++++++++++++++++++ 2 files changed, 138 insertions(+), 36 deletions(-) create mode 100644 doc/en/plugins_index.txt diff --git a/doc/en/plugins_index.py b/doc/en/plugins_index.py index 8a31a6d73..0e6caa1ec 100644 --- a/doc/en/plugins_index.py +++ b/doc/en/plugins_index.py @@ -1,4 +1,11 @@ +''' +Script to generate the file `plugins_index.txt` with information about pytest plugins taken directly +from a live pypi server. + +This will evolve to include test compatibility (pythons and pytest versions) information also. +''' from collections import namedtuple +import datetime from distutils.version import LooseVersion import itertools import os @@ -7,10 +14,16 @@ import xmlrpclib #=================================================================================================== -# iter_pypi_plugins +# iter_plugins #=================================================================================================== -def iter_pypi_plugins(client): - for plug_data in client.search({'name' : 'pytest-'}): +def iter_plugins(client, search='pytest-'): + ''' + Returns an iterator of (name, version) from pypi. + + :param client: xmlrpclib.ServerProxy + :param search: package names to search for + ''' + for plug_data in client.search({'name' : search}): yield plug_data['name'], plug_data['version'] @@ -18,6 +31,11 @@ def iter_pypi_plugins(client): # get_latest_versions #=================================================================================================== def get_latest_versions(plugins): + ''' + Returns an iterator of (name, version) from the given list of (name, version), but returning + only the latest version of the package. Uses distutils.LooseVersion to ensure compatibility + with PEP386. + ''' plugins = [(name, LooseVersion(version)) for (name, version) in plugins] for name, grouped_plugins in itertools.groupby(plugins, key=lambda x: x[0]): name, loose_version = list(grouped_plugins)[-1] @@ -28,19 +46,27 @@ def get_latest_versions(plugins): # obtain_plugins_table #=================================================================================================== def obtain_plugins_table(plugins, client): + ''' + Returns information to populate a table of plugins, their versions, authors, etc. + + The returned information is a list of columns of `ColumnData` namedtuples(text, link). Link + can be None if the text for that column should not be linked to anything. + + :param plugins: list of (name, version) + :param client: xmlrpclib.ServerProxy + + ''' rows = [] - RowData = namedtuple('RowData', 'text link') + ColumnData = namedtuple('ColumnData', 'text link') headers = ['Name', 'Version', 'Author', 'Summary'] - # pluginname and latest version, pypi link, maintainer/author, repository link, - # one-line description, test status py27/py33 for package_name, version in plugins: release_data = client.release_data(package_name, version) row = ( - RowData(package_name, release_data['package_url']), - RowData(version, release_data['release_url']), - RowData(release_data['author'], release_data['author_email']), - RowData(release_data['summary'], None), + ColumnData(package_name, release_data['package_url']), + ColumnData(version, release_data['release_url']), + ColumnData(release_data['author'], release_data['author_email']), + ColumnData(release_data['summary'], None), ) assert len(row) == len(headers) rows.append(row) @@ -51,56 +77,71 @@ def obtain_plugins_table(plugins, client): #=================================================================================================== # generate_plugins_index_from_table #=================================================================================================== -def generate_plugins_index_from_table(headers, rows, basename): +def generate_plugins_index_from_table(filename, headers, rows): + ''' + Generates a RST file with the table data given. + + :param filename: output filename + :param headers: see `obtain_plugins_table` + :param rows: see `obtain_plugins_table` + ''' + # creates a list of rows, each being a str containing appropriate column text and link + table_texts = [] + for row in rows: + column_texts = [] + for i, col_data in enumerate(row): + text = '`%s <%s>`_' % (col_data.text, col_data.link) if col_data.link else col_data.text + column_texts.append(text) + table_texts.append(column_texts) + # compute max length of each column so we can build the rst table + column_lengths = [len(x) for x in headers] + for column_texts in table_texts: + for i, row_text in enumerate(column_texts): + column_lengths[i] = max(column_lengths[i], len(row_text) + 2) + def get_row_limiter(char): return ' '.join(char * length for length in column_lengths) - def ref(s, link): - return s + '_' if link else s - - table_texts = [] - for row in rows: - row_texts = [] - for i, row_data in enumerate(row): - text = '`%s <%s>`_' % (row_data.text, row_data.link) if row_data.link else row_data.text - row_texts.append(text) - table_texts.append(row_texts) - - column_lengths = [len(x) for x in headers] - for row_texts in table_texts: - for i, row_text in enumerate(row_texts): - column_lengths[i] = max(column_lengths[i], len(row_text) + 2) - - with file(basename, 'w') as f: + with file(filename, 'w') as f: + # write welcome print >> f, '.. _plugins_index:' print >> f print >> f, 'List of Third-Party Plugins' print >> f, '===========================' print >> f - print >> f + + # table print >> f, get_row_limiter('=') for i, header in enumerate(headers): print >> f, '{:^{fill}}'.format(header, fill=column_lengths[i]), print >> f print >> f, get_row_limiter('=') - for row_texts in table_texts: - for i, row_text in enumerate(row_texts): + for column_texts in table_texts: + for i, row_text in enumerate(column_texts): print >> f, '{:^{fill}}'.format(row_text, fill=column_lengths[i]), print >> f print >> f print >> f, get_row_limiter('=') print >> f + + print >> f, '*(Last updated: %s)*' % datetime.date.today().strftime('%Y-%m-%d') #=================================================================================================== # generate_plugins_index #=================================================================================================== -def generate_plugins_index(client, basename): - plugins = get_latest_versions(iter_pypi_plugins(client)) +def generate_plugins_index(client, filename): + ''' + Generates an RST file with a table of the latest pytest plugins found in pypi. + + :param client: xmlrpclib.ServerProxy + :param filename: output filename + ''' + plugins = get_latest_versions(iter_plugins(client)) headers, rows = obtain_plugins_table(plugins, client) - generate_plugins_index_from_table(headers, rows, basename) + generate_plugins_index_from_table(filename, headers, rows) #=================================================================================================== @@ -108,8 +149,8 @@ def generate_plugins_index(client, basename): #=================================================================================================== def main(argv): client = xmlrpclib.ServerProxy('http://pypi.python.org/pypi') - basename = os.path.join(os.path.dirname(__file__), 'plugins_index.txt') - generate_plugins_index(client, basename) + filename = os.path.join(os.path.dirname(__file__), 'plugins_index.txt') + generate_plugins_index(client, filename) print 'OK' return 0 diff --git a/doc/en/plugins_index.txt b/doc/en/plugins_index.txt new file mode 100644 index 000000000..a73044839 --- /dev/null +++ b/doc/en/plugins_index.txt @@ -0,0 +1,61 @@ +.. _plugins_index: + +List of Third-Party Plugins +=========================== + +================================================================================== ============================================================================ ==================================================================================== ============================================================================================================================================= + Name Version Author Summary +================================================================================== ============================================================================ ==================================================================================== ============================================================================================================================================= + `pytest-bdd `_ `0.6.1 `_ `Oleg Pidsadnyi `_ BDD for pytest + `pytest-bdd-splinter `_ `0.5.2 `_ `Oleg Pidsadnyi `_ Splinter subplugin for Pytest BDD plugin + `pytest-bench `_ `0.1.0 `_ `Concordus Applications `_ Benchmark utility that plugs into pytest. + `pytest-blockage `_ `0.1 `_ `UNKNOWN `_ Disable network requests during a test run. + `pytest-browsermob-proxy `_ `0.1 `_ `Dave Hunt `_ BrowserMob proxy plugin for py.test. + `pytest-bugzilla `_ `0.2 `_ `Noufal Ibrahim `_ py.test bugzilla integration plugin + `pytest-cache `_ `1.0 `_ `Holger Krekel `_ pytest plugin with mechanisms for caching across test runs + `pytest-capturelog `_ `0.7 `_ `Meme Dough `_ py.test plugin to capture log messages + `pytest-codecheckers `_ `0.2 `_ `Ronny Pfannschmidt `_ pytest plugin to add source code sanity checks (pep8 and friends) + `pytest-contextfixture `_ `0.1.1 `_ `Andreas Pelme `_ Define pytest fixtures as context managers. + `pytest-couchdbkit `_ `0.5.1 `_ `RonnyPfannschmidt `_ py.test extension for per-test couchdb databases using couchdbkit + `pytest-cov `_ `1.6 `_ `Meme Dough `_ py.test plugin for coverage reporting with support for both centralised and distributed testing, including subprocesses and multiprocessing + `pytest-dbfixtures `_ `0.3.8.3 `_ `Clearcode - The A Room `_ dbfixtures plugin for py.test. + `pytest-django `_ `2.3.1 `_ `Andreas Pelme `_ A Django plugin for py.test. + `pytest-django-lite `_ `0.1.0 `_ `David Cramer `_ The bare minimum to integrate py.test with Django. + `pytest-figleaf `_ `1.0 `_ `holger krekel `_ py.test figleaf coverage plugin + `pytest-flakes `_ `0.2 `_ `Florian Schulze, Holger Krekel and Ronny Pfannschmidt `_ pytest plugin to check source code with pyflakes + `pytest-greendots `_ `0.2 `_ `UNKNOWN `_ Green progress dots + `pytest-growl `_ `0.1 `_ `Anthony Long `_ Growl notifications for pytest results. + `pytest-incremental `_ `0.3.0 `_ `Eduardo Naufel Schettino `_ an incremental test runner (pytest plugin) + `pytest-instafail `_ `0.1.0 `_ `Janne Vanhala `_ py.test plugin to show failures instantly + `pytest-ipdb `_ `0.1-prerelease `_ `Matthew de Verteuil `_ A py.test plug-in to enable drop to ipdb debugger on test failure. + `pytest-konira `_ `0.2 `_ `Alfredo Deza `_ Run Konira DSL tests with py.test + `pytest-localserver `_ `0.3 `_ `Sebastian Rahlf `_ py.test plugin to test server connections locally. + `pytest-marker-bugzilla `_ `0.06 `_ `Eric Sammons `_ py.test bugzilla integration plugin, using markers + `pytest-markfiltration `_ `0.8 `_ `adam goucher `_ UNKNOWN + `pytest-marks `_ `0.4 `_ `adam goucher `_ UNKNOWN + `pytest-monkeyplus `_ `1.1.0 `_ `Virgil Dupras `_ pytest's monkeypatch subclass with extra functionalities + `pytest-mozwebqa `_ `1.1.1 `_ `Dave Hunt `_ Mozilla WebQA plugin for py.test. + `pytest-oerp `_ `0.2.0 `_ `Leonardo Santagada `_ pytest plugin to test OpenERP modules + `pytest-osxnotify `_ `0.1.4 `_ `Daniel Bader `_ OS X notifications for py.test results. + `pytest-paste-config `_ `0.1 `_ `UNKNOWN `_ Allow setting the path to a paste config file + `pytest-pep8 `_ `1.0.5 `_ `Holger Krekel and Ronny Pfannschmidt `_ pytest plugin to check PEP8 requirements + `pytest-poo `_ `0.2 `_ `Andreas Pelme `_ Visualize your crappy tests + `pytest-pydev `_ `0.1 `_ `Sebastian Rahlf `_ py.test plugin to connect to a remote debug server with PyDev or PyCharm. + `pytest-qt `_ `1.0.2 `_ `Bruno Oliveira `_ pytest plugin that adds fixtures for testing Qt (PyQt and PySide) applications. + `pytest-quickcheck `_ `0.7 `_ `Tetsuya Morimoto `_ pytest plugin to generate random data inspired by QuickCheck + `pytest-rage `_ `0.1 `_ `Leonardo Santagada `_ pytest plugin to implement PEP712 + `pytest-random `_ `0.02 `_ `Leah Klearman `_ py.test plugin to randomize tests + `pytest-rerunfailures `_ `0.03 `_ `Leah Klearman `_ py.test plugin to re-run tests to eliminate flakey failures + `pytest-runfailed `_ `0.3 `_ `Dimitri Merejkowsky `_ implement a --failed option for pytest + `pytest-runner `_ `2.0 `_ `Jason R. Coombs `_ UNKNOWN + `pytest-sugar `_ `0.2.2 `_ `Teemu, Janne Vanhala `_ py.test plugin that adds instafail, ETA and neat graphics + `pytest-timeout `_ `0.3 `_ `Floris Bruynooghe `_ pytest plugin to abort tests after a timeout + `pytest-twisted `_ `1.4 `_ `Ralf Schmitt `_ A twisted plugin for py.test. + `pytest-xdist `_ `1.9 `_ `holger krekel and contributors `_ py.test xdist plugin for distributed testing and loop-on-failing modes + `pytest-xprocess `_ `0.8 `_ `Holger Krekel `_ pytest plugin to manage external processes across test runs + `pytest-yamlwsgi `_ `0.6 `_ `Ali Afshar `_ Run tests against wsgi apps defined in yaml + `pytest-zap `_ `0.1 `_ `Dave Hunt `_ OWASP ZAP plugin for py.test. + +================================================================================== ============================================================================ ==================================================================================== ============================================================================================================================================= + +*(Updated on 2013-10-11)* From 5b2b71bfd450ee0e22bfc4a15b5c0c1c8f6ba29c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 11 Oct 2013 20:53:03 -0300 Subject: [PATCH 3/4] included test for plugins_index and added command line options --HG-- branch : plugins-index --- doc/en/plugins_index.py | 24 ++++++++-- doc/en/test_plugins_index.py | 88 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 doc/en/test_plugins_index.py diff --git a/doc/en/plugins_index.py b/doc/en/plugins_index.py index 0e6caa1ec..17765d146 100644 --- a/doc/en/plugins_index.py +++ b/doc/en/plugins_index.py @@ -8,6 +8,7 @@ from collections import namedtuple import datetime from distutils.version import LooseVersion import itertools +from optparse import OptionParser import os import sys import xmlrpclib @@ -126,7 +127,17 @@ def generate_plugins_index_from_table(filename, headers, rows): print >> f, get_row_limiter('=') print >> f - print >> f, '*(Last updated: %s)*' % datetime.date.today().strftime('%Y-%m-%d') + print >> f, '*(Updated on %s)*' % _get_today_as_str() + + +#=================================================================================================== +# _get_today_as_str +#=================================================================================================== +def _get_today_as_str(): + ''' + internal. only exists so we can patch it in testing. + ''' + return datetime.date.today().strftime('%Y-%m-%d') #=================================================================================================== @@ -148,9 +159,16 @@ def generate_plugins_index(client, filename): # main #=================================================================================================== def main(argv): - client = xmlrpclib.ServerProxy('http://pypi.python.org/pypi') filename = os.path.join(os.path.dirname(__file__), 'plugins_index.txt') - generate_plugins_index(client, filename) + url = 'http://pypi.python.org/pypi' + + parser = OptionParser(description='Generates a restructured document of pytest plugins from PyPi') + parser.add_option('-f', '--filename', default=filename, help='output filename [default: %default]') + parser.add_option('-u', '--url', default=url, help='url of PyPi server to obtain data from [default: %default]') + (options, _) = parser.parse_args(argv[1:]) + + client = xmlrpclib.ServerProxy(options.url) + generate_plugins_index(client, options.filename) print 'OK' return 0 diff --git a/doc/en/test_plugins_index.py b/doc/en/test_plugins_index.py new file mode 100644 index 000000000..4183a9e13 --- /dev/null +++ b/doc/en/test_plugins_index.py @@ -0,0 +1,88 @@ +import os +import xmlrpclib + + +#=================================================================================================== +# test_plugins_index +#=================================================================================================== +def test_plugins_index(tmpdir, monkeypatch): + ''' + Blackbox testing for plugins_index script. Calls main() generating a file and compares produced + output to expected. + + .. note:: if the test fails, a file named `test_plugins_index.obtained` will be generated in + the same directory as this test file. Ensure the contents are correct and overwrite + the global `expected_output` with the new contents. + ''' + import plugins_index + + # dummy interface to xmlrpclib.ServerProxy + class DummyProxy(object): + + expected_url = 'http://dummy.pypi' + def __init__(self, url): + assert url == self.expected_url + + def search(self, query): + assert query == {'name' : 'pytest-'} + return [ + {'name': 'pytest-plugin1', 'version' : '0.8'}, + {'name': 'pytest-plugin1', 'version' : '1.0'}, + {'name': 'pytest-plugin2', 'version' : '1.2'}, + ] + + def release_data(self, package_name, version): + results = { + ('pytest-plugin1', '1.0') : { + 'package_url' : 'http://plugin1', + 'release_url' : 'http://plugin1/1.0', + 'author' : 'someone', + 'author_email' : 'someone@py.com', + 'summary' : 'some plugin', + }, + + ('pytest-plugin2', '1.2') : { + 'package_url' : 'http://plugin2', + 'release_url' : 'http://plugin2/1.2', + 'author' : 'other', + 'author_email' : 'other@py.com', + 'summary' : 'some other plugin', + }, + } + + return results[(package_name, version)] + + + monkeypatch.setattr(xmlrpclib, 'ServerProxy', DummyProxy, 'foo') + monkeypatch.setattr(plugins_index, '_get_today_as_str', lambda: '2013-10-20') + + output_file = str(tmpdir.join('output.txt')) + assert plugins_index.main(['', '-f', output_file, '-u', DummyProxy.expected_url]) == 0 + + with file(output_file, 'rU') as f: + obtained_output = f.read() + + if obtained_output != expected_output: + obtained_file = os.path.splitext(__file__)[0] + '.obtained' + with file(obtained_file, 'w') as f: + f.write(obtained_output) + + assert obtained_output == expected_output + + +expected_output = '''\ +.. _plugins_index: + +List of Third-Party Plugins +=========================== + +==================================== ============================= ============================= =================== + Name Version Author Summary +==================================== ============================= ============================= =================== + `pytest-plugin1 `_ `1.0 `_ `someone `_ some plugin + `pytest-plugin2 `_ `1.2 `_ `other `_ some other plugin + +==================================== ============================= ============================= =================== + +*(Updated on 2013-10-20)* +''' From 6e619e0a70dbdb2fba29d9f6ec43e6bf0993ff24 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 11 Oct 2013 20:57:35 -0300 Subject: [PATCH 4/4] fixed 'PyPI' spelling --HG-- branch : plugins-index --- doc/en/plugins_index.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/en/plugins_index.py b/doc/en/plugins_index.py index 17765d146..96726cdc0 100644 --- a/doc/en/plugins_index.py +++ b/doc/en/plugins_index.py @@ -1,6 +1,6 @@ ''' Script to generate the file `plugins_index.txt` with information about pytest plugins taken directly -from a live pypi server. +from a live PyPI server. This will evolve to include test compatibility (pythons and pytest versions) information also. ''' @@ -19,7 +19,7 @@ import xmlrpclib #=================================================================================================== def iter_plugins(client, search='pytest-'): ''' - Returns an iterator of (name, version) from pypi. + Returns an iterator of (name, version) from PyPI. :param client: xmlrpclib.ServerProxy :param search: package names to search for @@ -145,7 +145,7 @@ def _get_today_as_str(): #=================================================================================================== def generate_plugins_index(client, filename): ''' - Generates an RST file with a table of the latest pytest plugins found in pypi. + Generates an RST file with a table of the latest pytest plugins found in PyPI. :param client: xmlrpclib.ServerProxy :param filename: output filename @@ -162,9 +162,9 @@ def main(argv): filename = os.path.join(os.path.dirname(__file__), 'plugins_index.txt') url = 'http://pypi.python.org/pypi' - parser = OptionParser(description='Generates a restructured document of pytest plugins from PyPi') + parser = OptionParser(description='Generates a restructured document of pytest plugins from PyPI') parser.add_option('-f', '--filename', default=filename, help='output filename [default: %default]') - parser.add_option('-u', '--url', default=url, help='url of PyPi server to obtain data from [default: %default]') + parser.add_option('-u', '--url', default=url, help='url of PyPI server to obtain data from [default: %default]') (options, _) = parser.parse_args(argv[1:]) client = xmlrpclib.ServerProxy(options.url)