Skip to content

Commit

Permalink
Increase test coverage. (#373)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
jackton1 and pre-commit-ci[bot] authored Jun 19, 2021
1 parent 73cf773 commit 17a4d37
Show file tree
Hide file tree
Showing 9 changed files with 379 additions and 93 deletions.
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ exclude_lines =
if settings.DEBUG
# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise StopIteration
raise NotImplementedError
except ImportError
except IntegrityError
Expand Down
54 changes: 54 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Upload Python Package

on:
release:
types: [created]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0

- name: Run semver-diff
id: semver-diff
uses: tj-actions/[email protected]

- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.6.x'

- name: Upgrade pip
run: |
pip install -U pip
- name: Install dependencies
run: make install-deploy

- name: Setup git
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
- name: bumpversion
run: |
make increase-version PART="${{ steps.semver-diff.outputs.release_type }}"
- name: Build and publish
run: make release
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}

- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
base: "main"
title: "Upgraded ${{ steps.semver-diff.outputs.old_version }} → ${{ steps.semver-diff.outputs.new_version }}"
branch: "chore/upgrade-${{ steps.semver-diff.outputs.old_version }}-to-${{ steps.semver-diff.outputs.new_version }}"
commit-message: "Upgraded from ${{ steps.semver-diff.outputs.old_version }} → ${{ steps.semver-diff.outputs.new_version }}"
body: "View [CHANGES](https://github.com/${{ github.repository }}/compare/${{ steps.semver-diff.outputs.old_version }}...${{ steps.semver-diff.outputs.new_version }})"
token: ${{ secrets.PAT_TOKEN }}
75 changes: 44 additions & 31 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,48 @@ guard-%: ## Checks that env var is set else exits with non 0 mainly used in CI;
# --------------------------------------------------------
# ------- Python package (pip) management commands -------
# --------------------------------------------------------
clean-build: ## Clean project build artifacts.
@echo "Removing build assets..."
@$(PYTHON) setup.py clean
@rm -rf build/
@rm -rf dist/
@rm -rf *.egg-info

install: clean-build ## Install project dependencies.
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts

clean-build: ## remove build artifacts
@rm -fr build/
@rm -fr dist/
@rm -fr .eggs/
@find . -name '*.egg-info' -exec rm -fr {} +
@find . -name '*.egg' -exec rm -f {} +

clean-pyc: ## remove Python file artifacts
@find . -name '*.pyc' -exec rm -f {} +
@find . -name '*.pyo' -exec rm -f {} +
@find . -name '*~' -exec rm -f {} +
@find . -name '__pycache__' -exec rm -fr {} +

clean-test: ## remove test and coverage artifacts
@rm -fr .tox/
@rm -f .coverage
@rm -fr htmlcov/
@rm -fr .pytest_cache

install-wheel: ## Install wheel
@echo "Installing wheel..."
@pip install wheel

install: clean requirements.txt install-wheel ## Install project dependencies.
@echo "Installing project in dependencies..."
@$(PYTHON_PIP) install -r requirements.txt

install-lint: clean-build ## Install lint extra dependencies.
install-lint: clean setup.py install-wheel ## Install lint extra dependencies.
@echo "Installing lint extra requirements..."
@$(PYTHON_PIP) install -e .'[lint]'

install-test: clean-build clean-test-all ## Install test extra dependencies.
install-test: clean setup.py install-wheel ## Install test extra dependencies.
@echo "Installing test extra requirements..."
@$(PYTHON_PIP) install -e .'[test]'

install-dev: clean-build ## Install development extra dependencies.
install-deploy: clean setup.py install-wheel ## Install deploy extra dependencies.
@echo "Installing deploy extra requirements..."
@$(PYTHON_PIP) install -e .'[deploy]'

install-dev: clean setup.py install-wheel ## Install development extra dependencies.
@echo "Installing development requirements..."
@$(PYTHON_PIP) install -e .'[development]' -r requirements.txt

Expand Down Expand Up @@ -80,27 +102,18 @@ test:
@$(MANAGE_PY) test

