-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a82a084
Showing
15 changed files
with
939 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.