Skip to content

Commit

Permalink
SIANXKE-114: Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
beachmachine committed Sep 28, 2021
0 parents commit a82a084
Show file tree
Hide file tree
Showing 15 changed files with 939 additions and 0 deletions.
41 changes: 41 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Publish package
on:
release:
types: [created]

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
architecture: 'x64'

- name: Install dependencies and package
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Build source and binary distribution package
run: |
python setup.py sdist bdist_wheel
env:
PACKAGE_VERSION: ${{ github.ref }}

- name: Check distribution package
run: |
twine check dist/*
- name: Publish distribution package
run: |
twine upload dist/*
env:
TWINE_REPOSITORY: ${{ secrets.PYPI_REPOSITORY }}
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
TWINE_NON_INTERACTIVE: yes
43 changes: 43 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Run linter and tests
on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version:
- "3.6"
- "3.7"
- "3.8"
- "3.9"

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 dependencies and package
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 './deepcompare' --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 './deepcompare' --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Run tests
run: |
# run tests with coverage
coverage run --source='./deepcompare' -m pytest
coverage xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.idea
.vscode
.env
.DS_Store
.pytest_cache
.coverage
coverage.xml
*.egg-info
*.pyc
*.pyo
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2021-09-27
### Added
- Method to deep compare data structures containing `dict`, `list` and `tuple` types.
- Method to partially deep compare data structures containing `dict`, `list` and `tuple` types.

[Unreleased]: https://github.com/anexia-it/python-deepcompare/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/anexia-it/python-deepcompare/releases/tag/v1.0.0
37 changes: 37 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Guidance on how to contribute

> By submitting a pull request or filing a bug, issue, or feature request,
> you are agreeing to comply with this waiver of copyright interest.
> Details can be found in our [LICENSE](LICENSE).

There are two primary ways to help:
- Using the issue tracker, and
- Changing the code-base.


## Using the issue tracker

Use the issue tracker to suggest feature requests, report bugs, and ask questions.
This is also a great way to connect with the developers of the project as well
as others who are interested in this solution.

Use the issue tracker to find ways to contribute. Find a bug or a feature, mention in
the issue that you will take on that effort, then follow the _Changing the code-base_
guidance below.


## Changing the code-base

Generally speaking, you should fork this repository, make changes in your
own fork, and then submit a pull request. All new code should have associated
unit tests that validate implemented features and the presence or lack of defects.
Additionally, the code should follow any stylistic and architectural guidelines
prescribed by the project. In the absence of such guidelines, mimic the styles
and patterns in the existing code-base.

### Contribution guidelines
- Your code should follow PEP 8 -- Style Guide for Python Code
- Your changes should be covered by unit-tests
- If you add a unit-test within the `test_compare_flat.py`, make sure to add the equivalent test to the `test_partial_compare_flat.py` (and vice-versa).
- If you add a unit-test within the `test_compare_deep.py`, make sure to add the equivalent test to the `test_partial_compare_deep.py` (and vice-versa).
22 changes: 22 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
The MIT License (MIT)

Copyright (c) 2021 ANEXIA Internetdienstleisungs GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
python-deepcompare
==================

[![PyPI](https://badge.fury.io/py/python-deepcompare.svg)](https://pypi.org/project/python-deepcompare/)
[![Test Status](https://github.com/anexia-it/python-deepcompare/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/anexia-it/python-deepcompare/actions/workflows/test.yml)
[![Codecov](https://codecov.io/gh/anexia-it/python-deepcompare/branch/master/graph/badge.svg)](https://codecov.io/gh/anexia-it/python-deepcompare)

`python-deepcompare` is a library to deep compare data structures with each other. It can check if two data
structures contain the same data, or if a data structure is a subset of another data structure. The library
supports `Sequence` (e.g. `list` or `tuple`) and `Mapping` (e.g. `dict`) types for the deep comparison.

# Installation

With a [correctly configured](https://pipenv.pypa.io/en/latest/basics/#basic-usage-of-pipenv) `pipenv` toolchain:

```sh
pipenv install python-deepcompare
```

You may also use classic `pip` to install the package:

```sh
pip install python-deepcompare
```

# Getting started

## How it works
- As a default, the comparison treats all `Sequence` and all `Mapping` types the same (e.g. `(1, 2, 3)` is equal to
`[1, 2, 3]`). To enable strict type checks, use the `strict` keyword argument.
- The `partial_compare` method checks if the data structure given as the second parameter is a subset of the data
structure given as the first parameter.
- For `Mapping` types this means, that all keys of the second data structure are also keys on the first data
structure, and the values of the keys are also equal (e.g. `{'a': 1, 'b': 2}` is a subset
of `{'a': 1, 'b': 2, 'c': 3}`, but `{'a': 1, 'b': 2, 'd': 4}` is not).
- For `Sequence` types this means, that all values of the second data structure are also values of the first data
structure, and the values are in the same order (e.g. `[1, 3, 5]` is a subset
of `[1, 2, 3, 4, 5]`, but `[1, 5, 3]` is not).

## Usage

```python
import deepcompare

# test if two data structures are equal, but the types to not need to match exactly
deepcompare.compare(
{'key1': (1, 2, 3), 'key2': {'key3': [4, 5, 6]}},
{'key1': [1, 2, 3], 'key2': {'key3': (4, 5, 6)}},
) # returns: True

# test if two data structures are equal, and make sure the types match exactly
deepcompare.compare(
{'key1': (1, 2, 3), 'key2': {'key3': [4, 5, 6]}},
{'key1': [1, 2, 3], 'key2': {'key3': (4, 5, 6)}},
strict=True,
) # returns: False

# test if the second data structure is contained within the first, but
# the types to not need to match exactly
deepcompare.partial_compare(
{'key1': (1, 2, 3), 'key2': {'key3': [4, 5, 6]}, 'key4': True},
{'key1': [1, 2], 'key2': {'key3': (4, 6)}},
) # returns: True

# test if the second data structure is contained within the first, and
# make sure the types match exactly
deepcompare.partial_compare(
{'key1': (1, 2, 3), 'key2': {'key3': [4, 5, 6]}, 'key4': True},
{'key1': [1, 2], 'key2': {'key3': (4, 6)}},
strict=True,
) # returns: False
```

# List of developers

* Andreas Stocker <[email protected]>, Lead Developer
119 changes: 119 additions & 0 deletions deepcompare/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from collections import abc
from typing import Any

__all__ = [
'compare',
'partial_compare',
]

_LIST_TYPES = (abc.Sequence, )
_DICT_TYPES = (abc.Mapping, )


def compare(haystack: Any, subset: Any, strict: bool = False) -> bool:
"""
Deep compare the data structure given as `haystack` with the data structure given as `subset`. This method
descends `Sequence` and `Mapping` types. Use the `strict` parameter to make sure that the `haystack` and `subset`
values are of the same type.
:param haystack: The data structure used as a comparison reference
:param subset: The data structure compared to the haystack
:param strict: Strict data type matching
:return: If the second parameter is equal to the first parameter
"""
return _compare(haystack, subset, False, strict)


def partial_compare(haystack: Any, subset: Any, strict: bool = False) -> bool:
"""
Deep compare the data structure given as `haystack` with the data structure given as `subset`. This method
descends `Sequence` and `Mapping` types. This method checks if the data structure given as `subset` as actually a
subset of the data structure given as `haystack`. Use the `strict` parameter to make sure that the `haystack`
and `subset` values are of the same type.
:param haystack: The data structure used as a comparison reference
:param subset: The data structure compared to the haystack
:param strict: Strict data type matching
:return: If the second parameter is a subset of the first parameter
"""
return _compare(haystack, subset, True, strict)


def _compare(haystack: Any, subset: Any, partial: bool, strict: bool) -> bool:
"""
Deep compare the data structure given as `haystack` with the data structure given as `subset`. This method
descends `Sequence` and `Mapping` types.
:param haystack: The data structure used as a comparison reference
:param subset: The data structure compared to the haystack
:param partial: Subset data comparison
:param strict: Strict data type matching
:return: If the second parameter is equal to, or a subset of the first parameter
"""
if strict:
# check type if we are working on strict mode
if not issubclass(type(haystack), type(subset)):
return False

# if we compare compare two dict types, we check each key of the haystack object to be equal to the
# subset object. if we are working in partial mode, we ignore if some keys are missing on the subset object.
# however we check if all keys of the subset object are existing on the haystack object.
if isinstance(haystack, _DICT_TYPES) and isinstance(subset, _DICT_TYPES):
return _compare_mapping(haystack, subset, partial, strict)

# if we compare compare two list types, we check each value of the haystack object to be equal to the
# subset object. if we are working in partial mode, we ignore if the subset list is shorter than the haystack list.
elif isinstance(haystack, _LIST_TYPES) and isinstance(subset, _LIST_TYPES):
return _compare_sequence(haystack, subset, partial, strict)

# for any other type, we just compare the two values.
else:
return haystack == subset


def _compare_mapping(haystack: abc.Mapping, subset: abc.Mapping, partial: bool, strict: bool) -> bool:
# check if all keys of the subset are also on the haystack object
for key in subset.keys():
if key not in haystack:
return False

# check and compare each value of the haystack to the corresponding value on the subset object
for key in haystack.keys():
# ignore missing keys on subset if we are in partial mode
if partial and key not in subset:
continue

elif key not in subset:
return False

if not _compare(haystack[key], subset[key], partial, strict):
return False

return True


def _compare_sequence(haystack: abc.Sequence, subset: abc.Sequence, partial: bool, strict: bool) -> bool:
haystack_slice = haystack[:]

# if we do not partially compare the lists, we need to check if the lengths of the two lists to compare are
# equal.
if not partial and len(haystack) != len(subset):
return False

for subset_value in subset:
haystack_slice_index = 0

# find the index of the first value in the haystack slice list that equals to the current value of the
# subset list. if the haystack slice list does not contain the value of the subset, the lists are not equal.
for haystack_value in haystack_slice:
haystack_slice_index += 1

if _compare(haystack_value, subset_value, partial, strict):
break
else:
return False

# reduce the haystack slice list to the values that have not been compared yet.
haystack_slice = haystack_slice[haystack_slice_index:]

return True
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[build-system]
requires = [
"setuptools>=58",
"wheel",
]
build-backend = "setuptools.build_meta"

10 changes: 10 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Package and package dependencies
-e .

# Development dependencies
pytest>=6.2,<6.3
flake8>=3.9,<3.10
codecov>=2.1,<2.2
setuptools>=42
wheel>=0.37
twine>=3.4
Loading

0 comments on commit a82a084

Please sign in to comment.