diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0105c34f..91e777ba 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,38 +2,64 @@ name: Run tests on: push: + branches: + - master + pull_request: + branches: + - "*" jobs: tests: name: ${{ matrix.OS }} - Py${{ matrix.PYTHON_VERSION }} runs-on: ${{ matrix.OS }} strategy: - fail-fast: false + fail-fast: false matrix: - OS: ['ubuntu-latest', 'windows-latest'] - PYTHON_VERSION: ['3.5', '3.6', '3.7','3.8'] + OS: ['ubuntu-latest', 'macos-latest', 'windows-latest'] + PYTHON_VERSION: ['3.6', '3.9', 'pypy3'] + exclude: + - os: windows-latest + PYTHON_VERSION: pypy3 steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v1 with: python-version: ${{ matrix.PYTHON_VERSION }} - - name: Install test dependencies + - name: Install python dependencies run: | - pip install --upgrade pip setuptools - pip install .[test] - pip install codecov - - name: Install nbformat + pip install --upgrade pip setuptools wheel + - name: Get pip cache dir + id: pip-cache run: | - pip install . - pip freeze - - name: List dependencies + echo "::set-output name=dir::$(pip cache dir)" + - name: Cache pip + uses: actions/cache@v1 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ matrix.PYTHON_VERSION }}-${{ hashFiles('setup.py') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.PYTHON_VERSION }}- + ${{ runner.os }}-pip- + - name: Cache hypotheses + uses: actions/cache@v1 + with: + path: .hypothesis + key: ${{ runner.os }}-hypothesis-${{ matrix.PYTHON_VERSION }}-${{ hashFiles('setup.py') }} + restore-keys: | + ${{ runner.os }}-hypothesis-${{ matrix.PYTHON_VERSION }}- + ${{ runner.os }}-hypothesis- + - name: Install nbformat and test dependencies + run: | + pip install --upgrade .[test] codecov + - name: List installed packages run: | - pip list + pip freeze + pip check - name: Run tests run: | - py.test nbformat/tests -v --cov=nbformat + pytest nbformat/tests -v --cov=nbformat - name: Coverage run: | codecov diff --git a/.gitignore b/.gitignore index 82ec99b6..07dea63d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ __pycache__ .#* .coverage .cache +.hypothesis diff --git a/docs/conf.py b/docs/conf.py index b4d73a6a..16e21129 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # nbformat documentation build configuration file, created by # sphinx-quickstart on Thu May 14 17:26:52 2015. @@ -44,9 +43,6 @@ # source_suffix = ['.rst', '.md'] source_suffix = '.rst' -# The encoding of source files. -#source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = 'index' diff --git a/nbformat/__init__.py b/nbformat/__init__.py index fa05fc27..100e8188 100644 --- a/nbformat/__init__.py +++ b/nbformat/__init__.py @@ -3,7 +3,7 @@ Use this module to read or write notebook files as particular nbformat versions. """ -# Copyright (c) IPython Development Team. +# Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. import io @@ -15,6 +15,7 @@ from . import v3 from . import v4 from .sentinel import Sentinel +from .constants import DEFAULT_ENCODING, ENV_VAR_VALIDATOR __all__ = ['versions', 'validate', 'ValidationError', 'convert', 'from_dict', 'NotebookNode', 'current_nbformat', 'current_nbformat_minor', @@ -29,6 +30,7 @@ 4: v4, } + from .validator import validate, ValidationError from .converter import convert from . import reader @@ -137,7 +139,7 @@ def read(fp, as_version, **kwargs): try: buf = fp.read() except AttributeError: - with io.open(fp, encoding='utf-8') as f: + with io.open(fp, encoding=DEFAULT_ENCODING) as f: return reads(f.read(), as_version, **kwargs) return reads(buf, as_version, **kwargs) @@ -170,7 +172,7 @@ def write(nb, fp, version=NO_CONVERT, **kwargs): if not s.endswith(u'\n'): fp.write(u'\n') except AttributeError: - with io.open(fp, 'w', encoding='utf-8') as f: + with io.open(fp, 'w', encoding=DEFAULT_ENCODING) as f: f.write(s) if not s.endswith(u'\n'): f.write(u'\n') diff --git a/nbformat/asynchronous.py b/nbformat/asynchronous.py new file mode 100644 index 00000000..a65de46b --- /dev/null +++ b/nbformat/asynchronous.py @@ -0,0 +1,90 @@ +"""asynchronous API for nbformat""" + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +import asyncio +import os +import io +from pathlib import Path +import aiofiles +from aiofiles.threadpool.text import AsyncTextIOWrapper + +from . import ( + NO_CONVERT, ValidationError, + reads as reads_sync, writes as writes_sync, validate as validate_sync +) +from .constants import DEFAULT_ENCODING + +AIOFILES_OPENABLE = (str, bytes, os.PathLike) + + +def _loop(): + """get the current event loop + this may need some more work later + """ + return asyncio.get_event_loop() + + +# shim calls for tracing, etc. +def _reads(s, as_version, kwargs_): + return reads_sync(s, as_version, **kwargs_) + + +def _writes(nb, version, kwargs_): + return writes_sync(nb, version, **kwargs_) + + +def _validate(nbdict, ref, version, version_minor, relax_add_props, nbjson): + return validate_sync(nbdict, ref, version, version_minor, relax_add_props, nbjson) + + +__all__ = [ + 'NO_CONVERT', + 'DEFAULT_ENCODING', + 'ValidationError', + # asynchronous API + 'reads', + 'read', + 'writes', + 'write', + 'validate' +] + + +async def reads(s, as_version, **kwargs): + return await _loop().run_in_executor(None, _reads, s, as_version, kwargs) + + +async def writes(nb, version=NO_CONVERT, **kwargs): + return await _loop().run_in_executor(None, _writes, nb, version, kwargs) + + +async def read(fp, as_version, **kwargs): + if isinstance(fp, AIOFILES_OPENABLE): + async with aiofiles.open(fp, encoding=DEFAULT_ENCODING) as afp: + nb_str = await afp.read() + elif isinstance(fp, io.TextIOWrapper): + nb_str = await AsyncTextIOWrapper(fp, loop=_loop(), executor=None).read() + else: + raise NotImplementedError(f"Don't know how to read {type(fp)}") + + return await reads(nb_str, as_version, **kwargs) + + +async def write(nb, fp, version=NO_CONVERT, **kwargs): + nb_str = await writes(nb, version, **kwargs) + + if isinstance(fp, AIOFILES_OPENABLE): + async with aiofiles.open(fp, 'w+', encoding=DEFAULT_ENCODING) as afp: + return await afp.write(nb_str) + elif isinstance(fp, io.TextIOWrapper): + return await AsyncTextIOWrapper(fp, loop=_loop(), executor=None).write(nb_str) + else: + raise NotImplementedError(f"Don't know how to write {type(fp)}") + + +async def validate(nbdict=None, ref=None, version=None, version_minor=None, + relax_add_props=False, nbjson=None): + return await _loop().run_in_executor(None, _validate, nbdict, ref, version, + version_minor, relax_add_props, nbjson) diff --git a/nbformat/constants.py b/nbformat/constants.py new file mode 100644 index 00000000..4591df42 --- /dev/null +++ b/nbformat/constants.py @@ -0,0 +1,9 @@ +"""constants used throughout nbformat""" +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +# while JSON allows for other encodings, utf-8 is most widely supported +DEFAULT_ENCODING = 'utf-8' + +# environment variable +ENV_VAR_VALIDATOR = 'NBFORMAT_VALIDATOR' diff --git a/nbformat/json_compat.py b/nbformat/json_compat.py index 936d3c87..f2f1aec2 100644 --- a/nbformat/json_compat.py +++ b/nbformat/json_compat.py @@ -18,6 +18,8 @@ fastjsonschema = None _JsonSchemaException = ValidationError +from .constants import ENV_VAR_VALIDATOR + class JsonSchemaValidator: name = "jsonschema" @@ -78,5 +80,5 @@ def get_current_validator(): """ Return the default validator based on the value of an environment variable. """ - validator_name = os.environ.get("NBFORMAT_VALIDATOR", "jsonschema") + validator_name = os.environ.get(ENV_VAR_VALIDATOR, "jsonschema") return _validator_for_name(validator_name) diff --git a/nbformat/sign.py b/nbformat/sign.py index f7fe36c9..0087abf3 100644 --- a/nbformat/sign.py +++ b/nbformat/sign.py @@ -29,6 +29,7 @@ from jupyter_core.application import JupyterApp, base_flags from . import read, reads, NO_CONVERT, __version__ +from .constants import DEFAULT_ENCODING from ._compat import encodebytes try: @@ -569,7 +570,7 @@ def sign_notebook_file(self, notebook_path): if not os.path.exists(notebook_path): self.log.error("Notebook missing: %s" % notebook_path) self.exit(1) - with io.open(notebook_path, encoding='utf8') as f: + with io.open(notebook_path, encoding=DEFAULT_ENCODING) as f: nb = read(f, NO_CONVERT) self.sign_notebook(nb, notebook_path) diff --git a/nbformat/tests/base.py b/nbformat/tests/base.py index 312c22bb..bd80cda6 100644 --- a/nbformat/tests/base.py +++ b/nbformat/tests/base.py @@ -9,11 +9,13 @@ import unittest import io +from ..constants import DEFAULT_ENCODING + class TestsBase(unittest.TestCase): """Base tests class.""" @classmethod - def fopen(cls, f, mode=u'r',encoding='utf-8'): + def fopen(cls, f, mode=u'r',encoding=DEFAULT_ENCODING): return io.open(os.path.join(cls._get_files_path(), f), mode, encoding=encoding) @classmethod diff --git a/nbformat/tests/strategies.py b/nbformat/tests/strategies.py new file mode 100644 index 00000000..99cbaddd --- /dev/null +++ b/nbformat/tests/strategies.py @@ -0,0 +1,84 @@ +"""nbformat strategies for hypothesis""" + +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +import pytest +import re +from pathlib import Path +from hypothesis import given, strategies as st, assume, settings, HealthCheck + +from nbformat import validate, reads, writes, DEFAULT_ENCODING +from nbformat.v4 import new_code_cell, new_markdown_cell, new_notebook + + +HERE = Path(__file__).parent +ALL_NOTEBOOK_TEXT = [p.read_text(encoding=DEFAULT_ENCODING) for p in HERE.glob('*.ipynb')] +ALL_NOTEBOOKS = [ + reads(nb_text, int(re.findall(r'''nbformat":\s+(\d+)''', nb_text)[0])) + for nb_text in ALL_NOTEBOOK_TEXT +] + +def _is_valid(nb): + try: + validate(nb) + return True + except: + return False + +VALID_NOTEBOOKS = [nb for nb in ALL_NOTEBOOKS if _is_valid(nb)] +INVALID_NOTEBOOKS = [nb for nb in ALL_NOTEBOOKS if nb not in VALID_NOTEBOOKS] + +CELL_TYPES = [new_code_cell, new_markdown_cell] +# , nbformat.new_text_cell, nbformat.new_notebook_cell] + +# Most tests will need this decorator, because fileio and threads are slow +base_settings = settings(suppress_health_check=[HealthCheck.too_slow], deadline=None) + +a_cell_generator = st.sampled_from(CELL_TYPES) +a_test_notebook = st.sampled_from(ALL_NOTEBOOKS) +a_valid_test_notebook = st.sampled_from(VALID_NOTEBOOKS) +an_invalid_test_notebook = st.sampled_from(INVALID_NOTEBOOKS) + + +@st.composite +def a_cell(draw): + Cell = draw(a_cell_generator) + cell = Cell() + cell.source = draw(st.text()) + return cell + + +@st.composite +def a_new_notebook(draw): + notebook = new_notebook() + cell_count = draw(st.integers(min_value=1, max_value=100)) + notebook.cells = [draw(a_cell()) for i in range(cell_count)] + + return notebook + + +@st.composite +def a_valid_notebook(draw): + if draw(st.booleans()): + return draw(a_valid_test_notebook) + + return draw(a_new_notebook()) + + +@st.composite +def an_invalid_notebook(draw): + # TODO: some mutations to make a valid notebook invalid + return draw(an_invalid_test_notebook) + + +@st.composite +def a_valid_notebook_with_string(draw): + notebook = draw(a_valid_notebook()) + return notebook, writes(notebook) + + +@st.composite +def an_invalid_notebook_with_string(draw): + notebook = draw(an_invalid_notebook()) + return notebook, writes(notebook) diff --git a/nbformat/tests/test_async.py b/nbformat/tests/test_async.py new file mode 100644 index 00000000..23ccc0a3 --- /dev/null +++ b/nbformat/tests/test_async.py @@ -0,0 +1,129 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +import os +import io +import pytest +import contextlib +import tempfile +from pathlib import Path + +import aiofiles +from hypothesis import given +from testfixtures import LogCapture + +from ..constants import ENV_VAR_VALIDATOR, DEFAULT_ENCODING +from ..asynchronous import read, reads, write, writes, validate, ValidationError, NO_CONVERT +from ..json_compat import VALIDATORS + +from . import strategies as nbs + + +@contextlib.contextmanager +def json_validator(validator_name): + os.environ[ENV_VAR_VALIDATOR] = validator_name + try: + yield + finally: + os.environ.pop(ENV_VAR_VALIDATOR) + + + +# some issues with setting environment variables mean they can just be parametrized +# pytest's caplog conflicts with hypothesis +async def _valid(nb, txt): + """use the asynchronous API with a valid notebook, round-tripping to disk""" + with LogCapture() as caplog: + read_nb = await reads(txt, nb.nbformat) + assert 'Notebook JSON' not in str(caplog) + + await validate(read_nb) + + with tempfile.TemporaryDirectory() as td: + nb_path = os.path.join(td, 'notebook.ipynb') + await write(read_nb, nb_path) + await read(nb_path, nb['nbformat']) + + +async def _invalid(nb, txt): + """use the asynchronous API with an invalid notebook, round-tripping to disk""" + + with LogCapture() as caplog: + read_nb = await reads(txt, nb.nbformat) + assert 'Notebook JSON' in str(caplog) + + with pytest.raises(ValidationError): + await validate(read_nb) + + +@given(nb_txt=nbs.a_valid_notebook_with_string()) +@nbs.base_settings +@pytest.mark.asyncio +async def test_async_valid_default(nb_txt): + with json_validator('jsonschema'): + await _valid(*nb_txt) + + +@given(nb_txt=nbs.a_valid_notebook_with_string()) +@nbs.base_settings +@pytest.mark.asyncio +async def test_async_valid_fast(nb_txt): + with json_validator('fastjsonschema'): + await _valid(*nb_txt) + + +@given(nb_txt=nbs.an_invalid_notebook_with_string()) +@nbs.base_settings +@pytest.mark.asyncio +async def test_async_invalid_default(nb_txt): + with json_validator('jsonschema'): + await _invalid(*nb_txt) + + +@given(nb_txt=nbs.an_invalid_notebook_with_string()) +@nbs.base_settings +@pytest.mark.asyncio +async def test_async_invalid_fast(nb_txt): + with json_validator('fastjsonschema'): + await _invalid(*nb_txt) + + +@given(nb_txt=nbs.a_valid_notebook_with_string()) +@nbs.base_settings +@pytest.mark.asyncio +async def test_async_builtin_open(nb_txt): + """someone's probably using `open`""" + nb, txt = nb_txt + with tempfile.TemporaryDirectory() as td: + nb_path = Path(td) / 'notebook.ipynb' + + with open(nb_path, 'w+', encoding=DEFAULT_ENCODING) as fp: + await write(nb, fp) + + with open(nb_path, 'r', encoding=DEFAULT_ENCODING) as fp: + await read(fp, as_version=nb["nbformat"]) + + +@given(nb_txt=nbs.a_valid_notebook_with_string()) +@nbs.base_settings +@pytest.mark.asyncio +async def test_async_like_jupyter_server(nb_txt): + """ the atomic write stuff is rather complex, but it's basically `io.open` + """ + nb, txt = nb_txt + with tempfile.TemporaryDirectory() as td: + nb_path = Path(td) / 'notebook.ipynb' + + # like _save_notebook[1] + with io.open(nb_path, 'w+', encoding=DEFAULT_ENCODING) as fp: + await write(nb, fp) + + # like _read_notebook[2] + with io.open(nb_path, 'r', encoding=DEFAULT_ENCODING) as fp: + await read(fp, as_version=nb["nbformat"]) + + +""" +[1]: https://github.com/jupyter/jupyter_server/blob/1.0.5/jupyter_server/services/contents/fileio.py#L279-L282 +[2]: https://github.com/jupyter/jupyter_server/blob/1.0.5/jupyter_server/services/contents/fileio.py#L254-L258 +""" diff --git a/nbformat/tests/test_validator.py b/nbformat/tests/test_validator.py index 7723dc91..aace13cb 100644 --- a/nbformat/tests/test_validator.py +++ b/nbformat/tests/test_validator.py @@ -9,6 +9,7 @@ from .base import TestsBase from jsonschema import ValidationError from nbformat import read +from ..constants import ENV_VAR_VALIDATOR from ..validator import isvalid, validate, iter_validate from ..json_compat import VALIDATORS @@ -19,14 +20,14 @@ @pytest.fixture(autouse=True) def clean_env_before_and_after_tests(): """Fixture to clean up env variables before and after tests.""" - os.environ.pop("NBFORMAT_VALIDATOR", None) + os.environ.pop(ENV_VAR_VALIDATOR, None) yield - os.environ.pop("NBFORMAT_VALIDATOR", None) + os.environ.pop(ENV_VAR_VALIDATOR, None) # Helpers def set_validator(validator_name): - os.environ["NBFORMAT_VALIDATOR"] = validator_name + os.environ[ENV_VAR_VALIDATOR] = validator_name @pytest.mark.parametrize("validator_name", VALIDATORS) diff --git a/nbformat/v3/tests/formattest.py b/nbformat/v3/tests/formattest.py index e8667586..ded63a80 100644 --- a/nbformat/v3/tests/formattest.py +++ b/nbformat/v3/tests/formattest.py @@ -6,6 +6,7 @@ pjoin = os.path.join +from ...constants import DEFAULT_ENCODING from ..nbbase import ( NotebookNode, new_code_cell, new_text_cell, new_worksheet, new_notebook @@ -16,7 +17,7 @@ def open_utf8(fname, mode): - return io.open(fname, mode=mode, encoding='utf-8') + return io.open(fname, mode=mode, encoding=DEFAULT_ENCODING) class NBFormatTest: """Mixin for writing notebook format tests""" @@ -25,16 +26,16 @@ class NBFormatTest: nb0_ref = None ext = None mod = None - + def setUp(self): self.wd = tempfile.mkdtemp() - + def tearDown(self): shutil.rmtree(self.wd) - + def assertNBEquals(self, nba, nbb): self.assertEqual(nba, nbb) - + def test_writes(self): s = self.mod.writes(nb0) if self.nb0_ref: @@ -51,13 +52,10 @@ def test_roundtrip(self): def test_write_file(self): with open_utf8(pjoin(self.wd, "nb0.%s" % self.ext), 'w') as f: self.mod.write(nb0, f) - + def test_read_file(self): with open_utf8(pjoin(self.wd, "nb0.%s" % self.ext), 'w') as f: self.mod.write(nb0, f) - + with open_utf8(pjoin(self.wd, "nb0.%s" % self.ext), 'r') as f: nb = self.mod.read(f) - - - diff --git a/nbformat/v3/tests/nbexamples.py b/nbformat/v3/tests/nbexamples.py index bdafc75c..0e0a5475 100644 --- a/nbformat/v3/tests/nbexamples.py +++ b/nbformat/v3/tests/nbexamples.py @@ -148,5 +148,3 @@ print "ünîcødé" """ % (nbformat, nbformat_minor) - - diff --git a/nbformat/v4/tests/formattest.py b/nbformat/v4/tests/formattest.py index 853083ec..6c67bb10 100644 --- a/nbformat/v4/tests/formattest.py +++ b/nbformat/v4/tests/formattest.py @@ -6,11 +6,12 @@ pjoin = os.path.join +from ...constants import DEFAULT_ENCODING from .nbexamples import nb0 def open_utf8(fname, mode): - return io.open(fname, mode=mode, encoding='utf-8') + return io.open(fname, mode=mode, encoding=DEFAULT_ENCODING) class NBFormatTest: """Mixin for writing notebook format tests""" diff --git a/nbformat/v4/tests/nbexamples.py b/nbformat/v4/tests/nbexamples.py index 9602740b..2133a3f9 100644 --- a/nbformat/v4/tests/nbexamples.py +++ b/nbformat/v4/tests/nbexamples.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os from ..._compat import encodebytes @@ -131,5 +129,3 @@ 'language': 'python', } ) - - diff --git a/nbformat/v4/tests/test_convert.py b/nbformat/v4/tests/test_convert.py index e4492d30..12f0113f 100644 --- a/nbformat/v4/tests/test_convert.py +++ b/nbformat/v4/tests/test_convert.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import copy from nbformat import validate diff --git a/nbformat/v4/tests/test_json.py b/nbformat/v4/tests/test_json.py index ea157e54..84d52e2b 100644 --- a/nbformat/v4/tests/test_json.py +++ b/nbformat/v4/tests/test_json.py @@ -3,6 +3,7 @@ from unittest import TestCase from ..._compat import decodebytes +from ...constants import DEFAULT_ENCODING from ..nbjson import reads, writes from .. import nbjson, nbformat, nbformat_minor from .nbexamples import nb0 diff --git a/nbformat/v4/tests/test_validate.py b/nbformat/v4/tests/test_validate.py index a3f98371..93881f7e 100644 --- a/nbformat/v4/tests/test_validate.py +++ b/nbformat/v4/tests/test_validate.py @@ -9,6 +9,7 @@ import pytest from nbformat.validator import validate, ValidationError +from ...constants import DEFAULT_ENCODING from ..nbjson import reads from ..nbbase import ( nbformat, @@ -100,6 +101,6 @@ def test_invalid_raw_cell(): def test_sample_notebook(): here = os.path.dirname(__file__) - with io.open(os.path.join(here, os.pardir, os.pardir, 'tests', "test4.ipynb"), encoding='utf-8') as f: + with io.open(os.path.join(here, os.pardir, os.pardir, 'tests', "test4.ipynb"), encoding=DEFAULT_ENCODING) as f: nb = reads(f.read()) validate4(nb) diff --git a/setup.py b/setup.py index 5fb46c8d..e48c692b 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. @@ -61,20 +60,22 @@ author_email = 'jupyter@googlegroups.com', url = 'http://jupyter.org', license = 'BSD', - python_requires = '>=3.5', + python_requires = '>=3.6', platforms = "Linux, Mac OS X, Windows", keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'], classifiers = [ + 'Framework :: Jupyter', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], ) @@ -84,16 +85,19 @@ setuptools_args = {} install_requires = setuptools_args['install_requires'] = [ 'ipython_genutils', - 'traitlets>=4.1', 'jsonschema>=2.4,!=2.5.0', 'jupyter_core', + 'traitlets>=4.1', ] extras_require = setuptools_args['extras_require'] = { + 'async': ['aiofiles>=0.6.0'], 'fast': ['fastjsonschema'], - 'test': ['fastjsonschema', 'testpath', 'pytest', 'pytest-cov'], + 'test': ['hypothesis', 'pytest', 'pytest-asyncio', 'pytest-cov', 'testfixtures', 'testpath'], } +extras_require['test'] = sorted(set(sum(extras_require.values(), []))) + if 'setuptools' in sys.modules: setup_args.update(setuptools_args) setup_args['entry_points'] = {