From 2d0a6add0a181af0fff3d800bafa9f42211dc7e6 Mon Sep 17 00:00:00 2001 From: Shivam Gaur Date: Mon, 29 May 2017 17:32:28 +0800 Subject: [PATCH 1/4] Modified Readme --- .gitignore | 7 +++++++ README.md | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5b9a758..5f12c18 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,10 @@ wheelhouse/ *.zip *.csv +# Pycharm files +.idea +.idea/droste-engarde.iml +.idea/misc.xml +.idea/modules.xml +.idea/workspace.xml +.idea/vcs.xml diff --git a/README.md b/README.md index d089b8b..b3155d9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -Engarde +Droste Engarde ======= -[![Build Status](https://travis-ci.org/TomAugspurger/engarde.svg)](https://travis-ci.org/TomAugspurger/engarde) +Droste's fork of TomAugspurger's engarde repo. A python package for defensive data analysis. Documentation is at [readthedocs](http://engarde.readthedocs.org/en/latest/). From eb4faada453e8056987738fc9c9c63fa81d63d10 Mon Sep 17 00:00:00 2001 From: Shivam Gaur Date: Mon, 29 May 2017 18:06:42 +0800 Subject: [PATCH 2/4] Added decorator, for checking if a DataFrame has all the columns specified in a list. Fixed PEP8 errors where possible. --- engarde/checks.py | 44 ++++++++++++++++++++++++++++++++++++++----- engarde/decorators.py | 21 ++++++++++++++++++++- engarde/generic.py | 5 ++++- examples/Trains.ipynb | 8 ++++---- tests/test_checks.py | 30 +++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 11 deletions(-) diff --git a/engarde/checks.py b/engarde/checks.py index 8819d3c..354ea51 100644 --- a/engarde/checks.py +++ b/engarde/checks.py @@ -42,6 +42,7 @@ def none_missing(df, columns=None): raise return df + def is_monotonic(df, items=None, increasing=None, strict=False): """ Asserts that the DataFrame is monotonic. @@ -68,7 +69,7 @@ def is_monotonic(df, items=None, increasing=None, strict=False): if increasing: good = getattr(s, 'is_monotonic_increasing') elif increasing is None: - good = getattr(s, 'is_monotonic') | getattr(s, 'is_monotonic_decreasing') + good = getattr(s, 'is_monotonic') | getattr(s, 'is_monotonic_decreasing') # noqa else: good = getattr(s, 'is_monotonic_decreasing') if strict: @@ -83,6 +84,7 @@ def is_monotonic(df, items=None, increasing=None, strict=False): raise AssertionError return df + def is_shape(df, shape): """ Asserts that the DataFrame is of a known shape. @@ -101,7 +103,7 @@ def is_shape(df, shape): """ try: check = np.all(np.equal(df.shape, shape) | (np.equal(shape, [-1, -1]) | - np.equal(shape, [None, None]))) + np.equal(shape, [None, None]))) # noqa assert check except AssertionError as e: msg = ("Expected shape: {}\n" @@ -110,6 +112,7 @@ def is_shape(df, shape): raise return df + def unique_index(df): """ Assert that the index is unique @@ -151,6 +154,7 @@ def within_set(df, items=None): raise AssertionError('Not in set', bad) return df + def within_range(df, items=None): """ Assert that a DataFrame is within a range. @@ -172,6 +176,7 @@ def within_range(df, items=None): raise AssertionError("Outside range", bad) return df + def within_n_std(df, n=3): """ Assert that every value is within ``n`` standard @@ -195,6 +200,7 @@ def within_n_std(df, n=3): raise AssertionError(msg) return df + def has_dtypes(df, items): """ Assert that a DataFrame has ``dtypes`` @@ -241,7 +247,8 @@ def one_to_many(df, unitcol, manycol): subset = df[[manycol, unitcol]].drop_duplicates() for many in subset[manycol].unique(): if subset[subset[manycol] == many].shape[0] > 1: - msg = "{} in {} has multiple values for {}".format(many, manycol, unitcol) + msg = "{} in {} has multiple values for {}".format(many, manycol, + unitcol) raise AssertionError(msg) return df @@ -270,7 +277,34 @@ def is_same_as(df, df_to_compare, **kwargs): return df +def has_columns(df, columns, **kwargs): + """ + Assert that a pandas dataframe contains given columns + + Parameters + ========== + :param df: + :param columns: + + df : pandas DataFrame + columns : list of columns + **kwargs : dict + keyword arguments passed through to panda's ``assert_frame_equal`` + + Returns + ======= + :return: df : pandas DataFrame + """ + missing_columns = [] + for x in columns: + if x not in df.columns: + missing_columns.append(x) + + if len(missing_columns) > 0: + raise AssertionError("DataFrame does not contain " + "required columns: {}".format(missing_columns)) + return df + __all__ = ['is_monotonic', 'is_same_as', 'is_shape', 'none_missing', 'unique_index', 'within_n_std', 'within_range', 'within_set', - 'has_dtypes', 'verify', 'verify_all', 'verify_any'] - + 'has_dtypes', 'has_columns' 'verify', 'verify_all', 'verify_any'] diff --git a/engarde/decorators.py b/engarde/decorators.py index ae417fc..164cdde 100644 --- a/engarde/decorators.py +++ b/engarde/decorators.py @@ -5,6 +5,7 @@ import engarde.checks as ck + def none_missing(columns=None): """Asserts that no missing values (NaN) are found""" def decorate(func): @@ -38,6 +39,7 @@ def wrapper(*args, **kwargs): return wrapper return decorate + def is_monotonic(items=None, increasing=None, strict=False): def decorate(func): @wraps(func) @@ -49,6 +51,7 @@ def wrapper(*args, **kwargs): return wrapper return decorate + def within_set(items): """ Check that DataFrame values are within set. @@ -102,6 +105,7 @@ def wrapper(*args, **kwargs): return wrapper return decorate + def has_dtypes(items): """ Tests that the dtypes are as specified in items. @@ -115,6 +119,18 @@ def wrapper(*args, **kwargs): return wrapper return decorate +def has_columns(columns): + """ + Tests that a dataframe contains required columns + """ + def decorate(func): + @wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + ck.has_columns(result, columns) + return result + return wrapper + return decorate def one_to_many(unitcol, manycol): """ Tests that each value in ``manycol`` only is associated with @@ -124,7 +140,7 @@ def decorate(func): @wraps(func) def wrapper(*args, **kwargs): result = func(*args, **kwargs) - ck.one_to_many(results, unitcol, manycol) + ck.one_to_many(result, unitcol, manycol) return result return wrapper return decorate @@ -136,18 +152,21 @@ def verify(func, *args, **kwargs): """ return _verify(func, None, *args, **kwargs) + def verify_all(func, *args, **kwargs): """ Assert that all of `func(*args, **kwargs)` are true. """ return _verify(func, 'all', *args, **kwargs) + def verify_any(func, *args, **kwargs): """ Assert that any of `func(*args, **kwargs)` are true. """ return _verify(func, 'any', *args, **kwargs) + def _verify(func, _kind, *args, **kwargs): d = {None: ck.verify, 'all': ck.verify_all, 'any': ck.verify_any} vfunc = d[_kind] diff --git a/engarde/generic.py b/engarde/generic.py index 0265924..ba1833b 100644 --- a/engarde/generic.py +++ b/engarde/generic.py @@ -37,6 +37,7 @@ def verify(df, check, *args, **kwargs): raise return df + def verify_all(df, check, *args, **kwargs): """ Verify that all the entries in ``check(df, *args, **kwargs)`` @@ -51,6 +52,7 @@ def verify_all(df, check, *args, **kwargs): raise return df + def verify_any(df, check, *args, **kwargs): """ Verify that any of the entries in ``check(df, *args, **kwargs)`` @@ -69,9 +71,10 @@ def verify_any(df, check, *args, **kwargs): # Error reporting # --------------- + def bad_locations(df): columns = df.columns - all_locs = chain.from_iterable(zip(df.index, cycle([col])) for col in columns) + all_locs = chain.from_iterable(zip(df.index, cycle([col])) for col in columns) # noqa bad = pd.Series(list(all_locs))[np.asarray(df).ravel(1)] msg = bad.values return msg diff --git a/examples/Trains.ipynb b/examples/Trains.ipynb index 42f8921..44ff20f 100644 --- a/examples/Trains.ipynb +++ b/examples/Trains.ipynb @@ -226,7 +226,6 @@ { "ename": "AssertionError", "evalue": "('rational not true for all', id choiceid choice price1 time1 change1 comfort1 price2 time2 \\\n13 2 3 choice2 2450 121 0 0 2450 93 \n18 2 8 choice2 2975 108 0 0 2450 108 \n27 3 6 choice2 1920 106 0 0 1440 96 \n28 3 7 choice1 1920 106 0 0 1920 96 \n33 4 1 choice2 545 105 1 1 545 85 \n... ... ... ... ... ... ... ... ... ... \n2899 233 10 choice1 1350 110 0 0 1350 95 \n2900 234 1 choice2 4400 85 1 1 3300 85 \n2907 234 8 choice2 3300 95 1 0 3300 85 \n2914 235 1 choice2 3000 75 2 1 3000 65 \n2916 235 3 choice2 2550 75 1 0 2100 55 \n\n change2 comfort2 \n13 0 1 \n18 0 1 \n27 0 1 \n28 0 1 \n33 1 1 \n... ... ... \n2899 0 1 \n2900 0 1 \n2907 0 1 \n2914 1 1 \n2916 1 1 \n\n[467 rows x 11 columns])", - "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mAssertionError\u001b[0m Traceback (most recent call last)", @@ -236,7 +235,8 @@ "\u001b[0;32m/Users/tom.augspurger/sandbox/engarde/engarde/decorators.py\u001b[0m in \u001b[0;36mwrapper\u001b[0;34m(*operation_args, **operation_kwargs)\u001b[0m\n\u001b[1;32m 147\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mwrapper\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0moperation_args\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0moperation_kwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 148\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0moperation_func\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0moperation_args\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0moperation_kwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 149\u001b[0;31m \u001b[0mvfunc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mfunc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 150\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 151\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mwrapper\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;32m/Users/tom.augspurger/sandbox/engarde/engarde/generic.py\u001b[0m in \u001b[0;36mverify_all\u001b[0;34m(df, check, *args, **kwargs)\u001b[0m\n\u001b[1;32m 40\u001b[0m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcheck\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mdf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 41\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 42\u001b[0;31m \u001b[0;32massert\u001b[0m \u001b[0mnp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mall\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 43\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mAssertionError\u001b[0m \u001b[0;32mas\u001b[0m \u001b[0me\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 44\u001b[0m \u001b[0mmsg\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m\"{} not true for all\"\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mformat\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcheck\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__name__\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", "\u001b[0;31mAssertionError\u001b[0m: ('rational not true for all', id choiceid choice price1 time1 change1 comfort1 price2 time2 \\\n13 2 3 choice2 2450 121 0 0 2450 93 \n18 2 8 choice2 2975 108 0 0 2450 108 \n27 3 6 choice2 1920 106 0 0 1440 96 \n28 3 7 choice1 1920 106 0 0 1920 96 \n33 4 1 choice2 545 105 1 1 545 85 \n... ... ... ... ... ... ... ... ... ... \n2899 233 10 choice1 1350 110 0 0 1350 95 \n2900 234 1 choice2 4400 85 1 1 3300 85 \n2907 234 8 choice2 3300 95 1 0 3300 85 \n2914 235 1 choice2 3000 75 2 1 3000 65 \n2916 235 3 choice2 2550 75 1 0 2100 55 \n\n change2 comfort2 \n13 0 1 \n18 0 1 \n27 0 1 \n28 0 1 \n33 1 1 \n... ... ... \n2899 0 1 \n2900 0 1 \n2907 0 1 \n2914 1 1 \n2916 1 1 \n\n[467 rows x 11 columns])" - ] + ], + "output_type": "error" } ], "source": [ @@ -307,7 +307,7 @@ "language_info": { "codemirror_mode": { "name": "ipython", - "version": 3 + "version": 3.0 }, "file_extension": ".py", "mimetype": "text/x-python", @@ -319,4 +319,4 @@ }, "nbformat": 4, "nbformat_minor": 0 -} +} \ No newline at end of file diff --git a/tests/test_checks.py b/tests/test_checks.py index b7450c8..4d77e82 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -11,9 +11,11 @@ def _add_n(df, n=1): return df + n + def _noop(df): return df + def test_none_missing(): df = pd.DataFrame(np.random.randn(5, 3)) result = ck.none_missing(df) @@ -24,6 +26,7 @@ def test_none_missing(): result = dc.none_missing()(_add_n)(df, n=2) tm.assert_frame_equal(result, df + 2) + def test_none_missing_raises(): df = pd.DataFrame(np.random.randn(5, 3)) df.iloc[0, 0] = np.nan @@ -33,6 +36,7 @@ def test_none_missing_raises(): with pytest.raises(AssertionError): dc.none_missing()(_add_n)(df, n=2) + def test_monotonic_increasing_lax(): df = pd.DataFrame([1, 2, 2]) tm.assert_frame_equal(df, ck.is_monotonic(df, increasing=True)) @@ -51,6 +55,7 @@ def test_monotonic_increasing_lax(): with pytest.raises(AssertionError): dc.is_monotonic(increasing=True)(_add_n)(df) + def test_monotonic_increasing_strict(): df = pd.DataFrame([1, 2, 3]) tm.assert_frame_equal(df, ck.is_monotonic(df, increasing=True, strict=True)) @@ -69,6 +74,7 @@ def test_monotonic_increasing_strict(): with pytest.raises(AssertionError): dc.is_monotonic(increasing=True, strict=True)(_add_n)(df) + def test_monotonic_decreasing(): df = pd.DataFrame([2, 2, 1]) tm.assert_frame_equal(df, ck.is_monotonic(df, increasing=False)) @@ -87,6 +93,7 @@ def test_monotonic_decreasing(): with pytest.raises(AssertionError): dc.is_monotonic(increasing=False)(_add_n)(df) + def test_monotonic_decreasing_strict(): df = pd.DataFrame([3, 2, 1]) tm.assert_frame_equal(df, ck.is_monotonic(df, increasing=False, @@ -106,6 +113,7 @@ def test_monotonic_decreasing_strict(): with pytest.raises(AssertionError): dc.is_monotonic(increasing=False, strict=True)(_add_n)(df) + def test_monotonic_either(): df = pd.DataFrame({'A': [1, 2, 2], 'B': [3, 2, 2]}) tm.assert_frame_equal(df, ck.is_monotonic(df)) @@ -118,6 +126,7 @@ def test_monotonic_either(): with pytest.raises(AssertionError): dc.is_monotonic()(_add_n)(df) + def test_monotonic_either_stict(): df = pd.DataFrame({'A': [1, 2, 3], 'B': [3, 2, 1]}) tm.assert_frame_equal(df, ck.is_monotonic(df, strict=True)) @@ -130,12 +139,14 @@ def test_monotonic_either_stict(): with pytest.raises(AssertionError): dc.is_monotonic(strict=True)(_add_n)(df) + def test_monotonic_items(): df = pd.DataFrame({'A': [1, 2, 3], 'B': [3, 2, 3]}) tm.assert_frame_equal(df, ck.is_monotonic(df, items={'A': (True, True)})) tm.assert_frame_equal(dc.is_monotonic(items={'A': (True, True)}, strict=True)(_add_n)( df), df + 1) + def test_is_shape(): shape = 10, 2 ig_0 = -1, 2 @@ -155,6 +166,7 @@ def test_is_shape(): with pytest.raises(AssertionError): dc.is_shape((9, 2))(_add_n)(df) + def test_unique_index(): df = pd.DataFrame([1, 2, 3], index=['a', 'b', 'c']) tm.assert_frame_equal(df, ck.unique_index(df)) @@ -166,6 +178,7 @@ def test_unique_index(): with pytest.raises(AssertionError): dc.unique_index()(_add_n)(df.reindex(['a', 'a', 'b'])) + def test_within_set(): df = pd.DataFrame({'A': [1, 2, 3], 'B': ['a', 'b', 'c']}) items = {'A': [1, 2, 3], 'B': ['a', 'b', 'c']} @@ -182,6 +195,7 @@ def test_within_set(): with pytest.raises(AssertionError): dc.within_set(items=items)(_noop)(df) + def test_within_range(): df = pd.DataFrame({'A': [-1, 0, 1]}) items = {'A': (-1, 1)} @@ -194,6 +208,7 @@ def test_within_range(): with pytest.raises(AssertionError): dc.within_range(items)(_noop)(df) + def test_within_n_std(): df = pd.DataFrame({'A': np.arange(10)}) tm.assert_frame_equal(df, ck.within_n_std(df)) @@ -204,6 +219,7 @@ def test_within_n_std(): with pytest.raises(AssertionError): dc.within_n_std(.5)(_noop)(df) + def test_has_dtypes(): df = pd.DataFrame({'A': np.random.randint(0, 10, 10), 'B': np.random.randn(10), @@ -219,6 +235,7 @@ def test_has_dtypes(): with pytest.raises(AssertionError): dc.has_dtypes(items={'A': bool})(_noop)(df) + def test_one_to_many(): df = pd.DataFrame({ 'parameter': ['Cu', 'Cu', 'Pb', 'Pb'], @@ -228,6 +245,7 @@ def test_one_to_many(): result = ck.one_to_many(df, 'units', 'parameter') tm.assert_frame_equal(df, result) + def test_one_to_many_raises(): df = pd.DataFrame({ 'parameter': ['Cu', 'Cu', 'Pb', 'Pb'], @@ -237,6 +255,14 @@ def test_one_to_many_raises(): with pytest.raises(AssertionError): ck.one_to_many(df, 'units', 'parameter') + +def test_has_columns(): + df = pd.DataFrame([[0, 0, 0], [0, 0, 0], [0, 0, 0]], + columns=['a', 'b', 'c']) + with pytest.raises(AssertionError): + ck.has_columns(df, ['c', 'd', 'e']) + + def test_verify(): f = lambda x, n: len(x) > n df = pd.DataFrame({'A': [1, 2, 3]}) @@ -251,6 +277,7 @@ def test_verify(): ck.verify(df, f, n=4) dc.verify(f, n=4)(_noop)(df) + def test_verify_all(): f = lambda x, n: x > n df = pd.DataFrame({'A': [1, 2, 3]}) @@ -261,6 +288,7 @@ def test_verify_all(): ck.verify_all(df, f, n=2) dc.verify_all(f, n=2)(df) + def test_verify_any(): f = lambda x, n: x > n df = pd.DataFrame({'A': [1, 2, 3]}) @@ -271,6 +299,7 @@ def test_verify_any(): ck.verify_any(df, f, n=4) dc.verify_any(f, n=4)(df) + def test_is_same_as(): df = pd.DataFrame({'A': [1, 2, 3], 'B': [1, 2, 3]}) df_equal = pd.DataFrame({'A': [1, 2, 3], 'B': [1, 2, 3]}) @@ -286,6 +315,7 @@ def test_is_same_as(): ck.is_same_as(df, df_not_equal) dc.is_same_as(df_not_equal)(_noop)(df) + def test_is_same_as_with_kwargs(): df = pd.DataFrame({'A': [1, 2, 3], 'B': [1, 2, 3]}) df_equal = pd.DataFrame({'A': [1, 2, 3], 'B': [1, 2, 3]}) From 2ea74627e67d007fcb5b3a8627bce368f956955f Mon Sep 17 00:00:00 2001 From: Shivam Gaur Date: Mon, 29 May 2017 18:11:09 +0800 Subject: [PATCH 3/4] Changed readme file --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b3155d9..9b2e5b8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -Droste Engarde +Engarde ======= -Droste's fork of TomAugspurger's engarde repo. +[![Build Status](https://travis-ci.org/TomAugspurger/engarde.svg)](https://travis-ci.org/TomAugspurger/engarde) A python package for defensive data analysis. Documentation is at [readthedocs](http://engarde.readthedocs.org/en/latest/). @@ -68,4 +68,4 @@ See Also ======== - [assertr](https://github.com/tonyfischetti/assertr) -- [Validada](https://github.com/jnmclarty/validada) +- [Validada](https://github.com/jnmclarty/validada) \ No newline at end of file From 343bb3a104af6d673d600147071295c5e730b72f Mon Sep 17 00:00:00 2001 From: Shivam Gaur Date: Mon, 29 May 2017 20:48:21 +0800 Subject: [PATCH 4/4] added 'has_columns' to __all__ list in decorators.py --- engarde/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engarde/decorators.py b/engarde/decorators.py index 164cdde..28c780a 100644 --- a/engarde/decorators.py +++ b/engarde/decorators.py @@ -194,5 +194,5 @@ def wrapper(*args, **kwargs): __all__ = ['is_monotonic', 'is_same_as', 'is_shape', 'none_missing', 'unique_index', 'within_range', 'within_set', 'has_dtypes', - 'verify', 'verify_all', 'verify_any', 'within_n_std'] + 'has_columns', 'verify', 'verify_all', 'verify_any', 'within_n_std']