# ----------------------------------------------------------
# ---------- Upgrade project version (bumpversion) --------
# ---------- Release the project to PyPI -------------------
# ----------------------------------------------------------
increase-version: clean-build guard-PART ## Bump the project version (using the $PART env: defaults to 'patch').
@git checkout main
@echo "Increasing project '$(PART)' version..."
@$(PYTHON_PIP) install -q -e .'[deploy]'
@bumpversion --verbose $(PART)
@git-changelog . > CHANGELOG.md
@git add .
@[ -z "`git status --porcelain`" ] && echo "No changes found." || git commit -am "Updated CHANGELOG.md."

release-to-pypi: makemessages compilemessages increase-version ## Release project to pypi
@$(PYTHON_PIP) install -U twine
@$(PYTHON) setup.py sdist bdist_wheel
@twine upload -r pypi dist/*
@git-changelog . > CHANGELOG.md
@git add .
@[ -z "`git status --porcelain`" ] && echo "No changes found." || git commit -am "Updated CHANGELOG.md."
@git pull
@git push
@git push --tags
increase-version: guard-PART ## Increase project version
@bump2version $(PART)
@git switch -c main

dist: clean install-deploy ## builds source and wheel package
@pip install twine==3.4.1
@python setup.py sdist bdist_wheel

release: dist ## package and upload a release
@twine upload dist/*

# ----------------------------------------------------------
# --------- Run project Test -------------------------------
Expand Down
6 changes: 5 additions & 1 deletion django_clone/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
},
"replica": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "replica.sqlite3"),
},
}

# Password validation
Expand Down
103 changes: 61 additions & 42 deletions model_clone/mixins/clone.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import itertools
from itertools import repeat
from typing import Dict, List, Optional

Expand Down Expand Up @@ -154,7 +155,7 @@ def check(cls, **kwargs):
"Please provide either "
+ '"_clone_m2o_or_o2m_fields"'
+ " or "
+ '"_clone_excluded_m2o_or_o2m_fields" for {}'.format(
+ '"_clone_excluded_m2o_or_o2m_fields" for model {}'.format(
cls.__name__
)
),
Expand All @@ -169,7 +170,9 @@ def check(cls, **kwargs):
"Conflicting configuration.",
hint=(
'Please provide either "_clone_o2o_fields"'
+ ' or "_clone_excluded_o2o_fields" for {}'.format(cls.__name__)
+ ' or "_clone_excluded_o2o_fields" for model {}'.format(
cls.__name__
)
),
obj=cls,
id="{}.E002".format(ModelCloneConfig.name),
Expand Down Expand Up @@ -197,7 +200,7 @@ def make_clone(self, attrs=None, sub_clone=False, using=None):
:type using: str
:return: The model instance that has been cloned.
"""
using = self._state.db or self.__class__._default_manager.db
using = using or self._state.db or self.__class__._default_manager.db
attrs = attrs or {}
if not self.pk:
raise ValidationError(
Expand All @@ -206,8 +209,8 @@ def make_clone(self, attrs=None, sub_clone=False, using=None):
)
)
if sub_clone:
duplicate = self
duplicate.pk = None
duplicate = self # pragma: no cover
duplicate.pk = None # pragma: no cover
else:
duplicate = self._create_copy_of_instance(self, using=using)

Expand All @@ -216,15 +219,17 @@ def make_clone(self, attrs=None, sub_clone=False, using=None):

duplicate.save(using=using)

duplicate = self.__duplicate_o2o_fields(duplicate, using=using)
duplicate = self.__duplicate_o2m_fields(duplicate)
duplicate = self.__duplicate_m2o_fields(duplicate, using=using)
duplicate = self.__duplicate_o2o_fields(duplicate, using=using)
duplicate = self.__duplicate_o2m_fields(duplicate, using=using)
duplicate = self.__duplicate_m2m_fields(duplicate, using=using)

return duplicate

def bulk_clone(self, count, attrs=None, batch_size=None, auto_commit=False):
using = self._state.db or self.__class__._default_manager.db
def bulk_clone(
self, count, attrs=None, batch_size=None, using=None, auto_commit=False
):
using = using or self._state.db or self.__class__._default_manager.db
ops = connections[using].ops
objs = range(count)
clones = []
Expand Down Expand Up @@ -366,7 +371,6 @@ def _create_copy_of_instance(instance, using=None, force=False, sub_clone=False)

def __duplicate_o2o_fields(self, duplicate, using=None):
"""Duplicate one to one fields.
:param duplicate: The transient instance that should be duplicated.
:type duplicate: `django.db.models.Model`
:param using: The database alias used to save the created instances.
Expand Down Expand Up @@ -396,16 +400,19 @@ def __duplicate_o2o_fields(self, duplicate, using=None):

return duplicate

def __duplicate_o2m_fields(self, duplicate):
def __duplicate_o2m_fields(self, duplicate, using=None):
"""Duplicate one to many fields.
:param duplicate: The transient instance that should be duplicated.
:type duplicate: `django.db.models.Model`
:return: The duplicate instance with all the one to many fields duplicated.
:param using: The database alias used to save the created instances.
:type using: str
:return: The duplicate instance with all the transcient one to many duplicated instances.
"""
fields = set()

for f in self._meta.related_objects:
for f in itertools.chain(
self._meta.related_objects, self._meta.concrete_fields
):
if f.one_to_many:
if any(
[
Expand All @@ -415,17 +422,30 @@ def __duplicate_o2m_fields(self, duplicate):
not in self._clone_excluded_m2o_or_o2m_fields,
]
):
fields.add(f)
for item in getattr(self, f.get_accessor_name()).all():
if hasattr(item, "make_clone"):
try:
item.make_clone(
attrs={f.remote_field.name: duplicate},
using=using,
)
except IntegrityError:
item.make_clone(
attrs={f.remote_field.name: duplicate},
save_new=False,
sub_clone=True,
using=using,
)
else:
new_item = CloneMixin._create_copy_of_instance(
item,
force=True,
sub_clone=True,
using=using,
)
setattr(new_item, f.remote_field.name, duplicate)

# Clone one to many fields
for field in fields:
for item in getattr(self, field.get_accessor_name()).all():
try:
item.make_clone(attrs={field.remote_field.name: duplicate})
except IntegrityError:
item.make_clone(
attrs={field.remote_field.name: duplicate}, sub_clone=True
)
new_item.save(using=using)

return duplicate

Expand All @@ -438,8 +458,6 @@ def __duplicate_m2o_fields(self, duplicate, using=None):
:type using: str
:return: The duplicate instance with all the many to one fields duplicated.
"""
fields = set()

for f in self._meta.concrete_fields:
if f.many_to_one:
if any(
Expand All @@ -449,21 +467,17 @@ def __duplicate_m2o_fields(self, duplicate, using=None):
and f.name not in self._clone_excluded_m2o_or_o2m_fields,
]
):
fields.add(f)

# Clone many to one fields
for field in fields:
item = getattr(self, field.name)
if hasattr(item, "make_clone"):
try:
item_clone = item.make_clone()
except IntegrityError:
item_clone = item.make_clone(sub_clone=True)
else:
item.pk = None
item_clone = item.save(using=using)
item = getattr(self, f.name)
if hasattr(item, "make_clone"):
try:
item_clone = item.make_clone(using=using)
except IntegrityError:
item_clone = item.make_clone(sub_clone=True)
else:
item.pk = None # pragma: no cover
item_clone = item.save(using=using) # pragma: no cover

setattr(duplicate, field.name, item_clone)
setattr(duplicate, f.name, item_clone)

return duplicate

Expand Down Expand Up @@ -523,10 +537,15 @@ def __duplicate_m2m_fields(self, duplicate, using=None):
for item in objs:
if hasattr(through, "make_clone"):
try:
item.make_clone(attrs={field_name: duplicate})
item.make_clone(
attrs={field_name: duplicate},
using=using,
)
except IntegrityError:
item.make_clone(
attrs={field_name: duplicate}, sub_clone=True
attrs={field_name: duplicate},
sub_clone=True,
using=using,
)
else:
item.pk = None
Expand Down
Loading

0 comments on commit 17a4d37

Please sign in to comment.