mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-13 17:42:42 +08:00
Merge pull request #1289 from quantopian/wildcard
wildcard object and doctests
This commit is contained in:
+1
-1
@@ -43,7 +43,7 @@ before_script:
|
||||
- pip freeze | sort
|
||||
script:
|
||||
- flake8 zipline tests
|
||||
- nosetests --with-coverage tests/
|
||||
- nosetests --with-coverage
|
||||
# deactive env to get access to anaconda command
|
||||
- source deactivate
|
||||
- if [[ "$TRAVIS_SECURE_ENV_VARS" = "true" && "$TRAVIS_BRANCH" = "master" && "$TRAVIS_PULL_REQUEST" = "false" ]]; then DO_UPLOAD="true"; else DO_UPLOAD="false"; fi
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ install:
|
||||
- pip freeze | sort
|
||||
|
||||
test_script:
|
||||
- nosetests tests/
|
||||
- nosetests
|
||||
- flake8 zipline tests
|
||||
|
||||
branches:
|
||||
|
||||
@@ -5,6 +5,8 @@ with-ignore-docstrings=1
|
||||
with-timer=1
|
||||
timer-top-n=15
|
||||
cover-package=zipline
|
||||
with-doctest=1
|
||||
testmatch=(?:^|[\\b_\\.-])[Tt]est(?!ing)
|
||||
|
||||
[metadata]
|
||||
description-file = README.rst
|
||||
|
||||
@@ -125,7 +125,7 @@ def to_dict(l):
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> to_dict([2, 3, 4])
|
||||
>>> to_dict([2, 3, 4]) # doctest: +SKIP
|
||||
{'0': 2, '1': 3, '2': 4}
|
||||
"""
|
||||
return dict(zip(map(str, range(len(l))), l))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2013 Quantopian, Inc.
|
||||
#
|
||||
@@ -3046,7 +3046,7 @@ class TestAccountControls(WithDataPortal, WithSimParams, ZiplineTestCase):
|
||||
# format(i, actual_position, expected_positions[i]))
|
||||
|
||||
|
||||
class TestFutureFlip(WithSimParams, WithDataPortal, ZiplineTestCase):
|
||||
class TestFutureFlip(WithDataPortal, WithSimParams, ZiplineTestCase):
|
||||
START_DATE = pd.Timestamp('2006-01-09', tz='utc')
|
||||
END_DATE = pd.Timestamp('2006-01-10', tz='utc')
|
||||
sid, = ASSET_FINDER_EQUITY_SIDS = (1,)
|
||||
@@ -3057,8 +3057,8 @@ class TestFutureFlip(WithSimParams, WithDataPortal, ZiplineTestCase):
|
||||
{
|
||||
cls.sid: factory.create_trade_history(
|
||||
cls.sid,
|
||||
[1, 2, 4],
|
||||
[1e9, 1e9, 1e9],
|
||||
[1, 2],
|
||||
[1e9, 1e9],
|
||||
timedelta(days=1),
|
||||
cls.sim_params,
|
||||
cls.trading_schedule,
|
||||
@@ -3067,7 +3067,7 @@ class TestFutureFlip(WithSimParams, WithDataPortal, ZiplineTestCase):
|
||||
index=cls.sim_params.trading_days,
|
||||
)
|
||||
|
||||
@skip
|
||||
@skip('broken in zipline 1.0.0')
|
||||
def test_flip_algo(self):
|
||||
metadata = {1: {'symbol': 'TEST',
|
||||
'start_date': self.sim_params.trading_days[0],
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
import doctest
|
||||
from unittest import TestCase
|
||||
|
||||
from zipline import testing
|
||||
from zipline.lib import adjustment, normalize
|
||||
from zipline.pipeline import (
|
||||
engine,
|
||||
expression,
|
||||
)
|
||||
from zipline.utils import (
|
||||
cache,
|
||||
data,
|
||||
functional,
|
||||
input_validation,
|
||||
memoize,
|
||||
numpy_utils,
|
||||
preprocess,
|
||||
)
|
||||
|
||||
|
||||
class DoctestTestCase(TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
import pdb
|
||||
# Workaround for the issue addressed by this (unmerged) PR to pdbpp:
|
||||
# https://bitbucket.org/antocuni/pdb/pull-request/40/fix-ensure_file_can_write_unicode/diff # noqa
|
||||
if '_pdbpp_path_hack' in pdb.__file__:
|
||||
cls._skip = True
|
||||
else:
|
||||
cls._skip = False
|
||||
cls.flags = doctest.REPORT_CDIFF | doctest.IGNORE_EXCEPTION_DETAIL
|
||||
|
||||
def _check_docs(self, module):
|
||||
if self._skip:
|
||||
# Printing this directly to __stdout__ so that it doesn't get
|
||||
# captured by nose.
|
||||
print("Warning: Skipping doctests for %s because "
|
||||
"pdbpp is installed." % module.__name__, file=sys.__stdout__)
|
||||
return
|
||||
try:
|
||||
doctest.testmod(
|
||||
module,
|
||||
verbose=True,
|
||||
raise_on_error=True,
|
||||
optionflags=self.flags,
|
||||
)
|
||||
except doctest.UnexpectedException as e:
|
||||
raise e.exc_info[1]
|
||||
except doctest.DocTestFailure as e:
|
||||
print("Got:")
|
||||
print(e.got)
|
||||
raise
|
||||
|
||||
def test_adjustment_docs(self):
|
||||
self._check_docs(adjustment)
|
||||
|
||||
def test_expression_docs(self):
|
||||
self._check_docs(expression)
|
||||
|
||||
def test_engine_docs(self):
|
||||
self._check_docs(engine)
|
||||
|
||||
def test_memoize_docs(self):
|
||||
self._check_docs(memoize)
|
||||
|
||||
def test_testing_docs(self):
|
||||
self._check_docs(testing)
|
||||
|
||||
def test_preprocess_docs(self):
|
||||
self._check_docs(preprocess)
|
||||
|
||||
def test_input_validation_docs(self):
|
||||
self._check_docs(input_validation)
|
||||
|
||||
def test_cache_docs(self):
|
||||
self._check_docs(cache)
|
||||
|
||||
def test_numpy_utils_docs(self):
|
||||
self._check_docs(numpy_utils)
|
||||
|
||||
def test_data_docs(self):
|
||||
self._check_docs(data)
|
||||
|
||||
def test_functional_docs(self):
|
||||
self._check_docs(functional)
|
||||
|
||||
def test_normalize_docs(self):
|
||||
self._check_docs(normalize)
|
||||
@@ -122,7 +122,7 @@ def simplex_projection(v, b=1):
|
||||
|
||||
:Example:
|
||||
>>> proj = simplex_projection([.4 ,.3, -.4, .5])
|
||||
>>> print(proj)
|
||||
>>> proj # doctest: +NORMALIZE_WHITESPACE
|
||||
array([ 0.33333333, 0.23333333, 0. , 0.43333333])
|
||||
>>> print(proj.sum())
|
||||
1.0
|
||||
|
||||
@@ -226,7 +226,7 @@ class LabelArray(ndarray):
|
||||
|
||||
1. Someone tries to directly construct a new array by doing::
|
||||
|
||||
>>> ndarray.__new__(LabelArray, ...)
|
||||
>>> ndarray.__new__(LabelArray, ...) # doctest: +SKIP
|
||||
|
||||
In this case, obj will be None. We treat this as an error case and
|
||||
fail.
|
||||
|
||||
@@ -333,9 +333,9 @@ class Factor(RestrictedDTypeMixin, ComputableTerm):
|
||||
Factors. For example, constructing a Factor that computes the average of
|
||||
two other Factors is simply::
|
||||
|
||||
>>> f1 = SomeFactor(...)
|
||||
>>> f2 = SomeOtherFactor(...)
|
||||
>>> average = (f1 + f2) / 2.0
|
||||
>>> f1 = SomeFactor(...) # doctest: +SKIP
|
||||
>>> f2 = SomeOtherFactor(...) # doctest: +SKIP
|
||||
>>> average = (f1 + f2) / 2.0 # doctest: +SKIP
|
||||
|
||||
Factors can also be converted into :class:`zipline.pipeline.Filter` objects
|
||||
via comparison operators: (``<``, ``<=``, ``!=``, ``eq``, ``>``, ``>=``).
|
||||
@@ -492,8 +492,10 @@ class Factor(RestrictedDTypeMixin, ComputableTerm):
|
||||
to use the ``mask`` parameter to discard values at the extremes of the
|
||||
distribution::
|
||||
|
||||
>>> base = MyFactor(...)
|
||||
>>> normalized = base.demean(mask=base.percentile_between(1, 99))
|
||||
>>> base = MyFactor(...) # doctest: +SKIP
|
||||
>>> normalized = base.demean(
|
||||
... mask=base.percentile_between(1, 99),
|
||||
... ) # doctest: +SKIP
|
||||
|
||||
``demean()`` is only supported on Factors of dtype float64.
|
||||
|
||||
@@ -553,8 +555,10 @@ class Factor(RestrictedDTypeMixin, ComputableTerm):
|
||||
outliers, it is often useful to use the ``mask`` parameter to discard
|
||||
values at the extremes of the distribution::
|
||||
|
||||
>>> base = MyFactor(...)
|
||||
>>> normalized = base.zscore(mask=base.percentile_between(1, 99))
|
||||
>>> base = MyFactor(...) # doctest: +SKIP
|
||||
>>> normalized = base.zscore(
|
||||
... mask=base.percentile_between(1, 99),
|
||||
... ) # doctest: +SKIP
|
||||
|
||||
``zscore()`` is only supported on Factors of dtype float64.
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ def delimit(delimiters, content):
|
||||
"""
|
||||
Surround `content` with the first and last characters of `delimiters`.
|
||||
|
||||
>>> delimit('[]', "foo")
|
||||
[foo]
|
||||
>>> delimit('""', "foo")
|
||||
>>> delimit('[]', "foo") # doctest: +SKIP
|
||||
'[foo]'
|
||||
>>> delimit('""', "foo") # doctest: +SKIP
|
||||
'"foo"'
|
||||
"""
|
||||
if len(delimiters) != 2:
|
||||
@@ -220,7 +220,7 @@ def format_attrs(attrs):
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> format_attrs({'key1': 'value1', 'key2': 'value2'})
|
||||
>>> format_attrs({'key1': 'value1', 'key2': 'value2'}) # doctest: +SKIP
|
||||
'[key1=value1, key2=value2]'
|
||||
"""
|
||||
if not attrs:
|
||||
|
||||
+27
-1
@@ -12,6 +12,7 @@ import operator
|
||||
import os
|
||||
from os.path import abspath, dirname, join, realpath
|
||||
import shutil
|
||||
from sys import _getframe
|
||||
import tempfile
|
||||
|
||||
from logbook import TestHandler
|
||||
@@ -23,7 +24,7 @@ from six import itervalues, iteritems, with_metaclass
|
||||
from six.moves import filter, map
|
||||
from sqlalchemy import create_engine
|
||||
from testfixtures import TempDirectory
|
||||
from toolz import concat
|
||||
from toolz import concat, curry
|
||||
|
||||
from zipline.assets import AssetFinder, AssetDBWriter
|
||||
from zipline.assets.synthetic import make_simple_equity_info
|
||||
@@ -860,6 +861,7 @@ class SubTestFailures(AssertionError):
|
||||
)
|
||||
|
||||
|
||||
@nottest
|
||||
def subtest(iterator, *_names):
|
||||
"""
|
||||
Construct a subtest in a unittest.
|
||||
@@ -1374,3 +1376,27 @@ def patch_read_csv(url_map, module=pd, strict=False):
|
||||
|
||||
with patch.object(module, 'read_csv', patched_read_csv):
|
||||
yield
|
||||
|
||||
|
||||
@curry
|
||||
def ensure_doctest(f, name=None):
|
||||
"""Ensure that an object gets doctested. This is useful for instances
|
||||
of objects like curry or partial which are not discovered by default.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
f : any
|
||||
The thing to doctest.
|
||||
name : str, optional
|
||||
The name to use in the doctest function mapping. If this is None,
|
||||
Then ``f.__name__`` will be used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
f : any
|
||||
``f`` unchanged.
|
||||
"""
|
||||
_getframe(2).f_globals.setdefault('__test__', {})[
|
||||
f.__name__ if name is None else name
|
||||
] = f
|
||||
return f
|
||||
|
||||
@@ -210,7 +210,7 @@ def alias(attr_name):
|
||||
>>> class C(object):
|
||||
... attr = 1
|
||||
...
|
||||
>>> class D(object):
|
||||
>>> class D(C):
|
||||
... attr_alias = alias('attr')
|
||||
...
|
||||
>>> D.attr
|
||||
|
||||
@@ -39,12 +39,47 @@ from six import iteritems, viewkeys, PY2
|
||||
from toolz import dissoc, keyfilter
|
||||
import toolz.curried.operator as op
|
||||
|
||||
from zipline.testing.core import ensure_doctest
|
||||
from zipline.dispatch import dispatch
|
||||
from zipline.lib.adjustment import Adjustment
|
||||
from zipline.utils.functional import dzip_exact
|
||||
from zipline.utils.functional import dzip_exact, instance
|
||||
from zipline.utils.math_utils import tolerant_equals
|
||||
|
||||
|
||||
@instance
|
||||
@ensure_doctest
|
||||
class wildcard(object):
|
||||
"""An object that compares equal to any other object.
|
||||
|
||||
This is useful when using :func:`~zipline.testing.predicates.assert_equal`
|
||||
with a large recursive structure and some fields to be ignored.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> wildcard == 5
|
||||
True
|
||||
>>> wildcard == 'ayy'
|
||||
True
|
||||
|
||||
# reflected
|
||||
>>> 5 == wildcard
|
||||
True
|
||||
>>> 'ayy' == wildcard
|
||||
True
|
||||
"""
|
||||
@staticmethod
|
||||
def __eq__(other):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def __ne__(other):
|
||||
return False
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s>' % type(self).__name__
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
def keywords(func):
|
||||
"""Get the argument names of a function
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ class CachedObject(namedtuple("_CachedObject", "value expires")):
|
||||
>>> obj.unwrap(expires)
|
||||
1
|
||||
>>> obj.unwrap(expires + Timedelta('1 minute'))
|
||||
... # doctest: +IGNORE_EXCEPTION_DETAIL
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Expired: 2014-01-01 00:00:00+00:00
|
||||
|
||||
@@ -41,7 +41,7 @@ class CallbackManager(object):
|
||||
>>> with manager('example'):
|
||||
... print('inside example block')
|
||||
entering example block
|
||||
inside example
|
||||
inside example block
|
||||
exiting example block
|
||||
|
||||
These are reusable with different args:
|
||||
|
||||
@@ -23,7 +23,7 @@ def invert(d):
|
||||
"""
|
||||
Invert a dictionary into a dictionary of sets.
|
||||
|
||||
>>> invert({'a': 1, 'b': 2, 'c': 1})
|
||||
>>> invert({'a': 1, 'b': 2, 'c': 1}) # doctest: +SKIP
|
||||
{1: {'a', 'c'}, 2: {'b'}}
|
||||
"""
|
||||
out = {}
|
||||
|
||||
@@ -302,7 +302,8 @@ class AfterOpen(StatelessRule):
|
||||
A rule that triggers for some offset after the market opens.
|
||||
Example that triggers after 30 minutes of the market opening:
|
||||
|
||||
>>> AfterOpen(minutes=30)
|
||||
>>> AfterOpen(minutes=30) # doctest: +ELLIPSIS
|
||||
<zipline.utils.events.AfterOpen object at ...>
|
||||
"""
|
||||
def __init__(self, offset=None, **kwargs):
|
||||
self.offset = _build_offset(
|
||||
@@ -346,7 +347,8 @@ class BeforeClose(StatelessRule):
|
||||
A rule that triggers for some offset time before the market closes.
|
||||
Example that triggers for the last 30 minutes every day:
|
||||
|
||||
>>> BeforeClose(minutes=30)
|
||||
>>> BeforeClose(minutes=30) # doctest: +ELLIPSIS
|
||||
<zipline.utils.events.BeforeClose object at ...>
|
||||
"""
|
||||
def __init__(self, offset=None, **kwargs):
|
||||
self.offset = _build_offset(
|
||||
|
||||
@@ -69,7 +69,7 @@ class final(with_metaclass(ABCMeta)):
|
||||
|
||||
Example usage:
|
||||
>>> from six import with_metaclass
|
||||
>>> class C(with_metaclass(FinalMeta)):
|
||||
>>> class C(with_metaclass(FinalMeta, object)):
|
||||
... @final
|
||||
... def f(self):
|
||||
... return 'value'
|
||||
|
||||
@@ -220,14 +220,16 @@ def unzip(seq, elem_len=None):
|
||||
>>> seq = [('a', 1), ('b', 2), ('c', 3, 'extra')]
|
||||
>>> cs, ns = unzip(seq)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
...
|
||||
ValueError: element at index 2 was length 3, expected 2
|
||||
|
||||
# allows an explicit element length instead of infering
|
||||
>>> seq = [('a', 1, 'extra'), ('b', 2), ('c', 3)]
|
||||
>>> cs, ns = unzip(seq, 2)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: element at index 0 was length 3, expected 2
|
||||
|
||||
# handles empty sequences when a length is given
|
||||
>>> cs, ns = unzip([], elem_len=2)
|
||||
>>> cs == ns == ()
|
||||
|
||||
@@ -209,17 +209,19 @@ def expect_dtypes(**named):
|
||||
|
||||
Usage
|
||||
-----
|
||||
>>> from numpy import dtype, arange
|
||||
>>> @expect_dtypes(x=dtype(int))
|
||||
>>> from numpy import dtype, arange, int8, float64
|
||||
>>> @expect_dtypes(x=dtype(int8))
|
||||
... def foo(x, y):
|
||||
... return x, y
|
||||
...
|
||||
>>> foo(arange(3), 'foo')
|
||||
(array([0, 1, 2]), 'foo')
|
||||
>>> foo(arange(3, dtype=float), 'foo')
|
||||
>>> foo(arange(3, dtype=int8), 'foo')
|
||||
(array([0, 1, 2], dtype=int8), 'foo')
|
||||
>>> foo(arange(3, dtype=float64), 'foo') # doctest: +NORMALIZE_WHITESPACE
|
||||
... # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: foo() expected an argument with dtype 'int64' for argument 'x', but got dtype 'float64' instead. # noqa
|
||||
TypeError: ...foo() expected a value with dtype 'int8' for argument 'x',
|
||||
but got 'float64' instead.
|
||||
"""
|
||||
for name, type_ in iteritems(named):
|
||||
if not isinstance(type_, (dtype, tuple)):
|
||||
@@ -278,10 +280,11 @@ def expect_kinds(**named):
|
||||
2
|
||||
>>> foo(int32(2))
|
||||
2
|
||||
>>> foo(float32(2))
|
||||
>>> foo(float32(2)) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...n
|
||||
TypeError: foo() expected a numpy object of kind 'i' for argument 'x', but got 'f' instead. # noqa
|
||||
...
|
||||
TypeError: ...foo() expected a numpy object of kind 'i' for argument 'x',
|
||||
but got 'f' instead.
|
||||
"""
|
||||
for name, kind in iteritems(named):
|
||||
if not isinstance(kind, (str, tuple)):
|
||||
@@ -337,10 +340,11 @@ def expect_types(*_pos, **named):
|
||||
...
|
||||
>>> foo(2, '3')
|
||||
(2, '3')
|
||||
>>> foo(2.0, '3')
|
||||
>>> foo(2.0, '3') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: foo() expected an argument of type 'int' for argument 'x', but got float instead. # noqa
|
||||
TypeError: ...foo() expected a value of type int for argument 'x',
|
||||
but got float instead.
|
||||
"""
|
||||
if _pos:
|
||||
raise TypeError("expect_types() only takes keyword arguments.")
|
||||
@@ -463,10 +467,11 @@ def expect_element(*_pos, **named):
|
||||
'A'
|
||||
>>> foo('b')
|
||||
'B'
|
||||
>>> foo('c')
|
||||
>>> foo('c') # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: foo() expected a value in ('a', 'b') for argument 'x', but got 'c' instead. # noqa
|
||||
ValueError: ...foo() expected a value in ('a', 'b') for argument 'x',
|
||||
but got 'c' instead.
|
||||
|
||||
Notes
|
||||
-----
|
||||
@@ -505,10 +510,12 @@ def expect_dimensions(**dimensions):
|
||||
...
|
||||
>>> foo(array([1, 1]), array([[1, 1], [2, 2]]))
|
||||
2
|
||||
>>> foo(array([1, 1], array([1, 1])))
|
||||
>>> foo(array([1, 1]), array([1, 1])) # doctest: +NORMALIZE_WHITESPACE
|
||||
... # doctest: +ELLIPSIS
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: foo() expected a 2-D array for argument 'y', but got a 1-D array instead. # noqa
|
||||
ValueError: ...foo() expected a 2-D array for argument 'y',
|
||||
but got a 1-D array instead.
|
||||
"""
|
||||
def _expect_dimension(expected_ndim):
|
||||
def _check(func, argname, argvalue):
|
||||
|
||||
+15
-11
@@ -24,14 +24,15 @@ def compose_types(a, *cs):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class M(type):
|
||||
def __new__(mcls, name, bases, dict_):
|
||||
dict_['ayy'] = 'lmao'
|
||||
return super().__new__(mcls, name, bases, dict_)
|
||||
>>> class M(type):
|
||||
... def __new__(mcls, name, bases, dict_):
|
||||
... dict_['ayy'] = 'lmao'
|
||||
... return super(M, mcls).__new__(mcls, name, bases, dict_)
|
||||
|
||||
|
||||
class C(metaclass=M):
|
||||
pass
|
||||
>>> from six import with_metaclass
|
||||
>>> class C(with_metaclass(M, object)):
|
||||
... pass
|
||||
|
||||
|
||||
We now want to create a sublclass of ``C`` that is also an abstract class.
|
||||
@@ -43,10 +44,11 @@ def compose_types(a, *cs):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class D(C, metaclass=compose_types(M, ABCMeta)):
|
||||
@abstractmethod
|
||||
def f(self):
|
||||
raise NotImplementedError('f')
|
||||
>>> from abc import ABCMeta, abstractmethod
|
||||
>>> class D(with_metaclass(compose_types(M, ABCMeta), C)):
|
||||
... @abstractmethod
|
||||
... def f(self):
|
||||
... raise NotImplementedError('f')
|
||||
|
||||
|
||||
We can see that this class has both metaclasses applied to it:
|
||||
@@ -54,8 +56,10 @@ def compose_types(a, *cs):
|
||||
.. code-block:: python
|
||||
|
||||
>>> D.ayy
|
||||
lmao
|
||||
'lmao'
|
||||
>>> D()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError: Can't instantiate abstract class D with abstract methods f
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user