Skip to content

Commit

Permalink
feat: fixup all tests so the library works!
Browse files Browse the repository at this point in the history
  • Loading branch information
clintval committed Oct 17, 2023
1 parent 90d20a6 commit 0c4ff29
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 257 deletions.
Binary file added .github/img/cover.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ name: Test the Library

on:
- push
- pull_request

jobs:
TestAndLint:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v3
Expand Down
7 changes: 4 additions & 3 deletions .python-version
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
3.8
3.9
3.10
3.12
3.11
3.10
3.9
3.8
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
# caseless

[![Testing Status](https://github.com/clintval/caseless/actions/workflows/test.yml/badge.svg)](https://github.com/clintval/caseless/actions/workflows/test.yml)
[![Build Status](https://github.com/clintval/caseless/actions/workflows/test.yml/badge.svg)](https://github.com/clintval/caseless/actions/workflows/test.yml)
[![PyPi Release](https://badge.fury.io/py/caseless.svg)](https://badge.fury.io/py/caseless)
[![Python Versions](https://img.shields.io/pypi/pyversions/caseless.svg)](https://pypi.python.org/pypi/caseless/)
[![MyPy Checked](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

A caseless typed dictionary in Python
A caseless typed dictionary in Python.

```console
pip install caseless
```

Features:

- Caseless key matching
- Typing for support in typed codebases
![Guitar Lake, California](.github/img/cover.jpg)

```python
from caseless import CaselessDict

CaselessDict({"lower": "my-value"}) == CaselessDict({"LOWER": "my-value"})
CaselessDict({"lower": "UPPER"})["LOWER"] == "UPPER"
CaselessDict({"lower": "UPPER"}).get("LOWER") == "UPPER"
CaselessDict({"lower": "value"}) == CaselessDict({"LOWER": "value"})
```
33 changes: 16 additions & 17 deletions caseless/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from sys import getsizeof, maxsize
from typing import Any
from typing import ClassVar
from typing import Collection
from typing import Dict
from typing import Hashable
Expand All @@ -20,25 +19,24 @@
K = TypeVar("K", bound=Hashable)
V = TypeVar("V")


class CaselessDict(Mapping[K, V]):
"""A dictionary with case-insensitive string getters."""

underlying: ClassVar[Type[Dict]] = dict

def __init__(
self, *args: Union[Mapping[K, V], Collection[Tuple[K, V]]], **kwargs: Mapping[K, V]
) -> None:
self._map: Dict[K, V] = self.underlying(*args, **kwargs)
self._map: Dict[K, V] = dict(*args, **kwargs)
self._caseless: Dict[str, str] = {
k.lower(): k for k, v in self._map.items() if isinstance(k, (bytes, str))
k.lower(): k for k, v in self._map.items() if isinstance(k, str)
}
self._hash: int = -1

def __contains__(self, key: K) -> bool:
def __contains__(self, key: object) -> bool:
"""Test if <key> is contained within this mapping."""
return (
self._caseless[key.lower()] in self._map
if (isinstance(key, (bytes, str)) and key.lower() in self._caseless)
if (isinstance(key, str) and key.lower() in self._caseless)
else key in self._map
)

Expand All @@ -49,14 +47,15 @@ def __copy__(self) -> "CaselessDict":
def __eq__(self, other: Any) -> bool:
"""Test if <other> is equal to this class instance."""
return (
(hash(self) == hash(other)) & (self.items() == other.items())
if (isinstance(other, type(self)) and hasattr(other, "__hash__"))
else False
isinstance(other, type(self))
and hasattr(other, "__hash__")) and (hash(self) == hash(other)
and hasattr(other, "__len__") and len(self) == len(other)
and all([key in other and other[key] == value for key, value in self.items()])
)

def __getitem__(self, key: K) -> Any:
"""Return a value indexed with <key>."""
if isinstance(key, (bytes, str)) and key.lower() in self._caseless:
if isinstance(key, str) and key.lower() in self._caseless:
return self._map[cast(K, self._caseless[key.lower()])]
else:
return self._map[key]
Expand All @@ -66,7 +65,7 @@ def __hash__(self) -> int:
if self._hash == -1 and self:
current: int = 0
for (key, value) in self.items():
if isinstance(key, (bytes, str)):
if isinstance(key, str):
current ^= hash((key.lower(), value))
else:
current ^= hash((key, value))
Expand All @@ -91,7 +90,7 @@ def __nonzero__(self) -> bool:

def __reduce__(self) -> Tuple[Type["CaselessDict"], Tuple[List[Tuple[K, V]]]]:
"""Return a recipe for pickling."""
return (type(self), (list(self.items()),))
return type(self), (list(self.items()),)

def __repr__(self) -> str:
"""Return a representation of this class instance."""
Expand All @@ -115,15 +114,15 @@ def copy(self, mapping: Optional[Dict[K, V]] = None) -> "CaselessDict":
overrides: Dict[K, V] = {}
if mapping is not None:
for k, v in mapping.items():
if isinstance(k, (bytes, str)) and k.lower() in self._caseless:
if isinstance(k, str) and k.lower() in self._caseless:
overrides[cast(K, self._caseless[k.lower()])] = v
else:
overrides[k] = v
return type(self)((list(self.items())) + list(overrides.items()))

def get(self, key: K, default: Optional[Any] = None) -> Union[Any, V]:
"""Return a value indexed with <key> but if that key is not present, return <default>."""
if isinstance(key, (bytes, str)) and key.lower() in self._caseless:
if isinstance(key, str) and key.lower() in self._caseless:
caseless_key: K = cast(K, self._caseless[key.lower()])
return self._map.get(caseless_key, default)
else:
Expand All @@ -133,12 +132,12 @@ def items(self) -> ItemsView[K, V]:
"""Return this mapping as a list of paired key-values."""
return self._map.items()

def keys(self) -> KeysView[Hashable]:
def keys(self) -> KeysView[K]:
"""Return the keys in insertion order."""
return self._map.keys()

def updated(self, key: K, value: V) -> "CaselessDict":
"""Return a shallow copy of this mapping with an key-value pair."""
"""Return a shallow copy of this mapping with a key-value pair."""
return self.copy({key: value})

def values(self) -> ValuesView[V]:
Expand Down
Loading

0 comments on commit 0c4ff29

Please sign in to comment.