diff --git a/.commitlintrc.mjs b/.commitlintrc.mjs new file mode 100644 index 0000000..ae3ffa3 --- /dev/null +++ b/.commitlintrc.mjs @@ -0,0 +1,42 @@ +export default { + extends: [ + '@commitlint/config-conventional' + ], + ignores: [ + (message) => message.includes('Signed-off-by: dependabot[bot]') + ], + rules: { + 'header-max-length': [ + 2, + 'always', + 72 + ], + 'body-max-line-length': [ + 2, + 'always', + 72 + ], + 'body-leading-blank': [ + 2, + 'always' + ], + 'type-enum': [ + 2, + 'always', + [ + 'build', + 'chore', + 'ci', + 'deps', + 'docs', + 'feat', + 'fix', + 'perf', + 'refactor', + 'revert', + 'style', + 'test' + ] + ] + } +}; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06eb771..29e8d1f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,3 +45,20 @@ jobs: - name: Upload Coverage to Codecov if: ${{ matrix.python_version == env.DEFAULT_PYTHON }} uses: codecov/codecov-action@v4 + + commitlint: + name: Commitlint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check commitlint + uses: wagoid/commitlint-github-action@0d749a1a91d4770e983a7b8f83d4a3f0e7e0874e # v5.4.4 + + ruff: + name: Coding style - ruff + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 + - run: ruff check --fix + - run: ruff format diff --git a/.gitignore b/.gitignore index 8650d31..5cc98fa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ __pycache__ django_munigeo.egg-info/ /build /dist -.idea \ No newline at end of file +.idea diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7b97d2b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +default_language_version: + python: python3 +default_install_hook_types: [pre-commit, commit-msg] +default_stages: [pre-commit, manual] +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-added-large-files + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 # Sync with requirements.txt + hooks: + - id: ruff + name: ruff lint + - id: ruff-format + name: ruff format + args: [ --check ] + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.13.0 + hooks: + - id: commitlint + stages: [commit-msg, manual] + additional_dependencies: ["@commitlint/config-conventional"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 2241fac..75f4c1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pinned the `django-parler` version to `>=2` and add a migration required to upgrade it. ### Fixed -- Add a `tzinfo` to `Street` and `Address.modified_at` migrations to fix the warning +- Add a `tzinfo` to `Street` and `Address.modified_at` migrations to fix the warning saying that a timezone-naive date was passed to a `DateTimeField`. - helsinki importer: Reverted the change introduced in v0.3.6 which broke Helsinki division import - helsinki importer: Fixed empty field value handling diff --git a/README.md b/README.md index 81a24f7..1ae519b 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,18 @@ then ``` python manage.py geo_import helsinki --divisions ``` + +## Code format + +This project uses [Ruff](https://docs.astral.sh/ruff/) for code formatting and quality checking. + +Basic `ruff` commands: + +* lint: `ruff check` +* apply safe lint fixes: `ruff check --fix` +* check formatting: `ruff format --check` +* format: `ruff format` + +[`pre-commit`](https://pre-commit.com/) can be used to install and +run all the formatting tools as git hooks automatically before a +commit. diff --git a/munigeo/api.py b/munigeo/api.py index 7954aa4..e998c25 100644 --- a/munigeo/api.py +++ b/munigeo/api.py @@ -103,9 +103,8 @@ def translated_fields_to_representation(self, obj, ret): translated_fields = {} for lang_key, trans_dict in ret.pop("translations", {}).items(): - for field_name, translation in trans_dict.items(): - if not field_name in translated_fields: + if field_name not in translated_fields: translated_fields[field_name] = {lang_key: translation} else: translated_fields[field_name].update({lang_key: translation}) @@ -166,7 +165,6 @@ def geom_to_json(geom, target_srs): class GeoModelSerializer(serializers.ModelSerializer): - def __init__(self, *args, **kwargs): super(GeoModelSerializer, self).__init__(*args, **kwargs) model = self.Meta.model @@ -238,7 +236,7 @@ class AdministrativeDivisionSerializer( ): def to_representation(self, obj): ret = super(AdministrativeDivisionSerializer, self).to_representation(obj) - if not "request" in self.context: + if "request" not in self.context: return ret qparams = self.context["request"].query_params if qparams.get("geometry", "").lower() in ("true", "1"): diff --git a/munigeo/importer/athens.py b/munigeo/importer/athens.py index 24bf804..685266b 100644 --- a/munigeo/importer/athens.py +++ b/munigeo/importer/athens.py @@ -1,21 +1,12 @@ # -*- coding: utf-8 -*- -import csv -import io -import json import os # import unicodecsv -import requests import requests_cache -from django import db -from django.conf import settings -from django.contrib.gis.gdal import CoordTransform, DataSource, SpatialReference -from django.contrib.gis.geos import GEOSGeometry, MultiPolygon, Point +from django.contrib.gis.geos import Point -from munigeo import ocd from munigeo.importer.base import Importer, register_importer -from munigeo.importer.sync import ModelSyncher -from munigeo.models import * +from munigeo.models import PROJECTION_SRID, Municipality CITADEL_LIST = [ { @@ -85,7 +76,7 @@ def __init__(self, *args, **kwargs): self.muni_data_path = os.path.join(self.data_path, "gr", "athens") def import_municipalities(self): - muni, c = Municipality.objects.get_or_create(id=30001, name="Athens") + Municipality.objects.get_or_create(id=30001, name="Athens") self.logger.info("Athens municipality added.") def import_pois_from_citadel(self): diff --git a/munigeo/importer/base.py b/munigeo/importer/base.py index dc62892..b8b4013 100644 --- a/munigeo/importer/base.py +++ b/munigeo/importer/base.py @@ -4,12 +4,10 @@ import requests from django.conf import settings -from django.contrib.gis.gdal import CoordTransform, DataSource, SpatialReference -from django.contrib.gis.geos import GEOSGeometry, MultiPolygon, Point +from django.contrib.gis.geos import Point from django.utils.text import slugify -from munigeo.importer.sync import ModelSyncher -from munigeo.models import * +from munigeo.models import POI, PROJECTION_SRID, POICategory def convert_from_wgs84(coords): diff --git a/munigeo/importer/finland.py b/munigeo/importer/finland.py index de0230e..5cc07b3 100644 --- a/munigeo/importer/finland.py +++ b/munigeo/importer/finland.py @@ -16,11 +16,11 @@ from munigeo.importer.base import Importer, register_importer from munigeo.importer.sync import ModelSyncher from munigeo.models import ( + PROJECTION_SRID, AdministrativeDivision, AdministrativeDivisionGeometry, AdministrativeDivisionType, Municipality, - PROJECTION_SRID, ) from .helsinki import FIN_GRID, TM35_SRID @@ -176,7 +176,7 @@ def import_municipalities(self): self._process_muni(syncher, feat) if executor: for f in futures: - res = f.result() + _ = f.result() executor.shutdown() AdministrativeDivision.objects.rebuild() diff --git a/munigeo/importer/helsinki.py b/munigeo/importer/helsinki.py index b038ab0..409fdd3 100644 --- a/munigeo/importer/helsinki.py +++ b/munigeo/importer/helsinki.py @@ -18,7 +18,18 @@ from munigeo import ocd from munigeo.importer.base import Importer, register_importer from munigeo.importer.sync import ModelSyncher -from munigeo.models import * +from munigeo.models import ( + POI, + PROJECTION_SRID, + Address, + AdministrativeDivision, + AdministrativeDivisionGeometry, + AdministrativeDivisionType, + Municipality, + Plan, + POICategory, + Street, +) MUNI_URL = "https://tilastokeskus.fi/meta/luokitukset/kunta/001-2013/tekstitiedosto.txt" @@ -216,7 +227,7 @@ def make_div_id(obj): return obj.origin_id self.logger.info(div["name"]) - if not "origin_id" in div["fields"]: + if "origin_id" not in div["fields"]: raise Exception( "Field 'origin_id' not defined in config section '%s'" % div["name"] ) @@ -329,7 +340,7 @@ def import_plans(self): self._import_plans("Lv_rajaus.TAB", True) self._import_plans("Kaava_vir_rajaus.TAB", False) self.logger.info("Saving") - for key, obj in self.plan_map.items(): + for obj in self.plan_map.values(): if obj.found: obj.save() else: @@ -337,8 +348,6 @@ def import_plans(self): @db.transaction.atomic def import_addresses(self): - none_to_str = lambda s: s or "" - wfs_url = ( "https://kartta.hel.fi/ws/geoserver/avoindata/wfs?" "SERVICE=WFS&VERSION=1.0.0&REQUEST=GetFeature&" @@ -384,7 +393,6 @@ def make_addr_id(num, num_end, letter): street.addrs[make_addr_id(a.number, a.number_end, a.letter)] = a bulk_addr_list = [] - bulk_street_list = [] count = 0 self.logger.info("starting data synchronization") @@ -392,8 +400,8 @@ def make_addr_id(num, num_end, letter): count += 1 if count % 1000 == 0: self.logger.debug("{} processed".format(count)) - street_name = none_to_str(feat.get("katunimi")).strip() - street_name_sv = none_to_str(feat.get("gatan")).strip() + street_name = (feat.get("katunimi") or "").strip() + street_name_sv = (feat.get("gatan") or "").strip() num = feat.get("osoitenumero") @@ -413,10 +421,8 @@ def make_addr_id(num, num_end, letter): ) continue - num2 = none_to_str(feat.get("osoitenumero2")) - if num2 == 0: - num2 = "" - letter = none_to_str(feat.get("osoitekirjain")).strip() + num2 = feat.get("osoitenumero2") or "" + letter = (feat.get("osoitekirjain") or "").strip() coord_n = int(feat.get("n")) coord_e = int(feat.get("e")) @@ -475,8 +481,6 @@ def make_addr_id(num, num_end, letter): # addr.save() addr._found = True - # self.logger.info("%s: %s %d%s N%d E%d (%f,%f)" % (muni_name, street, num, letter, coord_n, coord_e, pnt.y, pnt.x)) - if len(bulk_addr_list) >= 10000: self.logger.info("Saving %d new addresses" % len(bulk_addr_list)) @@ -505,7 +509,7 @@ def make_addr_id(num, num_end, letter): self.logger.info("synchronization complete") def import_pois(self): - URL_BASE = "https://www.hel.fi/palvelukarttaws/rest/v2/unit/?service=%d" + url_base = "https://www.hel.fi/palvelukarttaws/rest/v2/unit/?service=%d" muni_dict = {} for muni in Municipality.objects.all(): @@ -518,7 +522,7 @@ def import_pois(self): ) self.logger.info("Importing %s" % cat_type) - ret = requests.get(URL_BASE % srv_id) + ret = requests.get(url_base % srv_id) for srv_info in ret.json(): srv_id = str(srv_info["id"]) try: @@ -527,12 +531,12 @@ def import_pois(self): poi = POI(origin_id=srv_id) poi.name = srv_info["name_fi"] poi.category = cat - if not "address_city_fi" in srv_info: + if "address_city_fi" not in srv_info: self.logger.info("No city!") self.logger.info(srv_info) continue city_name = srv_info["address_city_fi"] - if not city_name in muni_dict: + if city_name not in muni_dict: city_name = city_name.encode("utf8") post_code = srv_info.get("address_zip", "") if post_code.startswith("00"): @@ -564,7 +568,7 @@ def import_pois(self): poi.municipality = muni_dict[city_name] poi.street_address = srv_info.get("street_address_fi", None) poi.zip_code = srv_info.get("address_zip", None) - if not "northing_etrs_gk25" in srv_info: + if "northing_etrs_gk25" not in srv_info: self.logger.info("No location!") self.logger.info(srv_info) continue diff --git a/munigeo/importer/manchester.py b/munigeo/importer/manchester.py index d2a899e..d5ca731 100644 --- a/munigeo/importer/manchester.py +++ b/munigeo/importer/manchester.py @@ -1,21 +1,14 @@ # -*- coding: utf-8 -*- -import csv import io import json import os -# import unicodecsv import requests import requests_cache -from django import db -from django.conf import settings -from django.contrib.gis.gdal import CoordTransform, DataSource, SpatialReference -from django.contrib.gis.geos import GEOSGeometry, MultiPolygon, Point +from django.contrib.gis.geos import Point -from munigeo import ocd from munigeo.importer.base import Importer, register_importer -from munigeo.importer.sync import ModelSyncher -from munigeo.models import * +from munigeo.models import POI, PROJECTION_SRID, Municipality, POICategory POI_LIST = [ { @@ -91,6 +84,8 @@ def import_pois_from_csv(self): s = resp.text.encode("utf8").decode("utf8") f = io.StringIO(s) + import unicodecsv + reader = unicodecsv.reader(f, delimiter=",", quotechar='"', encoding="utf8") # skip header next(reader) @@ -117,7 +112,7 @@ def import_pois_from_csv(self): poi.save() def import_pois_from_rest(self): - URL_BASE = "http://www.manchester.gov.uk/site/custom_scripts/getServiceDetailsjs.php?service=%d&postcode=M2+5DB&count=10000&format=json" + url_base = "http://www.manchester.gov.uk/site/custom_scripts/getServiceDetailsjs.php?service=%d&postcode=M2+5DB&count=10000&format=json" muni = Municipality.objects.get(id=44001) for srv_id in list(SERVICE_CATEGORY_MAP.keys()): @@ -127,7 +122,7 @@ def import_pois_from_rest(self): ) self.logger.info("Importing %s" % cat_type) - ret = requests.get(URL_BASE % srv_id) + ret = requests.get(url_base % srv_id) if ret.status_code != 200: raise Exception("HTTP request failed with %d" % ret.status_code) # Fix quoting bug diff --git a/munigeo/management/commands/geo_import.py b/munigeo/management/commands/geo_import.py index 8c80e37..77bdc70 100644 --- a/munigeo/management/commands/geo_import.py +++ b/munigeo/management/commands/geo_import.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -import os -from optparse import make_option from django.conf import settings from django.core.management.base import BaseCommand, CommandError diff --git a/munigeo/migrations/0001_squashed_0004_building.py b/munigeo/migrations/0001_squashed_0004_building.py index 8b182ec..b4d5db8 100644 --- a/munigeo/migrations/0001_squashed_0004_building.py +++ b/munigeo/migrations/0001_squashed_0004_building.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.11 on 2018-07-11 06:15 import datetime + import django.contrib.gis.db.models.fields -from django.db import migrations, models import django.db.models.deletion import mptt.fields +from django.db import migrations, models from munigeo.utils import get_default_srid @@ -12,7 +13,6 @@ class Migration(migrations.Migration): - replaces = [ ("munigeo", "0001_initial"), ("munigeo", "0002_auto_20150608_1607"), diff --git a/munigeo/migrations/0002_add_parler_translations.py b/munigeo/migrations/0002_add_parler_translations.py index 819944d..e397502 100644 --- a/munigeo/migrations/0002_add_parler_translations.py +++ b/munigeo/migrations/0002_add_parler_translations.py @@ -1,11 +1,10 @@ # Generated by Django 2.0.5 on 2018-07-16 11:29 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("munigeo", "0001_squashed_0004_building"), ] diff --git a/munigeo/migrations/0003_migrate_translations_to_parler.py b/munigeo/migrations/0003_migrate_translations_to_parler.py index baf53f6..31b8f47 100644 --- a/munigeo/migrations/0003_migrate_translations_to_parler.py +++ b/munigeo/migrations/0003_migrate_translations_to_parler.py @@ -1,19 +1,19 @@ # Generated by Django 2.0.5 on 2018-07-16 13:04 -from django.db import migrations from django.conf import settings from django.core.exceptions import ObjectDoesNotExist +from django.db import migrations def _get_munigeo_models(apps): - AD = apps.get_model("munigeo", "AdministrativeDivision") - ADTranslation = apps.get_model("munigeo", "AdministrativeDivisionTranslation") + AD = apps.get_model("munigeo", "AdministrativeDivision") # noqa: N806 + ADTranslation = apps.get_model("munigeo", "AdministrativeDivisionTranslation") # noqa: N806 - Municipality = apps.get_model("munigeo", "Municipality") - MunicipalityTranslation = apps.get_model("munigeo", "MunicipalityTranslation") + Municipality = apps.get_model("munigeo", "Municipality") # noqa: N806 + MunicipalityTranslation = apps.get_model("munigeo", "MunicipalityTranslation") # noqa: N806 - Street = apps.get_model("munigeo", "Street") - StreetTranslation = apps.get_model("munigeo", "StreetTranslation") + Street = apps.get_model("munigeo", "Street") # noqa: N806 + StreetTranslation = apps.get_model("munigeo", "StreetTranslation") # noqa: N806 return [ (AD, ADTranslation), @@ -23,11 +23,10 @@ def _get_munigeo_models(apps): def forwards_func(apps, schema_editor): - for lang_code, _ in settings.LANGUAGES: name_field_key = "name_" + lang_code - for Model, ModelTranslation in _get_munigeo_models(apps): + for Model, ModelTranslation in _get_munigeo_models(apps): # noqa: N806 for object in Model.objects.all(): translated_name = getattr(object, name_field_key) if translated_name: @@ -39,11 +38,10 @@ def forwards_func(apps, schema_editor): def backwards_func(apps, schema_editor): - for lang_code, _ in settings.LANGUAGES: name_field_key = "name_" + lang_code - for Model, ModelTranslation in _get_munigeo_models(apps): + for Model, ModelTranslation in _get_munigeo_models(apps): # noqa: N806 for object in Model.objects.all(): try: translation = ModelTranslation.objects.get( @@ -56,7 +54,6 @@ def backwards_func(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ("munigeo", "0002_add_parler_translations"), ] diff --git a/munigeo/migrations/0004_delete_old_translations.py b/munigeo/migrations/0004_delete_old_translations.py index 7c1c1a2..c71c4fc 100644 --- a/munigeo/migrations/0004_delete_old_translations.py +++ b/munigeo/migrations/0004_delete_old_translations.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("munigeo", "0003_migrate_translations_to_parler"), ] diff --git a/munigeo/migrations/0005_update_translation_foreign_keys.py b/munigeo/migrations/0005_update_translation_foreign_keys.py index 857d624..0eb1ad1 100644 --- a/munigeo/migrations/0005_update_translation_foreign_keys.py +++ b/munigeo/migrations/0005_update_translation_foreign_keys.py @@ -1,12 +1,11 @@ # Generated by Django 3.0.8 on 2020-08-03 06:26 -from django.db import migrations, models import django.db.models.deletion import parler.fields +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("munigeo", "0004_delete_old_translations"), ] diff --git a/munigeo/models.py b/munigeo/models.py index 2ea76c9..de204e7 100644 --- a/munigeo/models.py +++ b/munigeo/models.py @@ -32,7 +32,6 @@ def __str__(self): class AdministrativeDivisionQuerySet(TranslatableQuerySet): - def by_ancestor(self, ancestor): manager = self.model.objects max_level = manager.determine_max_level() diff --git a/munigeo/ocd.py b/munigeo/ocd.py index 3e8b50f..e6debbe 100644 --- a/munigeo/ocd.py +++ b/munigeo/ocd.py @@ -10,11 +10,11 @@ def make_id(parent=None, **kwargs): if len(kwargs) > 1: raise ValueError("only one kwarg is allowed for make_id") type, type_id = list(kwargs.items())[0] - if not re.match("^[\w_-]+$", type, re.UNICODE): - raise ValueError("type must match [\w_]+ [%s]" % type) + if not re.match(r"^[\w_-]+$", type): + raise ValueError(r"type must match [\w_]+ [%s]" % type) type_id = type_id.lower() - type_id = re.sub("\.? ", "_", type_id) - type_id = re.sub("[^\w0-9~_.-]", "~", type_id, re.UNICODE) + type_id = re.sub(r"\.? ", "_", type_id) + type_id = re.sub(r"[^\w0-9~_.-]", "~", type_id) if parent: return "{parent}/{type}:{type_id}".format( parent=parent, type=type, type_id=type_id diff --git a/munigeo/oldapi.py b/munigeo/oldapi.py index 8f79482..4519ece 100644 --- a/munigeo/oldapi.py +++ b/munigeo/oldapi.py @@ -1,10 +1,9 @@ import json import re +from django.conf import settings from django.contrib.gis.gdal import CoordTransform, SpatialReference, SRSException from django.contrib.gis.geos import Point, Polygon -from django.contrib.gis.measure import D -from django.db.models import Q from modeltranslation.translator import NotRegistered, translator from tastypie import fields from tastypie.cache import SimpleCache @@ -13,7 +12,16 @@ from tastypie.http import HttpBadRequest from tastypie.resources import ModelResource -from munigeo.models import * +from munigeo.models import ( + POI, + PROJECTION_SRID, + Address, + AdministrativeDivision, + AdministrativeDivisionType, + Municipality, + Plan, + POICategory, +) # Use the GPS coordinate system by default DEFAULT_SRID = 4326 @@ -92,13 +100,13 @@ def dehydrate(self, bundle): for lang in LANGUAGES[1:]: key = "%s_%s" % (field_name, lang) val = getattr(bundle.obj, key, None) - if val == None: + if val is None: continue d[lang] = val # If no text provided, leave the field as null - for key, val in d.items(): - if val != None: + for val in d.values(): + if val is not None: break else: d = None @@ -205,7 +213,6 @@ class Meta: "end": ALL, "name": ALL, "parent": ALL_WITH_RELATIONS, - "type": ALL_WITH_RELATIONS, } excludes = ["lft", "rght", "tree_id"] list_allowed_methods = ["get"] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3664016 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,29 @@ +[tool.ruff] +target-version = "py39" + +[tool.ruff.lint] +select = [ + # Pyflakes + "F", + # pycodestyle + "E", + "W", + # isort + "I", + # pep8-naming + "N", + # flake8-bugbear without opinionated rules + "B0", + # flake8-pie + "PIE", + # flake8-print + "T20", +] +extend-per-file-ignores = { "*/migrations/*" = ["E501"], "*/tests/*" = ["E501"] } + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "helusers.tests.settings" + +[tool.coverage.run] +source = ["helusers"] +omit = ["helusers/migrations/*", "*/tests/*"] diff --git a/requirements.txt b/requirements.txt index edc01f0..30eb950 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,14 @@ -black django>=3.2.16 django-mptt django-parler>=2 django-parler-rest djangorestframework -isort psycopg2 pytest pytest-cov pyyaml requests requests-cache +ruff==0.8.4 # Sync with .pre-commit-config.yaml six unicodecsv diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a50e8e6..0000000 --- a/setup.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[aliases] -test=pytest - -[isort] -profile = black -atomic = true -order_by_type = false -extend_skip_glob = *migrations*