Skip to content

Commit

Permalink
feat: first implementation of the library (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
clintval authored Oct 17, 2023
1 parent a73ba77 commit eab7bc9
Show file tree
Hide file tree
Showing 10 changed files with 688 additions and 2 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.
24 changes: 24 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Test the Library

on:
- push

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

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install poetry tox tox-gh-actions
- name: Test with tox
run: tox
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.idea/
*.iml

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down
5 changes: 5 additions & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
3.12
3.11
3.10
3.9
3.8
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Clint Valentine
Copyright © 2023 Clint Valentine

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
# caseless
A caseless typed dictionary in Python

[![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/)
[![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.

```console
pip install caseless
```

![Guitar Lake, California](.github/img/cover.jpg)

```python
from caseless import CaselessDict

CaselessDict({"lower": "UPPER"})["LOWER"] == "UPPER"
CaselessDict({"lower": "UPPER"}).get("LOWER") == "UPPER"
CaselessDict({"lower": "value"}) == CaselessDict({"LOWER": "value"})
```
145 changes: 145 additions & 0 deletions caseless/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from sys import getsizeof, maxsize
from typing import Any
from typing import Collection
from typing import Dict
from typing import Hashable
from typing import ItemsView
from typing import Iterator
from typing import KeysView
from typing import List
from typing import Mapping
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
from typing import ValuesView
from typing import cast

K = TypeVar("K", bound=Hashable)
V = TypeVar("V")


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

def __init__(
self, *args: Union[Mapping[K, V], Collection[Tuple[K, V]]], **kwargs: Mapping[K, V]
) -> None:
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, str)
}
self._hash: int = -1

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, str) and key.lower() in self._caseless)
else key in self._map
)

def __copy__(self) -> "CaselessDict":
"""Return a shallow copy of this mapping."""
return type(self)(self.items())

def __eq__(self, other: Any) -> bool:
"""Test if <other> is equal to this class instance."""
return (
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, str) and key.lower() in self._caseless:
return self._map[cast(K, self._caseless[key.lower()])]
else:
return self._map[key]

def __hash__(self) -> int:
"""Return a hash of this dictionary using all key-value pairs."""
if self._hash == -1 and self:
current: int = 0
for (key, value) in self.items():
if isinstance(key, str):
current ^= hash((key.lower(), value))
else:
current ^= hash((key, value))
current ^= maxsize
self._hash = current
return self._hash

def __iter__(self) -> Iterator[K]:
"""Return an iterator over the keys."""
return iter(self._map.keys())

def __len__(self) -> int:
"""Return the length of the mapping."""
return len(self._map)

def __ne__(self, other: Any) -> bool:
return not self == other

def __nonzero__(self) -> bool:
"""Test if this mapping is of non-zero length."""
return bool(self._map)

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

def __repr__(self) -> str:
"""Return a representation of this class instance."""
return f"{self.__class__.__qualname__}({repr(self._map)})"

def __sizeof__(self) -> int:
"""Return the size of this class instance."""
return getsizeof(self._map)

def __str__(self) -> str:
"""Return a string representation of this class."""
return self.__repr__()

@classmethod
def fromkeys(cls, keys: Collection[K], default: V) -> "CaselessDict":
"""Build a mapping from a set of keys with a default value."""
return cls([(key, default) for key in keys])

def copy(self, mapping: Optional[Dict[K, V]] = None) -> "CaselessDict":
"""Return a shallow copy of this mapping."""
overrides: Dict[K, V] = {}
if mapping is not None:
for k, v in mapping.items():
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, str) and key.lower() in self._caseless:
caseless_key: K = cast(K, self._caseless[key.lower()])
return self._map.get(caseless_key, default)
else:
return self._map.get(key, default)

def items(self) -> ItemsView[K, V]:
"""Return this mapping as a list of paired key-values."""
return self._map.items()

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 a key-value pair."""
return self.copy({key: value})

def values(self) -> ValuesView[V]:
"""Return the values in insertion order."""
return self._map.values()
Loading

0 comments on commit eab7bc9

Please sign in to comment.