From 7e57940268d895a11f260aedbe395f9e4251ec97 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Tue, 16 Mar 2021 12:02:54 -0400 Subject: [PATCH 01/10] init dev branch --- attmap/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/attmap/_version.py b/attmap/_version.py index f23a6b3..bb8bac6 100644 --- a/attmap/_version.py +++ b/attmap/_version.py @@ -1 +1 @@ -__version__ = "0.13.0" +__version__ = "0.13.1-dev" From 53548a51e124857c974084daf92806d3b6c7a0b6 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Tue, 16 Mar 2021 12:03:44 -0400 Subject: [PATCH 02/10] prevent backslashes duplication; https://github.com/databio/yacman/issues/32 --- attmap/_att_map_like.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/attmap/_att_map_like.py b/attmap/_att_map_like.py index 86eb91a..8ccc5da 100644 --- a/attmap/_att_map_like.py +++ b/attmap/_att_map_like.py @@ -75,9 +75,8 @@ def _custom_repr(obj, prefix=""): :return str: custom object representation """ if isinstance(obj, list) and len(obj) > 0: - return "\n{} - ".format(prefix) + \ - "\n{} - ".format(prefix).join([str(i) for i in obj]) - return repr(obj).strip("'") + return f"\n{prefix} - " + f"\n{prefix} - ".join([str(i) for i in obj]) + return obj.strip("'") if hasattr(obj, "strip") else str(obj) class_name = self.__class__.__name__ if class_name in exclude_class_list: From d51ca7486ef117da4d2441a0142b5dc4e0686f77 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Tue, 16 Mar 2021 12:11:28 -0400 Subject: [PATCH 03/10] reformat, update CI --- .github/workflows/black.yml | 11 ++ .github/workflows/run-pytest.yml | 50 +++++++ .travis.yml | 20 --- attmap/__init__.py | 24 +++- attmap/_att_map_like.py | 38 +++-- attmap/attmap.py | 18 ++- attmap/attmap_echo.py | 5 +- attmap/helpers.py | 7 +- attmap/ordattmap.py | 19 +-- attmap/pathex_attmap.py | 13 +- setup.py | 25 ++-- tests/conftest.py | 14 +- tests/helpers.py | 19 ++- tests/regression/test_echo_subclass.py | 2 + tests/test_AttMap.py | 185 ++++++++++++++----------- tests/test_basic_ops_dynamic.py | 49 ++++--- tests/test_basic_ops_static.py | 30 +++- tests/test_echo.py | 23 +-- tests/test_equality.py | 63 ++++++--- tests/test_ordattmap.py | 130 +++++++++++------ tests/test_packaging.py | 30 ++-- tests/test_path_expansion.py | 111 +++++++++++---- tests/test_special_mutability.py | 6 +- tests/test_to_dict.py | 18 ++- tests/test_to_disk.py | 82 +++++++---- tests/test_to_map.py | 63 ++++++--- 26 files changed, 703 insertions(+), 352 deletions(-) create mode 100644 .github/workflows/black.yml create mode 100644 .github/workflows/run-pytest.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 0000000..f58e4c6 --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,11 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: psf/black@stable diff --git a/.github/workflows/run-pytest.yml b/.github/workflows/run-pytest.yml new file mode 100644 index 0000000..216adf2 --- /dev/null +++ b/.github/workflows/run-pytest.yml @@ -0,0 +1,50 @@ +name: Run pytests + +on: + push: + branches: [master, dev] + pull_request: + branches: [master, dev] + +jobs: + pytest: + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + os: [ubuntu-latest] # can't use macOS when using service containers or container jobs + runs-on: ${{ matrix.os }} + services: + postgres: + image: postgres + env: # needs to match DB config in: ../../tests/data/config.yaml + POSTGRES_USER: postgres + POSTGRES_PASSWORD: pipestat-password + POSTGRES_DB: pipestat-test + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dev dependancies + run: if [ -f requirements/requirements-dev.txt ]; then pip install -r requirements/requirements-dev.txt; fi + + - name: Install test dependancies + run: if [ -f requirements/requirements-test.txt ]; then pip install -r requirements/requirements-test.txt; fi + + - name: Install pipestat + run: python -m pip install . + + - name: Run pytest tests + run: pytest tests -x -vv --cov=./ --cov-report=xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + file: ./coverage.xml + name: py-${{ matrix.python-version }}-${{ matrix.os }} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 06b2ccb..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: python -python: - - "2.7" - - "3.5" - - "3.6" - - "3.7" -os: - - linux -install: - - pip install --upgrade six - - pip install . - - pip install -r requirements/requirements-dev.txt - - pip install -r requirements/requirements-test.txt -script: pytest --cov=attmap --hypothesis-show-statistics -after_success: - - coveralls -branches: - only: - - dev - - master diff --git a/attmap/__init__.py b/attmap/__init__.py index e85a327..898e767 100644 --- a/attmap/__init__.py +++ b/attmap/__init__.py @@ -1,19 +1,29 @@ """ Package-scope definitions """ from ._att_map_like import AttMapLike +from ._version import __version__ from .attmap import AttMap from .attmap_echo import * from .helpers import * from .ordattmap import OrdAttMap from .pathex_attmap import PathExAttMap -from ._version import __version__ AttributeDict = AttMap AttributeDictEcho = AttMapEcho -__all__ = ["AttMapLike", "AttMap", "AttMapEcho", "AttributeDict", - "AttributeDictEcho", "EchoAttMap", "OrdAttMap", "PathExAttMap", - "get_data_lines"] -__aliases__ = {"AttMap": ["AttributeDict"], - "AttMapEcho": ["AttributeDictEcho"], - "EchoAttMap": ["AttMapEcho"]} +__all__ = [ + "AttMapLike", + "AttMap", + "AttMapEcho", + "AttributeDict", + "AttributeDictEcho", + "EchoAttMap", + "OrdAttMap", + "PathExAttMap", + "get_data_lines", +] +__aliases__ = { + "AttMap": ["AttributeDict"], + "AttMapEcho": ["AttributeDictEcho"], + "EchoAttMap": ["AttMapEcho"], +} diff --git a/attmap/_att_map_like.py b/attmap/_att_map_like.py index 8ccc5da..239e37b 100644 --- a/attmap/_att_map_like.py +++ b/attmap/_att_map_like.py @@ -2,11 +2,13 @@ import abc import sys + if sys.version_info < (3, 3): from collections import Mapping, MutableMapping else: from collections.abc import Mapping, MutableMapping -from .helpers import is_custom_map, get_data_lines, get_logger + +from .helpers import get_data_lines, get_logger, is_custom_map __author__ = "Vince Reuter" __email__ = "vreuter@virginia.edu" @@ -61,8 +63,9 @@ def __len__(self): return sum(1 for _ in iter(self)) def __repr__(self): - return self._render(self._simplify_keyvalue( - self._data_for_repr(), self._new_empty_basic_map)) + return self._render( + self._simplify_keyvalue(self._data_for_repr(), self._new_empty_basic_map) + ) def _render(self, data, exclude_class_list=[]): def _custom_repr(obj, prefix=""): @@ -81,7 +84,7 @@ def _custom_repr(obj, prefix=""): class_name = self.__class__.__name__ if class_name in exclude_class_list: base = "" - else: + else: base = class_name + "\n" if data: @@ -108,13 +111,21 @@ def add_entries(self, entries): except AttributeError: entries_iter = entries for k, v in entries_iter: - self[k] = v if (k not in self or not isinstance(v, Mapping) - or not isinstance(self[k], Mapping)) \ + self[k] = ( + v + if ( + k not in self + or not isinstance(v, Mapping) + or not isinstance(self[k], Mapping) + ) else self[k].add_entries(v) + ) return self - def get_yaml_lines(self, conversions=( - (lambda obj: isinstance(obj, Mapping) and 0 == len(obj), None), )): + def get_yaml_lines( + self, + conversions=((lambda obj: isinstance(obj, Mapping) and 0 == len(obj), None),), + ): """ Get collection of lines that define YAML text rep. of this instance. @@ -126,8 +137,8 @@ def get_yaml_lines(self, conversions=( if 0 == len(self): return ["{}"] data = self._simplify_keyvalue( - self._data_for_repr(), self._new_empty_basic_map, - conversions=conversions) + self._data_for_repr(), self._new_empty_basic_map, conversions=conversions + ) return self._render(data).split("\n")[1:] def is_null(self, item): @@ -180,8 +191,9 @@ def _data_for_repr(self): :return Iterable[(hashable, object)]: collection of key-value pairs to include in object's text representation """ - return filter(lambda kv: not self._excl_from_repr(kv[0], self.__class__), - self.items()) + return filter( + lambda kv: not self._excl_from_repr(kv[0], self.__class__), self.items() + ) def _excl_from_eq(self, k): """ @@ -232,7 +244,7 @@ def _simplify_keyvalue(self, kvs, build, acc=None, conversions=None): if is_custom_map(v): v = self._simplify_keyvalue(v.items(), build, build()) if isinstance(v, Mapping): - for pred, proxy in (conversions or []): + for pred, proxy in conversions or []: if pred(v): v = proxy break diff --git a/attmap/attmap.py b/attmap/attmap.py index daab066..b8c7269 100644 --- a/attmap/attmap.py +++ b/attmap/attmap.py @@ -1,14 +1,14 @@ """ Dot notation support for Mappings. """ import sys + if sys.version_info < (3, 3): from collections import Mapping else: from collections.abc import Mapping -from .helpers import copy, get_logger, safedel_message from ._att_map_like import AttMapLike - +from .helpers import copy, get_logger, safedel_message _LOGGER = get_logger(__name__) @@ -61,11 +61,14 @@ def __ne__(self, other): @staticmethod def _cmp(a, b): """ Hook to tailor value comparison in determination of map equality. """ + def same_type(obj1, obj2, typenames=None): t1, t2 = str(obj1.__class__), str(obj2.__class__) return (t1 in typenames and t2 in typenames) if typenames else t1 == t2 - if same_type(a, b, ["", ""]) or \ - same_type(a, b, [""]): + + if same_type( + a, b, ["", ""] + ) or same_type(a, b, [""]): check = lambda x, y: (x == y).all() elif same_type(a, b, [""]): check = lambda x, y: (x == y).all().all() @@ -104,8 +107,9 @@ def _metamorph_maplike(self, m): """ _LOGGER.debug("Transforming map-like: {}".format(m)) if not isinstance(m, Mapping): - raise TypeError("Cannot integrate a non-Mapping: {}\nType: {}". - format(m, type(m))) + raise TypeError( + "Cannot integrate a non-Mapping: {}\nType: {}".format(m, type(m)) + ) return self._lower_type_bound(m.items()) def _new_empty_basic_map(self): @@ -120,4 +124,4 @@ def _repr_pretty_(self, p, cycle): :param bool cycle: whether a cyclic reference is detected :return str: text representation of the instance """ - return p.text(repr(self) if not cycle else '...') + return p.text(repr(self) if not cycle else "...") diff --git a/attmap/attmap_echo.py b/attmap/attmap_echo.py index 101b6bc..77551c9 100644 --- a/attmap/attmap_echo.py +++ b/attmap/attmap_echo.py @@ -32,8 +32,9 @@ def __getattr__(self, item, default=None, expand=True): return super(EchoAttMap, self).__getattr__(item, default, expand) except (AttributeError, TypeError): # If not, triage and cope accordingly. - if self._is_od_member(item) or \ - (item.startswith("__") and item.endswith("__")): + if self._is_od_member(item) or ( + item.startswith("__") and item.endswith("__") + ): # Accommodate security-through-obscurity approach of some libs. error_reason = "Protected-looking attribute: {}".format(item) raise AttributeError(error_reason) diff --git a/attmap/helpers.py b/attmap/helpers.py index e74a7e6..8822fba 100644 --- a/attmap/helpers.py +++ b/attmap/helpers.py @@ -1,8 +1,9 @@ """ Ancillary functions """ -from copy import deepcopy import logging import sys +from copy import deepcopy + if sys.version_info < (3, 3): from collections import Mapping else: @@ -15,7 +16,6 @@ def copy(obj): - def copy(self): """ Copy self to a new object. @@ -54,7 +54,8 @@ def render(lev, key, **kwargs): return space(lev) + ktext else: return space(lev) + "{} {}".format( - ktext, "null" if val is None else fun_val(val, space(lev))) + ktext, "null" if val is None else fun_val(val, space(lev)) + ) def go(kvs, curr_lev, acc): try: diff --git a/attmap/ordattmap.py b/attmap/ordattmap.py index 781160b..f13d929 100644 --- a/attmap/ordattmap.py +++ b/attmap/ordattmap.py @@ -1,7 +1,8 @@ """ Ordered attmap """ -from collections import OrderedDict import sys +from collections import OrderedDict + from .attmap import AttMap from .helpers import get_logger, safedel_message @@ -49,7 +50,8 @@ def __getitem__(self, item): def __setitem__(self, key, value, finalize=True): """ Support hook for value transformation before storage. """ super(OrdAttMap, self).__setitem__( - key, self._final_for_store(key, value) if finalize else value) + key, self._final_for_store(key, value) if finalize else value + ) def __delitem__(self, key): """ Make unmapped key deletion unexceptional. """ @@ -60,8 +62,7 @@ def __delitem__(self, key): def __eq__(self, other): """ Leverage base AttMap eq check, and check key order. """ - return AttMap.__eq__(self, other) and \ - list(self.keys()) == list(other.keys()) + return AttMap.__eq__(self, other) and list(self.keys()) == list(other.keys()) def __ne__(self, other): return not self == other @@ -84,8 +85,9 @@ def items(self): return [(k, self[k]) for k in self] def clear(self): - raise NotImplementedError("Clearance isn't implemented for {}". - format(self.__class__.__name__)) + raise NotImplementedError( + "Clearance isn't implemented for {}".format(self.__class__.__name__) + ) __marker = object() @@ -101,8 +103,9 @@ def pop(self, key, default=__marker): return default def popitem(self, last=True): - raise NotImplementedError("popitem isn't supported on a {}". - format(self.__class__.__name__)) + raise NotImplementedError( + "popitem isn't supported on a {}".format(self.__class__.__name__) + ) @staticmethod def _is_od_member(name): diff --git a/attmap/pathex_attmap.py b/attmap/pathex_attmap.py index c05acab..409e464 100644 --- a/attmap/pathex_attmap.py +++ b/attmap/pathex_attmap.py @@ -1,13 +1,16 @@ """ Canonical behavior for attmap in pepkit projects """ import sys -if sys.version_info < (3,4): + +if sys.version_info < (3, 4): from collections import Mapping else: from collections.abc import Mapping -from .ordattmap import OrdAttMap + from ubiquerg import expandpath +from .ordattmap import OrdAttMap + __author__ = "Vince Reuter" __email__ = "vreuter@virginia.edu" @@ -88,8 +91,10 @@ def _data_for_repr(self, expand=False): :return Iterable[(hashable, object)]: collection of key-value pairs to include in object's text representation """ - return filter(lambda kv: not self._excl_from_repr(kv[0], self.__class__), - self.items(expand)) + return filter( + lambda kv: not self._excl_from_repr(kv[0], self.__class__), + self.items(expand), + ) def to_map(self, expand=False): """ diff --git a/setup.py b/setup.py index 5158829..2d1ab2b 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,10 @@ #! /usr/bin/env python import os -from setuptools import setup import sys +from setuptools import setup + PACKAGE = "attmap" # Additional keyword arguments for setup(). @@ -12,8 +13,9 @@ def read_reqs(reqs_name): deps = [] - with open(os.path.join( - "requirements", "requirements-{}.txt".format(reqs_name)), 'r') as f: + with open( + os.path.join("requirements", "requirements-{}.txt".format(reqs_name)), "r" + ) as f: for l in f: if not l.strip(): continue @@ -24,19 +26,20 @@ def read_reqs(reqs_name): DEPENDENCIES = read_reqs("all") # 2to3 -if sys.version_info >= (3, ): +if sys.version_info >= (3,): extra["use_2to3"] = True extra["install_requires"] = DEPENDENCIES -with open("{}/_version.py".format(PACKAGE), 'r') as versionfile: +with open("{}/_version.py".format(PACKAGE), "r") as versionfile: version = versionfile.readline().split()[-1].strip("\"'\n") # Handle the pypi README formatting. try: import pypandoc - long_description = pypandoc.convert_file('README.md', 'rst') -except(IOError, ImportError, OSError): - long_description = open('README.md').read() + + long_description = pypandoc.convert_file("README.md", "rst") +except (IOError, ImportError, OSError): + long_description = open("README.md").read() setup( name=PACKAGE, @@ -44,7 +47,7 @@ def read_reqs(reqs_name): version=version, description="Multiple access patterns for key-value reference", long_description=long_description, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: BSD License", @@ -59,6 +62,8 @@ def read_reqs(reqs_name): include_package_data=True, test_suite="tests", tests_require=read_reqs("dev"), - setup_requires=(["pytest-runner"] if {"test", "pytest", "ptr"} & set(sys.argv) else []), + setup_requires=( + ["pytest-runner"] if {"test", "pytest", "ptr"} & set(sys.argv) else [] + ), **extra ) diff --git a/tests/conftest.py b/tests/conftest.py index 2e80ec0..7cf1edd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,22 @@ """ Fixtures and values shared among project's various test suites """ -from attmap import * import pytest +from attmap import * + __author__ = "Vince Reuter" __email__ = "vreuter@virginia.edu" -ALL_ATTMAPS = [AttributeDict, AttributeDictEcho, AttMap, AttMapEcho, - EchoAttMap, OrdAttMap, PathExAttMap] +ALL_ATTMAPS = [ + AttributeDict, + AttributeDictEcho, + AttMap, + AttMapEcho, + EchoAttMap, + OrdAttMap, + PathExAttMap, +] @pytest.fixture(scope="function", params=ALL_ATTMAPS) diff --git a/tests/helpers.py b/tests/helpers.py index 8e8fc68..0845364 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -2,9 +2,11 @@ import random import string -from attmap._att_map_like import AttMapLike + from hypothesis.strategies import * +from attmap._att_map_like import AttMapLike + __author__ = "Vince Reuter" __email__ = "vreuter@virginia.edu" @@ -13,8 +15,19 @@ # Note the arrangement by relative (increasing) complexity, as advised by the # hypothesis docs: https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.one_of ATOMIC_STRATEGIES = [ - booleans, binary, floats, integers, text, characters, uuids, - emails, timedeltas, times, dates, datetimes, complex_numbers + booleans, + binary, + floats, + integers, + text, + characters, + uuids, + emails, + timedeltas, + times, + dates, + datetimes, + complex_numbers, ] diff --git a/tests/regression/test_echo_subclass.py b/tests/regression/test_echo_subclass.py index e87d084..12614c6 100644 --- a/tests/regression/test_echo_subclass.py +++ b/tests/regression/test_echo_subclass.py @@ -1,6 +1,7 @@ """ Tests for subclassing EchoAttMap """ import pytest + from attmap import EchoAttMap __author__ = "Vince Reuter" @@ -9,6 +10,7 @@ class _SubEcho(EchoAttMap): """ Dummy class to derive from EchoAttMap """ + def __init__(self, entries=None): super(_SubEcho, self).__init__(entries) diff --git a/tests/test_AttMap.py b/tests/test_AttMap.py index 1a31f58..c1042d3 100644 --- a/tests/test_AttMap.py +++ b/tests/test_AttMap.py @@ -16,25 +16,19 @@ # Provide some basic atomic-type data for models tests. _BASE_KEYS = ("epigenomics", "H3K", "ac", "EWS", "FLI1") -_BASE_VALUES = \ - ("topic", "residue", "acetylation", "RNA binding protein", "FLI1") +_BASE_VALUES = ("topic", "residue", "acetylation", "RNA binding protein", "FLI1") _ENTRIES_PROVISION_MODES = ["gen", "dict", "zip", "list", "items"] _SEASON_HIERARCHY = { "spring": {"February": 28, "March": 31, "April": 30, "May": 31}, "summer": {"June": 30, "July": 31, "August": 31}, "fall": {"September": 30, "October": 31, "November": 30}, - "winter": {"December": 31, "January": 31} + "winter": {"December": 31, "January": 31}, } ADDITIONAL_NON_NESTED = {"West Complex": {"CPHG": 6}, "BIG": {"MR-4": 6}} -ADDITIONAL_NESTED = {"JPA": {"West Complex": {"CPHG": 6}}, - "Lane": {"BIG": {"MR-4": 6}}} -ADDITIONAL_VALUES_BY_NESTING = { - False: ADDITIONAL_NON_NESTED, - True: ADDITIONAL_NESTED -} -COMPARISON_FUNCTIONS = ["__eq__", "__ne__", "__len__", - "keys", "values", "items"] +ADDITIONAL_NESTED = {"JPA": {"West Complex": {"CPHG": 6}}, "Lane": {"BIG": {"MR-4": 6}}} +ADDITIONAL_VALUES_BY_NESTING = {False: ADDITIONAL_NON_NESTED, True: ADDITIONAL_NESTED} +COMPARISON_FUNCTIONS = ["__eq__", "__ne__", "__len__", "keys", "values", "items"] def pytest_generate_tests(metafunc): @@ -43,9 +37,10 @@ def pytest_generate_tests(metafunc): # Test case strives to validate expected behavior on empty container. collection_types = [tuple, list, set, dict] metafunc.parametrize( - "empty_collection", - argvalues=[ctype() for ctype in collection_types], - ids=[ctype.__name__ for ctype in collection_types]) + "empty_collection", + argvalues=[ctype() for ctype in collection_types], + ids=[ctype.__name__ for ctype in collection_types], + ) def basic_entries(): @@ -98,16 +93,18 @@ def test_empty_construction(self, empty_collection): assert m != dict() @pytest.mark.parametrize( - argnames="entries_gen,entries_provision_type", - argvalues=itertools.product([basic_entries, nested_entries], - _ENTRIES_PROVISION_MODES), - ids=["{entries}-{mode}".format(entries=gen.__name__, mode=mode) - for gen, mode in - itertools.product([basic_entries, nested_entries], - _ENTRIES_PROVISION_MODES)] + argnames="entries_gen,entries_provision_type", + argvalues=itertools.product( + [basic_entries, nested_entries], _ENTRIES_PROVISION_MODES + ), + ids=[ + "{entries}-{mode}".format(entries=gen.__name__, mode=mode) + for gen, mode in itertools.product( + [basic_entries, nested_entries], _ENTRIES_PROVISION_MODES + ) + ], ) - def test_construction_modes_supported( - self, entries_gen, entries_provision_type): + def test_construction_modes_supported(self, entries_gen, entries_provision_type): """ Construction wants key-value pairs; wrapping doesn't matter. """ entries_mapping = dict(entries_gen()) if entries_provision_type == "dict": @@ -122,8 +119,9 @@ def test_construction_modes_supported( elif entries_provision_type == "gen": entries = entries_gen else: - raise ValueError("Unexpected entries type: {}". - format(entries_provision_type)) + raise ValueError( + "Unexpected entries type: {}".format(entries_provision_type) + ) expected = AttMap(entries_mapping) observed = AttMap(entries) assert expected == observed @@ -132,11 +130,10 @@ def test_construction_modes_supported( def _validate_mapping_function_implementation(entries_gen, name_comp_func): data = dict(entries_gen()) attrdict = AttMap(data) - if __name__ == '__main__': + if __name__ == "__main__": if name_comp_func in ["__eq__", "__ne__"]: are_equal = getattr(attrdict, name_comp_func).__call__(data) - assert are_equal if name_comp_func == "__eq__" \ - else (not are_equal) + assert are_equal if name_comp_func == "__eq__" else (not are_equal) else: raw_dict_comp_func = getattr(data, name_comp_func) attrdict_comp_func = getattr(attrdict, name_comp_func) @@ -161,16 +158,14 @@ class AttMapUpdateTests: """ - _TOTALLY_ARBITRARY_VALUES = [ - "abc", 123, - (4, "text", ("nes", "ted")), list("-101") - ] + _TOTALLY_ARBITRARY_VALUES = ["abc", 123, (4, "text", ("nes", "ted")), list("-101")] _GETTERS = ["__getattr__", "__getitem__"] _SETTERS = ["__setattr__", "__setitem__"] @pytest.mark.parametrize( - argnames="setter_name,getter_name,is_novel", - argvalues=itertools.product(_SETTERS, _GETTERS, (False, True))) + argnames="setter_name,getter_name,is_novel", + argvalues=itertools.product(_SETTERS, _GETTERS, (False, True)), + ) def test_set_get_atomic(self, setter_name, getter_name, is_novel): """ For new and existing items, validate set/get behavior. """ @@ -204,16 +199,17 @@ def test_set_get_atomic(self, setter_name, getter_name, is_novel): class AttMapCollisionTests: - """ Tests for proper merging and type conversion of mappings. - AttMap converts a mapping being inserted as a value to an - AttMap. """ + """Tests for proper merging and type conversion of mappings. + AttMap converts a mapping being inserted as a value to an + AttMap.""" @pytest.mark.parametrize( - argnames="name_update_func", - argvalues=["add_entries", "__setattr__", "__setitem__"]) + argnames="name_update_func", + argvalues=["add_entries", "__setattr__", "__setitem__"], + ) def test_squash_existing(self, name_update_func): - """ When a value that's a mapping is assigned to existing key with - non-mapping value, the new value overwrites the old. """ + """When a value that's a mapping is assigned to existing key with + non-mapping value, the new value overwrites the old.""" ad = AttMap({"MR": 4}) assert 4 == ad.MR assert 4 == ad["MR"] @@ -229,11 +225,11 @@ def test_squash_existing(self, name_update_func): @pytest.mark.parametrize( - argnames="name_update_func", - argvalues=["add_entries", "__setattr__", "__setitem__"]) + argnames="name_update_func", argvalues=["add_entries", "__setattr__", "__setitem__"] +) @pytest.mark.parametrize( - argnames="name_fetch_func", - argvalues=["__getattr__", "__getitem__"]) + argnames="name_fetch_func", argvalues=["__getattr__", "__getitem__"] +) class AttMapNullTests: """ AttMap has configurable behavior regarding null values. """ @@ -252,8 +248,7 @@ def test_replace_null(self, name_update_func, name_fetch_func): assert getattr(ad, name_fetch_func)("lone_attr") is None setter = getattr(ad, name_update_func) non_null_value = AttMap({"was_null": "not_now"}) - self._do_update(name_update_func, setter, - ("lone_attr", non_null_value)) + self._do_update(name_update_func, setter, ("lone_attr", non_null_value)) assert non_null_value == getattr(ad, name_fetch_func)("lone_attr") @staticmethod @@ -281,8 +276,8 @@ def test_missing_getitem(self, missing): def test_numeric_key(self): """ Attribute request must be string. """ - ad = AttMap({1: 'a'}) - assert 'a' == ad[1] + ad = AttMap({1: "a"}) + assert "a" == ad[1] with pytest.raises(TypeError): getattr(ad, 1) @@ -290,19 +285,31 @@ def test_numeric_key(self): class AttMapSerializationTests: """ Tests for AttMap serialization. """ - DATA_PAIRS = [('a', 1), ('b', False), ('c', range(5)), - ('d', {'A': None, 'T': []}), - ('e', AttMap({'G': 1, 'C': [False, None]})), - ('f', [AttMap({"DNA": "deoxyribose", "RNA": "ribose"}), - AttMap({"DNA": "thymine", "RNA": "uracil"})])] + DATA_PAIRS = [ + ("a", 1), + ("b", False), + ("c", range(5)), + ("d", {"A": None, "T": []}), + ("e", AttMap({"G": 1, "C": [False, None]})), + ( + "f", + [ + AttMap({"DNA": "deoxyribose", "RNA": "ribose"}), + AttMap({"DNA": "thymine", "RNA": "uracil"}), + ], + ), + ] @pytest.mark.parametrize( - argnames="data", - argvalues=itertools.combinations(DATA_PAIRS, 2), - ids=lambda data: " data = {}".format(str(data))) + argnames="data", + argvalues=itertools.combinations(DATA_PAIRS, 2), + ids=lambda data: " data = {}".format(str(data)), + ) @pytest.mark.parametrize( - argnames="data_type", argvalues=[list, dict], - ids=lambda data_type: " data_type = {}".format(data_type)) + argnames="data_type", + argvalues=[list, dict], + ids=lambda data_type: " data_type = {}".format(data_type), + ) def test_pickle_restoration(self, tmpdir, data, data_type): """ Pickled and restored AttMap objects are identical. """ @@ -319,11 +326,11 @@ def test_pickle_restoration(self, tmpdir, data, data_type): # Serialize AttMap and write to disk. filepath = os.path.join(dirpath, filename) - with open(filepath, 'wb') as pkl: + with open(filepath, "wb") as pkl: pickle.dump(original_attrdict, pkl) # Validate equivalence between original and restored versions. - with open(filepath, 'rb') as pkl: + with open(filepath, "rb") as pkl: restored_attrdict = AttMap(pickle.load(pkl)) assert restored_attrdict == original_attrdict @@ -332,8 +339,13 @@ class AttMapObjectSyntaxAccessTests: """ Test behavior of dot attribute access / identity setting. """ DEFAULT_VALUE = "totally-arbitrary" - NORMAL_ITEM_ARG_VALUES = \ - ["__getattr__", "__getitem__", "__dict__", "__repr__", "__str__"] + NORMAL_ITEM_ARG_VALUES = [ + "__getattr__", + "__getitem__", + "__dict__", + "__repr__", + "__str__", + ] PICKLE_ITEM_ARG_VALUES = ["__getstate__", "__setstate__"] ATTR_DICT_DATA = {"a": 0, "b": range(1, 3), "c": {"CO": 70, "WA": 5}} UNMAPPED = ["arb-att-1", "random-attribute-2"] @@ -342,19 +354,24 @@ class AttMapObjectSyntaxAccessTests: def attrdict(self, request): """ Provide a test case with an AttMap. """ d = self.ATTR_DICT_DATA - return AttMapEcho(d) if request.getfixturevalue("return_identity") \ - else AttMap(d) + return ( + AttMapEcho(d) if request.getfixturevalue("return_identity") else AttMap(d) + ) @pytest.mark.parametrize( - argnames="return_identity", argvalues=[False, True], - ids=lambda ret_id: " identity setting={} ".format(ret_id)) + argnames="return_identity", + argvalues=[False, True], + ids=lambda ret_id: " identity setting={} ".format(ret_id), + ) @pytest.mark.parametrize( - argnames="attr_to_request", - argvalues=NORMAL_ITEM_ARG_VALUES + PICKLE_ITEM_ARG_VALUES + - UNMAPPED + list(ATTR_DICT_DATA.keys()), - ids=lambda attr: " requested={} ".format(attr)) - def test_attribute_access( - self, return_identity, attr_to_request, attrdict): + argnames="attr_to_request", + argvalues=NORMAL_ITEM_ARG_VALUES + + PICKLE_ITEM_ARG_VALUES + + UNMAPPED + + list(ATTR_DICT_DATA.keys()), + ids=lambda attr: " requested={} ".format(attr), + ) + def test_attribute_access(self, return_identity, attr_to_request, attrdict): """ Access behavior depends on request and behavior toggle. """ if attr_to_request == "__dict__": # The underlying mapping is still accessible. @@ -365,8 +382,11 @@ def test_attribute_access( elif attr_to_request in self.PICKLE_ITEM_ARG_VALUES: # We don't tinker with the pickle-relevant attributes. with pytest.raises(AttributeError): - print("Should have failed, but got result: {}". - format(getattr(attrdict, attr_to_request))) + print( + "Should have failed, but got result: {}".format( + getattr(attrdict, attr_to_request) + ) + ) elif attr_to_request in self.UNMAPPED: # Unmapped request behavior depends on parameterization. if return_identity: @@ -404,7 +424,8 @@ def test_is_null(self, item): @pytest.mark.parametrize( argnames=["k", "v"], - argvalues=list(zip(_KEYNAMES, ["sampleA", "WGBS", "random"]))) + argvalues=list(zip(_KEYNAMES, ["sampleA", "WGBS", "random"])), + ) def test_non_null(self, k, v): """ AD is sensitive to value updates """ ad = AttMap() @@ -420,8 +441,9 @@ class SampleYamlTests: """ AttMap metadata only appear in YAML if non-default. """ @staticmethod - def _yaml_data(sample, filepath, section_to_change=None, - attr_to_change=None, newval=None): + def _yaml_data( + sample, filepath, section_to_change=None, attr_to_change=None, newval=None + ): """ Serialize a Sample, possibly tweaking it first, write, and parse. @@ -436,15 +458,14 @@ def _yaml_data(sample, filepath, section_to_change=None, if section_to_change: getattr(sample, section_to_change)[attr_to_change] = newval sample.to_yaml(filepath) - with open(filepath, 'r') as f: + with open(filepath, "r") as f: data = yaml.safe_load(f) - with open(filepath, 'r') as f: + with open(filepath, "r") as f: lines = f.readlines() return lines, data -@pytest.mark.parametrize( - ["func", "exp"], [(repr, "AttMap: {}"), (str, "AttMap: {}")]) +@pytest.mark.parametrize(["func", "exp"], [(repr, "AttMap: {}"), (str, "AttMap: {}")]) def test_text_repr_empty(func, exp): """ Empty AttMap is correctly represented as text. """ assert exp == func(AttMap()) diff --git a/tests/test_basic_ops_dynamic.py b/tests/test_basic_ops_dynamic.py index 544d10c..45fad4f 100644 --- a/tests/test_basic_ops_dynamic.py +++ b/tests/test_basic_ops_dynamic.py @@ -1,16 +1,16 @@ """ Test basic Mapping operations' responsiveness to underlying data change. """ -from hypothesis import given import pytest -from .helpers import get_att_map, random_str_key, rand_non_null +from hypothesis import given from pandas import Series +from .helpers import get_att_map, rand_non_null, random_str_key + __author__ = "Vince Reuter" __email__ = "vreuter@virginia.edu" -@pytest.fixture( - scope="function", params=[{}, {"a": 1}, {"b": [1, 2, 3], "c": (1, 2)}]) +@pytest.fixture(scope="function", params=[{}, {"a": 1}, {"b": [1, 2, 3], "c": (1, 2)}]) def entries(request): """ Data to store as entries in an attmap. """ return request.param @@ -38,6 +38,7 @@ def test_length_increase(attmap_type, entries): def test_positive_membership(attmap_type, entries): """ Each key is a member; a nonmember should be flagged as such """ import random + m = get_att_map(attmap_type) assert not any(k in m for k in entries) for k in entries: @@ -57,7 +58,8 @@ def test_negative_membership(attmap_type, entries): @pytest.mark.parametrize( - "series", [Series(data) for data in [("a", 1), [("b", 2), ("c", 3)], []]]) + "series", [Series(data) for data in [("a", 1), [("b", 2), ("c", 3)], []]] +) def test_add_pandas_series(series, attmap_type): """ A pandas Series can be used as a simple container of entries to add. """ m = get_att_map(attmap_type) @@ -81,8 +83,10 @@ def test_del_unmapped_key(attmap_type, seed_data, delkey): @pytest.mark.xfail(reason="attmap text representations have changed.") -@pytest.mark.parametrize("f_extra_checks_pair", - [(repr, []), (str, [lambda s, dt: s.startswith(dt.__name__)])]) +@pytest.mark.parametrize( + "f_extra_checks_pair", + [(repr, []), (str, [lambda s, dt: s.startswith(dt.__name__)])], +) def test_text(attmap_type, entries, f_extra_checks_pair): """ Formal text representation of an attmap responds to data change. """ @@ -103,13 +107,20 @@ def _examine_results(ls, items): def find_line(key, val): matches = [l for l in ls if l.startswith(k) and "{}".format(v) in l] if len(matches) == 0: - raise Exception("No matches for key={}, val={} among lines:\n{}". - format(key, val, "\n".join(lines))) + raise Exception( + "No matches for key={}, val={} among lines:\n{}".format( + key, val, "\n".join(lines) + ) + ) elif len(matches) == 1: return matches[0] else: - raise Exception("Non-unique ({}) matched lines:\n{}". - format(len(matches), "\n".join(matches))) + raise Exception( + "Non-unique ({}) matched lines:\n{}".format( + len(matches), "\n".join(matches) + ) + ) + matched, missed = [], [] for k, v in items.items(): try: @@ -142,11 +153,16 @@ def find_line(key, val): class CheckNullTests: """ Test accuracy of the null value test methods. """ - DATA = [(("truly_null", None), True)] + \ - [(kv, False) for kv in [ - ("empty_list", []), ("empty_text", ""), ("empty_int", 0), - ("empty_float", 0), ("empty_map", {}) - ]] + DATA = [(("truly_null", None), True)] + [ + (kv, False) + for kv in [ + ("empty_list", []), + ("empty_text", ""), + ("empty_int", 0), + ("empty_float", 0), + ("empty_map", {}), + ] + ] @pytest.fixture(scope="function") def entries(self): @@ -170,7 +186,6 @@ def test_null_to_non_null(m, v): m[k] = v assert not m.is_null(k) and m.non_null(k) - @pytest.mark.skip(reason="test appears broken") @staticmethod @given(v=rand_non_null()) diff --git a/tests/test_basic_ops_static.py b/tests/test_basic_ops_static.py index e782092..fd250f5 100644 --- a/tests/test_basic_ops_static.py +++ b/tests/test_basic_ops_static.py @@ -1,19 +1,26 @@ """ Tests absent the mutable operations, of basic Mapping operations """ import pytest + from .helpers import get_att_map __author__ = "Vince Reuter" __email__ = "vreuter@virginia.edu" -ENTRY_DATA = [(), ("a", 1), ("b", [1, "2", 3]), - ("c", {1: 2}), ("d", {"e": 0, "F": "G"})] +ENTRY_DATA = [ + (), + ("a", 1), + ("b", [1, "2", 3]), + ("c", {1: 2}), + ("d", {"e": 0, "F": "G"}), +] @pytest.fixture( scope="function", - params=[{}, {"a": 1}, {"b": [1, "2", 3]}, {"c": {1: 2}}, {"d": {"F": "G"}}]) + params=[{}, {"a": 1}, {"b": [1, "2", 3]}, {"c": {1: 2}}, {"d": {"F": "G"}}], +) def entries(request): """ Data to store as entries in an attmap. """ return request.param @@ -57,10 +64,19 @@ def test_str(attmap_type, entries): class CheckNullTests: """ Test accuracy of the null value test methods. """ - DATA = [(("truly_null", None), True)] + [(kv, False) for kv in [ - ("empty_list", []), ("empty_text", ""), ("empty_map", {}), - ("empty_int", 0), ("empty_float", 0), ("bad_num", float("nan")), - ("pos_inf", float("inf")), ("neg_inf", float("-inf"))]] + DATA = [(("truly_null", None), True)] + [ + (kv, False) + for kv in [ + ("empty_list", []), + ("empty_text", ""), + ("empty_map", {}), + ("empty_int", 0), + ("empty_float", 0), + ("bad_num", float("nan")), + ("pos_inf", float("inf")), + ("neg_inf", float("-inf")), + ] + ] @pytest.fixture(scope="function") def entries(self): diff --git a/tests/test_echo.py b/tests/test_echo.py index 2bb7d5e..d86943e 100644 --- a/tests/test_echo.py +++ b/tests/test_echo.py @@ -1,9 +1,10 @@ """ Tests for the echo behavior """ import pytest -from attmap import AttMap, AttMapEcho from veracitools import ExpectContext +from attmap import AttMap, AttMapEcho + __author__ = "Vince Reuter" __email__ = "vreuter@virginia.edu" @@ -21,12 +22,14 @@ def exec_get_item(m, k): @pytest.mark.parametrize( ["maptype", "getter", "key", "expected"], - [(AttMap, getattr, "missing_key", AttributeError), - (AttMap, exec_get_item, "random", KeyError), - (AttMapEcho, exec_get_item, "arbkey", KeyError), - (AttMapEcho, getattr, "missing_key", "missing_key")]) -def test_echo_is_type_dependent_and_access_dependent( - maptype, getter, key, expected): + [ + (AttMap, getattr, "missing_key", AttributeError), + (AttMap, exec_get_item, "random", KeyError), + (AttMapEcho, exec_get_item, "arbkey", KeyError), + (AttMapEcho, getattr, "missing_key", "missing_key"), + ], +) +def test_echo_is_type_dependent_and_access_dependent(maptype, getter, key, expected): """ Retrieval of missing key/attr echoes it iff the type and access mode permit. """ m = maptype() assert key not in m @@ -35,10 +38,10 @@ def test_echo_is_type_dependent_and_access_dependent( @pytest.mark.parametrize( - ["data", "name", "defval"], - [({}, "b", "arbval"), ({"a": 1}, "c", "random")]) + ["data", "name", "defval"], [({}, "b", "arbval"), ({"a": 1}, "c", "random")] +) def test_echo_respects_default(data, name, defval): - assert name != defval # Pretest so that assertions actually have meaning. + assert name != defval # Pretest so that assertions actually have meaning. m = AttMapEcho(data) assert name == getattr(m, name) assert name == getattr(m, name, defval) diff --git a/tests/test_equality.py b/tests/test_equality.py index 358a75d..adf5da5 100644 --- a/tests/test_equality.py +++ b/tests/test_equality.py @@ -1,10 +1,14 @@ """ Tests for attmap equality comparison """ import copy + import numpy as np -from pandas import DataFrame as DF, Series import pytest -from attmap import AttMap, OrdAttMap, PathExAttMap, AttMapEcho +from pandas import DataFrame as DF +from pandas import Series + +from attmap import AttMap, AttMapEcho, OrdAttMap, PathExAttMap + from .conftest import ALL_ATTMAPS from .helpers import get_att_map @@ -19,10 +23,10 @@ def basic_data(): @pytest.mark.parametrize("attmap_type", ALL_ATTMAPS) -@pytest.mark.parametrize(["s1_data", "s2_data"], [ - ({"c": 3}, {"d": 4}), ({}, {"c": 3}), ({"d": 4}, {})]) -def test_series_labels_mismatch_is_not_equal( - basic_data, s1_data, s2_data, attmap_type): +@pytest.mark.parametrize( + ["s1_data", "s2_data"], [({"c": 3}, {"d": 4}), ({}, {"c": 3}), ({"d": 4}, {})] +) +def test_series_labels_mismatch_is_not_equal(basic_data, s1_data, s2_data, attmap_type): """ Maps with differently-labeled Series as values cannot be equal. """ d1 = copy.copy(basic_data) d1.update(s1_data) @@ -36,14 +40,17 @@ def test_series_labels_mismatch_is_not_equal( @pytest.mark.parametrize("attmap_type", ALL_ATTMAPS) -@pytest.mark.parametrize(["obj1", "obj2", "expected"], [ - (np.array([1, 2, 3]), np.array([1, 2, 4]), False), - (np.array(["a", "b", "c"]), np.array(["a", "b", "c"]), True), - (Series({"x": 0, "y": 0}), Series({"x": 0, "y": 1}), False), - (Series({"x": 0, "y": 0}), Series({"x": 0, "y": 0}), True), - (DF({"a": [1, 0], "b": [0, 1]}), DF({"a": [0, 0], "b": [0, 0]}), False), - (DF({"a": [1, 0], "b": [0, 1]}), DF({"a": [1, 0], "b": [0, 1]}), True) -]) +@pytest.mark.parametrize( + ["obj1", "obj2", "expected"], + [ + (np.array([1, 2, 3]), np.array([1, 2, 4]), False), + (np.array(["a", "b", "c"]), np.array(["a", "b", "c"]), True), + (Series({"x": 0, "y": 0}), Series({"x": 0, "y": 1}), False), + (Series({"x": 0, "y": 0}), Series({"x": 0, "y": 0}), True), + (DF({"a": [1, 0], "b": [0, 1]}), DF({"a": [0, 0], "b": [0, 0]}), False), + (DF({"a": [1, 0], "b": [0, 1]}), DF({"a": [1, 0], "b": [0, 1]}), True), + ], +) def test_eq_with_scistack_value_types(attmap_type, obj1, obj2, expected): """ Map comparison properly handles array-likes from scientific stack. """ key = "obj" @@ -54,13 +61,21 @@ def test_eq_with_scistack_value_types(attmap_type, obj1, obj2, expected): @pytest.mark.parametrize("data", [{}, {"a": 1}]) -@pytest.mark.parametrize(["this_type", "that_type", "exp"], [ - (AttMap, AttMap, True), - (AttMap, AttMapEcho, False), (AttMap, OrdAttMap, False), (AttMap, PathExAttMap, False), - (OrdAttMap, OrdAttMap, True), - (OrdAttMap, PathExAttMap, False), (OrdAttMap, AttMapEcho, False), - (PathExAttMap, PathExAttMap, True), (PathExAttMap, AttMapEcho, False), - (AttMapEcho, AttMapEcho, True)]) +@pytest.mark.parametrize( + ["this_type", "that_type", "exp"], + [ + (AttMap, AttMap, True), + (AttMap, AttMapEcho, False), + (AttMap, OrdAttMap, False), + (AttMap, PathExAttMap, False), + (OrdAttMap, OrdAttMap, True), + (OrdAttMap, PathExAttMap, False), + (OrdAttMap, AttMapEcho, False), + (PathExAttMap, PathExAttMap, True), + (PathExAttMap, AttMapEcho, False), + (AttMapEcho, AttMapEcho, True), + ], +) def test_equality_is_strict_in_type(data, this_type, that_type, exp): """ Attmap equality requires exact type match. """ m1 = get_att_map(this_type, data) @@ -74,8 +89,10 @@ def test_equality_is_strict_in_type(data, this_type, that_type, exp): @pytest.mark.parametrize("maptype", ALL_ATTMAPS) -@pytest.mark.parametrize(["data", "delkey"], [ - ({"a": 1}, "a"), ({"b": 2, "c": 3}, "b"), ({"b": 2, "c": 3}, "c")]) +@pytest.mark.parametrize( + ["data", "delkey"], + [({"a": 1}, "a"), ({"b": 2, "c": 3}, "b"), ({"b": 2, "c": 3}, "c")], +) def test_equality_fails_when_keysets_are_non_identical(maptype, data, delkey): """ Map comparison fails--unexceptionally--when operands differ in keys. """ m1 = get_att_map(maptype, data) diff --git a/tests/test_ordattmap.py b/tests/test_ordattmap.py index 36d2bd3..89cca22 100644 --- a/tests/test_ordattmap.py +++ b/tests/test_ordattmap.py @@ -1,11 +1,13 @@ """ Tests for ordered AttMap behavior """ +import sys from collections import OrderedDict from itertools import combinations -import sys + import pytest from hypothesis import given from hypothesis.strategies import * + from attmap import AttMap, AttMapEcho, OrdAttMap, PathExAttMap __author__ = "Vince Reuter" @@ -14,8 +16,13 @@ def pytest_generate_tests(metafunc): """ Test case generation and parameterization for this module """ - hwy_dat = [("Big Sur", 1), ("Jasper", 93), ("Sneffels", 62), - ("Robson", 16), ("Garibaldi", 99)] + hwy_dat = [ + ("Big Sur", 1), + ("Jasper", 93), + ("Sneffels", 62), + ("Robson", 16), + ("Garibaldi", 99), + ] keyhook, dathook = "hwy_dat_key", "raw_hwy_dat" if dathook in metafunc.fixturenames: metafunc.parametrize(dathook, [hwy_dat]) @@ -32,21 +39,36 @@ def kv_lists_strategy(pool=(integers, text, characters, uuids), **kwargs): """ kwds = {"min_size": 1, "unique_by": lambda kv: kv[0]} kwds.update(kwargs) - return one_of(*[lists(tuples(ks(), vs()), **kwds) - for ks, vs in combinations(pool, 2)]) + return one_of( + *[lists(tuples(ks(), vs()), **kwds) for ks, vs in combinations(pool, 2)] + ) -@pytest.mark.parametrize(["cls", "exp"], [ - (OrdAttMap, True), (OrderedDict, True), (AttMap, True), - (PathExAttMap, False), (AttMapEcho, False)]) +@pytest.mark.parametrize( + ["cls", "exp"], + [ + (OrdAttMap, True), + (OrderedDict, True), + (AttMap, True), + (PathExAttMap, False), + (AttMapEcho, False), + ], +) def test_subclassing(cls, exp): """ Verify that OrdAttMap type has expected type memberships. """ assert exp is issubclass(OrdAttMap, cls) -@pytest.mark.parametrize(["cls", "exp"], [ - (OrdAttMap, True), (OrderedDict, True), (AttMap, True), - (PathExAttMap, False), (AttMapEcho, False)]) +@pytest.mark.parametrize( + ["cls", "exp"], + [ + (OrdAttMap, True), + (OrderedDict, True), + (AttMap, True), + (PathExAttMap, False), + (AttMapEcho, False), + ], +) def test_type_membership(cls, exp): """ Verify that an OrdAttMap instance passes type checks as expected. """ assert exp is isinstance(OrdAttMap(), cls) @@ -93,17 +115,21 @@ def test_ordattmap_access(kvs, access): @given(kvs=kv_lists_strategy()) @pytest.mark.parametrize( - ["other_type", "expected"], - [(dict, False), (OrderedDict, False), (OrdAttMap, True)]) + ["other_type", "expected"], [(dict, False), (OrderedDict, False), (OrdAttMap, True)] +) def test_ordattmap_eq(kvs, other_type, expected): """ Verify equality comparison behavior. """ - obs = (OrdAttMap(kvs) == other_type(kvs)) + obs = OrdAttMap(kvs) == other_type(kvs) assert obs == expected -@pytest.mark.parametrize(["alter", "check"], [ - (lambda m, k: m.__delitem__(k), lambda _1, _2: True), - (lambda m, k: m.pop(k), lambda x, o: x == o)]) +@pytest.mark.parametrize( + ["alter", "check"], + [ + (lambda m, k: m.__delitem__(k), lambda _1, _2: True), + (lambda m, k: m.pop(k), lambda x, o: x == o), + ], +) def test_ordattmap_deletion(hwy_dat_key, raw_hwy_dat, alter, check): """ Validate key deletion behavior of OrdAttMap. """ m = OrdAttMap(raw_hwy_dat) @@ -118,16 +144,20 @@ def test_ordattmap_deletion(hwy_dat_key, raw_hwy_dat, alter, check): @pytest.mark.parametrize("base_type", [OrdAttMap, PathExAttMap]) @pytest.mark.parametrize( ["that_type", "final_exp"], - [(dict, sys.version_info >= (3, 6)), (OrderedDict, True), (OrdAttMap, True)]) + [(dict, sys.version_info >= (3, 6)), (OrderedDict, True), (OrdAttMap, True)], +) def test_ordattmap_overrides_eq_exclusion( - hwy_dat_key, raw_hwy_dat, base_type, that_type, final_exp): + hwy_dat_key, raw_hwy_dat, base_type, that_type, final_exp +): """ Verify ability to exclude key from comparisons. """ + class OrdSub(base_type): def _excl_from_eq(self, k): print("Considering for exclusion: {}".format(k)) res = super(OrdSub, self)._excl_from_eq(k) or k == hwy_dat_key print("Exclude: {}".format(res)) return res + msub = OrdSub(raw_hwy_dat) assert isinstance(msub, OrdAttMap) that = that_type(raw_hwy_dat) @@ -156,17 +186,26 @@ def _excl_from_repr(self, k, cls): this_keys = set(msub.keys()) that_keys = set(that.keys()) assert this_keys == that_keys - mismatches = [(k, msub[k], that[k]) for k in this_keys | that_keys - if msub[k] != that[k]] + mismatches = [ + (k, msub[k], that[k]) for k in this_keys | that_keys if msub[k] != that[k] + ] assert [] == mismatches assert hwy_dat_key in repr(that) assert hwy_dat_key not in repr(msub) -@pytest.mark.parametrize(["that_type", "exp"], [ - (OrdAttMap, True), (AttMapEcho, False), (AttMap, False), - (PathExAttMap, False), (OrderedDict, False), (dict, False)]) +@pytest.mark.parametrize( + ["that_type", "exp"], + [ + (OrdAttMap, True), + (AttMapEcho, False), + (AttMap, False), + (PathExAttMap, False), + (OrderedDict, False), + (dict, False), + ], +) def test_ordattmap_repr(raw_hwy_dat, that_type, exp): """ Test __repr__ of OrdAttMap. """ assert exp is (repr(OrdAttMap(raw_hwy_dat)) == repr(that_type(raw_hwy_dat))) @@ -175,30 +214,41 @@ def test_ordattmap_repr(raw_hwy_dat, that_type, exp): class BasicDataTests: """ Tests for some OrdAttMap behaviors on some very basic data. """ - BASIC_DATA = [("a", OrderedDict([("c", 3), ("b", 2)])), ("d", 4), - ("e", OrderedDict([("f", 6)]))] - + BASIC_DATA = [ + ("a", OrderedDict([("c", 3), ("b", 2)])), + ("d", 4), + ("e", OrderedDict([("f", 6)])), + ] + @pytest.fixture(params=[OrdAttMap, PathExAttMap]) def oam(self, request): """ Provide test case with a simple ordered attmap instance. """ return request.param(self.BASIC_DATA) - - @pytest.mark.parametrize(["get_value", "expected"], [ - (lambda m: type(m), OrderedDict), - (lambda m: m["a"], OrderedDict([("c", 3), ("b", 2)])), - (lambda m: m["d"], 4), - (lambda m: m["e"], OrderedDict([("f", 6)])) - ]) + + @pytest.mark.parametrize( + ["get_value", "expected"], + [ + (lambda m: type(m), OrderedDict), + (lambda m: m["a"], OrderedDict([("c", 3), ("b", 2)])), + (lambda m: m["d"], 4), + (lambda m: m["e"], OrderedDict([("f", 6)])), + ], + ) def test_ordattmap_simplification_to_map(self, oam, get_value, expected): """ Test the nested type simplification behavior for ordered attmap. """ assert expected == get_value(oam.to_map()) - @pytest.mark.parametrize(["lineno", "expected"], [ - (1, "a:"), - (2, " c: 3"), (3, " b: 2"), - (4, "d: 4"), (5, "e:"), - (6, " f: 6") - ]) + @pytest.mark.parametrize( + ["lineno", "expected"], + [ + (1, "a:"), + (2, " c: 3"), + (3, " b: 2"), + (4, "d: 4"), + (5, "e:"), + (6, " f: 6"), + ], + ) def test_ordattmap_repr(self, oam, lineno, expected): """ Test the ordering and indentation of ordered attmap repr. """ obstext = repr(oam) diff --git a/tests/test_packaging.py b/tests/test_packaging.py index c2ff17c..ed9b780 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -1,10 +1,12 @@ """ Validate what's available directly on the top-level import. """ +import itertools from abc import ABCMeta from collections import OrderedDict from inspect import isclass, isfunction -import itertools + import pytest + import attmap from attmap import * @@ -26,15 +28,23 @@ def get_base_check(*bases): ECHO_TEST_FUNS = [isclass, get_base_check(PathExAttMap)] -@pytest.mark.parametrize(["obj_name", "typecheck"], itertools.chain(*[ - [("AttMapLike", f) for f in [isclass, lambda obj: obj.__metaclass__ == ABCMeta]], - [("AttMap", f) for f in [isclass, get_base_check(AttMapLike)]], - [("OrdAttMap", f) for f in [isclass, get_base_check(OrderedDict, AttMap)]], - [("PathExAttMap", f) for f in [isclass, get_base_check(OrdAttMap)]], - [("AttMapEcho", f) for f in ECHO_TEST_FUNS], - [("EchoAttMap", f) for f in ECHO_TEST_FUNS], - [("get_data_lines", isfunction)] -])) +@pytest.mark.parametrize( + ["obj_name", "typecheck"], + itertools.chain( + *[ + [ + ("AttMapLike", f) + for f in [isclass, lambda obj: obj.__metaclass__ == ABCMeta] + ], + [("AttMap", f) for f in [isclass, get_base_check(AttMapLike)]], + [("OrdAttMap", f) for f in [isclass, get_base_check(OrderedDict, AttMap)]], + [("PathExAttMap", f) for f in [isclass, get_base_check(OrdAttMap)]], + [("AttMapEcho", f) for f in ECHO_TEST_FUNS], + [("EchoAttMap", f) for f in ECHO_TEST_FUNS], + [("get_data_lines", isfunction)], + ] + ), +) def test_top_level_exports(obj_name, typecheck): """ At package level, validate object availability and type. """ mod = attmap diff --git a/tests/test_path_expansion.py b/tests/test_path_expansion.py index 26136f3..363b52d 100644 --- a/tests/test_path_expansion.py +++ b/tests/test_path_expansion.py @@ -5,10 +5,11 @@ import os import random import string + import pytest -from attmap import * -from ubiquerg import expandpath, TmpEnv +from ubiquerg import TmpEnv, expandpath +from attmap import * __author__ = "Vince Reuter" __email__ = "vreuter@virginia.edu" @@ -34,9 +35,12 @@ def get_path_env_pair(perm): joined and env-var-adjusted ($ sign added) path, and the second is a mapping from env var name to value """ + def get_random_var(): - return "".join(random.choice(string.ascii_uppercase) - for _ in range(random.randint(3, 10))) + return "".join( + random.choice(string.ascii_uppercase) for _ in range(random.randint(3, 10)) + ) + parts, subs = [], {} for p in perm: if p.upper() == p: @@ -46,9 +50,15 @@ def get_random_var(): return os.path.join(*parts), subs -@pytest.mark.parametrize(["path", "env"], - [get_path_env_pair(p) for p in itertools.chain(*[ - itertools.permutations(_RVS, k) for k in range(1, len(_RVS))])]) +@pytest.mark.parametrize( + ["path", "env"], + [ + get_path_env_pair(p) + for p in itertools.chain( + *[itertools.permutations(_RVS, k) for k in range(1, len(_RVS))] + ) + ], +) @pytest.mark.parametrize("fetch", [getattr, lambda m, k: m[k]]) def test_PathExAttMap_expands_available_variables(pam, path, env, fetch): """ Insertion of text encoding environment variables should expand. """ @@ -69,21 +79,36 @@ def build_selective_substitution_space(): and fourth is binding between temp env var and value expected to replace it """ + def part(vs): return set(_RVS) - vs, vs - partitions = [part(set(c)) for c in itertools.chain(*[ - itertools.combinations(_ENV_VAR_NAMES, k) - for k in range(1, len(_ENV_VAR_NAMES))])] - get_sub = lambda: "".join( - random.choice(string.ascii_lowercase) for _ in range(20)) - paths = [os.path.join(*perm) for perm in itertools.permutations( - ["$" + v if v.upper() == v else v for v in _RVS], len(_RVS))] - return [(path, pres, repl, {ev: get_sub() for ev in repl}) - for path in paths for pres, repl in partitions] + + partitions = [ + part(set(c)) + for c in itertools.chain( + *[ + itertools.combinations(_ENV_VAR_NAMES, k) + for k in range(1, len(_ENV_VAR_NAMES)) + ] + ) + ] + get_sub = lambda: "".join(random.choice(string.ascii_lowercase) for _ in range(20)) + paths = [ + os.path.join(*perm) + for perm in itertools.permutations( + ["$" + v if v.upper() == v else v for v in _RVS], len(_RVS) + ) + ] + return [ + (path, pres, repl, {ev: get_sub() for ev in repl}) + for path in paths + for pres, repl in partitions + ] @pytest.mark.parametrize( - ["path", "pres", "repl", "env"], build_selective_substitution_space()) + ["path", "pres", "repl", "env"], build_selective_substitution_space() +) @pytest.mark.parametrize("fetch", [getattr, lambda m, k: m[k]]) def test_PathExAttMap_substitution_is_selective(path, pres, repl, env, pam, fetch): """ Values that are environment variables are replaced; others aren't. """ @@ -97,12 +122,21 @@ def test_PathExAttMap_substitution_is_selective(path, pres, repl, env, pam, fetc assert all(map(lambda s: s not in res, repl)) -@pytest.mark.parametrize("path", itertools.chain(*[ - itertools.permutations(["$" + p for p in _ENV_VAR_NAMES] + _ARB_VAR_NAMES, k) - for k in range(1, len(_ENV_VAR_NAMES) + len(_ARB_VAR_NAMES) + 1)])) +@pytest.mark.parametrize( + "path", + itertools.chain( + *[ + itertools.permutations( + ["$" + p for p in _ENV_VAR_NAMES] + _ARB_VAR_NAMES, k + ) + for k in range(1, len(_ENV_VAR_NAMES) + len(_ARB_VAR_NAMES) + 1) + ] + ), +) @pytest.mark.parametrize("fetch", [getattr, lambda m, k: m[k]]) -@pytest.mark.parametrize("env", - [{ev: "".join(string.ascii_lowercase for _ in range(20)) for ev in _RVS}]) +@pytest.mark.parametrize( + "env", [{ev: "".join(string.ascii_lowercase for _ in range(20)) for ev in _RVS}] +) def test_non_PathExAttMap_preserves_all_variables(path, fetch, env): """ Only a PathExAttMap eagerly attempts expansion of text as a path. """ m = AttMap() @@ -112,9 +146,16 @@ def test_non_PathExAttMap_preserves_all_variables(path, fetch, env): assert path == fetch(m, k) -@pytest.mark.parametrize(["path", "expected"], [ - ("http://localhost", "http://localhost"), - ("http://lh/$HOME/page.html", "http://lh/{}/page.html".format(os.environ["HOME"]))]) +@pytest.mark.parametrize( + ["path", "expected"], + [ + ("http://localhost", "http://localhost"), + ( + "http://lh/$HOME/page.html", + "http://lh/{}/page.html".format(os.environ["HOME"]), + ), + ], +) @pytest.mark.parametrize("fetch", [lambda m, k: m[k], lambda m, k: getattr(m, k)]) def test_url_expansion(path, expected, fetch): """ URL expansion considers env vars but doesn't ruin slashes. """ @@ -124,13 +165,23 @@ def test_url_expansion(path, expected, fetch): @pytest.mark.parametrize( - "varname", ["THIS_SHOULD_NOT_BE_SET", "REALLY_IMPROBABLE_ENV_VAR"]) -@pytest.mark.parametrize(["var_idx", "path_parts"], itertools.chain(*[ - [(i, list(p)) for p in itertools.permutations(c) for i in range(k + 1)] - for k in range(0, 4) for c in itertools.combinations(["a", "b", "c"], k)])) + "varname", ["THIS_SHOULD_NOT_BE_SET", "REALLY_IMPROBABLE_ENV_VAR"] +) +@pytest.mark.parametrize( + ["var_idx", "path_parts"], + itertools.chain( + *[ + [(i, list(p)) for p in itertools.permutations(c) for i in range(k + 1)] + for k in range(0, 4) + for c in itertools.combinations(["a", "b", "c"], k) + ] + ), +) @pytest.mark.parametrize("store", [setattr, lambda m, k, v: m.__setitem__(k, v)]) @pytest.mark.parametrize("fetch", [getattr, lambda m, k: m[k], lambda m, k: m.get(k)]) -def test_multiple_syntax_path_expansion(varname, path_parts, var_idx, tmpdir, store, fetch): +def test_multiple_syntax_path_expansion( + varname, path_parts, var_idx, tmpdir, store, fetch +): """ Test the different combinations of setting and retrieving an env var path. """ key = "arbitrary" parts = copy.copy(path_parts) diff --git a/tests/test_special_mutability.py b/tests/test_special_mutability.py index 455ef37..0d53f48 100644 --- a/tests/test_special_mutability.py +++ b/tests/test_special_mutability.py @@ -1,7 +1,9 @@ """ Tests for mutability of AttributeDict """ -from attmap import AttributeDict, AttributeDictEcho import pytest + +from attmap import AttributeDict, AttributeDictEcho + from .helpers import get_att_map __author__ = "Vince Reuter" @@ -33,7 +35,7 @@ def test_null_can_always_be_set_if_key_is_absent(self, arb_key, m): m[arb_key] = None assert arb_key in m assert m[arb_key] is None - + def test_empty_mapping_can_replace_nonempty(self, m, arb_key): """ Regardless of specific type, an empty map can replace nonempty. """ assert arb_key not in m diff --git a/tests/test_to_dict.py b/tests/test_to_dict.py index 2a18010..8f6af93 100644 --- a/tests/test_to_dict.py +++ b/tests/test_to_dict.py @@ -1,18 +1,26 @@ """ Tests for conversion to base/builtin dict type """ -import pytest -from tests.helpers import get_att_map import numpy as np +import pytest from pandas import Series +from tests.helpers import get_att_map __author__ = "Vince Reuter" __email__ = "vreuter@virginia.edu" -@pytest.mark.parametrize("entries", [ - {}, {"a": 1}, {"b": {"c": 3}}, {"A": [1, 2]}, - {"B": 1, "C": np.arange(3)}, {"E": Series(["a", "b"])}]) +@pytest.mark.parametrize( + "entries", + [ + {}, + {"a": 1}, + {"b": {"c": 3}}, + {"A": [1, 2]}, + {"B": 1, "C": np.arange(3)}, + {"E": Series(["a", "b"])}, + ], +) def test_to_dict_type(attmap_type, entries): """ Validate to_dict result is a base dict and that contents match. """ m = get_att_map(attmap_type, entries) diff --git a/tests/test_to_disk.py b/tests/test_to_disk.py index 9b459c7..a6cf365 100644 --- a/tests/test_to_disk.py +++ b/tests/test_to_disk.py @@ -1,16 +1,19 @@ """ Tests for YAML representation of instances. """ -from collections import namedtuple import json import os import sys +from collections import namedtuple + if sys.version_info < (3, 3): from collections import MutableMapping else: from collections.abc import MutableMapping + +import pytest import yaml + from attmap import * -import pytest from tests.conftest import ALL_ATTMAPS __author__ = "Vince Reuter" @@ -22,10 +25,10 @@ YAML_NAME = yaml.__name__ JSON_NAME = json.__name__ FORMATTER_LIBRARIES = { - YAML_NAME: FmtLib(lambda f: yaml.load(f, yaml.SafeLoader), - lambda m, f: f.write(m.to_yaml())), - JSON_NAME: FmtLib(lambda f: json.load(f), - lambda m, f: json.dump(m.to_map(), f)) + YAML_NAME: FmtLib( + lambda f: yaml.load(f, yaml.SafeLoader), lambda m, f: f.write(m.to_yaml()) + ), + JSON_NAME: FmtLib(lambda f: json.load(f), lambda m, f: json.dump(m.to_map(), f)), } @@ -53,7 +56,8 @@ def check_lines(m, explines, obs_fun, parse, check): @pytest.mark.parametrize( - ["funcname", "exp"], [("get_yaml_lines", ["{}"]), ("to_yaml", "{}\n")]) + ["funcname", "exp"], [("get_yaml_lines", ["{}"]), ("to_yaml", "{}\n")] +) def test_empty(funcname, exp, maptype): """ Verify behavior for YAML of empty attmap. """ assert exp == getattr(maptype({}), funcname)() @@ -61,7 +65,8 @@ def test_empty(funcname, exp, maptype): @pytest.mark.parametrize( ["get_obs", "parse_obs"], - [("get_yaml_lines", lambda ls: ls), ("to_yaml", lambda ls: ls.split("\n")[:-1])]) + [("get_yaml_lines", lambda ls: ls), ("to_yaml", lambda ls: ls.split("\n")[:-1])], +) def test_yaml(maptype, get_obs, parse_obs): """ Tests for attmap repr as YAML lines or full text chunk. """ eq = lambda a, b: a == b @@ -79,40 +84,61 @@ def test_disk_roundtrip(maptype, tmpdir, fmtlib): assert not os.path.exists(fp) fmtspec = FORMATTER_LIBRARIES[fmtlib] parse, write = fmtspec.parse, fmtspec.write - with open(fp, 'w') as f: + with open(fp, "w") as f: write(m, f) - with open(fp, 'r') as f: + with open(fp, "r") as f: recons = parse(f) assert recons == m.to_dict() @pytest.mark.parametrize( - ["args", "exp_newl_end"], - [(tuple(), True), ((False, ), False), ((True, ), True)]) + ["args", "exp_newl_end"], [(tuple(), True), ((False,), False), ((True,), True)] +) def test_yaml_newline(args, exp_newl_end, maptype): """ Map to_yaml adds newline by default but respect argument. """ data = {"randkey": "arbval"} assert maptype(data).to_yaml(*args).endswith("\n") is exp_newl_end -@pytest.mark.parametrize(["data", "env_var", "fmtlib", "exp_res"], [ - ({"arbkey": os.path.join("$HOME", "leaf.md")}, "HOME", JSON_NAME, - ["{{\"arbkey\": \"{}\"}}".format(os.path.join("$HOME", "leaf.md"))]), - ({"arbkey": os.path.join("$HOME", "leaf.md")}, "HOME", YAML_NAME, - ["arbkey: " + os.path.join("$HOME", "leaf.md")]), - ({"random": os.path.join("abc", "$HOME", "leaf.md")}, "HOME", JSON_NAME, - ["{{\"random\": \"{}\"}}".format(os.path.join("abc", "$HOME", "leaf.md"))]), - ({"random": os.path.join("abc", "$HOME", "leaf.md")}, "HOME", YAML_NAME, - ["random: " + os.path.join("abc", "$HOME", "leaf.md")]) -]) +@pytest.mark.parametrize( + ["data", "env_var", "fmtlib", "exp_res"], + [ + ( + {"arbkey": os.path.join("$HOME", "leaf.md")}, + "HOME", + JSON_NAME, + ['{{"arbkey": "{}"}}'.format(os.path.join("$HOME", "leaf.md"))], + ), + ( + {"arbkey": os.path.join("$HOME", "leaf.md")}, + "HOME", + YAML_NAME, + ["arbkey: " + os.path.join("$HOME", "leaf.md")], + ), + ( + {"random": os.path.join("abc", "$HOME", "leaf.md")}, + "HOME", + JSON_NAME, + ['{{"random": "{}"}}'.format(os.path.join("abc", "$HOME", "leaf.md"))], + ), + ( + {"random": os.path.join("abc", "$HOME", "leaf.md")}, + "HOME", + YAML_NAME, + ["random: " + os.path.join("abc", "$HOME", "leaf.md")], + ), + ], +) def test_disk_path_expansion(tmpdir, data, env_var, fmtlib, exp_res, maptype): """ Paths are not expanded when map goes to disk. """ # Pretests - assert len(data) == 1, \ - "To isolate focus, use just 1 item; got {} -- {}".format(len(data), data) - assert os.environ.get(env_var) is not None, \ - "Null or missing env var: {}".format(env_var) + assert len(data) == 1, "To isolate focus, use just 1 item; got {} -- {}".format( + len(data), data + ) + assert os.environ.get(env_var) is not None, "Null or missing env var: {}".format( + env_var + ) # Create the mapping. m = make_data(list(data.items()), maptype) @@ -124,9 +150,9 @@ def test_disk_path_expansion(tmpdir, data, env_var, fmtlib, exp_res, maptype): fp = tmpdir.join("disked_attmap.out").strpath assert not os.path.exists(fp) - with open(fp, 'w') as f: + with open(fp, "w") as f: write(m, f) - with open(fp, 'r') as f: + with open(fp, "r") as f: assert exp_res == [l.strip("\n") for l in f.readlines()] diff --git a/tests/test_to_map.py b/tests/test_to_map.py index 6eebc1b..3d87207 100644 --- a/tests/test_to_map.py +++ b/tests/test_to_map.py @@ -1,14 +1,17 @@ """ Test the basic mapping conversion functionality of an attmap """ import copy -from functools import partial import random import sys +from functools import partial + if sys.version_info < (3, 3): from collections import Mapping else: from collections.abc import Mapping + import pytest + from attmap import AttMap from tests.helpers import get_att_map @@ -20,10 +23,17 @@ def entries(): """ Basic data for a test case """ return copy.deepcopy( - {"arb_key": "text", "randn": random.randint(0, 10), - "nested": {"ntop": 0, "nmid": {"list": ["a", "b"]}, - "lowest": {"x": {"a": -1, "b": 1}}}, - "collection": {1, 2, 3}}) + { + "arb_key": "text", + "randn": random.randint(0, 10), + "nested": { + "ntop": 0, + "nmid": {"list": ["a", "b"]}, + "lowest": {"x": {"a": -1, "b": 1}}, + }, + "collection": {1, 2, 3}, + } + ) @pytest.fixture(scope="function") @@ -51,29 +61,41 @@ def test_type_conversion_completeness(am, attmap_type, exp_num_raw): def test_correct_size(am, entries): """ The transformed mapping should have its original size. """ - assert len(am) == len(entries), \ - "{} entries in attmap and {} in raw data".format(len(am), len(entries)) - assert len(am) == len(am.to_map()), \ - "{} entries in attmap and {} in conversion".format(len(am), len(am.to_map())) + assert len(am) == len(entries), "{} entries in attmap and {} in raw data".format( + len(am), len(entries) + ) + assert len(am) == len( + am.to_map() + ), "{} entries in attmap and {} in conversion".format(len(am), len(am.to_map())) def test_correct_keys(am, entries): """ Keys should be unaltered by the mapping type upcasting. """ + def text_keys(m): return ", ".join(m.keys()) + def check(m, name): - assert set(m.keys()) == set(entries.keys()), \ - "Mismatch between {} and raw keys.\nIn {}: {}\nIn raw data: {}".\ - format(name, name, text_keys(m), text_keys(entries)) + assert set(m.keys()) == set( + entries.keys() + ), "Mismatch between {} and raw keys.\nIn {}: {}\nIn raw data: {}".format( + name, name, text_keys(m), text_keys(entries) + ) + check(am, "attmap") check(am.to_map(), "converted") def test_values_equivalence(am, entries): """ Raw values should be equivalent. """ + def check(v1, v2): - return all([check(v1[k], v2[k]) for k in set(v1.keys()) | set(v2.keys())]) \ - if isinstance(v1, Mapping) else v1 == v2 + return ( + all([check(v1[k], v2[k]) for k in set(v1.keys()) | set(v2.keys())]) + if isinstance(v1, Mapping) + else v1 == v2 + ) + assert check(am.to_map(), entries) @@ -86,11 +108,15 @@ def _tally_types(m, t): :return int: number of values of the indicated type of interest """ if not isinstance(m, Mapping): - raise TypeError("Object in which to tally value types isn't a mapping: " - "{}".format(type(m))) + raise TypeError( + "Object in which to tally value types isn't a mapping: " + "{}".format(type(m)) + ) if not issubclass(t, Mapping): - raise ValueError("Type to tally should be a mapping subtype; got {}". - format(type(t))) + raise ValueError( + "Type to tally should be a mapping subtype; got {}".format(type(t)) + ) + def go(kvs, acc): try: head, tail = kvs[0], kvs[1:] @@ -99,6 +125,7 @@ def go(kvs, acc): k, v = head extra = 1 + go(list(v.items()), 0) if isinstance(v, t) else 0 return go(tail, acc + extra) + return go(list(m.items()), 1 if isinstance(m, t) else 0) From a736906b7869ae47bc65f51342958333cd4f2996 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Tue, 16 Mar 2021 12:12:45 -0400 Subject: [PATCH 04/10] update gh action --- .github/workflows/run-pytest.yml | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/.github/workflows/run-pytest.yml b/.github/workflows/run-pytest.yml index 216adf2..3bbdd24 100644 --- a/.github/workflows/run-pytest.yml +++ b/.github/workflows/run-pytest.yml @@ -8,21 +8,12 @@ on: jobs: pytest: + runs-on: ${{ matrix.os }} strategy: matrix: python-version: [3.6, 3.7, 3.8, 3.9] - os: [ubuntu-latest] # can't use macOS when using service containers or container jobs - runs-on: ${{ matrix.os }} - services: - postgres: - image: postgres - env: # needs to match DB config in: ../../tests/data/config.yaml - POSTGRES_USER: postgres - POSTGRES_PASSWORD: pipestat-password - POSTGRES_DB: pipestat-test - ports: - - 5432:5432 - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + os: [ubuntu-latest, macos-latest] + steps: - uses: actions/checkout@v2 @@ -37,14 +28,14 @@ jobs: - name: Install test dependancies run: if [ -f requirements/requirements-test.txt ]; then pip install -r requirements/requirements-test.txt; fi - - name: Install pipestat + - name: Install yacman run: python -m pip install . - name: Run pytest tests - run: pytest tests -x -vv --cov=./ --cov-report=xml + run: pytest tests --cov=./ --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: file: ./coverage.xml - name: py-${{ matrix.python-version }}-${{ matrix.os }} \ No newline at end of file + name: py-${{ matrix.python-version }}-${{ matrix.os }} From 2cb67c41292aa57cd6d21cada762097cda8d2e89 Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Thu, 10 Jun 2021 10:42:43 -0400 Subject: [PATCH 05/10] remove debug message --- attmap/attmap.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/attmap/attmap.py b/attmap/attmap.py index b8c7269..2346954 100644 --- a/attmap/attmap.py +++ b/attmap/attmap.py @@ -60,7 +60,7 @@ def __ne__(self, other): @staticmethod def _cmp(a, b): - """ Hook to tailor value comparison in determination of map equality. """ + """Hook to tailor value comparison in determination of map equality.""" def same_type(obj1, obj2, typenames=None): t1, t2 = str(obj1.__class__), str(obj2.__class__) @@ -105,7 +105,6 @@ def _metamorph_maplike(self, m): :return Mapping: a (perhaps more specialized) version of the given map :raise TypeError: if the given value isn't a Mapping """ - _LOGGER.debug("Transforming map-like: {}".format(m)) if not isinstance(m, Mapping): raise TypeError( "Cannot integrate a non-Mapping: {}\nType: {}".format(m, type(m)) @@ -113,7 +112,7 @@ def _metamorph_maplike(self, m): return self._lower_type_bound(m.items()) def _new_empty_basic_map(self): - """ Return the empty collection builder for Mapping type simplification. """ + """Return the empty collection builder for Mapping type simplification.""" return dict() def _repr_pretty_(self, p, cycle): From d542494789a3f9f0ba0ee75b57e9c561fe953dae Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Tue, 15 Jun 2021 13:02:26 -0400 Subject: [PATCH 06/10] add _excl_classes_from_todict, reformat --- .gitignore | 16 ++++----- .pre-commit-config.yaml | 21 +++++++++++ MANIFEST.in | 2 +- README.md | 1 - attmap/_att_map_like.py | 41 +++++++++++++++------- attmap/attmap_echo.py | 4 +-- attmap/ordattmap.py | 16 ++++----- attmap/pathex_attmap.py | 2 +- docs/changelog.md | 5 ++- docs/contributing.md | 7 ++-- docs/exclude_from_repr.md | 21 +++++++---- docs/support.md | 1 - mkdocs.yml | 1 - requirements/requirements-dev.txt | 2 +- requirements/requirements-test.txt | 2 +- setup.cfg | 1 - tests/conftest.py | 2 +- tests/helpers.py | 2 +- tests/regression/test_echo_subclass.py | 4 +-- tests/test_AttMap.py | 48 +++++++++++++------------- tests/test_basic_ops_dynamic.py | 30 ++++++++-------- tests/test_basic_ops_static.py | 24 ++++++------- tests/test_echo.py | 2 +- tests/test_equality.py | 10 +++--- tests/test_ordattmap.py | 32 ++++++++--------- tests/test_packaging.py | 2 +- tests/test_path_expansion.py | 12 +++---- tests/test_special_mutability.py | 18 +++++----- tests/test_to_dict.py | 2 +- tests/test_to_disk.py | 12 +++---- tests/test_to_map.py | 14 ++++---- 31 files changed, 200 insertions(+), 157 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.gitignore b/.gitignore index b42a723..dd43f47 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ doc/build/* # generic ignore list: *.lst -# Compiled source +# Compiled source *.com *.class *.dll @@ -24,8 +24,8 @@ doc/build/* *.o *.so *.pyc - -# Packages + +# Packages # it's better to unpack these files and commit the raw source # git has its own built in compression methods *.7z @@ -36,13 +36,13 @@ doc/build/* *.rar *.tar *.zip - -# Logs and databases + +# Logs and databases *.log *.sql *.sqlite - -# OS generated files + +# OS generated files .DS_Store .DS_Store? ._* @@ -51,7 +51,7 @@ doc/build/* ehthumbs.db Thumbs.db -# Gedit temporary files +# Gedit temporary files *~ # libreoffice lock files: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fd759dc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + - id: trailing-whitespace + - id: check-yaml + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace + - id: check-ast + + - repo: https://github.com/PyCQA/isort + rev: 5.8.0 + hooks: + - id: isort + args: ["--profile", "black"] + + - repo: https://github.com/psf/black + rev: 21.5b2 + hooks: + - id: black diff --git a/MANIFEST.in b/MANIFEST.in index de89ce2..2920959 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ include requirements/* include README.md -include LICENSE.txt \ No newline at end of file +include LICENSE.txt diff --git a/README.md b/README.md index b0052a2..2e38d4c 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,3 @@ Key-value Mapping supporting nesting and attribute-style access Originally motivated by and designed for the [pepkit family projects](https://pepkit.github.io/). - diff --git a/attmap/_att_map_like.py b/attmap/_att_map_like.py index 239e37b..1611fc4 100644 --- a/attmap/_att_map_like.py +++ b/attmap/_att_map_like.py @@ -2,6 +2,9 @@ import abc import sys +from typing import Generator, Iterator + +import pandas if sys.version_info < (3, 3): from collections import Mapping, MutableMapping @@ -20,7 +23,7 @@ class AttMapLike(MutableMapping): - """ Base class for multi-access-mode data objects. """ + """Base class for multi-access-mode data objects.""" __metaclass__ = abc.ABCMeta @@ -215,17 +218,29 @@ def _excl_from_repr(self, k, cls): """ return False + def _excl_classes_from_todict(self): + """ + Hook for exclusion of particular class from a dict conversion + """ + return + @abc.abstractproperty def _lower_type_bound(self): - """ Most specific type to which stored Mapping should be transformed """ + """Most specific type to which stored Mapping should be transformed""" pass @abc.abstractmethod def _new_empty_basic_map(self): - """ Return the empty collection builder for Mapping type simplification. """ + """Return the empty collection builder for Mapping type simplification.""" pass - def _simplify_keyvalue(self, kvs, build, acc=None, conversions=None): + def _simplify_keyvalue( + self, + kvs, + build, + acc=None, + conversions=None, + ): """ Simplify a collection of key-value pairs, "reducing" to simpler types. @@ -234,19 +249,19 @@ def _simplify_keyvalue(self, kvs, build, acc=None, conversions=None): :param Iterable acc: accumulating collection of simplified data :return Iterable: collection of simplified data """ - acc = acc or build() kvs = iter(kvs) try: k, v = next(kvs) except StopIteration: return acc - if is_custom_map(v): - v = self._simplify_keyvalue(v.items(), build, build()) - if isinstance(v, Mapping): - for pred, proxy in conversions or []: - if pred(v): - v = proxy - break - acc[k] = v + if not isinstance(v, self._excl_classes_from_todict() or tuple()): + if is_custom_map(v): + v = self._simplify_keyvalue(v.items(), build, build()) + if isinstance(v, Mapping): + for pred, proxy in conversions or []: + if pred(v): + v = proxy + break + acc[k] = v return self._simplify_keyvalue(kvs, build, acc, conversions) diff --git a/attmap/attmap_echo.py b/attmap/attmap_echo.py index 77551c9..4244e30 100644 --- a/attmap/attmap_echo.py +++ b/attmap/attmap_echo.py @@ -9,7 +9,7 @@ class EchoAttMap(PathExAttMap): - """ An AttMap that returns key/attr if it has no set value. """ + """An AttMap that returns key/attr if it has no set value.""" def __getattr__(self, item, default=None, expand=True): """ @@ -42,7 +42,7 @@ def __getattr__(self, item, default=None, expand=True): @property def _lower_type_bound(self): - """ Most specific type to which an inserted value may be converted """ + """Most specific type to which an inserted value may be converted""" return AttMapEcho diff --git a/attmap/ordattmap.py b/attmap/ordattmap.py index f13d929..212624c 100644 --- a/attmap/ordattmap.py +++ b/attmap/ordattmap.py @@ -17,7 +17,7 @@ class OrdAttMap(OrderedDict, AttMap): - """ Insertion-ordered mapping with dot notation access """ + """Insertion-ordered mapping with dot notation access""" def __init__(self, entries=None): super(OrdAttMap, self).__init__(entries or {}) @@ -48,27 +48,27 @@ def __getitem__(self, item): return AttMap.__getitem__(self, item) def __setitem__(self, key, value, finalize=True): - """ Support hook for value transformation before storage. """ + """Support hook for value transformation before storage.""" super(OrdAttMap, self).__setitem__( key, self._final_for_store(key, value) if finalize else value ) def __delitem__(self, key): - """ Make unmapped key deletion unexceptional. """ + """Make unmapped key deletion unexceptional.""" try: super(OrdAttMap, self).__delitem__(key) except KeyError: _LOGGER.debug(safedel_message(key)) def __eq__(self, other): - """ Leverage base AttMap eq check, and check key order. """ + """Leverage base AttMap eq check, and check key order.""" return AttMap.__eq__(self, other) and list(self.keys()) == list(other.keys()) def __ne__(self, other): return not self == other def __repr__(self): - """ Leverage base AttMap text representation. """ + """Leverage base AttMap text representation.""" return AttMap.__repr__(self) def __reversed__(self): @@ -109,14 +109,14 @@ def popitem(self, last=True): @staticmethod def _is_od_member(name): - """ Assess whether name appears to be a protected OrderedDict member. """ + """Assess whether name appears to be a protected OrderedDict member.""" return name.startswith("_OrderedDict") def _new_empty_basic_map(self): - """ For ordered maps, OrderedDict is the basic building block. """ + """For ordered maps, OrderedDict is the basic building block.""" return OrderedDict() @property def _lower_type_bound(self): - """ OrdAttMap is the type to which nested maps are converted. """ + """OrdAttMap is the type to which nested maps are converted.""" return OrdAttMap diff --git a/attmap/pathex_attmap.py b/attmap/pathex_attmap.py index 409e464..9b9520b 100644 --- a/attmap/pathex_attmap.py +++ b/attmap/pathex_attmap.py @@ -19,7 +19,7 @@ class PathExAttMap(OrdAttMap): - """ Used in pepkit projects, with Mapping conversion and path expansion """ + """Used in pepkit projects, with Mapping conversion and path expansion""" def __getattribute__(self, item, expand=True): res = super(PathExAttMap, self).__getattribute__(item) diff --git a/docs/changelog.md b/docs/changelog.md index 49f928a..5fef316 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +## [0.13.1] - unreleased +### Added +- `_excl_classes_from_todict`, which can be used to list classes to be exluded from the `dict` representation + ## [0.13.0] - 2021-02-22 ### Added - block style YAML representation support for lists @@ -158,4 +162,3 @@ ## [0.1] - 2019-02-04 ### New - Initial release - diff --git a/docs/contributing.md b/docs/contributing.md index 4dd5843..6273d33 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -3,11 +3,10 @@ Pull requests or issues are welcome. - After adding tests in `tests` for a new feature or a bug fix, please run the test suite. -- To do so, the only additional dependencies needed beyond those for the package can be +- To do so, the only additional dependencies needed beyond those for the package can be installed with: ```pip install -r requirements/requirements-dev.txt``` - -- Once those are installed, the tests can be run with `pytest`. Alternatively, -`python setup.py test` can be used. +- Once those are installed, the tests can be run with `pytest`. Alternatively, +`python setup.py test` can be used. diff --git a/docs/exclude_from_repr.md b/docs/exclude_from_repr.md index 44a8021..cb6e1c6 100644 --- a/docs/exclude_from_repr.md +++ b/docs/exclude_from_repr.md @@ -1,7 +1,7 @@ # Use cases and "how-to..." ## How to customize a subtype's text rendition -In a subclass, override `_excl_from_repr`, using key and/or type of value. +In a subclass, override `_excl_from_repr`, using key and/or type of value. The most basic implementation is a no-op, excluding nothing: ```python @@ -21,17 +21,17 @@ To exclude by value type, you can use something like: def _excl_from_repr(self, k, cls): return issubclass(cls, BaseOmissionType) ``` -where `BaseOmissionType` is a proxy for the name of some type of values that may +where `BaseOmissionType` is a proxy for the name of some type of values that may be stored in your mapping but that you prefer to not display in its text representation. -The two kinds of exclusion criteria may be combined as desired. -Note that it's often advisable to invoke the superclass version of the method, +The two kinds of exclusion criteria may be combined as desired. +Note that it's often advisable to invoke the superclass version of the method, but to achieve the intended effect this may be skipped. ## How to exclude the object type from a text rendition -Staring in `0.12.9`, you can override the `__repr__` with to use the new `_render` arg, `exclude_class_list`: +Starting in `0.12.9`, you can override the `__repr__` with to use the new `_render` arg, `exclude_class_list`: ```python def __repr__(self): @@ -41,4 +41,13 @@ def __repr__(self): return self._render(self._simplify_keyvalue( self._data_for_repr(), self._new_empty_basic_map), exclude_class_list="YacAttMap") -``` \ No newline at end of file +``` + +## How to exclude classes from `to_dict` conversion + +Starting in `0.13.1`, you can specify a collection of classes that will be skipped in the conversion of the object to `dict`. + +```python +def _excl_classes_from_todict(self): + return (pandas.DataFrame, ClassToExclude,) +``` diff --git a/docs/support.md b/docs/support.md index 9fe52bf..cfa4052 100644 --- a/docs/support.md +++ b/docs/support.md @@ -1,4 +1,3 @@ ## Support Please use the [issue tracker](https://github.com/pepkit/attmap/issues) at GitHub to file bug reports or feature requests. - diff --git a/mkdocs.yml b/mkdocs.yml index 64a5061..fe8db89 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -22,4 +22,3 @@ plugins: autodoc_package: attmap autodoc_build: "docs/autodoc_build" no_top_level: true - diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 288f812..0d08f46 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -1,7 +1,7 @@ hypothesis==4.38.0 mock>=2.0.0 +pandas>=0.20.2 pytest>=4.6.9 pyyaml>=3.12 -pandas>=0.20.2 ubiquerg>=0.3 veracitools diff --git a/requirements/requirements-test.txt b/requirements/requirements-test.txt index 5d57f7b..a1303c8 100644 --- a/requirements/requirements-test.txt +++ b/requirements/requirements-test.txt @@ -1,3 +1,3 @@ coveralls +pytest>=4.6.9 pytest-cov>=2.8.1 -pytest>=4.6.9 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 8fa991e..d6e4035 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,4 +11,3 @@ testpaths = tests python_files = test_*.py python_classes = Test* *Test *Tests *Tester python_functions = test_* test[A-Z]* - diff --git a/tests/conftest.py b/tests/conftest.py index 7cf1edd..c12126a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,5 +21,5 @@ @pytest.fixture(scope="function", params=ALL_ATTMAPS) def attmap_type(request, entries): - """ An AttMapLike subtype """ + """An AttMapLike subtype""" return request.param diff --git a/tests/helpers.py b/tests/helpers.py index 0845364..b036b16 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -32,7 +32,7 @@ def get_att_map(cls, entries=None): - """ Create a fresh, empty data object. """ + """Create a fresh, empty data object.""" assert issubclass(cls, AttMapLike) return cls(entries or {}) diff --git a/tests/regression/test_echo_subclass.py b/tests/regression/test_echo_subclass.py index 12614c6..aac50d5 100644 --- a/tests/regression/test_echo_subclass.py +++ b/tests/regression/test_echo_subclass.py @@ -9,7 +9,7 @@ class _SubEcho(EchoAttMap): - """ Dummy class to derive from EchoAttMap """ + """Dummy class to derive from EchoAttMap""" def __init__(self, entries=None): super(_SubEcho, self).__init__(entries) @@ -17,7 +17,7 @@ def __init__(self, entries=None): @pytest.mark.parametrize("entries", [None, {}]) def test_echo_subclass_smoke(entries): - """ Superclass ctor invocation avoids infinite recursion. """ + """Superclass ctor invocation avoids infinite recursion.""" try: _SubEcho(entries) except RuntimeError as e: diff --git a/tests/test_AttMap.py b/tests/test_AttMap.py index c1042d3..b73c08a 100644 --- a/tests/test_AttMap.py +++ b/tests/test_AttMap.py @@ -32,7 +32,7 @@ def pytest_generate_tests(metafunc): - """ Centralize dynamic test case parameterization. """ + """Centralize dynamic test case parameterization.""" if "empty_collection" in metafunc.fixturenames: # Test case strives to validate expected behavior on empty container. collection_types = [tuple, list, set, dict] @@ -44,13 +44,13 @@ def pytest_generate_tests(metafunc): def basic_entries(): - """ AttMap data that lack nested structure. """ + """AttMap data that lack nested structure.""" for k, v in zip(_BASE_KEYS, _BASE_VALUES): yield k, v def nested_entries(): - """ AttributeDict data with some nesting going on. """ + """AttributeDict data with some nesting going on.""" for k, v in _SEASON_HIERARCHY.items(): yield k, v @@ -58,7 +58,7 @@ def nested_entries(): @pytest.mark.parametrize("base", ["random", "irrelevant", "arbitrary"]) @pytest.mark.parametrize("protect", [False, True]) def test_echo_is_conditional(base, protect): - """ Protected member isn't echoed. """ + """Protected member isn't echoed.""" m = AttMapEcho({}) if protect: with pytest.raises(AttributeError): @@ -83,11 +83,11 @@ class AttributeConstructionDictTests: # data and fixtures specific to this class. def test_null_construction(self): - """ Null entries value creates empty AttMap. """ + """Null entries value creates empty AttMap.""" assert AttMap({}) == AttMap(None) def test_empty_construction(self, empty_collection): - """ Empty entries container create empty AttMap. """ + """Empty entries container create empty AttMap.""" m = AttMap(empty_collection) assert AttMap(None) == m assert m != dict() @@ -105,7 +105,7 @@ def test_empty_construction(self, empty_collection): ], ) def test_construction_modes_supported(self, entries_gen, entries_provision_type): - """ Construction wants key-value pairs; wrapping doesn't matter. """ + """Construction wants key-value pairs; wrapping doesn't matter.""" entries_mapping = dict(entries_gen()) if entries_provision_type == "dict": entries = entries_mapping @@ -167,7 +167,7 @@ class AttMapUpdateTests: argvalues=itertools.product(_SETTERS, _GETTERS, (False, True)), ) def test_set_get_atomic(self, setter_name, getter_name, is_novel): - """ For new and existing items, validate set/get behavior. """ + """For new and existing items, validate set/get behavior.""" # Establish the AttMap for the test case. data = dict(basic_entries()) @@ -231,10 +231,10 @@ def test_squash_existing(self, name_update_func): argnames="name_fetch_func", argvalues=["__getattr__", "__getitem__"] ) class AttMapNullTests: - """ AttMap has configurable behavior regarding null values. """ + """AttMap has configurable behavior regarding null values.""" def test_new_null(self, name_update_func, name_fetch_func): - """ When a key/item, isn't known, null is allowed. """ + """When a key/item, isn't known, null is allowed.""" ad = AttMap() setter = getattr(ad, name_update_func) args = ("new_key", None) @@ -243,7 +243,7 @@ def test_new_null(self, name_update_func, name_fetch_func): assert getter("new_key") is None def test_replace_null(self, name_update_func, name_fetch_func): - """ Null can be replaced by non-null. """ + """Null can be replaced by non-null.""" ad = AttMap({"lone_attr": None}) assert getattr(ad, name_fetch_func)("lone_attr") is None setter = getattr(ad, name_update_func) @@ -260,7 +260,7 @@ def _do_update(name_setter_func, setter_bound_method, args): class AttMapItemAccessTests: - """ Tests for access of items (key- or attribute- style). """ + """Tests for access of items (key- or attribute- style).""" @pytest.mark.parametrize(argnames="missing", argvalues=["att", ""]) def test_missing_getattr(self, missing): @@ -275,7 +275,7 @@ def test_missing_getitem(self, missing): attrd[missing] def test_numeric_key(self): - """ Attribute request must be string. """ + """Attribute request must be string.""" ad = AttMap({1: "a"}) assert "a" == ad[1] with pytest.raises(TypeError): @@ -283,7 +283,7 @@ def test_numeric_key(self): class AttMapSerializationTests: - """ Tests for AttMap serialization. """ + """Tests for AttMap serialization.""" DATA_PAIRS = [ ("a", 1), @@ -311,7 +311,7 @@ class AttMapSerializationTests: ids=lambda data_type: " data_type = {}".format(data_type), ) def test_pickle_restoration(self, tmpdir, data, data_type): - """ Pickled and restored AttMap objects are identical. """ + """Pickled and restored AttMap objects are identical.""" # Type the AttMap input data argument according to parameter. data = data_type(data) @@ -336,7 +336,7 @@ def test_pickle_restoration(self, tmpdir, data, data_type): class AttMapObjectSyntaxAccessTests: - """ Test behavior of dot attribute access / identity setting. """ + """Test behavior of dot attribute access / identity setting.""" DEFAULT_VALUE = "totally-arbitrary" NORMAL_ITEM_ARG_VALUES = [ @@ -352,7 +352,7 @@ class AttMapObjectSyntaxAccessTests: @pytest.fixture(scope="function") def attrdict(self, request): - """ Provide a test case with an AttMap. """ + """Provide a test case with an AttMap.""" d = self.ATTR_DICT_DATA return ( AttMapEcho(d) if request.getfixturevalue("return_identity") else AttMap(d) @@ -372,7 +372,7 @@ def attrdict(self, request): ids=lambda attr: " requested={} ".format(attr), ) def test_attribute_access(self, return_identity, attr_to_request, attrdict): - """ Access behavior depends on request and behavior toggle. """ + """Access behavior depends on request and behavior toggle.""" if attr_to_request == "__dict__": # The underlying mapping is still accessible. assert attrdict.__dict__ is getattr(attrdict, "__dict__") @@ -405,19 +405,19 @@ def test_attribute_access(self, return_identity, attr_to_request, attrdict): class NullityTests: - """ Tests of null/non-null values """ + """Tests of null/non-null values""" _KEYNAMES = ["sample_name", "protocol", "arbitrary_attribute"] @pytest.mark.parametrize(argnames="item", argvalues=_KEYNAMES) def test_missing_is_neither_null_nor_non_null(self, item): - """ Value of absent key is neither null nor non-null """ + """Value of absent key is neither null nor non-null""" ad = AttMap() assert not ad.is_null(item) and not ad.non_null(item) @pytest.mark.parametrize(argnames="item", argvalues=_KEYNAMES) def test_is_null(self, item): - """ Null-valued key/item evaluates as such. """ + """Null-valued key/item evaluates as such.""" ad = AttMap() ad[item] = None assert ad.is_null(item) and not ad.non_null(item) @@ -427,7 +427,7 @@ def test_is_null(self, item): argvalues=list(zip(_KEYNAMES, ["sampleA", "WGBS", "random"])), ) def test_non_null(self, k, v): - """ AD is sensitive to value updates """ + """AD is sensitive to value updates""" ad = AttMap() assert not ad.is_null(k) and not ad.non_null(k) ad[k] = None @@ -438,7 +438,7 @@ def test_non_null(self, k, v): @pytest.mark.usefixtures("write_project_files") class SampleYamlTests: - """ AttMap metadata only appear in YAML if non-default. """ + """AttMap metadata only appear in YAML if non-default.""" @staticmethod def _yaml_data( @@ -467,5 +467,5 @@ def _yaml_data( @pytest.mark.parametrize(["func", "exp"], [(repr, "AttMap: {}"), (str, "AttMap: {}")]) def test_text_repr_empty(func, exp): - """ Empty AttMap is correctly represented as text. """ + """Empty AttMap is correctly represented as text.""" assert exp == func(AttMap()) diff --git a/tests/test_basic_ops_dynamic.py b/tests/test_basic_ops_dynamic.py index 45fad4f..ab137eb 100644 --- a/tests/test_basic_ops_dynamic.py +++ b/tests/test_basic_ops_dynamic.py @@ -12,12 +12,12 @@ @pytest.fixture(scope="function", params=[{}, {"a": 1}, {"b": [1, 2, 3], "c": (1, 2)}]) def entries(request): - """ Data to store as entries in an attmap. """ + """Data to store as entries in an attmap.""" return request.param def test_length_decrease(attmap_type, entries): - """ Length/size of an attmap should match number of entries. """ + """Length/size of an attmap should match number of entries.""" m = get_att_map(attmap_type, entries) assert len(entries) == len(m) ks = list(entries.keys()) @@ -27,7 +27,7 @@ def test_length_decrease(attmap_type, entries): def test_length_increase(attmap_type, entries): - """ Length/size of an attmap should match number of entries. """ + """Length/size of an attmap should match number of entries.""" m = get_att_map(attmap_type) for (i, (k, v)) in enumerate(entries.items()): assert i == len(m) @@ -36,7 +36,7 @@ def test_length_increase(attmap_type, entries): def test_positive_membership(attmap_type, entries): - """ Each key is a member; a nonmember should be flagged as such """ + """Each key is a member; a nonmember should be flagged as such""" import random m = get_att_map(attmap_type) @@ -49,7 +49,7 @@ def test_positive_membership(attmap_type, entries): def test_negative_membership(attmap_type, entries): - """ Object key status responds to underlying data change. """ + """Object key status responds to underlying data change.""" m = get_att_map(attmap_type, entries) for k in entries: assert k in m @@ -61,7 +61,7 @@ def test_negative_membership(attmap_type, entries): "series", [Series(data) for data in [("a", 1), [("b", 2), ("c", 3)], []]] ) def test_add_pandas_series(series, attmap_type): - """ A pandas Series can be used as a simple container of entries to add. """ + """A pandas Series can be used as a simple container of entries to add.""" m = get_att_map(attmap_type) raw_data = series.to_dict() keys = set(raw_data.keys()) @@ -73,7 +73,7 @@ def test_add_pandas_series(series, attmap_type): @pytest.mark.parametrize("seed_data", [{}, {"a": 1}, {"b": 2, "c": 3}]) @pytest.mark.parametrize("delkey", ["d", "e"]) def test_del_unmapped_key(attmap_type, seed_data, delkey): - """ Attempt to remove unmapped key should not fail. """ + """Attempt to remove unmapped key should not fail.""" m = get_att_map(attmap_type, entries=seed_data) assert delkey not in m try: @@ -88,7 +88,7 @@ def test_del_unmapped_key(attmap_type, seed_data, delkey): [(repr, []), (str, [lambda s, dt: s.startswith(dt.__name__)])], ) def test_text(attmap_type, entries, f_extra_checks_pair): - """ Formal text representation of an attmap responds to data change. """ + """Formal text representation of an attmap responds to data change.""" get_rep, extra_checks = f_extra_checks_pair m = get_att_map(attmap_type) @@ -151,7 +151,7 @@ def find_line(key, val): class CheckNullTests: - """ Test accuracy of the null value test methods. """ + """Test accuracy of the null value test methods.""" DATA = [(("truly_null", None), True)] + [ (kv, False) @@ -166,20 +166,20 @@ class CheckNullTests: @pytest.fixture(scope="function") def entries(self): - """ Provide some basic entries for a test case's attmap. """ + """Provide some basic entries for a test case's attmap.""" return dict([kv for kv, _ in self.DATA]) @staticmethod @pytest.fixture(scope="function") def m(attmap_type): - """ Build an AttMap instance of the given subtype. """ + """Build an AttMap instance of the given subtype.""" return get_att_map(attmap_type) @pytest.mark.skip(reason="test appears broken") @staticmethod @given(v=rand_non_null()) def test_null_to_non_null(m, v): - """ Non-null value can overwrite null. """ + """Non-null value can overwrite null.""" k = random_str_key() m[k] = None assert m.is_null(k) and not m.non_null(k) @@ -190,7 +190,7 @@ def test_null_to_non_null(m, v): @staticmethod @given(v=rand_non_null()) def test_non_null_to_null(m, v): - """ Null value can overwrite non-null. """ + """Null value can overwrite non-null.""" k = random_str_key() m[k] = v assert not m.is_null(k) and m.non_null(k) @@ -199,7 +199,7 @@ def test_non_null_to_null(m, v): @staticmethod def test_null_to_absent(m): - """ Null value for previously absent key is inserted. """ + """Null value for previously absent key is inserted.""" k = random_str_key() m[k] = None assert m.is_null(k) and not m.non_null(k) @@ -210,7 +210,7 @@ def test_null_to_absent(m): @staticmethod @given(v=rand_non_null()) def test_non_null_to_absent(m, v): - """ Non-null value for previously absent key is inserted. """ + """Non-null value for previously absent key is inserted.""" k = random_str_key() m[k] = v assert not m.is_null(k) and m.non_null(k) diff --git a/tests/test_basic_ops_static.py b/tests/test_basic_ops_static.py index fd250f5..6c6d38d 100644 --- a/tests/test_basic_ops_static.py +++ b/tests/test_basic_ops_static.py @@ -22,47 +22,47 @@ params=[{}, {"a": 1}, {"b": [1, "2", 3]}, {"c": {1: 2}}, {"d": {"F": "G"}}], ) def entries(request): - """ Data to store as entries in an attmap. """ + """Data to store as entries in an attmap.""" return request.param @pytest.fixture(scope="function", params=["Z", 1, None]) def nonmember(request): - """ Object that should not be present as key in a mapping. """ + """Object that should not be present as key in a mapping.""" return request.param def test_length(attmap_type, entries): - """ Length/size of an attmap should match number of entries. """ + """Length/size of an attmap should match number of entries.""" assert len(entries) == len(get_att_map(attmap_type, entries)) def test_positive_membership(attmap_type, entries): - """ Each key is a member; a nonmember should be flagged as such """ + """Each key is a member; a nonmember should be flagged as such""" m = get_att_map(attmap_type, entries) assert [] == [k for k in entries if k not in m] def test_negative_membership(attmap_type, entries, nonmember): - """ Check correctness of membership test for nonmember key. """ + """Check correctness of membership test for nonmember key.""" m = get_att_map(attmap_type, entries) assert nonmember not in m def test_repr(attmap_type, entries): - """ Check raw text representation of an attmap. """ + """Check raw text representation of an attmap.""" obs = repr(get_att_map(attmap_type, entries)) assert obs.startswith(attmap_type.__name__) def test_str(attmap_type, entries): - """ Check informal text representation of an attmap. """ + """Check informal text representation of an attmap.""" m = get_att_map(attmap_type, entries) assert repr(m) == str(m) class CheckNullTests: - """ Test accuracy of the null value test methods. """ + """Test accuracy of the null value test methods.""" DATA = [(("truly_null", None), True)] + [ (kv, False) @@ -80,24 +80,24 @@ class CheckNullTests: @pytest.fixture(scope="function") def entries(self): - """ Provide some basic entries for a test case's attmap. """ + """Provide some basic entries for a test case's attmap.""" return dict([kv for kv, _ in self.DATA]) @staticmethod @pytest.fixture(scope="function", params=[k for ((k, _), _) in DATA]) def k(request): - """ Key to test """ + """Key to test""" return request.param @staticmethod def test_present_is_null(attmap_type, entries, k): - """ Null check on key's value works as expected. """ + """Null check on key's value works as expected.""" m = get_att_map(attmap_type, entries) assert (entries[k] is None) == m.is_null(k) @staticmethod def test_present_non_null(attmap_type, entries, k): - """ Non-null check on key's value works as expected. """ + """Non-null check on key's value works as expected.""" m = get_att_map(attmap_type, entries) assert (entries[k] is not None) == m.non_null(k) diff --git a/tests/test_echo.py b/tests/test_echo.py index d86943e..964b1bc 100644 --- a/tests/test_echo.py +++ b/tests/test_echo.py @@ -30,7 +30,7 @@ def exec_get_item(m, k): ], ) def test_echo_is_type_dependent_and_access_dependent(maptype, getter, key, expected): - """ Retrieval of missing key/attr echoes it iff the type and access mode permit. """ + """Retrieval of missing key/attr echoes it iff the type and access mode permit.""" m = maptype() assert key not in m with ExpectContext(expected, getter) as ctx: diff --git a/tests/test_equality.py b/tests/test_equality.py index adf5da5..12339ed 100644 --- a/tests/test_equality.py +++ b/tests/test_equality.py @@ -18,7 +18,7 @@ @pytest.fixture(scope="function") def basic_data(): - """ Provide a test case with a couple of key-value pairs to work with. """ + """Provide a test case with a couple of key-value pairs to work with.""" return {"a": 1, "b": 2} @@ -27,7 +27,7 @@ def basic_data(): ["s1_data", "s2_data"], [({"c": 3}, {"d": 4}), ({}, {"c": 3}), ({"d": 4}, {})] ) def test_series_labels_mismatch_is_not_equal(basic_data, s1_data, s2_data, attmap_type): - """ Maps with differently-labeled Series as values cannot be equal. """ + """Maps with differently-labeled Series as values cannot be equal.""" d1 = copy.copy(basic_data) d1.update(s1_data) d2 = copy.copy(basic_data) @@ -52,7 +52,7 @@ def test_series_labels_mismatch_is_not_equal(basic_data, s1_data, s2_data, attma ], ) def test_eq_with_scistack_value_types(attmap_type, obj1, obj2, expected): - """ Map comparison properly handles array-likes from scientific stack. """ + """Map comparison properly handles array-likes from scientific stack.""" key = "obj" m1 = get_att_map(attmap_type, {key: obj1}) m2 = get_att_map(attmap_type, {key: obj2}) @@ -77,7 +77,7 @@ def test_eq_with_scistack_value_types(attmap_type, obj1, obj2, expected): ], ) def test_equality_is_strict_in_type(data, this_type, that_type, exp): - """ Attmap equality requires exact type match. """ + """Attmap equality requires exact type match.""" m1 = get_att_map(this_type, data) m2 = get_att_map(that_type, data) assert type(m1) == this_type @@ -94,7 +94,7 @@ def test_equality_is_strict_in_type(data, this_type, that_type, exp): [({"a": 1}, "a"), ({"b": 2, "c": 3}, "b"), ({"b": 2, "c": 3}, "c")], ) def test_equality_fails_when_keysets_are_non_identical(maptype, data, delkey): - """ Map comparison fails--unexceptionally--when operands differ in keys. """ + """Map comparison fails--unexceptionally--when operands differ in keys.""" m1 = get_att_map(maptype, data) m2 = get_att_map(maptype, data) assert m1 == m2 diff --git a/tests/test_ordattmap.py b/tests/test_ordattmap.py index 89cca22..5f1cc82 100644 --- a/tests/test_ordattmap.py +++ b/tests/test_ordattmap.py @@ -15,7 +15,7 @@ def pytest_generate_tests(metafunc): - """ Test case generation and parameterization for this module """ + """Test case generation and parameterization for this module""" hwy_dat = [ ("Big Sur", 1), ("Jasper", 93), @@ -55,7 +55,7 @@ def kv_lists_strategy(pool=(integers, text, characters, uuids), **kwargs): ], ) def test_subclassing(cls, exp): - """ Verify that OrdAttMap type has expected type memberships. """ + """Verify that OrdAttMap type has expected type memberships.""" assert exp is issubclass(OrdAttMap, cls) @@ -70,19 +70,19 @@ def test_subclassing(cls, exp): ], ) def test_type_membership(cls, exp): - """ Verify that an OrdAttMap instance passes type checks as expected. """ + """Verify that an OrdAttMap instance passes type checks as expected.""" assert exp is isinstance(OrdAttMap(), cls) @given(kvs=kv_lists_strategy()) def test_ordattmap_insertion_order(kvs): - """ Verify order preservation. """ + """Verify order preservation.""" assert kvs == list(OrdAttMap(kvs).items()) @given(kvs=kv_lists_strategy()) def test_ordattmap_size(kvs): - """ Verify size determination. """ + """Verify size determination.""" exp = len(kvs) assert exp > 0 assert exp == len(OrdAttMap(kvs)) @@ -90,7 +90,7 @@ def test_ordattmap_size(kvs): @given(kvs=kv_lists_strategy()) def test_ordattmap_contains(kvs): - """ Verify key containment check. """ + """Verify key containment check.""" m = OrdAttMap(kvs) missing = [k for k, _ in kvs if k not in m] if missing: @@ -100,7 +100,7 @@ def test_ordattmap_contains(kvs): @given(kvs=kv_lists_strategy(pool=(text, characters))) @pytest.mark.parametrize("access", [lambda m, k: m[k], getattr]) def test_ordattmap_access(kvs, access): - """ Verify dual value access modes. """ + """Verify dual value access modes.""" if sys.version_info.major < 3: kvs = [(k.encode("utf-8"), v) for k, v in kvs] m = OrdAttMap(kvs) @@ -118,7 +118,7 @@ def test_ordattmap_access(kvs, access): ["other_type", "expected"], [(dict, False), (OrderedDict, False), (OrdAttMap, True)] ) def test_ordattmap_eq(kvs, other_type, expected): - """ Verify equality comparison behavior. """ + """Verify equality comparison behavior.""" obs = OrdAttMap(kvs) == other_type(kvs) assert obs == expected @@ -131,7 +131,7 @@ def test_ordattmap_eq(kvs, other_type, expected): ], ) def test_ordattmap_deletion(hwy_dat_key, raw_hwy_dat, alter, check): - """ Validate key deletion behavior of OrdAttMap. """ + """Validate key deletion behavior of OrdAttMap.""" m = OrdAttMap(raw_hwy_dat) assert hwy_dat_key in m obs = alter(m, hwy_dat_key) @@ -149,7 +149,7 @@ def test_ordattmap_deletion(hwy_dat_key, raw_hwy_dat, alter, check): def test_ordattmap_overrides_eq_exclusion( hwy_dat_key, raw_hwy_dat, base_type, that_type, final_exp ): - """ Verify ability to exclude key from comparisons. """ + """Verify ability to exclude key from comparisons.""" class OrdSub(base_type): def _excl_from_eq(self, k): @@ -172,7 +172,7 @@ def _excl_from_eq(self, k): @pytest.mark.parametrize("that_type", [dict, OrderedDict, OrdAttMap]) def test_ordattmap_overrides_repr_exclusion(hwy_dat_key, raw_hwy_dat, that_type): - """ Verify ability to exclude key from __repr__. """ + """Verify ability to exclude key from __repr__.""" class OrdSub(OrdAttMap): def _excl_from_repr(self, k, cls): @@ -207,12 +207,12 @@ def _excl_from_repr(self, k, cls): ], ) def test_ordattmap_repr(raw_hwy_dat, that_type, exp): - """ Test __repr__ of OrdAttMap. """ + """Test __repr__ of OrdAttMap.""" assert exp is (repr(OrdAttMap(raw_hwy_dat)) == repr(that_type(raw_hwy_dat))) class BasicDataTests: - """ Tests for some OrdAttMap behaviors on some very basic data. """ + """Tests for some OrdAttMap behaviors on some very basic data.""" BASIC_DATA = [ ("a", OrderedDict([("c", 3), ("b", 2)])), @@ -222,7 +222,7 @@ class BasicDataTests: @pytest.fixture(params=[OrdAttMap, PathExAttMap]) def oam(self, request): - """ Provide test case with a simple ordered attmap instance. """ + """Provide test case with a simple ordered attmap instance.""" return request.param(self.BASIC_DATA) @pytest.mark.parametrize( @@ -235,7 +235,7 @@ def oam(self, request): ], ) def test_ordattmap_simplification_to_map(self, oam, get_value, expected): - """ Test the nested type simplification behavior for ordered attmap. """ + """Test the nested type simplification behavior for ordered attmap.""" assert expected == get_value(oam.to_map()) @pytest.mark.parametrize( @@ -250,7 +250,7 @@ def test_ordattmap_simplification_to_map(self, oam, get_value, expected): ], ) def test_ordattmap_repr(self, oam, lineno, expected): - """ Test the ordering and indentation of ordered attmap repr. """ + """Test the ordering and indentation of ordered attmap repr.""" obstext = repr(oam) print("OBSERVED TEXT (below):\n{}".format(obstext)) ls = obstext.split("\n") diff --git a/tests/test_packaging.py b/tests/test_packaging.py index ed9b780..0a7f2fd 100644 --- a/tests/test_packaging.py +++ b/tests/test_packaging.py @@ -46,7 +46,7 @@ def get_base_check(*bases): ), ) def test_top_level_exports(obj_name, typecheck): - """ At package level, validate object availability and type. """ + """At package level, validate object availability and type.""" mod = attmap try: obj = getattr(mod, obj_name) diff --git a/tests/test_path_expansion.py b/tests/test_path_expansion.py index 363b52d..9dbf6cd 100644 --- a/tests/test_path_expansion.py +++ b/tests/test_path_expansion.py @@ -22,7 +22,7 @@ @pytest.fixture(scope="function") def pam(): - """ Provide a test case with a clean/fresh map. """ + """Provide a test case with a clean/fresh map.""" return PathExAttMap() @@ -61,7 +61,7 @@ def get_random_var(): ) @pytest.mark.parametrize("fetch", [getattr, lambda m, k: m[k]]) def test_PathExAttMap_expands_available_variables(pam, path, env, fetch): - """ Insertion of text encoding environment variables should expand. """ + """Insertion of text encoding environment variables should expand.""" k = random.choice(string.ascii_lowercase) with TmpEnv(**env): pam[k] = path @@ -111,7 +111,7 @@ def part(vs): ) @pytest.mark.parametrize("fetch", [getattr, lambda m, k: m[k]]) def test_PathExAttMap_substitution_is_selective(path, pres, repl, env, pam, fetch): - """ Values that are environment variables are replaced; others aren't. """ + """Values that are environment variables are replaced; others aren't.""" k = random.choice(string.ascii_lowercase) with TmpEnv(**env): pam[k] = path @@ -138,7 +138,7 @@ def test_PathExAttMap_substitution_is_selective(path, pres, repl, env, pam, fetc "env", [{ev: "".join(string.ascii_lowercase for _ in range(20)) for ev in _RVS}] ) def test_non_PathExAttMap_preserves_all_variables(path, fetch, env): - """ Only a PathExAttMap eagerly attempts expansion of text as a path. """ + """Only a PathExAttMap eagerly attempts expansion of text as a path.""" m = AttMap() k = random.choice(string.ascii_letters) with TmpEnv(**env): @@ -158,7 +158,7 @@ def test_non_PathExAttMap_preserves_all_variables(path, fetch, env): ) @pytest.mark.parametrize("fetch", [lambda m, k: m[k], lambda m, k: getattr(m, k)]) def test_url_expansion(path, expected, fetch): - """ URL expansion considers env vars but doesn't ruin slashes. """ + """URL expansion considers env vars but doesn't ruin slashes.""" key = "arbitrary" m = PathExAttMap({key: path}) assert expected == fetch(m, key) @@ -182,7 +182,7 @@ def test_url_expansion(path, expected, fetch): def test_multiple_syntax_path_expansion( varname, path_parts, var_idx, tmpdir, store, fetch ): - """ Test the different combinations of setting and retrieving an env var path. """ + """Test the different combinations of setting and retrieving an env var path.""" key = "arbitrary" parts = copy.copy(path_parts) env_var = "$" + varname diff --git a/tests/test_special_mutability.py b/tests/test_special_mutability.py index 0d53f48..105c2a0 100644 --- a/tests/test_special_mutability.py +++ b/tests/test_special_mutability.py @@ -16,49 +16,49 @@ @pytest.fixture(scope="function", params=["arbitrary", "random"]) def arb_key(request): - """ Provide arbitrary key name for a test case. """ + """Provide arbitrary key name for a test case.""" return request.param class UniversalMutabilityTests: - """ Tests of attmap behavior with respect to mutability """ + """Tests of attmap behavior with respect to mutability""" @staticmethod @pytest.fixture(scope="function", params=OLD_ATTMAPS) def m(request): - """ Provide a test case with a fresh empty data object. """ + """Provide a test case with a fresh empty data object.""" return get_att_map(request.param) def test_null_can_always_be_set_if_key_is_absent(self, arb_key, m): - """ When AttributeDict lacks a key, a null value can be set """ + """When AttributeDict lacks a key, a null value can be set""" assert arb_key not in m m[arb_key] = None assert arb_key in m assert m[arb_key] is None def test_empty_mapping_can_replace_nonempty(self, m, arb_key): - """ Regardless of specific type, an empty map can replace nonempty. """ + """Regardless of specific type, an empty map can replace nonempty.""" assert arb_key not in m m[arb_key] = {} assert arb_key in m def test_inserted_mapping_adopts_container_type(self, m, arb_key): - """ When mapping is inserted as value, it adopts its container's type. """ + """When mapping is inserted as value, it adopts its container's type.""" assert arb_key not in m m[arb_key] = {} assert isinstance(m[arb_key], m.__class__) class NullStorageTests: - """ Tests for behavior of a attmap w.r.t. storing null value """ + """Tests for behavior of a attmap w.r.t. storing null value""" @pytest.fixture(scope="function", params=NULLABLE_ATTMAPS) def m(self, request): - """ Provide a test case with a fresh empty data object. """ + """Provide a test case with a fresh empty data object.""" return get_att_map(request.param) def test_null_overwrites_existing(self, arb_key, m): - """ Verify that a null value can replace a non-null one. """ + """Verify that a null value can replace a non-null one.""" assert arb_key not in m arb_val = "placeholder" m[arb_key] = arb_val diff --git a/tests/test_to_dict.py b/tests/test_to_dict.py index 8f6af93..98b35c2 100644 --- a/tests/test_to_dict.py +++ b/tests/test_to_dict.py @@ -22,7 +22,7 @@ ], ) def test_to_dict_type(attmap_type, entries): - """ Validate to_dict result is a base dict and that contents match. """ + """Validate to_dict result is a base dict and that contents match.""" m = get_att_map(attmap_type, entries) assert type(m) is not dict d = m.to_dict() diff --git a/tests/test_to_disk.py b/tests/test_to_disk.py index a6cf365..59aba9c 100644 --- a/tests/test_to_disk.py +++ b/tests/test_to_disk.py @@ -33,7 +33,7 @@ def pytest_generate_tests(metafunc): - """ Dynamic test case generation and parameterization for this module """ + """Dynamic test case generation and parameterization for this module""" if "maptype" in metafunc.fixturenames: metafunc.parametrize("maptype", ALL_ATTMAPS) @@ -59,7 +59,7 @@ def check_lines(m, explines, obs_fun, parse, check): ["funcname", "exp"], [("get_yaml_lines", ["{}"]), ("to_yaml", "{}\n")] ) def test_empty(funcname, exp, maptype): - """ Verify behavior for YAML of empty attmap. """ + """Verify behavior for YAML of empty attmap.""" assert exp == getattr(maptype({}), funcname)() @@ -68,7 +68,7 @@ def test_empty(funcname, exp, maptype): [("get_yaml_lines", lambda ls: ls), ("to_yaml", lambda ls: ls.split("\n")[:-1])], ) def test_yaml(maptype, get_obs, parse_obs): - """ Tests for attmap repr as YAML lines or full text chunk. """ + """Tests for attmap repr as YAML lines or full text chunk.""" eq = lambda a, b: a == b seteq = lambda a, b: len(a) == len(b) and set(a) == set(b) checks = {OrdAttMap: eq, PathExAttMap: eq, AttMapEcho: eq, AttMap: seteq} @@ -78,7 +78,7 @@ def test_yaml(maptype, get_obs, parse_obs): @pytest.mark.parametrize("fmtlib", [YAML_NAME, JSON_NAME]) def test_disk_roundtrip(maptype, tmpdir, fmtlib): - """ Verify ability to parse, write, and reconstitute attmap. """ + """Verify ability to parse, write, and reconstitute attmap.""" m = make_data(ENTRIES, maptype) fp = tmpdir.join("disked_attmap.out").strpath assert not os.path.exists(fp) @@ -95,7 +95,7 @@ def test_disk_roundtrip(maptype, tmpdir, fmtlib): ["args", "exp_newl_end"], [(tuple(), True), ((False,), False), ((True,), True)] ) def test_yaml_newline(args, exp_newl_end, maptype): - """ Map to_yaml adds newline by default but respect argument. """ + """Map to_yaml adds newline by default but respect argument.""" data = {"randkey": "arbval"} assert maptype(data).to_yaml(*args).endswith("\n") is exp_newl_end @@ -130,7 +130,7 @@ def test_yaml_newline(args, exp_newl_end, maptype): ], ) def test_disk_path_expansion(tmpdir, data, env_var, fmtlib, exp_res, maptype): - """ Paths are not expanded when map goes to disk. """ + """Paths are not expanded when map goes to disk.""" # Pretests assert len(data) == 1, "To isolate focus, use just 1 item; got {} -- {}".format( diff --git a/tests/test_to_map.py b/tests/test_to_map.py index 3d87207..a9549e0 100644 --- a/tests/test_to_map.py +++ b/tests/test_to_map.py @@ -21,7 +21,7 @@ @pytest.fixture(scope="function") def entries(): - """ Basic data for a test case """ + """Basic data for a test case""" return copy.deepcopy( { "arb_key": "text", @@ -38,18 +38,18 @@ def entries(): @pytest.fixture(scope="function") def exp_num_raw(entries): - """ Expected number of entries """ + """Expected number of entries""" return _get_num_raw(entries) @pytest.fixture(scope="function") def am(attmap_type, entries): - """ Prepopulated attribute mapping of a particular subtype """ + """Prepopulated attribute mapping of a particular subtype""" return get_att_map(attmap_type, entries) def test_type_conversion_completeness(am, attmap_type, exp_num_raw): - """ Each nested mapping should be converted. """ + """Each nested mapping should be converted.""" assert type(am) is attmap_type num_subtypes = _tally_types(am, AttMap) assert exp_num_raw == num_subtypes @@ -60,7 +60,7 @@ def test_type_conversion_completeness(am, attmap_type, exp_num_raw): def test_correct_size(am, entries): - """ The transformed mapping should have its original size. """ + """The transformed mapping should have its original size.""" assert len(am) == len(entries), "{} entries in attmap and {} in raw data".format( len(am), len(entries) ) @@ -70,7 +70,7 @@ def test_correct_size(am, entries): def test_correct_keys(am, entries): - """ Keys should be unaltered by the mapping type upcasting. """ + """Keys should be unaltered by the mapping type upcasting.""" def text_keys(m): return ", ".join(m.keys()) @@ -87,7 +87,7 @@ def check(m, name): def test_values_equivalence(am, entries): - """ Raw values should be equivalent. """ + """Raw values should be equivalent.""" def check(v1, v2): return ( From 9b091313ee6e3ee55994ac285cbcca72c5fb976a Mon Sep 17 00:00:00 2001 From: Michal Stolarczyk Date: Tue, 15 Jun 2021 13:12:16 -0400 Subject: [PATCH 07/10] remove obsolete imports --- attmap/_att_map_like.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/attmap/_att_map_like.py b/attmap/_att_map_like.py index 1611fc4..63fb95a 100644 --- a/attmap/_att_map_like.py +++ b/attmap/_att_map_like.py @@ -2,9 +2,6 @@ import abc import sys -from typing import Generator, Iterator - -import pandas if sys.version_info < (3, 3): from collections import Mapping, MutableMapping From 906a4789fdadb8b1dc8d7efebd90f49958e283d0 Mon Sep 17 00:00:00 2001 From: nsheff Date: Thu, 4 Nov 2021 13:49:44 -0400 Subject: [PATCH 08/10] update codecov, separate test workflows --- .github/workflows/run-codecov.yml | 21 +++++++++++++++++++++ .github/workflows/run-pytest.yml | 20 +++++++------------- 2 files changed, 28 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/run-codecov.yml diff --git a/.github/workflows/run-codecov.yml b/.github/workflows/run-codecov.yml new file mode 100644 index 0000000..a41a1fd --- /dev/null +++ b/.github/workflows/run-codecov.yml @@ -0,0 +1,21 @@ +name: Run codecov + +on: + pull_request: + branches: [master] + +jobs: + pytest: + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.9] + os: [ubuntu-latest] + + steps: + - uses: actions/checkout@v2 + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v2 + with: + file: ./coverage.xml + name: py-${{ matrix.python-version }}-${{ matrix.os }} diff --git a/.github/workflows/run-pytest.yml b/.github/workflows/run-pytest.yml index 3bbdd24..1fc5e3b 100644 --- a/.github/workflows/run-pytest.yml +++ b/.github/workflows/run-pytest.yml @@ -2,17 +2,17 @@ name: Run pytests on: push: - branches: [master, dev] + branches: [dev] pull_request: - branches: [master, dev] + branches: [master] jobs: pytest: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] - os: [ubuntu-latest, macos-latest] + python-version: [3.6, 3.9] + os: [ubuntu-latest] steps: - uses: actions/checkout@v2 @@ -22,20 +22,14 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install dev dependancies + - name: Install dev dependencies run: if [ -f requirements/requirements-dev.txt ]; then pip install -r requirements/requirements-dev.txt; fi - - name: Install test dependancies + - name: Install test dependencies run: if [ -f requirements/requirements-test.txt ]; then pip install -r requirements/requirements-test.txt; fi - - name: Install yacman + - name: Install attmap run: python -m pip install . - name: Run pytest tests run: pytest tests --cov=./ --cov-report=xml - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - file: ./coverage.xml - name: py-${{ matrix.python-version }}-${{ matrix.os }} From 75dd841b7fecbd3ab58b32688c18c2c825e7e7d7 Mon Sep 17 00:00:00 2001 From: nsheff Date: Thu, 4 Nov 2021 13:50:20 -0400 Subject: [PATCH 09/10] lint only on PR --- .github/workflows/black.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index f58e4c6..8b48ddf 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -1,6 +1,6 @@ name: Lint -on: [push, pull_request] +on: [pull_request] jobs: lint: From c4fc7903d727d918bd0b014ba0a523326b8a7644 Mon Sep 17 00:00:00 2001 From: nsheff Date: Thu, 4 Nov 2021 20:37:35 -0400 Subject: [PATCH 10/10] version bump, changelog --- attmap/_version.py | 2 +- docs/changelog.md | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/attmap/_version.py b/attmap/_version.py index bb8bac6..7e0dc0e 100644 --- a/attmap/_version.py +++ b/attmap/_version.py @@ -1 +1 @@ -__version__ = "0.13.1-dev" +__version__ = "0.13.1" diff --git a/docs/changelog.md b/docs/changelog.md index 5fef316..1e964c0 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,11 @@ # Changelog -## [0.13.1] - unreleased +## [0.13.1] - 2021-11-04 ### Added -- `_excl_classes_from_todict`, which can be used to list classes to be exluded from the `dict` representation +- `_excl_classes_from_todict`, which can be used to list classes to be excluded from the `dict` representation + +### Fixed +- A bug that caused double-backslashes ## [0.13.0] - 2021-02-22 ### Added